This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 89bf021a32 [rest] Fix OpenAPI contract for client generation (#9289)
89bf021a32 is described below
commit 89bf021a326127207be2208ecd574d1aa38358f1
Author: Jingsong Lee <[email protected]>
AuthorDate: Tue Aug 18 21:48:21 2026 +0800
[rest] Fix OpenAPI contract for client generation (#9289)
---
docs/README.md | 5 +-
docs/docs/concepts/rest/rest-api.md | 3 +
docs/package.json | 6 +-
docs/scripts/validate-rest-openapi.js | 200 +++++++++++++
docs/static/rest-catalog-open-api.yaml | 324 ++++++++++-----------
.../org/apache/paimon/rest/RESTApiJsonTest.java | 56 +++-
6 files changed, 418 insertions(+), 176 deletions(-)
diff --git a/docs/README.md b/docs/README.md
index d05c37f3ba..67d65ae18e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -22,6 +22,9 @@ The site will be available at
http://localhost:3000/docs/master/.
## Build
```bash
+# Validate the REST Catalog OpenAPI contract
+yarn test:rest-openapi
+
# Production build
yarn build
@@ -40,7 +43,7 @@ docs/
│ ├── css/ # Custom styles
│ └── plugins/ # Remark plugins (variable interpolation)
├── static/ # Static assets (images, logos, OpenAPI spec)
-├── scripts/ # Migration utilities
+├── scripts/ # Documentation validation and migration utilities
├── docusaurus.config.js
├── sidebars.js
└── package.json
diff --git a/docs/docs/concepts/rest/rest-api.md
b/docs/docs/concepts/rest/rest-api.md
index c22e3784cc..8a2f2e69e0 100644
--- a/docs/docs/concepts/rest/rest-api.md
+++ b/docs/docs/concepts/rest/rest-api.md
@@ -22,6 +22,9 @@ specific language governing permissions and limitations
under the License.
-->
+The OpenAPI 3.1 document below defines the language-neutral wire contract for
REST Catalog
+servers and clients. It can also be used to generate or validate SDK models in
other languages.
+
<body>
<iframe src="/docs/master/rest-catalog-open-api.yaml" width="100%"
height="800px" />
</body>
diff --git a/docs/package.json b/docs/package.json
index 39727101ac..f0fb43967b 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -5,7 +5,8 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
- "build": "docusaurus build",
+ "build": "yarn test:rest-openapi && docusaurus build",
+ "test:rest-openapi": "node scripts/validate-rest-openapi.js",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
@@ -25,7 +26,8 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "^3.7.0",
- "@docusaurus/types": "^3.7.0"
+ "@docusaurus/types": "^3.7.0",
+ "js-yaml": "^4.1.0"
},
"browserslist": {
"production": [
diff --git a/docs/scripts/validate-rest-openapi.js
b/docs/scripts/validate-rest-openapi.js
new file mode 100644
index 0000000000..62e3598cf5
--- /dev/null
+++ b/docs/scripts/validate-rest-openapi.js
@@ -0,0 +1,200 @@
+/*
+ * 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.
+ */
+
+const fs = require('fs');
+const path = require('path');
+const yaml = require('js-yaml');
+
+const specPath = path.resolve(__dirname, '..', 'static',
'rest-catalog-open-api.yaml');
+const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
+
+function check(condition, message) {
+ if (!condition) {
+ throw new Error(message);
+ }
+}
+
+function decodePointerSegment(segment) {
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~');
+}
+
+function resolveLocalRef(ref) {
+ check(ref.startsWith('#/'), `Only local OpenAPI references are supported,
found: ${ref}`);
+ return ref
+ .slice(2)
+ .split('/')
+ .map(decodePointerSegment)
+ .reduce((current, segment) => {
+ check(
+ current && Object.prototype.hasOwnProperty.call(current, segment),
+ `Unresolved OpenAPI reference: ${ref}`,
+ );
+ return current[segment];
+ }, spec);
+}
+
+function visit(value) {
+ if (Array.isArray(value)) {
+ value.forEach(visit);
+ return;
+ }
+ if (!value || typeof value !== 'object') {
+ return;
+ }
+ check(!Object.prototype.hasOwnProperty.call(value, 'nullable'), 'OpenAPI 3.1
schemas must not use nullable');
+ if (typeof value.$ref === 'string') {
+ resolveLocalRef(value.$ref);
+ }
+ Object.values(value).forEach(visit);
+}
+
+function schema(name) {
+ const value = spec.components && spec.components.schemas &&
spec.components.schemas[name];
+ check(value, `Missing OpenAPI schema: ${name}`);
+ return value;
+}
+
+function requireProperties(schemaName, names) {
+ const properties = schema(schemaName).properties || {};
+ names.forEach((name) => {
+ check(properties[name], `Schema ${schemaName} is missing property:
${name}`);
+ });
+ return properties;
+}
+
+function requireTypedIntegerProperties(schemaName, names) {
+ const properties = requireProperties(schemaName, names);
+ names.forEach((name) => {
+ check(
+ properties[name].type === 'integer' && properties[name].format ===
'int64',
+ `Schema ${schemaName}.${name} must be an int64 integer`,
+ );
+ });
+}
+
+function requireSchemaReference(schemaName, composition, referencedSchemaName)
{
+ const references = schema(schemaName)[composition] || [];
+ const expected = `#/components/schemas/${referencedSchemaName}`;
+ check(
+ references.some((reference) => reference.$ref === expected),
+ `Schema ${schemaName}.${composition} is missing reference: ${expected}`,
+ );
+}
+
+function requireArrayOfIdentifiers(schemaName, propertyName) {
+ const properties = requireProperties(schemaName, [propertyName]);
+ check(properties[propertyName].type === 'array', `Schema
${schemaName}.${propertyName} must be an array`);
+ check(
+ properties[propertyName].items &&
+ properties[propertyName].items.$ref ===
'#/components/schemas/Identifier',
+ `Schema ${schemaName}.${propertyName} items must reference Identifier`,
+ );
+}
+
+function requireNullableStringProperty(schemaName, propertyName) {
+ const property = requireProperties(schemaName, [propertyName])[propertyName];
+ check(
+ Array.isArray(property.type) &&
+ property.type.includes('string') &&
+ property.type.includes('null'),
+ `Schema ${schemaName}.${propertyName} must accept string and null`,
+ );
+}
+
+check(spec.openapi === '3.1.1', `Expected OpenAPI 3.1.1, found:
${spec.openapi}`);
+check(spec.paths && spec.components && spec.components.schemas, 'Incomplete
OpenAPI document');
+visit(spec);
+
+const operationIds = new Set();
+for (const pathItem of Object.values(spec.paths)) {
+ for (const operation of Object.values(pathItem)) {
+ if (!operation || typeof operation !== 'object' || !operation.operationId)
{
+ continue;
+ }
+ check(!operationIds.has(operation.operationId), `Duplicate operationId:
${operation.operationId}`);
+ operationIds.add(operation.operationId);
+ }
+}
+
+[
+ 'getConfig',
+ 'createDatabase',
+ 'getDatabase',
+ 'alterDatabase',
+ 'dropDatabase',
+ 'createTable',
+ 'getTable',
+ 'alterTable',
+ 'dropTable',
+].forEach((operationId) => {
+ check(operationIds.has(operationId), `Missing provider-facing operationId:
${operationId}`);
+});
+
+requireProperties('ConfigResponse', ['defaults', 'overrides']);
+requireProperties('CreateDatabaseRequest', ['name', 'options']);
+requireProperties('AlterDatabaseRequest', ['removals', 'updates']);
+requireProperties('CreateTableRequest', ['identifier', 'schema']);
+requireProperties('AlterTableRequest', ['changes']);
+requireProperties('Identifier', ['database', 'object']);
+requireProperties('Schema', ['fields', 'partitionKeys', 'primaryKeys',
'options', 'comment']);
+requireProperties('DataField', ['id', 'name', 'type', 'description',
'defaultValue']);
+
+requireSchemaReference('DataType', 'oneOf', 'VectorType');
+requireProperties('VectorType', ['type', 'element', 'length']);
+requireSchemaReference('SchemaChange', 'anyOf', 'DropPrimaryKey');
+check(
+ schema('BaseSchemaChange').discriminator.mapping.dropPrimaryKey ===
+ '#/components/schemas/DropPrimaryKey',
+ 'BaseSchemaChange discriminator is missing dropPrimaryKey',
+);
+const dropPrimaryKey = requireProperties('DropPrimaryKey', ['action']);
+check(
+ dropPrimaryKey.action.const === 'dropPrimaryKey',
+ 'Schema DropPrimaryKey.action must be dropPrimaryKey',
+);
+check(
+ schema('BaseInstant').discriminator.propertyName === 'type',
+ 'BaseInstant discriminator must use the JSON field type',
+);
+
+const updateViewComment = requireProperties('UpdateViewComment', ['action',
'comment']);
+check(!updateViewComment.key, 'Schema UpdateViewComment must use comment
instead of key');
+['UpdateComment', 'UpdateViewComment',
'UpdateFunctionComment'].forEach((schemaName) =>
+ requireNullableStringProperty(schemaName, 'comment'),
+);
+
+const errorResourceTypes = requireProperties('ErrorResponse',
['resourceType']).resourceType.enum || [];
+['FUNCTION', 'DEFINITION'].forEach((resourceType) => {
+ check(
+ errorResourceTypes.includes(resourceType),
+ `Schema ErrorResponse.resourceType is missing value: ${resourceType}`,
+ );
+});
+
+requireArrayOfIdentifiers('ListTablesGloballyResponse', 'tables');
+requireArrayOfIdentifiers('ListViewsGloballyResponse', 'views');
+requireArrayOfIdentifiers('ListFunctionsGloballyResponse', 'functions');
+requireProperties('ListFunctionsGloballyResponse', ['nextPageToken']);
+const getFunctionProperties = requireProperties('GetFunctionResponse',
['uuid']);
+check(getFunctionProperties.uuid.type === 'string', 'Schema
GetFunctionResponse.uuid must be a string');
+
+['GetDatabaseResponse', 'GetTableResponse', 'GetViewResponse',
'GetFunctionResponse'].forEach(
+ (schemaName) => requireTypedIntegerProperties(schemaName, ['createdAt',
'updatedAt']),
+);
+
+console.log(`Validated REST OpenAPI contract with ${operationIds.size}
operations.`);
diff --git a/docs/static/rest-catalog-open-api.yaml
b/docs/static/rest-catalog-open-api.yaml
index 47f5acef5e..452919f53f 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -48,14 +48,11 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ConfigResponse'
- example: {
- "overrides": {
- "key": "value"
- },
- "defaults": {
- "prefix": "prefix"
- }
- }
+ example:
+ overrides:
+ key: value
+ defaults:
+ prefix: prefix
"401":
$ref: '#/components/responses/UnauthorizedErrorResponse'
"500":
@@ -2229,10 +2226,9 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- example: {
- "message": "Malformed request",
- "code": 400
- }
+ example:
+ message: Malformed request
+ code: 400
UnauthorizedErrorResponse:
description:
Used for 401 errors.
@@ -2240,10 +2236,9 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- example: {
- "message": "No auth for this resource",
- "code": 401
- }
+ example:
+ message: No auth for this resource
+ code: 401
ForbiddenErrorResponse:
description:
Used for 403 errors.
@@ -2251,10 +2246,9 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- example: {
- "message": "Table has no permission",
- "code": 403
- }
+ example:
+ message: Table has no permission
+ code: 403
ResourceNotExistErrorResponse:
description:
Used for 404 errors, which means the resource does not exist.
@@ -2262,12 +2256,11 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- example: {
- "message": "Resource is not exist",
- "resourceType": "TABLE",
- "resourceName": "user",
- "code": 404
- }
+ example:
+ message: Resource does not exist
+ resourceType: TABLE
+ resourceName: user
+ code: 404
DatabaseNotExistErrorResponse:
description:
Not Found - DatabaseNotExistException, the database does not exist
@@ -2276,12 +2269,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given database does not exist",
- "resourceType": "DATABASE",
- "resourceName": "db",
- "code": 404
- }
+ message: The given database does not exist
+ resourceType: DATABASE
+ resourceName: db
+ code: 404
TableNotExistErrorResponse:
description:
Not Found - TableNotExistException, the table does not exist
@@ -2290,12 +2281,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given table does not exist",
- "resourceType": "TABLE",
- "resourceName": "table",
- "code": 404
- }
+ message: The given table does not exist
+ resourceType: TABLE
+ resourceName: table
+ code: 404
SnapshotNotExistErrorResponse:
description:
Not Found - SnapshotNotExistException, the snapshot does not exist
@@ -2304,12 +2293,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given snapshot does not exist",
- "resourceType": "SNAPSHOT",
- "resourceName": "1",
- "code": 404
- }
+ message: The given snapshot does not exist
+ resourceType: SNAPSHOT
+ resourceName: "1"
+ code: 404
BranchNotExistErrorResponse:
description:
Not Found - BranchNotExistException, the branch does not exist
@@ -2318,12 +2305,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given branch does not exist",
- "resourceType": "BRANCH",
- "resourceName": "branch",
- "code": 404
- }
+ message: The given branch does not exist
+ resourceType: BRANCH
+ resourceName: branch
+ code: 404
TagNotExistErrorResponse:
description:
Not Found - TagNotExistException, the tag does not exist
@@ -2332,12 +2317,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given tag does not exist",
- "resourceType": "TAG",
- "resourceName": "tag1",
- "code": 404
- }
+ message: The given tag does not exist
+ resourceType: TAG
+ resourceName: tag1
+ code: 404
ViewNotExistErrorResponse:
description:
Not Found - ViewNotExistException, the view does not exist
@@ -2346,12 +2329,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given view does not exist",
- "resourceType": "VIEW",
- "resourceName": "view",
- "code": 404
- }
+ message: The given view does not exist
+ resourceType: VIEW
+ resourceName: view
+ code: 404
FunctionNotExistErrorResponse:
description:
Not Found - FunctionNotExistException, the function does not exist
@@ -2360,12 +2341,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given function does not exist",
- "resourceType": "FUNCTION",
- "resourceName": "function",
- "code": 404
- }
+ message: The given function does not exist
+ resourceType: FUNCTION
+ resourceName: function
+ code: 404
ResourceAlreadyExistErrorResponse:
description:
Used for 409 errors.
@@ -2373,12 +2352,11 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- example: {
- "message": "Resource has exist",
- "resourceType": "TABLE",
- "resourceName": "account",
- "code": 409
- }
+ example:
+ message: Resource already exists
+ resourceType: TABLE
+ resourceName: account
+ code: 409
DatabaseAlreadyExistErrorResponse:
description: Conflict - The database already exists
content:
@@ -2386,12 +2364,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given database already exists",
- "resourceType": "DATABASE",
- "resourceName": "db",
- "code": 409
- }
+ message: The given database already exists
+ resourceType: DATABASE
+ resourceName: db
+ code: 409
TableAlreadyExistErrorResponse:
description: Conflict - The table already exists
content:
@@ -2399,12 +2375,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given table already exists",
- "resourceType": "TABLE",
- "resourceName": "table",
- "code": 409
- }
+ message: The given table already exists
+ resourceType: TABLE
+ resourceName: table
+ code: 409
BranchAlreadyExistErrorResponse:
description: Conflict - The branch already exists
content:
@@ -2412,12 +2386,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given branch already exists",
- "resourceType": "BRANCH",
- "resourceName": "branch",
- "code": 409
- }
+ message: The given branch already exists
+ resourceType: BRANCH
+ resourceName: branch
+ code: 409
TagAlreadyExistErrorResponse:
description: Conflict - The tag already exists
content:
@@ -2425,12 +2397,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given tag already exists",
- "resourceType": "TAG",
- "resourceName": "tag1",
- "code": 409
- }
+ message: The given tag already exists
+ resourceType: TAG
+ resourceName: tag1
+ code: 409
ViewAlreadyExistErrorResponse:
description: Conflict - The view already exists
content:
@@ -2438,12 +2408,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given view already exists",
- "resourceType": "VIEW",
- "resourceName": "view",
- "code": 409
- }
+ message: The given view already exists
+ resourceType: VIEW
+ resourceName: view
+ code: 409
FunctionAlreadyExistErrorResponse:
description: Conflict - The view already exists
content:
@@ -2451,12 +2419,10 @@ components:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
- {
- "message": "The given function already exists",
- "resourceType": "FUNCTION",
- "resourceName": "function",
- "code": 409
- }
+ message: The given function already exists
+ resourceType: FUNCTION
+ resourceName: function
+ code: 409
ServerErrorResponse:
description:
Used for server 5xx errors.
@@ -2464,10 +2430,9 @@ components:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
- example: {
- "message": "Internal Server Error",
- "code": 500
- }
+ example:
+ message: Internal Server Error
+ code: 500
schemas:
CreateDatabaseRequest:
type: object
@@ -2599,12 +2564,10 @@ components:
message:
type: string
resourceType:
- type: string
- nullable: true
- enum: [ "DATABASE", "TABLE", "PARTITION", "COLUMN", "SNAPSHOT",
"BRANCH", "TAG", "VIEW", "DIALECT", "UNKNOWN" ]
+ type: [ string, "null" ]
+ enum: [ "DATABASE", "TABLE", "PARTITION", "COLUMN", "SNAPSHOT",
"BRANCH", "TAG", "VIEW", "DIALECT", "FUNCTION", "DEFINITION", "UNKNOWN", null ]
resourceName:
- type: string
- nullable: true
+ type: [ string, "null" ]
code:
type: integer
format: int32
@@ -2720,7 +2683,7 @@ components:
type: string
const: "updateComment"
comment:
- type: string
+ type: [ string, "null" ]
AddDefinition:
allOf:
- $ref: '#/components/schemas/BaseFunctionChange'
@@ -2865,8 +2828,8 @@ components:
action:
type: string
const: "updateComment"
- key:
- type: string
+ comment:
+ type: [ string, "null" ]
AddDialect:
allOf:
- $ref: '#/components/schemas/BaseViewChange'
@@ -2910,6 +2873,8 @@ components:
$ref: '#/components/schemas/DataType'
description:
type: string
+ defaultValue:
+ type: string
DataType:
oneOf:
- $ref: '#/components/schemas/PrimitiveType'
@@ -2917,6 +2882,7 @@ components:
- $ref: '#/components/schemas/MultisetType'
- $ref: '#/components/schemas/MapType'
- $ref: '#/components/schemas/RowType'
+ - $ref: '#/components/schemas/VectorType'
PrimitiveType:
type: string
ArrayType:
@@ -2959,6 +2925,18 @@ components:
type: array
items:
$ref: '#/components/schemas/DataField'
+ VectorType:
+ type: object
+ properties:
+ type:
+ type: string
+ pattern: ^VECTOR.*
+ example: VECTOR
+ element:
+ $ref: '#/components/schemas/DataType'
+ length:
+ type: integer
+ format: int32
Identifier:
type: object
properties:
@@ -3008,10 +2986,12 @@ components:
owner:
type: string
createdAt:
+ type: integer
format: int64
createdBy:
type: string
updatedAt:
+ type: integer
format: int64
updatedBy:
type: string
@@ -3028,6 +3008,7 @@ components:
- $ref: '#/components/schemas/UpdateColumnType'
- $ref: '#/components/schemas/UpdateColumnPosition'
- $ref: '#/components/schemas/UpdateColumnNullability'
+ - $ref: '#/components/schemas/DropPrimaryKey'
BaseSchemaChange:
discriminator:
propertyName: action
@@ -3043,6 +3024,7 @@ components:
updateColumnType: '#/components/schemas/UpdateColumnType'
updateColumnPosition: '#/components/schemas/UpdateColumnPosition'
updateColumnNullability:
'#/components/schemas/UpdateColumnNullability'
+ dropPrimaryKey: '#/components/schemas/DropPrimaryKey'
type: object
required:
- action
@@ -3077,7 +3059,7 @@ components:
type: string
const: "updateComment"
comment:
- type: string
+ type: [ string, "null" ]
AddColumn:
allOf:
- $ref: '#/components/schemas/BaseSchemaChange'
@@ -3182,6 +3164,13 @@ components:
type: string
newNullability:
type: boolean
+ DropPrimaryKey:
+ allOf:
+ - $ref: '#/components/schemas/BaseSchemaChange'
+ properties:
+ action:
+ type: string
+ const: "dropPrimaryKey"
Move:
type: object
properties:
@@ -3204,8 +3193,7 @@ components:
tableId:
type: string
baseSnapshotUuid:
- type: string
- nullable: true
+ type: [ string, "null" ]
snapshot:
$ref: '#/components/schemas/Snapshot'
statistics:
@@ -3218,9 +3206,8 @@ components:
instant:
$ref: '#/components/schemas/Instant'
fromSnapshot:
- type: integer
+ type: [ integer, "null" ]
format: int64
- nullable: true
RollbackSchemaRequest:
type: object
required:
@@ -3235,7 +3222,7 @@ components:
- $ref: '#/components/schemas/TagInstant'
BaseInstant:
discriminator:
- propertyName: action
+ propertyName: type
mapping:
snapshot: '#/components/schemas/SnapshotInstant'
tag: '#/components/schemas/TagInstant'
@@ -3268,12 +3255,10 @@ components:
type: object
properties:
version:
- type: integer
+ type: [ integer, "null" ]
format: int32
- nullable: true
uuid:
- type: string
- nullable: true
+ type: [ string, "null" ]
id:
type: integer
format: int64
@@ -3285,8 +3270,7 @@ components:
deltaManifestList:
type: string
changelogManifestList:
- type: string
- nullable: true
+ type: [ string, "null" ]
indexManifest:
type: string
commitUser:
@@ -3439,10 +3423,12 @@ components:
owner:
type: string
createdAt:
+ type: integer
format: int64
createdBy:
type: string
updatedAt:
+ type: integer
format: int64
updatedBy:
type: string
@@ -3471,8 +3457,7 @@ components:
tables:
type: array
items:
- identifier:
- $ref: '#/components/schemas/Identifier'
+ $ref: '#/components/schemas/Identifier'
nextPageToken:
type: string
ConfigResponse:
@@ -3501,8 +3486,7 @@ components:
branch:
type: string
fromTag:
- nullable: true
- type: string
+ type: [ string, "null" ]
RenameBranchRequest:
type: object
properties:
@@ -3528,13 +3512,11 @@ components:
tagName:
type: string
snapshotId:
- type: integer
+ type: [ integer, "null" ]
format: int64
- nullable: true
description: Optional snapshot id, if not provided uses latest
snapshot
timeRetained:
- type: string
- nullable: true
+ type: [ string, "null" ]
description: Optional time retained as string (e.g., "1d", "12h",
"30m")
ignoreIfExists:
type: boolean
@@ -3548,12 +3530,10 @@ components:
snapshot:
$ref: '#/components/schemas/Snapshot'
tagCreateTime:
- type: integer
+ type: [ integer, "null" ]
format: int64
- nullable: true
tagTimeRetained:
- type: string
- nullable: true
+ type: [ string, "null" ]
ListTagsResponse:
type: object
properties:
@@ -3572,12 +3552,10 @@ components:
type: integer
format: int64
tagCreateTime:
- type: integer
+ type: [ integer, "null" ]
format: int64
- nullable: true
tagTimeRetained:
- type: string
- nullable: true
+ type: [ string, "null" ]
GetViewResponse:
type: object
properties:
@@ -3590,10 +3568,12 @@ components:
owner:
type: string
createdAt:
+ type: integer
format: int64
createdBy:
type: string
updatedAt:
+ type: integer
format: int64
updatedBy:
type: string
@@ -3623,14 +3603,15 @@ components:
type: array
items:
$ref: '#/components/schemas/Identifier'
+ nextPageToken:
+ type: string
ListViewsGloballyResponse:
type: object
properties:
- tables:
+ views:
type: array
items:
- identifier:
- $ref: '#/components/schemas/Identifier'
+ $ref: '#/components/schemas/Identifier'
nextPageToken:
type: string
ListFunctionsResponse:
@@ -3654,6 +3635,8 @@ components:
GetFunctionResponse:
type: object
properties:
+ uuid:
+ type: string
name:
type: string
inputParams:
@@ -3679,10 +3662,12 @@ components:
owner:
type: string
createdAt:
+ type: integer
format: int64
createdBy:
type: string
updatedAt:
+ type: integer
format: int64
updatedBy:
type: string
@@ -3799,28 +3784,25 @@ components:
examples:
TableNotExistError:
summary: The requested table does not exist
- value: {
- "message": "The given table does not exist",
- "resourceType": "TABLE",
- "resourceName": "table",
- "code": 404
- }
+ value:
+ message: The given table does not exist
+ resourceType: TABLE
+ resourceName: table
+ code: 404
SnapshotNotExistError:
summary: The requested snapshot does not exist
- value: {
- "message": "The given snapshot does not exist",
- "resourceType": "SNAPSHOT",
- "resourceName": "1",
- "code": 404
- }
+ value:
+ message: The given snapshot does not exist
+ resourceType: SNAPSHOT
+ resourceName: "1"
+ code: 404
TagNotExistError:
summary: The requested tag does not exist
- value: {
- "message": "The given tag does not exist",
- "resourceType": "TAG",
- "resourceName": "tag1",
- "code": 404
- }
+ value:
+ message: The given tag does not exist
+ resourceType: TAG
+ resourceName: tag1
+ code: 404
securitySchemes:
BearerAuth:
type: http
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
index 7508e00473..4a8b4ed964 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTApiJsonTest.java
@@ -18,6 +18,7 @@
package org.apache.paimon.rest;
+import org.apache.paimon.function.FunctionChange;
import org.apache.paimon.rest.requests.AlterDatabaseRequest;
import org.apache.paimon.rest.requests.AlterFunctionRequest;
import org.apache.paimon.rest.requests.AlterTableRequest;
@@ -46,10 +47,12 @@ import
org.apache.paimon.rest.responses.ListDatabasesResponse;
import org.apache.paimon.rest.responses.ListPartitionsResponse;
import org.apache.paimon.rest.responses.ListTablesResponse;
import org.apache.paimon.rest.responses.ListViewsResponse;
+import org.apache.paimon.schema.SchemaChange;
import org.apache.paimon.table.Instant;
import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.IntType;
+import org.apache.paimon.view.ViewChange;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
@@ -155,15 +158,61 @@ public class RESTApiJsonTest {
String name = "col1";
IntType type = DataTypes.INT();
String descStr = "desc";
+ String defaultValue = "42";
String dataFieldStr =
String.format(
- "{\"id\": %d,\"name\":\"%s\",\"type\":\"%s\",
\"description\":\"%s\"}",
- id, name, type, descStr);
+ "{\"id\": %d,\"name\":\"%s\",\"type\":\"%s\","
+ +
"\"description\":\"%s\",\"defaultValue\":\"%s\"}",
+ id, name, type, descStr, defaultValue);
DataField parseData = RESTApi.fromJson(dataFieldStr, DataField.class);
assertEquals(id, parseData.id());
assertEquals(name, parseData.name());
assertEquals(type, parseData.type());
assertEquals(descStr, parseData.description());
+ assertEquals(defaultValue, parseData.defaultValue());
+
+ DataField field = new DataField(id, name, type, descStr, defaultValue);
+ assertEquals(field, RESTApi.fromJson(RESTApi.toJson(field),
DataField.class));
+ }
+
+ @Test
+ public void providerFacingTypeAndChangeJsonShapeTest() throws Exception {
+ DataField vectorField =
+ new DataField(2, "embedding", DataTypes.VECTOR(3,
DataTypes.FLOAT()));
+ Map<?, ?> vectorJson = RESTApi.fromJson(RESTApi.toJson(vectorField),
Map.class);
+ Map<?, ?> vectorType = (Map<?, ?>) vectorJson.get("type");
+ assertEquals("VECTOR", vectorType.get("type"));
+ assertEquals("FLOAT", vectorType.get("element"));
+ assertEquals(3, vectorType.get("length"));
+
+ assertEquals(
+ Collections.singletonMap("action", "dropPrimaryKey"),
+
RESTApi.fromJson(RESTApi.toJson(SchemaChange.dropPrimaryKey()), Map.class));
+
+ Map<String, Object> expectedViewComment = new HashMap<>();
+ expectedViewComment.put("action", "updateComment");
+ expectedViewComment.put("comment", "new comment");
+ assertEquals(
+ expectedViewComment,
+ RESTApi.fromJson(
+ RESTApi.toJson(ViewChange.updateComment("new
comment")), Map.class));
+ }
+
+ @Test
+ public void nullableCommentChangesJsonShapeTest() throws Exception {
+ Map<String, Object> expected = new HashMap<>();
+ expected.put("action", "updateComment");
+ expected.put("comment", null);
+
+ assertEquals(
+ expected,
+
RESTApi.fromJson(RESTApi.toJson(SchemaChange.updateComment(null)), Map.class));
+ assertEquals(
+ expected,
+
RESTApi.fromJson(RESTApi.toJson(ViewChange.updateComment(null)), Map.class));
+ assertEquals(
+ expected,
+
RESTApi.fromJson(RESTApi.toJson(FunctionChange.updateComment(null)),
Map.class));
}
@Test
@@ -363,6 +412,9 @@ public class RESTApiJsonTest {
RollbackTableRequest rollbackTableRequestBySnapshot =
MockRESTMessage.rollbackTableRequestBySnapshot(snapshotId);
String rollbackTableRequestBySnapshotStr =
RESTApi.toJson(rollbackTableRequestBySnapshot);
+ Map<?, ?> rollbackJson =
RESTApi.fromJson(rollbackTableRequestBySnapshotStr, Map.class);
+ Map<?, ?> instantJson = (Map<?, ?>) rollbackJson.get("instant");
+ assertEquals("snapshot", instantJson.get("type"));
Instant.SnapshotInstant rollbackTableRequestParseData =
(Instant.SnapshotInstant)
RESTApi.fromJson(