This is an automated email from the ASF dual-hosted git repository.

diqiu50 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 32d08effca [#13208] improvement(spark-connector): Add 
SparkCatalogExtension SPI for external catalogs (#13209)
32d08effca is described below

commit 32d08effca0b47fde982865f45f2729cb579a7b2
Author: Yuhui <[email protected]>
AuthorDate: Wed Sep 16 22:36:14 2026 +0800

    [#13208] improvement(spark-connector): Add SparkCatalogExtension SPI for 
external catalogs (#13209)
    
    ### What changes were proposed in this pull request?
    
    - Add `SparkCatalogExtension`, a `ServiceLoader` SPI that maps a
    Gravitino catalog provider to a Spark catalog class.
    - Add `SparkCatalogExtensions` to load and cache registered extensions,
    matching providers case-insensitively and skipping extensions that fail
    to link.
    - `CatalogNameAdaptor.getCatalogName` returns the extension's catalog
    class when one serves the provider, before falling back to the built-in
    mapping.
    
    ### Why are the changes needed?
    
    Catalog implementations shipped outside the connector jar can be
    registered without changing the connector.
    
    Fixed: #13208
    
    ### Does this PR introduce _any_ user-facing change?
    
    New SPI
    `org.apache.gravitino.spark.connector.plugin.SparkCatalogExtension`.
    Existing behavior is unchanged when no extension is present.
    
    ### How was this patch tested?
    
    Added `TestSparkCatalogExtensions` with a fake extension registered via
    `META-INF/services`; existing `TestCatalogNameAdaptor` passes.
---
 spark-connector/spark-common/build.gradle.kts      |   5 +-
 .../connector/plugin/GravitinoDriverPlugin.java    |   6 +-
 .../connector/plugin/SparkCatalogExtension.java    |  48 +++++++++
 .../connector/plugin/SparkCatalogExtensions.java   | 116 +++++++++++++++++++++
 .../connector/version/CatalogNameAdaptor.java      |   5 +
 .../plugin/BlankSparkCatalogExtension.java         |  35 +++++++
 .../plugin/BrokenSparkCatalogExtension.java        |  35 +++++++
 .../plugin/DuplicateSparkCatalogExtension.java     |  35 +++++++
 .../plugin/FakeSparkCatalogExtension.java          |  36 +++++++
 .../plugin/TestSparkCatalogExtensions.java         |  73 +++++++++++++
 .../plugin/ThrowingSparkCatalogExtension.java      |  35 +++++++
 ...no.spark.connector.plugin.SparkCatalogExtension |  27 +++++
 12 files changed, 454 insertions(+), 2 deletions(-)

diff --git a/spark-connector/spark-common/build.gradle.kts 
b/spark-connector/spark-common/build.gradle.kts
index c7aceb9e16..06d18e2363 100644
--- a/spark-connector/spark-common/build.gradle.kts
+++ b/spark-connector/spark-common/build.gradle.kts
@@ -220,7 +220,8 @@ tasks.clean {
 sourceSets {
   named("test") {
     resources {
-      exclude("**/*")
+      // Keep only SPI registrations; the version modules supply their own 
test configs and data.
+      exclude { !it.relativePath.pathString.startsWith("META-INF") }
     }
   }
 }
@@ -228,6 +229,8 @@ sourceSets {
 val testJar by tasks.registering(Jar::class) {
   archiveClassifier.set("tests")
   from(sourceSets["test"].output)
+  // The SPI fixture is only for this module's tests; keep it off the version 
modules' classpath.
+  exclude("META-INF/services/**")
 }
 
 configurations {
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
index 59636ec0ef..ed3abc71d7 100644
--- 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
@@ -162,7 +162,11 @@ public class GravitinoDriverPlugin implements DriverPlugin 
{
 
     String catalogClassName = CatalogNameAdaptor.getCatalogName(provider);
     if (StringUtils.isBlank(catalogClassName)) {
-      LOG.warn("Skip registering {} because {} is not supported yet.", 
catalogName, provider);
+      LOG.warn(
+          "Skip registering {} because no Spark catalog is bound to provider 
{}: neither built into"
+              + " the connector nor supplied by a SparkCatalogExtension.",
+          catalogName,
+          provider);
       return;
     }
 
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/SparkCatalogExtension.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/SparkCatalogExtension.java
new file mode 100644
index 0000000000..d81c7e90c3
--- /dev/null
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/SparkCatalogExtension.java
@@ -0,0 +1,48 @@
+/*
+ * 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.spark.connector.plugin;
+
+import java.util.ServiceLoader;
+
+/**
+ * Service provider interface that lets a jar outside the connector supply the 
Spark catalog
+ * implementation for a Gravitino catalog provider. Implementations are 
discovered through {@link
+ * ServiceLoader} and take precedence over the catalogs the connector ships 
itself. The {@code
+ * lakehouse-iceberg} and {@code lakehouse-paimon} providers stay behind their 
opt-in flags: an
+ * extension for either is only consulted once the corresponding flag is 
enabled. When several
+ * extensions declare the same provider, the first one discovered is used and 
the others are ignored
+ * with a warning.
+ */
+public interface SparkCatalogExtension {
+
+  /**
+   * The Gravitino catalog provider this extension serves, for example {@code 
jdbc-oracle}.
+   *
+   * @return the provider name, matched case-insensitively
+   */
+  String provider();
+
+  /**
+   * The fully qualified name of the Spark {@code TableCatalog} class 
registered for the provider.
+   * Spark loads it when the session is created, so it must be on the Spark 
class path.
+   *
+   * @return the Spark catalog class name
+   */
+  String catalogClassName();
+}
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/SparkCatalogExtensions.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/SparkCatalogExtensions.java
new file mode 100644
index 0000000000..c76a862962
--- /dev/null
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/SparkCatalogExtensions.java
@@ -0,0 +1,116 @@
+/*
+ * 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.spark.connector.plugin;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Resolves Spark catalog classes contributed through {@link 
SparkCatalogExtension}. */
+public class SparkCatalogExtensions {
+
+  private static final Logger LOG = 
LoggerFactory.getLogger(SparkCatalogExtensions.class);
+
+  private static volatile Map<String, String> catalogClassNamesByProvider;
+
+  private SparkCatalogExtensions() {}
+
+  /**
+   * Looks up the Spark catalog class an extension registered for a provider.
+   *
+   * @param provider the Gravitino catalog provider
+   * @return the catalog class name, or null when no extension serves the 
provider
+   */
+  @Nullable
+  public static String catalogClassName(String provider) {
+    return extensions().get(provider.toLowerCase(Locale.ROOT));
+  }
+
+  private static Map<String, String> extensions() {
+    Map<String, String> loaded = catalogClassNamesByProvider;
+    if (loaded == null) {
+      synchronized (SparkCatalogExtensions.class) {
+        loaded = catalogClassNamesByProvider;
+        if (loaded == null) {
+          loaded = load();
+          catalogClassNamesByProvider = loaded;
+        }
+      }
+    }
+    return loaded;
+  }
+
+  private static Map<String, String> load() {
+    Map<String, String> byProvider = new HashMap<>();
+    Iterator<SparkCatalogExtension> iterator =
+        ServiceLoader.load(SparkCatalogExtension.class, 
extensionClassLoader()).iterator();
+    while (true) {
+      try {
+        if (!iterator.hasNext()) {
+          break;
+        }
+        SparkCatalogExtension extension = iterator.next();
+        String provider = extension.provider();
+        String catalogClassName = extension.catalogClassName();
+        if (StringUtils.isBlank(provider) || 
StringUtils.isBlank(catalogClassName)) {
+          LOG.error(
+              "Skip Spark catalog extension {}: provider and catalog class 
name must not be blank,"
+                  + " got provider={}, catalogClassName={}.",
+              extension.getClass().getName(),
+              provider,
+              catalogClassName);
+          continue;
+        }
+        String normalized = provider.toLowerCase(Locale.ROOT);
+        String previous = byProvider.putIfAbsent(normalized, catalogClassName);
+        if (previous != null) {
+          LOG.warn(
+              "Ignore Spark catalog extension {} for provider {}: already 
served by {}.",
+              catalogClassName,
+              normalized,
+              previous);
+        }
+      } catch (ServiceConfigurationError | LinkageError | RuntimeException e) {
+        // ServiceLoader reports a missing provider class, a malformed 
META-INF entry or a failing
+        // constructor as ServiceConfigurationError; an extension built 
against another Spark
+        // version fails with a LinkageError once its methods run, and a 
misconfigured one may
+        // throw from provider() or catalogClassName(). Skip that entry and 
keep the rest.
+        LOG.error("Skip a Spark catalog extension that cannot be loaded.", e);
+      }
+    }
+    LOG.info("Discovered Spark catalog extensions: {}", byProvider);
+    return byProvider;
+  }
+
+  /**
+   * Prefers the context class loader so extension jars added through {@code 
--jars} are visible
+   * when the connector itself sits on the driver class path.
+   */
+  private static ClassLoader extensionClassLoader() {
+    ClassLoader loader = Thread.currentThread().getContextClassLoader();
+    return loader != null ? loader : 
SparkCatalogExtensions.class.getClassLoader();
+  }
+}
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/version/CatalogNameAdaptor.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/version/CatalogNameAdaptor.java
index c5c4bbcc78..02aa3fcef1 100644
--- 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/version/CatalogNameAdaptor.java
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/version/CatalogNameAdaptor.java
@@ -21,6 +21,7 @@ package org.apache.gravitino.spark.connector.version;
 import com.google.common.collect.ImmutableMap;
 import java.util.Locale;
 import java.util.Map;
+import org.apache.gravitino.spark.connector.plugin.SparkCatalogExtensions;
 import org.apache.spark.package$;
 import org.apache.spark.util.VersionUtils$;
 
@@ -99,6 +100,10 @@ public class CatalogNameAdaptor {
   }
 
   public static String getCatalogName(String provider) {
+    String extensionCatalogName = 
SparkCatalogExtensions.catalogClassName(provider);
+    if (extensionCatalogName != null) {
+      return extensionCatalogName;
+    }
     int majorVersion = VersionUtils$.MODULE$.majorVersion(sparkVersion());
     int minorVersion = VersionUtils$.MODULE$.minorVersion(sparkVersion());
     return getCatalogName(provider, majorVersion, minorVersion);
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/BlankSparkCatalogExtension.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/BlankSparkCatalogExtension.java
new file mode 100644
index 0000000000..822acdd1af
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/BlankSparkCatalogExtension.java
@@ -0,0 +1,35 @@
+/*
+ * 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.spark.connector.plugin;
+
+/** Returns a blank catalog class name and must be skipped. */
+public class BlankSparkCatalogExtension implements SparkCatalogExtension {
+
+  public static final String PROVIDER = "jdbc-blank";
+
+  @Override
+  public String provider() {
+    return PROVIDER;
+  }
+
+  @Override
+  public String catalogClassName() {
+    return " ";
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/BrokenSparkCatalogExtension.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/BrokenSparkCatalogExtension.java
new file mode 100644
index 0000000000..4f9a0d7d73
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/BrokenSparkCatalogExtension.java
@@ -0,0 +1,35 @@
+/*
+ * 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.spark.connector.plugin;
+
+/** Simulates an extension compiled against a class that is absent at runtime. 
*/
+public class BrokenSparkCatalogExtension implements SparkCatalogExtension {
+
+  public static final String PROVIDER = "jdbc-broken";
+
+  @Override
+  public String provider() {
+    throw new NoClassDefFoundError("org/example/AbsentSparkClass");
+  }
+
+  @Override
+  public String catalogClassName() {
+    return "org.example.BrokenCatalog";
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/DuplicateSparkCatalogExtension.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/DuplicateSparkCatalogExtension.java
new file mode 100644
index 0000000000..3c67524079
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/DuplicateSparkCatalogExtension.java
@@ -0,0 +1,35 @@
+/*
+ * 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.spark.connector.plugin;
+
+/** Declares the same provider as {@link FakeSparkCatalogExtension} in another 
case, and loses. */
+public class DuplicateSparkCatalogExtension implements SparkCatalogExtension {
+
+  public static final String CATALOG_CLASS_NAME = 
"org.example.OtherTestDbCatalog";
+
+  @Override
+  public String provider() {
+    return FakeSparkCatalogExtension.PROVIDER.toUpperCase();
+  }
+
+  @Override
+  public String catalogClassName() {
+    return CATALOG_CLASS_NAME;
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/FakeSparkCatalogExtension.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/FakeSparkCatalogExtension.java
new file mode 100644
index 0000000000..e00248822a
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/FakeSparkCatalogExtension.java
@@ -0,0 +1,36 @@
+/*
+ * 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.spark.connector.plugin;
+
+/** Registered through META-INF/services in the test resources. */
+public class FakeSparkCatalogExtension implements SparkCatalogExtension {
+
+  public static final String PROVIDER = "jdbc-testdb";
+  public static final String CATALOG_CLASS_NAME = "org.example.TestDbCatalog";
+
+  @Override
+  public String provider() {
+    return PROVIDER;
+  }
+
+  @Override
+  public String catalogClassName() {
+    return CATALOG_CLASS_NAME;
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestSparkCatalogExtensions.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestSparkCatalogExtensions.java
new file mode 100644
index 0000000000..9c6644ec3f
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestSparkCatalogExtensions.java
@@ -0,0 +1,73 @@
+/*
+ * 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.spark.connector.plugin;
+
+import org.apache.gravitino.spark.connector.version.CatalogNameAdaptor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestSparkCatalogExtensions {
+
+  @Test
+  void testResolvesRegisteredProvider() {
+    Assertions.assertEquals(
+        FakeSparkCatalogExtension.CATALOG_CLASS_NAME,
+        
SparkCatalogExtensions.catalogClassName(FakeSparkCatalogExtension.PROVIDER));
+    Assertions.assertEquals(
+        FakeSparkCatalogExtension.CATALOG_CLASS_NAME,
+        SparkCatalogExtensions.catalogClassName("JDBC-TestDB"));
+  }
+
+  @Test
+  void testFirstDiscoveredExtensionWinsForDuplicateProvider() {
+    Assertions.assertEquals(
+        FakeSparkCatalogExtension.CATALOG_CLASS_NAME,
+        
SparkCatalogExtensions.catalogClassName(FakeSparkCatalogExtension.PROVIDER));
+  }
+
+  @Test
+  void testBlankCatalogClassNameIsSkipped() {
+    Assertions.assertNull(
+        
SparkCatalogExtensions.catalogClassName(BlankSparkCatalogExtension.PROVIDER));
+  }
+
+  @Test
+  void testLinkageFailureIsSkipped() {
+    Assertions.assertNull(
+        
SparkCatalogExtensions.catalogClassName(BrokenSparkCatalogExtension.PROVIDER));
+  }
+
+  @Test
+  void testRuntimeFailureIsSkipped() {
+    Assertions.assertNull(
+        
SparkCatalogExtensions.catalogClassName(ThrowingSparkCatalogExtension.PROVIDER));
+  }
+
+  @Test
+  void testUnknownProviderIsNotServed() {
+    
Assertions.assertNull(SparkCatalogExtensions.catalogClassName("jdbc-unknown"));
+  }
+
+  @Test
+  void testExtensionTakesPrecedenceInCatalogNameAdaptor() {
+    Assertions.assertEquals(
+        FakeSparkCatalogExtension.CATALOG_CLASS_NAME,
+        CatalogNameAdaptor.getCatalogName(FakeSparkCatalogExtension.PROVIDER));
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/ThrowingSparkCatalogExtension.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/ThrowingSparkCatalogExtension.java
new file mode 100644
index 0000000000..1a59360710
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/ThrowingSparkCatalogExtension.java
@@ -0,0 +1,35 @@
+/*
+ * 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.spark.connector.plugin;
+
+/** Simulates an extension whose configuration lookup fails at runtime. */
+public class ThrowingSparkCatalogExtension implements SparkCatalogExtension {
+
+  public static final String PROVIDER = "jdbc-throwing";
+
+  @Override
+  public String provider() {
+    throw new IllegalStateException("required setting is missing");
+  }
+
+  @Override
+  public String catalogClassName() {
+    return "org.example.ThrowingCatalog";
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/resources/META-INF/services/org.apache.gravitino.spark.connector.plugin.SparkCatalogExtension
 
b/spark-connector/spark-common/src/test/resources/META-INF/services/org.apache.gravitino.spark.connector.plugin.SparkCatalogExtension
new file mode 100644
index 0000000000..5fbe03d2e4
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/resources/META-INF/services/org.apache.gravitino.spark.connector.plugin.SparkCatalogExtension
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+# A missing class exercises the ServiceConfigurationError skip path and a 
linkage failure the
+# LinkageError skip path; the fake below must still load.
+org.apache.gravitino.spark.connector.plugin.MissingSparkCatalogExtension
+org.apache.gravitino.spark.connector.plugin.BrokenSparkCatalogExtension
+org.apache.gravitino.spark.connector.plugin.ThrowingSparkCatalogExtension
+org.apache.gravitino.spark.connector.plugin.FakeSparkCatalogExtension
+org.apache.gravitino.spark.connector.plugin.DuplicateSparkCatalogExtension
+org.apache.gravitino.spark.connector.plugin.BlankSparkCatalogExtension

Reply via email to