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) {

Reply via email to