From 90aa75ccc457f49eae6ca693c19cf7741f60ca4f Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Sun, 29 Jun 2025 12:28:19 -0700 Subject: [PATCH 1/7] Fail table registration when target namespace doesn't exist --- .../TestRegisterTableProcedure.java | 29 +++++++++++++++++++ .../procedures/RegisterTableProcedure.java | 7 +++++ .../iceberg/spark/SparkCatalogConfig.java | 8 +++-- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java index a06a67b7d612..39a9684428c6 100644 --- a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java +++ b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java @@ -19,6 +19,7 @@ package org.apache.iceberg.spark.extensions; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.atIndex; import java.util.List; @@ -82,4 +83,32 @@ public void testRegisterTable() throws NoSuchTableException, ParseException { .as("Should have the right datafile count in the procedure result") .contains(originalFileCount, atIndex(2)); } + + @TestTemplate + public void testRegisterTableToNonexistentNamespace() + throws NoSuchTableException, ParseException { + String targetNameWithNonexistentNamespace = + (catalogName.equals("spark_catalog") ? "" : catalogName + ".") + + "missing_namespace." + + "register_table"; + long numRows = 1000; + + sql("CREATE TABLE %s (id int, data string) using ICEBERG", tableName); + spark + .range(0, numRows) + .withColumn("data", functions.col("id").cast(DataTypes.StringType)) + .writeTo(tableName) + .append(); + + Table table = Spark3Util.loadIcebergTable(spark, tableName); + String metadataJson = TableUtil.metadataFileLocation(table); + + assertThatThrownBy( + () -> + sql( + "CALL %s.system.register_table('%s', '%s')", + catalogName, targetNameWithNonexistentNamespace, metadataJson)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Cannot register table to nonexistent target namespace"); + } } diff --git a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java index 857949e052c8..c12c9000f6d5 100644 --- a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java +++ b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java @@ -22,6 +22,7 @@ import org.apache.iceberg.SnapshotSummary; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.SupportsNamespaces; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.spark.Spark3Util; @@ -86,6 +87,12 @@ public InternalRow[] call(InternalRow args) { "Cannot handle an empty argument metadata_file"); Catalog icebergCatalog = ((HasIcebergCatalog) tableCatalog()).icebergCatalog(); + if (tableName.hasNamespace() && icebergCatalog instanceof SupportsNamespaces) { + Preconditions.checkArgument( + ((SupportsNamespaces) icebergCatalog).namespaceExists(tableName.namespace()), + "Cannot register table to nonexistent target namespace %s", + tableName.namespace()); + } Table table = icebergCatalog.registerTable(tableName, metadataFile); Long currentSnapshotId = null; Long totalDataFiles = null; diff --git a/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java b/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java index ef6c49db57a2..4e872f049783 100644 --- a/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java +++ b/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java @@ -28,8 +28,12 @@ public enum SparkCatalogConfig { "testhive", SparkCatalog.class.getName(), ImmutableMap.of( - "type", "hive", - "default-namespace", "default")), + "type", + "hive", + "default-namespace", + "default", + CatalogProperties.CACHE_ENABLED, + "false")), HADOOP( "testhadoop", SparkCatalog.class.getName(), From aa1f797f8a058153fa66effb9c7888ec60b48a4a Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Tue, 1 Jul 2025 23:29:51 -0700 Subject: [PATCH 2/7] Revert changes to Spark procedure --- .../iceberg/spark/procedures/RegisterTableProcedure.java | 7 ------- .../java/org/apache/iceberg/spark/SparkCatalogConfig.java | 8 ++------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java index c12c9000f6d5..857949e052c8 100644 --- a/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java +++ b/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/procedures/RegisterTableProcedure.java @@ -22,7 +22,6 @@ import org.apache.iceberg.SnapshotSummary; import org.apache.iceberg.Table; import org.apache.iceberg.catalog.Catalog; -import org.apache.iceberg.catalog.SupportsNamespaces; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.relocated.com.google.common.base.Preconditions; import org.apache.iceberg.spark.Spark3Util; @@ -87,12 +86,6 @@ public InternalRow[] call(InternalRow args) { "Cannot handle an empty argument metadata_file"); Catalog icebergCatalog = ((HasIcebergCatalog) tableCatalog()).icebergCatalog(); - if (tableName.hasNamespace() && icebergCatalog instanceof SupportsNamespaces) { - Preconditions.checkArgument( - ((SupportsNamespaces) icebergCatalog).namespaceExists(tableName.namespace()), - "Cannot register table to nonexistent target namespace %s", - tableName.namespace()); - } Table table = icebergCatalog.registerTable(tableName, metadataFile); Long currentSnapshotId = null; Long totalDataFiles = null; diff --git a/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java b/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java index 4e872f049783..ef6c49db57a2 100644 --- a/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java +++ b/spark/v4.0/spark/src/test/java/org/apache/iceberg/spark/SparkCatalogConfig.java @@ -28,12 +28,8 @@ public enum SparkCatalogConfig { "testhive", SparkCatalog.class.getName(), ImmutableMap.of( - "type", - "hive", - "default-namespace", - "default", - CatalogProperties.CACHE_ENABLED, - "false")), + "type", "hive", + "default-namespace", "default")), HADOOP( "testhadoop", SparkCatalog.class.getName(), From 25871badc5a9c4c7d40c249a3fb25a5cca0dd95e Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Sat, 5 Jul 2025 10:37:48 -0700 Subject: [PATCH 3/7] Check namespace existence before registering tables --- .../apache/iceberg/BaseMetastoreCatalog.java | 15 ++++++++ .../org/apache/iceberg/jdbc/JdbcCatalog.java | 11 ++++++ .../iceberg/rest/RESTSessionCatalog.java | 9 ++++- .../apache/iceberg/catalog/CatalogTests.java | 13 +++++++ .../apache/iceberg/jdbc/TestJdbcCatalog.java | 36 +++++++++++++++++++ .../TestRegisterTableProcedure.java | 14 ++++---- 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java b/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java index 29068df380a9..ced66b5d17a0 100644 --- a/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java +++ b/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java @@ -22,9 +22,12 @@ import java.io.IOException; import java.util.Map; import org.apache.iceberg.catalog.Catalog; +import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.catalog.SupportsNamespaces; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.AlreadyExistsException; import org.apache.iceberg.exceptions.CommitFailedException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.io.InputFile; import org.apache.iceberg.metrics.MetricsReporter; @@ -78,6 +81,8 @@ public Table registerTable(TableIdentifier identifier, String metadataFileLocati metadataFileLocation != null && !metadataFileLocation.isEmpty(), "Cannot register an empty metadata file location as a table"); + targetNamespaceExists(identifier); + // Throw an exception if this table already exists in the catalog. if (tableExists(identifier)) { throw new AlreadyExistsException("Table already exists: %s", identifier); @@ -91,6 +96,16 @@ public Table registerTable(TableIdentifier identifier, String metadataFileLocati return new BaseTable(ops, fullTableName(name(), identifier), metricsReporter()); } + protected void targetNamespaceExists(TableIdentifier identifier) { + Namespace namespace = identifier.namespace(); + if (this instanceof SupportsNamespaces + && !(((SupportsNamespaces) this).namespaceExists(namespace))) { + throw new NoSuchNamespaceException( + "Cannot register table %s to catalog %s. Namespace %s does not exist", + identifier, name(), namespace); + } + } + @Override public TableBuilder buildTable(TableIdentifier identifier, Schema schema) { return new BaseMetastoreCatalogTableBuilder(identifier, schema); diff --git a/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java b/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java index a413e6c4e29e..ddd44e512959 100644 --- a/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java +++ b/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java @@ -374,6 +374,17 @@ public void renameTable(TableIdentifier from, TableIdentifier to) { } } + @Override + public void targetNamespaceExists(TableIdentifier identifier) { + Namespace namespace = identifier.namespace(); + if (PropertyUtil.propertyAsBoolean(catalogProperties, JdbcUtil.STRICT_MODE_PROPERTY, false) + && !JdbcUtil.namespaceExists(catalogName, connections, namespace)) { + throw new NoSuchNamespaceException( + "Cannot register table %s to catalog %s. Namespace %s does not exist", + identifier, catalogName, namespace); + } + } + @Override public String name() { return catalogName; diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java index edcbc5229362..ae0517d75f7e 100644 --- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java +++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java @@ -499,6 +499,13 @@ public Table registerTable( "Invalid metadata file location: %s", metadataFileLocation); + Namespace namespace = ident.namespace(); + if (!namespaceExists(context, namespace)) { + throw new NoSuchNamespaceException( + "Cannot register table %s to catalog %s. Namespace %s does not exist", + ident, name(), namespace); + } + RegisterTableRequest request = ImmutableRegisterTableRequest.builder() .name(ident.name()) @@ -510,7 +517,7 @@ public Table registerTable( client .withAuthSession(contextualSession) .post( - paths.register(ident.namespace()), + paths.register(namespace), request, LoadTableResponse.class, Map.of(), diff --git a/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java b/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java index c2fd24856fb2..06a5781131ac 100644 --- a/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java +++ b/core/src/test/java/org/apache/iceberg/catalog/CatalogTests.java @@ -3147,6 +3147,19 @@ public void testRegisterTable() { assertThat(catalog.tableExists(TABLE)).isFalse(); } + @Test + public void testRegisterTableToNonExistingNamespace() { + assumeThat(requiresNamespaceCreate()) + .isTrue(); // exclude TestJdbcCatalog and TestJdbcCatalogWithV1Schema + TableIdentifier targetIdentifier = TableIdentifier.of("non_existing", "table"); + assertThatThrownBy( + () -> + catalog() + .registerTable(targetIdentifier, "table_metadata_loc_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } + @Test public void testRegisterExistingTable() { C catalog = catalog(); diff --git a/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java b/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java index 0b7315787a26..22f400b6dca8 100644 --- a/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java +++ b/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java @@ -1109,6 +1109,42 @@ public void testCommitExceptionWithMessage() { } } + @Test + public void testRegisterTableToNonExistingNamespace() { + try (JdbcCatalog jdbcCatalog = initCatalog("non_strict_jdbc_catalog", ImmutableMap.of())) { + TableIdentifier identifier = TableIdentifier.of("a", "t1"); + jdbcCatalog.createNamespace(identifier.namespace()); + jdbcCatalog.createTable(identifier, SCHEMA); + Table table = jdbcCatalog.loadTable(identifier); + TableOperations ops = ((BaseTable) table).operations(); + String metadataLocation = ops.current().metadataFileLocation(); + + TableIdentifier registeredTableId = TableIdentifier.of("non-existing", "t1"); + Table registeredTable = jdbcCatalog.registerTable(registeredTableId, metadataLocation); + + assertThat(registeredTable).isNotNull(); + assertThat(jdbcCatalog.tableExists(registeredTableId)) + .as("Registered table must exist") + .isTrue(); + } + } + + @Test + public void testRegisterTableToNonExistingNamespaceStrictMode() { + try (JdbcCatalog jdbcCatalog = + initCatalog( + "strict_jdbc_catalog", ImmutableMap.of(JdbcUtil.STRICT_MODE_PROPERTY, "true"))) { + + assertThatThrownBy( + () -> + jdbcCatalog.registerTable( + TableIdentifier.of("non-existing", "t1"), + "table_metadata_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } + } + private String createMetadataLocationViaJdbcCatalog(TableIdentifier identifier) throws SQLException { // temporary connection just to actually create a concrete metadata location diff --git a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java index 39a9684428c6..bf1d65e8773c 100644 --- a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java +++ b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java @@ -26,6 +26,7 @@ import org.apache.iceberg.ParameterizedTestExtension; import org.apache.iceberg.Table; import org.apache.iceberg.TableUtil; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.spark.Spark3Util; import org.apache.spark.sql.catalyst.analysis.NoSuchTableException; import org.apache.spark.sql.catalyst.parser.ParseException; @@ -87,10 +88,6 @@ public void testRegisterTable() throws NoSuchTableException, ParseException { @TestTemplate public void testRegisterTableToNonexistentNamespace() throws NoSuchTableException, ParseException { - String targetNameWithNonexistentNamespace = - (catalogName.equals("spark_catalog") ? "" : catalogName + ".") - + "missing_namespace." - + "register_table"; long numRows = 1000; sql("CREATE TABLE %s (id int, data string) using ICEBERG", tableName); @@ -103,12 +100,17 @@ public void testRegisterTableToNonexistentNamespace() Table table = Spark3Util.loadIcebergTable(spark, tableName); String metadataJson = TableUtil.metadataFileLocation(table); + String targetNameWithNonexistentNamespace = + (catalogName.equals("spark_catalog") ? "" : catalogName + ".") + + "missing_namespace." + + "register_table"; + assertThatThrownBy( () -> sql( "CALL %s.system.register_table('%s', '%s')", catalogName, targetNameWithNonexistentNamespace, metadataJson)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Cannot register table to nonexistent target namespace"); + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageContaining("Namespace does not exist: "); } } From a4ba226e8d6dead1720971367af6c80f50aa5328 Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Sat, 5 Jul 2025 13:12:14 -0700 Subject: [PATCH 4/7] Test catalog implementations not coverted by CatalogTests --- .../aws/dynamodb/TestDynamoDbCatalog.java | 12 ++++++++++++ .../apache/iceberg/aws/glue/TestGlueCatalog.java | 16 ++++++++++++++++ .../apache/iceberg/hadoop/TestHadoopCatalog.java | 12 ++++++++++++ .../apache/iceberg/dell/ecs/TestEcsCatalog.java | 13 +++++++++++++ .../iceberg/snowflake/SnowflakeCatalogTest.java | 14 ++++++++++++++ .../extensions/TestRegisterTableProcedure.java | 6 +++--- 6 files changed, 70 insertions(+), 3 deletions(-) diff --git a/aws/src/integration/java/org/apache/iceberg/aws/dynamodb/TestDynamoDbCatalog.java b/aws/src/integration/java/org/apache/iceberg/aws/dynamodb/TestDynamoDbCatalog.java index 2d83582c1337..cc3c8311ee2f 100644 --- a/aws/src/integration/java/org/apache/iceberg/aws/dynamodb/TestDynamoDbCatalog.java +++ b/aws/src/integration/java/org/apache/iceberg/aws/dynamodb/TestDynamoDbCatalog.java @@ -39,6 +39,7 @@ import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.AlreadyExistsException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.NoSuchTableException; import org.apache.iceberg.exceptions.ValidationException; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; @@ -404,6 +405,17 @@ public void testRegisterExistingTable() { assertThat(catalog.dropNamespace(namespace)).isTrue(); } + @Test + public void testRegisterTableToNonExistingNamespace() { + TableIdentifier targetIdentifier = TableIdentifier.of("non-existing", "table"); + assertThatThrownBy( + () -> + catalog.registerTable( + targetIdentifier, "table_metadata_loc_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } + private static String genRandomName() { return UUID.randomUUID().toString().replace("-", ""); } diff --git a/aws/src/test/java/org/apache/iceberg/aws/glue/TestGlueCatalog.java b/aws/src/test/java/org/apache/iceberg/aws/glue/TestGlueCatalog.java index 2042948eb3c9..7cb687b44be0 100644 --- a/aws/src/test/java/org/apache/iceberg/aws/glue/TestGlueCatalog.java +++ b/aws/src/test/java/org/apache/iceberg/aws/glue/TestGlueCatalog.java @@ -32,6 +32,7 @@ import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; import org.apache.iceberg.exceptions.NamespaceNotEmptyException; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.exceptions.ValidationException; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; import org.apache.iceberg.relocated.com.google.common.collect.Lists; @@ -679,4 +680,19 @@ public void testTableLevelS3TagProperties() { S3FileIOProperties.S3_TAG_ICEBERG_NAMESPACE), "db"); } + + @Test + public void testRegisterTableToNonExistingNamespace() { + TableIdentifier targetIdentifier = TableIdentifier.of("non_existing", "table"); + Mockito.doThrow(NoSuchNamespaceException.class) + .when(glue) + .getDatabase(Mockito.any(GetDatabaseRequest.class)); + + assertThatThrownBy( + () -> + glueCatalog.registerTable( + targetIdentifier, "table_metadata_loc_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } } diff --git a/core/src/test/java/org/apache/iceberg/hadoop/TestHadoopCatalog.java b/core/src/test/java/org/apache/iceberg/hadoop/TestHadoopCatalog.java index 61f5db9029b4..b2363d0b3846 100644 --- a/core/src/test/java/org/apache/iceberg/hadoop/TestHadoopCatalog.java +++ b/core/src/test/java/org/apache/iceberg/hadoop/TestHadoopCatalog.java @@ -678,4 +678,16 @@ public void testRegisterExistingTable() throws IOException { .hasMessage("Table already exists: a.t1"); assertThat(catalog.dropTable(identifier)).isTrue(); } + + @Test + public void testRegisterTableToNonexistentDB() throws IOException { + HadoopCatalog catalog = hadoopCatalog(); + TableIdentifier targetIdentifier = TableIdentifier.of("non_existing", "table"); + assertThatThrownBy( + () -> + catalog.registerTable( + targetIdentifier, "table_metadata_loc_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } } diff --git a/dell/src/test/java/org/apache/iceberg/dell/ecs/TestEcsCatalog.java b/dell/src/test/java/org/apache/iceberg/dell/ecs/TestEcsCatalog.java index 4714d37d72b9..9191abe13ca9 100644 --- a/dell/src/test/java/org/apache/iceberg/dell/ecs/TestEcsCatalog.java +++ b/dell/src/test/java/org/apache/iceberg/dell/ecs/TestEcsCatalog.java @@ -174,6 +174,7 @@ public void testRenameTable() { @Test public void testRegisterTable() { + ecsCatalog.createNamespace(Namespace.of("a")); TableIdentifier identifier = TableIdentifier.of("a", "t1"); ecsCatalog.createTable(identifier, SCHEMA); Table registeringTable = ecsCatalog.loadTable(identifier); @@ -191,6 +192,7 @@ public void testRegisterTable() { @Test public void testRegisterExistingTable() { + ecsCatalog.createNamespace(Namespace.of("a")); TableIdentifier identifier = TableIdentifier.of("a", "t1"); ecsCatalog.createTable(identifier, SCHEMA); Table registeringTable = ecsCatalog.loadTable(identifier); @@ -201,4 +203,15 @@ public void testRegisterExistingTable() { .hasMessage("Table already exists: a.t1"); assertThat(ecsCatalog.dropTable(identifier, true)).isTrue(); } + + @Test + public void testRegisterTableToNonExistingNamespace() { + TableIdentifier targetIdentifier = TableIdentifier.of("non-existing", "table"); + assertThatThrownBy( + () -> + ecsCatalog.registerTable( + targetIdentifier, "table_metadata_loc_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } } diff --git a/snowflake/src/test/java/org/apache/iceberg/snowflake/SnowflakeCatalogTest.java b/snowflake/src/test/java/org/apache/iceberg/snowflake/SnowflakeCatalogTest.java index ecad072de724..54cc86a833ac 100644 --- a/snowflake/src/test/java/org/apache/iceberg/snowflake/SnowflakeCatalogTest.java +++ b/snowflake/src/test/java/org/apache/iceberg/snowflake/SnowflakeCatalogTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.IOException; import java.util.Map; @@ -30,6 +31,7 @@ import org.apache.iceberg.TableMetadataParser; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.exceptions.NoSuchNamespaceException; import org.apache.iceberg.inmemory.InMemoryFileIO; import org.apache.iceberg.io.FileIO; import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; @@ -295,4 +297,16 @@ public void testSchemaExists() { assertThat(catalog.namespaceExists(Namespace.of("DB_1", "NONEXISTENT_SCHEMA"))).isFalse(); assertThat(catalog.namespaceExists(Namespace.of("NONEXISTENT_DB", "SCHEMA_1"))).isFalse(); } + + @Test + public void testRegisterTableToNonexistentDB() { + String dbName = "NONEXISTENT_DB"; + TableIdentifier targetIdentifier = TableIdentifier.of(dbName, "table"); + assertThatThrownBy( + () -> + catalog.registerTable( + targetIdentifier, "table_metadata_loc_from_different_catalogs")) + .isInstanceOf(NoSuchNamespaceException.class) + .hasMessageStartingWith("Cannot register table"); + } } diff --git a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java index bf1d65e8773c..ab930a481e42 100644 --- a/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java +++ b/spark/v4.0/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRegisterTableProcedure.java @@ -86,7 +86,7 @@ public void testRegisterTable() throws NoSuchTableException, ParseException { } @TestTemplate - public void testRegisterTableToNonexistentNamespace() + public void testRegisterTableToNonExistingNamespace() throws NoSuchTableException, ParseException { long numRows = 1000; @@ -102,7 +102,7 @@ public void testRegisterTableToNonexistentNamespace() String targetNameWithNonexistentNamespace = (catalogName.equals("spark_catalog") ? "" : catalogName + ".") - + "missing_namespace." + + "non_existing_namespace." + "register_table"; assertThatThrownBy( @@ -111,6 +111,6 @@ public void testRegisterTableToNonexistentNamespace() "CALL %s.system.register_table('%s', '%s')", catalogName, targetNameWithNonexistentNamespace, metadataJson)) .isInstanceOf(NoSuchNamespaceException.class) - .hasMessageContaining("Namespace does not exist: "); + .hasMessageContaining("Cannot register table"); } } From 6438f16e9d14737e010801c0aa5d1c7c5116e099 Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Sun, 6 Jul 2025 14:49:09 -0700 Subject: [PATCH 5/7] Align catalog client and server behavior by JDBC strict mode --- .../apache/iceberg/rest/RESTCompatibilityKitCatalogTests.java | 4 +--- .../java/org/apache/iceberg/rest/RESTCatalogServer.java | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/open-api/src/test/java/org/apache/iceberg/rest/RESTCompatibilityKitCatalogTests.java b/open-api/src/test/java/org/apache/iceberg/rest/RESTCompatibilityKitCatalogTests.java index 87ec90663db2..cef22b85153e 100644 --- a/open-api/src/test/java/org/apache/iceberg/rest/RESTCompatibilityKitCatalogTests.java +++ b/open-api/src/test/java/org/apache/iceberg/rest/RESTCompatibilityKitCatalogTests.java @@ -72,9 +72,7 @@ protected RESTCatalog initCatalog(String catalogName, Map additi @Override protected boolean requiresNamespaceCreate() { return PropertyUtil.propertyAsBoolean( - restCatalog.properties(), - RESTCompatibilityKitSuite.RCK_REQUIRES_NAMESPACE_CREATE, - super.requiresNamespaceCreate()); + restCatalog.properties(), RESTCompatibilityKitSuite.RCK_REQUIRES_NAMESPACE_CREATE, true); } @Override diff --git a/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java b/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java index e79a590127fd..f3797c0c7cbc 100644 --- a/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java +++ b/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java @@ -79,6 +79,7 @@ private CatalogContext initializeBackendCatalog() throws IOException { catalogProperties.putIfAbsent(CatalogProperties.CATALOG_IMPL, JdbcCatalog.class.getName()); catalogProperties.putIfAbsent(CatalogProperties.URI, "jdbc:sqlite::memory:"); catalogProperties.putIfAbsent("jdbc.schema-version", "V1"); + catalogProperties.putIfAbsent("jdbc.strict-mode", "true"); // Configure a default location if one is not specified String warehouseLocation = catalogProperties.get(CatalogProperties.WAREHOUSE_LOCATION); From 8374e71257d4681f3b37316d179d264833b2d225 Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Sun, 6 Jul 2025 14:50:07 -0700 Subject: [PATCH 6/7] Make error messages consistent in core module --- core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java | 2 +- core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java | 2 +- .../main/java/org/apache/iceberg/jdbc/JdbcTableOperations.java | 2 +- .../main/java/org/apache/iceberg/jdbc/JdbcViewOperations.java | 2 +- .../main/java/org/apache/iceberg/rest/RESTSessionCatalog.java | 2 +- core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java b/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java index ced66b5d17a0..74ad1e433b57 100644 --- a/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java +++ b/core/src/main/java/org/apache/iceberg/BaseMetastoreCatalog.java @@ -101,7 +101,7 @@ protected void targetNamespaceExists(TableIdentifier identifier) { if (this instanceof SupportsNamespaces && !(((SupportsNamespaces) this).namespaceExists(namespace))) { throw new NoSuchNamespaceException( - "Cannot register table %s to catalog %s. Namespace %s does not exist", + "Cannot register table %s to catalog %s. Namespace does not exist: %s", identifier, name(), namespace); } } diff --git a/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java b/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java index a73bf97aeaa0..04c1b2d34fa2 100644 --- a/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java +++ b/core/src/main/java/org/apache/iceberg/jdbc/JdbcCatalog.java @@ -395,7 +395,7 @@ public void targetNamespaceExists(TableIdentifier identifier) { if (PropertyUtil.propertyAsBoolean(catalogProperties, JdbcUtil.STRICT_MODE_PROPERTY, false) && !JdbcUtil.namespaceExists(catalogName, connections, namespace)) { throw new NoSuchNamespaceException( - "Cannot register table %s to catalog %s. Namespace %s does not exist", + "Cannot register table %s to catalog %s. Namespace does not exist: %s", identifier, catalogName, namespace); } } diff --git a/core/src/main/java/org/apache/iceberg/jdbc/JdbcTableOperations.java b/core/src/main/java/org/apache/iceberg/jdbc/JdbcTableOperations.java index 619296ad3336..61adf38e0cdf 100644 --- a/core/src/main/java/org/apache/iceberg/jdbc/JdbcTableOperations.java +++ b/core/src/main/java/org/apache/iceberg/jdbc/JdbcTableOperations.java @@ -173,7 +173,7 @@ private void createTable(String newMetadataLocation) throws SQLException, Interr if (PropertyUtil.propertyAsBoolean(catalogProperties, JdbcUtil.STRICT_MODE_PROPERTY, false) && !JdbcUtil.namespaceExists(catalogName, connections, namespace)) { throw new NoSuchNamespaceException( - "Cannot create table %s in catalog %s. Namespace %s does not exist", + "Cannot create table %s in catalog %s. Namespace does not exist: %s", tableIdentifier, catalogName, namespace); } diff --git a/core/src/main/java/org/apache/iceberg/jdbc/JdbcViewOperations.java b/core/src/main/java/org/apache/iceberg/jdbc/JdbcViewOperations.java index 10f46941d694..e02b5dc17d82 100644 --- a/core/src/main/java/org/apache/iceberg/jdbc/JdbcViewOperations.java +++ b/core/src/main/java/org/apache/iceberg/jdbc/JdbcViewOperations.java @@ -180,7 +180,7 @@ private void createView(String newMetadataLocation) throws SQLException, Interru if (PropertyUtil.propertyAsBoolean(catalogProperties, JdbcUtil.STRICT_MODE_PROPERTY, false) && !JdbcUtil.namespaceExists(catalogName, connections, namespace)) { throw new NoSuchNamespaceException( - "Cannot create view %s in catalog %s. Namespace %s does not exist", + "Cannot create view %s in catalog %s. Namespace does not exist: %s", viewIdentifier, catalogName, namespace); } diff --git a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java index 58cf700f6c0a..8c2c0dc75c11 100644 --- a/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java +++ b/core/src/main/java/org/apache/iceberg/rest/RESTSessionCatalog.java @@ -504,7 +504,7 @@ public Table registerTable( Namespace namespace = ident.namespace(); if (!namespaceExists(context, namespace)) { throw new NoSuchNamespaceException( - "Cannot register table %s to catalog %s. Namespace %s does not exist", + "Cannot register table %s to catalog %s. Namespace does not exist: %s", ident, name(), namespace); } diff --git a/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java b/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java index 22f400b6dca8..cb8d42e0e035 100644 --- a/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java +++ b/core/src/test/java/org/apache/iceberg/jdbc/TestJdbcCatalog.java @@ -953,7 +953,7 @@ public void testCreateTableInNonExistingNamespaceStrictMode() { assertThatThrownBy(() -> jdbcCatalog.createTable(identifier, SCHEMA, PARTITION_SPEC)) .isInstanceOf(NoSuchNamespaceException.class) .hasMessage( - "Cannot create table testDb.ns1.ns2.someTable in catalog strict_jdbc_catalog. Namespace testDb.ns1.ns2 does not exist"); + "Cannot create table testDb.ns1.ns2.someTable in catalog strict_jdbc_catalog. Namespace does not exist: testDb.ns1.ns2"); assertThat(jdbcCatalog.tableExists(identifier)).isFalse(); From 58e9711315ab0a4e1df4dbdc9ea0c0495d121a14 Mon Sep 17 00:00:00 2001 From: hsiang-c Date: Sun, 6 Jul 2025 19:58:10 -0700 Subject: [PATCH 7/7] Turn on strict mode only in the tests of iceberg-open-api module --- build.gradle | 3 +++ .../java/org/apache/iceberg/rest/RESTCatalogServer.java | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d604298b8de0..45fce39f1368 100644 --- a/build.gradle +++ b/build.gradle @@ -1057,6 +1057,9 @@ project(':iceberg-open-api') { systemProperties System.properties .findAll { k, v -> k.startsWith("rck") } .collectEntries { k, v -> { [(k):v, (k.replaceFirst("rck.", "")):v] }} // strip prefix + + // Turn on strict mode for JdbcCatalog backend in RESTCatalogServer only in :iceberg-open-api module + environment "CATALOG_JDBC_STRICT__MODE", "true" } def restCatalogSpec = "$projectDir/rest-catalog-open-api.yaml" diff --git a/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java b/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java index f3797c0c7cbc..e79a590127fd 100644 --- a/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java +++ b/open-api/src/testFixtures/java/org/apache/iceberg/rest/RESTCatalogServer.java @@ -79,7 +79,6 @@ private CatalogContext initializeBackendCatalog() throws IOException { catalogProperties.putIfAbsent(CatalogProperties.CATALOG_IMPL, JdbcCatalog.class.getName()); catalogProperties.putIfAbsent(CatalogProperties.URI, "jdbc:sqlite::memory:"); catalogProperties.putIfAbsent("jdbc.schema-version", "V1"); - catalogProperties.putIfAbsent("jdbc.strict-mode", "true"); // Configure a default location if one is not specified String warehouseLocation = catalogProperties.get(CatalogProperties.WAREHOUSE_LOCATION);