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

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


The following commit(s) were added to refs/heads/master by this push:
     new d2a3b48a9c8 [fix](streaming-job) Isolate schemas for CDC snapshot 
splits (#65645)
d2a3b48a9c8 is described below

commit d2a3b48a9c8c3180e07031f28b903134134e3247
Author: wudi <[email protected]>
AuthorDate: Mon Jul 20 17:16:57 2026 +0800

    [fix](streaming-job) Isolate schemas for CDC snapshot splits (#65645)
    
    ### What problem does this PR solve?
    During snapshot backfill, a snapshot split carried schemas for every
    captured table. A change record from another table could then be
    evaluated with the current split key, causing an invalid-field failure
    when the tables use different primary-key names.
    
    This change limits each MySQL and PostgreSQL snapshot split to its own
    table schema while retaining the reader-level schemas for the global
    stream split. It also strengthens the multi-table concurrent-DML cases
    by explicitly enabling snapshot backfill, using different primary-key
    names, and keeping a source writer active across multiple successful
    tasks.
---
 .../source/reader/JdbcIncrementalSourceReader.java |  2 +-
 .../source/reader/mysql/MySqlSourceReader.java     |  2 +-
 .../source/reader/mysql/MySqlSourceReaderTest.java | 73 +++++++++++++++++
 .../reader/postgres/PostgresSourceReaderTest.java  | 74 +++++++++++++++++
 ...ming_mysql_job_snapshot_with_concurrent_dml.out | 23 ++++++
 ...g_mysql_job_snapshot_with_concurrent_dml.groovy | 95 ++++++++++++++++++----
 ...snapshot_with_concurrent_dml_multi_table.groovy | 71 ++++++++++------
 7 files changed, 298 insertions(+), 42 deletions(-)

diff --git 
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/JdbcIncrementalSourceReader.java
 
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/JdbcIncrementalSourceReader.java
index a6887ce8a96..b4c7dcada31 100644
--- 
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/JdbcIncrementalSourceReader.java
+++ 
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/JdbcIncrementalSourceReader.java
@@ -733,7 +733,7 @@ public abstract class JdbcIncrementalSourceReader extends 
AbstractCdcSourceReade
                         splitStart,
                         splitEnd,
                         null,
-                        tableSchemas);
+                        Collections.singletonMap(tableId, tableChange));
         return split;
     }
 
diff --git 
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
 
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
index 430ab7b6668..53c74af0cb6 100644
--- 
a/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
+++ 
b/fs_brokers/cdc_client/src/main/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReader.java
@@ -743,7 +743,7 @@ public class MySqlSourceReader extends 
AbstractCdcSourceReader {
                         splitStart,
                         splitEnd,
                         null,
-                        tableSchemas);
+                        Collections.singletonMap(tableId, tableChange));
         return split;
     }
 
diff --git 
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
 
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
index fe17794f030..2422d6c7c98 100644
--- 
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
+++ 
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/mysql/MySqlSourceReaderTest.java
@@ -24,16 +24,26 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.apache.doris.job.cdc.DataSourceConfigKeys;
+import org.apache.doris.job.cdc.request.JobBaseConfig;
 
 import com.github.shyiko.mysql.binlog.GtidSet;
 import org.apache.flink.cdc.connectors.mysql.source.config.MySqlSourceConfig;
 import org.apache.flink.cdc.connectors.mysql.source.offset.BinlogOffset;
+import org.apache.flink.cdc.connectors.mysql.source.split.MySqlSnapshotSplit;
 import org.junit.jupiter.api.Test;
 
 import java.lang.reflect.Method;
