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) {}
 }

Reply via email to