This is an automated email from the ASF dual-hosted git repository.
diqiu50 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 7e26ce9ade [#12526] feat(trino-connector): Support Starburst SPI
compatibility (#12527)
7e26ce9ade is described below
commit 7e26ce9ade193292c4b97b99b2257b6b492e5821
Author: Yuhui <[email protected]>
AuthorDate: Fri Aug 21 10:02:07 2026 +0800
[#12526] feat(trino-connector): Support Starburst SPI compatibility (#12527)
### What changes were proposed in this pull request?
Add compatibility handling for Starburst SPI variants in the Trino
connector, including
`BlockEncodingManager` instantiation, SPI version parsing, and newer
DynamicFilter/ConnectorFactory
SPI methods.
### Why are the changes needed?
Starburst SPI differs from baseline open-source Trino in several
interfaces and internal
constructor signatures. Without these compatibility methods, the
Gravitino connector may
fail to load or execute on Starburst.
Fix: #12526
### Does this PR introduce any user-facing change?
The Gravitino Trino connector can run with compatible Starburst SPI
distributions. No new
configuration or public Gravitino API is introduced.
### How was this patch tested?
```text
./gradlew spotlessApply
./gradlew :trino-connector:trino-connector:test -PskipITs
```
Both commands completed successfully.
---
.../trino/connector/GravitinoConnectorFactory.java | 48 +++++++-
.../trino/connector/GravitinoDynamicFilter.java | 16 +++
.../trino/connector/util/json/JsonCodec.java | 123 ++++++++++++++++++++-
.../connector/TestGravitinoConnectorFactory.java | 74 +++++++++++++
.../trino/connector/util/json/TestJsonCodec.java | 105 ++++++++++++++++++
5 files changed, 362 insertions(+), 4 deletions(-)
diff --git
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
index 1420a60c23..5315342713 100644
---
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
+++
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnectorFactory.java
@@ -19,6 +19,7 @@
package org.apache.gravitino.trino.connector;
import static io.trino.spi.StandardErrorCode.NOT_SUPPORTED;
+import static
org.apache.gravitino.trino.connector.GravitinoErrorCode.GRAVITINO_ILLEGAL_ARGUMENT;
import static
org.apache.gravitino.trino.connector.GravitinoErrorCode.GRAVITINO_RUNTIME_ERROR;
import com.google.common.annotations.VisibleForTesting;
@@ -28,7 +29,12 @@ import io.trino.spi.TrinoException;
import io.trino.spi.connector.Connector;
import io.trino.spi.connector.ConnectorContext;
import io.trino.spi.connector.ConnectorFactory;
+import java.util.Locale;
import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.client.GravitinoAdminClient;
import org.apache.gravitino.trino.connector.catalog.CatalogConnectorContext;
@@ -48,6 +54,9 @@ public class GravitinoConnectorFactory implements
ConnectorFactory {
private static final Logger LOG =
LoggerFactory.getLogger(GravitinoConnectorFactory.class);
private static final int MIN_SUPPORT_TRINO_SPI_VERSION = 435;
private static final int MAX_SUPPORT_TRINO_SPI_VERSION = Integer.MAX_VALUE;
+ private static final Pattern TRINO_SPI_VERSION_PATTERN =
Pattern.compile("^(\\d+)");
+ private static final Set<String> SECURITY_SENSITIVE_PROPERTY_SUFFIXES =
+ Set.of("password", "secret", "token", "credential", "accesskey",
"secretkey", "privatekey");
/** The default connector name. */
public static final String DEFAULT_CONNECTOR_NAME = "gravitino";
@@ -142,6 +151,17 @@ public class GravitinoConnectorFactory implements
ConnectorFactory {
}
}
+ // Note: this method is not annotated with @Override because it does not
exist in the
+ // ConnectorFactory interface of the baseline open-source Trino SPI version
this connector
+ // compiles against. Some newer Trino/Starburst SPI versions declare it as
an abstract method,
+ // where it is dispatched at runtime by signature, providing cross-version
compatibility.
+ public Set<String> getSecuritySensitivePropertyNames(
+ String catalogName, Map<String, String> config, ConnectorContext
context) {
+ return config.keySet().stream()
+ .filter(GravitinoConnectorFactory::isSecuritySensitivePropertyName)
+ .collect(Collectors.toUnmodifiableSet());
+ }
+
protected GravitinoConnector createConnector(CatalogConnectorContext
connectorContext) {
throw new TrinoException(NOT_SUPPORTED, "Should be overridden in
subclass");
}
@@ -157,7 +177,7 @@ public class GravitinoConnectorFactory implements
ConnectorFactory {
private void checkTrinoSpiVersion(ConnectorContext context, GravitinoConfig
config) {
String spiVersion = context.getSpiVersion();
- trinoVersion = Integer.parseInt(spiVersion);
+ trinoVersion = parseTrinoSpiVersion(spiVersion);
// check catalog name with metalake are supported in this trino version
if (!config.singleMetalakeMode() && !supportCatalogNameWithMetalake()) {
@@ -192,6 +212,32 @@ public class GravitinoConnectorFactory implements
ConnectorFactory {
}
}
+ @VisibleForTesting
+ static boolean isSecuritySensitivePropertyName(String propertyName) {
+ String normalizedPropertyName =
propertyName.toLowerCase(Locale.ROOT).replaceAll("[._-]", "");
+ return
SECURITY_SENSITIVE_PROPERTY_SUFFIXES.stream().anyMatch(normalizedPropertyName::endsWith);
+ }
+
+ @VisibleForTesting
+ static int parseTrinoSpiVersion(String spiVersion) {
+ Matcher matcher = TRINO_SPI_VERSION_PATTERN.matcher(spiVersion);
+ if (!matcher.find()) {
+ throw new TrinoException(
+ GRAVITINO_ILLEGAL_ARGUMENT,
+ String.format("Invalid Trino SPI version '%s': expected leading
digits", spiVersion));
+ }
+
+ try {
+ return Integer.parseInt(matcher.group(1));
+ } catch (NumberFormatException e) {
+ throw new TrinoException(
+ GRAVITINO_ILLEGAL_ARGUMENT,
+ String.format(
+ "Invalid Trino SPI version '%s': numeric version is out of
range", spiVersion),
+ e);
+ }
+ }
+
protected boolean supportCatalogNameWithMetalake() {
return true;
}
diff --git
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoDynamicFilter.java
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoDynamicFilter.java
index bfc4f430e1..da75a1128d 100644
---
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoDynamicFilter.java
+++
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoDynamicFilter.java
@@ -21,6 +21,8 @@ package org.apache.gravitino.trino.connector;
import io.trino.spi.connector.ColumnHandle;
import io.trino.spi.connector.DynamicFilter;
import io.trino.spi.predicate.TupleDomain;
+import java.lang.reflect.Method;
+import java.util.OptionalLong;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@@ -55,6 +57,20 @@ class GravitinoDynamicFilter implements DynamicFilter {
return delegate.isAwaitable();
}
+ // Note: this method is not annotated with @Override because it does not
exist in the
+ // DynamicFilter interface of the baseline open-source Trino SPI version
this connector compiles
+ // against. It is present in newer Trino/Starburst SPI versions, where it is
dispatched at
+ // runtime by signature, providing cross-version compatibility. When the
delegate implements the
+ // same method, its value is forwarded so the underlying SPI's timeout
preference is preserved.
+ public OptionalLong getPreferredDynamicFilterTimeout() {
+ try {
+ Method method =
delegate.getClass().getMethod("getPreferredDynamicFilterTimeout");
+ return (OptionalLong) method.invoke(delegate);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ return OptionalLong.empty();
+ }
+ }
+
@Override
public TupleDomain<ColumnHandle> getCurrentPredicate() {
return
delegate.getCurrentPredicate().transformKeys(GravitinoHandle::unWrap);
diff --git
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/json/JsonCodec.java
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/json/JsonCodec.java
index 0566858223..00de547b70 100644
---
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/json/JsonCodec.java
+++
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/util/json/JsonCodec.java
@@ -30,6 +30,7 @@ import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
+import com.google.common.annotations.VisibleForTesting;
import io.airlift.json.RecordAutoDetectModule;
import io.trino.spi.TrinoException;
import io.trino.spi.block.Block;
@@ -47,15 +48,22 @@ import io.trino.spi.type.StandardTypes;
import io.trino.spi.type.Type;
import io.trino.spi.type.TypeManager;
import io.trino.spi.type.TypeSignature;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.Set;
import java.util.function.Function;
import org.apache.gravitino.trino.connector.GravitinoConnectorPluginManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Utility class for JSON serialization and deserialization in the Gravitino
Trino connector.
* Provides functionality for handling various Trino-specific types and plugin
classes.
*/
public class JsonCodec {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JsonCodec.class);
private static volatile ObjectMapper mapper;
private static volatile Type jsonType;
@@ -103,14 +111,123 @@ public class JsonCodec {
static BlockEncodingSerde createBlockEncodingSerde(TypeManager typeManager)
throws Exception {
ClassLoader classLoader = typeManager.getClass().getClassLoader();
- Class blockEncodingManagerClass =
+ Class<?> blockEncodingManagerClass =
classLoader.loadClass("io.trino.metadata.BlockEncodingManager");
- Class internalBlockEncodingSerdeClass =
+ Class<?> internalBlockEncodingSerdeClass =
classLoader.loadClass("io.trino.metadata.InternalBlockEncodingSerde");
+ Object blockEncodingManager =
+ instantiateBlockEncodingManager(blockEncodingManagerClass,
classLoader);
return (BlockEncodingSerde)
internalBlockEncodingSerdeClass
.getConstructor(blockEncodingManagerClass, TypeManager.class)
-
.newInstance(blockEncodingManagerClass.getConstructor().newInstance(),
typeManager);
+ .newInstance(blockEncodingManager, typeManager);
+ }
+
+ /**
+ * Instantiate BlockEncodingManager across Trino/Starburst variants. OSS
Trino exposes a public
+ * no-arg constructor; Starburst replaces it with
BlockEncodingManager(FeaturesConfig) (used to
+ * gate type-specific encodings via feature flags). Newer Trino branches
additionally publish a
+ * {@code Set<BlockEncoding>} variant for Guice multibindings. We probe each
known signature in
+ * order, then fall back to a generic constructor scan that fills unknown
reference parameters
+ * with default values as a last-resort compatibility mechanism.
+ */
+ @VisibleForTesting
+ static Object instantiateBlockEncodingManager(
+ Class<?> blockEncodingManagerClass, ClassLoader classLoader) throws
Exception {
+ try {
+ Object instance =
blockEncodingManagerClass.getConstructor().newInstance();
+ LOG.debug("Instantiated BlockEncodingManager with its public no-argument
constructor");
+ return instance;
+ } catch (NoSuchMethodException ignored) {
+ // fall through to parameterized variants
+ }
+
+ try {
+ Class<?> featuresConfigClass =
classLoader.loadClass("io.trino.FeaturesConfig");
+ Constructor<?> ctor =
blockEncodingManagerClass.getConstructor(featuresConfigClass);
+ Object featuresConfig =
featuresConfigClass.getConstructor().newInstance();
+ Object instance = ctor.newInstance(featuresConfig);
+ LOG.debug("Instantiated BlockEncodingManager with FeaturesConfig");
+ return instance;
+ } catch (NoSuchMethodException | ClassNotFoundException ignored) {
+ // fall through
+ }
+
+ try {
+ Constructor<?> setCtor =
blockEncodingManagerClass.getConstructor(Set.class);
+ Object instance = setCtor.newInstance(Collections.emptySet());
+ LOG.debug("Instantiated BlockEncodingManager with an empty BlockEncoding
set");
+ return instance;
+ } catch (NoSuchMethodException ignored) {
+ // fall through to last-resort scan
+ }
+
+ NoSuchMethodException lastError = null;
+ for (Constructor<?> ctor :
blockEncodingManagerClass.getDeclaredConstructors()) {
+ if (!ctor.trySetAccessible()) {
+ continue;
+ }
+
+ Class<?>[] paramTypes = ctor.getParameterTypes();
+ Object[] args = new Object[paramTypes.length];
+ boolean resolvable = true;
+ for (int i = 0; i < paramTypes.length && resolvable; i++) {
+ if (Set.class.isAssignableFrom(paramTypes[i])) {
+ args[i] = Collections.emptySet();
+ } else if (paramTypes[i].isPrimitive()) {
+ args[i] = defaultPrimitive(paramTypes[i]);
+ } else {
+ Object defaultInstance = tryDefaultInstance(paramTypes[i]);
+ if (defaultInstance == null) {
+ resolvable = false;
+ } else {
+ args[i] = defaultInstance;
+ }
+ }
+ }
+ if (!resolvable) {
+ continue;
+ }
+
+ try {
+ Object instance = ctor.newInstance(args);
+ LOG.debug("Instantiated BlockEncodingManager with fallback constructor
{}", ctor);
+ return instance;
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ lastError =
+ new NoSuchMethodException(
+ "Failed invoking BlockEncodingManager constructor " + ctor +
": " + e.getMessage());
+ }
+ }
+
+ throw new NoSuchMethodException(
+ "No usable BlockEncodingManager constructor found in "
+ + blockEncodingManagerClass.getName()
+ + (lastError == null ? "" : " (last error: " +
lastError.getMessage() + ")"));
+ }
+
+ private static Object tryDefaultInstance(Class<?> type) {
+ try {
+ Constructor<?> ctor = type.getDeclaredConstructor();
+ if (!ctor.trySetAccessible()) {
+ return null;
+ }
+ return ctor.newInstance();
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ return null;
+ }
+ }
+
+ private static Object defaultPrimitive(Class<?> type) {
+ if (type == boolean.class) return false;
+ if (type == char.class) return '\0';
+ if (type == byte.class) return (byte) 0;
+ if (type == short.class) return (short) 0;
+ if (type == int.class) return 0;
+ if (type == long.class) return 0L;
+ if (type == float.class) return 0f;
+ if (type == double.class) return 0d;
+ throw new IllegalArgumentException("Unsupported primitive type: " + type);
}
static void registerHandleSerializationModule(ObjectMapper objectMapper) {
diff --git
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactory.java
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactory.java
new file mode 100644
index 0000000000..0b0c71c6b6
--- /dev/null
+++
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.trino.spi.TrinoException;
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class TestGravitinoConnectorFactory {
+
+ @Test
+ void testSecuritySensitivePropertyNames() {
+ GravitinoConnectorFactory factory = new GravitinoConnectorFactory(null);
+ Map<String, String> config =
+ Map.ofEntries(
+ Map.entry("trino.jdbc.password", "password"),
+ Map.entry("gravitino.client.oauth2.clientSecret", "secret"),
+ Map.entry("hive.s3.aws-access-key", "access-key"),
+ Map.entry("hive.s3.aws-secret-key", "secret-key"),
+ Map.entry("authentication.token", "token"),
+ Map.entry("oauth2.credential", "credential"),
+ Map.entry("tls.private_key", "private-key"),
+ Map.entry("gravitino.uri", "uri"));
+
+ Set<String> sensitivePropertyNames =
+ factory.getSecuritySensitivePropertyNames("catalog", config, null);
+
+ assertThat(sensitivePropertyNames)
+ .containsExactlyInAnyOrder(
+ "trino.jdbc.password",
+ "gravitino.client.oauth2.clientSecret",
+ "hive.s3.aws-access-key",
+ "hive.s3.aws-secret-key",
+ "authentication.token",
+ "oauth2.credential",
+ "tls.private_key");
+ assertThatThrownBy(() -> sensitivePropertyNames.add("another.password"))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ @Test
+ void testParseTrinoSpiVersion() {
+
assertThat(GravitinoConnectorFactory.parseTrinoSpiVersion("478")).isEqualTo(478);
+
assertThat(GravitinoConnectorFactory.parseTrinoSpiVersion("478-e")).isEqualTo(478);
+
assertThat(GravitinoConnectorFactory.parseTrinoSpiVersion("478.1-vendor")).isEqualTo(478);
+ }
+
+ @Test
+ void testRejectInvalidTrinoSpiVersion() {
+ assertThatThrownBy(() ->
GravitinoConnectorFactory.parseTrinoSpiVersion("starburst-478"))
+ .isInstanceOf(TrinoException.class)
+ .hasMessage("Invalid Trino SPI version 'starburst-478': expected
leading digits");
+ }
+}
diff --git
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/util/json/TestJsonCodec.java
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/util/json/TestJsonCodec.java
new file mode 100644
index 0000000000..1b76b3000b
--- /dev/null
+++
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/util/json/TestJsonCodec.java
@@ -0,0 +1,105 @@
+/*
+ * 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.json;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import io.trino.FeaturesConfig;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+class TestJsonCodec {
+
+ static class NoArgManager {}
+
+ static class FeaturesConfigManager {
+ final FeaturesConfig featuresConfig;
+
+ public FeaturesConfigManager(FeaturesConfig featuresConfig) {
+ this.featuresConfig = featuresConfig;
+ }
+ }
+
+ static class SetManager {
+ final Set<?> encodings;
+
+ public SetManager(Set<?> encodings) {
+ this.encodings = encodings;
+ }
+ }
+
+ static class FallbackManager {
+ final int flag;
+
+ private FallbackManager(int flag) {
+ this.flag = flag;
+ }
+ }
+
+ static class UnresolvableDependency {
+ UnresolvableDependency(String required) {}
+ }
+
+ static class UnresolvableManager {
+ final UnresolvableDependency dependency;
+
+ private UnresolvableManager(UnresolvableDependency dependency) {
+ this.dependency = dependency;
+ }
+ }
+
+ private static final ClassLoader CLASS_LOADER =
TestJsonCodec.class.getClassLoader();
+
+ @Test
+ void testPrefersNoArgConstructor() throws Exception {
+ Object instance =
JsonCodec.instantiateBlockEncodingManager(NoArgManager.class, CLASS_LOADER);
+ assertThat(instance).isInstanceOf(NoArgManager.class);
+ }
+
+ @Test
+ void testFallsBackToFeaturesConfigConstructor() throws Exception {
+ Object instance =
+ JsonCodec.instantiateBlockEncodingManager(FeaturesConfigManager.class,
CLASS_LOADER);
+ assertThat(instance).isInstanceOfSatisfying(FeaturesConfigManager.class, m
-> {});
+ }
+
+ @Test
+ void testFallsBackToSetConstructor() throws Exception {
+ Object instance =
JsonCodec.instantiateBlockEncodingManager(SetManager.class, CLASS_LOADER);
+ assertThat(instance)
+ .isInstanceOfSatisfying(SetManager.class, m ->
assertThat(m.encodings).isEmpty());
+ }
+
+ @Test
+ void testFallsBackToReflectiveScanForOtherConstructors() throws Exception {
+ Object instance =
+ JsonCodec.instantiateBlockEncodingManager(FallbackManager.class,
CLASS_LOADER);
+ assertThat(instance)
+ .isInstanceOfSatisfying(FallbackManager.class, m ->
assertThat(m.flag).isEqualTo(0));
+ }
+
+ @Test
+ void testThrowsWhenNoConstructorIsResolvable() {
+ assertThatThrownBy(
+ () ->
+
JsonCodec.instantiateBlockEncodingManager(UnresolvableManager.class,
CLASS_LOADER))
+ .isInstanceOf(NoSuchMethodException.class);
+ }
+}