+import java.sql.Types;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
+import io.debezium.relational.Column;
+import io.debezium.relational.Table;
+import io.debezium.relational.TableEditor;
+import io.debezium.relational.TableId;
+import io.debezium.relational.history.TableChanges;
+
 public class MySqlSourceReaderTest {
 
     private static final String SERVER_UUID = 
"24bc7850-2c16-11e6-a073-0242ac110002";
@@ -115,6 +125,46 @@ public class MySqlSourceReaderTest {
         assertFalse(config.isIncludeSchemaChanges());
     }
 
+    @Test
+    void snapshotSplitContainsOnlyCurrentTableSchema() throws Exception {
+        TableId tableId = TableId.parse("testdb.orders");
+        TableId otherTableId = TableId.parse("testdb.other_orders");
+        TableChanges.TableChange tableChange = tableChange(tableId, "pk");
+        Map<TableId, TableChanges.TableChange> tableSchemas = new HashMap<>();
+        tableSchemas.put(tableId, tableChange);
+        tableSchemas.put(otherTableId, tableChange(otherTableId, "other_id"));
+
+        MySqlSourceReader reader =
+                new MySqlSourceReader() {
+                    @Override
+                    protected Class<?> probeSplitKeyClass(
+                            TableId ignoredTableId,
+                            Column ignoredSplitColumn,
+                            JobBaseConfig ignoredJobConfig) {
+                        return Integer.class;
+                    }
+                };
+        reader.setTableSchemas(tableSchemas);
+
+        Method method =
+                MySqlSourceReader.class.getDeclaredMethod(
+                        "createSnapshotSplit", Map.class, JobBaseConfig.class);
+        method.setAccessible(true);
+        MySqlSnapshotSplit split =
+                (MySqlSnapshotSplit)
+                        method.invoke(
+                                reader,
+                                snapshotOffset("testdb.orders", "pk"),
+                                new JobBaseConfig("job-1", "MYSQL", Map.of(), 
null));
+
+        assertEquals(tableId, split.getTableId());
+        assertEquals(Map.of(tableId, tableChange), split.getTableSchemas());
+        assertFalse(split.getTableSchemas().containsKey(otherTableId));
+        assertEquals(2, reader.getTableSchemas().size());
+        assertTrue(reader.getTableSchemas().containsKey(tableId));
+        assertTrue(reader.getTableSchemas().containsKey(otherTableId));
+    }
+
     // Drive the real generateMySqlConfig JSON-offset path and return the 
rebuilt startup offset.
     private BinlogOffset startupBinlogOffset(String offsetJson) throws 
Exception {
         return sourceConfig(offsetJson).getStartupOptions().binlogOffset;
@@ -140,4 +190,27 @@ public class MySqlSourceReaderTest {
         m.setAccessible(true);
         return (MySqlSourceConfig) m.invoke(new MySqlSourceReader(), cfg, 
"job-1", 0);
     }
+
+    private static Map<String, Object> snapshotOffset(String tableId, String 
splitKey) {
+        Map<String, Object> offset = new HashMap<>();
+        offset.put("splitId", tableId + ":0");
+        offset.put("tableId", tableId);
+        offset.put("splitKey", List.of(splitKey));
+        offset.put("splitStart", new Object[] {1});
+        offset.put("splitEnd", new Object[] {10});
+        return offset;
+    }
+
+    private static TableChanges.TableChange tableChange(TableId tableId, 
String splitKey) {
+        TableEditor editor = Table.editor().tableId(tableId);
+        editor.addColumns(
+                Column.editor()
+                        .name(splitKey)
+                        .type("INT")
+                        .jdbcType(Types.INTEGER)
+                        .optional(false)
+                        .create());
+        editor.setPrimaryKeyNames(splitKey);
+        return new 
TableChanges.TableChange(TableChanges.TableChangeType.CREATE, editor.create());
+    }
 }
diff --git 
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReaderTest.java
 
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReaderTest.java
index f9f0efe9a73..c5119bc8ab2 100644
--- 
a/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReaderTest.java
+++ 
b/fs_brokers/cdc_client/src/test/java/org/apache/doris/cdcclient/source/reader/postgres/PostgresSourceReaderTest.java
@@ -17,22 +17,33 @@
 
 package org.apache.doris.cdcclient.source.reader.postgres;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.apache.doris.cdcclient.source.reader.JdbcIncrementalSourceReader;
 import org.apache.doris.job.cdc.DataSourceConfigKeys;
+import org.apache.doris.job.cdc.request.JobBaseConfig;
 
 import io.debezium.config.Configuration;
 import io.debezium.connector.postgresql.connection.PostgresConnection;
 import io.debezium.jdbc.JdbcConfiguration;
 import io.debezium.jdbc.JdbcConnection;
+import io.debezium.relational.Column;
+import io.debezium.relational.Table;
+import io.debezium.relational.TableEditor;
+import io.debezium.relational.TableId;
+import io.debezium.relational.history.TableChanges;
+import org.apache.flink.cdc.connectors.base.source.meta.split.SnapshotSplit;
 import 
org.apache.flink.cdc.connectors.postgres.source.config.PostgresSourceConfig;
 import org.junit.jupiter.api.Test;
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.sql.Types;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 class PostgresSourceReaderTest {
@@ -72,6 +83,46 @@ class PostgresSourceReaderTest {
         assertFalse(config.isIncludeSchemaChanges());
     }
 
+    @Test
+    void snapshotSplitContainsOnlyCurrentTableSchema() throws Exception {
+        TableId tableId = TableId.parse("public.orders", false);
+        TableId otherTableId = TableId.parse("public.other_orders", false);
+        TableChanges.TableChange tableChange = tableChange(tableId, "pk");
+        Map<TableId, TableChanges.TableChange> tableSchemas = new HashMap<>();
+        tableSchemas.put(tableId, tableChange);
+        tableSchemas.put(otherTableId, tableChange(otherTableId, "other_id"));
+
+        PostgresSourceReader reader =
+                new PostgresSourceReader() {
+                    @Override
+                    protected Class<?> probeSplitKeyClass(
+                            TableId ignoredTableId,
+                            Column ignoredSplitColumn,
+                            JobBaseConfig ignoredJobConfig) {
+                        return Integer.class;
+                    }
+                };
+        reader.setTableSchemas(tableSchemas);
+
+        Method method =
+                JdbcIncrementalSourceReader.class.getDeclaredMethod(
+                        "createSnapshotSplit", Map.class, JobBaseConfig.class);
+        method.setAccessible(true);
+        SnapshotSplit split =
+                (SnapshotSplit)
+                        method.invoke(
+                                reader,
+                                snapshotOffset("public.orders", "pk"),
+                                new JobBaseConfig("job-1", "POSTGRES", 
Map.of(), null));
+
+        assertEquals(tableId, split.getTableId());
+        assertEquals(Map.of(tableId, tableChange), split.getTableSchemas());
+        assertFalse(split.getTableSchemas().containsKey(otherTableId));
+        assertEquals(2, reader.getTableSchemas().size());
+        assertTrue(reader.getTableSchemas().containsKey(tableId));
+        assertTrue(reader.getTableSchemas().containsKey(otherTableId));
+    }
+
     private static PostgresSourceConfig sourceConfig(Map<String, String> 
overrides)
             throws Exception {
         Map<String, String> cfg = new HashMap<>();
@@ -97,4 +148,27 @@ class PostgresSourceReaderTest {
         field.setAccessible(true);
         return field.get(connection);
     }
+
+    private static Map<String, Object> snapshotOffset(String tableId, String 
splitKey) {
+        Map<String, Object> offset = new HashMap<>();
+        offset.put("splitId", tableId + ":0");
+        offset.put("tableId", tableId);
+        offset.put("splitKey", List.of(splitKey));
+        offset.put("splitStart", new Object[] {1});
+        offset.put("splitEnd", new Object[] {10});
+        return offset;
+    }
+
+    private static TableChanges.TableChange tableChange(TableId tableId, 
String splitKey) {
+        TableEditor editor = Table.editor().tableId(tableId);
+        editor.addColumns(
+                Column.editor()
+                        .name(splitKey)
+                        .type("int4")
+                        .jdbcType(Types.INTEGER)
+                        .optional(false)
+                        .create());
+        editor.setPrimaryKeyNames(splitKey);
+        return new 
TableChanges.TableChange(TableChanges.TableChangeType.CREATE, editor.create());
+    }
 }
diff --git 
a/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.out
 
b/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.out
index 7f1b97bd503..e97d6041b53 100644
--- 
a/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.out
+++ 
b/regression-test/data/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.out
@@ -2,15 +2,27 @@
 -- !select_count --
 1007
 
+-- !select_count_t2 --
+1007
+
 -- !select_updates --
 1      99
 100    99
 500    99
 999    99
 
+-- !select_updates_t2 --
+1      99
+100    99
+500    99
+999    99
+
 -- !select_deletes --
 0
 
+-- !select_deletes_t2 --
+0
+
 -- !select_inserts --
 1001   concurrent_ins  1
 1002   concurrent_ins  1
@@ -23,3 +35,14 @@
 1009   concurrent_ins  1
 1010   concurrent_ins  1
 
+-- !select_inserts_t2 --
+1001   concurrent_ins  1
+1002   concurrent_ins  1
+1003   concurrent_ins  1
+1004   concurrent_ins  1
+1005   concurrent_ins  1
+1006   concurrent_ins  1
+1007   concurrent_ins  1
+1008   concurrent_ins  1
+1009   concurrent_ins  1
+1010   concurrent_ins  1
diff --git 
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.groovy
 
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.groovy
index 597bea37320..b387fa5dd81 100644
--- 
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.groovy
+++ 
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_mysql_job_snapshot_with_concurrent_dml.groovy
@@ -18,19 +18,22 @@
 
 import org.awaitility.Awaitility
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS
 import static java.util.concurrent.TimeUnit.SECONDS
 
 suite("test_streaming_mysql_job_snapshot_with_concurrent_dml", 
"p0,external,mysql,external_docker,external_docker_mysql,nondatalake") {
     def jobName = "test_streaming_mysql_job_snapshot_with_concurrent_dml_name"
     def currentDb = (sql "select database()")[0][0]
     def table1 = "streaming_snapshot_dml_mysql"
-    def unrelated = "streaming_snapshot_dml_unrelated_mysql"
+    def table2 = "streaming_snapshot_dml_other_pk_mysql"
+    def excludedTable = "streaming_snapshot_dml_excluded_mysql"
     def mysqlDb = "test_cdc_db"
     def totalRows = 1000
 
     sql """DROP JOB IF EXISTS where jobname = '${jobName}'"""
     sql """drop table if exists ${currentDb}.${table1} force"""
-    sql """drop table if exists ${currentDb}.${unrelated} force"""
+    sql """drop table if exists ${currentDb}.${table2} force"""
+    sql """drop table if exists ${currentDb}.${excludedTable} force"""
 
     String enabled = context.config.otherConfigs.get("enableJdbcTest")
     if (enabled != null && enabled.equalsIgnoreCase("true")) {
@@ -59,17 +62,33 @@ 
suite("test_streaming_mysql_job_snapshot_with_concurrent_dml", "p0,external,mysq
             }
             sql sb.toString()
 
-            sql """DROP TABLE IF EXISTS ${mysqlDb}.${unrelated}"""
-            sql """CREATE TABLE ${mysqlDb}.${unrelated} (
+            sql """DROP TABLE IF EXISTS ${mysqlDb}.${table2}"""
+            sql """CREATE TABLE ${mysqlDb}.${table2} (
+                  `other_id` int NOT NULL,
+                  `tag` varchar(64),
+                  `version` int,
+                  PRIMARY KEY (`other_id`)
+                ) ENGINE=InnoDB"""
+
+            StringBuilder table2Rows = new StringBuilder()
+            table2Rows.append("INSERT INTO ${mysqlDb}.${table2} (other_id, 
tag, version) VALUES ")
+            for (int i = 1; i <= totalRows; i++) {
+                if (i > 1) table2Rows.append(", ")
+                table2Rows.append("(${i}, 'snap', 0)")
+            }
+            sql table2Rows.toString()
+
+            sql """DROP TABLE IF EXISTS ${mysqlDb}.${excludedTable}"""
+            sql """CREATE TABLE ${mysqlDb}.${excludedTable} (
                   `id` int NOT NULL,
                   `tag` varchar(64),
                   PRIMARY KEY (`id`)
                 ) ENGINE=InnoDB"""
-            sql """INSERT INTO ${mysqlDb}.${unrelated} (id, tag) VALUES (1, 
'pre_snap')"""
+            sql """INSERT INTO ${mysqlDb}.${excludedTable} (id, tag) VALUES 
(1, 'pre_snap')"""
         }
 
-        // snapshot_split_size=10 + snapshot_parallelism=1 -> 100 serial 
splits, slow enough that
-        // the concurrent DML below actually overlaps with snapshot.
+        // Two tables with different primary-key names make a foreign-table 
backfill record
+        // incompatible with the current split key. Serial small splits keep 
the snapshot active.
         sql """CREATE JOB ${jobName}
                 ON STREAMING
                 FROM MYSQL (
@@ -79,27 +98,54 @@ 
suite("test_streaming_mysql_job_snapshot_with_concurrent_dml", "p0,external,mysq
                     "user" = "root",
                     "password" = "123456",
                     "database" = "${mysqlDb}",
-                    "include_tables" = "${table1}",
+                    "include_tables" = "${table1},${table2}",
                     "offset" = "initial",
                     "snapshot_split_size" = "10",
-                    "snapshot_parallelism" = "1"
+                    "snapshot_parallelism" = "1",
+                    "skip_snapshot_backfill" = "false"
                 )
                 TO DATABASE ${currentDb} (
                   "table.create.properties.replication_num" = "1"
                 )
             """
 
-        // Concurrent DML on source while cdc-client is still snapshotting.
+        def succeedTaskCount = {
+            def rows = sql """select SucceedTaskCount from 
jobs("type"="insert") where Name='${jobName}' and ExecuteType='STREAMING'"""
+            rows.size() == 1 ? (rows.get(0).get(0).toString() as long) : 0L
+        }
+        Awaitility.await().atMost(120, SECONDS).pollInterval(100, 
MILLISECONDS).until({
+            succeedTaskCount() >= 1
+        })
+
+        def writeProbeDml = {
+            connect("root", "123456", 
"jdbc:mysql://${externalEnvIp}:${mysql_port}") {
+                sql """UPDATE ${mysqlDb}.${table1} SET version=version+1 WHERE 
id=500"""
+                sql """UPDATE ${mysqlDb}.${table2} SET version=version+1 WHERE 
other_id=500"""
+            }
+        }
+
+        writeProbeDml()
+        long probeStartTaskCount = succeedTaskCount()
+        long probeTargetTaskCount = probeStartTaskCount + 10
+        Awaitility.await().atMost(120, SECONDS).pollInterval(200, 
MILLISECONDS).until({
+            writeProbeDml()
+            succeedTaskCount() >= probeTargetTaskCount
+        })
+        log.info("MySQL probe DML covered tasks 
${probeStartTaskCount}..${succeedTaskCount()}")
+
+        // Apply deterministic DML after the probe updates so final results 
remain stable.
         connect("root", "123456", 
"jdbc:mysql://${externalEnvIp}:${mysql_port}") {
             for (int i = 1; i <= 10; i++) {
                 sql """INSERT INTO ${mysqlDb}.${table1} (id, tag, version) 
VALUES (${totalRows + i}, 'concurrent_ins', 1)"""
+                sql """INSERT INTO ${mysqlDb}.${table2} (other_id, tag, 
version) VALUES (${totalRows + i}, 'concurrent_ins', 1)"""
             }
             sql """UPDATE ${mysqlDb}.${table1} SET version=99 WHERE id IN (1, 
100, 500, 999)"""
             sql """DELETE FROM ${mysqlDb}.${table1} WHERE id IN (2, 200, 
800)"""
+            sql """UPDATE ${mysqlDb}.${table2} SET version=99 WHERE other_id 
IN (1, 100, 500, 999)"""
+            sql """DELETE FROM ${mysqlDb}.${table2} WHERE other_id IN (2, 200, 
800)"""
 
-            // DML on unrelated table - must NOT leak into Doris (not in 
include_tables).
-            sql """INSERT INTO ${mysqlDb}.${unrelated} (id, tag) VALUES (2, 
'concurrent_unrelated_ins')"""
-            sql """UPDATE ${mysqlDb}.${unrelated} SET 
tag='concurrent_unrelated_upd' WHERE id=1"""
+            sql """INSERT INTO ${mysqlDb}.${excludedTable} (id, tag) VALUES 
(2, 'concurrent_excluded_ins')"""
+            sql """UPDATE ${mysqlDb}.${excludedTable} SET 
tag='concurrent_excluded_upd' WHERE id=1"""
         }
 
         def expectedRows = totalRows + 10 - 3
@@ -113,15 +159,26 @@ 
suite("test_streaming_mysql_job_snapshot_with_concurrent_dml", "p0,external,mysq
                         def del2 = sql """select count(1) from 
${currentDb}.${table1} where id=2"""
                         def del800 = sql """select count(1) from 
${currentDb}.${table1} where id=800"""
                         def ins1010 = sql """select count(1) from 
${currentDb}.${table1} where id=${totalRows + 10}"""
+                        def cnt2 = sql """select count(1) from 
${currentDb}.${table2}"""
+                        def upd2 = sql """select version from 
${currentDb}.${table2} where other_id=999"""
+                        def del2Table2 = sql """select count(1) from 
${currentDb}.${table2} where other_id=2"""
+                        def ins2 = sql """select count(1) from 
${currentDb}.${table2} where other_id=${totalRows + 10}"""
                         def v1 = upd1.size() == 0 ? null : upd1.get(0).get(0)
                         def v999 = upd999.size() == 0 ? null : 
upd999.get(0).get(0)
-                        log.info("incr cnt=${cnt} v1=${v1} v999=${v999} 
del2=${del2} del800=${del800} ins1010=${ins1010}")
+                        def v2 = upd2.size() == 0 ? null : upd2.get(0).get(0)
+                        log.info("incr cnt=${cnt} cnt2=${cnt2} v1=${v1} 
v999=${v999} v2=${v2} "
+                                + "del2=${del2} del800=${del800} 
del2Table2=${del2Table2} "
+                                + "ins1010=${ins1010} ins2=${ins2}")
                         cnt.get(0).get(0) == expectedRows &&
+                                cnt2.get(0).get(0) == expectedRows &&
                                 v1 != null && v1.toString() == '99' &&
                                 v999 != null && v999.toString() == '99' &&
+                                v2 != null && v2.toString() == '99' &&
                                 del2.get(0).get(0) == 0 &&
                                 del800.get(0).get(0) == 0 &&
-                                ins1010.get(0).get(0) == 1
+                                del2Table2.get(0).get(0) == 0 &&
+                                ins1010.get(0).get(0) == 1 &&
+                                ins2.get(0).get(0) == 1
                     }
             )
         } catch (Exception ex) {
@@ -132,13 +189,17 @@ 
suite("test_streaming_mysql_job_snapshot_with_concurrent_dml", "p0,external,mysq
             throw ex
         }
 
-        def showUnrelated = sql """show tables from ${currentDb} like 
'${unrelated}'"""
-        assert showUnrelated.size() == 0
+        def showExcluded = sql """show tables from ${currentDb} like 
'${excludedTable}'"""
+        assert showExcluded.size() == 0
 
         qt_select_count """select count(1) from ${currentDb}.${table1}"""
+        qt_select_count_t2 """select count(1) from ${currentDb}.${table2}"""
         qt_select_updates """select id, version from ${currentDb}.${table1} 
where id in (1, 100, 500, 999) order by id"""
+        qt_select_updates_t2 """select other_id, version from 
${currentDb}.${table2} where other_id in (1, 100, 500, 999) order by other_id"""
         qt_select_deletes """select count(1) from ${currentDb}.${table1} where 
id in (2, 200, 800)"""
+        qt_select_deletes_t2 """select count(1) from ${currentDb}.${table2} 
where other_id in (2, 200, 800)"""
         qt_select_inserts """select id, tag, version from 
${currentDb}.${table1} where id > ${totalRows} order by id"""
+        qt_select_inserts_t2 """select other_id, tag, version from 
${currentDb}.${table2} where other_id > ${totalRows} order by other_id"""
 
         sql """DROP JOB IF EXISTS where jobname = '${jobName}'"""
 
diff --git 
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table.groovy
 
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table.groovy
index 6ecdffdea8d..6dd528a3a7e 100644
--- 
a/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table.groovy
+++ 
b/regression-test/suites/job_p0/streaming_job/cdc/test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table.groovy
@@ -18,6 +18,7 @@
 
 import org.awaitility.Awaitility
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS
 import static java.util.concurrent.TimeUnit.SECONDS
 
 // Multi-table from-to snapshot + concurrent DML: with >=2 tables each 
snapshot split flips the
@@ -31,6 +32,7 @@ 
suite("test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table",
     def table1 = "streaming_snapshot_dml_multi_pg_t1"
     def table2 = "streaming_snapshot_dml_multi_pg_t2"
     def tables = [table1, table2]
+    def primaryKeys = [(table1): "pk", (table2): "other_id"]
     def pgDB = "postgres"
     def pgSchema = "cdc_test"
     def pgUser = "postgres"
@@ -51,21 +53,22 @@ 
suite("test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table",
         // ===== Prepare PG side: two tables, each 1000 snapshot rows =====
         connect("${pgUser}", "${pgPassword}", 
"jdbc:postgresql://${externalEnvIp}:${pg_port}/${pgDB}") {
             tables.each { t ->
+                def primaryKey = primaryKeys[t]
                 sql """DROP TABLE IF EXISTS ${pgDB}.${pgSchema}.${t}"""
                 sql """
                 create table ${pgDB}.${pgSchema}.${t} (
-                    id      integer PRIMARY KEY,
+                    ${primaryKey} integer PRIMARY KEY,
                     tag     varchar(64),
                     version integer
                 );
                 """
-                sql """INSERT INTO ${pgDB}.${pgSchema}.${t} (id, tag, version)
+                sql """INSERT INTO ${pgDB}.${pgSchema}.${t} (${primaryKey}, 
tag, version)
                        SELECT g, 'snap', 0 FROM generate_series(1, 
${totalRows}) g"""
             }
         }
 
-        // snapshot_split_size=10 + snapshot_parallelism=1 -> 100 serial 
splits per table, slow
-        // enough that the concurrent DML overlaps snapshot while the 
publication keeps flipping.
+        // Different primary-key names make a foreign-table record 
incompatible with the current
+        // split key. Serial small splits keep the snapshot active during 
concurrent DML.
         sql """CREATE JOB ${jobName}
                 ON STREAMING
                 FROM POSTGRES (
@@ -79,28 +82,49 @@ 
suite("test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table",
                     "include_tables" = "${table1},${table2}",
                     "offset" = "initial",
                     "snapshot_split_size" = "10",
-                    "snapshot_parallelism" = "1"
+                    "snapshot_parallelism" = "1",
+                    "skip_snapshot_backfill" = "false"
                 )
                 TO DATABASE ${currentDb} (
                   "table.create.properties.replication_num" = "1"
                 )
             """
 
-        // Wait until the first snapshot split commits (slot created, snapshot 
in progress) so the
-        // DML below lands inside the snapshot window and overlaps the 
publication flipping.
-        Awaitility.await().atMost(120, SECONDS).pollInterval(1, 
SECONDS).until({
-            def c = sql """select SucceedTaskCount from jobs("type"="insert") 
where Name='${jobName}' and ExecuteType='STREAMING'"""
-            c.size() == 1 && (c.get(0).get(0).toString() as long) >= 1
+        // Wait until snapshot processing starts, then keep writing while 
subsequent splits run.
+        def succeedTaskCount = {
+            def rows = sql """select SucceedTaskCount from 
jobs("type"="insert") where Name='${jobName}' and ExecuteType='STREAMING'"""
+            rows.size() == 1 ? (rows.get(0).get(0).toString() as long) : 0L
+        }
+        Awaitility.await().atMost(120, SECONDS).pollInterval(100, 
MILLISECONDS).until({
+            succeedTaskCount() >= 1
+        })
+
+        def writeProbeDml = {
+            connect("${pgUser}", "${pgPassword}",
+                    "jdbc:postgresql://${externalEnvIp}:${pg_port}/${pgDB}") {
+                sql """UPDATE ${pgDB}.${pgSchema}.${table1} SET 
version=version+1 WHERE pk=500"""
+                sql """UPDATE ${pgDB}.${pgSchema}.${table2} SET 
version=version+1 WHERE other_id=500"""
+            }
+        }
+
+        writeProbeDml()
+        long probeStartTaskCount = succeedTaskCount()
+        long probeTargetTaskCount = probeStartTaskCount + 10
+        Awaitility.await().atMost(120, SECONDS).pollInterval(200, 
MILLISECONDS).until({
+            writeProbeDml()
+            succeedTaskCount() >= probeTargetTaskCount
         })
+        log.info("PostgreSQL probe DML covered tasks 
${probeStartTaskCount}..${succeedTaskCount()}")
 
-        // Concurrent DML on BOTH tables while still snapshotting. Same DML 
shape on each table.
+        // Apply deterministic DML after the probe updates so final results 
remain stable.
         connect("${pgUser}", "${pgPassword}", 
"jdbc:postgresql://${externalEnvIp}:${pg_port}/${pgDB}") {
             tables.each { t ->
+                def primaryKey = primaryKeys[t]
                 for (int i = 1; i <= 10; i++) {
-                    sql """INSERT INTO ${pgDB}.${pgSchema}.${t} (id, tag, 
version) VALUES (${totalRows + i}, 'concurrent_ins', 1)"""
+                    sql """INSERT INTO ${pgDB}.${pgSchema}.${t} 
(${primaryKey}, tag, version) VALUES (${totalRows + i}, 'concurrent_ins', 1)"""
                 }
-                sql """UPDATE ${pgDB}.${pgSchema}.${t} SET version=99 WHERE id 
IN (1, 100, 500, 999)"""
-                sql """DELETE FROM ${pgDB}.${pgSchema}.${t} WHERE id IN (2, 
200, 800)"""
+                sql """UPDATE ${pgDB}.${pgSchema}.${t} SET version=99 WHERE 
${primaryKey} IN (1, 100, 500, 999)"""
+                sql """DELETE FROM ${pgDB}.${pgSchema}.${t} WHERE 
${primaryKey} IN (2, 200, 800)"""
             }
         }
 
@@ -112,17 +136,18 @@ 
suite("test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table",
                     {
                         boolean allOk = true
                         for (def t : tables) {
+                            def primaryKey = primaryKeys[t]
                             def showTbl = sql """show tables from ${currentDb} 
like '${t}'"""
                             if (showTbl.size() == 0) {
                                 allOk = false
                                 break
                             }
                             def cnt = sql """select count(1) from 
${currentDb}.${t}"""
-                            def upd1 = sql """select version from 
${currentDb}.${t} where id=1"""
-                            def upd999 = sql """select version from 
${currentDb}.${t} where id=999"""
-                            def del2 = sql """select count(1) from 
${currentDb}.${t} where id=2"""
-                            def del800 = sql """select count(1) from 
${currentDb}.${t} where id=800"""
-                            def ins = sql """select count(1) from 
${currentDb}.${t} where id=${totalRows + 10}"""
+                            def upd1 = sql """select version from 
${currentDb}.${t} where ${primaryKey}=1"""
+                            def upd999 = sql """select version from 
${currentDb}.${t} where ${primaryKey}=999"""
+                            def del2 = sql """select count(1) from 
${currentDb}.${t} where ${primaryKey}=2"""
+                            def del800 = sql """select count(1) from 
${currentDb}.${t} where ${primaryKey}=800"""
+                            def ins = sql """select count(1) from 
${currentDb}.${t} where ${primaryKey}=${totalRows + 10}"""
                             def v1 = upd1.size() == 0 ? null : 
upd1.get(0).get(0)
                             def v999 = upd999.size() == 0 ? null : 
upd999.get(0).get(0)
                             log.info("table=${t} cnt=${cnt} v1=${v1} 
v999=${v999} del2=${del2} del800=${del800} ins=${ins}")
@@ -150,10 +175,10 @@ 
suite("test_streaming_postgres_job_snapshot_with_concurrent_dml_multi_table",
 
         qt_select_count_t1 """select count(1) from ${currentDb}.${table1}"""
         qt_select_count_t2 """select count(1) from ${currentDb}.${table2}"""
-        qt_select_updates_t1 """select id, version from ${currentDb}.${table1} 
where id in (1, 100, 500, 999) order by id"""
-        qt_select_updates_t2 """select id, version from ${currentDb}.${table2} 
where id in (1, 100, 500, 999) order by id"""
-        qt_select_inserts_t1 """select id, tag, version from 
${currentDb}.${table1} where id > ${totalRows} order by id"""
-        qt_select_inserts_t2 """select id, tag, version from 
${currentDb}.${table2} where id > ${totalRows} order by id"""
+        qt_select_updates_t1 """select pk, version from ${currentDb}.${table1} 
where pk in (1, 100, 500, 999) order by pk"""
+        qt_select_updates_t2 """select other_id, version from 
${currentDb}.${table2} where other_id in (1, 100, 500, 999) order by other_id"""
+        qt_select_inserts_t1 """select pk, tag, version from 
${currentDb}.${table1} where pk > ${totalRows} order by pk"""
+        qt_select_inserts_t2 """select other_id, tag, version from 
${currentDb}.${table2} where other_id > ${totalRows} order by other_id"""
 
         sql """DROP JOB IF EXISTS where jobname = '${jobName}'"""
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to