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 7f9e652897 [#9561] feat(core): Add UDF management core operations and 
validations (#9560)
7f9e652897 is described below

commit 7f9e6528970132e3603a32ca3a3aff1c8d75aaab
Author: mchades <[email protected]>
AuthorDate: Fri Jan 16 11:34:58 2026 +0800

    [#9561] feat(core): Add UDF management core operations and validations 
(#9560)
    
    ### What changes were proposed in this pull request?
    
    This PR implements the core operations for UDF (User-Defined Function)
    management in Gravitino, including:
    
    **ManagedFunctionOperations implementation:**
    - `registerFunction`: Register scalar, aggregate, and table functions
    with multi-definition support
    - `getFunction`: Retrieve function by identifier (latest version)
    - `listFunctions`: List all functions in a schema namespace
    - `dropFunction`: Remove a function
    
    **Unit tests:**
    - Added comprehensive unit tests for ManagedFunctionOperations with test
    cases covering CRUD operations (except alter) and validation scenarios
    
    ### Why are the changes needed?
    
    Fix: #9561
    
    This is part of the UDF management feature for Gravitino. The core
    operations layer provides the business logic for function management,
    ensuring data integrity through proper validations.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No, this is an internal implementation.
    
    ### How was this patch tested?
    
    Added unit tests in `TestManagedFunctionOperations.java` covering:
    - Function registration and listing
    - Function retrieval (get)
    - Function deletion (drop)
    - Error cases for invalid operations
---
 .../exceptions/NoSuchFunctionVersionException.java |  49 --
 .../org/apache/gravitino/function/Function.java    |  13 +-
 .../apache/gravitino/function/FunctionCatalog.java |  20 +-
 .../src/main/java/org/apache/gravitino/Entity.java |   3 +-
 .../catalog/FunctionNormalizeDispatcher.java       |   7 -
 .../catalog/FunctionOperationDispatcher.java       |  28 +-
 .../catalog/ManagedFunctionOperations.java         | 239 ++++++++--
 .../org/apache/gravitino/meta/FunctionEntity.java  | 319 +++++++++++++
 .../gravitino/storage/relational/JDBCBackend.java  |   2 +
 .../catalog/TestManagedFunctionOperations.java     | 493 +++++++++++++++++++++
 10 files changed, 1030 insertions(+), 143 deletions(-)

diff --git 
a/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionVersionException.java
 
b/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionVersionException.java
deleted file mode 100644
index 7c0f3a22cc..0000000000
--- 
a/api/src/main/java/org/apache/gravitino/exceptions/NoSuchFunctionVersionException.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * 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
index 7634823adc..d5b88dd878 100644
--- a/api/src/main/java/org/apache/gravitino/function/Function.java
+++ b/api/src/main/java/org/apache/gravitino/function/Function.java
@@ -34,8 +34,7 @@ import org.apache.gravitino.rel.types.Type;
  * <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.
+ * implementations for different runtime engines.
  */
 @Evolving
 public interface Function extends Auditable {
@@ -93,14 +92,4 @@ public interface Function extends Auditable {
    * @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
index 73d9ce06ff..2b4b409d9e 100644
--- a/api/src/main/java/org/apache/gravitino/function/FunctionCatalog.java
+++ b/api/src/main/java/org/apache/gravitino/function/FunctionCatalog.java
@@ -23,7 +23,6 @@ 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;
 
@@ -52,29 +51,14 @@ public interface FunctionCatalog {
   /**
    * 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.
+   * result.
    *
    * @param ident A function identifier.
-   * @return The latest version of the function with the given name.
+   * @return 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.
    *
diff --git a/core/src/main/java/org/apache/gravitino/Entity.java 
b/core/src/main/java/org/apache/gravitino/Entity.java
index fc575b72b1..2df8e7a8da 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -80,7 +80,8 @@ public interface Entity extends Serializable {
     TABLE_STATISTIC,
     JOB_TEMPLATE,
     JOB,
-    AUDIT;
+    AUDIT,
+    FUNCTION;
   }
 
   /**
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
index 4930a780aa..fa7317cc7c 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/FunctionNormalizeDispatcher.java
@@ -28,7 +28,6 @@ import org.apache.gravitino.Namespace;
 import org.apache.gravitino.connector.capability.Capability;
 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.function.Function;
 import org.apache.gravitino.function.FunctionChange;
@@ -67,12 +66,6 @@ public class FunctionNormalizeDispatcher implements 
FunctionDispatcher {
     return dispatcher.getFunction(normalizeCaseSensitive(ident));
   }
 
-  @Override
-  public Function getFunction(NameIdentifier ident, int version)
-      throws NoSuchFunctionException, NoSuchFunctionVersionException {
-    return dispatcher.getFunction(normalizeCaseSensitive(ident), version);
-  }
-
   @Override
   public boolean functionExists(NameIdentifier ident) {
     return dispatcher.functionExists(normalizeCaseSensitive(ident));
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/FunctionOperationDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/catalog/FunctionOperationDispatcher.java
index 2392053f69..d02894d35d 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/FunctionOperationDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/FunctionOperationDispatcher.java
@@ -25,7 +25,6 @@ import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 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.function.Function;
 import org.apache.gravitino.function.FunctionChange;
@@ -92,10 +91,10 @@ public class FunctionOperationDispatcher extends 
OperationDispatcher implements
   }
 
   /**
-   * Get a function by {@link NameIdentifier} from the catalog. Returns the 
latest version.
+   * Get a function by {@link NameIdentifier} from the catalog. Returns the 
current version.
    *
    * @param ident A function identifier.
-   * @return The latest version of the function with the given name.
+   * @return The current version of the function with the given name.
    * @throws NoSuchFunctionException If the function does not exist.
    */
   @Override
@@ -110,29 +109,6 @@ public class FunctionOperationDispatcher extends 
OperationDispatcher implements
         ident, LockType.READ, () -> managedFunctionOps.getFunction(ident));
   }
 
-  /**
-   * Get a function by {@link NameIdentifier} and version from the catalog.
-   *
-   * @param ident A function identifier.
-   * @param version The function version, counted from 0.
-   * @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.
-   */
-  @Override
-  public Function getFunction(NameIdentifier ident, int version)
-      throws NoSuchFunctionException, NoSuchFunctionVersionException {
-    Preconditions.checkArgument(version >= 0, "Function version must be 
non-negative");
-    NameIdentifier schemaIdent = NameIdentifier.of(ident.namespace().levels());
-    // Validate schema exists in the underlying catalog
-    if (!schemaOps.schemaExists(schemaIdent)) {
-      throw new NoSuchFunctionException("Schema does not exist: %s", 
schemaIdent);
-    }
-
-    return TreeLockUtils.doWithTreeLock(
-        ident, LockType.READ, () -> managedFunctionOps.getFunction(ident, 
version));
-  }
-
   /**
    * Register a scalar or aggregate function with one or more definitions 
(overloads).
    *
diff --git 
a/core/src/main/java/org/apache/gravitino/catalog/ManagedFunctionOperations.java
 
b/core/src/main/java/org/apache/gravitino/catalog/ManagedFunctionOperations.java
index f080f6ee8b..a2cefd583b 100644
--- 
a/core/src/main/java/org/apache/gravitino/catalog/ManagedFunctionOperations.java
+++ 
b/core/src/main/java/org/apache/gravitino/catalog/ManagedFunctionOperations.java
@@ -18,21 +18,40 @@
  */
 package org.apache.gravitino.catalog;
 
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityAlreadyExistsException;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.exceptions.FunctionAlreadyExistsException;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.exceptions.NoSuchFunctionException;
-import org.apache.gravitino.exceptions.NoSuchFunctionVersionException;
 import org.apache.gravitino.exceptions.NoSuchSchemaException;
 import org.apache.gravitino.function.Function;
 import org.apache.gravitino.function.FunctionCatalog;
 import org.apache.gravitino.function.FunctionChange;
 import org.apache.gravitino.function.FunctionColumn;
 import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionParam;
 import org.apache.gravitino.function.FunctionType;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.FunctionEntity;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.expressions.Expression;
 import org.apache.gravitino.rel.types.Type;
 import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.utils.PrincipalUtils;
 
 /**
  * {@code ManagedFunctionOperations} provides the storage-level operations for 
managing functions in
@@ -48,14 +67,7 @@ import org.apache.gravitino.storage.IdGenerator;
  * </ul>
  */
 public class ManagedFunctionOperations implements FunctionCatalog {
-
-  @SuppressWarnings("UnusedVariable")
-  private static final int INIT_VERSION = 0;
-
-  @SuppressWarnings("UnusedVariable")
   private final EntityStore store;
-
-  @SuppressWarnings("UnusedVariable")
   private final IdGenerator idGenerator;
 
   /**
@@ -71,29 +83,34 @@ public class ManagedFunctionOperations implements 
FunctionCatalog {
 
   @Override
   public NameIdentifier[] listFunctions(Namespace namespace) throws 
NoSuchSchemaException {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException("listFunctions: FunctionEntity not 
yet implemented");
+    return Arrays.stream(listFunctionInfos(namespace))
+        .map(f -> NameIdentifier.of(namespace, f.name()))
+        .toArray(NameIdentifier[]::new);
   }
 
   @Override
   public Function[] listFunctionInfos(Namespace namespace) throws 
NoSuchSchemaException {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException(
-        "listFunctionInfos: FunctionEntity not yet implemented");
-  }
+    try {
+      List<FunctionEntity> functions =
+          store.list(namespace, FunctionEntity.class, 
Entity.EntityType.FUNCTION);
+      return functions.toArray(FunctionEntity[]::new);
 
-  @Override
-  public Function getFunction(NameIdentifier ident) throws 
NoSuchFunctionException {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException("getFunction: FunctionEntity not 
yet implemented");
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchSchemaException(e, "Schema %s does not exist", 
namespace);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to list functions in namespace " + 
namespace, e);
+    }
   }
 
   @Override
-  public Function getFunction(NameIdentifier ident, int version)
-      throws NoSuchFunctionException, NoSuchFunctionVersionException {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException(
-        "getFunction with version: FunctionEntity not yet implemented");
+  public Function getFunction(NameIdentifier ident) throws 
NoSuchFunctionException {
+    try {
+      return store.get(ident, Entity.EntityType.FUNCTION, 
FunctionEntity.class);
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchFunctionException(e, "Function %s does not exist", 
ident);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to get function " + ident, e);
+    }
   }
 
   @Override
@@ -105,8 +122,14 @@ public class ManagedFunctionOperations implements 
FunctionCatalog {
       Type returnType,
       FunctionDefinition[] definitions)
       throws NoSuchSchemaException, FunctionAlreadyExistsException {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException("registerFunction: FunctionEntity 
not yet implemented");
+    return doRegisterFunction(
+        ident,
+        comment,
+        functionType,
+        deterministic,
+        Optional.of(returnType),
+        Optional.empty(),
+        definitions);
   }
 
   @Override
@@ -117,9 +140,14 @@ public class ManagedFunctionOperations implements 
FunctionCatalog {
       FunctionColumn[] returnColumns,
       FunctionDefinition[] definitions)
       throws NoSuchSchemaException, FunctionAlreadyExistsException {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException(
-        "registerFunction for table-valued functions: FunctionEntity not yet 
implemented");
+    return doRegisterFunction(
+        ident,
+        comment,
+        FunctionType.TABLE,
+        deterministic,
+        Optional.empty(),
+        Optional.of(returnColumns),
+        definitions);
   }
 
   @Override
@@ -131,7 +159,158 @@ public class ManagedFunctionOperations implements 
FunctionCatalog {
 
   @Override
   public boolean dropFunction(NameIdentifier ident) {
-    // TODO: Implement when FunctionEntity is available
-    throw new UnsupportedOperationException("dropFunction: FunctionEntity not 
yet implemented");
+    try {
+      return store.delete(ident, Entity.EntityType.FUNCTION);
+    } catch (NoSuchEntityException e) {
+      return false;
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to drop function " + ident, e);
+    }
+  }
+
+  private Function doRegisterFunction(
+      NameIdentifier ident,
+      String comment,
+      FunctionType functionType,
+      boolean deterministic,
+      Optional<Type> returnType,
+      Optional<FunctionColumn[]> returnColumns,
+      FunctionDefinition[] definitions)
+      throws NoSuchSchemaException, FunctionAlreadyExistsException {
+    Preconditions.checkArgument(
+        definitions != null && definitions.length > 0,
+        "At least one function definition must be provided");
+    validateDefinitionsNoArityOverlap(definitions);
+
+    String currentUser = PrincipalUtils.getCurrentUserName();
+    Instant now = Instant.now();
+    AuditInfo auditInfo = 
AuditInfo.builder().withCreator(currentUser).withCreateTime(now).build();
+
+    FunctionEntity functionEntity =
+        FunctionEntity.builder()
+            .withId(idGenerator.nextId())
+            .withName(ident.name())
+            .withNamespace(ident.namespace())
+            .withComment(comment)
+            .withFunctionType(functionType)
+            .withDeterministic(deterministic)
+            .withReturnType(returnType.orElse(null))
+            .withReturnColumns(returnColumns.orElse(null))
+            .withDefinitions(definitions)
+            .withAuditInfo(auditInfo)
+            .build();
+
+    try {
+      store.put(functionEntity, false /* overwrite */);
+      return functionEntity;
+
+    } catch (NoSuchEntityException e) {
+      throw new NoSuchSchemaException(e, "Schema %s does not exist", 
ident.namespace());
+    } catch (EntityAlreadyExistsException e) {
+      throw new FunctionAlreadyExistsException(e, "Function %s already 
exists", ident);
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to register function " + ident, e);
+    }
+  }
+
+  /**
+   * Validates that all definitions in the array do not have overlapping 
arities. This is used when
+   * registering a function with multiple definitions.
+   *
+   * <p>Gravitino enforces strict validation to prevent ambiguity. Operations 
MUST fail if any
+   * definition's invocation arities overlap with another. For example, if an 
existing definition
+   * {@code foo(int, float default 1.0)} supports arities {@code (int)} and 
{@code (int, float)},
+   * adding a new definition {@code foo(int, string default 'x')} (which 
supports {@code (int)} and
+   * {@code (int, string)}) will be REJECTED because both support the call 
{@code foo(1)}. This
+   * ensures every function invocation deterministically maps to a single 
definition.
+   *
+   * @param definitions The array of definitions to validate.
+   * @throws IllegalArgumentException If any two definitions have overlapping 
arities.
+   * @see #computeArities(FunctionDefinition) for details on how arity 
signatures are computed
+   */
+  private void validateDefinitionsNoArityOverlap(FunctionDefinition[] 
definitions) {
+    // Track each arity signature with its source definition index
+    Map<String, Integer> seenArities = new HashMap<>();
+    for (int i = 0; i < definitions.length; i++) {
+      for (String arity : computeArities(definitions[i])) {
+        Integer existingIndex = seenArities.put(arity, i);
+        if (existingIndex != null) {
+          throw new IllegalArgumentException(
+              String.format(
+                  "Cannot register function: definitions at index %d and %d 
have overlapping "
+                      + "arity '%s'. This would create ambiguous function 
invocations.",
+                  existingIndex, i, arity));
+        }
+      }
+    }
+  }
+
+  /**
+   * Computes all possible invocation arities for a function definition. A 
definition with N
+   * parameters where the last M have default values supports arities from 
(N-M) to N parameters.
+   *
+   * <p>For example:
+   *
+   * <ul>
+   *   <li>{@code foo(int a)} → arities: {@code ["int"]}
+   *   <li>{@code foo(int a, float b)} → arities: {@code ["int,float"]}
+   *   <li>{@code foo(int a, float b default 1.0)} → arities: {@code ["int", 
"int,float"]}
+   *   <li>{@code foo(int a, float b default 1.0, string c default 'x')} → 
arities: {@code ["int",
+   *       "int,float", "int,float,string"]}
+   *   <li>{@code foo()} (no args) → arities: {@code [""]}
+   * </ul>
+   *
+   * @param definition The function definition.
+   * @return A set of arity signatures (e.g., "int", "int,float", "").
+   */
+  private Set<String> computeArities(FunctionDefinition definition) {
+    FunctionParam[] params = definition.parameters();
+    int firstOptionalIndex = findFirstOptionalParamIndex(params);
+
+    // Generate all possible arities from firstOptionalIndex to params.length
+    Set<String> arities = new HashSet<>();
+    for (int paramCount = firstOptionalIndex; paramCount <= params.length; 
paramCount++) {
+      String arity =
+          Arrays.stream(params, 0, paramCount)
+              .map(p -> p.dataType().simpleString())
+              .collect(Collectors.joining(","));
+      arities.add(arity);
+    }
+    return arities;
+  }
+
+  /**
+   * Finds the index of the first optional parameter (one with a default 
value). Also validates that
+   * all optional parameters appear at the end of the parameter list.
+   *
+   * @param params The function parameters.
+   * @return The index of the first optional parameter, or params.length if 
all are required.
+   * @throws IllegalArgumentException If a required parameter follows an 
optional one.
+   */
+  private int findFirstOptionalParamIndex(FunctionParam[] params) {
+    int firstOptionalIndex = params.length;
+    for (int i = 0; i < params.length; i++) {
+      boolean hasDefault = hasDefaultValue(params[i]);
+
+      if (firstOptionalIndex < params.length && !hasDefault) {
+        // Found a required param after an optional one - invalid order
+        throw new IllegalArgumentException(
+            String.format(
+                "Invalid parameter order: required parameter '%s' at position 
%d "
+                    + "follows optional parameter(s). All parameters with 
default values "
+                    + "must appear at the end of the parameter list.",
+                params[i].name(), i));
+      }
+
+      if (hasDefault && firstOptionalIndex == params.length) {
+        firstOptionalIndex = i;
+      }
+    }
+    return firstOptionalIndex;
+  }
+
+  private boolean hasDefaultValue(FunctionParam param) {
+    Expression defaultValue = param.defaultValue();
+    return defaultValue != null && defaultValue != 
Column.DEFAULT_VALUE_NOT_SET;
   }
 }
diff --git a/core/src/main/java/org/apache/gravitino/meta/FunctionEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/FunctionEntity.java
new file mode 100644
index 0000000000..e8fb9d9d6b
--- /dev/null
+++ b/core/src/main/java/org/apache/gravitino/meta/FunctionEntity.java
@@ -0,0 +1,319 @@
+/*
+ * 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.meta;
+
+import com.google.common.collect.Maps;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import lombok.ToString;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.Field;
+import org.apache.gravitino.HasIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionColumn;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionType;
+import org.apache.gravitino.rel.types.Type;
+
+/** A class representing a function entity in the metadata store. */
+@ToString
+public class FunctionEntity implements Entity, Auditable, HasIdentifier, 
Function {
+
+  public static final Field ID =
+      Field.required("id", Long.class, "The unique id of the function 
entity.");
+  public static final Field NAME =
+      Field.required("name", String.class, "The name of the function entity.");
+  public static final Field COMMENT =
+      Field.optional("comment", String.class, "The comment or description of 
the function entity.");
+  public static final Field FUNCTION_TYPE =
+      Field.required("function_type", FunctionType.class, "The type of the 
function.");
+  public static final Field DETERMINISTIC =
+      Field.required("deterministic", Boolean.class, "Whether the function is 
deterministic.");
+  public static final Field RETURN_TYPE =
+      Field.optional(
+          "return_type", Type.class, "The return type for scalar or aggregate 
functions.");
+  public static final Field RETURN_COLUMNS =
+      Field.optional(
+          "return_columns",
+          FunctionColumn[].class,
+          "The output columns for table-valued functions.");
+  public static final Field DEFINITIONS =
+      Field.required("definitions", FunctionDefinition[].class, "The 
definitions of the function.");
+  public static final Field AUDIT_INFO =
+      Field.required("audit_info", AuditInfo.class, "The audit details of the 
function entity.");
+
+  private Long id;
+  private String name;
+  private Namespace namespace;
+  private String comment;
+  private FunctionType functionType;
+  private boolean deterministic;
+  private Type returnType;
+  private FunctionColumn[] returnColumns;
+  private FunctionDefinition[] definitions;
+  private AuditInfo auditInfo;
+
+  private FunctionEntity() {}
+
+  @Override
+  public Map<Field, Object> fields() {
+    Map<Field, Object> fields = Maps.newHashMap();
+    fields.put(ID, id);
+    fields.put(NAME, name);
+    fields.put(COMMENT, comment);
+    fields.put(FUNCTION_TYPE, functionType);
+    fields.put(DETERMINISTIC, deterministic);
+    fields.put(RETURN_TYPE, returnType);
+    fields.put(RETURN_COLUMNS, returnColumns);
+    fields.put(DEFINITIONS, definitions);
+    fields.put(AUDIT_INFO, auditInfo);
+
+    return Collections.unmodifiableMap(fields);
+  }
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public Long id() {
+    return id;
+  }
+
+  @Override
+  public Namespace namespace() {
+    return namespace;
+  }
+
+  @Override
+  public String comment() {
+    return comment;
+  }
+
+  @Override
+  public FunctionType functionType() {
+    return functionType;
+  }
+
+  @Override
+  public boolean deterministic() {
+    return deterministic;
+  }
+
+  @Override
+  public Type returnType() {
+    return returnType;
+  }
+
+  @Override
+  public FunctionColumn[] returnColumns() {
+    return returnColumns != null ? returnColumns : new FunctionColumn[0];
+  }
+
+  @Override
+  public FunctionDefinition[] definitions() {
+    return definitions;
+  }
+
+  @Override
+  public AuditInfo auditInfo() {
+    return auditInfo;
+  }
+
+  @Override
+  public EntityType type() {
+    return EntityType.FUNCTION;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+
+    if (!(o instanceof FunctionEntity)) {
+      return false;
+    }
+
+    FunctionEntity that = (FunctionEntity) o;
+    return Objects.equals(id, that.id)
+        && Objects.equals(name, that.name)
+        && Objects.equals(namespace, that.namespace)
+        && Objects.equals(comment, that.comment)
+        && functionType == that.functionType
+        && deterministic == that.deterministic
+        && Objects.equals(returnType, that.returnType)
+        && Arrays.equals(returnColumns, that.returnColumns)
+        && Arrays.equals(definitions, that.definitions)
+        && Objects.equals(auditInfo, that.auditInfo);
+  }
+
+  @Override
+  public int hashCode() {
+    int result =
+        Objects.hash(
+            id, name, namespace, comment, functionType, deterministic, 
returnType, auditInfo);
+    result = 31 * result + Arrays.hashCode(returnColumns);
+    result = 31 * result + Arrays.hashCode(definitions);
+    return result;
+  }
+
+  /**
+   * Creates a new builder for constructing a FunctionEntity.
+   *
+   * @return A new builder instance.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Builder class for creating instances of {@link FunctionEntity}. */
+  public static class Builder {
+    private final FunctionEntity functionEntity;
+
+    private Builder() {
+      functionEntity = new FunctionEntity();
+    }
+
+    /**
+     * Sets the unique id of the function entity.
+     *
+     * @param id The unique id.
+     * @return This builder instance.
+     */
+    public Builder withId(Long id) {
+      functionEntity.id = id;
+      return this;
+    }
+
+    /**
+     * Sets the name of the function entity.
+     *
+     * @param name The name of the function.
+     * @return This builder instance.
+     */
+    public Builder withName(String name) {
+      functionEntity.name = name;
+      return this;
+    }
+
+    /**
+     * Sets the namespace of the function entity.
+     *
+     * @param namespace The namespace.
+     * @return This builder instance.
+     */
+    public Builder withNamespace(Namespace namespace) {
+      functionEntity.namespace = namespace;
+      return this;
+    }
+
+    /**
+     * Sets the comment of the function entity.
+     *
+     * @param comment The comment or description.
+     * @return This builder instance.
+     */
+    public Builder withComment(String comment) {
+      functionEntity.comment = comment;
+      return this;
+    }
+
+    /**
+     * Sets the function type.
+     *
+     * @param functionType The type of the function (SCALAR, AGGREGATE, or 
TABLE).
+     * @return This builder instance.
+     */
+    public Builder withFunctionType(FunctionType functionType) {
+      functionEntity.functionType = functionType;
+      return this;
+    }
+
+    /**
+     * Sets whether the function is deterministic.
+     *
+     * @param deterministic True if the function is deterministic, false 
otherwise.
+     * @return This builder instance.
+     */
+    public Builder withDeterministic(boolean deterministic) {
+      functionEntity.deterministic = deterministic;
+      return this;
+    }
+
+    /**
+     * Sets the return type for scalar or aggregate functions.
+     *
+     * @param returnType The return type.
+     * @return This builder instance.
+     */
+    public Builder withReturnType(Type returnType) {
+      functionEntity.returnType = returnType;
+      return this;
+    }
+
+    /**
+     * Sets the return columns for table-valued functions.
+     *
+     * @param returnColumns The output columns.
+     * @return This builder instance.
+     */
+    public Builder withReturnColumns(FunctionColumn[] returnColumns) {
+      functionEntity.returnColumns = returnColumns;
+      return this;
+    }
+
+    /**
+     * Sets the function definitions.
+     *
+     * @param definitions The definitions (overloads) of the function.
+     * @return This builder instance.
+     */
+    public Builder withDefinitions(FunctionDefinition[] definitions) {
+      functionEntity.definitions = definitions;
+      return this;
+    }
+
+    /**
+     * Sets the audit information.
+     *
+     * @param auditInfo The audit information.
+     * @return This builder instance.
+     */
+    public Builder withAuditInfo(AuditInfo auditInfo) {
+      functionEntity.auditInfo = auditInfo;
+      return this;
+    }
+
+    /**
+     * Builds the FunctionEntity instance.
+     *
+     * @return The constructed FunctionEntity.
+     */
+    public FunctionEntity build() {
+      functionEntity.validate();
+      return functionEntity;
+    }
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java 
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index 206d9521ff..fee9d4f16f 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -397,6 +397,7 @@ public class JDBCBackend implements RelationalBackend {
         return JobMetaService.getInstance()
             .deleteJobsByLegacyTimeline(legacyTimeline, 
GARBAGE_COLLECTOR_SINGLE_DELETION_LIMIT);
       case AUDIT:
+      case FUNCTION:
         return 0;
         // TODO: Implement hard delete logic for these entity types.
 
@@ -426,6 +427,7 @@ public class JDBCBackend implements RelationalBackend {
       case TABLE_STATISTIC:
       case JOB_TEMPLATE:
       case JOB:
+      case FUNCTION: // todo: remove once function versioning is supported
         // These entity types have not implemented multi-versions, so we can 
skip.
         return 0;
 
diff --git 
a/core/src/test/java/org/apache/gravitino/catalog/TestManagedFunctionOperations.java
 
b/core/src/test/java/org/apache/gravitino/catalog/TestManagedFunctionOperations.java
new file mode 100644
index 0000000000..417eb1a361
--- /dev/null
+++ 
b/core/src/test/java/org/apache/gravitino/catalog/TestManagedFunctionOperations.java
@@ -0,0 +1,493 @@
+/*
+ * 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.catalog;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.exceptions.FunctionAlreadyExistsException;
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchFunctionException;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionDefinitions;
+import org.apache.gravitino.function.FunctionImpl;
+import org.apache.gravitino.function.FunctionImpls;
+import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.function.FunctionParams;
+import org.apache.gravitino.function.FunctionType;
+import org.apache.gravitino.meta.FunctionEntity;
+import org.apache.gravitino.rel.expressions.literals.Literals;
+import org.apache.gravitino.rel.types.Types;
+import org.apache.gravitino.storage.IdGenerator;
+import org.apache.gravitino.storage.RandomIdGenerator;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestManagedFunctionOperations {
+
+  private static final String METALAKE_NAME = "test_metalake";
+  private static final String CATALOG_NAME = "test_catalog";
+  private static final String SCHEMA_NAME = "schema1";
+
+  private final IdGenerator idGenerator = new RandomIdGenerator();
+  private final Map<NameIdentifier, FunctionEntity> entityMap = new 
HashMap<>();
+
+  private EntityStore store;
+  private ManagedFunctionOperations functionOperations;
+
+  @BeforeEach
+  public void setUp() throws Exception {
+    entityMap.clear();
+    store = createMockEntityStore();
+    functionOperations = new ManagedFunctionOperations(store, idGenerator);
+  }
+
+  @Test
+  public void testRegisterAndListFunctions() {
+    NameIdentifier func1Ident = getFunctionIdent("func1");
+    FunctionParam[] params1 = new FunctionParam[] {FunctionParams.of("a", 
Types.IntegerType.get())};
+    FunctionDefinition[] definitions1 = new FunctionDefinition[] 
{createSimpleDefinition(params1)};
+
+    functionOperations.registerFunction(
+        func1Ident,
+        "Test function 1",
+        FunctionType.SCALAR,
+        true,
+        Types.StringType.get(),
+        definitions1);
+
+    NameIdentifier func2Ident = getFunctionIdent("func2");
+    FunctionParam[] params2 =
+        new FunctionParam[] {
+          FunctionParams.of("x", Types.StringType.get()),
+          FunctionParams.of("y", Types.StringType.get())
+        };
+    FunctionDefinition[] definitions2 = new FunctionDefinition[] 
{createSimpleDefinition(params2)};
+
+    functionOperations.registerFunction(
+        func2Ident,
+        "Test function 2",
+        FunctionType.SCALAR,
+        false,
+        Types.IntegerType.get(),
+        definitions2);
+
+    // List functions
+    NameIdentifier[] functionIdents = 
functionOperations.listFunctions(getFunctionNamespace());
+    Assertions.assertEquals(2, functionIdents.length);
+    Set<String> functionNames =
+        
Arrays.stream(functionIdents).map(NameIdentifier::name).collect(Collectors.toSet());
+
+    Assertions.assertTrue(functionNames.contains("func1"));
+    Assertions.assertTrue(functionNames.contains("func2"));
+  }
+
+  @Test
+  public void testRegisterAndGetFunction() {
+    NameIdentifier funcIdent = getFunctionIdent("my_func");
+    FunctionParam[] params =
+        new FunctionParam[] {FunctionParams.of("input", 
Types.StringType.get())};
+    FunctionDefinition[] definitions = new FunctionDefinition[] 
{createSimpleDefinition(params)};
+
+    Function newFunc =
+        functionOperations.registerFunction(
+            funcIdent,
+            "My test function",
+            FunctionType.SCALAR,
+            true,
+            Types.IntegerType.get(),
+            definitions);
+
+    Assertions.assertEquals("my_func", newFunc.name());
+    Assertions.assertEquals("My test function", newFunc.comment());
+    Assertions.assertEquals(FunctionType.SCALAR, newFunc.functionType());
+    Assertions.assertTrue(newFunc.deterministic());
+    Assertions.assertEquals(Types.IntegerType.get(), newFunc.returnType());
+
+    // Get function
+    Function loadedFunc = functionOperations.getFunction(funcIdent);
+    Assertions.assertEquals(newFunc.name(), loadedFunc.name());
+    Assertions.assertEquals(newFunc.comment(), loadedFunc.comment());
+
+    // Test register function that already exists
+    Assertions.assertThrows(
+        FunctionAlreadyExistsException.class,
+        () ->
+            functionOperations.registerFunction(
+                funcIdent,
+                "Another function",
+                FunctionType.SCALAR,
+                true,
+                Types.StringType.get(),
+                definitions));
+
+    // Test get non-existing function
+    NameIdentifier nonExistingIdent = getFunctionIdent("non_existing_func");
+    Assertions.assertThrows(
+        NoSuchFunctionException.class, () -> 
functionOperations.getFunction(nonExistingIdent));
+  }
+
+  @Test
+  public void testRegisterAndDropFunction() {
+    NameIdentifier funcIdent = getFunctionIdent("func_to_drop");
+    FunctionParam[] params = new FunctionParam[] {FunctionParams.of("a", 
Types.IntegerType.get())};
+    FunctionDefinition[] definitions = new FunctionDefinition[] 
{createSimpleDefinition(params)};
+
+    functionOperations.registerFunction(
+        funcIdent,
+        "Function to drop",
+        FunctionType.SCALAR,
+        true,
+        Types.StringType.get(),
+        definitions);
+
+    // Drop the function
+    boolean dropped = functionOperations.dropFunction(funcIdent);
+    Assertions.assertTrue(dropped);
+
+    // Verify the function is dropped
+    Assertions.assertThrows(
+        NoSuchFunctionException.class, () -> 
functionOperations.getFunction(funcIdent));
+
+    // Test drop non-existing function
+    Assertions.assertFalse(functionOperations.dropFunction(funcIdent));
+  }
+
+  @Test
+  public void testRegisterFunctionWithOverlappingDefinitions() {
+    NameIdentifier funcIdent = getFunctionIdent("func_overlap_register");
+
+    // Try to register with two definitions that have overlapping arities
+    FunctionParam[] params1 =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.IntegerType.get()),
+          FunctionParams.of("b", Types.FloatType.get(), null, 
Literals.floatLiteral(1.0f))
+        };
+    FunctionParam[] params2 =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.IntegerType.get()),
+          FunctionParams.of("c", Types.StringType.get(), null, 
Literals.stringLiteral("x"))
+        };
+
+    FunctionDefinition[] definitions =
+        new FunctionDefinition[] {createSimpleDefinition(params1), 
createSimpleDefinition(params2)};
+
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            functionOperations.registerFunction(
+                funcIdent,
+                "Test function",
+                FunctionType.SCALAR,
+                true,
+                Types.StringType.get(),
+                definitions));
+  }
+
+  @Test
+  public void testInvalidParameterOrder() {
+    // Test that parameters with default values must appear at the end
+    NameIdentifier funcIdent = getFunctionIdent("func_invalid_params");
+
+    // Create params with invalid order: (a default 1, b required, c default 2)
+    FunctionParam[] invalidParams =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.IntegerType.get(), "param a", 
Literals.integerLiteral(1)),
+          FunctionParams.of("b", Types.StringType.get()), // Required param 
after optional
+          FunctionParams.of("c", Types.IntegerType.get(), "param c", 
Literals.integerLiteral(2))
+        };
+    FunctionDefinition[] definitions =
+        new FunctionDefinition[] {createSimpleDefinition(invalidParams)};
+
+    // Should throw IllegalArgumentException when trying to register
+    IllegalArgumentException ex =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                functionOperations.registerFunction(
+                    funcIdent,
+                    "Invalid function",
+                    FunctionType.SCALAR,
+                    true,
+                    Types.StringType.get(),
+                    definitions));
+
+    Assertions.assertTrue(
+        ex.getMessage().contains("Invalid parameter order"),
+        "Expected error about invalid parameter order, got: " + 
ex.getMessage());
+    Assertions.assertTrue(
+        ex.getMessage().contains("required parameter 'b'"),
+        "Expected error to mention parameter 'b', got: " + ex.getMessage());
+    Assertions.assertTrue(
+        ex.getMessage().contains("position 1"),
+        "Expected error to mention position 1, got: " + ex.getMessage());
+
+    // Test with valid order: all optional params at the end
+    FunctionParam[] validParams =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.IntegerType.get()),
+          FunctionParams.of("b", Types.StringType.get()),
+          FunctionParams.of("c", Types.IntegerType.get(), "param c", 
Literals.integerLiteral(1)),
+          FunctionParams.of("d", Types.IntegerType.get(), "param d", 
Literals.integerLiteral(2))
+        };
+    FunctionDefinition[] validDefinitions =
+        new FunctionDefinition[] {createSimpleDefinition(validParams)};
+
+    // This should succeed
+    functionOperations.registerFunction(
+        funcIdent,
+        "Valid function",
+        FunctionType.SCALAR,
+        true,
+        Types.StringType.get(),
+        validDefinitions);
+
+    // Verify the function was registered
+    Function func = functionOperations.getFunction(funcIdent);
+    Assertions.assertNotNull(func);
+    Assertions.assertEquals("Valid function", func.comment());
+  }
+
+  @Test
+  public void testNonOverlappingDefinitions() {
+    // Test that definitions with different arities can coexist
+    NameIdentifier funcIdent = getFunctionIdent("func_non_overlap");
+
+    // Two definitions with completely different parameter types (no overlap)
+    FunctionParam[] params1 = new FunctionParam[] {FunctionParams.of("a", 
Types.IntegerType.get())};
+    FunctionParam[] params2 = new FunctionParam[] {FunctionParams.of("a", 
Types.StringType.get())};
+
+    FunctionDefinition[] definitions =
+        new FunctionDefinition[] {createSimpleDefinition(params1), 
createSimpleDefinition(params2)};
+
+    // Should succeed - no arity overlap
+    Function func =
+        functionOperations.registerFunction(
+            funcIdent,
+            "Non-overlapping function",
+            FunctionType.SCALAR,
+            true,
+            Types.StringType.get(),
+            definitions);
+
+    Assertions.assertNotNull(func);
+    Assertions.assertEquals(2, func.definitions().length);
+  }
+
+  @Test
+  public void testNoArgsFunction() {
+    // Test function with no parameters
+    NameIdentifier funcIdent = getFunctionIdent("func_no_args");
+
+    FunctionParam[] params = new FunctionParam[] {};
+    FunctionDefinition[] definitions = new FunctionDefinition[] 
{createSimpleDefinition(params)};
+
+    Function func =
+        functionOperations.registerFunction(
+            funcIdent,
+            "No args function",
+            FunctionType.SCALAR,
+            true,
+            Types.StringType.get(),
+            definitions);
+
+    Assertions.assertNotNull(func);
+    Assertions.assertEquals(0, func.definitions()[0].parameters().length);
+  }
+
+  @Test
+  public void testMultipleDefaultParams() {
+    // Test function with multiple default parameters generates correct arities
+    NameIdentifier funcIdent = getFunctionIdent("func_multi_default");
+
+    // foo(int a, float b default 1.0, string c default 'x')
+    // Should generate arities: ["integer"], ["integer,float"], 
["integer,float,string"]
+    FunctionParam[] params =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.IntegerType.get()),
+          FunctionParams.of("b", Types.FloatType.get(), "param b", 
Literals.floatLiteral(1.0f)),
+          FunctionParams.of("c", Types.StringType.get(), "param c", 
Literals.stringLiteral("x"))
+        };
+    FunctionDefinition[] definitions = new FunctionDefinition[] 
{createSimpleDefinition(params)};
+
+    Function func =
+        functionOperations.registerFunction(
+            funcIdent,
+            "Multi default function",
+            FunctionType.SCALAR,
+            true,
+            Types.StringType.get(),
+            definitions);
+
+    Assertions.assertNotNull(func);
+    Assertions.assertEquals(3, func.definitions()[0].parameters().length);
+  }
+
+  @Test
+  public void testOverlappingAritiesWithDifferentTypes() {
+    // Test that two definitions with same arity count but different types 
don't overlap
+    NameIdentifier funcIdent = getFunctionIdent("func_same_arity_diff_types");
+
+    // foo(int, int) and foo(string, string) - same arity count but different 
types
+    FunctionParam[] params1 =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.IntegerType.get()),
+          FunctionParams.of("b", Types.IntegerType.get())
+        };
+    FunctionParam[] params2 =
+        new FunctionParam[] {
+          FunctionParams.of("a", Types.StringType.get()),
+          FunctionParams.of("b", Types.StringType.get())
+        };
+
+    FunctionDefinition[] definitions =
+        new FunctionDefinition[] {createSimpleDefinition(params1), 
createSimpleDefinition(params2)};
+
+    // Should succeed - arities are "integer,integer" vs "string,string"
+    Function func =
+        functionOperations.registerFunction(
+            funcIdent,
+            "Same arity different types",
+            FunctionType.SCALAR,
+            true,
+            Types.StringType.get(),
+            definitions);
+
+    Assertions.assertNotNull(func);
+    Assertions.assertEquals(2, func.definitions().length);
+  }
+
+  @SuppressWarnings("unchecked")
+  private EntityStore createMockEntityStore() throws Exception {
+    EntityStore mockStore = mock(EntityStore.class);
+
+    // Mock put operation
+    doAnswer(
+            invocation -> {
+              FunctionEntity entity = invocation.getArgument(0);
+              boolean overwrite = invocation.getArgument(1);
+              NameIdentifier ident = entity.nameIdentifier();
+
+              if (!overwrite && entityMap.containsKey(ident)) {
+                throw new EntityAlreadyExistsException("Entity %s already 
exists", ident);
+              }
+              entityMap.put(ident, entity);
+              return null;
+            })
+        .when(mockStore)
+        .put(any(FunctionEntity.class), any(Boolean.class));
+
+    // Mock get operation
+    when(mockStore.get(
+            any(NameIdentifier.class), eq(Entity.EntityType.FUNCTION), 
eq(FunctionEntity.class)))
+        .thenAnswer(
+            invocation -> {
+              NameIdentifier ident = invocation.getArgument(0);
+              FunctionEntity entity = findEntityByIdent(ident);
+              if (entity == null) {
+                throw new NoSuchEntityException("Entity %s does not exist", 
ident);
+              }
+              return entity;
+            });
+
+    // Mock delete operation (2 parameters - default method that calls 
3-parameter version)
+    when(mockStore.delete(any(NameIdentifier.class), 
eq(Entity.EntityType.FUNCTION)))
+        .thenAnswer(
+            invocation -> {
+              NameIdentifier ident = invocation.getArgument(0);
+              FunctionEntity entity = findEntityByIdent(ident);
+              if (entity == null) {
+                return false;
+              }
+              entityMap.remove(entity.nameIdentifier());
+              return true;
+            });
+
+    // Mock list operation
+    when(mockStore.list(
+            any(Namespace.class), eq(FunctionEntity.class), 
eq(Entity.EntityType.FUNCTION)))
+        .thenAnswer(
+            invocation -> {
+              Namespace namespace = invocation.getArgument(0);
+              return entityMap.values().stream()
+                  .filter(e -> e.namespace().equals(namespace))
+                  .collect(Collectors.toList());
+            });
+
+    return mockStore;
+  }
+
+  /**
+   * Finds an entity by identifier. This method handles both the internal 
store identifier format
+   * (used by getFunction) and original identifiers (used by alterFunction and 
dropFunction).
+   *
+   * <p>Store identifier format: namespace = original_namespace + 
function_name, name = internal id
+   * Original identifier format: namespace = schema_namespace, name = 
function_name
+   */
+  private FunctionEntity findEntityByIdent(NameIdentifier ident) {
+    // First, try to find by original identifier (direct match)
+    FunctionEntity directMatch = entityMap.get(ident);
+    if (directMatch != null) {
+      return directMatch;
+    }
+
+    // If not found, try to interpret as store identifier
+    String[] levels = ident.namespace().levels();
+    if (levels.length < 1) {
+      return null;
+    }
+    String functionName = levels[levels.length - 1];
+    Namespace originalNamespace = Namespace.of(Arrays.copyOf(levels, 
levels.length - 1));
+
+    for (FunctionEntity entity : entityMap.values()) {
+      if (entity.name().equals(functionName) && 
entity.namespace().equals(originalNamespace)) {
+        return entity;
+      }
+    }
+    return null;
+  }
+
+  private Namespace getFunctionNamespace() {
+    return Namespace.of(METALAKE_NAME, CATALOG_NAME, SCHEMA_NAME);
+  }
+
+  private NameIdentifier getFunctionIdent(String functionName) {
+    return NameIdentifier.of(getFunctionNamespace(), functionName);
+  }
+
+  private FunctionDefinition createSimpleDefinition(FunctionParam[] params) {
+    FunctionImpl impl = FunctionImpls.ofJava(FunctionImpl.RuntimeType.SPARK, 
"com.example.TestUDF");
+    return FunctionDefinitions.of(params, new FunctionImpl[] {impl});
+  }
+}

Reply via email to