Functions
A function is a piece of logic registered in the catalog so engines can find it and people can govern it. Registering one puts it in the same hierarchy as tables, with a name, an owner, tags, and permissions.
Gravitino stores the definition rather than executing it. Whether a function runs, and how, is a question for the engine that reads it.
Quick start
The example registers the scalar function add_one in the schema public of the catalog sales,
with a SQL implementation for Trino, then looks at it and tags it certified. The tag must already
exist; see Tags. Registering, altering, and dropping a function go through the API; the
UI shows functions and tags them.
- UI
- REST
-
Register the function. The UI does not register functions. Use step 1 of the REST tab, or the Java or Python client.
-
Find it. In Explore, open
salesand thenpublic. The schema's functions are listed with its tables and views, with their tags and policies. -
Look at it. Click
add_one. The Overview tab shows its properties, owner, and when and by whom it was registered. The Definitions tab shows each definition's signature, hereadd_one(x: integer) => integer, and each implementation's language, runtime, body or class, and resources. -
Tag it. Back in
public, select theadd_onerow, click + Tags, choosecertifiedunder Additional tags, and click Update tags.
The function's own page does not show its tags, function type, determinism flag, or comment. Read those through the API.
-
Register the function. A function needs a name, a type, and at least one definition with its parameters, return type, and implementations.
Register a scalar functioncurl -sS -X POST "https://{gravitino_host}/api/metalakes/example/catalogs/sales/schemas/public/functions" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "add_one",
"functionType": "scalar",
"deterministic": true,
"comment": "Adds one to an integer",
"definitions": [{
"parameters": [{"name": "x", "dataType": "integer"}],
"returnType": "integer",
"impls": [{"language": "SQL", "runtime": "TRINO", "sql": "x + 1"}]
}]
}' -
Look at it. The response carries the type, the determinism flag, the comment, and every definition and implementation.
Get a functioncurl -sS "https://{gravitino_host}/api/metalakes/example/catalogs/sales/schemas/public/functions/add_one" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Authorization: Bearer $TOKEN" -
Tag it.
Tag a functioncurl -sS -X POST "https://{gravitino_host}/api/metalakes/example/objects/function/sales.public.add_one/tags" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"tagsToAdd": ["certified"]
}'
An engine reading through Gravitino resolves the function by name and runs the implementation for its own runtime.
The function model
Function types
| Type | Returns |
|---|---|
SCALAR | One value per row |
AGGREGATE | One value per group |
TABLE | A set of rows, for table-valued operations |
The type is declared when the function is registered and tells an engine how to call it.
Determinism
A function is marked deterministic or not. A deterministic function returns the same result for the same arguments every time, which lets an engine cache, reorder, or eliminate calls to it. Marking a function deterministic when it is not, such as one that reads the clock or a random source, invites an engine to optimize in ways that change the answer.
Gravitino records the flag and does not verify it.
Definitions and implementations
A function has two levels beneath it.
A definition is an overload: one parameter list and one return type. A function with several
definitions is the same name accepting different arguments, the way round(x) and round(x, digits)
are one function in most engines. A TABLE function's definition declares its output columns, in
returnColumns, instead of a return type.
An implementation is how a definition is expressed for a given engine. Each declares a language and a runtime, so one definition can carry a SQL version for Trino and a Java version for Spark, and each engine uses the one it can execute.
| Language | Carries |
|---|---|
SQL | The SQL text of the function body |
PYTHON | A handler entrypoint, and optionally inline code |
JAVA | A class name |
Runtimes are SPARK and TRINO. Java and Python implementations can also carry resources, such as
a jar or a package the runtime needs to load.
Examples by function type
Which registered functions an engine can call depends on its connector. The Trino connector runs
scalar functions that have a SQL implementation for the TRINO runtime. The Spark connector runs
scalar and aggregate functions that have a Java implementation for the SPARK runtime.
| Function type | Trino connector | Spark connector |
|---|---|---|
SCALAR | SQL implementation, TRINO | JAVA implementation, SPARK |
AGGREGATE | Not available | JAVA implementation, SPARK |
TABLE | Not available | Not available |
Gravitino registers, lists, tags, and grants on every type and language. Table functions and Python implementations are not yet run by either connector in 1.3; that is a limit of the connectors, not of the function model. The connector pages cover the details: Trino and Spark.
The examples register into the schema public of the catalog sales, as in the quick start.
Scalar function
mask_email hides the local part of an email address. One definition carries two implementations:
SQL for Trino, and a Java class for Spark. Each engine picks the implementation for its runtime.
curl -sS -X POST "https://{gravitino_host}/api/metalakes/example/catalogs/sales/schemas/public/functions" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "mask_email",
"functionType": "scalar",
"deterministic": true,
"comment": "Replaces the part of an email address before the @ with ***",
"definitions": [{
"parameters": [{"name": "email", "dataType": "string"}],
"returnType": "string",
"impls": [
{
"language": "SQL",
"runtime": "TRINO",
"sql": "concat('"'"'***'"'"', substr(email, strpos(email, '"'"'@'"'"')))"
},
{
"language": "JAVA",
"runtime": "SPARK",
"className": "com.example.udf.MaskEmail",
"resources": {"jars": ["s3://{bucket}/udfs/example-udfs.jar"]}
}
]
}]
}'
SELECT sales.public.mask_email('jane.doe@example.com');
-- ***@example.com
SELECT customer_id, sales.public.mask_email(email) AS email
FROM sales.public.customers;
The Spark class implements Spark's UnboundFunction and binds to a ScalarFunction. The connector
does not fetch the jar named in resources; put it on the Spark classpath with --jars or
spark.jars.
Aggregate function
geo_mean returns the geometric mean of a group of values. An aggregate function has a return type
like a scalar one, and the engine calls it once per group.
curl -sS -X POST "https://{gravitino_host}/api/metalakes/example/catalogs/sales/schemas/public/functions" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "geo_mean",
"functionType": "aggregate",
"deterministic": true,
"comment": "Geometric mean of positive values",
"definitions": [{
"parameters": [{"name": "x", "dataType": "double"}],
"returnType": "double",
"impls": [{
"language": "JAVA",
"runtime": "SPARK",
"className": "com.example.udf.GeoMean",
"resources": {"jars": ["s3://{bucket}/udfs/example-udfs.jar"]}
}]
}]
}'
SELECT region, sales.public.geo_mean(growth_rate) AS avg_growth
FROM sales.public.quarterly_results
GROUP BY region;
Here the class's bind returns a Spark AggregateFunction rather than a ScalarFunction.
Table function
split_tags turns a comma-separated string into rows. A table function declares its output columns
in returnColumns and has no returnType. The implementation here is Python, with the handler and
its code stored inline.
curl -sS -X POST "https://{gravitino_host}/api/metalakes/example/catalogs/sales/schemas/public/functions" \
-H "Accept: application/vnd.gravitino.v1+json" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "split_tags",
"functionType": "table",
"deterministic": true,
"comment": "One row per tag in a comma-separated list",
"definitions": [{
"parameters": [{"name": "tags", "dataType": "string"}],
"returnColumns": [
{"name": "tag", "dataType": "string"},
{"name": "position", "dataType": "integer"}
],
"impls": [{
"language": "PYTHON",
"runtime": "SPARK",
"handler": "split_tags",
"codeBlock": "def split_tags(tags):\n for i, t in enumerate(tags.split(\",\")):\n yield (t.strip(), i)"
}]
}]
}'
The function is listed, tagged, and governed like any other, and its definition is readable through the API for tools that run Python table functions themselves. Neither connector exposes it to queries in 1.3.
Permissions
Registering a function requires REGISTER_FUNCTION on its schema or above. Calling a function from
an engine requires EXECUTE_FUNCTION, and altering a registered function requires
MODIFY_FUNCTION.
Granting at a wider scope covers everything beneath it. Dropping a function is reserved for the metalake owner and the object owner.
See Privileges for a comprehensive list of privilege grants.
Use the API
Functions can be registered, listed, altered, and dropped over REST and through the Java and Python clients. The REST reference documents each call, with the client code under the Java and Python tabs.