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 10cc489abd [Cherry-pick to branch-1.3] [#11285] fix(iceberg-rest): 
default JDBC catalog to strict mode (#11318) (#11323)
10cc489abd is described below

commit 10cc489abddd85c57b6e25e908b1975bbd93187f
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Jun 1 20:40:29 2026 +0800

    [Cherry-pick to branch-1.3] [#11285] fix(iceberg-rest): default JDBC 
catalog to strict mode (#11318) (#11323)
    
    **Cherry-pick Information:**
    - Original commit: 012f72d413bd63c0c94d4a864f81174c25094356
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: roryqi <[email protected]>
---
 .../lakehouse/iceberg/IcebergConstants.java        |  2 +
 docs/iceberg-rest-service.md                       |  1 +
 .../iceberg/common/utils/IcebergCatalogUtil.java   |  6 +++
 .../common/utils/TestIcebergCatalogUtil.java       | 51 +++++++++++++++++++++
 .../integration/test/IcebergRESTServiceIT.java     | 52 ++++++++++++++++++++++
 5 files changed, 112 insertions(+)

diff --git 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergConstants.java
 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergConstants.java
index cbfe027188..58606f43f3 100644
--- 
a/catalogs/catalog-common/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergConstants.java
+++ 
b/catalogs/catalog-common/src/main/java/org/apache/gravitino/catalog/lakehouse/iceberg/IcebergConstants.java
@@ -38,6 +38,8 @@ public class IcebergConstants {
   public static final String GRAVITINO_JDBC_SCHEMA_VERSION = 
"jdbc-schema-version";
   public static final String ICEBERG_JDBC_SCHEMA_VERSION = 
"jdbc.schema-version";
 
+  public static final String ICEBERG_JDBC_STRICT_MODE = "jdbc.strict-mode";
+
   public static final String GRAVITINO_JDBC_DRIVER = "jdbc-driver";
   public static final String WAREHOUSE = "warehouse";
   public static final String URI = "uri";
diff --git a/docs/iceberg-rest-service.md b/docs/iceberg-rest-service.md
index d050e6d21e..d25832d6cb 100644
--- a/docs/iceberg-rest-service.md
+++ b/docs/iceberg-rest-service.md
@@ -119,6 +119,7 @@ The Gravitino Iceberg REST catalog service uses the memory 
catalog backend by de
 | `gravitino.iceberg-rest.jdbc-initialize`      | Whether to initialize the 
meta tables when creating the JDBC catalog.                                     
                                                                                
                                                             | `true`           
       | No       | 0.2.0         |
 | `gravitino.iceberg-rest.jdbc-driver`          | `com.mysql.jdbc.Driver` or 
`com.mysql.cj.jdbc.Driver` for MySQL, `org.postgresql.Driver` for PostgreSQL.   
                                                                                
                                                            | (none)            
      | Yes      | 0.3.0         |
 | `gravitino.iceberg-rest.jdbc-schema-version`  | The schema version of the 
JDBC catalog. Defaults to `V1` to enable view support. Set to `V0` only if you 
need to opt out of view support. Once the underlying database is migrated to 
V1, this property is no longer required on subsequent restarts.  | `V1`         
           | No       | 1.2.0         |
+| `gravitino.iceberg-rest.jdbc.strict-mode`     | Whether the JDBC catalog 
runs in strict mode. Defaults to `true` so that creating a table or view in a 
namespace that does not exist fails with `NoSuchNamespace` (HTTP 404), matching 
the Iceberg REST specification. Set to `false` to restore the legacy behavior 
of implicitly creating the namespace.  | `true`                  | No       | 
1.3.0         |
 
 If you have a JDBC Iceberg catalog prior, you must set `catalog-backend-name` 
to keep consistent with your Jdbc Iceberg catalog name to operate the prior 
namespace and tables.
 
diff --git 
a/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/utils/IcebergCatalogUtil.java
 
b/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/utils/IcebergCatalogUtil.java
index 8935a84d9b..8af0c6325c 100644
--- 
a/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/utils/IcebergCatalogUtil.java
+++ 
b/iceberg/iceberg-common/src/main/java/org/apache/gravitino/iceberg/common/utils/IcebergCatalogUtil.java
@@ -115,6 +115,12 @@ public class IcebergCatalogUtil {
     // Default to V1 schema to support view operations; can be overridden by 
explicit config.
     properties.putIfAbsent(IcebergConstants.ICEBERG_JDBC_SCHEMA_VERSION, "V1");
 
+    // Default to strict mode so that creating a table or view in a 
non-existent namespace fails
+    // with NoSuchNamespaceException (HTTP 404) instead of implicitly creating 
the namespace,
+    // matching the Iceberg REST spec and the memory backend behavior. Can be 
overridden by
+    // explicit config.
+    properties.putIfAbsent(IcebergConstants.ICEBERG_JDBC_STRICT_MODE, "true");
+
     HdfsConfiguration hdfsConfiguration = new HdfsConfiguration();
     properties.forEach(hdfsConfiguration::set);
     jdbcCatalog.setConf(hdfsConfiguration);
diff --git 
a/iceberg/iceberg-common/src/test/java/org/apache/gravitino/iceberg/common/utils/TestIcebergCatalogUtil.java
 
b/iceberg/iceberg-common/src/test/java/org/apache/gravitino/iceberg/common/utils/TestIcebergCatalogUtil.java
index 6f9cf71194..bf0fbd8e2c 100644
--- 
a/iceberg/iceberg-common/src/test/java/org/apache/gravitino/iceberg/common/utils/TestIcebergCatalogUtil.java
+++ 
b/iceberg/iceberg-common/src/test/java/org/apache/gravitino/iceberg/common/utils/TestIcebergCatalogUtil.java
@@ -19,22 +19,31 @@
 
 package org.apache.gravitino.iceberg.common.utils;
 
+import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
 import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergCatalogBackend;
 import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
 import org.apache.gravitino.iceberg.common.IcebergConfig;
 import org.apache.iceberg.CatalogProperties;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Catalog;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.NoSuchNamespaceException;
 import org.apache.iceberg.hive.HiveCatalog;
 import org.apache.iceberg.inmemory.InMemoryCatalog;
 import org.apache.iceberg.jdbc.JdbcCatalog;
 import org.apache.iceberg.jdbc.JdbcCatalogWithMetadataLocationSupport;
+import org.apache.iceberg.types.Types;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 public class TestIcebergCatalogUtil {
 
+  @TempDir private Path warehouse;
+
   @Test
   void testLoadCatalog() {
     Catalog catalog;
@@ -117,6 +126,48 @@ public class TestIcebergCatalogUtil {
         "Explicitly configured V0 schema should not be overridden to V1");
   }
 
+  @Test
+  void testJdbcCatalogDefaultStrictModeRejectsImplicitNamespace() {
+    Map<String, String> properties = new HashMap<>();
+    properties.put(
+        CatalogProperties.URI, 
"jdbc:sqlite:file:strict_default?mode=memory&cache=shared");
+    properties.put(CatalogProperties.WAREHOUSE_LOCATION, warehouse.toString());
+    properties.put(IcebergConstants.GRAVITINO_JDBC_DRIVER, "org.sqlite.JDBC");
+    properties.put(IcebergConstants.ICEBERG_JDBC_USER, "test");
+    properties.put(IcebergConstants.ICEBERG_JDBC_PASSWORD, "test");
+    // jdbc.strict-mode is intentionally not set; default should be true
+    Catalog catalog =
+        IcebergCatalogUtil.loadCatalogBackend(
+            IcebergCatalogBackend.JDBC, new IcebergConfig(properties));
+
+    Schema schema = new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get()));
+    TableIdentifier identifier = TableIdentifier.of(Namespace.of("absent_ns"), 
"t");
+    // Strict mode must reject creating a table in a namespace that does not 
exist.
+    Assertions.assertThrows(
+        NoSuchNamespaceException.class, () -> catalog.createTable(identifier, 
schema));
+  }
+
+  @Test
+  void testJdbcCatalogExplicitStrictModeDisabledAllowsImplicitNamespace() {
+    Map<String, String> properties = new HashMap<>();
+    properties.put(
+        CatalogProperties.URI, 
"jdbc:sqlite:file:strict_disabled?mode=memory&cache=shared");
+    properties.put(CatalogProperties.WAREHOUSE_LOCATION, warehouse.toString());
+    properties.put(IcebergConstants.GRAVITINO_JDBC_DRIVER, "org.sqlite.JDBC");
+    properties.put(IcebergConstants.ICEBERG_JDBC_USER, "test");
+    properties.put(IcebergConstants.ICEBERG_JDBC_PASSWORD, "test");
+    // Explicitly disable strict mode; loadJdbcCatalog must not override it 
back to true.
+    properties.put(IcebergConstants.ICEBERG_JDBC_STRICT_MODE, "false");
+    Catalog catalog =
+        IcebergCatalogUtil.loadCatalogBackend(
+            IcebergCatalogBackend.JDBC, new IcebergConfig(properties));
+
+    Schema schema = new Schema(Types.NestedField.required(1, "id", 
Types.IntegerType.get()));
+    TableIdentifier identifier = 
TableIdentifier.of(Namespace.of("implicit_ns"), "t");
+    // With strict mode disabled the namespace is created implicitly, so this 
must succeed.
+    Assertions.assertNotNull(catalog.createTable(identifier, schema));
+  }
+
   @Test
   void testValidLoadCustomCatalog() {
     Catalog catalog;
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
index 51aca2dc02..9109f9734a 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergRESTServiceIT.java
@@ -305,6 +305,58 @@ public abstract class IcebergRESTServiceIT extends 
IcebergRESTServiceBaseIT {
         () -> sql(String.format("CREATE TABLE %s.create_foo1", 
getTestNamespace())));
   }
 
+  @Test
+  void testCreateTableInNonExistentNamespace() {
+    // The child namespace is intentionally never created.
+    String namespaceName = getTestNamespace("absent_create_ns");
+    Throwable thrown =
+        Assertions.assertThrows(
+            Throwable.class,
+            () ->
+                sql(
+                    String.format(
+                        "CREATE TABLE %s.create_in_absent(id bigint) using 
iceberg",
+                        namespaceName)));
+    // Creating a table in a namespace that does not exist must surface a 
NoSuchNamespace error
+    // (HTTP 404) instead of implicitly creating the namespace. This holds for 
the memory and hive
+    // backends, and for the JDBC backend now that strict mode is enabled by 
default.
+    Assertions.assertTrue(
+        isNoSuchNamespace(thrown), () -> "Expected a NoSuchNamespace error but 
got: " + thrown);
+  }
+
+  @Test
+  @EnabledIf("isSupportsViewCatalog")
+  void testCreateViewInNonExistentNamespace() {
+    String sourceTable = getTestNamespace() + ".view_source_for_absent_ns";
+    sql(String.format("CREATE TABLE %s (id bigint) using iceberg", 
sourceTable));
+    // The child namespace is intentionally never created.
+    String namespaceName = getTestNamespace("absent_view_ns");
+    Throwable thrown =
+        Assertions.assertThrows(
+            Throwable.class,
+            () ->
+                sql(
+                    String.format(
+                        "CREATE VIEW %s.view_in_absent AS SELECT * FROM %s",
+                        namespaceName, sourceTable)));
+    // Strict mode must also reject creating a view in a namespace that does 
not exist, instead of
+    // implicitly creating the namespace.
+    Assertions.assertTrue(
+        isNoSuchNamespace(thrown), () -> "Expected a NoSuchNamespace error but 
got: " + thrown);
+  }
+
+  // Walks the cause chain because the missing namespace may surface as either 
Spark's or Iceberg's
+  // NoSuchNamespaceException, and Spark may wrap it.
+  private static boolean isNoSuchNamespace(Throwable thrown) {
+    for (Throwable t = thrown; t != null; t = t.getCause()) {
+      if (t instanceof NoSuchNamespaceException
+          || t instanceof 
org.apache.iceberg.exceptions.NoSuchNamespaceException) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   @Test
   void testDropTable() {
     sql(

Reply via email to