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});
+ }
+}