This is an automated email from the ASF dual-hosted git repository.
lzljs3620320 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 1a8c173dc0 [core] support function: add function API (#5548)
1a8c173dc0 is described below
commit 1a8c173dc07d7192e40e3ccb413e88d765f9cfbe
Author: jerry <[email protected]>
AuthorDate: Wed May 14 13:26:26 2025 +0800
[core] support function: add function API (#5548)
---
docs/static/rest-catalog-open-api.yaml | 415 ++++++++++++++++++++-
.../org/apache/paimon/catalog/AbstractCatalog.java | 33 ++
.../java/org/apache/paimon/catalog/Catalog.java | 142 +++++++
.../org/apache/paimon/catalog/DelegateCatalog.java | 32 ++
.../java/org/apache/paimon/function/Function.java | 46 +++
.../org/apache/paimon/function/FunctionChange.java | 354 ++++++++++++++++++
.../apache/paimon/function/FunctionDefinition.java | 242 ++++++++++++
.../org/apache/paimon/function/FunctionImpl.java | 120 ++++++
.../java/org/apache/paimon/rest/RESTCatalog.java | 88 +++++
.../java/org/apache/paimon/rest/ResourcePaths.java | 9 +
.../paimon/rest/requests/AlterFunctionRequest.java | 49 +++
.../rest/requests/CreateFunctionRequest.java | 133 +++++++
.../paimon/rest/responses/ErrorResponse.java | 4 +
.../paimon/rest/responses/GetFunctionResponse.java | 136 +++++++
.../rest/responses/ListFunctionsResponse.java | 67 ++++
.../org/apache/paimon/rest/MockRESTMessage.java | 88 ++++-
.../org/apache/paimon/rest/RESTCatalogServer.java | 230 ++++++++++++
.../org/apache/paimon/rest/RESTCatalogTest.java | 102 +++++
.../apache/paimon/rest/RESTObjectMapperTest.java | 32 ++
paimon-open-api/rest-catalog-open-api.yaml | 402 +++++++++++++++++++-
.../paimon/open/api/RESTCatalogController.java | 137 ++++++-
21 files changed, 2844 insertions(+), 17 deletions(-)
diff --git a/docs/static/rest-catalog-open-api.yaml
b/docs/static/rest-catalog-open-api.yaml
index 47336b069d..78ca769ec6 100644
--- a/docs/static/rest-catalog-open-api.yaml
+++ b/docs/static/rest-catalog-open-api.yaml
@@ -95,8 +95,8 @@ paths:
post:
tags:
- database
- summary: Create Databases
- operationId: createDatabases
+ summary: Create Database
+ operationId: createDatabase
parameters:
- name: prefix
in: path
@@ -622,6 +622,8 @@ paths:
description: Success, no content
"401":
$ref: '#/components/responses/UnauthorizedErrorResponse'
+ "403":
+ $ref: '#/components/responses/ForbiddenErrorResponse'
404:
description:
Not Found
@@ -1182,6 +1184,152 @@ paths:
$ref: '#/components/responses/ViewAlreadyExistErrorResponse'
"500":
$ref: '#/components/responses/ServerErrorResponse'
+ /v1/{prefix}/functions:
+ get:
+ tags:
+ - function
+ summary: List functions
+ operationId: listFunctions
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: maxResults
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ListFunctionsResponse'
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+ post:
+ tags:
+ - function
+ summary: Create Function
+ operationId: createFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateFunctionRequest'
+ responses:
+ "200":
+ description: Success, no content
+ "400":
+ $ref: '#/components/responses/BadRequestErrorResponse'
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "409":
+ $ref: '#/components/responses/FunctionAlreadyExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+
+ /v1/{prefix}/functions/{function}:
+ get:
+ tags:
+ - function
+ summary: Get function
+ operationId: getFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: function
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GetFunctionResponse'
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/DatabaseNotExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+ post:
+ tags:
+ - function
+ summary: Alter function
+ operationId: alterFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: function
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AlterFunctionRequest'
+ responses:
+ "200":
+ description: Success, no content
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/FunctionNotExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+ delete:
+ tags:
+ - function
+ summary: Drop function
+ operationId: dropFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: function
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success, no content
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/FunctionNotExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+
components:
#############################
# Reusable Response Objects #
@@ -1209,6 +1357,17 @@ components:
"message": "No auth for this resource",
"code": 401
}
+ ForbiddenErrorResponse:
+ description:
+ Used for 403 errors.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ErrorResponse'
+ example: {
+ "message": "Table has no permission",
+ "code": 403
+ }
ResourceNotExistErrorResponse:
description:
Used for 404 errors, which means the resource does not exist.
@@ -1292,6 +1451,20 @@ components:
"resourceName": "view",
"code": 404
}
+ FunctionNotExistErrorResponse:
+ description:
+ Not Found - FunctionNotExistException, the function does not exist
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/responses/ResourceNotExistErrorResponse'
+ example:
+ {
+ "message": "The given function does not exist",
+ "resourceType": "FUNCTION",
+ "resourceName": "function",
+ "code": 404
+ }
ResourceAlreadyExistErrorResponse:
description:
Used for 409 errors.
@@ -1357,6 +1530,19 @@ components:
"resourceName": "view",
"code": 409
}
+ FunctionAlreadyExistErrorResponse:
+ description: Conflict - The view already exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/responses/ResourceAlreadyExistErrorResponse'
+ example:
+ {
+ "message": "The given function already exists",
+ "resourceType": "FUNCTION",
+ "resourceName": "function",
+ "code": 409
+ }
ServerErrorResponse:
description:
Used for server 5xx errors.
@@ -1451,6 +1637,179 @@ components:
type: array
items:
$ref: '#/components/schemas/ViewChange'
+ CreateFunctionRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ inputParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ returnParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ deterministic:
+ type: boolean
+ definitions:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/FunctionDefinition"
+ comment:
+ type: string
+ options:
+ type: object
+ additionalProperties:
+ type: string
+ AlterFunctionRequest:
+ type: object
+ properties:
+ changes:
+ type: array
+ items:
+ $ref: '#/components/schemas/FunctionChange'
+ FunctionChange:
+ anyOf:
+ - $ref: '#/components/schemas/SetFunctionOption'
+ - $ref: '#/components/schemas/RemoveFunctionOption'
+ - $ref: '#/components/schemas/UpdateFunctionComment'
+ - $ref: '#/components/schemas/AddDefinition'
+ - $ref: '#/components/schemas/UpdateDefinition'
+ - $ref: '#/components/schemas/DropDefinition'
+ BaseFunctionChange:
+ discriminator:
+ propertyName: action
+ mapping:
+ setOption: '#/components/schemas/SetFunctionOption'
+ removeOption: '#/components/schemas/RemoveFunctionOption'
+ updateComment: '#/components/schemas/UpdateFunctionComment'
+ addDefinition: '#/components/schemas/AddDefinition'
+ updateDefinition: '#/components/schemas/UpdateDefinition'
+ dropDefinition: '#/components/schemas/DropDefinition'
+ type: object
+ required:
+ - action
+ properties:
+ action:
+ type: string
+ SetFunctionOption:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "setOption"
+ key:
+ type: string
+ value:
+ type: string
+ RemoveFunctionOption:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "removeOption"
+ key:
+ type: string
+ UpdateFunctionComment:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "updateComment"
+ comment:
+ type: string
+ AddDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "addDefinition"
+ name:
+ type: string
+ definition:
+ $ref: "#/components/schemas/FunctionDefinition"
+ UpdateDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "updateDefinition"
+ name:
+ type: string
+ definition:
+ $ref: "#/components/schemas/FunctionDefinition"
+ DropDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "dropDefinition"
+ name:
+ type: string
+ FunctionDefinition:
+ anyOf:
+ - $ref: '#/components/schemas/FileFunctionDefinition'
+ - $ref: '#/components/schemas/SQLFunctionDefinition'
+ - $ref: '#/components/schemas/LambdaFunctionDefinition'
+ BaseFunctionDefinition:
+ discriminator:
+ propertyName: type
+ mapping:
+ file: '#/components/schemas/FileFunctionDefinition'
+ sql: '#/components/schemas/SQLFunctionDefinition'
+ lambda: '#/components/schemas/LambdaFunctionDefinition'
+ type: object
+ required:
+ - type
+ properties:
+ type:
+ type: string
+ SQLFunctionDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionDefinition'
+ properties:
+ type:
+ type: string
+ const: "sql"
+ definition:
+ type: string
+ FileFunctionDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionDefinition'
+ properties:
+ type:
+ type: string
+ const: "file"
+ fileType:
+ type: string
+ storagePaths:
+ type: array
+ items:
+ type: string
+ language:
+ type: string
+ className:
+ type: string
+ functionName:
+ type: string
+ LambdaFunctionDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionDefinition'
+ properties:
+ type:
+ type: string
+ const: "lambda"
+ definition:
+ type: string
+ language:
+ type: string
ViewChange:
anyOf:
- $ref: '#/components/schemas/SetViewOption'
@@ -1846,13 +2205,13 @@ components:
required:
- type
properties:
- 'type':
+ type:
type: string
SnapshotInstant:
allOf:
- $ref: '#/components/schemas/BaseInstant'
properties:
- 'type':
+ type:
type: string
const: "snapshot"
snapshotId:
@@ -1862,7 +2221,7 @@ components:
allOf:
- $ref: '#/components/schemas/BaseInstant'
properties:
- 'type':
+ type:
type: string
const: "tag"
tagName:
@@ -2126,6 +2485,52 @@ components:
$ref: '#/components/schemas/GetViewResponse'
nextPageToken:
type: string
+ ListFunctionsResponse:
+ type: object
+ properties:
+ functions:
+ type: array
+ items:
+ type: string
+ nextPageToken:
+ type: string
+ GetFunctionResponse:
+ type: object
+ properties:
+ uuid:
+ type: string
+ name:
+ type: string
+ inputParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ returnParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ deterministic:
+ type: boolean
+ definitions:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/FunctionDefinition"
+ comment:
+ type: string
+ options:
+ type: object
+ additionalProperties:
+ type: string
+ owner:
+ type: string
+ createdAt:
+ format: int64
+ createdBy:
+ type: string
+ updatedAt:
+ format: int64
+ updatedBy:
+ type: string
ViewSchema:
type: object
properties:
diff --git
a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
index 87d34f301a..bd3624b66e 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
@@ -25,6 +25,8 @@ import org.apache.paimon.factories.FactoryUtil;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.FileStatus;
import org.apache.paimon.fs.Path;
+import org.apache.paimon.function.Function;
+import org.apache.paimon.function.FunctionChange;
import org.apache.paimon.options.Options;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionStatistics;
@@ -49,6 +51,7 @@ import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -519,6 +522,36 @@ public abstract class AbstractCatalog implements Catalog {
public void alterPartitions(Identifier identifier,
List<PartitionStatistics> partitions)
throws TableNotExistException {}
+ @Override
+ public List<String> listFunctions() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Function getFunction(String functionName) throws
FunctionNotExistException {
+ throw new FunctionNotExistException(functionName);
+ }
+
+ @Override
+ public void createFunction(String functionName, Function function, boolean
ignoreIfExists)
+ throws FunctionAlreadyExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void dropFunction(String functionName, boolean ignoreIfNotExists)
+ throws FunctionNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void alterFunction(
+ String functionName, List<FunctionChange> changes, boolean
ignoreIfNotExists)
+ throws FunctionNotExistException, DefinitionAlreadyExistException,
+ DefinitionNotExistException {
+ throw new UnsupportedOperationException();
+ }
+
/**
* Create a {@link FormatTable} identified by the given {@link Identifier}.
*
diff --git a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
index c940da3ac2..1494c2df19 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java
@@ -21,6 +21,8 @@ package org.apache.paimon.catalog;
import org.apache.paimon.PagedList;
import org.apache.paimon.Snapshot;
import org.apache.paimon.annotation.Public;
+import org.apache.paimon.function.Function;
+import org.apache.paimon.function.FunctionChange;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.schema.Schema;
@@ -666,6 +668,50 @@ public interface Catalog extends AutoCloseable {
void alterPartitions(Identifier identifier, List<PartitionStatistics>
partitions)
throws TableNotExistException;
+ /** List all functions in catalog. */
+ List<String> listFunctions();
+
+ /**
+ * Get function by name.
+ *
+ * @param functionName
+ * @throws FunctionNotExistException
+ */
+ Function getFunction(String functionName) throws FunctionNotExistException;
+
+ /**
+ * Create function.
+ *
+ * @param functionName
+ * @param function
+ * @param ignoreIfExists
+ * @throws FunctionAlreadyExistException
+ */
+ void createFunction(String functionName, Function function, boolean
ignoreIfExists)
+ throws FunctionAlreadyExistException;
+
+ /**
+ * Drop function.
+ *
+ * @param functionName
+ * @param ignoreIfNotExists
+ * @throws FunctionNotExistException
+ */
+ void dropFunction(String functionName, boolean ignoreIfNotExists)
+ throws FunctionNotExistException;
+
+ /**
+ * Alter function.
+ *
+ * @param functionName
+ * @param changes
+ * @param ignoreIfNotExists
+ * @throws FunctionNotExistException
+ */
+ void alterFunction(String functionName, List<FunctionChange> changes,
boolean ignoreIfNotExists)
+ throws FunctionNotExistException, DefinitionAlreadyExistException,
+ DefinitionNotExistException;
+
// ==================== Table Auth ==========================
/**
@@ -1098,4 +1144,100 @@ public interface Catalog extends AutoCloseable {
return dialect;
}
}
+
+ /** Exception for trying to create a function that already exists. */
+ class FunctionAlreadyExistException extends Exception {
+
+ private static final String MSG = "Function %s already exists.";
+
+ private final String functionName;
+
+ public FunctionAlreadyExistException(String functionName) {
+ this(functionName, null);
+ }
+
+ public FunctionAlreadyExistException(String functionName, Throwable
cause) {
+ super(String.format(MSG, functionName), cause);
+ this.functionName = functionName;
+ }
+
+ public String functionName() {
+ return functionName;
+ }
+ }
+
+ /** Exception for trying to get a function that doesn't exist. */
+ class FunctionNotExistException extends Exception {
+
+ private static final String MSG = "Function %s doesn't exist.";
+
+ private final String functionName;
+
+ public FunctionNotExistException(String functionName) {
+ this(functionName, null);
+ }
+
+ public FunctionNotExistException(String functionName, Throwable cause)
{
+ super(String.format(MSG, functionName), cause);
+ this.functionName = functionName;
+ }
+
+ public String functionName() {
+ return functionName;
+ }
+ }
+
+ /** Exception for trying to add a definition that already exists. */
+ class DefinitionAlreadyExistException extends Exception {
+
+ private static final String MSG = "Definition %s in function %s
already exists.";
+
+ private final String functionName;
+ private final String name;
+
+ public DefinitionAlreadyExistException(String functionName, String
name) {
+ this(functionName, name, null);
+ }
+
+ public DefinitionAlreadyExistException(String functionName, String
name, Throwable cause) {
+ super(String.format(MSG, name, functionName), cause);
+ this.functionName = functionName;
+ this.name = name;
+ }
+
+ public String functionName() {
+ return functionName;
+ }
+
+ public String name() {
+ return name;
+ }
+ }
+
+ /** Exception for trying to update definition that doesn't exist. */
+ class DefinitionNotExistException extends Exception {
+
+ private static final String MSG = "Definition %s in function %s
doesn't exist.";
+
+ private final String functionName;
+ private final String name;
+
+ public DefinitionNotExistException(String functionName, String name) {
+ this(functionName, name, null);
+ }
+
+ public DefinitionNotExistException(String functionName, String name,
Throwable cause) {
+ super(String.format(MSG, name, functionName), cause);
+ this.functionName = functionName;
+ this.name = name;
+ }
+
+ public String functionName() {
+ return functionName;
+ }
+
+ public String name() {
+ return name;
+ }
+ }
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
index c781cb9f83..768e4dfc3d 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/DelegateCatalog.java
@@ -20,6 +20,8 @@ package org.apache.paimon.catalog;
import org.apache.paimon.PagedList;
import org.apache.paimon.Snapshot;
+import org.apache.paimon.function.Function;
+import org.apache.paimon.function.FunctionChange;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.schema.Schema;
@@ -209,6 +211,36 @@ public abstract class DelegateCatalog implements Catalog {
wrapped.alterPartitions(identifier, partitions);
}
+ @Override
+ public List<String> listFunctions() {
+ return wrapped.listFunctions();
+ }
+
+ @Override
+ public Function getFunction(String functionName) throws
FunctionNotExistException {
+ return wrapped.getFunction(functionName);
+ }
+
+ @Override
+ public void createFunction(String functionName, Function function, boolean
ignoreIfExists)
+ throws FunctionAlreadyExistException {
+ wrapped.createFunction(functionName, function, ignoreIfExists);
+ }
+
+ @Override
+ public void dropFunction(String functionName, boolean ignoreIfNotExists)
+ throws FunctionNotExistException {
+ wrapped.dropFunction(functionName, ignoreIfNotExists);
+ }
+
+ @Override
+ public void alterFunction(
+ String functionName, List<FunctionChange> changes, boolean
ignoreIfNotExists)
+ throws FunctionNotExistException, DefinitionAlreadyExistException,
+ DefinitionNotExistException {
+ wrapped.alterFunction(functionName, changes, ignoreIfNotExists);
+ }
+
@Override
public void markDonePartitions(Identifier identifier, List<Map<String,
String>> partitions)
throws TableNotExistException {
diff --git a/paimon-core/src/main/java/org/apache/paimon/function/Function.java
b/paimon-core/src/main/java/org/apache/paimon/function/Function.java
new file mode 100644
index 0000000000..1f892387d4
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/function/Function.java
@@ -0,0 +1,46 @@
+/*
+ * 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.function;
+
+import org.apache.paimon.types.DataField;
+
+import java.util.List;
+import java.util.Map;
+
+/** Interface for function. */
+public interface Function {
+
+ String uuid();
+
+ String name();
+
+ List<DataField> inputParams();
+
+ List<DataField> returnParams();
+
+ boolean isDeterministic();
+
+ Map<String, FunctionDefinition> definitions();
+
+ FunctionDefinition definition(String name);
+
+ String comment();
+
+ Map<String, String> options();
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/function/FunctionChange.java
b/paimon-core/src/main/java/org/apache/paimon/function/FunctionChange.java
new file mode 100644
index 0000000000..01bcac532d
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/function/FunctionChange.java
@@ -0,0 +1,354 @@
+/*
+ * 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.function;
+
+import org.apache.paimon.annotation.Public;
+
+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
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonSubTypes;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import javax.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/** Function change. */
+@Public
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = FunctionChange.Actions.FIELD_TYPE)
+@JsonSubTypes({
+ @JsonSubTypes.Type(
+ value = FunctionChange.SetFunctionOption.class,
+ name = FunctionChange.Actions.SET_OPTION_ACTION),
+ @JsonSubTypes.Type(
+ value = FunctionChange.RemoveFunctionOption.class,
+ name = FunctionChange.Actions.REMOVE_OPTION_ACTION),
+ @JsonSubTypes.Type(
+ value = FunctionChange.UpdateFunctionComment.class,
+ name = FunctionChange.Actions.UPDATE_COMMENT_ACTION),
+ @JsonSubTypes.Type(
+ value = FunctionChange.AddDefinition.class,
+ name = FunctionChange.Actions.ADD_DEFINITION_ACTION),
+ @JsonSubTypes.Type(
+ value = FunctionChange.UpdateDefinition.class,
+ name = FunctionChange.Actions.UPDATE_DEFINITION_ACTION),
+ @JsonSubTypes.Type(
+ value = FunctionChange.DropDefinition.class,
+ name = FunctionChange.Actions.DROP_DEFINITION_ACTION)
+})
+public interface FunctionChange extends Serializable {
+
+ static FunctionChange setOption(String key, String value) {
+ return new FunctionChange.SetFunctionOption(key, value);
+ }
+
+ static FunctionChange removeOption(String key) {
+ return new FunctionChange.RemoveFunctionOption(key);
+ }
+
+ static FunctionChange updateComment(String comment) {
+ return new FunctionChange.UpdateFunctionComment(comment);
+ }
+
+ static FunctionChange addDefinition(String name, FunctionDefinition
definition) {
+ return new FunctionChange.AddDefinition(name, definition);
+ }
+
+ static FunctionChange updateDefinition(String name, FunctionDefinition
definition) {
+ return new FunctionChange.UpdateDefinition(name, definition);
+ }
+
+ static FunctionChange dropDefinition(String name) {
+ return new FunctionChange.DropDefinition(name);
+ }
+
+ /** set a function option for function change. */
+ final class SetFunctionOption implements FunctionChange {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String FIELD_KEY = "key";
+ private static final String FIELD_VALUE = "value";
+
+ @JsonProperty(FIELD_KEY)
+ private final String key;
+
+ @JsonProperty(FIELD_VALUE)
+ private final String value;
+
+ @JsonCreator
+ private SetFunctionOption(
+ @JsonProperty(FIELD_KEY) String key,
@JsonProperty(FIELD_VALUE) String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @JsonGetter(FIELD_KEY)
+ public String key() {
+ return key;
+ }
+
+ @JsonGetter(FIELD_VALUE)
+ public String value() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SetFunctionOption that = (SetFunctionOption) o;
+ return key.equals(that.key) && value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, value);
+ }
+ }
+
+ /** remove a function option for function change. */
+ final class RemoveFunctionOption implements FunctionChange {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String FIELD_KEY = "key";
+
+ @JsonProperty(FIELD_KEY)
+ private final String key;
+
+ private RemoveFunctionOption(@JsonProperty(FIELD_KEY) String key) {
+ this.key = key;
+ }
+
+ @JsonGetter(FIELD_KEY)
+ public String key() {
+ return key;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ RemoveFunctionOption that = (RemoveFunctionOption) o;
+ return key.equals(that.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key);
+ }
+ }
+
+ /** update a function comment for function change. */
+ final class UpdateFunctionComment implements FunctionChange {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String FIELD_COMMENT = "comment";
+
+ // If comment is null, means to remove comment
+ @JsonProperty(FIELD_COMMENT)
+ private final @Nullable String comment;
+
+ private UpdateFunctionComment(@JsonProperty(FIELD_COMMENT) @Nullable
String comment) {
+ this.comment = comment;
+ }
+
+ @JsonGetter(FIELD_COMMENT)
+ public @Nullable String comment() {
+ return comment;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ UpdateFunctionComment that = (UpdateFunctionComment) object;
+ return Objects.equals(comment, that.comment);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(comment);
+ }
+ }
+
+ /** add definition for function change. */
+ final class AddDefinition implements FunctionChange {
+ private static final long serialVersionUID = 1L;
+ private static final String FIELD_DEFINITION_NAME = "name";
+ private static final String FIELD_DEFINITION = "definition";
+
+ @JsonProperty(FIELD_DEFINITION_NAME)
+ private final String name;
+
+ @JsonProperty(FIELD_DEFINITION)
+ private final FunctionDefinition definition;
+
+ @JsonCreator
+ public AddDefinition(
+ @JsonProperty(FIELD_DEFINITION_NAME) String name,
+ @JsonProperty(FIELD_DEFINITION) FunctionDefinition definition)
{
+ this.name = name;
+ this.definition = definition;
+ }
+
+ @JsonGetter(FIELD_DEFINITION_NAME)
+ public String name() {
+ return name;
+ }
+
+ @JsonGetter(FIELD_DEFINITION)
+ public FunctionDefinition definition() {
+ return definition;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ AddDefinition that = (AddDefinition) object;
+ return Objects.equals(name, that.name) &&
Objects.equals(definition, that.definition);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, definition);
+ }
+ }
+
+ /** update definition for function change. */
+ final class UpdateDefinition implements FunctionChange {
+ private static final long serialVersionUID = 1L;
+ private static final String FIELD_DEFINITION_NAME = "name";
+ private static final String FIELD_DEFINITION = "definition";
+
+ @JsonProperty(FIELD_DEFINITION_NAME)
+ private final String name;
+
+ @JsonProperty(FIELD_DEFINITION)
+ private final FunctionDefinition definition;
+
+ @JsonCreator
+ public UpdateDefinition(
+ @JsonProperty(FIELD_DEFINITION_NAME) String name,
+ @JsonProperty(FIELD_DEFINITION) FunctionDefinition definition)
{
+ this.name = name;
+ this.definition = definition;
+ }
+
+ @JsonGetter(FIELD_DEFINITION_NAME)
+ public String name() {
+ return name;
+ }
+
+ @JsonGetter(FIELD_DEFINITION)
+ public FunctionDefinition definition() {
+ return definition;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ UpdateDefinition that = (UpdateDefinition) object;
+ return Objects.equals(name, that.name) &&
Objects.equals(definition, that.definition);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(definition, definition);
+ }
+ }
+
+ /** drop definition for function change. */
+ final class DropDefinition implements FunctionChange {
+ private static final long serialVersionUID = 1L;
+ private static final String FIELD_DEFINITION_NAME = "name";
+
+ @JsonProperty(FIELD_DEFINITION_NAME)
+ private final String name;
+
+ @JsonCreator
+ public DropDefinition(@JsonProperty(FIELD_DEFINITION_NAME) String
name) {
+ this.name = name;
+ }
+
+ @JsonGetter(FIELD_DEFINITION_NAME)
+ public String name() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object == null || getClass() != object.getClass()) {
+ return false;
+ }
+ DropDefinition that = (DropDefinition) object;
+ return Objects.equals(name, that.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name);
+ }
+ }
+
+ /** Actions for function alter. */
+ class Actions {
+ public static final String FIELD_TYPE = "action";
+ public static final String ADD_DEFINITION_ACTION = "addDefinition";
+ public static final String UPDATE_DEFINITION_ACTION =
"updateDefinition";
+ public static final String DROP_DEFINITION_ACTION = "dropDefinition";
+ public static final String SET_OPTION_ACTION = "setOption";
+ public static final String REMOVE_OPTION_ACTION = "removeOption";
+ public static final String UPDATE_COMMENT_ACTION = "updateComment";
+
+ private Actions() {}
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/function/FunctionDefinition.java
b/paimon-core/src/main/java/org/apache/paimon/function/FunctionDefinition.java
new file mode 100644
index 0000000000..5683797ee2
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/function/FunctionDefinition.java
@@ -0,0 +1,242 @@
+/*
+ * 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.function;
+
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonSubTypes;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import java.util.List;
+import java.util.Objects;
+
+/** Function definition. */
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.PROPERTY,
+ property = FunctionDefinition.Types.FIELD_TYPE)
+@JsonSubTypes({
+ @JsonSubTypes.Type(
+ value = FunctionDefinition.FileFunctionDefinition.class,
+ name = FunctionDefinition.Types.FILE_TYPE),
+ @JsonSubTypes.Type(
+ value = FunctionDefinition.SQLFunctionDefinition.class,
+ name = FunctionDefinition.Types.SQL_TYPE),
+ @JsonSubTypes.Type(
+ value = FunctionDefinition.LambdaFunctionDefinition.class,
+ name = FunctionDefinition.Types.LAMBDA_TYPE)
+})
+public interface FunctionDefinition {
+
+ static FunctionDefinition file(
+ String fileType,
+ List<String> storagePaths,
+ String language,
+ String className,
+ String functionName) {
+ return new FunctionDefinition.FileFunctionDefinition(
+ fileType, storagePaths, language, className, functionName);
+ }
+
+ static FunctionDefinition sql(String definition) {
+ return new FunctionDefinition.SQLFunctionDefinition(definition);
+ }
+
+ static FunctionDefinition lambda(String definition, String language) {
+ return new FunctionDefinition.LambdaFunctionDefinition(definition,
language);
+ }
+
+ /** File function definition. */
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ final class FileFunctionDefinition implements FunctionDefinition {
+
+ private static final String FIELD_FILE_TYPE = "fileType";
+ private static final String FIELD_STORAGE_PATHS = "storagePaths";
+ private static final String FIELD_LANGUAGE = "language";
+ private static final String FIELD_CLASS_NAME = "className";
+ private static final String FIELD_FUNCTION_NAME = "functionName";
+
+ @JsonProperty(FIELD_FILE_TYPE)
+ private final String fileType;
+
+ @JsonProperty(FIELD_STORAGE_PATHS)
+ private final List<String> storagePaths;
+
+ @JsonProperty(FIELD_LANGUAGE)
+ private String language;
+
+ @JsonProperty(FIELD_CLASS_NAME)
+ private String className;
+
+ @JsonProperty(FIELD_FUNCTION_NAME)
+ private String functionName;
+
+ public FileFunctionDefinition(
+ @JsonProperty(FIELD_FILE_TYPE) String fileType,
+ @JsonProperty(FIELD_STORAGE_PATHS) List<String> storagePaths,
+ @JsonProperty(FIELD_LANGUAGE) String language,
+ @JsonProperty(FIELD_CLASS_NAME) String className,
+ @JsonProperty(FIELD_FUNCTION_NAME) String functionName) {
+ this.fileType = fileType;
+ this.storagePaths = storagePaths;
+ this.language = language;
+ this.className = className;
+ this.functionName = functionName;
+ }
+
+ @JsonGetter(FIELD_FILE_TYPE)
+ public String fileType() {
+ return fileType;
+ }
+
+ @JsonGetter(FIELD_STORAGE_PATHS)
+ public List<String> storagePaths() {
+ return storagePaths;
+ }
+
+ @JsonGetter(FIELD_LANGUAGE)
+ public String language() {
+ return language;
+ }
+
+ @JsonGetter(FIELD_CLASS_NAME)
+ public String className() {
+ return className;
+ }
+
+ @JsonGetter(FIELD_FUNCTION_NAME)
+ public String functionName() {
+ return functionName;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ FileFunctionDefinition that = (FileFunctionDefinition) o;
+ return fileType.equals(that.fileType)
+ && Objects.equals(storagePaths, that.storagePaths)
+ && Objects.equals(language, that.language)
+ && Objects.equals(className, that.className)
+ && Objects.equals(functionName, that.functionName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = Objects.hash(fileType, language, className,
functionName);
+ result = 31 * result + Objects.hashCode(storagePaths);
+ return result;
+ }
+ }
+
+ /** SQL function definition. */
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ final class SQLFunctionDefinition implements FunctionDefinition {
+
+ private static final String FIELD_DEFINITION = "definition";
+
+ private final String definition;
+
+ public SQLFunctionDefinition(@JsonProperty(FIELD_DEFINITION) String
definition) {
+ this.definition = definition;
+ }
+
+ @JsonGetter(FIELD_DEFINITION)
+ public String definition() {
+ return definition;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SQLFunctionDefinition that = (SQLFunctionDefinition) o;
+ return Objects.equals(definition, that.definition);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(definition);
+ }
+ }
+
+ /** Lambda function definition. */
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ final class LambdaFunctionDefinition implements FunctionDefinition {
+
+ private static final String FIELD_DEFINITION = "definition";
+ private static final String FIELD_LANGUAGE = "language";
+
+ private final String definition;
+ private final String language;
+
+ public LambdaFunctionDefinition(
+ @JsonProperty(FIELD_DEFINITION) String definition,
+ @JsonProperty(FIELD_LANGUAGE) String language) {
+ this.definition = definition;
+ this.language = language;
+ }
+
+ @JsonGetter(FIELD_DEFINITION)
+ public String definition() {
+ return definition;
+ }
+
+ @JsonGetter(FIELD_LANGUAGE)
+ public String language() {
+ return language;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ LambdaFunctionDefinition that = (LambdaFunctionDefinition) o;
+ return definition.equals(that.definition) &&
language.equals(that.language);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(definition, language);
+ }
+ }
+
+ /** Types for FunctionDefinition. */
+ class Types {
+ public static final String FIELD_TYPE = "type";
+ public static final String FILE_TYPE = "file";
+ public static final String SQL_TYPE = "sql";
+ public static final String LAMBDA_TYPE = "lambda";
+
+ private Types() {}
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/function/FunctionImpl.java
b/paimon-core/src/main/java/org/apache/paimon/function/FunctionImpl.java
new file mode 100644
index 0000000000..683d23d173
--- /dev/null
+++ b/paimon-core/src/main/java/org/apache/paimon/function/FunctionImpl.java
@@ -0,0 +1,120 @@
+/*
+ * 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.function;
+
+import org.apache.paimon.rest.responses.GetFunctionResponse;
+import org.apache.paimon.types.DataField;
+
+import java.util.List;
+import java.util.Map;
+
+/** Function implementation. */
+public class FunctionImpl implements Function {
+
+ private final String uuid;
+
+ private final String name;
+
+ private final List<DataField> inputParams;
+
+ private final List<DataField> returnParams;
+
+ private final boolean deterministic;
+
+ private final Map<String, FunctionDefinition> definitions;
+
+ private final String comment;
+
+ private final Map<String, String> options;
+
+ public FunctionImpl(
+ String uuid,
+ String functionName,
+ List<DataField> inputParams,
+ List<DataField> returnParams,
+ boolean deterministic,
+ Map<String, FunctionDefinition> definitions,
+ String comment,
+ Map<String, String> options) {
+ this.uuid = uuid;
+ this.name = functionName;
+ this.inputParams = inputParams;
+ this.returnParams = returnParams;
+ this.deterministic = deterministic;
+ this.definitions = definitions;
+ this.comment = comment;
+ this.options = options;
+ }
+
+ public FunctionImpl(GetFunctionResponse response) {
+ this.uuid = response.uuid();
+ this.name = response.name();
+ this.inputParams = response.inputParams();
+ this.returnParams = response.returnParams();
+ this.deterministic = response.isDeterministic();
+ this.definitions = response.definitions();
+ this.comment = response.comment();
+ this.options = response.options();
+ }
+
+ @Override
+ public String uuid() {
+ return this.uuid;
+ }
+
+ @Override
+ public String name() {
+ return this.name;
+ }
+
+ @Override
+ public List<DataField> inputParams() {
+ return inputParams;
+ }
+
+ @Override
+ public List<DataField> returnParams() {
+ return returnParams;
+ }
+
+ @Override
+ public boolean isDeterministic() {
+ return deterministic;
+ }
+
+ @Override
+ public Map<String, FunctionDefinition> definitions() {
+ return definitions;
+ }
+
+ @Override
+ public FunctionDefinition definition(String name) {
+ return definitions.get(name);
+ }
+
+ @Override
+ public String comment() {
+ return comment;
+ }
+
+ @Override
+ public Map<String, String> options() {
+ return options;
+ }
+}
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 d443658e38..a8a1a695ff 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
@@ -30,6 +30,8 @@ import org.apache.paimon.catalog.PropertyChange;
import org.apache.paimon.catalog.TableMetadata;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
+import org.apache.paimon.function.FunctionChange;
+import org.apache.paimon.function.FunctionImpl;
import org.apache.paimon.options.Options;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionStatistics;
@@ -43,12 +45,14 @@ import
org.apache.paimon.rest.exceptions.NoSuchResourceException;
import org.apache.paimon.rest.exceptions.NotImplementedException;
import org.apache.paimon.rest.exceptions.ServiceFailureException;
import org.apache.paimon.rest.requests.AlterDatabaseRequest;
+import org.apache.paimon.rest.requests.AlterFunctionRequest;
import org.apache.paimon.rest.requests.AlterTableRequest;
import org.apache.paimon.rest.requests.AlterViewRequest;
import org.apache.paimon.rest.requests.AuthTableQueryRequest;
import org.apache.paimon.rest.requests.CommitTableRequest;
import org.apache.paimon.rest.requests.CreateBranchRequest;
import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.CreateFunctionRequest;
import org.apache.paimon.rest.requests.CreateTableRequest;
import org.apache.paimon.rest.requests.CreateViewRequest;
import org.apache.paimon.rest.requests.ForwardBranchRequest;
@@ -60,12 +64,14 @@ import org.apache.paimon.rest.responses.CommitTableResponse;
import org.apache.paimon.rest.responses.ConfigResponse;
import org.apache.paimon.rest.responses.ErrorResponse;
import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetFunctionResponse;
import org.apache.paimon.rest.responses.GetTableResponse;
import org.apache.paimon.rest.responses.GetTableSnapshotResponse;
import org.apache.paimon.rest.responses.GetTableTokenResponse;
import org.apache.paimon.rest.responses.GetViewResponse;
import org.apache.paimon.rest.responses.ListBranchesResponse;
import org.apache.paimon.rest.responses.ListDatabasesResponse;
+import org.apache.paimon.rest.responses.ListFunctionsResponse;
import org.apache.paimon.rest.responses.ListPartitionsResponse;
import org.apache.paimon.rest.responses.ListTableDetailsResponse;
import org.apache.paimon.rest.responses.ListTablesResponse;
@@ -823,6 +829,88 @@ public class RESTCatalog implements Catalog {
// not require special reporting.
}
+ @Override
+ public List<String> listFunctions() {
+ return listDataFromPageApi(
+ queryParams ->
+ client.get(
+ resourcePaths.functions(),
+ queryParams,
+ ListFunctionsResponse.class,
+ restAuthFunction));
+ }
+
+ @Override
+ public org.apache.paimon.function.Function getFunction(String functionName)
+ throws FunctionNotExistException {
+ try {
+ GetFunctionResponse response =
+ client.get(
+ resourcePaths.function(functionName),
+ GetFunctionResponse.class,
+ restAuthFunction);
+ return new FunctionImpl(response);
+ } catch (NoSuchResourceException e) {
+ throw new FunctionNotExistException(functionName, e);
+ }
+ }
+
+ @Override
+ public void createFunction(
+ String functionName,
+ org.apache.paimon.function.Function function,
+ boolean ignoreIfExists)
+ throws FunctionAlreadyExistException {
+ try {
+ client.post(
+ resourcePaths.functions(),
+ new CreateFunctionRequest(function),
+ restAuthFunction);
+ } catch (AlreadyExistsException e) {
+ if (ignoreIfExists) {
+ return;
+ }
+ throw new FunctionAlreadyExistException(functionName, e);
+ }
+ }
+
+ @Override
+ public void dropFunction(String functionName, boolean ignoreIfNotExists)
+ throws FunctionNotExistException {
+ try {
+ client.delete(resourcePaths.function(functionName),
restAuthFunction);
+ } catch (NoSuchResourceException e) {
+ if (ignoreIfNotExists) {
+ return;
+ }
+ throw new FunctionNotExistException(functionName, e);
+ }
+ }
+
+ @Override
+ public void alterFunction(
+ String functionName, List<FunctionChange> changes, boolean
ignoreIfNotExists)
+ throws FunctionNotExistException, DefinitionAlreadyExistException,
+ DefinitionNotExistException {
+ try {
+ client.post(
+ resourcePaths.function(functionName),
+ new AlterFunctionRequest(changes),
+ restAuthFunction);
+ } catch (AlreadyExistsException e) {
+ throw new DefinitionAlreadyExistException(functionName,
e.resourceName());
+ } catch (NoSuchResourceException e) {
+ if (StringUtils.equals(e.resourceType(),
ErrorResponse.RESOURCE_TYPE_DEFINITION)) {
+ throw new DefinitionNotExistException(functionName,
e.resourceName());
+ }
+ if (!ignoreIfNotExists) {
+ throw new FunctionNotExistException(functionName);
+ }
+ } catch (BadRequestException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+ }
+
@Override
public View getView(Identifier identifier) throws ViewNotExistException {
try {
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
index cf1bcd3bcc..ac79e977cb 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -36,6 +36,7 @@ public class ResourcePaths {
protected static final String TABLE_DETAILS = "table-details";
protected static final String VIEW_DETAILS = "view-details";
protected static final String ROLLBACK = "rollback";
+ protected static final String FUNCTIONS = "functions";
private static final Joiner SLASH = Joiner.on("/").skipNulls();
@@ -213,4 +214,12 @@ public class ResourcePaths {
public String renameView() {
return SLASH.join(V1, prefix, VIEWS, "rename");
}
+
+ public String functions() {
+ return SLASH.join(V1, prefix, FUNCTIONS);
+ }
+
+ public String function(String functionName) {
+ return SLASH.join(V1, prefix, FUNCTIONS, encodeString(functionName));
+ }
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/requests/AlterFunctionRequest.java
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/AlterFunctionRequest.java
new file mode 100644
index 0000000000..79986aedc3
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/AlterFunctionRequest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.function.FunctionChange;
+import org.apache.paimon.rest.RESTRequest;
+
+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.JsonIgnoreProperties;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/** Request for altering function. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AlterFunctionRequest implements RESTRequest {
+
+ private static final String FIELD_CHANGES = "changes";
+
+ @JsonProperty(FIELD_CHANGES)
+ private final List<FunctionChange> changes;
+
+ @JsonCreator
+ public AlterFunctionRequest(@JsonProperty(FIELD_CHANGES)
List<FunctionChange> changes) {
+ this.changes = changes;
+ }
+
+ @JsonGetter(FIELD_CHANGES)
+ public List<FunctionChange> changes() {
+ return changes;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateFunctionRequest.java
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateFunctionRequest.java
new file mode 100644
index 0000000000..31c816f835
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/requests/CreateFunctionRequest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.function.Function;
+import org.apache.paimon.function.FunctionDefinition;
+import org.apache.paimon.rest.RESTRequest;
+import org.apache.paimon.types.DataField;
+
+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.JsonIgnoreProperties;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+import java.util.Map;
+
+/** Request for creating function. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CreateFunctionRequest implements RESTRequest {
+
+ private static final String FIELD_NAME = "name";
+ private static final String FIELD_INPUT_PARAMETERS = "inputParams";
+ private static final String FIELD_RETURN_PARAMETERS = "returnParams";
+ private static final String FIELD_DEFINITIONS = "definitions";
+ private static final String FIELD_DETERMINISTIC = "deterministic";
+ private static final String FIELD_COMMENT = "comment";
+ private static final String FIELD_OPTIONS = "options";
+
+ @JsonProperty(FIELD_NAME)
+ private final String functionName;
+
+ @JsonProperty(FIELD_INPUT_PARAMETERS)
+ private final List<DataField> inputParams;
+
+ @JsonProperty(FIELD_RETURN_PARAMETERS)
+ private final List<DataField> returnParams;
+
+ @JsonProperty(FIELD_DETERMINISTIC)
+ private final boolean deterministic;
+
+ @JsonProperty(FIELD_DEFINITIONS)
+ private final Map<String, FunctionDefinition> definitions;
+
+ @JsonProperty(FIELD_COMMENT)
+ private final String comment;
+
+ @JsonProperty(FIELD_OPTIONS)
+ private final Map<String, String> options;
+
+ @JsonCreator
+ public CreateFunctionRequest(
+ @JsonProperty(FIELD_NAME) String functionName,
+ @JsonProperty(FIELD_INPUT_PARAMETERS) List<DataField> inputParams,
+ @JsonProperty(FIELD_RETURN_PARAMETERS) List<DataField>
returnParams,
+ @JsonProperty(FIELD_DETERMINISTIC) boolean deterministic,
+ @JsonProperty(FIELD_DEFINITIONS) Map<String, FunctionDefinition>
definitions,
+ @JsonProperty(FIELD_COMMENT) String comment,
+ @JsonProperty(FIELD_OPTIONS) Map<String, String> options) {
+ this.functionName = functionName;
+ this.inputParams = inputParams;
+ this.returnParams = returnParams;
+ this.deterministic = deterministic;
+ this.definitions = definitions;
+ this.comment = comment;
+ this.options = options;
+ }
+
+ public CreateFunctionRequest(Function function) {
+ this.functionName = function.name();
+ this.inputParams = function.inputParams();
+ this.returnParams = function.returnParams();
+ this.deterministic = function.isDeterministic();
+ this.definitions = function.definitions();
+ this.comment = function.comment();
+ this.options = function.options();
+ }
+
+ @JsonGetter(FIELD_NAME)
+ public String name() {
+ return functionName;
+ }
+
+ @JsonGetter(FIELD_INPUT_PARAMETERS)
+ public List<DataField> inputParams() {
+ return inputParams;
+ }
+
+ @JsonGetter(FIELD_RETURN_PARAMETERS)
+ public List<DataField> returnParams() {
+ return returnParams;
+ }
+
+ @JsonGetter(FIELD_DETERMINISTIC)
+ public boolean isDeterministic() {
+ return deterministic;
+ }
+
+ @JsonGetter(FIELD_DEFINITIONS)
+ public Map<String, FunctionDefinition> definitions() {
+ return definitions;
+ }
+
+ public FunctionDefinition definition(String dialect) {
+ return definitions.get(dialect);
+ }
+
+ @JsonGetter(FIELD_COMMENT)
+ public String comment() {
+ return comment;
+ }
+
+ @JsonGetter(FIELD_OPTIONS)
+ public Map<String, String> options() {
+ return options;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
index d1de92e7d6..d4f9662dfa 100644
---
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ErrorResponse.java
@@ -47,6 +47,10 @@ public class ErrorResponse implements RESTResponse {
public static final String RESOURCE_TYPE_DIALECT = "DIALECT";
+ public static final String RESOURCE_TYPE_FUNCTION = "FUNCTION";
+
+ public static final String RESOURCE_TYPE_DEFINITION = "DEFINITION";
+
private static final String FIELD_MESSAGE = "message";
private static final String FIELD_RESOURCE_TYPE = "resourceType";
private static final String FIELD_RESOURCE_NAME = "resourceName";
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetFunctionResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetFunctionResponse.java
new file mode 100644
index 0000000000..6780c389e4
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetFunctionResponse.java
@@ -0,0 +1,136 @@
+/*
+ * 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.function.FunctionDefinition;
+import org.apache.paimon.types.DataField;
+
+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.JsonIgnoreProperties;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+import java.util.Map;
+
+/** Response for getting a function. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class GetFunctionResponse extends AuditRESTResponse {
+
+ private static final String FIELD_UUID = "uuid";
+ private static final String FIELD_NAME = "name";
+ private static final String FIELD_INPUT_PARAMETERS = "inputParams";
+ private static final String FIELD_RETURN_PARAMETERS = "returnParams";
+ private static final String FIELD_DEFINITIONS = "definitions";
+ private static final String FIELD_DETERMINISTIC = "deterministic";
+ private static final String FIELD_COMMENT = "comment";
+ private static final String FIELD_OPTIONS = "options";
+
+ @JsonProperty(FIELD_UUID)
+ private final String uuid;
+
+ @JsonProperty(FIELD_NAME)
+ private final String functionName;
+
+ @JsonProperty(FIELD_INPUT_PARAMETERS)
+ private final List<DataField> inputParams;
+
+ @JsonProperty(FIELD_RETURN_PARAMETERS)
+ private final List<DataField> returnParams;
+
+ @JsonProperty(FIELD_DETERMINISTIC)
+ private final boolean deterministic;
+
+ @JsonProperty(FIELD_DEFINITIONS)
+ private final Map<String, FunctionDefinition> definitions;
+
+ @JsonProperty(FIELD_COMMENT)
+ private final String comment;
+
+ @JsonProperty(FIELD_OPTIONS)
+ private final Map<String, String> options;
+
+ @JsonCreator
+ public GetFunctionResponse(
+ @JsonProperty(FIELD_UUID) String uuid,
+ @JsonProperty(FIELD_NAME) String functionName,
+ @JsonProperty(FIELD_INPUT_PARAMETERS) List<DataField> inputParams,
+ @JsonProperty(FIELD_RETURN_PARAMETERS) List<DataField>
returnParams,
+ @JsonProperty(FIELD_DETERMINISTIC) boolean deterministic,
+ @JsonProperty(FIELD_DEFINITIONS) Map<String, FunctionDefinition>
definitions,
+ @JsonProperty(FIELD_COMMENT) String comment,
+ @JsonProperty(FIELD_OPTIONS) Map<String, String> options,
+ @JsonProperty(FIELD_OWNER) String owner,
+ @JsonProperty(FIELD_CREATED_AT) long createdAt,
+ @JsonProperty(FIELD_CREATED_BY) String createdBy,
+ @JsonProperty(FIELD_UPDATED_AT) long updatedAt,
+ @JsonProperty(FIELD_UPDATED_BY) String updatedBy) {
+ super(owner, createdAt, createdBy, updatedAt, updatedBy);
+ this.functionName = functionName;
+ this.uuid = uuid;
+ this.inputParams = inputParams;
+ this.returnParams = returnParams;
+ this.deterministic = deterministic;
+ this.definitions = definitions;
+ this.comment = comment;
+ this.options = options;
+ }
+
+ public String uuid() {
+ return this.uuid;
+ }
+
+ public String name() {
+ return this.functionName;
+ }
+
+ @JsonGetter(FIELD_INPUT_PARAMETERS)
+ public List<DataField> inputParams() {
+ return inputParams;
+ }
+
+ @JsonGetter(FIELD_RETURN_PARAMETERS)
+ public List<DataField> returnParams() {
+ return returnParams;
+ }
+
+ @JsonGetter(FIELD_DETERMINISTIC)
+ public boolean isDeterministic() {
+ return deterministic;
+ }
+
+ @JsonGetter(FIELD_DEFINITIONS)
+ public Map<String, FunctionDefinition> definitions() {
+ return definitions;
+ }
+
+ public FunctionDefinition definition(String dialect) {
+ return definitions.get(dialect);
+ }
+
+ @JsonGetter(FIELD_COMMENT)
+ public String comment() {
+ return comment;
+ }
+
+ @JsonGetter(FIELD_OPTIONS)
+ public Map<String, String> options() {
+ return options;
+ }
+}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListFunctionsResponse.java
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListFunctionsResponse.java
new file mode 100644
index 0000000000..0c2165077a
--- /dev/null
+++
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/ListFunctionsResponse.java
@@ -0,0 +1,67 @@
+/*
+ * 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.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.JsonIgnoreProperties;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/** Response for listing functions. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ListFunctionsResponse implements PagedResponse<String> {
+
+ private static final String FIELD_FUNCTIONS = "functions";
+ private static final String FIELD_NEXT_PAGE_TOKEN = "nextPageToken";
+
+ @JsonProperty(FIELD_FUNCTIONS)
+ private final List<String> functions;
+
+ @JsonProperty(FIELD_NEXT_PAGE_TOKEN)
+ private final String nextPageToken;
+
+ public ListFunctionsResponse(@JsonProperty(FIELD_FUNCTIONS) List<String>
functions) {
+ this(functions, null);
+ }
+
+ @JsonCreator
+ public ListFunctionsResponse(
+ @JsonProperty(FIELD_FUNCTIONS) List<String> functions,
+ @JsonProperty(FIELD_NEXT_PAGE_TOKEN) String nextPageToken) {
+ this.functions = functions;
+ this.nextPageToken = nextPageToken;
+ }
+
+ @JsonGetter(FIELD_FUNCTIONS)
+ public List<String> functions() {
+ return this.functions;
+ }
+
+ @JsonGetter(FIELD_NEXT_PAGE_TOKEN)
+ public String getNextPageToken() {
+ return this.nextPageToken;
+ }
+
+ @Override
+ public List<String> data() {
+ return functions();
+ }
+}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
index 6adfd66caf..0bf9a17920 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
@@ -19,17 +19,24 @@
package org.apache.paimon.rest;
import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.function.Function;
+import org.apache.paimon.function.FunctionChange;
+import org.apache.paimon.function.FunctionDefinition;
+import org.apache.paimon.function.FunctionImpl;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.rest.requests.AlterDatabaseRequest;
+import org.apache.paimon.rest.requests.AlterFunctionRequest;
import org.apache.paimon.rest.requests.AlterTableRequest;
import org.apache.paimon.rest.requests.AlterViewRequest;
import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.CreateFunctionRequest;
import org.apache.paimon.rest.requests.CreateTableRequest;
import org.apache.paimon.rest.requests.CreateViewRequest;
import org.apache.paimon.rest.requests.RenameTableRequest;
import org.apache.paimon.rest.requests.RollbackTableRequest;
import org.apache.paimon.rest.responses.AlterDatabaseResponse;
import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetFunctionResponse;
import org.apache.paimon.rest.responses.GetTableResponse;
import org.apache.paimon.rest.responses.GetTableTokenResponse;
import org.apache.paimon.rest.responses.GetViewResponse;
@@ -51,6 +58,7 @@ import org.apache.paimon.view.ViewSchema;
import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
import org.apache.paimon.shade.guava30.com.google.common.collect.Lists;
+import org.apache.paimon.shade.guava30.com.google.common.collect.Maps;
import java.util.ArrayList;
import java.util.Arrays;
@@ -112,10 +120,6 @@ public class MockRESTMessage {
return new ListTablesResponse(Lists.newArrayList("table"));
}
- public static ListTablesResponse listTablesEmptyResponse() {
- return new ListTablesResponse(Lists.newArrayList());
- }
-
public static CreateTableRequest createTableRequest(String name) {
Identifier identifier = Identifier.create(databaseName(), name);
Map<String, String> options = new HashMap<>();
@@ -140,7 +144,7 @@ public class MockRESTMessage {
}
public static AlterTableRequest alterTableRequest() {
- return new AlterTableRequest(getChanges());
+ return new AlterTableRequest(getSchemaChanges());
}
public static ListPartitionsResponse listPartitionsResponse() {
@@ -150,7 +154,7 @@ public class MockRESTMessage {
return new ListPartitionsResponse(ImmutableList.of(partition));
}
- public static List<SchemaChange> getChanges() {
+ public static List<SchemaChange> getSchemaChanges() {
// add option
SchemaChange addOption =
SchemaChange.setOption("snapshot.time-retained", "2h");
// update comment
@@ -281,6 +285,78 @@ public class MockRESTMessage {
return new AlterViewRequest(viewChanges);
}
+ public static GetFunctionResponse getFunctionResponse() {
+ Function function = function("function");
+ return new GetFunctionResponse(
+ function.uuid(),
+ function.name(),
+ function.inputParams(),
+ function.returnParams(),
+ function.isDeterministic(),
+ function.definitions(),
+ function.comment(),
+ function.options(),
+ "owner",
+ 1L,
+ "owner",
+ 1L,
+ "owner");
+ }
+
+ public static CreateFunctionRequest createFunctionRequest() {
+ Function function = function("function");
+ return new CreateFunctionRequest(
+ function.name(),
+ function.inputParams(),
+ function.returnParams(),
+ function.isDeterministic(),
+ function.definitions(),
+ function.comment(),
+ function.options());
+ }
+
+ public static Function function(String functionName) {
+ List<DataField> inputParams =
+ Lists.newArrayList(
+ new DataField(0, "length", DataTypes.DOUBLE()),
+ new DataField(1, "width", DataTypes.DOUBLE()));
+ List<DataField> returnParams =
+ Lists.newArrayList(new DataField(0, "area",
DataTypes.DOUBLE()));
+ FunctionDefinition flinkFunction =
+ FunctionDefinition.file(
+ "jar", Lists.newArrayList("/a/b/c.jar"), "java",
"className", "eval");
+ FunctionDefinition sparkFunction =
+ FunctionDefinition.lambda(
+ "(Double length, Double width) -> length * width",
"java");
+ FunctionDefinition trinoFunction = FunctionDefinition.sql("length *
width");
+ Map<String, FunctionDefinition> definitions = Maps.newHashMap();
+ definitions.put("flink", flinkFunction);
+ definitions.put("spark", sparkFunction);
+ definitions.put("trino", trinoFunction);
+ return new FunctionImpl(
+ UUID.randomUUID().toString(),
+ functionName,
+ inputParams,
+ returnParams,
+ false,
+ definitions,
+ "comment",
+ ImmutableMap.of());
+ }
+
+ public static AlterFunctionRequest alterFunctionRequest() {
+ List<FunctionChange> functionChanges = new ArrayList<>();
+ functionChanges.add(FunctionChange.setOption("key", "value"));
+ functionChanges.add(FunctionChange.removeOption("key"));
+ functionChanges.add(FunctionChange.updateComment("comment"));
+ functionChanges.add(
+ FunctionChange.addDefinition("engine",
FunctionDefinition.sql("x * y")));
+ functionChanges.add(
+ FunctionChange.updateDefinition("engine",
FunctionDefinition.sql("x * y")));
+ functionChanges.add(FunctionChange.dropDefinition("engine"));
+ return new AlterFunctionRequest(functionChanges);
+ }
+
private static ViewSchema viewSchema() {
List<DataField> fields =
Arrays.asList(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
index 0bce34dbbc..ff5d9e4116 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
@@ -33,6 +33,10 @@ import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
import org.apache.paimon.fs.local.LocalFileIO;
import org.apache.paimon.fs.local.LocalFileIOLoader;
+import org.apache.paimon.function.Function;
+import org.apache.paimon.function.FunctionChange;
+import org.apache.paimon.function.FunctionDefinition;
+import org.apache.paimon.function.FunctionImpl;
import org.apache.paimon.operation.Lock;
import org.apache.paimon.options.CatalogOptions;
import org.apache.paimon.options.Options;
@@ -41,12 +45,14 @@ import org.apache.paimon.partition.PartitionStatistics;
import org.apache.paimon.rest.auth.AuthProvider;
import org.apache.paimon.rest.auth.RESTAuthParameter;
import org.apache.paimon.rest.requests.AlterDatabaseRequest;
+import org.apache.paimon.rest.requests.AlterFunctionRequest;
import org.apache.paimon.rest.requests.AlterTableRequest;
import org.apache.paimon.rest.requests.AlterViewRequest;
import org.apache.paimon.rest.requests.AuthTableQueryRequest;
import org.apache.paimon.rest.requests.CommitTableRequest;
import org.apache.paimon.rest.requests.CreateBranchRequest;
import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.CreateFunctionRequest;
import org.apache.paimon.rest.requests.CreateTableRequest;
import org.apache.paimon.rest.requests.CreateViewRequest;
import org.apache.paimon.rest.requests.MarkDonePartitionsRequest;
@@ -57,12 +63,14 @@ import org.apache.paimon.rest.responses.CommitTableResponse;
import org.apache.paimon.rest.responses.ConfigResponse;
import org.apache.paimon.rest.responses.ErrorResponse;
import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetFunctionResponse;
import org.apache.paimon.rest.responses.GetTableResponse;
import org.apache.paimon.rest.responses.GetTableSnapshotResponse;
import org.apache.paimon.rest.responses.GetTableTokenResponse;
import org.apache.paimon.rest.responses.GetViewResponse;
import org.apache.paimon.rest.responses.ListBranchesResponse;
import org.apache.paimon.rest.responses.ListDatabasesResponse;
+import org.apache.paimon.rest.responses.ListFunctionsResponse;
import org.apache.paimon.rest.responses.ListPartitionsResponse;
import org.apache.paimon.rest.responses.ListTableDetailsResponse;
import org.apache.paimon.rest.responses.ListTablesResponse;
@@ -128,6 +136,7 @@ public class RESTCatalogServer {
public static final String AUTHORIZATION_HEADER_KEY = "Authorization";
private final String databaseUri;
+ private final String functionUri;
private final FileSystemCatalog catalog;
private final MockWebServer server;
@@ -140,6 +149,7 @@ public class RESTCatalogServer {
private final Map<String, TableSnapshot> tableWithSnapshotId2SnapshotStore
= new HashMap<>();
private final List<String> noPermissionDatabases = new ArrayList<>();
private final List<String> noPermissionTables = new ArrayList<>();
+ private final Map<String, Function> functionStore = new HashMap<>();
private final Map<String, List<String>> columnAuthHandler = new
HashMap<>();
public final ConfigResponse configResponse;
public final String warehouse;
@@ -154,6 +164,7 @@ public class RESTCatalogServer {
this.configResponse.getDefaults().get(RESTCatalogInternalOptions.PREFIX.key());
this.resourcePaths = new ResourcePaths(prefix);
this.databaseUri = resourcePaths.databases();
+ this.functionUri = resourcePaths.functions();
Options conf = new Options();
this.configResponse.getDefaults().forEach(conf::setString);
conf.setString(CatalogOptions.WAREHOUSE.key(), dataPath);
@@ -264,6 +275,11 @@ public class RESTCatalogServer {
} else if (databaseUri.equals(request.getPath())
|| request.getPath().contains(databaseUri + "?")) {
return databasesApiHandler(restAuthParameter.method(),
data, parameters);
+ } else if (functionUri.equals(request.getPath())) {
+ return functionsApiHandler(restAuthParameter.method(),
data, parameters);
+ } else if (request.getPath().startsWith(functionUri)) {
+ return functionApiHandler(
+ request.getPath(), restAuthParameter.method(),
data, parameters);
} else if
(resourcePaths.renameTable().equals(request.getPath())) {
return renameTableHandle(restAuthParameter.data());
} else if
(resourcePaths.renameView().equals(request.getPath())) {
@@ -500,6 +516,22 @@ public class RESTCatalogServer {
e.getMessage(),
409);
return mockResponse(response, 409);
+ } catch (Catalog.FunctionAlreadyExistException e) {
+ response =
+ new ErrorResponse(
+ ErrorResponse.RESOURCE_TYPE_COLUMN,
+ e.functionName(),
+ e.getMessage(),
+ 409);
+ return mockResponse(response, 409);
+ } catch (Catalog.DefinitionAlreadyExistException e) {
+ response =
+ new ErrorResponse(
+ ErrorResponse.RESOURCE_TYPE_DEFINITION,
+ e.functionName(),
+ e.getMessage(),
+ 409);
+ return mockResponse(response, 409);
} catch (Catalog.ViewNotExistException e) {
response =
new ErrorResponse(
@@ -516,6 +548,22 @@ public class RESTCatalogServer {
e.getMessage(),
404);
return mockResponse(response, 404);
+ } catch (Catalog.FunctionNotExistException e) {
+ response =
+ new ErrorResponse(
+ ErrorResponse.RESOURCE_TYPE_FUNCTION,
+ e.functionName(),
+ e.getMessage(),
+ 404);
+ return mockResponse(response, 404);
+ } catch (Catalog.DefinitionNotExistException e) {
+ response =
+ new ErrorResponse(
+ ErrorResponse.RESOURCE_TYPE_DEFINITION,
+ e.functionName(),
+ e.getMessage(),
+ 404);
+ return mockResponse(response, 404);
} catch (Catalog.ViewAlreadyExistException e) {
response =
new ErrorResponse(
@@ -711,6 +759,158 @@ public class RESTCatalogServer {
}
}
+ private MockResponse functionDetailsHandler(String functionName) throws
Exception {
+ if (functionStore.containsKey(functionName)) {
+ Function function = functionStore.get(functionName);
+ GetFunctionResponse response =
+ new GetFunctionResponse(
+ function.uuid(),
+ function.name(),
+ function.inputParams(),
+ function.returnParams(),
+ function.isDeterministic(),
+ function.definitions(),
+ function.comment(),
+ function.options(),
+ "owner",
+ 1L,
+ "owner",
+ 1L,
+ "owner");
+ return mockResponse(response, 200);
+ } else {
+ throw new Catalog.FunctionNotExistException(functionName);
+ }
+ }
+
+ private MockResponse functionsApiHandler(
+ String method, String data, Map<String, String> parameters) throws
Exception {
+ switch (method) {
+ case "GET":
+ List<String> functions = new
ArrayList<>(functionStore.keySet());
+ return generateFinalListFunctionsResponse(parameters,
functions);
+ case "POST":
+ CreateFunctionRequest requestBody =
+ OBJECT_MAPPER.readValue(data,
CreateFunctionRequest.class);
+ String functionName = requestBody.name();
+ if (!functionStore.containsKey(functionName)) {
+ Function function =
+ new FunctionImpl(
+ UUID.randomUUID().toString(),
+ functionName,
+ requestBody.inputParams(),
+ requestBody.returnParams(),
+ requestBody.isDeterministic(),
+ requestBody.definitions(),
+ requestBody.comment(),
+ requestBody.options());
+ functionStore.put(functionName, function);
+ return new MockResponse().setResponseCode(200);
+ } else {
+ throw new
Catalog.FunctionAlreadyExistException(functionName);
+ }
+ default:
+ return new MockResponse().setResponseCode(404);
+ }
+ }
+
+ private MockResponse functionApiHandler(
+ String path, String method, String data, Map<String, String>
parameters)
+ throws Exception {
+ String[] resources = path.substring((functionUri +
"/").length()).split("/");
+ String functionName = RESTUtil.decodeString(resources[0]);
+ if (!functionStore.containsKey(functionName)) {
+ throw new Catalog.FunctionNotExistException(functionName);
+ }
+ Function function = functionStore.get(functionName);
+ switch (method) {
+ case "DELETE":
+ functionStore.remove(functionName);
+ break;
+ case "GET":
+ GetFunctionResponse response =
+ new GetFunctionResponse(
+ function.uuid(),
+ function.name(),
+ function.inputParams(),
+ function.returnParams(),
+ function.isDeterministic(),
+ function.definitions(),
+ function.comment(),
+ function.options(),
+ "owner",
+ 1L,
+ "owner",
+ 1L,
+ "owner");
+ return mockResponse(response, 200);
+ case "POST":
+ AlterFunctionRequest requestBody =
+ OBJECT_MAPPER.readValue(data,
AlterFunctionRequest.class);
+ HashMap<String, FunctionDefinition> newDefinitions =
+ new HashMap<>(function.definitions());
+ Map<String, String> newOptions = new
HashMap<>(function.options());
+ String newComment = function.comment();
+ for (FunctionChange functionChange : requestBody.changes()) {
+ if (functionChange instanceof
FunctionChange.SetFunctionOption) {
+ FunctionChange.SetFunctionOption setFunctionOption =
+ (FunctionChange.SetFunctionOption)
functionChange;
+ newOptions.put(setFunctionOption.key(),
setFunctionOption.value());
+ } else if (functionChange instanceof
FunctionChange.RemoveFunctionOption) {
+ FunctionChange.RemoveFunctionOption
removeFunctionOption =
+ (FunctionChange.RemoveFunctionOption)
functionChange;
+ newOptions.remove(removeFunctionOption.key());
+ } else if (functionChange instanceof
FunctionChange.UpdateFunctionComment) {
+ FunctionChange.UpdateFunctionComment
updateFunctionComment =
+ (FunctionChange.UpdateFunctionComment)
functionChange;
+ newComment = updateFunctionComment.comment();
+ } else if (functionChange instanceof
FunctionChange.AddDefinition) {
+ FunctionChange.AddDefinition addDefinition =
+ (FunctionChange.AddDefinition) functionChange;
+ if (function.definition(addDefinition.name()) != null)
{
+ throw new Catalog.DefinitionAlreadyExistException(
+ functionName, addDefinition.name());
+ }
+ newDefinitions.put(addDefinition.name(),
addDefinition.definition());
+ } else if (functionChange instanceof
FunctionChange.UpdateDefinition) {
+ FunctionChange.UpdateDefinition updateDefinition =
+ (FunctionChange.UpdateDefinition)
functionChange;
+ if (function.definition(updateDefinition.name()) !=
null) {
+ newDefinitions.put(
+ updateDefinition.name(),
updateDefinition.definition());
+ } else {
+ throw new Catalog.DefinitionNotExistException(
+ functionName, updateDefinition.name());
+ }
+ } else if (functionChange instanceof
FunctionChange.DropDefinition) {
+ FunctionChange.DropDefinition dropDefinition =
+ (FunctionChange.DropDefinition) functionChange;
+ if
(function.definitions().containsKey(dropDefinition.name())) {
+ newDefinitions.remove(dropDefinition.name());
+ } else {
+ throw new Catalog.DefinitionNotExistException(
+ functionName, dropDefinition.name());
+ }
+ }
+ }
+ function =
+ new FunctionImpl(
+ functionName,
+ function.uuid(),
+ function.inputParams(),
+ function.returnParams(),
+ function.isDeterministic(),
+ newDefinitions,
+ newComment,
+ newOptions);
+ functionStore.put(functionName, function);
+ break;
+ default:
+ return new MockResponse().setResponseCode(404);
+ }
+ return new MockResponse().setResponseCode(200);
+ }
+
private MockResponse databasesApiHandler(
String method, String data, Map<String, String> parameters) throws
Exception {
switch (method) {
@@ -733,6 +933,36 @@ public class RESTCatalogServer {
}
}
+ private MockResponse generateFinalListFunctionsResponse(
+ Map<String, String> parameters, List<String> functions) {
+ RESTResponse response;
+ if (!functions.isEmpty()) {
+ int maxResults;
+ try {
+ maxResults = getMaxResults(parameters);
+ } catch (Exception e) {
+ LOG.error(
+ "parse maxResults {} to int failed",
+ parameters.getOrDefault(MAX_RESULTS, null));
+ return mockResponse(
+ new ErrorResponse(
+ ErrorResponse.RESOURCE_TYPE_TABLE,
+ null,
+ "invalid input queryParameter maxResults"
+ + parameters.get(MAX_RESULTS),
+ 400),
+ 400);
+ }
+ String pageToken = parameters.getOrDefault(PAGE_TOKEN, null);
+ PagedList<String> pagedDbs = buildPagedEntities(functions,
maxResults, pageToken);
+ response =
+ new ListFunctionsResponse(pagedDbs.getElements(),
pagedDbs.getNextPageToken());
+ } else {
+ response = new ListFunctionsResponse(new ArrayList<>(), null);
+ }
+ return mockResponse(response, 200);
+ }
+
private MockResponse generateFinalListDatabasesResponse(
Map<String, String> parameters, List<String> databases) {
RESTResponse response;
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
index fe3e4cbcfb..29632df4a4 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
@@ -27,6 +27,9 @@ import org.apache.paimon.catalog.PropertyChange;
import org.apache.paimon.data.BinaryString;
import org.apache.paimon.data.GenericRow;
import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.function.Function;
+import org.apache.paimon.function.FunctionChange;
+import org.apache.paimon.function.FunctionDefinition;
import org.apache.paimon.options.Options;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.partition.PartitionStatistics;
@@ -1311,6 +1314,105 @@ public abstract class RESTCatalogTest extends
CatalogTestBase {
false));
}
+ @Test
+ void testFunction() throws Exception {
+ Function function = MockRESTMessage.function("function");
+
+ catalog.createFunction(function.name(), function, true);
+ assertThrows(
+ Catalog.FunctionAlreadyExistException.class,
+ () -> catalog.createFunction(function.name(), function,
false));
+
+ assertThat(catalog.listFunctions().contains(function.name())).isTrue();
+
+ Function getFunction = catalog.getFunction(function.name());
+ assertThat(getFunction.name()).isEqualTo(function.name());
+ for (String dialect : function.definitions().keySet()) {
+
assertThat(getFunction.definition(dialect)).isEqualTo(function.definition(dialect));
+ }
+ catalog.dropFunction(function.name(), true);
+
+
assertThat(catalog.listFunctions().contains(function.name())).isFalse();
+ assertThrows(
+ Catalog.FunctionNotExistException.class,
+ () -> catalog.dropFunction(function.name(), false));
+ assertThrows(
+ Catalog.FunctionNotExistException.class,
+ () -> catalog.getFunction(function.name()));
+ }
+
+ @Test
+ void testAlterFunction() throws Exception {
+ String functionName = "alter_function_name";
+ Function function = MockRESTMessage.function(functionName);
+ FunctionDefinition definition = FunctionDefinition.sql("x * y + 1");
+ FunctionChange.AddDefinition addDefinition =
+ (FunctionChange.AddDefinition)
FunctionChange.addDefinition("flink_1", definition);
+ assertDoesNotThrow(
+ () -> catalog.alterFunction(functionName,
ImmutableList.of(addDefinition), true));
+ assertThrows(
+ Catalog.FunctionNotExistException.class,
+ () -> catalog.alterFunction(functionName,
ImmutableList.of(addDefinition), false));
+ catalog.createFunction(function.name(), function, true);
+ // set options
+ String key = UUID.randomUUID().toString();
+ String value = UUID.randomUUID().toString();
+ FunctionChange setOption = FunctionChange.setOption(key, value);
+ catalog.alterFunction(functionName, ImmutableList.of(setOption),
false);
+ Function catalogFunction = catalog.getFunction(functionName);
+ assertThat(catalogFunction.options().get(key)).isEqualTo(value);
+
+ // remove options
+ catalog.alterFunction(
+ functionName,
ImmutableList.of(FunctionChange.removeOption(key)), false);
+ catalogFunction = catalog.getFunction(functionName);
+
assertThat(catalogFunction.options().containsKey(key)).isEqualTo(false);
+
+ // update comment
+ String newComment = "new comment";
+ catalog.alterFunction(
+ functionName,
ImmutableList.of(FunctionChange.updateComment(newComment)), false);
+ catalogFunction = catalog.getFunction(functionName);
+ assertThat(catalogFunction.comment()).isEqualTo(newComment);
+ // add definition
+ catalog.alterFunction(functionName, ImmutableList.of(addDefinition),
false);
+ catalogFunction = catalog.getFunction(functionName);
+ assertThat(catalogFunction.definition(addDefinition.name()))
+ .isEqualTo(addDefinition.definition());
+ assertThrows(
+ Catalog.DefinitionAlreadyExistException.class,
+ () -> catalog.alterFunction(functionName,
ImmutableList.of(addDefinition), false));
+
+ // update definition
+ FunctionChange.UpdateDefinition updateDefinition =
+ (FunctionChange.UpdateDefinition)
+ FunctionChange.updateDefinition("flink_1", definition);
+ catalog.alterFunction(functionName,
ImmutableList.of(updateDefinition), false);
+ catalogFunction = catalog.getFunction(functionName);
+ assertThat(catalogFunction.definition(updateDefinition.name()))
+ .isEqualTo(updateDefinition.definition());
+ assertThrows(
+ Catalog.DefinitionNotExistException.class,
+ () ->
+ catalog.alterFunction(
+ functionName,
+ ImmutableList.of(
+
FunctionChange.updateDefinition("no_exist", definition)),
+ false));
+
+ // drop dialect
+ FunctionChange.DropDefinition dropDefinition =
+ (FunctionChange.DropDefinition)
+ FunctionChange.dropDefinition(updateDefinition.name());
+ catalog.alterFunction(functionName, ImmutableList.of(dropDefinition),
false);
+ catalogFunction = catalog.getFunction(functionName);
+
assertThat(catalogFunction.definition(updateDefinition.name())).isNull();
+
+ assertThrows(
+ Catalog.DefinitionNotExistException.class,
+ () -> catalog.alterFunction(functionName,
ImmutableList.of(dropDefinition), false));
+ }
+
@Test
void testTableAuth() throws Exception {
Identifier identifier = Identifier.create("test_table_db",
"auth_table");
diff --git
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
index c395d2e303..9a36191d09 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
@@ -19,9 +19,11 @@
package org.apache.paimon.rest;
import org.apache.paimon.rest.requests.AlterDatabaseRequest;
+import org.apache.paimon.rest.requests.AlterFunctionRequest;
import org.apache.paimon.rest.requests.AlterTableRequest;
import org.apache.paimon.rest.requests.AlterViewRequest;
import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.CreateFunctionRequest;
import org.apache.paimon.rest.requests.CreateTableRequest;
import org.apache.paimon.rest.requests.CreateViewRequest;
import org.apache.paimon.rest.requests.RenameTableRequest;
@@ -30,6 +32,7 @@ import org.apache.paimon.rest.responses.AlterDatabaseResponse;
import org.apache.paimon.rest.responses.ConfigResponse;
import org.apache.paimon.rest.responses.ErrorResponse;
import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetFunctionResponse;
import org.apache.paimon.rest.responses.GetTableResponse;
import org.apache.paimon.rest.responses.GetTableTokenResponse;
import org.apache.paimon.rest.responses.GetViewResponse;
@@ -42,6 +45,8 @@ import org.apache.paimon.types.DataField;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.IntType;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
+
import org.junit.Test;
import java.util.HashMap;
@@ -280,4 +285,31 @@ public class RESTObjectMapperTest {
assertEquals(parseData.viewChanges().get(i),
request.viewChanges().get(i));
}
}
+
+ @Test
+ public void getFunctionResponseParseTest() throws Exception {
+ GetFunctionResponse response = MockRESTMessage.getFunctionResponse();
+ String responseStr = OBJECT_MAPPER.writeValueAsString(response);
+ GetFunctionResponse parseData =
+ OBJECT_MAPPER.readValue(responseStr,
GetFunctionResponse.class);
+ assertEquals(response.uuid(), parseData.uuid());
+ }
+
+ @Test
+ public void createFunctionRequestParseTest() throws
JsonProcessingException {
+ CreateFunctionRequest request =
MockRESTMessage.createFunctionRequest();
+ String requestStr = OBJECT_MAPPER.writeValueAsString(request);
+ CreateFunctionRequest parseData =
+ OBJECT_MAPPER.readValue(requestStr,
CreateFunctionRequest.class);
+ assertEquals(parseData.name(), request.name());
+ }
+
+ @Test
+ public void alterFunctionRequestParseTest() throws JsonProcessingException
{
+ AlterFunctionRequest request = MockRESTMessage.alterFunctionRequest();
+ String requestStr = OBJECT_MAPPER.writeValueAsString(request);
+ AlterFunctionRequest parseData =
+ OBJECT_MAPPER.readValue(requestStr,
AlterFunctionRequest.class);
+ assertEquals(parseData.changes().size(), request.changes().size());
+ }
}
diff --git a/paimon-open-api/rest-catalog-open-api.yaml
b/paimon-open-api/rest-catalog-open-api.yaml
index 182ebfa31b..78ca769ec6 100644
--- a/paimon-open-api/rest-catalog-open-api.yaml
+++ b/paimon-open-api/rest-catalog-open-api.yaml
@@ -95,8 +95,8 @@ paths:
post:
tags:
- database
- summary: Create Databases
- operationId: createDatabases
+ summary: Create Database
+ operationId: createDatabase
parameters:
- name: prefix
in: path
@@ -1184,6 +1184,152 @@ paths:
$ref: '#/components/responses/ViewAlreadyExistErrorResponse'
"500":
$ref: '#/components/responses/ServerErrorResponse'
+ /v1/{prefix}/functions:
+ get:
+ tags:
+ - function
+ summary: List functions
+ operationId: listFunctions
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: maxResults
+ in: query
+ schema:
+ type: integer
+ format: int32
+ - name: pageToken
+ in: query
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ListFunctionsResponse'
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+ post:
+ tags:
+ - function
+ summary: Create Function
+ operationId: createFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateFunctionRequest'
+ responses:
+ "200":
+ description: Success, no content
+ "400":
+ $ref: '#/components/responses/BadRequestErrorResponse'
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "409":
+ $ref: '#/components/responses/FunctionAlreadyExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+
+ /v1/{prefix}/functions/{function}:
+ get:
+ tags:
+ - function
+ summary: Get function
+ operationId: getFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: function
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GetFunctionResponse'
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/DatabaseNotExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+ post:
+ tags:
+ - function
+ summary: Alter function
+ operationId: alterFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: function
+ in: path
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/AlterFunctionRequest'
+ responses:
+ "200":
+ description: Success, no content
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/FunctionNotExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+ delete:
+ tags:
+ - function
+ summary: Drop function
+ operationId: dropFunction
+ parameters:
+ - name: prefix
+ in: path
+ required: true
+ schema:
+ type: string
+ - name: function
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ "200":
+ description: Success, no content
+ "401":
+ $ref: '#/components/responses/UnauthorizedErrorResponse'
+ "404":
+ $ref: '#/components/responses/FunctionNotExistErrorResponse'
+ "500":
+ $ref: '#/components/responses/ServerErrorResponse'
+
components:
#############################
# Reusable Response Objects #
@@ -1305,6 +1451,20 @@ components:
"resourceName": "view",
"code": 404
}
+ FunctionNotExistErrorResponse:
+ description:
+ Not Found - FunctionNotExistException, the function does not exist
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/responses/ResourceNotExistErrorResponse'
+ example:
+ {
+ "message": "The given function does not exist",
+ "resourceType": "FUNCTION",
+ "resourceName": "function",
+ "code": 404
+ }
ResourceAlreadyExistErrorResponse:
description:
Used for 409 errors.
@@ -1370,6 +1530,19 @@ components:
"resourceName": "view",
"code": 409
}
+ FunctionAlreadyExistErrorResponse:
+ description: Conflict - The view already exists
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/responses/ResourceAlreadyExistErrorResponse'
+ example:
+ {
+ "message": "The given function already exists",
+ "resourceType": "FUNCTION",
+ "resourceName": "function",
+ "code": 409
+ }
ServerErrorResponse:
description:
Used for server 5xx errors.
@@ -1464,6 +1637,179 @@ components:
type: array
items:
$ref: '#/components/schemas/ViewChange'
+ CreateFunctionRequest:
+ type: object
+ properties:
+ name:
+ type: string
+ inputParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ returnParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ deterministic:
+ type: boolean
+ definitions:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/FunctionDefinition"
+ comment:
+ type: string
+ options:
+ type: object
+ additionalProperties:
+ type: string
+ AlterFunctionRequest:
+ type: object
+ properties:
+ changes:
+ type: array
+ items:
+ $ref: '#/components/schemas/FunctionChange'
+ FunctionChange:
+ anyOf:
+ - $ref: '#/components/schemas/SetFunctionOption'
+ - $ref: '#/components/schemas/RemoveFunctionOption'
+ - $ref: '#/components/schemas/UpdateFunctionComment'
+ - $ref: '#/components/schemas/AddDefinition'
+ - $ref: '#/components/schemas/UpdateDefinition'
+ - $ref: '#/components/schemas/DropDefinition'
+ BaseFunctionChange:
+ discriminator:
+ propertyName: action
+ mapping:
+ setOption: '#/components/schemas/SetFunctionOption'
+ removeOption: '#/components/schemas/RemoveFunctionOption'
+ updateComment: '#/components/schemas/UpdateFunctionComment'
+ addDefinition: '#/components/schemas/AddDefinition'
+ updateDefinition: '#/components/schemas/UpdateDefinition'
+ dropDefinition: '#/components/schemas/DropDefinition'
+ type: object
+ required:
+ - action
+ properties:
+ action:
+ type: string
+ SetFunctionOption:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "setOption"
+ key:
+ type: string
+ value:
+ type: string
+ RemoveFunctionOption:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "removeOption"
+ key:
+ type: string
+ UpdateFunctionComment:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "updateComment"
+ comment:
+ type: string
+ AddDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "addDefinition"
+ name:
+ type: string
+ definition:
+ $ref: "#/components/schemas/FunctionDefinition"
+ UpdateDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "updateDefinition"
+ name:
+ type: string
+ definition:
+ $ref: "#/components/schemas/FunctionDefinition"
+ DropDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionChange'
+ properties:
+ action:
+ type: string
+ const: "dropDefinition"
+ name:
+ type: string
+ FunctionDefinition:
+ anyOf:
+ - $ref: '#/components/schemas/FileFunctionDefinition'
+ - $ref: '#/components/schemas/SQLFunctionDefinition'
+ - $ref: '#/components/schemas/LambdaFunctionDefinition'
+ BaseFunctionDefinition:
+ discriminator:
+ propertyName: type
+ mapping:
+ file: '#/components/schemas/FileFunctionDefinition'
+ sql: '#/components/schemas/SQLFunctionDefinition'
+ lambda: '#/components/schemas/LambdaFunctionDefinition'
+ type: object
+ required:
+ - type
+ properties:
+ type:
+ type: string
+ SQLFunctionDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionDefinition'
+ properties:
+ type:
+ type: string
+ const: "sql"
+ definition:
+ type: string
+ FileFunctionDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionDefinition'
+ properties:
+ type:
+ type: string
+ const: "file"
+ fileType:
+ type: string
+ storagePaths:
+ type: array
+ items:
+ type: string
+ language:
+ type: string
+ className:
+ type: string
+ functionName:
+ type: string
+ LambdaFunctionDefinition:
+ allOf:
+ - $ref: '#/components/schemas/BaseFunctionDefinition'
+ properties:
+ type:
+ type: string
+ const: "lambda"
+ definition:
+ type: string
+ language:
+ type: string
ViewChange:
anyOf:
- $ref: '#/components/schemas/SetViewOption'
@@ -1859,13 +2205,13 @@ components:
required:
- type
properties:
- 'type':
+ type:
type: string
SnapshotInstant:
allOf:
- $ref: '#/components/schemas/BaseInstant'
properties:
- 'type':
+ type:
type: string
const: "snapshot"
snapshotId:
@@ -1875,7 +2221,7 @@ components:
allOf:
- $ref: '#/components/schemas/BaseInstant'
properties:
- 'type':
+ type:
type: string
const: "tag"
tagName:
@@ -2139,6 +2485,52 @@ components:
$ref: '#/components/schemas/GetViewResponse'
nextPageToken:
type: string
+ ListFunctionsResponse:
+ type: object
+ properties:
+ functions:
+ type: array
+ items:
+ type: string
+ nextPageToken:
+ type: string
+ GetFunctionResponse:
+ type: object
+ properties:
+ uuid:
+ type: string
+ name:
+ type: string
+ inputParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ returnParams:
+ type: array
+ items:
+ $ref: '#/components/schemas/DataField'
+ deterministic:
+ type: boolean
+ definitions:
+ type: object
+ additionalProperties:
+ $ref: "#/components/schemas/FunctionDefinition"
+ comment:
+ type: string
+ options:
+ type: object
+ additionalProperties:
+ type: string
+ owner:
+ type: string
+ createdAt:
+ format: int64
+ createdBy:
+ type: string
+ updatedAt:
+ format: int64
+ updatedBy:
+ type: string
ViewSchema:
type: object
properties:
diff --git
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
index bcb3f70bc6..5b60d10d75 100644
---
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
+++
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
@@ -20,12 +20,14 @@ package org.apache.paimon.open.api;
import org.apache.paimon.partition.Partition;
import org.apache.paimon.rest.requests.AlterDatabaseRequest;
+import org.apache.paimon.rest.requests.AlterFunctionRequest;
import org.apache.paimon.rest.requests.AlterTableRequest;
import org.apache.paimon.rest.requests.AlterViewRequest;
import org.apache.paimon.rest.requests.AuthTableQueryRequest;
import org.apache.paimon.rest.requests.CommitTableRequest;
import org.apache.paimon.rest.requests.CreateBranchRequest;
import org.apache.paimon.rest.requests.CreateDatabaseRequest;
+import org.apache.paimon.rest.requests.CreateFunctionRequest;
import org.apache.paimon.rest.requests.CreateTableRequest;
import org.apache.paimon.rest.requests.CreateViewRequest;
import org.apache.paimon.rest.requests.ForwardBranchRequest;
@@ -37,12 +39,14 @@ import org.apache.paimon.rest.responses.CommitTableResponse;
import org.apache.paimon.rest.responses.ConfigResponse;
import org.apache.paimon.rest.responses.ErrorResponse;
import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetFunctionResponse;
import org.apache.paimon.rest.responses.GetTableResponse;
import org.apache.paimon.rest.responses.GetTableSnapshotResponse;
import org.apache.paimon.rest.responses.GetTableTokenResponse;
import org.apache.paimon.rest.responses.GetViewResponse;
import org.apache.paimon.rest.responses.ListBranchesResponse;
import org.apache.paimon.rest.responses.ListDatabasesResponse;
+import org.apache.paimon.rest.responses.ListFunctionsResponse;
import org.apache.paimon.rest.responses.ListPartitionsResponse;
import org.apache.paimon.rest.responses.ListTableDetailsResponse;
import org.apache.paimon.rest.responses.ListTablesResponse;
@@ -149,7 +153,7 @@ public class RESTCatalogController {
content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))})
})
@PostMapping("/v1/{prefix}/databases")
- public void createDatabases(
+ public void createDatabase(
@PathVariable String prefix, @RequestBody CreateDatabaseRequest
request) {}
@Operation(
@@ -953,4 +957,135 @@ public class RESTCatalogController {
@PathVariable String database,
@PathVariable String view,
@RequestBody AlterViewRequest request) {}
+
+ @Operation(
+ summary = "List functions",
+ tags = {"function"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "200",
+ content = {
+ @Content(schema = @Schema(implementation =
ListFunctionsResponse.class))
+ }),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))})
+ })
+ @GetMapping("/v1/{prefix}/functions")
+ public ListFunctionsResponse listFunctions(
+ @PathVariable String prefix,
+ @RequestParam(required = false) Integer maxResults,
+ @RequestParam(required = false) String pageToken) {
+ return new ListFunctionsResponse(ImmutableList.of("f1"), null);
+ }
+
+ @Operation(
+ summary = "Get function",
+ tags = {"function"})
+ @ApiResponses({
+ @ApiResponse(
+ responseCode = "200",
+ content = {@Content(schema = @Schema(implementation =
GetFunctionResponse.class))}),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Resource not found",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))})
+ })
+ @GetMapping("/v1/{prefix}/functions/{function}")
+ public GetFunctionResponse getFunction(
+ @PathVariable String prefix, @PathVariable String function) {
+ return new GetFunctionResponse(
+ UUID.randomUUID().toString(),
+ function,
+ ImmutableList.of(),
+ ImmutableList.of(),
+ false,
+ ImmutableMap.of(),
+ null,
+ null,
+ "owner",
+ 1L,
+ "owner",
+ 1L,
+ "owner");
+ }
+
+ @Operation(
+ summary = "Create Function",
+ tags = {"function"})
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Success, no
content"),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Resource has exist",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))})
+ })
+ @PostMapping("/v1/{prefix}/functions")
+ public void createFunction(
+ @PathVariable String prefix, @RequestBody CreateFunctionRequest
request) {}
+
+ @Operation(
+ summary = "Drop function",
+ tags = {"function"})
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Success, no
content"),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Resource not found",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))})
+ })
+ @GetMapping("/v1/{prefix}/functions/{function}")
+ public void dropFunction(@PathVariable String prefix, @PathVariable String
function) {}
+
+ @Operation(
+ summary = "Alter function",
+ tags = {"function"})
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "Success, no
content"),
+ @ApiResponse(
+ responseCode = "401",
+ description = "Unauthorized",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Resource not found",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Resource has exist",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))}),
+ @ApiResponse(
+ responseCode = "500",
+ content = {@Content(schema = @Schema(implementation =
ErrorResponse.class))})
+ })
+ @PostMapping("/v1/{prefix}/functions/{function}")
+ public void alterFunction(
+ @PathVariable String prefix,
+ @PathVariable String function,
+ @RequestBody AlterFunctionRequest request) {}
}