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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 1ed975bdb1 [#9525] feat(api): Add APIs for UDF operations (#9551)
1ed975bdb1 is described below

commit 1ed975bdb1a890828d7f3af1c14bd4b2691b6cf2
Author: mchades <[email protected]>
AuthorDate: Mon Jan 5 20:54:00 2026 +0800

    [#9525] feat(api): Add APIs for UDF operations (#9551)
    
    ### What changes were proposed in this pull request?
    
    Add APIs for UDF operations
    
    ### Why are the changes needed?
    
    Fix: #9525
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    CI pass
---
 .../main/java/org/apache/gravitino/Catalog.java    |   9 +
 .../exceptions/FunctionAlreadyExistsException.java |  50 +++
 .../exceptions/NoSuchFunctionException.java        |  49 +++
 .../exceptions/NoSuchFunctionVersionException.java |  49 +++
 .../org/apache/gravitino/function/Function.java    | 106 ++++++
 .../apache/gravitino/function/FunctionCatalog.java | 157 ++++++++
 .../apache/gravitino/function/FunctionChange.java  | 400 +++++++++++++++++++++
 .../apache/gravitino/function/FunctionColumn.java  | 110 ++++++
 .../gravitino/function/FunctionDefinition.java     |  40 +++
 .../gravitino/function/FunctionDefinitions.java    | 103 ++++++
 .../apache/gravitino/function/FunctionImpl.java    | 118 ++++++
 .../apache/gravitino/function/FunctionImpls.java   | 125 +++++++
 .../apache/gravitino/function/FunctionParam.java   |  57 +++
 .../apache/gravitino/function/FunctionParams.java  | 153 ++++++++
 .../gravitino/function/FunctionResources.java      | 112 ++++++
 .../apache/gravitino/function/FunctionType.java    |  73 ++++
 .../org/apache/gravitino/function/JavaImpl.java    |  87 +++++
 .../org/apache/gravitino/function/PythonImpl.java  | 101 ++++++
 .../org/apache/gravitino/function/SQLImpl.java     |  86 +++++
 19 files changed, 1985 insertions(+)

diff --git a/api/src/main/java/org/apache/gravitino/Catalog.java 
b/api/src/main/java/org/apache/gravitino/Catalog.java
index 680c4c6b1a..ed63285e95 100644
--- a/api/src/main/java/org/apache/gravitino/Catalog.java
+++ b/api/src/main/java/org/apache/gravitino/Catalog.java
@@ -24,6 +24,7 @@ import org.apache.gravitino.annotation.Evolving;
 import org.apache.gravitino.authorization.SupportsRoles;
 import org.apache.gravitino.credential.SupportsCredentials;
 import org.apache.gravitino.file.FilesetCatalog;
+import org.apache.gravitino.function.FunctionCatalog;
 import org.apache.gravitino.messaging.TopicCatalog;
 import org.apache.gravitino.model.ModelCatalog;
 import org.apache.gravitino.policy.SupportsPolicies;
@@ -237,6 +238,14 @@ public interface Catalog extends Auditable {
     throw new UnsupportedOperationException("Catalog does not support model 
operations");
   }
 
+  /**
+   * @return the {@link FunctionCatalog} if the catalog supports function 
operations.
+   * @throws UnsupportedOperationException if the catalog does not support 
function operations.
+   */
+  default FunctionCatalog asFunctionCatalog() throws 
UnsupportedOperationException {
+    throw new UnsupportedOperationException("Catalog does not support function 
operations");
+  }
+
   /**
    * @return the {@link SupportsTags} if the catalog supports tag operations.
    * @throws UnsupportedOperationException if the catalog does not support tag 
operations.
diff --git 
a/api/src/main/java/org/apache/gravitino/exceptions/FunctionAlreadyExistsException.java
 
b/api/src/main/java/org/apache/gravitino/exceptions/FunctionAlreadyExistsException.java
new file mode 100644
index 0000000000..9e2cf69d9a
--- /dev/null
+++ 
b/api/src/main/java/org/apache/gravitino/exceptions/FunctionAlreadyExistsException.java
@@ -0,0 +1,50 @@
+/*
+ * 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.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** Exception thrown when a function with a specified name already exists. */
+public class FunctionAlreadyExistsException extends AlreadyExistsException {
+
+  /**
+   * Constructs a new exception with the specified detail message.
+   *
+   * @param message the detail message.
+   * @param args the arguments to the message.
+   */
+  @FormatMethod
+  public FunctionAlreadyExistsException(@FormatString String message, 
Object... args) {
+    super(message, args);
+  }
+
+  /**
+   * Constructs a new exception with the specified detail message and cause.
+   *
+   * @param cause the cause.
+   * @param message the detail message.
+   * @param args the arguments to the message.
+   */
+  @FormatMethod
+  public FunctionAlreadyExistsException(
+      Throwable cause, @FormatString String message, Object... args) {
+    super(cause, message, args);
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionException.java
 
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionException.java
new file mode 100644
index 0000000000..97a2d64920
--- /dev/null
+++ 
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionException.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.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** Exception thrown when a function with the specified name does not exist. */
+public class NoSuchFunctionException extends NotFoundException {
+
+  /**
+   * Constructs a new exception with the specified detail message.
+   *
+   * @param message the detail message.
+   * @param args the arguments to the message.
+   */
+  @FormatMethod
+  public NoSuchFunctionException(@FormatString String message, Object... args) 
{
+    super(message, args);
+  }
+
+  /**
+   * Constructs a new exception with the specified detail message and cause.
+   *
+   * @param cause the cause.
+   * @param message the detail message.
+   * @param args the arguments to the message.
+   */
+  @FormatMethod
+  public NoSuchFunctionException(Throwable cause, String message, Object... 
args) {
+    super(cause, message, args);
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionVersionException.java
 
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionVersionException.java
new file mode 100644
index 0000000000..7c0f3a22cc
--- /dev/null
+++ 
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionVersionException.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.gravitino.exceptions;
+
+import com.google.errorprone.annotations.FormatMethod;
+import com.google.errorprone.annotations.FormatString;
+
+/** Exception thrown when a function with the specified version does not 
exist. */
+public class NoSuchFunctionVersionException extends NotFoundException {
+
+  /**
+   * Constructs a new exception with the specified detail message.
+   *
+   * @param message the detail message.
+   * @param args the arguments to the message.
+   */
+  @FormatMethod
+  public NoSuchFunctionVersionException(@FormatString String message, 
Object... args) {
+    super(message, args);
+  }
+
+  /**
+   * Constructs a new exception with the specified detail message and cause.
+   *
+   * @param cause the cause.
+   * @param message the detail message.
+   * @param args the arguments to the message.
+   */
+  @FormatMethod
+  public NoSuchFunctionVersionException(Throwable cause, String message, 
Object... args) {
+    super(cause, message, args);
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/Function.java 
b/api/src/main/java/org/apache/gravitino/function/Function.java
new file mode 100644
index 0000000000..7634823adc
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/Function.java
@@ -0,0 +1,106 @@
+/*
+ * 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.gravitino.function;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.rel.types.Type;
+
+/**
+ * An interface representing a user-defined function under a schema {@link 
Namespace}. A function is
+ * a reusable computational unit that can be invoked within queries across 
different compute
+ * engines. Users can register a function in Gravitino to manage the function 
metadata and enable
+ * cross-engine function sharing. The typical use case is to define custom 
business logic once and
+ * reuse it across multiple compute engines like Spark, Trino, and AI engines.
+ *
+ * <p>A function is characterized by its name, type (scalar for row-by-row 
operations, aggregate for
+ * group operations, or table-valued for set-returning operations), whether it 
is deterministic, its
+ * return type or columns (for table function), and its definitions that 
contain parameters and
+ * implementations for different runtime engines. Each function maintains a 
version number starting
+ * from 0, which increments with each alteration.
+ */
+@Evolving
+public interface Function extends Auditable {
+  /** An empty array of {@link FunctionColumn}. */
+  FunctionColumn[] EMPTY = new FunctionColumn[0];
+
+  /**
+   * @return The function name.
+   */
+  String name();
+
+  /**
+   * @return The function type.
+   */
+  FunctionType functionType();
+
+  /**
+   * @return Whether the function is deterministic.
+   */
+  boolean deterministic();
+
+  /**
+   * @return The optional comment of the function.
+   */
+  @Nullable
+  default String comment() {
+    return null;
+  }
+
+  /**
+   * The return type for scalar or aggregate functions.
+   *
+   * @return The return type, null if this is a table-valued function.
+   */
+  @Nullable
+  default Type returnType() {
+    return null;
+  }
+
+  /**
+   * The output columns for a table-valued function.
+   *
+   * <p>A table-valued function is a function that returns a table instead of 
a scalar value or an
+   * aggregate result. The returned table has a fixed schema defined by the 
columns returned from
+   * this method.
+   *
+   * @return The output columns that define the schema of the table returned 
by this function, or an
+   *     empty array if this is a scalar or aggregate function.
+   */
+  default FunctionColumn[] returnColumns() {
+    return EMPTY;
+  }
+
+  /**
+   * @return The definitions of the function.
+   */
+  FunctionDefinition[] definitions();
+
+  /**
+   * Returns the internal revision version of the function.
+   *
+   * <p>This version is a 0-based counter, where {@code 0} represents the 
initial definition of the
+   * function, and the value is incremented by 1 on each later alteration.
+   *
+   * @return The 0-based revision version of the function.
+   */
+  int version();
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionCatalog.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionCatalog.java
new file mode 100644
index 0000000000..73d9ce06ff
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionCatalog.java
@@ -0,0 +1,157 @@
+/*
+ * 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.gravitino.function;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.exceptions.FunctionAlreadyExistsException;
+import org.apache.gravitino.exceptions.NoSuchFunctionException;
+import org.apache.gravitino.exceptions.NoSuchFunctionVersionException;
+import org.apache.gravitino.exceptions.NoSuchSchemaException;
+import org.apache.gravitino.rel.types.Type;
+
+/** The FunctionCatalog interface defines the public API for managing 
functions in a schema. */
+@Evolving
+public interface FunctionCatalog {
+
+  /**
+   * List the functions in a namespace from the catalog.
+   *
+   * @param namespace A namespace.
+   * @return An array of function identifiers in the namespace.
+   * @throws NoSuchSchemaException If the schema does not exist.
+   */
+  NameIdentifier[] listFunctions(Namespace namespace) throws 
NoSuchSchemaException;
+
+  /**
+   * List the functions with details in a namespace from the catalog.
+   *
+   * @param namespace A namespace.
+   * @return An array of functions in the namespace.
+   * @throws NoSuchSchemaException If the schema does not exist.
+   */
+  Function[] listFunctionInfos(Namespace namespace) throws 
NoSuchSchemaException;
+
+  /**
+   * Get a function by {@link NameIdentifier} from the catalog. The identifier 
only contains the
+   * schema and function name. A function may include multiple definitions 
(overloads) in the
+   * result. This method returns the latest version of the function.
+   *
+   * @param ident A function identifier.
+   * @return The latest version of the function with the given name.
+   * @throws NoSuchFunctionException If the function does not exist.
+   */
+  Function getFunction(NameIdentifier ident) throws NoSuchFunctionException;
+
+  /**
+   * Get a function by {@link NameIdentifier} and version from the catalog. 
The identifier only
+   * contains the schema and function name. A function may include multiple 
definitions (overloads)
+   * in the result.
+   *
+   * @param ident A function identifier.
+   * @param version The zero-based function version index (0 for the first 
created version), as
+   *     returned by {@link Function#version()}.
+   * @return The function with the given name and version.
+   * @throws NoSuchFunctionException If the function does not exist.
+   * @throws NoSuchFunctionVersionException If the function version does not 
exist.
+   */
+  Function getFunction(NameIdentifier ident, int version)
+      throws NoSuchFunctionException, NoSuchFunctionVersionException;
+
+  /**
+   * Check if a function with the given name exists in the catalog.
+   *
+   * @param ident The function identifier.
+   * @return True if the function exists, false otherwise.
+   */
+  default boolean functionExists(NameIdentifier ident) {
+    try {
+      getFunction(ident);
+      return true;
+    } catch (NoSuchFunctionException e) {
+      return false;
+    }
+  }
+
+  /**
+   * Register a scalar or aggregate function with one or more definitions 
(overloads).
+   *
+   * @param ident The function identifier.
+   * @param comment The optional function comment.
+   * @param functionType The function type.
+   * @param deterministic Whether the function is deterministic.
+   * @param returnType The return type.
+   * @param definitions The function definitions.
+   * @return The registered function.
+   * @throws NoSuchSchemaException If the schema does not exist.
+   * @throws FunctionAlreadyExistsException If the function already exists.
+   */
+  Function registerFunction(
+      NameIdentifier ident,
+      String comment,
+      FunctionType functionType,
+      boolean deterministic,
+      Type returnType,
+      FunctionDefinition[] definitions)
+      throws NoSuchSchemaException, FunctionAlreadyExistsException;
+
+  /**
+   * Register a table-valued function with one or more definitions (overloads).
+   *
+   * @param ident The function identifier.
+   * @param comment The optional function comment.
+   * @param deterministic Whether the function is deterministic.
+   * @param returnColumns The return columns.
+   * @param definitions The function definitions.
+   * @return The registered function.
+   * @throws NoSuchSchemaException If the schema does not exist.
+   * @throws FunctionAlreadyExistsException If the function already exists.
+   */
+  Function registerFunction(
+      NameIdentifier ident,
+      String comment,
+      boolean deterministic,
+      FunctionColumn[] returnColumns,
+      FunctionDefinition[] definitions)
+      throws NoSuchSchemaException, FunctionAlreadyExistsException;
+
+  /**
+   * Applies {@link FunctionChange changes} to a function in the catalog.
+   *
+   * <p>Implementations may reject the changes. If any change is rejected, no 
changes should be
+   * applied to the function.
+   *
+   * @param ident the {@link NameIdentifier} instance of the function to alter.
+   * @param changes the several {@link FunctionChange} instances to apply to 
the function.
+   * @return the updated {@link Function} instance.
+   * @throws NoSuchFunctionException If the function does not exist.
+   * @throws IllegalArgumentException If the change is rejected by the 
implementation.
+   */
+  Function alterFunction(NameIdentifier ident, FunctionChange... changes)
+      throws NoSuchFunctionException, IllegalArgumentException;
+
+  /**
+   * Drop a function by name.
+   *
+   * @param ident The name identifier of the function.
+   * @return True if the function is deleted, false if the function does not 
exist.
+   */
+  boolean dropFunction(NameIdentifier ident);
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionChange.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionChange.java
new file mode 100644
index 0000000000..5684cafca5
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionChange.java
@@ -0,0 +1,400 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/** Represents a change that can be applied to a function. */
+@Evolving
+public interface FunctionChange {
+  /** An empty array of parameters. */
+  FunctionParam[] EMPTY_PARAMS = new FunctionParam[0];
+
+  /**
+   * Create a {@link FunctionChange} to update the comment of a function.
+   *
+   * @param newComment The new comment value.
+   * @return The change instance.
+   */
+  static FunctionChange updateComment(String newComment) {
+    return new UpdateComment(newComment);
+  }
+
+  /**
+   * Create a {@link FunctionChange} to add a new definition (overload) to a 
function.
+   *
+   * @param definition The new definition to add.
+   * @return The change instance.
+   */
+  static FunctionChange addDefinition(FunctionDefinition definition) {
+    return new AddDefinition(definition);
+  }
+
+  /**
+   * Create a {@link FunctionChange} to remove an existing definition 
(overload) from a function.
+   *
+   * @param parameters The parameters that identify the definition to remove.
+   * @return The change instance.
+   */
+  static FunctionChange removeDefinition(FunctionParam[] parameters) {
+    return new RemoveDefinition(parameters);
+  }
+
+  /**
+   * Create a {@link FunctionChange} to add an implementation to a specific 
definition.
+   *
+   * @param parameters The parameters that identify the definition to update.
+   * @param implementation The implementation to add.
+   * @return The change instance.
+   */
+  static FunctionChange addImpl(FunctionParam[] parameters, FunctionImpl 
implementation) {
+    return new AddImpl(parameters, implementation);
+  }
+
+  /**
+   * Create a {@link FunctionChange} to update an implementation for a 
specific definition and
+   * runtime.
+   *
+   * @param parameters The parameters that identify the definition to update.
+   * @param runtime The runtime that identifies the implementation to replace.
+   * @param implementation The new implementation.
+   * @return The change instance.
+   */
+  static FunctionChange updateImpl(
+      FunctionParam[] parameters, FunctionImpl.RuntimeType runtime, 
FunctionImpl implementation) {
+    return new UpdateImpl(parameters, runtime, implementation);
+  }
+
+  /**
+   * Create a {@link FunctionChange} to remove an implementation for a 
specific definition and
+   * runtime.
+   *
+   * @param parameters The parameters that identify the definition to update.
+   * @param runtime The runtime that identifies the implementation to remove.
+   * @return The change instance.
+   */
+  static FunctionChange removeImpl(FunctionParam[] parameters, 
FunctionImpl.RuntimeType runtime) {
+    return new RemoveImpl(parameters, runtime);
+  }
+
+  /** A {@link FunctionChange} to update the comment of a function. */
+  final class UpdateComment implements FunctionChange {
+    private final String newComment;
+
+    UpdateComment(String newComment) {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(newComment), "New comment cannot be null or 
empty");
+      this.newComment = newComment;
+    }
+
+    /**
+     * @return The new comment of the function.
+     */
+    public String newComment() {
+      return newComment;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof UpdateComment)) {
+        return false;
+      }
+      UpdateComment that = (UpdateComment) obj;
+      return Objects.equals(newComment, that.newComment);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(newComment);
+    }
+
+    @Override
+    public String toString() {
+      return "UpdateComment{newComment='" + newComment + "'}";
+    }
+  }
+
+  /** A {@link FunctionChange} to add a new definition to a function. */
+  final class AddDefinition implements FunctionChange {
+    private final FunctionDefinition definition;
+
+    AddDefinition(FunctionDefinition definition) {
+      Preconditions.checkArgument(definition != null, "Definition cannot be 
null");
+      this.definition = definition;
+    }
+
+    /**
+     * @return The definition to add.
+     */
+    public FunctionDefinition definition() {
+      return definition;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof AddDefinition)) {
+        return false;
+      }
+      AddDefinition that = (AddDefinition) obj;
+      return Objects.equals(definition, that.definition);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(definition);
+    }
+
+    @Override
+    public String toString() {
+      return "AddDefinition{definition=" + definition + '}';
+    }
+  }
+
+  /** A {@link FunctionChange} to remove an existing definition from a 
function. */
+  final class RemoveDefinition implements FunctionChange {
+    private final FunctionParam[] parameters;
+
+    RemoveDefinition(FunctionParam[] parameters) {
+      Preconditions.checkArgument(parameters != null, "Parameters cannot be 
null");
+      this.parameters = Arrays.copyOf(parameters, parameters.length);
+    }
+
+    /**
+     * @return The parameters that identify the definition to remove.
+     */
+    public FunctionParam[] parameters() {
+      return parameters.length == 0 ? EMPTY_PARAMS : Arrays.copyOf(parameters, 
parameters.length);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof RemoveDefinition)) {
+        return false;
+      }
+      RemoveDefinition that = (RemoveDefinition) obj;
+      return Arrays.equals(parameters, that.parameters);
+    }
+
+    @Override
+    public int hashCode() {
+      return Arrays.hashCode(parameters);
+    }
+
+    @Override
+    public String toString() {
+      return "RemoveDefinition{parameters=" + Arrays.toString(parameters) + 
'}';
+    }
+  }
+
+  /** A {@link FunctionChange} to add an implementation to a definition. */
+  final class AddImpl implements FunctionChange {
+    private final FunctionParam[] parameters;
+    private final FunctionImpl implementation;
+
+    AddImpl(FunctionParam[] parameters, FunctionImpl implementation) {
+      Preconditions.checkArgument(parameters != null, "Parameters cannot be 
null");
+      this.parameters = Arrays.copyOf(parameters, parameters.length);
+      Preconditions.checkArgument(implementation != null, "Implementation 
cannot be null");
+      this.implementation = implementation;
+    }
+
+    /**
+     * @return The parameters that identify the definition to update.
+     */
+    public FunctionParam[] parameters() {
+      return parameters.length == 0 ? EMPTY_PARAMS : Arrays.copyOf(parameters, 
parameters.length);
+    }
+
+    /**
+     * @return The implementation to add.
+     */
+    public FunctionImpl implementation() {
+      return implementation;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof AddImpl)) {
+        return false;
+      }
+      AddImpl that = (AddImpl) obj;
+      return Arrays.equals(parameters, that.parameters)
+          && Objects.equals(implementation, that.implementation);
+    }
+
+    @Override
+    public int hashCode() {
+      int result = Arrays.hashCode(parameters);
+      result = 31 * result + Objects.hashCode(implementation);
+      return result;
+    }
+
+    @Override
+    public String toString() {
+      return "AddImpl{parameters="
+          + Arrays.toString(parameters)
+          + ", implementation="
+          + implementation
+          + '}';
+    }
+  }
+
+  /**
+   * A {@link FunctionChange} to replace an implementation (identified by 
runtime) for a specific
+   * definition.
+   */
+  final class UpdateImpl implements FunctionChange {
+    private final FunctionParam[] parameters;
+    private final FunctionImpl.RuntimeType runtime;
+    private final FunctionImpl implementation;
+
+    UpdateImpl(
+        FunctionParam[] parameters, FunctionImpl.RuntimeType runtime, 
FunctionImpl implementation) {
+      Preconditions.checkArgument(parameters != null, "Parameters cannot be 
null");
+      this.parameters = Arrays.copyOf(parameters, parameters.length);
+      Preconditions.checkArgument(runtime != null, "Runtime cannot be null");
+      this.runtime = runtime;
+      Preconditions.checkArgument(implementation != null, "Implementation 
cannot be null");
+      this.implementation = implementation;
+      Preconditions.checkArgument(
+          runtime == implementation.runtime(),
+          "Runtime of implementation must match the runtime being updated");
+    }
+
+    /**
+     * @return The parameters that identify the definition to update.
+     */
+    public FunctionParam[] parameters() {
+      return parameters.length == 0 ? EMPTY_PARAMS : Arrays.copyOf(parameters, 
parameters.length);
+    }
+
+    /**
+     * @return The runtime that identifies the implementation to replace.
+     */
+    public FunctionImpl.RuntimeType runtime() {
+      return runtime;
+    }
+
+    /**
+     * @return The new implementation.
+     */
+    public FunctionImpl implementation() {
+      return implementation;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof UpdateImpl)) {
+        return false;
+      }
+      UpdateImpl that = (UpdateImpl) obj;
+      return Arrays.equals(parameters, that.parameters)
+          && runtime == that.runtime
+          && Objects.equals(implementation, that.implementation);
+    }
+
+    @Override
+    public int hashCode() {
+      int result = Arrays.hashCode(parameters);
+      result = 31 * result + Objects.hash(runtime, implementation);
+      return result;
+    }
+
+    @Override
+    public String toString() {
+      return "UpdateImpl{parameters="
+          + Arrays.toString(parameters)
+          + ", runtime="
+          + runtime
+          + ", implementation="
+          + implementation
+          + '}';
+    }
+  }
+
+  /** A {@link FunctionChange} to remove an implementation for a specific 
runtime. */
+  final class RemoveImpl implements FunctionChange {
+    private final FunctionParam[] parameters;
+    private final FunctionImpl.RuntimeType runtime;
+
+    RemoveImpl(FunctionParam[] parameters, FunctionImpl.RuntimeType runtime) {
+      Preconditions.checkArgument(parameters != null, "Parameters cannot be 
null");
+      this.parameters = Arrays.copyOf(parameters, parameters.length);
+      Preconditions.checkArgument(runtime != null, "Runtime cannot be null");
+      this.runtime = runtime;
+    }
+
+    /**
+     * @return The parameters that identify the definition to update.
+     */
+    public FunctionParam[] parameters() {
+      return parameters.length == 0 ? EMPTY_PARAMS : Arrays.copyOf(parameters, 
parameters.length);
+    }
+
+    /**
+     * @return The runtime that identifies the implementation to remove.
+     */
+    public FunctionImpl.RuntimeType runtime() {
+      return runtime;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof RemoveImpl)) {
+        return false;
+      }
+      RemoveImpl that = (RemoveImpl) obj;
+      return Arrays.equals(parameters, that.parameters) && runtime == 
that.runtime;
+    }
+
+    @Override
+    public int hashCode() {
+      int result = Arrays.hashCode(parameters);
+      result = 31 * result + Objects.hashCode(runtime);
+      return result;
+    }
+
+    @Override
+    public String toString() {
+      return "RemoveImpl{parameters=" + Arrays.toString(parameters) + ", 
runtime=" + runtime + '}';
+    }
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionColumn.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionColumn.java
new file mode 100644
index 0000000000..022bf78ff0
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionColumn.java
@@ -0,0 +1,110 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.rel.types.Type;
+
+/** Represents a return column of a table-valued function. */
+@Evolving
+public class FunctionColumn {
+  private final String name;
+  private final Type dataType;
+  private final String comment;
+
+  private FunctionColumn(String name, Type dataType, String comment) {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(name), "Function column name cannot be null or 
empty");
+    this.name = name;
+    Preconditions.checkArgument(dataType != null, "Function column type cannot 
be null");
+    this.dataType = dataType;
+    this.comment = comment;
+  }
+
+  /**
+   * Create a {@link FunctionColumn} instance.
+   *
+   * @param name The column name.
+   * @param dataType The column type.
+   * @param comment The optional comment of the column.
+   * @return A {@link FunctionColumn} instance.
+   */
+  public static FunctionColumn of(String name, Type dataType, String comment) {
+    return new FunctionColumn(name, dataType, comment);
+  }
+
+  /**
+   * @return The column name.
+   */
+  public String name() {
+    return name;
+  }
+
+  /**
+   * @return The column type.
+   */
+  public Type dataType() {
+    return dataType;
+  }
+
+  /**
+   * @return The optional column comment, null if not provided.
+   */
+  @Nullable
+  public String comment() {
+    return comment;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof FunctionColumn)) {
+      return false;
+    }
+    FunctionColumn that = (FunctionColumn) obj;
+    return Objects.equals(name, that.name)
+        && Objects.equals(dataType, that.dataType)
+        && Objects.equals(comment, that.comment);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, dataType, comment);
+  }
+
+  @Override
+  public String toString() {
+    return "FunctionColumn{"
+        + "name='"
+        + name
+        + '\''
+        + ", dataType="
+        + dataType
+        + ", comment='"
+        + comment
+        + '\''
+        + '}';
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionDefinition.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionDefinition.java
new file mode 100644
index 0000000000..c7ea9562d4
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionDefinition.java
@@ -0,0 +1,40 @@
+/*
+ * 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.gravitino.function;
+
+import org.apache.gravitino.annotation.Evolving;
+
+/**
+ * A function definition that pairs a specific parameter list with its 
implementations. A single
+ * function can include multiple definitions (overloads), each with distinct 
parameters and
+ * implementations.
+ */
+@Evolving
+public interface FunctionDefinition {
+
+  /**
+   * @return The parameters for this definition. Maybe an empty array for a 
no-arg definition.
+   */
+  FunctionParam[] parameters();
+
+  /**
+   * @return The implementations associated with this definition.
+   */
+  FunctionImpl[] impls();
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionDefinitions.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionDefinitions.java
new file mode 100644
index 0000000000..af0c4472b5
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionDefinitions.java
@@ -0,0 +1,103 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import org.apache.commons.lang3.ArrayUtils;
+
+/** Helper methods to create {@link FunctionDefinition} instances. */
+public final class FunctionDefinitions {
+
+  private FunctionDefinitions() {}
+
+  /**
+   * Create an array of {@link FunctionDefinition} instances.
+   *
+   * @param definitions The function definitions.
+   * @return An array of {@link FunctionDefinition} instances.
+   */
+  public static FunctionDefinition[] of(FunctionDefinition... definitions) {
+    Preconditions.checkArgument(
+        ArrayUtils.isNotEmpty(definitions), "Definitions cannot be null or 
empty");
+    return Arrays.copyOf(definitions, definitions.length);
+  }
+
+  /**
+   * Create a {@link FunctionDefinition} instance.
+   *
+   * @param parameters The parameters for this definition, it may be null or 
empty.
+   * @param impls The implementations for this definition, it must not be null 
or empty.
+   * @return A {@link FunctionDefinition} instance.
+   */
+  public static FunctionDefinition of(FunctionParam[] parameters, 
FunctionImpl[] impls) {
+    return new FunctionDefinitionImpl(parameters, impls);
+  }
+
+  private static final class FunctionDefinitionImpl implements 
FunctionDefinition {
+    private final FunctionParam[] parameters;
+    private final FunctionImpl[] impls;
+
+    FunctionDefinitionImpl(FunctionParam[] parameters, FunctionImpl[] impls) {
+      this.parameters =
+          parameters == null ? new FunctionParam[0] : 
Arrays.copyOf(parameters, parameters.length);
+      Preconditions.checkArgument(
+          impls != null && impls.length > 0, "Impls cannot be null or empty");
+      this.impls = Arrays.copyOf(impls, impls.length);
+    }
+
+    @Override
+    public FunctionParam[] parameters() {
+      return Arrays.copyOf(parameters, parameters.length);
+    }
+
+    @Override
+    public FunctionImpl[] impls() {
+      return Arrays.copyOf(impls, impls.length);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof FunctionDefinition)) {
+        return false;
+      }
+      FunctionDefinition that = (FunctionDefinition) obj;
+      return Arrays.equals(parameters, that.parameters()) && 
Arrays.equals(impls, that.impls());
+    }
+
+    @Override
+    public int hashCode() {
+      int result = Arrays.hashCode(parameters);
+      result = 31 * result + Arrays.hashCode(impls);
+      return result;
+    }
+
+    @Override
+    public String toString() {
+      return "FunctionDefinition{parameters="
+          + Arrays.toString(parameters)
+          + ", impls="
+          + Arrays.toString(impls)
+          + '}';
+    }
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/FunctionImpl.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionImpl.java
new file mode 100644
index 0000000000..58d283e342
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionImpl.java
@@ -0,0 +1,118 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/**
+ * Base class of function implementations.
+ *
+ * <p>A function implementation must declare its language and optional 
external resources. Concrete
+ * implementations are provided by {@link SQLImpl}, {@link JavaImpl}, and 
{@link PythonImpl}.
+ */
+@Evolving
+public abstract class FunctionImpl {
+  /** Supported implementation languages. */
+  public enum Language {
+    /** SQL implementation. */
+    SQL,
+    /** Java implementation. */
+    JAVA,
+    /** Python implementation. */
+    PYTHON
+  }
+
+  /** Supported execution runtimes for function implementations. */
+  public enum RuntimeType {
+    /** Spark runtime. */
+    SPARK,
+    /** Trino runtime. */
+    TRINO;
+
+    /**
+     * Parse a runtime value from string.
+     *
+     * @param value Runtime name.
+     * @return Parsed runtime.
+     * @throws IllegalArgumentException If the runtime is not supported.
+     */
+    public static RuntimeType fromString(String value) {
+      Preconditions.checkArgument(StringUtils.isNotBlank(value), "Function 
runtime must be set");
+      return RuntimeType.valueOf(value.trim().toUpperCase());
+    }
+  }
+
+  private final Language language;
+  private final RuntimeType runtime;
+  private final FunctionResources resources;
+  private final Map<String, String> properties;
+
+  /**
+   * Construct a {@link FunctionImpl}.
+   *
+   * @param language The language of the function implementation.
+   * @param runtime The runtime of the function implementation.
+   * @param resources The resources required by the function implementation.
+   * @param properties The properties of the function implementation.
+   */
+  protected FunctionImpl(
+      Language language,
+      RuntimeType runtime,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    Preconditions.checkArgument(language != null, "Function implementation 
language must be set");
+    this.language = language;
+    Preconditions.checkArgument(runtime != null, "Function runtime must be 
set");
+    this.runtime = runtime;
+    this.resources = resources == null ? FunctionResources.empty() : resources;
+    this.properties = properties == null ? ImmutableMap.of() : 
ImmutableMap.copyOf(properties);
+  }
+
+  /**
+   * @return The implementation language.
+   */
+  public Language language() {
+    return language;
+  }
+
+  /**
+   * @return The target runtime.
+   */
+  public RuntimeType runtime() {
+    return runtime;
+  }
+
+  /**
+   * @return The external resources required by this implementation.
+   */
+  public FunctionResources resources() {
+    return resources;
+  }
+
+  /**
+   * @return The additional properties of this implementation.
+   */
+  public Map<String, String> properties() {
+    return properties;
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/FunctionImpls.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionImpls.java
new file mode 100644
index 0000000000..495a7ab099
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionImpls.java
@@ -0,0 +1,125 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import java.util.Map;
+import org.apache.commons.lang3.ArrayUtils;
+
+/** Helper methods to create {@link FunctionImpl} instances. */
+public class FunctionImpls {
+
+  /**
+   * Copy an array of function implementations.
+   *
+   * @param impls The function implementations.
+   * @return A copy of the input array.
+   */
+  public static FunctionImpl[] of(FunctionImpl... impls) {
+    Preconditions.checkArgument(ArrayUtils.isNotEmpty(impls), "Impls cannot be 
null or empty");
+    return Arrays.copyOf(impls, impls.length);
+  }
+
+  /**
+   * Create a SQL implementation.
+   *
+   * @param runtime Target runtime.
+   * @param sql SQL text body.
+   * @return A {@link SQLImpl} instance.
+   */
+  public static SQLImpl ofSql(FunctionImpl.RuntimeType runtime, String sql) {
+    return ofSql(runtime, sql, null, null);
+  }
+
+  /**
+   * Create a SQL implementation.
+   *
+   * @param runtime Target runtime.
+   * @param sql SQL text body.
+   * @param resources External resources required by the implementation.
+   * @param properties Additional implementation properties.
+   * @return A {@link SQLImpl} instance.
+   */
+  public static SQLImpl ofSql(
+      FunctionImpl.RuntimeType runtime,
+      String sql,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    return new SQLImpl(runtime, sql, resources, properties);
+  }
+
+  /**
+   * Create a Java implementation.
+   *
+   * @param runtime Target runtime.
+   * @param className Fully qualified class name.
+   * @return A {@link JavaImpl} instance.
+   */
+  public static JavaImpl ofJava(FunctionImpl.RuntimeType runtime, String 
className) {
+    return ofJava(runtime, className, null, null);
+  }
+
+  /**
+   * Create a Java implementation.
+   *
+   * @param runtime Target runtime.
+   * @param className Fully qualified class name.
+   * @param resources External resources required by the implementation.
+   * @param properties Additional implementation properties.
+   * @return A {@link JavaImpl} instance.
+   */
+  public static JavaImpl ofJava(
+      FunctionImpl.RuntimeType runtime,
+      String className,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    return new JavaImpl(runtime, className, resources, properties);
+  }
+
+  /**
+   * Create a Python implementation.
+   *
+   * @param runtime Target runtime.
+   * @param handler Python handler entrypoint.
+   * @return A {@link PythonImpl} instance.
+   */
+  public static PythonImpl ofPython(FunctionImpl.RuntimeType runtime, String 
handler) {
+    return ofPython(runtime, handler, null, null, null);
+  }
+
+  /**
+   * Create a Python implementation.
+   *
+   * @param runtime Target runtime.
+   * @param handler Python handler entrypoint.
+   * @param codeBlock Inline code block for the handler.
+   * @param resources External resources required by the implementation.
+   * @param properties Additional implementation properties.
+   * @return A {@link PythonImpl} instance.
+   */
+  public static PythonImpl ofPython(
+      FunctionImpl.RuntimeType runtime,
+      String handler,
+      String codeBlock,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    return new PythonImpl(runtime, handler, codeBlock, resources, properties);
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/FunctionParam.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionParam.java
new file mode 100644
index 0000000000..e0484451be
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionParam.java
@@ -0,0 +1,57 @@
+/*
+ * 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.gravitino.function;
+
+import static org.apache.gravitino.rel.Column.DEFAULT_VALUE_NOT_SET;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.annotation.Evolving;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
+
+/** Represents a function parameter. */
+@Evolving
+public interface FunctionParam {
+
+  /**
+   * @return The name of the parameter.
+   */
+  String name();
+
+  /**
+   * @return The data type of the parameter.
+   */
+  Type dataType();
+
+  /**
+   * @return The optional comment of the parameter, null if not provided.
+   */
+  @Nullable
+  default String comment() {
+    return null;
+  }
+
+  /**
+   * @return The default value of the parameter if provided, otherwise {@link
+   *     org.apache.gravitino.rel.Column#DEFAULT_VALUE_NOT_SET}.
+   */
+  default Expression defaultValue() {
+    return DEFAULT_VALUE_NOT_SET;
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionParams.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionParams.java
new file mode 100644
index 0000000000..743b99c88d
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionParams.java
@@ -0,0 +1,153 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.expressions.Expression;
+import org.apache.gravitino.rel.types.Type;
+
+/** Helper methods to create {@link FunctionParam} instances. */
+public class FunctionParams {
+
+  private FunctionParams() {}
+
+  /**
+   * Create a copy of the given array of {@link FunctionParam} instances.
+   *
+   * @param params The array of parameters.
+   * @return A copy of the given array of {@link FunctionParam} instances.
+   */
+  public static FunctionParam[] of(FunctionParam... params) {
+    return params.length == 0 ? new FunctionParam[0] : Arrays.copyOf(params, 
params.length);
+  }
+
+  /**
+   * Create a {@link FunctionParam} instance.
+   *
+   * @param name The parameter name.
+   * @param dataType The parameter type.
+   * @return A {@link FunctionParam} instance.
+   */
+  public static FunctionParam of(String name, Type dataType) {
+    return of(name, dataType, null, Column.DEFAULT_VALUE_NOT_SET);
+  }
+
+  /**
+   * Create a {@link FunctionParam} instance with an optional comment.
+   *
+   * @param name The parameter name.
+   * @param dataType The parameter type.
+   * @param comment The optional comment.
+   * @return A {@link FunctionParam} instance.
+   */
+  public static FunctionParam of(String name, Type dataType, String comment) {
+    return of(name, dataType, comment, Column.DEFAULT_VALUE_NOT_SET);
+  }
+
+  /**
+   * Create a {@link FunctionParam} instance with an optional comment and 
default value.
+   *
+   * @param name The parameter name.
+   * @param dataType The parameter type.
+   * @param comment The optional comment.
+   * @param defaultValue The optional default value expression.
+   * @return A {@link FunctionParam} instance.
+   */
+  public static FunctionParam of(
+      String name, Type dataType, String comment, Expression defaultValue) {
+    return new FunctionParamImpl(name, dataType, comment, defaultValue);
+  }
+
+  private static final class FunctionParamImpl implements FunctionParam {
+    private final String name;
+    private final Type dataType;
+    private final String comment;
+    private final Expression defaultValue;
+
+    private FunctionParamImpl(String name, Type dataType, String comment, 
Expression defaultValue) {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(name), "Parameter name cannot be null or 
empty");
+      Preconditions.checkArgument(dataType != null, "Parameter data type 
cannot be null");
+      this.name = name;
+      this.dataType = dataType;
+      this.comment = comment;
+      this.defaultValue = defaultValue == null ? Column.DEFAULT_VALUE_NOT_SET 
: defaultValue;
+    }
+
+    @Override
+    public String name() {
+      return name;
+    }
+
+    @Override
+    public Type dataType() {
+      return dataType;
+    }
+
+    @Override
+    public String comment() {
+      return comment;
+    }
+
+    @Override
+    public Expression defaultValue() {
+      return defaultValue;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (!(obj instanceof FunctionParam)) {
+        return false;
+      }
+      FunctionParam that = (FunctionParam) obj;
+      return Objects.equals(name, that.name())
+          && Objects.equals(dataType, that.dataType())
+          && Objects.equals(comment, that.comment())
+          && Objects.equals(defaultValue, that.defaultValue());
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(name, dataType, comment, defaultValue);
+    }
+
+    @Override
+    public String toString() {
+      return "FunctionParam{"
+          + "name='"
+          + name
+          + '\''
+          + ", dataType="
+          + dataType
+          + ", comment='"
+          + comment
+          + '\''
+          + ", defaultValue="
+          + defaultValue
+          + '}';
+    }
+  }
+}
diff --git 
a/api/src/main/java/org/apache/gravitino/function/FunctionResources.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionResources.java
new file mode 100644
index 0000000000..6fa49ec3da
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionResources.java
@@ -0,0 +1,112 @@
+/*
+ * 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.gravitino.function;
+
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.gravitino.annotation.Evolving;
+
+/** Represents external resources that are required by a function 
implementation. */
+@Evolving
+public class FunctionResources {
+  private static final String[] EMPTY = new String[0];
+
+  private final String[] jars;
+  private final String[] files;
+  private final String[] archives;
+
+  private FunctionResources(String[] jars, String[] files, String[] archives) {
+    this.jars = jars == null ? EMPTY : Arrays.copyOf(jars, jars.length);
+    this.files = files == null ? EMPTY : Arrays.copyOf(files, files.length);
+    this.archives = archives == null ? EMPTY : Arrays.copyOf(archives, 
archives.length);
+  }
+
+  /**
+   * @return An empty {@link FunctionResources} instance.
+   */
+  public static FunctionResources empty() {
+    return new FunctionResources(EMPTY, EMPTY, EMPTY);
+  }
+
+  /**
+   * Create a {@link FunctionResources} instance.
+   *
+   * @param jars The jar resources.
+   * @param files The file resources.
+   * @param archives The archive resources.
+   * @return A {@link FunctionResources} instance.
+   */
+  public static FunctionResources of(String[] jars, String[] files, String[] 
archives) {
+    return new FunctionResources(jars, files, archives);
+  }
+
+  /**
+   * @return The jar resources.
+   */
+  public String[] jars() {
+    return jars.length == 0 ? EMPTY : Arrays.copyOf(jars, jars.length);
+  }
+
+  /**
+   * @return The file resources.
+   */
+  public String[] files() {
+    return files.length == 0 ? EMPTY : Arrays.copyOf(files, files.length);
+  }
+
+  /**
+   * @return The archive resources.
+   */
+  public String[] archives() {
+    return archives.length == 0 ? EMPTY : Arrays.copyOf(archives, 
archives.length);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof FunctionResources)) {
+      return false;
+    }
+    FunctionResources that = (FunctionResources) obj;
+    return Arrays.equals(jars, that.jars)
+        && Arrays.equals(files, that.files)
+        && Arrays.equals(archives, that.archives);
+  }
+
+  @Override
+  public int hashCode() {
+    int result = Objects.hash(Arrays.hashCode(jars), Arrays.hashCode(files));
+    result = 31 * result + Arrays.hashCode(archives);
+    return result;
+  }
+
+  @Override
+  public String toString() {
+    return "FunctionResources{"
+        + "jars="
+        + Arrays.toString(jars)
+        + ", files="
+        + Arrays.toString(files)
+        + ", archives="
+        + Arrays.toString(archives)
+        + '}';
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/FunctionType.java 
b/api/src/main/java/org/apache/gravitino/function/FunctionType.java
new file mode 100644
index 0000000000..43e7ea14da
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionType.java
@@ -0,0 +1,73 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/** Function type supported by Gravitino. */
+@Evolving
+public enum FunctionType {
+  /** Scalar function. */
+  SCALAR,
+
+  /** Aggregate function. */
+  AGGREGATE,
+
+  /** Table-valued function. */
+  TABLE;
+
+  private static final Map<String, FunctionType> NAME_TO_TYPE;
+
+  static {
+    Map<String, FunctionType> map = new HashMap<>();
+    for (FunctionType value : values()) {
+      map.put(value.typeName().toLowerCase(Locale.ROOT), value);
+    }
+    map.put("agg", AGGREGATE);
+    NAME_TO_TYPE = Collections.unmodifiableMap(map);
+  }
+
+  /**
+   * Parse the function type from a string value.
+   *
+   * @param type the string to parse.
+   * @return the parsed {@link FunctionType}.
+   * @throws IllegalArgumentException if the value cannot be parsed.
+   */
+  public static FunctionType fromString(String type) {
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(type) && 
NAME_TO_TYPE.containsKey(type.toLowerCase(Locale.ROOT)),
+        "Invalid function type: " + type);
+    return NAME_TO_TYPE.get(type.toLowerCase(Locale.ROOT));
+  }
+
+  /**
+   * @return the canonical string representation used by APIs.
+   */
+  public String typeName() {
+    return name().toLowerCase(Locale.ROOT);
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/JavaImpl.java 
b/api/src/main/java/org/apache/gravitino/function/JavaImpl.java
new file mode 100644
index 0000000000..0a32467d40
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/JavaImpl.java
@@ -0,0 +1,87 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+
+/** Java implementation with class name. */
+public class JavaImpl extends FunctionImpl {
+  private final String className;
+
+  JavaImpl(
+      RuntimeType runtime,
+      String className,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    super(Language.JAVA, runtime, resources, properties);
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(className), "Java class name cannot be null or 
empty");
+    this.className = className;
+  }
+
+  /**
+   * @return The fully qualified class name.
+   */
+  public String className() {
+    return className;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof JavaImpl)) {
+      return false;
+    }
+    JavaImpl that = (JavaImpl) obj;
+    return Objects.equals(language(), that.language())
+        && Objects.equals(runtime(), that.runtime())
+        && Objects.equals(resources(), that.resources())
+        && Objects.equals(properties(), that.properties())
+        && Objects.equals(className, that.className);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(language(), runtime(), resources(), properties(), 
className);
+  }
+
+  @Override
+  public String toString() {
+    return "JavaImpl{"
+        + "language='"
+        + language()
+        + '\''
+        + ", runtime='"
+        + runtime()
+        + '\''
+        + ", className='"
+        + className
+        + '\''
+        + ", resources="
+        + resources()
+        + ", properties="
+        + properties()
+        + '}';
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/PythonImpl.java 
b/api/src/main/java/org/apache/gravitino/function/PythonImpl.java
new file mode 100644
index 0000000000..862bca1887
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/PythonImpl.java
@@ -0,0 +1,101 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+
+/** Python implementation with handler and optional inline code. */
+public class PythonImpl extends FunctionImpl {
+  private final String handler;
+  private final String codeBlock;
+
+  PythonImpl(
+      RuntimeType runtime,
+      String handler,
+      String codeBlock,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    super(Language.PYTHON, runtime, resources, properties);
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(handler), "Python handler cannot be null or 
empty");
+    this.handler = handler;
+    this.codeBlock = codeBlock;
+  }
+
+  /**
+   * @return The handler entrypoint.
+   */
+  public String handler() {
+    return handler;
+  }
+
+  /**
+   * @return The Python UDF code block.
+   */
+  public String codeBlock() {
+    return codeBlock;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof PythonImpl)) {
+      return false;
+    }
+    PythonImpl that = (PythonImpl) obj;
+    return Objects.equals(language(), that.language())
+        && Objects.equals(runtime(), that.runtime())
+        && Objects.equals(resources(), that.resources())
+        && Objects.equals(properties(), that.properties())
+        && Objects.equals(handler, that.handler)
+        && Objects.equals(codeBlock, that.codeBlock);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(language(), runtime(), resources(), properties(), 
handler, codeBlock);
+  }
+
+  @Override
+  public String toString() {
+    return "PythonImpl{"
+        + "language='"
+        + language()
+        + '\''
+        + ", runtime='"
+        + runtime()
+        + '\''
+        + ", handler='"
+        + handler
+        + '\''
+        + ", codeBlock='"
+        + codeBlock
+        + '\''
+        + ", resources="
+        + resources()
+        + ", properties="
+        + properties()
+        + '}';
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/function/SQLImpl.java 
b/api/src/main/java/org/apache/gravitino/function/SQLImpl.java
new file mode 100644
index 0000000000..a9070c34a7
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/function/SQLImpl.java
@@ -0,0 +1,86 @@
+/*
+ * 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.gravitino.function;
+
+import com.google.common.base.Preconditions;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+
+/** SQL implementation with runtime and SQL body. */
+public class SQLImpl extends FunctionImpl {
+  private final String sql;
+
+  SQLImpl(
+      RuntimeType runtime,
+      String sql,
+      FunctionResources resources,
+      Map<String, String> properties) {
+    super(Language.SQL, runtime, resources, properties);
+    Preconditions.checkArgument(StringUtils.isNotBlank(sql), "SQL text cannot 
be null or empty");
+    this.sql = sql;
+  }
+
+  /**
+   * @return The SQL that defines the function.
+   */
+  public String sql() {
+    return sql;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof SQLImpl)) {
+      return false;
+    }
+    SQLImpl that = (SQLImpl) obj;
+    return Objects.equals(language(), that.language())
+        && Objects.equals(runtime(), that.runtime())
+        && Objects.equals(resources(), that.resources())
+        && Objects.equals(properties(), that.properties())
+        && Objects.equals(sql, that.sql);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(language(), runtime(), resources(), properties(), sql);
+  }
+
+  @Override
+  public String toString() {
+    return "SQLImpl{"
+        + "language='"
+        + language()
+        + '\''
+        + ", runtime='"
+        + runtime()
+        + '\''
+        + ", sql='"
+        + sql
+        + '\''
+        + ", resources="
+        + resources()
+        + ", properties="
+        + properties()
+        + '}';
+  }
+}

Reply via email to