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

lvyanquan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink-cdc.git


The following commit(s) were added to refs/heads/master by this push:
     new 36d212124 [FLINK-40130][connect/paimon] Fix missing primary key type 
compatibility check of Paimon sink (#4471)
36d212124 is described below

commit 36d2121246ce1250883e6b79df426a86ae8fa20a
Author: haruki <[email protected]>
AuthorDate: Mon Jul 13 20:03:24 2026 +0800

    [FLINK-40130][connect/paimon] Fix missing primary key type compatibility 
check of Paimon sink (#4471)
---
 .../flink/cdc/common/utils/SchemaMergingUtils.java |  8 ++-
 .../sink/v2/bucket/BucketAssignOperator.java       | 38 ++++++++++++++-
 .../paimon/sink/v2/PaimonSinkITCase.java           | 57 +++++++++++++++++++++-
 3 files changed, 98 insertions(+), 5 deletions(-)

diff --git 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/utils/SchemaMergingUtils.java
 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/utils/SchemaMergingUtils.java
index ad781d19a..456a7ebc2 100644
--- 
a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/utils/SchemaMergingUtils.java
+++ 
b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/utils/SchemaMergingUtils.java
@@ -327,8 +327,12 @@ public class SchemaMergingUtils {
         return coercedRow;
     }
 
-    @VisibleForTesting
-    static boolean isDataTypeCompatible(@Nullable DataType currentType, 
DataType upcomingType) {
+    /**
+     * Checks whether the upcoming data type can fit into the current data 
type. A missing current
+     * type is treated as incompatible.
+     */
+    public static boolean isDataTypeCompatible(
+            @Nullable DataType currentType, DataType upcomingType) {
         // If two types are identical, they're compatible of course.
         if (Objects.equals(currentType, upcomingType)) {
             return true;
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/bucket/BucketAssignOperator.java
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/bucket/BucketAssignOperator.java
index d523feb72..e3c6f095b 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/bucket/BucketAssignOperator.java
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/bucket/BucketAssignOperator.java
@@ -28,8 +28,10 @@ import org.apache.flink.cdc.common.event.FlushEvent;
 import org.apache.flink.cdc.common.event.SchemaChangeEvent;
 import org.apache.flink.cdc.common.event.TableId;
 import org.apache.flink.cdc.common.event.TruncateTableEvent;
+import org.apache.flink.cdc.common.schema.Column;
 import org.apache.flink.cdc.common.schema.Schema;
 import org.apache.flink.cdc.common.utils.Preconditions;
+import org.apache.flink.cdc.common.utils.SchemaMergingUtils;
 import org.apache.flink.cdc.common.utils.SchemaUtils;
 import org.apache.flink.cdc.connectors.paimon.sink.v2.OperatorIDGenerator;
 import org.apache.flink.cdc.connectors.paimon.sink.v2.PaimonWriterHelper;
@@ -248,6 +250,7 @@ public class BucketAssignOperator extends 
AbstractStreamOperatorAdapter<Event>
                         
catalog.getTable(PaimonWriterHelper.identifierFromTableId(tableId)));
         MixedSchemaInfo mixedSchemaInfo =
                 new MixedSchemaInfo(
+                        tableId,
                         new TableSchemaInfo(upstreamSchema, zoneId),
                         new TableSchemaInfo(physicalSchema, zoneId));
         if (!mixedSchemaInfo.isSameColumnsIgnoringCommentAndDefaultValue()) {
@@ -276,6 +279,7 @@ public class BucketAssignOperator extends 
AbstractStreamOperatorAdapter<Event>
             if (schema.isPresent()) {
                 MixedSchemaInfo mixedSchemaInfo =
                         new MixedSchemaInfo(
+                                tableId,
                                 new TableSchemaInfo(schema.get(), zoneId),
                                 new TableSchemaInfo(
                                         
PaimonWriterHelper.deduceSchemaForPaimonTable(
@@ -366,6 +370,8 @@ public class BucketAssignOperator extends 
AbstractStreamOperatorAdapter<Event>
 
     /** MixedSchemaInfo is used to store the mixed schema info of upstream and 
paimon table. */
     private static class MixedSchemaInfo {
+        private final TableId tableId;
+
         private final TableSchemaInfo upstreamSchemaInfo;
 
         private final TableSchemaInfo paimonSchemaInfo;
@@ -373,12 +379,16 @@ public class BucketAssignOperator extends 
AbstractStreamOperatorAdapter<Event>
         private final boolean sameColumnsIgnoringCommentAndDefaultValue;
 
         public MixedSchemaInfo(
-                TableSchemaInfo upstreamSchemaInfo, TableSchemaInfo 
paimonSchemaInfo) {
+                TableId tableId,
+                TableSchemaInfo upstreamSchemaInfo,
+                TableSchemaInfo paimonSchemaInfo) {
+            this.tableId = tableId;
             this.upstreamSchemaInfo = upstreamSchemaInfo;
             this.paimonSchemaInfo = paimonSchemaInfo;
             this.sameColumnsIgnoringCommentAndDefaultValue =
                     PaimonWriterHelper.sameColumnsIgnoreCommentAndDefaultValue(
                             upstreamSchemaInfo.getSchema(), 
paimonSchemaInfo.getSchema());
+            validatePrimaryKeyTypes();
         }
 
         public TableSchemaInfo getUpstreamSchemaInfo() {
@@ -392,5 +402,31 @@ public class BucketAssignOperator extends 
AbstractStreamOperatorAdapter<Event>
         public boolean isSameColumnsIgnoringCommentAndDefaultValue() {
             return sameColumnsIgnoringCommentAndDefaultValue;
         }
+
+        private void validatePrimaryKeyTypes() {
+            for (String columnName : 
paimonSchemaInfo.getSchema().primaryKeys()) {
+                Column upstreamPrimaryKeyColumn =
+                        
upstreamSchemaInfo.getSchema().getColumn(columnName).orElse(null);
+                Column paimonPrimaryKeyColumn =
+                        
paimonSchemaInfo.getSchema().getColumn(columnName).get();
+                if (upstreamPrimaryKeyColumn == null) {
+                    throw new IllegalStateException(
+                            String.format(
+                                    "The primary key column %s of %s is not 
found in upstream schema.",
+                                    columnName, tableId));
+                }
+                if (!SchemaMergingUtils.isDataTypeCompatible(
+                        paimonPrimaryKeyColumn.getType().nullable(),
+                        upstreamPrimaryKeyColumn.getType().nullable())) {
+                    throw new IllegalStateException(
+                            String.format(
+                                    "The primary key column %s of %s is %s, 
which is not compatible with upstream column type %s.",
+                                    columnName,
+                                    tableId,
+                                    paimonPrimaryKeyColumn.getType(),
+                                    upstreamPrimaryKeyColumn.getType()));
+                }
+            }
+        }
     }
 }
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java
index 381c87b24..9a9ab6a42 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java
@@ -216,7 +216,18 @@ public class PaimonSinkITCase {
                         .physicalColumn("col2", STRING())
                         .option("deletion-vectors.enabled", 
String.valueOf(enableDeleteVectors))
                         .build();
-        CreateTableEvent createTableEvent = new CreateTableEvent(table1, 
schema);
+
+        Schema upstreamSchema = schema;
+        if (schemaChange == 
SchemaChange.COMPATIBLE_NOT_NULL_PRIMARY_KEY_COLUMN) {
+            upstreamSchema =
+                    schema.copy(
+                            Schema.newBuilder()
+                                    .physicalColumn("col1", 
VARCHAR(64).notNull())
+                                    .physicalColumn("col2", STRING())
+                                    .build()
+                                    .getColumns());
+        }
+        CreateTableEvent createTableEvent = new CreateTableEvent(table1, 
upstreamSchema);
         testEvents.add(createTableEvent);
         PaimonMetadataApplier metadataApplier = new 
PaimonMetadataApplier(catalogOptions);
         if (schemaChange != null) {
@@ -263,6 +274,18 @@ public class PaimonSinkITCase {
                             .physicalColumn("col2", VARCHAR(10));
                     break;
                 }
+            case INCOMPATIBLE_PRIMARY_KEY_COLUMN:
+                {
+                    builder.physicalColumn("col1", INT().notNull())
+                            .physicalColumn("col2", STRING());
+                    break;
+                }
+            case COMPATIBLE_NOT_NULL_PRIMARY_KEY_COLUMN:
+                {
+                    builder.physicalColumn("col1", STRING().notNull())
+                            .physicalColumn("col2", STRING());
+                    break;
+                }
         }
         return schema.copy(builder.build().getColumns());
     }
@@ -503,7 +526,9 @@ public class PaimonSinkITCase {
         ADD_COLUMN,
         REMOVE_COLUMN,
         REORDER_COLUMN,
-        MODIFY_COLUMN;
+        MODIFY_COLUMN,
+        INCOMPATIBLE_PRIMARY_KEY_COLUMN,
+        COMPATIBLE_NOT_NULL_PRIMARY_KEY_COLUMN;
     }
 
     @ParameterizedTest
@@ -524,6 +549,34 @@ public class PaimonSinkITCase {
         bucketAssignOperator.setSchemaEvolutionClient(schemaEvolutionClient);
         bucketAssignOperator.open(new TaskInfoImpl("test_TaskInfo", 1, 0, 1, 
0));
 
+        if (schemaChange == SchemaChange.INCOMPATIBLE_PRIMARY_KEY_COLUMN) {
+            Assertions.assertThatThrownBy(
+                            () ->
+                                    writeAndCommit(
+                                            bucketAssignOperator,
+                                            writer,
+                                            committer,
+                                            createTestEvents(false, false, 
true, schemaChange)
+                                                    .toArray(new Event[0])))
+                    .isExactlyInstanceOf(IllegalStateException.class)
+                    .hasMessage(
+                            "The primary key column col1 of test.table1 is INT 
NOT NULL, which is not compatible with upstream column type STRING NOT NULL.");
+            return;
+        }
+
+        if (schemaChange == 
SchemaChange.COMPATIBLE_NOT_NULL_PRIMARY_KEY_COLUMN) {
+            writeAndCommit(
+                    bucketAssignOperator,
+                    writer,
+                    committer,
+                    createTestEvents(false, false, true, 
schemaChange).toArray(new Event[0]));
+            Assertions.assertThat(fetchResults(table1))
+                    .containsExactlyInAnyOrder(
+                            Row.ofKind(RowKind.INSERT, "1", "1"),
+                            Row.ofKind(RowKind.INSERT, "2", "2"));
+            return;
+        }
+
         // 1. receive only DataChangeEvents during one checkpoint
         writeAndCommit(
                 bucketAssignOperator,

Reply via email to