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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new f5c20ce29a [iceberg] Refuse types the mirror cannot publish before the 
commit (#9477)
f5c20ce29a is described below

commit f5c20ce29ad965170eb0a8b0317dc3eb609502cf
Author: Jiajia Li <[email protected]>
AuthorDate: Mon Oct 5 22:47:50 2026 +0800

    [iceberg] Refuse types the mirror cannot publish before the commit (#9477)
---
 .../org/apache/paimon/schema/SchemaValidation.java |  45 ++++++++
 .../apache/paimon/schema/SchemaManagerTest.java    | 125 +++++++++++++++++++++
 .../core/IcebergRowLineageCompatibilityTest.java   |  16 +--
 3 files changed, 172 insertions(+), 14 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java 
b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
index 2da251e5ea..cd5a3fafed 100644
--- a/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
+++ b/paimon-core/src/main/java/org/apache/paimon/schema/SchemaValidation.java
@@ -136,6 +136,8 @@ public class SchemaValidation {
 
     private static final int MAX_ICEBERG_TIMESTAMP_PRECISION = 6;
 
+    private static final int MIN_ICEBERG_VARIANT_FORMAT_VERSION = 3;
+
     public static final List<Class<? extends DataType>> 
PRIMARY_KEY_UNSUPPORTED_LOGICAL_TYPES =
             Arrays.asList(
                     MapType.class,
@@ -254,6 +256,7 @@ public class SchemaValidation {
         validateVariantTypes(schema, options);
         validateIcebergTimestampPrecisions(tableRowType, options);
         validateIcebergTimePrecisions(tableRowType, options);
+        validateIcebergPublishableTypes(tableRowType, options);
         validateBlobFields(tableRowType, options);
         Set<String> blobDescriptorFields = 
validateBlobDescriptorFields(tableRowType, options);
         Set<String> blobViewFields =
@@ -687,9 +690,51 @@ public class SchemaValidation {
             validateIcebergGeospatialTypes(schema.logicalRowType(), options);
             validateIcebergTimestampPrecisions(schema.logicalRowType(), 
options);
             validateIcebergTimePrecisions(schema.logicalRowType(), options);
+            validateIcebergPublishableTypes(schema.logicalRowType(), options);
         }
     }
 
+    /**
+     * Refuses the types the mirror cannot publish: the conversion that would 
catch them only runs
+     * once the snapshot is durable.
+     */
+    public static void validateIcebergPublishableTypes(DataType dataType, 
CoreOptions options) {
+        if 
(options.toConfiguration().get(IcebergOptions.METADATA_ICEBERG_STORAGE)
+                == IcebergOptions.StorageType.DISABLED) {
+            return;
+        }
+        checkArgument(
+                !containsType(dataType, SchemaValidation::isUnpublishableType),
+                "Columns of type %s or %s cannot be published as Iceberg 
metadata. The schema "
+                        + "history counts too, so dropping such a column does 
not make an existing "
+                        + "table publishable; disable '%s' instead.",
+                DataTypeRoot.BLOB,
+                DataTypeRoot.VECTOR,
+                IcebergOptions.METADATA_ICEBERG_STORAGE.key());
+        checkArgument(
+                options.toConfiguration().get(IcebergOptions.FORMAT_VERSION)
+                                >= MIN_ICEBERG_VARIANT_FORMAT_VERSION
+                        || !containsType(dataType, type -> 
type.is(DataTypeRoot.VARIANT)),
+                "Columns of type %s require Iceberg format version %s. Set 
'%s' = '%s', or remove "
+                        + "them.",
+                DataTypeRoot.VARIANT,
+                MIN_ICEBERG_VARIANT_FORMAT_VERSION,
+                IcebergOptions.FORMAT_VERSION.key(),
+                MIN_ICEBERG_VARIANT_FORMAT_VERSION);
+        checkArgument(
+                
options.toConfiguration().get(IcebergOptions.METADATA_ICEBERG_STORAGE)
+                                != IcebergOptions.StorageType.REST_CATALOG
+                        || !containsType(dataType, type -> 
type.is(DataTypeRoot.VARIANT)),
+                "Variant columns do not support '%s'='%s' because the bundled 
Iceberg REST client "
+                        + "cannot parse the Iceberg variant type.",
+                IcebergOptions.METADATA_ICEBERG_STORAGE.key(),
+                IcebergOptions.StorageType.REST_CATALOG);
+    }
+
+    private static boolean isUnpublishableType(DataType dataType) {
+        return dataType.isAnyOf(DataTypeRoot.BLOB, DataTypeRoot.VECTOR);
+    }
+
     /** Validate geospatial types in a schema that will be published as 
Iceberg metadata. */
     public static void validateIcebergGeospatialTypes(DataType dataType, 
CoreOptions options) {
         boolean hasGeospatial =
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java 
b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
index ea2d4d8c9d..f6fceda8db 100644
--- a/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/schema/SchemaManagerTest.java
@@ -36,6 +36,7 @@ import org.apache.paimon.table.sink.TableWriteImpl;
 import org.apache.paimon.types.ArrayType;
 import org.apache.paimon.types.BigIntType;
 import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.DoubleType;
 import org.apache.paimon.types.IntType;
@@ -197,6 +198,130 @@ public class SchemaManagerTest {
                 .hasStackTraceContaining("precision from 3 to 6");
     }
 
+    @Test
+    public void testIcebergMetadataRefusesUnpublishableTypes() throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "-1");
+        options.put(IcebergOptions.METADATA_ICEBERG_STORAGE.key(), 
"table-location");
+
+        DataType vector = DataTypes.VECTOR(4, DataTypes.FLOAT());
+        assertThatThrownBy(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.createTable(
+                                                        
unpublishableSchema(options, vector))))
+                .hasStackTraceContaining("cannot be published as Iceberg 
metadata");
+
+        Map<String, String> disabled = new HashMap<>(options);
+        disabled.remove(IcebergOptions.METADATA_ICEBERG_STORAGE.key());
+        assertThatCode(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.createTable(
+                                                        
unpublishableSchema(disabled, vector))))
+                .doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testIcebergMetadataRefusesVariantBelowFormatVersionThree() 
throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "-1");
+        options.put(IcebergOptions.METADATA_ICEBERG_STORAGE.key(), 
"table-location");
+
+        assertThatThrownBy(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.createTable(
+                                                        unpublishableSchema(
+                                                                options, 
DataTypes.VARIANT()))))
+                .hasStackTraceContaining("require Iceberg format version 3");
+
+        Map<String, String> formatVersionThree = new HashMap<>(options);
+        formatVersionThree.put(IcebergOptions.FORMAT_VERSION.key(), "3");
+        assertThatCode(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.createTable(
+                                                        unpublishableSchema(
+                                                                
formatVersionThree,
+                                                                
DataTypes.VARIANT()))))
+                .doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testIcebergMetadataRefusesVariantThroughRestCatalog() throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "-1");
+        options.put(IcebergOptions.METADATA_ICEBERG_STORAGE.key(), 
"rest-catalog");
+        options.put(IcebergOptions.FORMAT_VERSION.key(), "3");
+
+        assertThatThrownBy(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.createTable(
+                                                        unpublishableSchema(
+                                                                options, 
DataTypes.VARIANT()))))
+                .hasStackTraceContaining(
+                        "do not support 
'metadata.iceberg.storage'='rest-catalog'");
+    }
+
+    @Test
+    public void testIcebergMetadataRefusesBlobColumns() throws Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "-1");
+        options.put(CoreOptions.ROW_TRACKING_ENABLED.key(), "true");
+        options.put(CoreOptions.DATA_EVOLUTION_ENABLED.key(), "true");
+        options.put(IcebergOptions.METADATA_ICEBERG_STORAGE.key(), 
"table-location");
+
+        assertThatThrownBy(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.createTable(
+                                                        unpublishableSchema(
+                                                                options, 
DataTypes.BLOB()))))
+                .hasStackTraceContaining("cannot be published as Iceberg 
metadata");
+    }
+
+    @Test
+    public void 
testEnableIcebergMetadataValidatesHistoricalUnpublishableTypes() throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(CoreOptions.BUCKET.key(), "-1");
+        retryArtificialException(
+                () ->
+                        manager.createTable(
+                                unpublishableSchema(
+                                        options, DataTypes.VECTOR(4, 
DataTypes.FLOAT()))));
+        retryArtificialException(() -> 
manager.commitChanges(SchemaChange.dropColumn("payload")));
+
+        assertThatThrownBy(
+                        () ->
+                                retryArtificialException(
+                                        () ->
+                                                manager.commitChanges(
+                                                        SchemaChange.setOption(
+                                                                IcebergOptions
+                                                                        
.METADATA_ICEBERG_STORAGE
+                                                                        .key(),
+                                                                
"table-location"))))
+                .hasStackTraceContaining("cannot be published as Iceberg 
metadata");
+    }
+
+    private Schema unpublishableSchema(Map<String, String> options, DataType 
type) {
+        return new Schema(
+                Arrays.asList(
+                        new DataField(0, "id", DataTypes.INT()), new 
DataField(1, "payload", type)),
+                Collections.emptyList(),
+                Collections.emptyList(),
+                options,
+                "");
+    }
+
     @Test
     public void testIcebergMetadataAllowsMicrosecondTimestamps() throws 
