This is an automated email from the ASF dual-hosted git repository.

XiaoHongbo-Hope pushed a commit to branch release-2.1
in repository https://gitbox.apache.org/repos/asf/paimon.git

commit a5f3dd4b99a48be16fa49dcac76a78e7c983c570
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Sep 15 22:04:56 2026 +0800

    [rest] Add semantic view definition management to REST Catalog (#9848)
---
 docs/docs/concepts/rest/management-api.md          |   4 +
 docs/docs/concepts/rest/rest-api.md                |   6 +
 docs/docs/concepts/rest/semantic-views.md          | 168 +++++++++
 docs/docs/program-api/rest-api.mdx                 |   8 +
 docs/scripts/validate-rest-openapi.js              |  67 ++++
 docs/static/rest-catalog-open-api.yaml             | 269 ++++++++++++-
 .../paimon/management/SemanticViewManagement.java  |  58 +++
 .../main/java/org/apache/paimon/rest/RESTApi.java  |  65 ++++
 .../paimon/rest/RESTSemanticViewManagement.java    |  66 ++++
 .../java/org/apache/paimon/rest/ResourcePaths.java |  22 +-
 .../rest/requests/UpsertSemanticViewRequest.java   |  51 +++
 .../paimon/rest/responses/ErrorResponse.java       |   1 +
 .../rest/responses/GetSemanticViewResponse.java    |  66 ++++
 .../rest/responses/ListSemanticViewsResponse.java  |  75 ++++
 .../java/org/apache/paimon/view/SemanticView.java  |  42 +++
 .../apache/paimon/view/SemanticViewDefinition.java |  84 +++++
 .../paimon/rest/RESTApiSemanticViewTest.java       | 420 +++++++++++++++++++++
 .../requests/RequestJacksonCompatibilityTest.java  |   1 +
 .../java/org/apache/paimon/rest/RESTCatalog.java   |   7 +
 .../RESTCatalogSemanticViewManagementTest.java     | 156 ++++++++
 20 files changed, 1632 insertions(+), 4 deletions(-)

diff --git a/docs/docs/concepts/rest/management-api.md 
b/docs/docs/concepts/rest/management-api.md
index 08726c2f62..d604b800b6 100644
--- a/docs/docs/concepts/rest/management-api.md
+++ b/docs/docs/concepts/rest/management-api.md
@@ -29,6 +29,10 @@ version is `1.0` and may evolve incompatibly while the 
design is being validated
 directly. These methods are intentionally not part of the generic `Catalog` 
interface. Other
 catalog implementations do not expose this management contract.
 
+`RESTCatalog.semanticViewManagement()` provides [Semantic View definition 
management](semantic-views.md).
+Its object CRUD routes are defined in the Catalog OpenAPI; permissions and 
labels use this
+management contract.
+
 `LabelManagement` provides generic label operations backed by the catalog's 
existing `RESTApi`
 client, authentication, and prefix configuration. The REST server must 
implement entity resolution,
 authorization, and atomic label storage.
diff --git a/docs/docs/concepts/rest/rest-api.md 
b/docs/docs/concepts/rest/rest-api.md
index 7be2bb8caf..18e54faea4 100644
--- a/docs/docs/concepts/rest/rest-api.md
+++ b/docs/docs/concepts/rest/rest-api.md
@@ -73,3 +73,9 @@ dropped, no data is deleted, and the request needs 
`replaceStatistics=true` with
 `partitionStatistics` entry for the same spec. Any other path under the table 
location stays
 invalid, so a server that does not implement this rejects the request rather 
than storing it. A
 server also rejects additive statistics for a partition that already has a 
custom location.
+
+## Semantic views
+
+See [Semantic Views](semantic-views.md) for experimental definition 
management, complete-document
+POST upserts, and pagination. These use dedicated catalog routes and preserve 
the
+existing SQL View contract.
diff --git a/docs/docs/concepts/rest/semantic-views.md 
b/docs/docs/concepts/rest/semantic-views.md
new file mode 100644
index 0000000000..bc3469dd5a
--- /dev/null
+++ b/docs/docs/concepts/rest/semantic-views.md
@@ -0,0 +1,168 @@
+---
+title: "Semantic Views"
+---
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Semantic Views
+
+A semantic view is a named catalog object that stores a complete semantic 
model definition.
+The model describes datasets, relationships, dimensions, measures, and related 
business semantics.
+This experimental REST Catalog extension manages the object and its 
definition. The Java client
+preserves the original document text. Servers validate the model formats, 
syntax versions,
+and features they support; consumers provide query execution.
+
+Registration does not imply Spark/Flink execution support or grant access to 
source data.
+This implementation supplies Java clients, wire models, and the
+[Catalog OpenAPI contract](/rest-catalog-open-api.yaml). REST server providers 
must implement
+persistence, validation, authorization, and the atomic operations described 
below.
+
+## REST operations
+
+```text
+GET    /v1/{prefix}/databases/{database}/semantic-views
+GET    /v1/{prefix}/databases/{database}/semantic-views/{semanticView}
+POST   /v1/{prefix}/databases/{database}/semantic-views/{semanticView}
+DELETE /v1/{prefix}/databases/{database}/semantic-views/{semanticView}
+```
+
+Use the opaque prefix returned by config. Database and object names are 
independent UTF-8 encoded
+path segments, including slashes, percent signs, spaces, Unicode, and dot-only 
names. Names are
+never split on dots, and a catalog name inside model content does not 
determine the REST prefix.
+
+### Upsert and read
+
+POST creates or atomically replaces one complete definition in an existing 
database:
+
+```json
+{
+  "definition": {
+    "format": "databricks-yaml",
+    "content": "version: '1.1'\nsource: main.sales.orders\nmeasures:\n  - 
name: revenue\n    expr: SUM(paid_amount)\n"
+  }
+}
+```
+
+`format` and `content` are required nonblank strings. `format` identifies both 
the model syntax
+and document encoding, for example `databricks-yaml`, `snowflake-yaml`, or 
`ossie-yaml`. These are
+Paimon format identifiers, not a closed enumeration; each server decides which 
formats it supports.
+The example requires server support for `databricks-yaml`. Model syntax 
versions and expression
+SQL dialects belong inside the content, following the chosen format's 
specification.
+Clients do not parse, normalize, or discard fields in the content.
+Content is limited to **1 MiB of UTF-8 bytes**, checked before sending by the 
Java client.
+
+POST and GET return HTTP 200 with the full object:
+
+```json
+{
+  "name": "order_metrics",
+  "definition": {
+    "format": "databricks-yaml",
+    "content": "version: '1.1'\nsource: main.sales.orders\nmeasures:\n  - 
name: revenue\n    expr: SUM(paid_amount)\n"
+  }
+}
+```
+
+`name` is the local object name within the database addressed by the path. 
POST returns the
+committed object, and subsequent reads must expose the complete committed 
definition.
+
+Upserts are unconditional: the last successful write takes effect. Each update 
replaces the
+whole definition, so concurrent edits can overwrite one another. Replacement 
preserves identity,
+creation metadata, owner, permissions, and labels. Definition-level comments 
and synonyms are
+part of the complete replacement.
+
+### List and delete
+
+List accepts optional `maxResults` (1–1000; omitted uses the server default) 
and opaque `pageToken`:
+
+```json
+{"semanticViews":["order_metrics"],"nextPageToken":"opaque-token"}
+```
+
+Lists return visible names only; GET retrieves a full definition. The last 
page omits the token.
+An empty page terminates pagination and must not carry a continuation token. A 
missing database
+returns 404; an existing database with no visible models returns an empty 
array.
+
+DELETE uses the object path with no query parameters or request body. It 
returns 200 without a
+body; an absent object returns 404, and a blocking dependency returns 409.
+
+## Java catalog access
+
+```java
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.management.SemanticViewManagement;
+import org.apache.paimon.view.SemanticView;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+SemanticViewManagement models = restCatalog.semanticViewManagement();
+Identifier id = Identifier.create("sales", "order_metrics");
+SemanticViewDefinition definition = new 
SemanticViewDefinition("databricks-yaml", yamlText);
+SemanticView saved = models.upsertSemanticView(id, definition);
+SemanticView current = models.getSemanticView(id);
+models.upsertSemanticView(id, replacementDefinition);
+models.listSemanticViews("sales"); // Follows all pages.
+models.listSemanticViewsPaged("sales", 100, null);
+models.deleteSemanticView(id);
+```
+
+The accessor reuses the catalog's REST client, prefix, authentication, and 
configured headers.
+It is specific to `RESTCatalog`, not the generic `Catalog` interface. 
`RESTApi` also exposes the same
+upsert/get/list/paged-list/delete operations; its read and upsert methods 
return `GetSemanticViewResponse`.
+`SemanticView` does not implement ordinary `View.query()` or `View.rowType()`.
+
+## Server integration contract
+
+- Ordinary SQL views and semantic views share the database's view namespace. 
Both creation paths
+  must reject a cross-type name collision with 409. They cannot implicitly 
convert each other.
+- Existing `/views`, `/view-details`, and global view lists return ordinary 
SQL views only.
+  Existing get/alter/drop SQL view routes treat a semantic view name as a 
missing ordinary view (404).
+  Existing table/function naming rules continue to apply.
+- Reuse permission `ResourceType.VIEW` and structured database/view 
identities. Creation checks
+  the parent database's `CREATEVIEW`; replacement checks `ALTER`; deletion 
checks `DROP`; reads
+  check `SELECT`. Discovery follows database `LIST` and object visibility 
rules. Server resource
+  resolution must understand the semantic subtype. Source access is checked 
separately at execution.
+- Labels use `entityType=VIEW` and a canonical `entityName` supplied according 
to the provider's
+  naming and escaping rules; the semantic view response does not supply that 
label identity.
+  Clients must not assume that concatenating database and view names with a 
dot produces it.
+  The label resolver must support semantic views. Definition replacement 
preserves bindings;
+  deletion cleans up direct labels and permissions without deleting referenced 
sources.
+- Semantic views count when determining whether a database is empty. Database 
cascade deletion
+  follows existing catalog rules and cleans up semantic metadata and its 
direct bindings.
+- Unsupported model semantics must be rejected without side effects or 
dropping fields. SQL
+  analysis, source validation, dependencies, and execution authorization 
belong to the server's
+  model format adapter. The client exposes no generic executable-validation 
status.
+
+The HTTP client tests verify wire behavior and error propagation. They do not 
prove a provider's
+storage atomicity, permission enforcement, namespace isolation, or database 
lifecycle behavior.
+
+## Errors and scope
+
+Errors use `ErrorResponse`; semantic view errors use 
`resourceType=SEMANTIC_VIEW`.
+
+| HTTP | Meaning | Java client exception |
+| --- | --- | --- |
+| 400 | Invalid input or unsupported model format, syntax version, or feature 
| `BadRequestException` |
+| 401 / 403 | Authentication or permission failure | `NotAuthorizedException` 
/ `ForbiddenException` |
+| 404 | Missing database or model | `NoSuchResourceException` |
+| 409 | Name conflict or dependency blocks deletion | `AlreadyExistsException` 
(existing REST mapping) |
+| 413 | Content exceeds the size limit | `RESTException` |
+| 501 | Server does not implement semantic views | `NotImplementedException` |
+
+Client-side invalid input throws `IllegalArgumentException` before HTTP. An 
unsupported endpoint
+never falls back to SQL views, labels, or table options. Query compilation, 
measure rewriting,
+materialization, rename, and per-measure or per-field REST resources are 
outside this version.
diff --git a/docs/docs/program-api/rest-api.mdx 
b/docs/docs/program-api/rest-api.mdx
index 0a1d9e3686..ed19d4e380 100644
--- a/docs/docs/program-api/rest-api.mdx
+++ b/docs/docs/program-api/rest-api.mdx
@@ -141,3 +141,11 @@ To use the full table API, add the [Java 
bundle](java-api#dependency) and create
 `metastore=rest`, the server `uri`, `warehouse`, and the same authentication 
options. See
 [REST catalog configuration](../concepts/rest/) for an example. Loading table 
metadata and reading
 its data files are separate operations, so configure storage access as 
required by the catalog.
+
+## Semantic view definitions
+
+`RESTApi` exposes `upsertSemanticView`, `getSemanticView`, `listSemanticViews`,
+`listSemanticViewsPaged`, and `deleteSemanticView`. Upsert and get return 
`GetSemanticViewResponse`.
+Use `RESTCatalog.semanticViewManagement()` for the domain API backed by an 
existing catalog.
+See [Semantic Views](../concepts/rest/semantic-views.md) for Java examples, 
upsert behavior,
+server integration requirements, and the definition format.
diff --git a/docs/scripts/validate-rest-openapi.js 
b/docs/scripts/validate-rest-openapi.js
index 0d3c40155c..e51eb2295d 100644
--- a/docs/scripts/validate-rest-openapi.js
+++ b/docs/scripts/validate-rest-openapi.js
@@ -346,6 +346,73 @@ function validateCatalogOpenApi() {
   ['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse', 
'GetFunctionResponse'].forEach(
     (schemaName) => contract.requireTypedIntegerProperties(schemaName, 
['createdAt', 'updatedAt']),
   );
+  const semanticCollection = 
'/v1/{prefix}/databases/{database}/semantic-views';
+  const semanticItem = `${semanticCollection}/{semanticView}`;
+  [
+    [semanticCollection, 'get', 'listSemanticViews'],
+    [semanticItem, 'get', 'getSemanticView'],
+    [semanticItem, 'post', 'upsertSemanticView'],
+    [semanticItem, 'delete', 'deleteSemanticView'],
+  ].forEach(([resourcePath, method, operationId]) => {
+    contract.checkSpec(
+      contract.spec.paths[resourcePath]?.[method]?.operationId === operationId,
+      `${operationId} must use ${method.toUpperCase()} ${resourcePath}`,
+    );
+    contract.requireResponses(operationId, ['200', '400', '401', '403', '404', 
'500', '501']);
+  });
+  contract.checkSpec(!contract.spec.paths[semanticItem].patch, 'Semantic views 
use POST replacement');
+  contract.requireResponses('upsertSemanticView', ['409', '413']);
+  contract.requireResponses('deleteSemanticView', ['409']);
+  const semanticUpsert = contract.requireOperation('upsertSemanticView');
+  contract.checkSpec(
+    semanticUpsert.requestBody.required &&
+      semanticUpsert.requestBody.content['application/json'].schema.$ref ===
+        '#/components/schemas/UpsertSemanticViewRequest' &&
+      semanticUpsert.responses['200'].content['application/json'].schema.$ref 
===
+        '#/components/schemas/GetSemanticViewResponse',
+    'Semantic view upsert must take a complete definition and return the 
committed object',
+  );
+  const semanticDelete = contract.requireOperation('deleteSemanticView');
+  contract.checkSpec(
+    !semanticDelete.requestBody &&
+      !(semanticDelete.parameters || []).some((parameter) => parameter.in === 
'query'),
+    'Semantic view DELETE must use only the resource path without query 
parameters or a body',
+  );
+  const pageSize = 
contract.requireOperation('listSemanticViews').parameters.find(
+    (parameter) => parameter.name === 'maxResults',
+  );
+  contract.checkSpec(
+    pageSize.schema.minimum === 1 && pageSize.schema.maximum === 1000,
+    'Semantic view page size must be between 1 and 1000',
+  );
+  contract.requireRequiredProperties('SemanticViewDefinition', ['format', 
'content']);
+  const definition = contract.requireProperties('SemanticViewDefinition', 
['format', 'content']);
+  contract.checkSpec(
+    Object.keys(definition).length === 2 && !definition.format.enum &&
+      definition.content['x-max-utf8-bytes'] === 1048576,
+    'Semantic definitions require only an extensible format and content with a 
1 MiB UTF-8 limit',
+  );
+  contract.requireRequiredProperties('UpsertSemanticViewRequest', 
['definition']);
+  contract.requireRequiredProperties('GetSemanticViewResponse', ['name', 
'definition']);
+  contract.checkSpec(
+    Object.keys(contract.requireProperties('UpsertSemanticViewRequest', 
['definition'])).length === 1 &&
+      Object.keys(contract.requireProperties('GetSemanticViewResponse', 
['name', 'definition'])).length === 2,
+    'Semantic upserts contain only a definition; responses contain only a name 
and definition',
+  );
+  const semanticNames = 
contract.requireProperties('ListSemanticViewsResponse', ['semanticViews', 
'nextPageToken']);
+  contract.checkSpec(
+    semanticNames.semanticViews.type === 'array' && 
semanticNames.semanticViews.items.type === 'string',
+    'Semantic view lists must contain names only',
+  );
+  contract.checkSpec(
+    errorResourceTypes.includes('SEMANTIC_VIEW'),
+    'Semantic view errors must identify SEMANTIC_VIEW',
+  );
+  contract.checkSpec(
+    
contract.requireOperation('alterView').requestBody.content['application/json'].schema.$ref
 ===
+      '#/components/schemas/AlterViewRequest',
+    'Ordinary view POST must retain its existing alteration request',
+  );
   return contract.operations.size;
 }
 
diff --git a/docs/static/rest-catalog-open-api.yaml 
b/docs/static/rest-catalog-open-api.yaml
index 772555f067..02ee6df595 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -2225,11 +2225,231 @@ paths:
         "500":
           $ref: '#/components/responses/ServerErrorResponse'
 
+  /v1/{prefix}/databases/{database}/semantic-views:
+    parameters:
+      - name: prefix
+        in: path
+        required: true
+        description: Opaque catalog prefix returned by config.
+        schema:
+          type: string
+          minLength: 1
+      - name: database
+        in: path
+        required: true
+        description: Database name encoded as one UTF-8 path segment.
+        schema:
+          type: string
+          minLength: 1
+    get:
+      tags: [ semantic-view ]
+      summary: List Semantic Views
+      operationId: listSemanticViews
+      description: >-
+        Lists visible semantic view names only. Ordinary SQL view lists do not 
include these objects.
+        Missing databases return 404. Empty pages terminate pagination and 
omit nextPageToken.
+      parameters:
+        - name: maxResults
+          in: query
+          description: Maximum page size; omission uses the server default.
+          schema:
+            type: integer
+            format: int32
+            minimum: 1
+            maximum: 1000
+        - name: pageToken
+          in: query
+          description: Opaque continuation token from the preceding response.
+          schema:
+            type: string
+      responses:
+        "200":
+          description: Success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ListSemanticViewsResponse'
+        "400":
+          $ref: '#/components/responses/BadRequestErrorResponse'
+        "401":
+          $ref: '#/components/responses/UnauthorizedErrorResponse'
+        "403":
+          $ref: '#/components/responses/ForbiddenErrorResponse'
+        "404":
+          $ref: '#/components/responses/SemanticViewNotExistErrorResponse'
+        "500":
+          $ref: '#/components/responses/ServerErrorResponse'
+        "501":
+          $ref: 
'#/components/responses/SemanticViewNotImplementedErrorResponse'
+  /v1/{prefix}/databases/{database}/semantic-views/{semanticView}:
+    parameters:
+      - name: prefix
+        in: path
+        required: true
+        description: Opaque catalog prefix returned by config.
+        schema:
+          type: string
+          minLength: 1
+      - name: database
+        in: path
+        required: true
+        description: Database name encoded as one UTF-8 path segment.
+        schema:
+          type: string
+          minLength: 1
+      - name: semanticView
+        in: path
+        required: true
+        description: Semantic view name encoded as one UTF-8 path segment, 
including dot-only names.
+        schema:
+          type: string
+          minLength: 1
+    get:
+      tags: [ semantic-view ]
+      summary: Get Semantic View
+      operationId: getSemanticView
+      description: Returns the name and complete stored definition.
+      responses:
+        "200":
+          description: Success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GetSemanticViewResponse'
+        "400":
+          $ref: '#/components/responses/BadRequestErrorResponse'
+        "401":
+          $ref: '#/components/responses/UnauthorizedErrorResponse'
+        "403":
+          $ref: '#/components/responses/ForbiddenErrorResponse'
+        "404":
+          $ref: '#/components/responses/SemanticViewNotExistErrorResponse'
+        "500":
+          $ref: '#/components/responses/ServerErrorResponse'
+        "501":
+          $ref: 
'#/components/responses/SemanticViewNotImplementedErrorResponse'
+    post:
+      tags: [ semantic-view ]
+      summary: Upsert Semantic View
+      operationId: upsertSemanticView
+      description: >-
+        Creates or atomically replaces the complete definition in an existing 
database.
+        Replacements preserve identity, creation metadata, owner, permissions, 
and labels.
+        Upserts are unconditional; the last successful write takes effect.
+        Ordinary SQL views share the view namespace;
+        both SQL view creation and semantic view upsert reject cross-type name 
collisions with 409.
+        The server validates supported model formats, syntax versions, and 
model features,
+        rejecting unsupported semantics without side effects or silently 
dropping fields.
+        Registration does not imply query engine support or grant access to 
sources.
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/UpsertSemanticViewRequest'
+            example:
+              definition:
+                format: databricks-yaml
+                content: |
+                  version: "1.1"
+                  source: main.sales.orders
+                  measures:
+                    - name: revenue
+                      expr: SUM(paid_amount)
+      responses:
+        "200":
+          description: Success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GetSemanticViewResponse'
+        "400":
+          $ref: '#/components/responses/BadRequestErrorResponse'
+        "401":
+          $ref: '#/components/responses/UnauthorizedErrorResponse'
+        "403":
+          $ref: '#/components/responses/ForbiddenErrorResponse'
+        "404":
+          $ref: '#/components/responses/SemanticViewNotExistErrorResponse'
+        "409":
+          $ref: '#/components/responses/SemanticViewConflictErrorResponse'
+        "413":
+          $ref: '#/components/responses/SemanticViewTooLargeErrorResponse'
+        "500":
+          $ref: '#/components/responses/ServerErrorResponse'
+        "501":
+          $ref: 
'#/components/responses/SemanticViewNotImplementedErrorResponse'
+    delete:
+      tags: [ semantic-view ]
+      summary: Delete Semantic View
+      operationId: deleteSemanticView
+      description: >-
+        Deletes the object and its direct labels and permissions, never 
referenced sources.
+        Missing objects return 404. Dependencies may prevent deletion with 409.
+        No query parameters or request body are accepted.
+      responses:
+        "200":
+          description: Success, no content
+        "400":
+          $ref: '#/components/responses/BadRequestErrorResponse'
+        "401":
+          $ref: '#/components/responses/UnauthorizedErrorResponse'
+        "403":
+          $ref: '#/components/responses/ForbiddenErrorResponse'
+        "404":
+          $ref: '#/components/responses/SemanticViewNotExistErrorResponse'
+        "409":
+          $ref: '#/components/responses/SemanticViewConflictErrorResponse'
+        "500":
+          $ref: '#/components/responses/ServerErrorResponse'
+        "501":
+          $ref: 
'#/components/responses/SemanticViewNotImplementedErrorResponse'
+
 components:
   #############################
   # Reusable Response Objects #
   #############################
   responses:
+    SemanticViewNotExistErrorResponse:
+      description: Database or semantic view does not exist.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+          example:
+            message: Database or semantic view does not exist.
+            code: 404
+            resourceType: SEMANTIC_VIEW
+    SemanticViewConflictErrorResponse:
+      description: SQL view name collision or dependency prevents deletion.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+          example:
+            message: SQL view name collision or dependency prevents deletion.
+            code: 409
+            resourceType: SEMANTIC_VIEW
+    SemanticViewTooLargeErrorResponse:
+      description: Definition content exceeds 1 MiB in UTF-8 bytes; no content 
is stored or truncated.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+          example:
+            message: Definition content exceeds 1 MiB in UTF-8 bytes; no 
content is stored or truncated.
+            code: 413
+            resourceType: SEMANTIC_VIEW
+    SemanticViewNotImplementedErrorResponse:
+      description: The server does not implement semantic view management.
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/ErrorResponse'
+          example:
+            message: The server does not implement semantic view management.
+            code: 501
+            resourceType: SEMANTIC_VIEW
     BadRequestErrorResponse:
       description:
         Used for 400 errors, means illegal request.
@@ -2445,6 +2665,53 @@ components:
             message: Internal Server Error
             code: 500
   schemas:
+    SemanticViewDefinition:
+      type: object
+      required: [ format, content ]
+      properties:
+        format:
+          type: string
+          minLength: 1
+          description: >-
+            Nonblank, extensible model document format identifying both model 
syntax and encoding,
+            for example databricks-yaml, snowflake-yaml, or ossie-yaml. These 
are Paimon format
+            identifiers; servers decide which formats they support and reject 
unsupported ones.
+            Model syntax versions and expression dialects belong inside the 
content.
+          examples: [ databricks-yaml, snowflake-yaml, ossie-yaml ]
+        content:
+          type: string
+          minLength: 1
+          description: >-
+            Complete nonblank document, preserved verbatim, including future 
fields.
+            Maximum 1048576 UTF-8 bytes (not characters). Servers must never 
silently truncate.
+            A version inside the document identifies model syntax.
+          x-max-utf8-bytes: 1048576
+    UpsertSemanticViewRequest:
+      type: object
+      required: [ definition ]
+      properties:
+        definition:
+          $ref: '#/components/schemas/SemanticViewDefinition'
+    GetSemanticViewResponse:
+      type: object
+      required: [ name, definition ]
+      properties:
+        name:
+          type: string
+        definition:
+          $ref: '#/components/schemas/SemanticViewDefinition'
+    ListSemanticViewsResponse:
+      type: object
+      required: [ semanticViews ]
+      properties:
+        semanticViews:
+          type: array
+          items:
+            type: string
+          description: Visible semantic view names only, without full 
definitions.
+        nextPageToken:
+          type: string
+          description: Opaque continuation token; omitted on the last or an 
empty page.
     CreateDatabaseRequest:
       type: object
       properties:
@@ -2591,7 +2858,7 @@ components:
           type: string
         resourceType:
           type: [ string, "null" ]
-          enum: [ "DATABASE", "TABLE", "PARTITION", "COLUMN", "SNAPSHOT", 
"BRANCH", "TAG", "VIEW", "DIALECT", "FUNCTION", "DEFINITION", "UNKNOWN", null ]
+          enum: [ "DATABASE", "TABLE", "PARTITION", "COLUMN", "SNAPSHOT", 
"BRANCH", "TAG", "VIEW", "SEMANTIC_VIEW", "DIALECT", "FUNCTION", "DEFINITION", 
"UNKNOWN", null ]
         resourceName:
           type: [ string, "null" ]
         code:
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/management/SemanticViewManagement.java
 
b/paimon-api/src/main/java/org/apache/paimon/management/SemanticViewManagement.java
new file mode 100644
index 0000000000..07b7f60903
--- /dev/null
+++ 
b/paimon-api/src/main/java/org/apache/paimon/management/SemanticViewManagement.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.management;
+
+import org.apache.paimon.PagedList;
+import org.apache.paimon.annotation.Experimental;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.view.SemanticView;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+import javax.annotation.Nullable;
+
+import java.util.List;
+
+/** Definition management for semantic views. Registration does not imply 
query engine support. */
+@Experimental
+public interface SemanticViewManagement {
+
+    /**
+     * Creates or atomically replaces the complete definition, preserving 
identity and grants. The
+     * last successful write takes effect.
+     */
+    SemanticView upsertSemanticView(Identifier identifier, 
SemanticViewDefinition definition);
+
+    SemanticView getSemanticView(Identifier identifier);
+
+    /** Lists names only, following pagination. */
+    default List<String> listSemanticViews(String database) {
+        return PagedList.listAllFromPagedApi(
+                token -> listSemanticViewsPaged(database, null, token));
+    }
+
+    /** Lists names with a page size of 1 to 1000, or the server default when 
null. */
+    PagedList<String> listSemanticViewsPaged(
+            String database, @Nullable Integer maxResults, @Nullable String 
pageToken);
+
+    /**
+     * Deletes the object and its direct management bindings, never its 
sources. Missing objects
+     * fail with 404.
+     */
+    void deleteSemanticView(Identifier identifier);
+}
diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java 
b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
index 79c9fea3bf..8246cb1533 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/RESTApi.java
@@ -69,6 +69,7 @@ import 
org.apache.paimon.rest.requests.RevokePermissionRequest;
 import org.apache.paimon.rest.requests.RollbackSchemaRequest;
 import org.apache.paimon.rest.requests.RollbackTableRequest;
 import org.apache.paimon.rest.requests.UpsertLabelRequest;
+import org.apache.paimon.rest.requests.UpsertSemanticViewRequest;
 import org.apache.paimon.rest.responses.AlterDatabaseResponse;
 import org.apache.paimon.rest.responses.AuthTableQueryResponse;
 import org.apache.paimon.rest.responses.CommitTableResponse;
@@ -80,6 +81,7 @@ import org.apache.paimon.rest.responses.GetDatabaseResponse;
 import org.apache.paimon.rest.responses.GetFunctionResponse;
 import org.apache.paimon.rest.responses.GetLabelResponse;
 import org.apache.paimon.rest.responses.GetSchemaResponse;
+import org.apache.paimon.rest.responses.GetSemanticViewResponse;
 import org.apache.paimon.rest.responses.GetTableResponse;
 import org.apache.paimon.rest.responses.GetTableSnapshotResponse;
 import org.apache.paimon.rest.responses.GetTableTokenResponse;
@@ -97,6 +99,7 @@ import 
org.apache.paimon.rest.responses.ListPartitionsResponse;
 import org.apache.paimon.rest.responses.ListPermissionsResponse;
 import org.apache.paimon.rest.responses.ListPoliciesResponse;
 import org.apache.paimon.rest.responses.ListSchemasResponse;
+import org.apache.paimon.rest.responses.ListSemanticViewsResponse;
 import org.apache.paimon.rest.responses.ListSnapshotsResponse;
 import org.apache.paimon.rest.responses.ListTableDetailsResponse;
 import org.apache.paimon.rest.responses.ListTablesGloballyResponse;
@@ -114,6 +117,7 @@ import org.apache.paimon.table.TableSnapshot;
 import org.apache.paimon.utils.JsonSerdeUtil;
 import org.apache.paimon.utils.Pair;
 import org.apache.paimon.utils.StringUtils;
+import org.apache.paimon.view.SemanticViewDefinition;
 import org.apache.paimon.view.ViewChange;
 import org.apache.paimon.view.ViewSchema;
 
@@ -962,6 +966,67 @@ public class RESTApi {
         client.delete(resourcePaths.label(entityType, entityName, key), 
restAuthFunction);
     }
 
+    /**
+     * Creates or atomically replaces the complete semantic model definition. 
The last successful
+     * write takes effect. SQL view name conflicts return HTTP 409, mapped to
+     * AlreadyExistsException.
+     */
+    @Experimental
+    public GetSemanticViewResponse upsertSemanticView(
+            Identifier identifier, SemanticViewDefinition definition) {
+        checkArgument(identifier != null, "identifier must not be null");
+        return client.post(
+                resourcePaths.semanticView(
+                        identifier.getDatabaseName(), 
identifier.getObjectName()),
+                new UpsertSemanticViewRequest(definition),
+                GetSemanticViewResponse.class,
+                restAuthFunction);
+    }
+
+    /** Gets the complete model definition. Missing objects return HTTP 404. */
+    @Experimental
+    public GetSemanticViewResponse getSemanticView(Identifier identifier) {
+        checkArgument(identifier != null, "identifier must not be null");
+        return client.get(
+                resourcePaths.semanticView(
+                        identifier.getDatabaseName(), 
identifier.getObjectName()),
+                GetSemanticViewResponse.class,
+                restAuthFunction);
+    }
+
+    /** Lists semantic view names only, following catalog pagination. */
+    @Experimental
+    public List<String> listSemanticViews(String database) {
+        return PagedList.listAllFromPagedApi(
+                token -> listSemanticViewsPaged(database, null, token));
+    }
+
+    /** Lists names with a page size of 1 to 1000, or the server default when 
null. */
+    @Experimental
+    public PagedList<String> listSemanticViewsPaged(
+            String database, @Nullable Integer maxResults, @Nullable String 
pageToken) {
+        checkArgument(
+                maxResults == null || (maxResults >= 1 && maxResults <= 1000),
+                "maxResults must be between 1 and 1000");
+        ListSemanticViewsResponse response =
+                client.get(
+                        resourcePaths.semanticViews(database),
+                        buildPagedQueryParams(maxResults, pageToken),
+                        ListSemanticViewsResponse.class,
+                        restAuthFunction);
+        return new PagedList<>(response.getSemanticViews(), 
response.getNextPageToken());
+    }
+
+    /** Deletes a semantic view; an absent object returns HTTP 404. */
+    @Experimental
+    public void deleteSemanticView(Identifier identifier) {
+        checkArgument(identifier != null, "identifier must not be null");
+        client.delete(
+                resourcePaths.semanticView(
+                        identifier.getDatabaseName(), 
identifier.getObjectName()),
+                restAuthFunction);
+    }
+
     /** Lists permissions on an exact resource in the configured REST catalog. 
*/
     @Experimental
     public ListPermissionsResponse listPermissions(ListPermissionsRequest 
request) {
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/RESTSemanticViewManagement.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/RESTSemanticViewManagement.java
new file mode 100644
index 0000000000..095da3b1fe
--- /dev/null
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/RESTSemanticViewManagement.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest;
+
+import org.apache.paimon.PagedList;
+import org.apache.paimon.annotation.Experimental;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.management.SemanticViewManagement;
+import org.apache.paimon.rest.responses.GetSemanticViewResponse;
+import org.apache.paimon.view.SemanticView;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+import javax.annotation.Nullable;
+
+/** Semantic view management using the configured REST catalog prefix and 
authentication. */
+@Experimental
+public class RESTSemanticViewManagement implements SemanticViewManagement {
+
+    private final RESTApi api;
+
+    public RESTSemanticViewManagement(RESTApi api) {
+        this.api = api;
+    }
+
+    @Override
+    public SemanticView upsertSemanticView(
+            Identifier identifier, SemanticViewDefinition definition) {
+        return toSemanticView(api.upsertSemanticView(identifier, definition));
+    }
+
+    @Override
+    public SemanticView getSemanticView(Identifier identifier) {
+        return toSemanticView(api.getSemanticView(identifier));
+    }
+
+    @Override
+    public PagedList<String> listSemanticViewsPaged(
+            String database, @Nullable Integer maxResults, @Nullable String 
pageToken) {
+        return api.listSemanticViewsPaged(database, maxResults, pageToken);
+    }
+
+    @Override
+    public void deleteSemanticView(Identifier identifier) {
+        api.deleteSemanticView(identifier);
+    }
+
+    private static SemanticView toSemanticView(GetSemanticViewResponse 
response) {
+        return new SemanticView(response.getName(), response.getDefinition());
+    }
+}
diff --git a/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java 
b/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
index 92cd466a22..f3065708da 100644
--- a/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++ b/paimon-api/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -40,6 +40,7 @@ public class ResourcePaths {
     protected static final String CONSUMERS = "consumers";
     protected static final String SCHEMAS = "schemas";
     protected static final String VIEWS = "views";
+    protected static final String SEMANTIC_VIEWS = "semantic-views";
     protected static final String TABLE_DETAILS = "table-details";
     protected static final String VIEW_DETAILS = "view-details";
     protected static final String ROLLBACK = "rollback";
@@ -75,16 +76,16 @@ public class ResourcePaths {
         checkArgument(
                 entityName != null && !entityName.trim().isEmpty(), 
"entityName must not be blank");
         return SLASH.join(
-                V1, prefix, LABELS, encodeLabelSegment(entityType), 
encodeLabelSegment(entityName));
+                V1, prefix, LABELS, encodePathSegment(entityType), 
encodePathSegment(entityName));
     }
 
     @Experimental
     public String label(String entityType, String entityName, String key) {
         checkArgument(key != null && !key.trim().isEmpty(), "key must not be 
blank");
-        return SLASH.join(labels(entityType, entityName), 
encodeLabelSegment(key));
+        return SLASH.join(labels(entityType, entityName), 
encodePathSegment(key));
     }
 
-    private static String encodeLabelSegment(String value) {
+    private static String encodePathSegment(String value) {
         // Form encoding leaves dot segments unchanged, but they must be 
treated as names here.
         if (".".equals(value) || "..".equals(value)) {
             return value.replace(".", "%2E");
@@ -92,6 +93,21 @@ public class ResourcePaths {
         return encodeString(value);
     }
 
+    /** Semantic view names are encoded as independent path segments. */
+    @Experimental
+    public String semanticViews(String database) {
+        checkArgument(database != null && !database.trim().isEmpty(), 
"database must not be blank");
+        return SLASH.join(V1, prefix, DATABASES, encodePathSegment(database), 
SEMANTIC_VIEWS);
+    }
+
+    @Experimental
+    public String semanticView(String database, String semanticView) {
+        checkArgument(
+                semanticView != null && !semanticView.trim().isEmpty(),
+                "semanticView must not be blank");
+        return SLASH.join(semanticViews(database), 
encodePathSegment(semanticView));
+    }
+
     @Experimental
     public String permissions() {
         return SLASH.join(V1, prefix, PERMISSIONS);
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/requests/UpsertSemanticViewRequest.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/UpsertSemanticViewRequest.java
new file mode 100644
index 0000000000..b025489b19
--- /dev/null
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/requests/UpsertSemanticViewRequest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest.requests;
+
+import org.apache.paimon.annotation.Experimental;
+import org.apache.paimon.rest.RESTRequest;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.beans.ConstructorProperties;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** Unconditional replacement of the complete semantic view definition. */
+@Experimental
+public class UpsertSemanticViewRequest implements RESTRequest {
+
+    private final SemanticViewDefinition definition;
+
+    @JsonCreator
+    @ConstructorProperties({"definition"})
+    public UpsertSemanticViewRequest(
+            @JsonProperty("definition") SemanticViewDefinition definition) {
+        checkArgument(definition != null, "definition must not be null");
+        this.definition = definition;
+    }
+
+    @JsonGetter("definition")
+    public SemanticViewDefinition getDefinition() {
+        return definition;
+    }
+}
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java 
b/paimon-api/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
index 712a8cc110..007a18f68e 100644
--- 
a/paimon-api/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
@@ -50,6 +50,7 @@ public class ErrorResponse implements RESTResponse {
     public static final String RESOURCE_TYPE_LABEL = "LABEL";
 
     public static final String RESOURCE_TYPE_VIEW = "VIEW";
+    public static final String RESOURCE_TYPE_SEMANTIC_VIEW = "SEMANTIC_VIEW";
 
     public static final String RESOURCE_TYPE_DIALECT = "DIALECT";
 
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/responses/GetSemanticViewResponse.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/responses/GetSemanticViewResponse.java
new file mode 100644
index 0000000000..45b81288b2
--- /dev/null
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/responses/GetSemanticViewResponse.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest.responses;
+
+import org.apache.paimon.annotation.Experimental;
+import org.apache.paimon.rest.RESTResponse;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.beans.ConstructorProperties;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** A semantic view's name and complete committed definition. */
+@Experimental
+public class GetSemanticViewResponse implements RESTResponse {
+
+    private static final String FIELD_NAME = "name";
+    private static final String FIELD_DEFINITION = "definition";
+
+    @JsonProperty(FIELD_NAME)
+    private final String name;
+
+    @JsonProperty(FIELD_DEFINITION)
+    private final SemanticViewDefinition definition;
+
+    @JsonCreator
+    @ConstructorProperties({FIELD_NAME, FIELD_DEFINITION})
+    public GetSemanticViewResponse(
+            @JsonProperty(FIELD_NAME) String name,
+            @JsonProperty(FIELD_DEFINITION) SemanticViewDefinition definition) 
{
+        checkArgument(name != null && !name.trim().isEmpty(), "name must not 
be blank");
+        checkArgument(definition != null, "definition must not be null");
+        this.name = name;
+        this.definition = definition;
+    }
+
+    @JsonGetter(FIELD_NAME)
+    public String getName() {
+        return name;
+    }
+
+    @JsonGetter(FIELD_DEFINITION)
+    public SemanticViewDefinition getDefinition() {
+        return definition;
+    }
+}
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/rest/responses/ListSemanticViewsResponse.java
 
b/paimon-api/src/main/java/org/apache/paimon/rest/responses/ListSemanticViewsResponse.java
new file mode 100644
index 0000000000..66795a6d4e
--- /dev/null
+++ 
b/paimon-api/src/main/java/org/apache/paimon/rest/responses/ListSemanticViewsResponse.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest.responses;
+
+import org.apache.paimon.annotation.Experimental;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonInclude;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nullable;
+
+import java.beans.ConstructorProperties;
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+
+/** One page of semantic view names in a database. */
+@Experimental
+public class ListSemanticViewsResponse implements PagedResponse<String> {
+
+    private static final String FIELD_SEMANTIC_VIEWS = "semanticViews";
+    private static final String FIELD_NEXT_PAGE_TOKEN = "nextPageToken";
+
+    @JsonProperty(FIELD_SEMANTIC_VIEWS)
+    private final List<String> semanticViews;
+
+    @Nullable
+    @JsonProperty(FIELD_NEXT_PAGE_TOKEN)
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private final String nextPageToken;
+
+    @JsonCreator
+    @ConstructorProperties({FIELD_SEMANTIC_VIEWS, FIELD_NEXT_PAGE_TOKEN})
+    public ListSemanticViewsResponse(
+            @JsonProperty(FIELD_SEMANTIC_VIEWS) List<String> semanticViews,
+            @Nullable @JsonProperty(FIELD_NEXT_PAGE_TOKEN) String 
nextPageToken) {
+        this.semanticViews = semanticViews == null ? emptyList() : 
semanticViews;
+        this.nextPageToken = nextPageToken;
+    }
+
+    @JsonGetter(FIELD_SEMANTIC_VIEWS)
+    public List<String> getSemanticViews() {
+        return semanticViews;
+    }
+
+    @Override
+    @Nullable
+    @JsonGetter(FIELD_NEXT_PAGE_TOKEN)
+    public String getNextPageToken() {
+        return nextPageToken;
+    }
+
+    @Override
+    public List<String> data() {
+        return semanticViews;
+    }
+}
diff --git a/paimon-api/src/main/java/org/apache/paimon/view/SemanticView.java 
b/paimon-api/src/main/java/org/apache/paimon/view/SemanticView.java
new file mode 100644
index 0000000000..888bee241e
--- /dev/null
+++ b/paimon-api/src/main/java/org/apache/paimon/view/SemanticView.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.view;
+
+import org.apache.paimon.annotation.Experimental;
+
+/** A registered semantic model, independent of a SQL view's query and result 
schema. */
+@Experimental
+public class SemanticView {
+
+    private final String name;
+    private final SemanticViewDefinition definition;
+
+    public SemanticView(String name, SemanticViewDefinition definition) {
+        this.name = name;
+        this.definition = definition;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public SemanticViewDefinition getDefinition() {
+        return definition;
+    }
+}
diff --git 
a/paimon-api/src/main/java/org/apache/paimon/view/SemanticViewDefinition.java 
b/paimon-api/src/main/java/org/apache/paimon/view/SemanticViewDefinition.java
new file mode 100644
index 0000000000..d40099951d
--- /dev/null
+++ 
b/paimon-api/src/main/java/org/apache/paimon/view/SemanticViewDefinition.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.view;
+
+import org.apache.paimon.annotation.Experimental;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.beans.ConstructorProperties;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/** Complete semantic model text. Model format support is validated by the 
server. */
+@Experimental
+public class SemanticViewDefinition {
+
+    /** Maximum UTF-8 content size accepted by the semantic view REST 
contract. */
+    public static final int MAX_CONTENT_BYTES = 1024 * 1024;
+
+    private final String format;
+    private final String content;
+
+    @JsonCreator
+    @ConstructorProperties({"format", "content"})
+    public SemanticViewDefinition(
+            @JsonProperty("format") String format, @JsonProperty("content") 
String content) {
+        checkArgument(format != null && !format.trim().isEmpty(), "format must 
not be blank");
+        checkArgument(content != null && !content.trim().isEmpty(), "content 
must not be blank");
+        checkArgument(
+                content.getBytes(StandardCharsets.UTF_8).length <= 
MAX_CONTENT_BYTES,
+                "content must not exceed 1 MiB in UTF-8");
+        this.format = format;
+        this.content = content;
+    }
+
+    /** Identifies both model syntax and document encoding, for example {@code 
databricks-yaml}. */
+    @JsonGetter("format")
+    public String getFormat() {
+        return format;
+    }
+
+    /** Returns the original document without parsing or normalization. */
+    @JsonGetter("content")
+    public String getContent() {
+        return content;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof SemanticViewDefinition)) {
+            return false;
+        }
+        SemanticViewDefinition that = (SemanticViewDefinition) obj;
+        return format.equals(that.format) && content.equals(that.content);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(format, content);
+    }
+}
diff --git 
a/paimon-api/src/test/java/org/apache/paimon/rest/RESTApiSemanticViewTest.java 
b/paimon-api/src/test/java/org/apache/paimon/rest/RESTApiSemanticViewTest.java
new file mode 100644
index 0000000000..e753f9d066
--- /dev/null
+++ 
b/paimon-api/src/test/java/org/apache/paimon/rest/RESTApiSemanticViewTest.java
@@ -0,0 +1,420 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest;
+
+import org.apache.paimon.PagedList;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.rest.exceptions.AlreadyExistsException;
+import org.apache.paimon.rest.exceptions.ForbiddenException;
+import org.apache.paimon.rest.exceptions.NoSuchResourceException;
+import org.apache.paimon.rest.exceptions.NotImplementedException;
+import org.apache.paimon.rest.exceptions.RESTException;
+import org.apache.paimon.rest.requests.UpsertSemanticViewRequest;
+import org.apache.paimon.rest.responses.GetSemanticViewResponse;
+import org.apache.paimon.rest.responses.ListSemanticViewsResponse;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.Consumer;
+
+import static org.apache.paimon.rest.RESTCatalogInternalOptions.PREFIX;
+import static org.apache.paimon.rest.RESTCatalogOptions.TOKEN;
+import static org.apache.paimon.rest.RESTCatalogOptions.TOKEN_PROVIDER;
+import static org.apache.paimon.rest.RESTCatalogOptions.URI;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** HTTP contract tests for semantic view definition management. */
+class RESTApiSemanticViewTest {
+
+    private static final String BASE_PATH = 
"/v1/catalog%2Fid/databases/sales/semantic-views";
+    private static final Identifier IDENTIFIER = Identifier.create("sales", 
"revenue");
+    private static final SemanticViewDefinition DEFINITION =
+            new SemanticViewDefinition(
+                    "provider-v2-yaml",
+                    "# 指标\r\nversion: '1.1'\nsource: db.orders\nfuture: {expr: 
\"a + b\\c\"}\n");
+
+    private final Queue<Reply> replies = new ConcurrentLinkedQueue<>();
+    private final List<Request> requests = new CopyOnWriteArrayList<>();
+    private HttpServer server;
+    private RESTApi api;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+        server.createContext(
+                "/v1/",
+                exchange -> {
+                    requests.add(new Request(exchange));
+                    Reply reply = replies.poll();
+                    if (reply == null) {
+                        reply = new Reply(500, 
"{\"code\":500,\"message\":\"unexpected request\"}");
+                    }
+                    byte[] data = reply.body.getBytes(StandardCharsets.UTF_8);
+                    exchange.getResponseHeaders().set("Content-Type", 
"application/json");
+                    exchange.sendResponseHeaders(reply.code, data.length);
+                    try (OutputStream output = exchange.getResponseBody()) {
+                        output.write(data);
+                    } finally {
+                        exchange.close();
+                    }
+                });
+        server.start();
+
+        Options options = new Options();
+        options.set(URI, "http://127.0.0.1:"; + server.getAddress().getPort());
+        options.set(PREFIX, "catalog/id");
+        options.set(TOKEN_PROVIDER, "bear");
+        options.set(TOKEN, "test-token");
+        api = new RESTApi(options, false);
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (server != null) {
+            server.stop(0);
+        }
+    }
+
+    @Test
+    void testCompleteDefinitionUpsertAndReadPreserveText() throws Exception {
+        for (int i = 0; i < 2; i++) {
+            enqueue(200, responseJson());
+            GetSemanticViewResponse result = 
api.upsertSemanticView(IDENTIFIER, DEFINITION);
+            assertThat(result.getDefinition()).isEqualTo(DEFINITION);
+            assertThat(result.getName()).isEqualTo("revenue");
+        }
+        enqueue(200, responseJson());
+        
assertThat(api.getSemanticView(IDENTIFIER).getDefinition()).isEqualTo(DEFINITION);
+        assertThat(requests).hasSize(3);
+        for (int i = 0; i < 2; i++) {
+            Request request = requests.get(i);
+            assertThat(request.method).isEqualTo("POST");
+            assertThat(request.path).isEqualTo(BASE_PATH + "/revenue");
+            assertThat(request.query).isNull();
+            assertThat(request.authorization).isEqualTo("Bearer test-token");
+            Map<?, ?> body = RESTApi.fromJson(request.body, Map.class);
+            
assertThat(body.keySet()).extracting(Object::toString).containsExactly("definition");
+            Map<String, String> definition = (Map<String, String>) 
body.get("definition");
+            assertThat(definition)
+                    .containsOnlyKeys("format", "content")
+                    .containsEntry("format", "provider-v2-yaml")
+                    .containsEntry("content", DEFINITION.getContent());
+        }
+        assertThat(requests.get(2).method).isEqualTo("GET");
+    }
+
+    @Test
+    void testIndependentPathEncodingIncludingDotSegments() throws Exception {
+        Identifier id = Identifier.create("sales /华+%?#", "model.a/b+% ?#");
+        enqueue(200, responseJson());
+        api.upsertSemanticView(id, DEFINITION);
+        enqueue(200, responseJson());
+        api.getSemanticView(id);
+        enqueue(200, "");
+        api.deleteSemanticView(id);
+        String path =
+                
"/v1/catalog%2Fid/databases/sales+%2F%E5%8D%8E%2B%25%3F%23/semantic-views/model.a%2Fb%2B%25+%3F%23";
+        assertThat(requests).extracting(r -> r.path).containsExactly(path, 
path, path);
+        enqueue(200, responseJson());
+        api.getSemanticView(Identifier.create(".", ".."));
+        assertThat(requests.get(3).path)
+                
.isEqualTo("/v1/catalog%2Fid/databases/%2E/semantic-views/%2E%2E");
+    }
+
+    @Test
+    void testPagedAndAllNamesPreserveOpaqueTokens() throws Exception {
+        enqueue(200, "{\"semanticViews\":[\"one\"],\"nextPageToken\":\"next 
+/%?&\"}");
+        PagedList<String> page = api.listSemanticViewsPaged("sales", 1000, 
"start +/%");
+        assertThat(page.getElements()).containsExactly("one");
+        assertThat(page.getNextPageToken()).isEqualTo("next +/%?&");
+        assertThat(queryParameters(requests.get(0).query))
+                .containsEntry("maxResults", "1000")
+                .containsEntry("pageToken", "start +/%");
+        enqueue(200, "{\"semanticViews\":[\"one\"],\"nextPageToken\":\"next 
+/%?&\"}");
+        enqueue(200, "{\"semanticViews\":[\"two\"]}");
+        assertThat(api.listSemanticViews("sales")).containsExactly("one", 
"two");
+        assertThat(requests.get(1).query).isNull();
+        assertThat(queryParameters(requests.get(2).query))
+                .containsOnlyKeys("pageToken")
+                .containsEntry("pageToken", "next +/%?&");
+        assertThat(requests)
+                .allSatisfy(
+                        r -> {
+                            assertThat(r.method).isEqualTo("GET");
+                            assertThat(r.path).isEqualTo(BASE_PATH);
+                        });
+        // Even a malformed empty page with a continuation token must not keep 
fetching.
+        enqueue(200, "{\"semanticViews\":[],\"nextPageToken\":\"unused\"}");
+        assertThat(api.listSemanticViews("sales")).isEmpty();
+        assertThat(requests).hasSize(4);
+    }
+
+    @Test
+    void testDeleteUsesIdentityWithoutBodyOrQuery() {
+        enqueue(200, "");
+        api.deleteSemanticView(IDENTIFIER);
+        assertThat(requests).hasSize(1);
+        Request request = requests.get(0);
+        assertThat(request.method).isEqualTo("DELETE");
+        assertThat(request.path).isEqualTo(BASE_PATH + "/revenue");
+        assertThat(request.query).isNull();
+        assertThat(request.body).isEmpty();
+        assertThat(request.authorization).isEqualTo("Bearer test-token");
+    }
+
+    @Test
+    void testMissingObjectsPropagateWithoutFallback() {
+        List<Consumer<RESTApi>> operations =
+                Arrays.asList(
+                        client -> client.upsertSemanticView(IDENTIFIER, 
DEFINITION),
+                        client -> client.getSemanticView(IDENTIFIER),
+                        client -> client.listSemanticViews("sales"),
+                        client -> client.listSemanticViewsPaged("sales", 1, 
null),
+                        client -> client.deleteSemanticView(IDENTIFIER));
+        for (Consumer<RESTApi> operation : operations) {
+            enqueue(
+                    404,
+                    
"{\"code\":404,\"resourceType\":\"SEMANTIC_VIEW\",\"message\":\"missing 
model\"}");
+            assertThatThrownBy(() -> operation.accept(api))
+                    .isInstanceOf(NoSuchResourceException.class)
+                    .hasMessageContaining("missing model");
+        }
+        assertThat(requests).hasSize(operations.size());
+    }
+
+    @Test
+    void testNameAndDependencyConflictsHaveNoFallback() {
+        enqueue(409, "{\"code\":409,\"message\":\"SQL view already exists\"}");
+        assertThatThrownBy(() -> api.upsertSemanticView(IDENTIFIER, 
DEFINITION))
+                .isInstanceOf(AlreadyExistsException.class)
+                .hasMessageContaining("SQL view already exists");
+        enqueue(409, "{\"code\":409,\"message\":\"dependent objects\"}");
+        assertThatThrownBy(() -> api.deleteSemanticView(IDENTIFIER))
+                .isInstanceOf(AlreadyExistsException.class)
+                .hasMessageContaining("dependent objects");
+        assertThat(requests).extracting(r -> r.method).containsExactly("POST", 
"DELETE");
+        assertThat(requests).allSatisfy(r -> 
assertThat(r.path).isEqualTo(BASE_PATH + "/revenue"));
+    }
+
+    @Test
+    void testForbiddenUnsupportedAndOversizedResponsesHaveNoFallback() {
+        enqueue(403, "{\"code\":403,\"message\":\"denied\"}");
+        assertThatThrownBy(() -> api.upsertSemanticView(IDENTIFIER, 
DEFINITION))
+                .isInstanceOf(ForbiddenException.class);
+        enqueue(501, "{\"code\":501,\"message\":\"unsupported\"}");
+        assertThatThrownBy(() -> api.getSemanticView(IDENTIFIER))
+                .isInstanceOf(NotImplementedException.class);
+        enqueue(413, "{\"code\":413,\"message\":\"too large\"}");
+        assertThatThrownBy(() -> api.upsertSemanticView(IDENTIFIER, 
DEFINITION))
+                .isInstanceOf(RESTException.class)
+                .hasMessageContaining("too large");
+        assertThat(requests).hasSize(3);
+    }
+
+    @Test
+    void testInvalidInputsAndUtf8SizeBoundaryBeforeHttp() {
+        for (String invalid : Arrays.asList(null, "", "  ")) {
+            assertThatThrownBy(() -> new SemanticViewDefinition(invalid, 
"content"))
+                    .isInstanceOf(IllegalArgumentException.class);
+            assertThatThrownBy(() -> new 
SemanticViewDefinition("provider-yaml", invalid))
+                    .isInstanceOf(IllegalArgumentException.class);
+            assertThatThrownBy(() -> api.listSemanticViews(invalid))
+                    .isInstanceOf(IllegalArgumentException.class);
+        }
+        for (String invalid : Arrays.asList("", " ")) {
+            assertThatThrownBy(() -> 
api.getSemanticView(Identifier.create("sales", invalid)))
+                    .isInstanceOf(IllegalArgumentException.class);
+        }
+        assertThatThrownBy(() -> api.getSemanticView(null))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> api.upsertSemanticView(null, DEFINITION))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> api.deleteSemanticView(null))
+                .isInstanceOf(IllegalArgumentException.class);
+        assertThatThrownBy(() -> api.upsertSemanticView(IDENTIFIER, null))
+                .isInstanceOf(IllegalArgumentException.class);
+        for (int invalid : new int[] {-1, 0, 1001}) {
+            assertThatThrownBy(() -> api.listSemanticViewsPaged("sales", 
invalid, null))
+                    .isInstanceOf(IllegalArgumentException.class);
+        }
+        char[] chars = new char[SemanticViewDefinition.MAX_CONTENT_BYTES / 3];
+        Arrays.fill(chars, '华');
+        String limit = new String(chars) + "x";
+        assertThat(limit.getBytes(StandardCharsets.UTF_8))
+                .hasSize(SemanticViewDefinition.MAX_CONTENT_BYTES);
+        assertThat(new SemanticViewDefinition("provider-yaml", 
limit).getContent())
+                .isEqualTo(limit);
+        assertThatThrownBy(() -> new SemanticViewDefinition("provider-yaml", 
limit + "x"))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("1 MiB");
+        assertThat(requests).isEmpty();
+    }
+
+    @Test
+    void testIncompleteResponsesAreRejected() throws Exception {
+        for (String field : Arrays.asList("name", "definition")) {
+            Map<String, Object> response = RESTApi.fromJson(responseJson(), 
Map.class);
+            response.remove(field);
+            enqueue(200, RESTApi.toJson(response));
+            assertThatThrownBy(() -> api.getSemanticView(IDENTIFIER))
+                    .isInstanceOf(RESTException.class);
+        }
+        assertThat(requests).hasSize(2);
+    }
+
+    @Test
+    void testModelFormatsUseTwoFieldWireDefinitions() throws Exception {
+        for (String format :
+                Arrays.asList("databricks-yaml", "snowflake-yaml", 
"ossie-yaml", "provider-json")) {
+            // The REST layer transports provider-defined text without 
interpreting its grammar.
+            SemanticViewDefinition definition =
+                    new SemanticViewDefinition(format, 
DEFINITION.getContent());
+            enqueue(200, RESTApi.toJson(new GetSemanticViewResponse("revenue", 
definition)));
+            assertThat(api.upsertSemanticView(IDENTIFIER, 
definition).getDefinition())
+                    .isEqualTo(definition);
+            String body = requests.get(requests.size() - 1).body;
+            Map<?, ?> request = RESTApi.fromJson(body, Map.class);
+            Map<String, String> fields = (Map<String, String>) 
request.get("definition");
+            assertThat(fields)
+                    .containsOnlyKeys("format", "content")
+                    .containsEntry("format", format)
+                    .containsEntry("content", DEFINITION.getContent());
+            UpsertSemanticViewRequest external =
+                    new com.fasterxml.jackson.databind.ObjectMapper()
+                            .readValue(body, UpsertSemanticViewRequest.class);
+            assertThat(external.getDefinition()).isEqualTo(definition);
+        }
+        assertThat(requests).hasSize(4);
+    }
+
+    @Test
+    void testNestedJacksonCompatibilityAndRequiredFields() throws Exception {
+        String json = RESTApi.toJson(new 
UpsertSemanticViewRequest(DEFINITION));
+        UpsertSemanticViewRequest external =
+                new com.fasterxml.jackson.databind.ObjectMapper()
+                        .readValue(json, UpsertSemanticViewRequest.class);
+        assertThat(external.getDefinition()).isEqualTo(DEFINITION);
+        assertThat(
+                        RESTApi.fromJson(RESTApi.toJson(external), 
UpsertSemanticViewRequest.class)
+                                .getDefinition())
+                .isEqualTo(DEFINITION);
+        assertThat(
+                        RESTApi.fromJson(
+                                RESTApi.toJson(new 
UpsertSemanticViewRequest(DEFINITION)),
+                                Map.class))
+                .containsOnlyKeys("definition");
+        for (String invalid :
+                Arrays.asList(
+                        "{}",
+                        "{\"definition\":null}",
+                        "{\"definition\":{}}",
+                        "{\"definition\":{\"format\":\"databricks-yaml\"}}",
+                        "{\"definition\":{\"content\":\"source: orders\"}}")) {
+            assertThatThrownBy(() -> RESTApi.fromJson(invalid, 
UpsertSemanticViewRequest.class))
+                    .hasRootCauseInstanceOf(IllegalArgumentException.class);
+        }
+        GetSemanticViewResponse response =
+                RESTApi.fromJson(
+                        responseJson().replace("\"name\":", 
"\"future\":true,\"name\":"),
+                        GetSemanticViewResponse.class);
+        assertThat(response.getDefinition()).isEqualTo(DEFINITION);
+        assertThat(RESTApi.fromJson(RESTApi.toJson(response), Map.class))
+                .containsOnlyKeys("name", "definition");
+        assertThat(
+                        new com.fasterxml.jackson.databind.ObjectMapper()
+                                .readValue(responseJson(), 
GetSemanticViewResponse.class)
+                                .getDefinition())
+                .isEqualTo(DEFINITION);
+        ListSemanticViewsResponse empty =
+                RESTApi.fromJson("{\"future\":true}", 
ListSemanticViewsResponse.class);
+        assertThat(empty.getSemanticViews()).isEmpty();
+        assertThat(empty.getNextPageToken()).isNull();
+        assertThat(RESTApi.fromJson(RESTApi.toJson(empty), Map.class))
+                .containsOnlyKeys("semanticViews");
+    }
+
+    private static String responseJson() throws Exception {
+        return RESTApi.toJson(new GetSemanticViewResponse("revenue", 
DEFINITION));
+    }
+
+    private void enqueue(int code, String body) {
+        replies.add(new Reply(code, body));
+    }
+
+    private static Map<String, String> queryParameters(String query) throws 
Exception {
+        Map<String, String> values = new HashMap<>();
+        for (String parameter : query.split("&")) {
+            String[] pair = parameter.split("=", 2);
+            values.put(URLDecoder.decode(pair[0], "UTF-8"), 
URLDecoder.decode(pair[1], "UTF-8"));
+        }
+        return values;
+    }
+
+    private static class Reply {
+        private final int code;
+        private final String body;
+
+        private Reply(int code, String body) {
+            this.code = code;
+            this.body = body;
+        }
+    }
+
+    private static class Request {
+        private final String method;
+        private final String path;
+        private final String query;
+        private final String body;
+        private final String authorization;
+
+        private Request(HttpExchange exchange) throws IOException {
+            method = exchange.getRequestMethod();
+            path = exchange.getRequestURI().getRawPath();
+            query = exchange.getRequestURI().getRawQuery();
+            authorization = 
exchange.getRequestHeaders().getFirst("Authorization");
+            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+            byte[] data = new byte[1024];
+            int read;
+            while ((read = exchange.getRequestBody().read(data)) != -1) {
+                buffer.write(data, 0, read);
+            }
+            body = new String(buffer.toByteArray(), StandardCharsets.UTF_8);
+        }
+    }
+}
diff --git 
a/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
 
b/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
index 5674e90b17..e7e8a5517e 100644
--- 
a/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
+++ 
b/paimon-api/src/test/java/org/apache/paimon/rest/requests/RequestJacksonCompatibilityTest.java
@@ -166,6 +166,7 @@ public class RequestJacksonCompatibilityTest {
 
     private static final Set<Class<? extends RESTRequest>> COMPLEX_REQUESTS =
             Stream.<Class<? extends RESTRequest>>of(
+                            UpsertSemanticViewRequest.class,
                             AlterFunctionRequest.class,
                             AlterTableRequest.class,
                             AlterViewRequest.class,
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
index e18eae4d03..f691fc06f5 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
@@ -43,6 +43,7 @@ import org.apache.paimon.function.FunctionChange;
 import org.apache.paimon.management.LabelManagement;
 import org.apache.paimon.management.PermissionManagement;
 import org.apache.paimon.management.PolicyManagement;
+import org.apache.paimon.management.SemanticViewManagement;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.partition.Partition;
 import org.apache.paimon.partition.PartitionStatistics;
@@ -159,6 +160,12 @@ public class RESTCatalog implements Catalog {
         return new RESTLabelManagement(api);
     }
 
+    /** Definition management for semantic views, using this catalog's 
configuration. */
+    @Experimental
+    public SemanticViewManagement semanticViewManagement() {
+        return new RESTSemanticViewManagement(api);
+    }
+
     @Override
     public List<String> listDatabases() {
         return api.listDatabases();
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogSemanticViewManagementTest.java
 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogSemanticViewManagementTest.java
new file mode 100644
index 0000000000..f2c3e9d4af
--- /dev/null
+++ 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogSemanticViewManagementTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.rest;
+
+import org.apache.paimon.PagedList;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.management.SemanticViewManagement;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.rest.responses.GetSemanticViewResponse;
+import org.apache.paimon.view.SemanticView;
+import org.apache.paimon.view.SemanticViewDefinition;
+
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.paimon.options.CatalogOptions.WAREHOUSE;
+import static org.apache.paimon.rest.RESTCatalogOptions.TOKEN;
+import static org.apache.paimon.rest.RESTCatalogOptions.TOKEN_PROVIDER;
+import static org.apache.paimon.rest.RESTCatalogOptions.URI;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Exercises semantic view management through a configured REST catalog and 
its HTTP client. */
+class RESTCatalogSemanticViewManagementTest {
+
+    private static final String BASE_PATH = 
"/v1/catalog%2Fid/databases/sales/semantic-views";
+    private static final Identifier ID = Identifier.create("sales", "revenue");
+    private static final SemanticViewDefinition DEFINITION =
+            new SemanticViewDefinition("provider-yaml", "source: orders\n");
+
+    private MockWebServer server;
+    private RESTCatalog catalog;
+    private SemanticViewManagement models;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        server = new MockWebServer();
+        server.start();
+        enqueue(
+                200,
+                "{\"defaults\":{},\"overrides\":{\"prefix\":\"catalog/id\","
+                        + "\"header.X-Catalog-Context\":\"configured\"}}");
+
+        Options options = new Options();
+        options.set(URI, server.url("/").toString());
+        options.set(WAREHOUSE, "warehouse-id");
+        options.set(TOKEN_PROVIDER, "bear");
+        options.set(TOKEN, "test-token");
+        catalog = new RESTCatalog(CatalogContext.create(options));
+        models = catalog.semanticViewManagement();
+
+        RecordedRequest config = server.takeRequest(10, TimeUnit.SECONDS);
+        assertThat(config).isNotNull();
+        
assertThat(config.getRequestUrl().encodedPath()).isEqualTo("/v1/config");
+        
assertThat(config.getRequestUrl().queryParameter("warehouse")).isEqualTo("warehouse-id");
+        assertThat(server.getRequestCount()).isEqualTo(1);
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        if (catalog != null) {
+            catalog.close();
+        }
+        if (server != null) {
+            server.shutdown();
+        }
+    }
+
+    @Test
+    void testLifecycleUsesSharedConfiguration() throws Exception {
+        String response = RESTApi.toJson(new 
GetSemanticViewResponse("revenue", DEFINITION));
+        enqueue(200, response);
+        SemanticView model = models.upsertSemanticView(ID, DEFINITION);
+        Map<?, ?> body =
+                RESTApi.fromJson(
+                        takeRequest("POST", BASE_PATH + 
"/revenue").getBody().readUtf8(),
+                        Map.class);
+        
assertThat(body.keySet()).extracting(Object::toString).containsExactly("definition");
+        assertModel(model);
+
+        enqueue(200, response);
+        assertModel(models.getSemanticView(ID));
+        takeRequest("GET", BASE_PATH + "/revenue");
+
+        enqueue(200, "");
+        models.deleteSemanticView(ID);
+        RecordedRequest deletion = takeRequest("DELETE", BASE_PATH + 
"/revenue");
+        assertThat(deletion.getRequestUrl().query()).isNull();
+        assertThat(deletion.getBodySize()).isZero();
+        assertThat(server.getRequestCount()).isEqualTo(4);
+    }
+
+    @Test
+    void testPagedAndFullListingThroughManagement() throws Exception {
+        enqueue(200, "{\"semanticViews\":[\"one\"],\"nextPageToken\":\"next 
+/%\"}");
+        PagedList<String> page = models.listSemanticViewsPaged("sales", 1, 
"start +/%");
+        assertThat(page.getElements()).containsExactly("one");
+        assertThat(page.getNextPageToken()).isEqualTo("next +/%");
+        RecordedRequest request = takeRequest("GET", BASE_PATH);
+        
assertThat(request.getRequestUrl().queryParameter("maxResults")).isEqualTo("1");
+        
assertThat(request.getRequestUrl().queryParameter("pageToken")).isEqualTo("start
 +/%");
+        enqueue(200, "{\"semanticViews\":[\"one\"],\"nextPageToken\":\"next 
+/%\"}");
+        enqueue(200, "{\"semanticViews\":[\"two\"]}");
+        assertThat(models.listSemanticViews("sales")).containsExactly("one", 
"two");
+        assertThat(takeRequest("GET", 
BASE_PATH).getRequestUrl().query()).isNull();
+        assertThat(takeRequest("GET", 
BASE_PATH).getRequestUrl().queryParameter("pageToken"))
+                .isEqualTo("next +/%");
+        assertThat(server.getRequestCount()).isEqualTo(4);
+    }
+
+    private static void assertModel(SemanticView model) {
+        assertThat(model.getName()).isEqualTo("revenue");
+        assertThat(model.getDefinition()).isEqualTo(DEFINITION);
+    }
+
+    private void enqueue(int status, String body) {
+        server.enqueue(
+                new MockResponse()
+                        .setResponseCode(status)
+                        .setHeader("Content-Type", "application/json")
+                        .setBody(body));
+    }
+
+    private RecordedRequest takeRequest(String method, String path) throws 
Exception {
+        RecordedRequest request = server.takeRequest(10, TimeUnit.SECONDS);
+        assertThat(request).isNotNull();
+        assertThat(request.getMethod()).isEqualTo(method);
+        assertThat(request.getRequestUrl().encodedPath()).isEqualTo(path);
+        assertThat(request.getHeader("Authorization")).isEqualTo("Bearer 
test-token");
+        
assertThat(request.getHeader("X-Catalog-Context")).isEqualTo("configured");
+        return request;
+    }
+}

Reply via email to