This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new 4e2adc23ae [Cherry-pick to branch-1.3] [#13139] fix(trino-connector):
Build a complete function specification for SQL UDFs (#13142) (#13200)
4e2adc23ae is described below
commit 4e2adc23ae687c92551f74524fe794d07eb2f152
Author: Yuhui <[email protected]>
AuthorDate: Wed Sep 16 15:06:31 2026 +0800
[Cherry-pick to branch-1.3] [#13139] fix(trino-connector): Build a complete
function specification for SQL UDFs (#13142) (#13200)
**Cherry-pick Information:**
- Original commit: 3867e08c3e2247bbee57615fc76a1d8fe9cea1b0
- Target branch: `branch-1.3`
- Status: ✅ Clean cherry-pick (no conflicts)
---
.../workflows/trino-integration-test-action.yml | 10 +-
...manage-user-defined-function-using-gravitino.md | 8 +
docs/trino-connector/udf-support.md | 17 +-
.../integration/test/container/TrinoContainer.java | 45 +++--
.../integration/test/TrinoQueryITBase.java | 12 +-
.../test/TrinoTlsOAuthCredentialVendingIT.java | 39 +++-
.../connector/integration/test/TrinoUDFIT.java | 87 ++++-----
.../trino/connector/GravitinoMetadata.java | 17 +-
.../connector/util/TrinoRoutineSpecification.java | 143 ++++++++++++++
.../connector/TestGravitinoMetadataFunction.java | 69 ++++++-
.../util/TestTrinoRoutineSpecification.java | 214 +++++++++++++++++++++
.../entitiesContent/FunctionDetailsPage.js | 11 +-
12 files changed, 590 insertions(+), 82 deletions(-)
diff --git a/.github/workflows/trino-integration-test-action.yml
b/.github/workflows/trino-integration-test-action.yml
index bb91b838be..7bc28846dc 100644
--- a/.github/workflows/trino-integration-test-action.yml
+++ b/.github/workflows/trino-integration-test-action.yml
@@ -64,9 +64,17 @@ jobs:
# Disable the Trino cascading query integration test, because the
connector jars are private now.
#trino-connector/integration-test/trino-test-tools/run_test.sh
+ # The SQL-file harness above does not execute the JUnit ITs under
+ # trino-connector/integration-test, so run them explicitly. This runs in
embedded mode;
+ # ITs that require the deploy distribution
(TrinoTlsOAuthCredentialVendingIT) are skipped.
+ - name: Trino JUnit Integration Test
+ id: junitIntegrationTest
+ run: |
+ ./gradlew :trino-connector:integration-test:test
-PskipDockerTests=false -PskipWeb=true
+
- name: Upload integrate tests reports
uses: actions/upload-artifact@v7
- if: ${{ (failure() && steps.integrationTest.outcome == 'failure') ||
contains(github.event.pull_request.labels.*.name, 'upload log') }}
+ if: ${{ (failure() && (steps.integrationTest.outcome == 'failure' ||
steps.junitIntegrationTest.outcome == 'failure')) ||
contains(github.event.pull_request.labels.*.name, 'upload log') }}
with:
name: trino-connector-integrate-test-reports-${{ inputs.java-version
}}
path: |
diff --git a/docs/manage-user-defined-function-using-gravitino.md
b/docs/manage-user-defined-function-using-gravitino.md
index b67068e536..3d0b977734 100755
--- a/docs/manage-user-defined-function-using-gravitino.md
+++ b/docs/manage-user-defined-function-using-gravitino.md
@@ -15,6 +15,14 @@ determinism, and how definitions and implementations relate,
see [Functions](./f
creating the catalog and schema a function lives in, see
[Manage Catalogs and Schemas](./manage-catalogs-and-schemas.md).
+:::note
+Registering a function stores its metadata in Gravitino; whether an engine can
call it depends on
+the engine's connector. The Trino connector exposes only implementations with
language `SQL` and
+runtime `TRINO`; Python and Java implementations, and any implementation with
another runtime, are
+managed in Gravitino but are not visible or callable from Trino. See
+[Trino Connector UDF Support](./trino-connector/udf-support.md).
+:::
+
## Function Operations
### Register a SQL Function
diff --git a/docs/trino-connector/udf-support.md
b/docs/trino-connector/udf-support.md
index 67d4c094e2..a8c22072a1 100644
--- a/docs/trino-connector/udf-support.md
+++ b/docs/trino-connector/udf-support.md
@@ -19,7 +19,18 @@ When Gravitino catalogs contain registered functions, the
Trino connector:
2. Filters to include only functions with `RuntimeType.TRINO` and
`Language.SQL`.
3. Maps each function implementation to a Trino `LanguageFunction` with a
signature token derived from the function name and parameter types.
-Functions registered with other runtimes (e.g., `SPARK`) are **not** visible
in Trino.
+Only functions with language `SQL` and runtime `TRINO` are visible to and
callable from Trino. Functions registered for other languages or runtimes (for
example a Python or Java implementation with runtime `SPARK`) are managed in
Gravitino but are **not** exposed through this connector: they do not appear in
`SHOW FUNCTIONS`, and invoking one fails with a Trino `Function
"<catalog>.<schema>.<name>" not registered` error. The function still exists in
Gravitino; the connector simply filter [...]
+
+### SQL body format
+
+The `sql` field of a `SQL`/`TRINO` implementation is the function body. The
connector assembles a complete [Trino SQL
routine](https://trino.io/docs/current/routines/function.html) specification
(`FUNCTION <name>(<params>) RETURNS <type> [NOT] DETERMINISTIC SECURITY INVOKER
...`) from the function name, parameters, return type and deterministic flag
before handing it to Trino. The body may be:
+
+- A bare expression, e.g. `x + 1`. The connector wraps it as `RETURN x + 1`.
+- A control statement, e.g. `RETURN x + 1` or `BEGIN ... END`.
+
+The form is decided by the first token of the body, ignoring leading SQL
comments. Since `return`, `begin` and `function` are also valid identifiers, a
parameter with one of these names shadows the keyword: the body is then always
treated as an expression, e.g. `return + 1` for a parameter named `return`. A
body that is itself a complete `FUNCTION ...` specification is not supported
and the function is skipped with a warning.
+
+Function, parameter and row field names are quoted in the generated
specification. Trino resolves routine and parameter names case-insensitively
regardless of quoting, so the body can reference parameters as plain
identifiers.
## Prerequisites
@@ -62,5 +73,7 @@ SELECT catalog.my_schema.add_one(5);
- **Read-only**: The Trino connector supports listing and invoking Gravitino
UDFs. Creating or dropping functions via Trino SQL (`CREATE FUNCTION` / `DROP
FUNCTION`) is not yet supported.
- **SQL only**: Only SQL-language implementations are mapped. Java and Python
implementations are not exposed to Trino.
-- **TRINO runtime only**: Only functions with `RuntimeType.TRINO` are visible.
Functions registered with `RuntimeType.SPARK` or other runtimes are filtered
out.
+- **TRINO runtime only**: Only functions with `RuntimeType.TRINO` are visible.
Functions registered with `RuntimeType.SPARK` or other runtimes are filtered
out and fail with `Function ... not registered` when invoked.
+- **Scalar only**: Only `SCALAR` functions are exposed. Aggregate and
table-valued functions are skipped.
+- **No parameter defaults**: Trino SQL routines do not support parameter
default values, so a parameter's `defaultValue` is ignored and the parameter is
required when calling from Trino.
- **Type mapping**: Function parameter and return types are converted from
Gravitino types to Trino types. Unsupported types will cause the function to be
skipped with a warning log.
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoContainer.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoContainer.java
index cc3777747d..d2d16f9da7 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoContainer.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/TrinoContainer.java
@@ -39,6 +39,7 @@ import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
import org.rnorth.ducttape.Preconditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -135,23 +136,12 @@ public class TrinoContainer extends BaseContainer {
}
public boolean initTrinoJdbcConnection() {
- final String dbUrl = String.format("jdbc:trino://127.0.0.1:%d",
getMappedPort(coordinatorPort));
- Properties properties = new Properties();
- properties.setProperty("user", "admin");
- if (tlsEnabled) {
- properties.setProperty("SSL", "true");
- properties.setProperty("SSLVerification", "FULL");
- properties.setProperty("SSLTrustStorePath", truststorePath);
- properties.setProperty("SSLTrustStorePassword", truststorePassword);
- properties.setProperty("SSLTrustStoreType", truststoreType);
- }
-
long now = System.currentTimeMillis();
boolean result = false;
while (!result && System.currentTimeMillis() - now <= 20000) {
try {
- trinoJdbcConnection = DriverManager.getConnection(dbUrl, properties);
+ trinoJdbcConnection = openJdbcConnection("admin", Map.of());
result = true;
} catch (SQLException e) {
LOG.error(e.getMessage(), e);
@@ -198,6 +188,37 @@ public class TrinoContainer extends BaseContainer {
return true;
}
+ /**
+ * Opens a new JDBC connection to the coordinator as the given user,
applying the container's TLS
+ * settings when enabled.
+ *
+ * @param user the Trino session user
+ * @param extraCredentials extra credentials to attach to the session, e.g.
a forwarded token
+ * @return a new connection; the caller closes it
+ * @throws SQLException if the connection cannot be established
+ */
+ public Connection openJdbcConnection(String user, Map<String, String>
extraCredentials)
+ throws SQLException {
+ String dbUrl = String.format("jdbc:trino://127.0.0.1:%d",
getMappedPort(coordinatorPort));
+ Properties properties = new Properties();
+ properties.setProperty("user", user);
+ if (!extraCredentials.isEmpty()) {
+ properties.setProperty(
+ "extraCredentials",
+ extraCredentials.entrySet().stream()
+ .map(e -> e.getKey() + ":" + e.getValue())
+ .collect(Collectors.joining(",")));
+ }
+ if (tlsEnabled) {
+ properties.setProperty("SSL", "true");
+ properties.setProperty("SSLVerification", "FULL");
+ properties.setProperty("SSLTrustStorePath", truststorePath);
+ properties.setProperty("SSLTrustStorePassword", truststorePassword);
+ properties.setProperty("SSLTrustStoreType", truststoreType);
+ }
+ return DriverManager.getConnection(dbUrl, properties);
+ }
+
public ArrayList<ArrayList<String>> executeQuerySQL(String sql) {
LOG.info("executeQuerySQL: {}", sql);
ArrayList<ArrayList<String>> queryData = new ArrayList<>();
diff --git
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java
index 405df92076..2ec7462fa4 100644
---
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java
+++
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoQueryITBase.java
@@ -204,7 +204,11 @@ public class TrinoQueryITBase {
gravitinoClient.dropMetalake(metalakeName, true);
}
- private static void createCatalog(
+ /**
+ * Creates the catalog in Gravitino if needed and waits until Trino has
loaded it. The IT
+ * connector runs in single-metalake mode, so the catalog is exposed under
its bare name.
+ */
+ protected static void createCatalog(
String catalogName, String provider, Map<String, String> properties)
throws Exception {
boolean exists = metalake.catalogExists(catalogName);
if (!exists) {
@@ -219,14 +223,14 @@ public class TrinoQueryITBase {
while (!catalogCreated && tries-- >= 0) {
try {
String result = trinoQueryRunner.runQuery("show catalogs");
- if (result.contains(metalakeName + "." + catalogName)) {
+ if (result.contains("\"" + catalogName + "\"")) {
catalogCreated = true;
break;
}
LOG.info("Waiting for catalog {} to be created", catalogName);
// connection exception need retry.
- } catch (Exception ConnectionException) {
- LOG.info("Waiting for connecting to Trino");
+ } catch (Exception e) {
+ LOG.info("Waiting for connecting to Trino: {}", e.getMessage());
}
sleep(1000);
}
diff --git
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoTlsOAuthCredentialVendingIT.java
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoTlsOAuthCredentialVendingIT.java
index a98997d648..408d4715b2 100644
---
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoTlsOAuthCredentialVendingIT.java
+++
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoTlsOAuthCredentialVendingIT.java
@@ -37,6 +37,8 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
+import java.sql.Connection;
+import java.sql.Statement;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
@@ -47,6 +49,7 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Configs;
+import org.apache.gravitino.Schema;
import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
import org.apache.gravitino.client.GravitinoMetalake;
import org.apache.gravitino.credential.CredentialConstants;
@@ -60,10 +63,10 @@ import
org.apache.gravitino.integration.test.util.TestDatabaseName;
import org.apache.gravitino.server.authentication.OAuthConfig;
import org.apache.gravitino.storage.S3Properties;
import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.Container;
@@ -73,6 +76,7 @@ import org.testcontainers.containers.Container;
* Gravitino and Iceberg REST requests, and Iceberg REST S3 credential vending.
*/
@Tag("gravitino-docker-test")
+@EnabledIfSystemProperty(named = ITUtils.TEST_MODE, matches =
ITUtils.DEPLOY_TEST_MODE)
public class TrinoTlsOAuthCredentialVendingIT extends BaseIT {
private static final Logger LOG =
LoggerFactory.getLogger(TrinoTlsOAuthCredentialVendingIT.class);
@@ -100,8 +104,6 @@ public class TrinoTlsOAuthCredentialVendingIT extends
BaseIT {
@BeforeAll
@Override
public void startIntegrationTest() throws Exception {
- Assumptions.assumeFalse(ITUtils.isEmbedded(), "This test requires the
deploy distribution");
-
containerSuite.startLocalStackContainer();
localStack = containerSuite.getLocalStackContainer();
createBucket();
@@ -112,7 +114,8 @@ public class TrinoTlsOAuthCredentialVendingIT extends
BaseIT {
configureGravitino();
copyIcebergAwsBundle();
-
OAuthMockDataProvider.getInstance().setTokenData(mintToken().getBytes(StandardCharsets.UTF_8));
+ OAuthMockDataProvider.getInstance()
+ .setTokenData(mintToken("admin").getBytes(StandardCharsets.UTF_8));
super.startIntegrationTest();
createCatalog();
@@ -166,6 +169,29 @@ public class TrinoTlsOAuthCredentialVendingIT extends
BaseIT {
trinoContainer.executeUpdateSQL("DROP SCHEMA " + catalogName + "." +
schema);
}
+ @Test
+ public void testForwardedUserTokenIsUsedForGravitinoRequests() throws
Exception {
+ assertTrue(trinoContainer.checkSyncCatalogFromGravitino(10, catalogName));
+
+ // The session carries a user token in extra-credentials, so with
+ // gravitino.client.session.forwardUser=true the connector must call
Gravitino as that user
+ // instead of the configured service identity.
+ String forwardedUser = "alice";
+ String schema = "forwarded";
+ try (Connection connection =
+ trinoContainer.openJdbcConnection(
+ forwardedUser, ImmutableMap.of("token",
mintToken(forwardedUser)));
+ Statement statement = connection.createStatement()) {
+ statement.executeUpdate("CREATE SCHEMA " + catalogName + "." + schema);
+ }
+
+ Schema created =
+
client.loadMetalake(metalakeName).loadCatalog(catalogName).asSchemas().loadSchema(schema);
+ assertEquals(forwardedUser, created.auditInfo().creator());
+
+ trinoContainer.executeUpdateSQL("DROP SCHEMA " + catalogName + "." +
schema);
+ }
+
@AfterAll
@Override
public void stopIntegrationTest() throws IOException, InterruptedException {
@@ -336,6 +362,7 @@ public class TrinoTlsOAuthCredentialVendingIT extends
BaseIT {
+ CLIENT_CREDENTIAL
+ "\n"
+ "gravitino.client.oauth2.scope=test\n"
+ + "gravitino.client.session.forwardUser=true\n"
+ "gravitino.iceberg.rest-uri="
+ containerIcebergRestUri()
+ "\n"
@@ -472,9 +499,9 @@ public class TrinoTlsOAuthCredentialVendingIT extends
BaseIT {
}
@SuppressWarnings("JavaUtilDate")
- private String mintToken() {
+ private String mintToken(String subject) {
return Jwts.builder()
- .setSubject("admin")
+ .setSubject(subject)
.setAudience(AUDIENCE)
.setExpiration(new Date(System.currentTimeMillis() + 3_600_000))
.signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256)
diff --git
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoUDFIT.java
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoUDFIT.java
index a5d57fc364..3705573b46 100644
---
a/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoUDFIT.java
+++
b/trino-connector/integration-test/src/test/java/org/apache/gravitino/trino/connector/integration/test/TrinoUDFIT.java
@@ -18,8 +18,6 @@
*/
package org.apache.gravitino.trino.connector.integration.test;
-import static java.lang.Thread.sleep;
-
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -78,33 +76,7 @@ public class TrinoUDFIT extends TrinoQueryITBase {
private static void createHiveCatalog() throws Exception {
Map<String, String> properties = new HashMap<>();
properties.put("metastore.uris", hiveMetastoreUri);
-
- boolean exists = metalake.catalogExists(CATALOG_NAME);
- if (!exists) {
- metalake.createCatalog(
- CATALOG_NAME, Catalog.Type.RELATIONAL, "hive", "UDF test catalog",
properties);
- }
-
- // Wait for catalog to sync to Trino
- boolean catalogReady = false;
- int tries = 180;
- while (!catalogReady && tries-- >= 0) {
- try {
- String result = trinoQueryRunner.runQuery("show catalogs");
- if (result.contains(metalakeName + "." + CATALOG_NAME)) {
- catalogReady = true;
- break;
- }
- } catch (Exception e) {
- LOG.info("Waiting for catalog to sync to Trino");
- }
- sleep(1000);
- }
-
- if (!catalogReady) {
- throw new Exception("Catalog " + CATALOG_NAME + " sync timeout");
- }
-
+ createCatalog(CATALOG_NAME, "hive", properties);
catalog = metalake.loadCatalog(CATALOG_NAME);
}
@@ -156,9 +128,8 @@ public class TrinoUDFIT extends TrinoQueryITBase {
Assertions.assertNotNull(function);
// Query Trino to verify the function is listed
- String trinoCatalogName = metalakeName + "." + CATALOG_NAME;
String showFunctionsQuery =
- String.format("SHOW FUNCTIONS FROM %s.%s", trinoCatalogName,
SCHEMA_NAME);
+ String.format("SHOW FUNCTIONS FROM %s.%s", CATALOG_NAME, SCHEMA_NAME);
String result = trinoQueryRunner.runQuery(showFunctionsQuery);
LOG.info("SHOW FUNCTIONS result: {}", result);
@@ -192,25 +163,50 @@ public class TrinoUDFIT extends TrinoQueryITBase {
Assertions.assertNotNull(function);
// Invoke the function via SELECT and verify the result
- String trinoCatalogName = metalakeName + "." + CATALOG_NAME;
String selectQuery =
- String.format("SELECT %s.%s.%s(5)", trinoCatalogName, SCHEMA_NAME,
functionName);
+ String.format("SELECT %s.%s.%s(5)", CATALOG_NAME, SCHEMA_NAME,
functionName);
String result = trinoQueryRunner.runQuery(selectQuery);
LOG.info("SELECT result: {}", result);
- // Parse the query result and verify the exact numeric output
- String trimmedResult = result.trim();
- Assertions.assertTrue(
- trimmedResult.contains("10"),
- "Expected SELECT test_add_five(5) to return 10. Got: " +
trimmedResult);
- Assertions.assertFalse(
- trimmedResult.contains("100"),
- "Result should be exactly 10, not a number containing 10. Got: " +
trimmedResult);
+ Assertions.assertEquals("\"10\"", result.trim(), "Expected
test_add_five(5) to return 10");
// Cleanup
functionCatalog.dropFunction(NameIdentifier.of(SCHEMA_NAME, functionName));
}
+ @Test
+ public void testBareExpressionBodyCanBeListedAndInvoked() throws Exception {
+ String functionName = "test_double";
+ FunctionCatalog functionCatalog = catalog.asFunctionCatalog();
+
+ // SQL body is a bare expression without the RETURN keyword
+ functionCatalog.registerFunction(
+ NameIdentifier.of(SCHEMA_NAME, functionName),
+ "Doubles the input",
+ FunctionType.SCALAR,
+ true,
+ FunctionDefinitions.of(
+ FunctionDefinitions.of(
+ FunctionParams.of(FunctionParams.of("n",
Types.IntegerType.get())),
+ Types.IntegerType.get(),
+
FunctionImpls.of(FunctionImpls.ofSql(FunctionImpl.RuntimeType.TRINO, "n *
2")))));
+
+ String showResult =
+ trinoQueryRunner.runQuery(
+ String.format("SHOW FUNCTIONS FROM %s.%s", CATALOG_NAME,
SCHEMA_NAME));
+ Assertions.assertTrue(
+ showResult.contains(functionName),
+ "Expected function " + functionName + " to be listed. Got: " +
showResult);
+
+ String selectResult =
+ trinoQueryRunner
+ .runQuery(String.format("SELECT %s.%s.%s(21)", CATALOG_NAME,
SCHEMA_NAME, functionName))
+ .trim();
+ Assertions.assertEquals("\"42\"", selectResult, "Expected test_double(21)
to return 42");
+
+ functionCatalog.dropFunction(NameIdentifier.of(SCHEMA_NAME, functionName));
+ }
+
@Test
public void testListLanguageFunctionsFiltersNonTrinoRuntime() throws
Exception {
String functionName = "spark_only_func";
@@ -233,9 +229,8 @@ public class TrinoUDFIT extends TrinoQueryITBase {
Assertions.assertNotNull(function);
// Query Trino - SPARK runtime function should be filtered out
- String trinoCatalogName = metalakeName + "." + CATALOG_NAME;
String showFunctionsQuery =
- String.format("SHOW FUNCTIONS FROM %s.%s", trinoCatalogName,
SCHEMA_NAME);
+ String.format("SHOW FUNCTIONS FROM %s.%s", CATALOG_NAME, SCHEMA_NAME);
String result = trinoQueryRunner.runQuery(showFunctionsQuery);
LOG.info("SHOW FUNCTIONS result (should not contain spark_only_func): {}",
result);
@@ -283,9 +278,8 @@ public class TrinoUDFIT extends TrinoQueryITBase {
FunctionImpls.ofSql(FunctionImpl.RuntimeType.TRINO,
"RETURN concat(a, b)")))));
// Query Trino to verify both functions are listed
- String trinoCatalogName = metalakeName + "." + CATALOG_NAME;
String showFunctionsQuery =
- String.format("SHOW FUNCTIONS FROM %s.%s", trinoCatalogName,
SCHEMA_NAME);
+ String.format("SHOW FUNCTIONS FROM %s.%s", CATALOG_NAME, SCHEMA_NAME);
String result = trinoQueryRunner.runQuery(showFunctionsQuery);
LOG.info("SHOW FUNCTIONS result: {}", result);
@@ -308,9 +302,8 @@ public class TrinoUDFIT extends TrinoQueryITBase {
catalog.asSchemas().createSchema(emptySchema, "empty schema",
Collections.emptyMap());
}
- String trinoCatalogName = metalakeName + "." + CATALOG_NAME;
String showFunctionsQuery =
- String.format("SHOW FUNCTIONS FROM %s.%s", trinoCatalogName,
emptySchema);
+ String.format("SHOW FUNCTIONS FROM %s.%s", CATALOG_NAME, emptySchema);
String result = trinoQueryRunner.runQuery(showFunctionsQuery);
LOG.info("SHOW FUNCTIONS for empty schema: {}", result);
diff --git
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoMetadata.java
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoMetadata.java
index 7e74259be8..3d578aa72a 100644
---
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoMetadata.java
+++
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoMetadata.java
@@ -85,11 +85,13 @@ import org.apache.gravitino.function.Function;
import org.apache.gravitino.function.FunctionDefinition;
import org.apache.gravitino.function.FunctionImpl;
import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.function.FunctionType;
import org.apache.gravitino.function.SQLImpl;
import org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadata;
import
org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadataAdapter;
import org.apache.gravitino.trino.connector.metadata.GravitinoSchema;
import org.apache.gravitino.trino.connector.metadata.GravitinoTable;
+import org.apache.gravitino.trino.connector.util.TrinoRoutineSpecification;
/**
* The GravitinoMetadata class provides operations for Apache Gravitino
metadata on the Gravitino
@@ -880,9 +882,14 @@ public abstract class GravitinoMetadata implements
ConnectorMetadata {
* Converts a Gravitino function to a collection of Trino LanguageFunction
instances. Only SQL
* implementations with TRINO runtime are included. Each definition with a
Trino SQL
* implementation produces one LanguageFunction. The signature token is
generated from the
- * function name and parameter types.
+ * function name and parameter types, and the stored SQL body is expanded
into a complete Trino
+ * function specification.
*/
private Collection<LanguageFunction> toLanguageFunctions(Function function) {
+ // Trino language functions are scalar SQL routines
+ if (function.functionType() != FunctionType.SCALAR) {
+ return List.of();
+ }
List<LanguageFunction> result = new ArrayList<>();
for (FunctionDefinition definition : function.definitions()) {
for (FunctionImpl impl : definition.impls()) {
@@ -892,9 +899,13 @@ public abstract class GravitinoMetadata implements
ConnectorMetadata {
String sql = ((SQLImpl) impl).sql();
try {
String signatureToken = buildSignatureToken(function.name(),
definition.parameters());
- result.add(new LanguageFunction(signatureToken, sql, List.of(),
Optional.empty()));
+ String specification =
+ TrinoRoutineSpecification.build(
+ function, definition, sql,
metadataAdapter.getDataTypeTransformer());
+ result.add(
+ new LanguageFunction(signatureToken, specification, List.of(),
Optional.empty()));
} catch (TrinoException e) {
- LOG.warn(e, "Failed to build signature token for function %s",
function.name());
+ LOG.warn(e, "Failed to build language function for %s",
function.name());
}
}
}
diff --git
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/TrinoRoutineSpecification.java
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/TrinoRoutineSpecification.java
new file mode 100644
index 0000000000..6cc503eff1
--- /dev/null
+++
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/TrinoRoutineSpecification.java
@@ -0,0 +1,143 @@
+/*
+ * 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.trino.connector.util;
+
+import io.trino.spi.TrinoException;
+import io.trino.spi.type.Type;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.trino.connector.GravitinoErrorCode;
+
+/**
+ * Builds the SQL routine specification Trino expects for a language function
from a Gravitino
+ * function definition and its stored SQL body:
+ *
+ * <pre>FUNCTION name(params) RETURNS type [NOT] DETERMINISTIC SECURITY
INVOKER body</pre>
+ *
+ * <p>The stored body is either a bare expression, which is wrapped in a
{@code RETURN} statement,
+ * or a control statement ({@code RETURN ...} / {@code BEGIN ... END}) that is
used as-is. The form
+ * is decided by the first token of the body; since {@code return}, {@code
begin} and {@code
+ * function} are also valid identifiers, a parameter with one of these names
shadows the keyword and
+ * the body is treated as an expression. {@code SECURITY INVOKER} is always
declared because the
+ * function has no owner identity for Trino's {@code SECURITY DEFINER} default.
+ *
+ * <p>Identifiers are always quoted. Trino resolves routine and parameter
names case-insensitively
+ * regardless of quoting, so this is equivalent to plain identifiers while
also covering reserved
+ * words and names with special characters.
+ */
+public final class TrinoRoutineSpecification {
+
+ private TrinoRoutineSpecification() {}
+
+ /**
+ * Builds the routine specification for one definition of a function.
+ *
+ * @param function the Gravitino function
+ * @param definition the definition whose parameters and return type
describe the routine
+ * @param sql the stored SQL body
+ * @param typeTransformer converts Gravitino types to Trino types
+ * @return the complete routine specification
+ * @throws TrinoException if the definition or body is not supported or a
type cannot be mapped
+ */
+ public static String build(
+ Function function,
+ FunctionDefinition definition,
+ String sql,
+ GeneralDataTypeTransformer typeTransformer) {
+ if (definition.returnType() == null) {
+ throw new TrinoException(
+ GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT,
+ "Function " + function.name() + " has a definition without a return
type");
+ }
+ Set<String> parameterNames =
+ Arrays.stream(definition.parameters())
+ .map(param -> param.name().toLowerCase(Locale.ENGLISH))
+ .collect(Collectors.toSet());
+ String body = stripLeadingComments(sql);
+ if (startsWithKeyword(body, "FUNCTION", parameterNames)) {
+ throw new TrinoException(
+ GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT,
+ "The SQL body of function "
+ + function.name()
+ + " must be an expression or a RETURN/BEGIN statement, not a
full FUNCTION"
+ + " specification");
+ }
+
+ String parameters =
+ Arrays.stream(definition.parameters())
+ .map(
+ param ->
+ quoteIdentifier(param.name())
+ + " "
+ +
formatType(typeTransformer.getTrinoType(param.dataType())))
+ .collect(Collectors.joining(", ", "(", ")"));
+ String statement =
+ startsWithKeyword(body, "RETURN", parameterNames)
+ || startsWithKeyword(body, "BEGIN", parameterNames)
+ ? body
+ : "RETURN " + body;
+ return "FUNCTION "
+ + quoteIdentifier(function.name())
+ + parameters
+ + " RETURNS "
+ + formatType(typeTransformer.getTrinoType(definition.returnType()))
+ + (function.deterministic() ? " DETERMINISTIC" : " NOT DETERMINISTIC")
+ + " SECURITY INVOKER "
+ + statement;
+ }
+
+ private static String quoteIdentifier(String name) {
+ return "\"" + name.replace("\"", "\"\"") + "\"";
+ }
+
+ // Unlike Type#getDisplayName(), the type signature quotes row field names.
+ private static String formatType(Type type) {
+ return type.getTypeSignature().toString();
+ }
+
+ /** Removes leading whitespace and SQL comments so the first token can be
inspected. */
+ static String stripLeadingComments(String sql) {
+ String body = sql.trim();
+ while (true) {
+ if (body.startsWith("--")) {
+ int end = body.indexOf('\n');
+ body = end < 0 ? "" : body.substring(end + 1).trim();
+ } else if (body.startsWith("/*")) {
+ int end = body.indexOf("*/", 2);
+ body = end < 0 ? "" : body.substring(end + 2).trim();
+ } else {
+ return body;
+ }
+ }
+ }
+
+ private static boolean startsWithKeyword(String sql, String keyword,
Set<String> parameterNames) {
+ return sql.regionMatches(true, 0, keyword, 0, keyword.length())
+ && (sql.length() == keyword.length() ||
!isIdentifierChar(sql.charAt(keyword.length())))
+ && !parameterNames.contains(keyword.toLowerCase(Locale.ENGLISH));
+ }
+
+ private static boolean isIdentifierChar(char c) {
+ return Character.isLetterOrDigit(c) || c == '_';
+ }
+}
diff --git
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoMetadataFunction.java
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoMetadataFunction.java
index bc7cfa0568..47d6834873 100644
---
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoMetadataFunction.java
+++
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoMetadataFunction.java
@@ -60,7 +60,9 @@ public class TestGravitinoMetadataFunction {
assertEquals(1, functions.size());
LanguageFunction langFunc = functions.iterator().next();
- assertEquals("RETURN x + 1", langFunc.sql());
+ assertEquals(
+ "FUNCTION \"my_func\"(\"x\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER RETURN x + 1",
+ langFunc.sql());
assertEquals("my_func(integer)", langFunc.signatureToken());
}
@@ -80,7 +82,9 @@ public class TestGravitinoMetadataFunction {
assertEquals(1, functions.size());
LanguageFunction langFunc = functions.iterator().next();
- assertEquals("RETURN x + 1", langFunc.sql());
+ assertEquals(
+ "FUNCTION \"my_func\"(\"x\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER RETURN x + 1",
+ langFunc.sql());
}
@Test
@@ -100,7 +104,9 @@ public class TestGravitinoMetadataFunction {
Collection<LanguageFunction> functions =
metadata.listLanguageFunctions(session, "test_schema");
assertEquals(1, functions.size());
- assertEquals("RETURN 2", functions.iterator().next().sql());
+ assertEquals(
+ "FUNCTION \"trino_func\"(\"x\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER RETURN 2",
+ functions.iterator().next().sql());
}
@Test
@@ -173,8 +179,12 @@ public class TestGravitinoMetadataFunction {
assertEquals(2, functions.size());
List<String> sqlBodies =
functions.stream().map(LanguageFunction::sql).sorted().toList();
- assertEquals("RETURN length(x)", sqlBodies.get(0));
- assertEquals("RETURN x + 1", sqlBodies.get(1));
+ assertEquals(
+ "FUNCTION \"multi_func\"(\"x\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER RETURN x + 1",
+ sqlBodies.get(0));
+ assertEquals(
+ "FUNCTION \"multi_func\"(\"x\" varchar) RETURNS integer DETERMINISTIC
SECURITY INVOKER RETURN length(x)",
+ sqlBodies.get(1));
}
@Test
@@ -217,7 +227,53 @@ public class TestGravitinoMetadataFunction {
assertEquals(1, functions.size());
LanguageFunction lf = functions.iterator().next();
assertEquals("const_func()", lf.signatureToken());
- assertEquals("RETURN 42", lf.sql());
+ assertEquals(
+ "FUNCTION \"const_func\"() RETURNS integer DETERMINISTIC SECURITY
INVOKER RETURN 42",
+ lf.sql());
+ }
+
+ @Test
+ public void testBareExpressionBodyIsWrappedIntoSpecification() {
+ FunctionParam param = createMockParam("n", Types.IntegerType.get());
+ FunctionImpl impl = FunctionImpls.ofSql(FunctionImpl.RuntimeType.TRINO, "n
* 2");
+ FunctionDefinition def = createMockDefinition(new FunctionParam[] {param},
impl);
+ Function function = createMockFunctionWithDefinitions("fn_sql_double",
def);
+
+ Collection<LanguageFunction> functions = listFunctions(function);
+ assertEquals(1, functions.size());
+ assertEquals(
+ "FUNCTION \"fn_sql_double\"(\"n\" integer) RETURNS integer
DETERMINISTIC SECURITY INVOKER RETURN n * 2",
+ functions.iterator().next().sql());
+ }
+
+ @Test
+ public void testNonScalarFunctionIsSkipped() {
+ FunctionParam param = createMockParam("x", Types.IntegerType.get());
+ FunctionImpl impl = FunctionImpls.ofSql(FunctionImpl.RuntimeType.TRINO,
"sum(x)");
+ FunctionDefinition def = createMockDefinition(new FunctionParam[] {param},
impl);
+ Function function = createMockFunctionWithDefinitions("agg_func", def);
+ when(function.functionType()).thenReturn(FunctionType.AGGREGATE);
+
+ assertTrue(listFunctions(function).isEmpty());
+ }
+
+ @Test
+ public void testUnsupportedBodyIsSkipped() {
+ FunctionParam param = createMockParam("x", Types.IntegerType.get());
+ String spec = "FUNCTION \"my_func\"(\"x\" integer) RETURNS bigint BEGIN
RETURN x; END";
+ FunctionImpl impl = FunctionImpls.ofSql(FunctionImpl.RuntimeType.TRINO,
spec);
+ FunctionDefinition def = createMockDefinition(new FunctionParam[] {param},
impl);
+ Function function = createMockFunctionWithDefinitions("my_func", def);
+
+ assertTrue(listFunctions(function).isEmpty());
+ }
+
+ private Collection<LanguageFunction> listFunctions(Function... functions) {
+ CatalogConnectorMetadata catalogMetadata =
mock(CatalogConnectorMetadata.class);
+ when(catalogMetadata.supportsFunctions()).thenReturn(true);
+ when(catalogMetadata.listFunctionInfos("s")).thenReturn(functions);
+ return createTestMetadata(catalogMetadata)
+ .listLanguageFunctions(mock(ConnectorSession.class), "s");
}
private GravitinoMetadata createTestMetadata(CatalogConnectorMetadata
catalogMetadata) {
@@ -252,6 +308,7 @@ public class TestGravitinoMetadataFunction {
private FunctionDefinition createMockDefinition(FunctionParam[] params,
FunctionImpl... impls) {
FunctionDefinition definition = mock(FunctionDefinition.class);
when(definition.parameters()).thenReturn(params);
+ when(definition.returnType()).thenReturn(Types.IntegerType.get());
when(definition.impls()).thenReturn(impls);
return definition;
}
diff --git
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/util/TestTrinoRoutineSpecification.java
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/util/TestTrinoRoutineSpecification.java
new file mode 100644
index 0000000000..c74ca2d85d
--- /dev/null
+++
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/util/TestTrinoRoutineSpecification.java
@@ -0,0 +1,214 @@
+/*
+ * 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.trino.connector.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import io.trino.spi.TrinoException;
+import io.trino.sql.SqlFormatter;
+import io.trino.sql.parser.SqlParser;
+import org.apache.gravitino.function.Function;
+import org.apache.gravitino.function.FunctionDefinition;
+import org.apache.gravitino.function.FunctionParam;
+import org.apache.gravitino.function.FunctionType;
+import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.rel.types.Types;
+import org.junit.jupiter.api.Test;
+
+public class TestTrinoRoutineSpecification {
+
+ private static final GeneralDataTypeTransformer TRANSFORMER = new
GeneralDataTypeTransformer();
+ private static final SqlParser PARSER = new SqlParser();
+
+ @Test
+ public void testBareExpression() {
+ String spec = build("fn_double", "n * 2", param("n",
Types.IntegerType.get()));
+ assertEquals(
+ "FUNCTION \"fn_double\"(\"n\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER"
+ + " RETURN n * 2",
+ spec);
+ parse(spec);
+ }
+
+ @Test
+ public void testReturnStatementIsNotWrapped() {
+ for (String body :
+ new String[] {
+ "RETURN x + 1",
+ "return x + 1",
+ "RETURN(x + 1)",
+ "RETURN/* comment */ x + 1",
+ "/* comment */ RETURN x + 1",
+ "-- comment\nRETURN x + 1",
+ " /* a */ -- b\n /* c */ RETURN x + 1"
+ }) {
+ String spec = build("f", body, param("x", Types.IntegerType.get()));
+ parse(spec);
+ assertTrue(
+ spec.endsWith(
+ " SECURITY INVOKER " +
TrinoRoutineSpecification.stripLeadingComments(body)),
+ spec);
+ }
+ }
+
+ @Test
+ public void testBeginBlockIsNotWrapped() {
+ String spec = build("f", "BEGIN RETURN x; END", param("x",
Types.IntegerType.get()));
+ assertEquals(
+ "FUNCTION \"f\"(\"x\" integer) RETURNS integer DETERMINISTIC SECURITY
INVOKER"
+ + " BEGIN RETURN x; END",
+ spec);
+ parse(spec);
+ }
+
+ @Test
+ public void testIdentifierLikeReturnPrefixIsAnExpression() {
+ String spec = build("f", "returned + 1", param("returned",
Types.IntegerType.get()));
+ assertEquals(
+ "FUNCTION \"f\"(\"returned\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER"
+ + " RETURN returned + 1",
+ spec);
+ parse(spec);
+ }
+
+ @Test
+ public void testParameterNamedLikeKeywordShadowsTheKeyword() {
+ for (String name : new String[] {"return", "begin", "function", "RETURN"})
{
+ String spec = build("f", name + " + 1", param(name,
Types.IntegerType.get()));
+ parse(spec);
+ assertTrue(spec.endsWith(" SECURITY INVOKER RETURN " + name + " + 1"),
spec);
+ assertEquals("RETURN (" + name + " + 1)", formatStatement(spec), spec);
+ }
+ }
+
+ @Test
+ public void testReservedWordsAndSpecialNamesAreQuoted() {
+ String spec =
+ build(
+ "order",
+ "1",
+ param("select", Types.IntegerType.get()),
+ param("value-with-dash", Types.IntegerType.get()),
+ param("say \"hi\"", Types.IntegerType.get()));
+ assertEquals(
+ "FUNCTION \"order\"(\"select\" integer, \"value-with-dash\" integer,"
+ + " \"say \"\"hi\"\"\" integer) RETURNS integer DETERMINISTIC
SECURITY INVOKER"
+ + " RETURN 1",
+ spec);
+ parse(spec);
+ }
+
+ @Test
+ public void testNestedRowFieldNamesAreQuoted() {
+ Type row =
+ Types.StructType.of(
+ Types.StructType.Field.nullableField("value-with-dash",
Types.IntegerType.get()),
+ Types.StructType.Field.nullableField("select",
Types.StringType.get()));
+ Type nested = Types.MapType.valueNullable(Types.StringType.get(),
Types.ListType.nullable(row));
+ String spec = build("f", "x", Types.ListType.nullable(row), param("x",
nested));
+ assertEquals(
+ "FUNCTION \"f\"(\"x\" map(varchar,array(row(\"value-with-dash\"
integer,\"select\""
+ + " varchar)))) RETURNS array(row(\"value-with-dash\"
integer,\"select\" varchar))"
+ + " DETERMINISTIC SECURITY INVOKER RETURN x",
+ spec);
+ parse(spec);
+ }
+
+ @Test
+ public void testNonDeterministic() {
+ String spec =
+ TrinoRoutineSpecification.build(
+ function("f", false), definition(Types.IntegerType.get()),
"random()", TRANSFORMER);
+ assertEquals(
+ "FUNCTION \"f\"() RETURNS integer NOT DETERMINISTIC SECURITY INVOKER
RETURN random()",
+ spec);
+ parse(spec);
+ }
+
+ @Test
+ public void testFullSpecificationIsRejected() {
+ TrinoException e =
+ assertThrows(
+ TrinoException.class,
+ () ->
+ build(
+ "f",
+ "/* c */ FUNCTION f(x integer) RETURNS integer RETURN x",
+ param("x", Types.IntegerType.get())));
+ assertEquals(
+ "The SQL body of function f must be an expression or a RETURN/BEGIN
statement, not a"
+ + " full FUNCTION specification",
+ e.getMessage());
+ }
+
+ @Test
+ public void testMissingReturnTypeIsRejected() {
+ TrinoException e =
+ assertThrows(
+ TrinoException.class,
+ () -> build("f", "1", (Type) null, param("x",
Types.IntegerType.get())));
+ assertEquals("Function f has a definition without a return type",
e.getMessage());
+ }
+
+ private static void parse(String spec) {
+ PARSER.createFunctionSpecification(spec);
+ }
+
+ // Renders the routine body as Trino understands it, so a test can assert
that a parameter
+ // reference survived instead of being swallowed by a keyword.
+ private static String formatStatement(String spec) {
+ String formatted =
SqlFormatter.formatSql(PARSER.createFunctionSpecification(spec));
+ return formatted.substring(formatted.indexOf("RETURN ")).trim();
+ }
+
+ private static String build(String name, String body, FunctionParam...
params) {
+ return build(name, body, Types.IntegerType.get(), params);
+ }
+
+ private static String build(String name, String body, Type returnType,
FunctionParam... params) {
+ return TrinoRoutineSpecification.build(
+ function(name, true), definition(returnType, params), body,
TRANSFORMER);
+ }
+
+ private static Function function(String name, boolean deterministic) {
+ Function function = mock(Function.class);
+ when(function.name()).thenReturn(name);
+ when(function.functionType()).thenReturn(FunctionType.SCALAR);
+ when(function.deterministic()).thenReturn(deterministic);
+ return function;
+ }
+
+ private static FunctionDefinition definition(Type returnType,
FunctionParam... params) {
+ FunctionDefinition definition = mock(FunctionDefinition.class);
+ when(definition.parameters()).thenReturn(params);
+ when(definition.returnType()).thenReturn(returnType);
+ return definition;
+ }
+
+ private static FunctionParam param(String name, Type type) {
+ FunctionParam param = mock(FunctionParam.class);
+ when(param.name()).thenReturn(name);
+ when(param.dataType()).thenReturn(type);
+ return param;
+ }
+}
diff --git
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
index 1aa282714a..c21f6c0a4d 100644
---
a/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
+++
b/web-v2/web/src/app/catalogs/rightContent/entitiesContent/FunctionDetailsPage.js
@@ -100,10 +100,19 @@ const buildSignature = (name, definition) => {
)
}
+// Only SQL implementations with the TRINO runtime are exposed as Trino
language functions
+const isTrinoVisible = impl => impl?.language === 'SQL' && impl?.runtime ===
'TRINO'
+
const buildImplDetails = impl => {
const details = [
{ label: 'Language', value: impl?.language || '-' },
- { label: 'Runtime', value: impl?.runtime || '-' }
+ { label: 'Runtime', value: impl?.runtime || '-' },
+ {
+ label: 'Trino Connector',
+ value: isTrinoVisible(impl)
+ ? 'Eligible (language SQL, runtime TRINO)'
+ : 'Not exposed (requires language SQL and runtime TRINO)'
+ }
]
if (impl?.sql) {