Exception {
         Map<String, String> options = new HashMap<>();
diff --git 
a/paimon-iceberg/src/test/java/org/apache/paimon/core/IcebergRowLineageCompatibilityTest.java
 
b/paimon-iceberg/src/test/java/org/apache/paimon/core/IcebergRowLineageCompatibilityTest.java
index b10cb345d1..24ae1633a3 100644
--- 
a/paimon-iceberg/src/test/java/org/apache/paimon/core/IcebergRowLineageCompatibilityTest.java
+++ 
b/paimon-iceberg/src/test/java/org/apache/paimon/core/IcebergRowLineageCompatibilityTest.java
@@ -261,25 +261,13 @@ public class IcebergRowLineageCompatibilityTest {
     }
 
     @Test
-    public void testVariantRejectedWithFormatVersion2() throws Exception {
+    public void testVariantRejectedWithFormatVersion2() {
         RowType rowType =
                 RowType.of(
                         new DataType[] {DataTypes.INT(), DataTypes.VARIANT()},
                         new String[] {"k", "payload"});
-        FileStoreTable table = createPaimonTable(rowType, 
formatVersionOptions(2), "parquet");
-        String commitUser = UUID.randomUUID().toString();
-        TableWriteImpl<?> write =
-                table.newWrite(commitUser)
-                        .withIOManager(new IOManagerImpl(tempDir.toString() + 
"/tmp"));
-        TableCommitImpl commit = table.newCommit(commitUser);
-
-        write.write(GenericRow.of(1, GenericVariant.fromJson("{\"a\": 1}")));
-        // hasStackTraceContaining: robust whether or not the commit path 
wraps the
-        // IllegalArgumentException from the guard
-        assertThatThrownBy(() -> commit.commit(1, write.prepareCommit(false, 
1)))
+        assertThatThrownBy(() -> createPaimonTable(rowType, 
formatVersionOptions(2), "parquet"))
                 .hasStackTraceContaining("VARIANT");
-        write.close();
-        commit.close();
     }
 
     @Test

Reply via email to