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 1f0121906e [cdc] Fix PostgreSQL time(n) mapping and widen TIME 
precision in schema evolution (#8609)
1f0121906e is described below

commit 1f0121906e67610b84361590fe0566bd544b32f4
Author: Eunbin Son <[email protected]>
AuthorDate: Thu Jul 23 22:14:47 2026 +0900

    [cdc] Fix PostgreSQL time(n) mapping and widen TIME precision in schema 
evolution (#8609)
---
 .../action/cdc/postgres/PostgresRecordParser.java  |  3 ++
 .../cdc/UpdatedDataFieldsProcessFunctionBase.java  | 14 ++++++
 .../cdc/postgres/PostgresRecordParserTest.java     | 51 +++++++++++++++++++++-
 .../UpdatedDataFieldsProcessFunctionBaseTest.java  | 19 ++++++++
 4 files changed, 86 insertions(+), 1 deletion(-)

diff --git 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParser.java
 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParser.java
index 9375c1630e..003ae7b529 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParser.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParser.java
@@ -44,6 +44,7 @@ import io.debezium.data.Bits;
 import io.debezium.time.Date;
 import io.debezium.time.MicroTime;
 import io.debezium.time.MicroTimestamp;
+import io.debezium.time.Time;
 import io.debezium.time.Timestamp;
 import io.debezium.time.ZonedTimestamp;
 import org.apache.flink.api.common.functions.FlatMapFunction;
@@ -159,6 +160,8 @@ public class PostgresRecordParser
             case "int32":
                 if (Date.SCHEMA_NAME.equals(field.name())) {
                     return DataTypes.DATE();
+                } else if (Time.SCHEMA_NAME.equals(field.name())) {
+                    return DataTypes.TIME(3);
                 }
                 return DataTypes.INT();
             case "int64":
diff --git 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBase.java
 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBase.java
index 6f74a346e9..877fc8d20b 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBase.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/main/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBase.java
@@ -82,6 +82,9 @@ public abstract class UpdatedDataFieldsProcessFunctionBase<I, 
O> extends Process
     private static final List<DataTypeRoot> TIMESTAMP_TYPES =
             Arrays.asList(DataTypeRoot.TIMESTAMP_WITHOUT_TIME_ZONE);
 
+    private static final List<DataTypeRoot> TIME_TYPES =
+            Arrays.asList(DataTypeRoot.TIME_WITHOUT_TIME_ZONE);
+
     protected UpdatedDataFieldsProcessFunctionBase(
             CatalogLoader catalogLoader, TypeMapping typeMapping) {
         this.catalogLoader = catalogLoader;
@@ -260,6 +263,17 @@ public abstract class 
UpdatedDataFieldsProcessFunctionBase<I, O> extends Process
                     : ConvertAction.IGNORE;
         }
 
+        oldIdx = TIME_TYPES.indexOf(oldType.getTypeRoot());
+        newIdx = TIME_TYPES.indexOf(newType.getTypeRoot());
+        if (oldIdx >= 0 && newIdx >= 0) {
+            // Debezium encodes every PostgreSQL time(0..3) column as millis 
(TIME(3)), while the
+            // JDBC schema path keeps the declared TIME(n); widen on precision 
so time(0..2) can
+            // converge on TIME(3) instead of failing schema evolution.
+            return DataTypeChecks.getPrecision(oldType) <= 
DataTypeChecks.getPrecision(newType)
+                    ? ConvertAction.CONVERT
+                    : ConvertAction.IGNORE;
+        }
+
         return ConvertAction.EXCEPTION;
     }
 
diff --git 
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParserTest.java
 
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParserTest.java
index f75fcdf61e..5db746f74d 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParserTest.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/action/cdc/postgres/PostgresRecordParserTest.java
@@ -88,6 +88,44 @@ public class PostgresRecordParserTest {
                 .isEqualTo(DataTypes.BIGINT().nullable());
     }
 
+    /**
+     * Verifies that {@code io.debezium.time.Time} (int32, 
milliseconds-past-midnight) is mapped to
+     * {@code TIME(3)}, not {@code INT}.
+     *
+     * <p>PostgreSQL {@code time(n)} columns with {@code n <= 3} are encoded 
by Debezium (default
+     * adaptive mode) using the {@code io.debezium.time.Time} logical type 
(int32 millis-of-day).
+     * The JDBC schema path maps the same columns to {@code TIME(n)}, so 
without this fix the two
+     * paths disagree and the sync crashes with "Cannot convert field from 
TIME(3) to INT". This
+     * mirrors the int64 {@code Timestamp} fix from PR #8222.
+     */
+    @Test
+    public void testTimeMillisFieldMapsToTime3() throws Exception {
+        String json = debeziumJson("int32", "io.debezium.time.Time");
+        List<RichCdcMultiplexRecord> out = parse(json);
+
+        assertThat(out).isNotEmpty();
+        DataField field = findField(out.get(0), "ts_col");
+        assertThat(field.type())
+                .as("io.debezium.time.Time (int32) must map to TIME(3), not 
INT")
+                .isEqualTo(DataTypes.TIME(3).nullable());
+    }
+
+    /**
+     * Verifies that a plain int32 field (no logical type name) still maps to 
INT — i.e. the fix
+     * does not break the default fallthrough.
+     */
+    @Test
+    public void testPlainInt32FieldMapsToInt() throws Exception {
+        String json = debeziumJson("int32", null);
+        List<RichCdcMultiplexRecord> out = parse(json);
+
+        assertThat(out).isNotEmpty();
+        DataField field = findField(out.get(0), "ts_col");
+        assertThat(field.type())
+                .as("int32 with no logical type name must remain INT")
+                .isEqualTo(DataTypes.INT().nullable());
+    }
+
     // 
-------------------------------------------------------------------------
     // helpers
     // 
-------------------------------------------------------------------------
@@ -122,6 +160,15 @@ public class PostgresRecordParserTest {
      * ts_col} with the given logical type {@code schemaName} (may be null for 
a plain int64).
      */
     private static String debeziumJson(String schemaName) {
+        return debeziumJson("int64", schemaName);
+    }
+
+    /**
+     * Builds a minimal Debezium PostgreSQL CDC JSON event containing one 
column {@code ts_col} of
+     * the given primitive {@code type} and logical type {@code schemaName} 
(may be null for no
+     * logical type).
+     */
+    private static String debeziumJson(String type, String schemaName) {
         String nameField = schemaName == null ? "" : "\"name\":\"" + 
schemaName + "\",";
         return "{"
                 + "\"schema\":{"
@@ -133,7 +180,9 @@ public class PostgresRecordParserTest {
                 + "      \"field\":\"after\","
                 + "      \"fields\":["
                 + "        
{\"type\":\"int32\",\"optional\":false,\"field\":\"id\"},"
-                + "        {\"type\":\"int64\",\"optional\":true,"
+                + "        {\"type\":\""
+                + type
+                + "\",\"optional\":true,"
                 + nameField
                 + "         \"field\":\"ts_col\"}"
                 + "      ]"
diff --git 
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBaseTest.java
 
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBaseTest.java
index a201e8308c..0307cee204 100644
--- 
a/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBaseTest.java
+++ 
b/paimon-flink/paimon-flink-cdc/src/test/java/org/apache/paimon/flink/sink/cdc/UpdatedDataFieldsProcessFunctionBaseTest.java
@@ -29,6 +29,7 @@ import org.apache.paimon.types.MapType;
 import org.apache.paimon.types.MultisetType;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.SmallIntType;
+import org.apache.paimon.types.TimeType;
 import org.apache.paimon.types.TimestampType;
 import org.apache.paimon.types.VarCharType;
 
@@ -95,6 +96,24 @@ public class UpdatedDataFieldsProcessFunctionBaseTest {
         
assertEquals(UpdatedDataFieldsProcessFunctionBase.ConvertAction.IGNORE, 
convertAction);
     }
 
+    @Test
+    public void testCanConvertTime() {
+        // Debezium maps every PostgreSQL time(0..3) column to TIME(3), while 
the JDBC schema path
+        // keeps TIME(n); widening TIME(0) -> TIME(3) must convert instead of 
throwing.
+        TimeType oldType = new TimeType(true, 0);
+        TimeType biggerPrecision = new TimeType(true, 3);
+        TimeType smallerPrecision = new TimeType(true, 0);
+
+        assertEquals(
+                UpdatedDataFieldsProcessFunctionBase.ConvertAction.CONVERT,
+                UpdatedDataFieldsProcessFunctionBase.canConvert(
+                        oldType, biggerPrecision, 
TypeMapping.defaultMapping()));
+        assertEquals(
+                UpdatedDataFieldsProcessFunctionBase.ConvertAction.IGNORE,
+                UpdatedDataFieldsProcessFunctionBase.canConvert(
+                        biggerPrecision, smallerPrecision, 
TypeMapping.defaultMapping()));
+    }
+
     @Test
     public void testCanConvertTimestamp() {
         TimestampType oldType = new TimestampType(true, 3);

Reply via email to