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

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-11885-29df45855c65beaaffbf7fad2db67954231f12d6
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit 67941ddefacebbcadab6ba72f7e3877d10cbaee9
Author: Li Zhi <[email protected]>
AuthorDate: Wed Oct 14 11:03:03 2026 +0000

    [Fix][Connector-CDC] Resolve stop.mode=latest stop offset at binlog phase 
start (#11885)
    
    Co-authored-by: AtomCode (deepseek-v4-flash) <[email protected]>
---
 docs/en/connectors/source/MySQL-CDC.md             |  22 ++-
 docs/zh/connectors/source/MySQL-CDC.md             |  21 ++-
 .../enumerator/IncrementalSplitAssigner.java       |  92 +++++++++++-
 .../enumerator/state/IncrementalPhaseState.java    |  15 +-
 .../source/enumerator/HybridSplitAssignerTest.java |  15 +-
 .../enumerator/IncrementalSplitAssignerTest.java   |  83 +++++++++++
 .../state/IncrementalPhaseStateTest.java           |  91 ++++++++++++
 .../cdc/mysql/MysqlCDCStopModeSpecificIT.java      | 162 +++++++++++++++++++++
 .../test/resources/mysqlcdc_stop_mode_latest.conf  |  56 +++++++
 .../mysqlcdc_stop_mode_latest_specific.conf        |  51 +++++++
 .../mysqlcdc_stop_mode_latest_timestamp.conf       |  50 +++++++
 11 files changed, 650 insertions(+), 8 deletions(-)

diff --git a/docs/en/connectors/source/MySQL-CDC.md 
b/docs/en/connectors/source/MySQL-CDC.md
index 8c73533b18..ee7734c318 100644
--- a/docs/en/connectors/source/MySQL-CDC.md
+++ b/docs/en/connectors/source/MySQL-CDC.md
@@ -200,7 +200,7 @@ When an initial consistent snapshot is made for large 
databases, your establishe
 | startup.specific-offset.skip-events       | Long     | No       | 0       | 
Number of binlog events to skip after the configured specific startup offset. 
This option can only be used when `startup.mode` is `specific`.                 
                                                                                
                                                                                
                                                                                
                 [...]
 | startup.specific-offset.skip-rows         | Long     | No       | 0       | 
Number of rows to skip after the configured specific startup offset. This 
option can only be used when `startup.mode` is `specific`.                      
                                                                                
                                                                                
                                                                                
                     [...]
 | startup.timestamp                         | Long     | No       | -       | 
Start from the specified timestamp, in milliseconds since Unix epoch. **Note, 
This option is required when the `startup.mode` option uses `timestamp`.**      
                                                                                
                                                                                
                                                                                
                 [...]
-| stop.mode                                 | Enum     | No       | NEVER   | 
Optional stop mode for MySQL CDC consumer, valid enumerations are `never`, 
`latest` or `specific`. <br/> `never`: Real-time job don't stop the 
source.<br/> `latest`: Stop from the latest offset.<br/> `specific`: Stop from 
user-supplied specific offset.                                                  
                                                                                
                                 [...]
+| stop.mode                                 | Enum     | No       | NEVER   | 
Optional stop mode for MySQL CDC consumer, valid enumerations are `never`, 
`latest` or `specific`. <br/> `never`: Real-time job don't stop the 
source.<br/> `latest`: Stop from the latest offset. When combined with a 
snapshot-taking startup mode (`initial`/`earliest`), the stop offset is 
resolved once the snapshot phase completes, so changes written to the source 
while the snapshot is running are still captured  [...]
 | stop.specific-offset.file                 | String   | No       | -       | 
Stop from the specified binlog file name. **Note, This option is required when 
the `stop.mode` option used `specific`.**                                       
                                                                                
                                                                                
                                                                                
                [...]
 | stop.specific-offset.pos                  | Long     | No       | -       | 
Stop from the specified binlog file position. **Note, This option is required 
when the `stop.mode` option used `specific`.**                                  
                                                                                
                                                                                
                                                                                
                 [...]
 | snapshot.split.size                       | Integer  | No       | 8096    | 
The split size (number of rows) of table snapshot, captured tables are split 
into multiple splits when read the snapshot of table.                           
                                                                                
                                                                                
                                                                                
                  [...]
@@ -541,6 +541,26 @@ startup offset (or startup timestamp) and the configured 
stop offset, then termi
 > **Note**: bounded-read termination is currently supported on the **Zeta** 
 > engine only.
 > Flink and Spark engines do not support bounded incremental-split termination 
 > yet.
 
+`stop.mode = "latest"` also makes the job a bounded read: the stop offset is 
the binlog
+position at the moment the snapshot phase completes (for `startup.mode = 
"initial"`/`"earliest"`)
+or at job start (for the other startup modes). In particular, when combined 
with a
+snapshot-taking startup mode, changes written to the source **while the 
snapshot is running
+are not dropped** — they are captured by the binlog phase before the job 
terminates.
+
+```hocon
+source {
+  MySQL-CDC {
+    server-id = 5654
+    username = "st_user_source"
+    password = "mysqlpw"
+    table-names = ["mysql_cdc.mysql_cdc_e2e_source_table"]
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    startup.mode = "initial"
+    stop.mode = "latest"
+  }
+}
+```
+
 ```hocon
 source {
   MySQL-CDC {
diff --git a/docs/zh/connectors/source/MySQL-CDC.md 
b/docs/zh/connectors/source/MySQL-CDC.md
index 8a7b26c905..1aee4c39b5 100644
--- a/docs/zh/connectors/source/MySQL-CDC.md
+++ b/docs/zh/connectors/source/MySQL-CDC.md
@@ -199,7 +199,7 @@ show variables where variable_name in ('log_bin', 
'binlog_format', 'binlog_row_i
 | startup.specific-offset.skip-events       | Long     | 否    | 0       | 配置 
specific 启动偏移量后需要跳过的 binlog event 数量. 该选项只能在 `startup.mode` 为 `specific` 时使用.   
                                                                                
                                                                      |
 | startup.specific-offset.skip-rows         | Long     | 否    | 0       | 配置 
specific 启动偏移量后需要跳过的行数. 该选项只能在 `startup.mode` 为 `specific` 时使用.                 
                                                                                
                                                                   |
 | startup.timestamp                         | Long     | 否    | -       | 
从指定时间戳启动,单位为 Unix 纪元以来的毫秒数。**注意,当 `startup.mode` 为 `timestamp` 时,此选项必填。**       
                                                                                
                                                                             |
-| stop.mode                                 | Enum     | 否    | NEVER   | 
MySQL CDC 消费者的可选停止模式, 有效枚举值为 `never`, `latest` 和 `specific`. <br/> `never`: 
实时任务一直运行不停止.<br/> `latest`: 从最新的偏移量处停止.<br/> `specific`: 从用户提供的特定偏移量处停止.        
                                                                                
 |
+| stop.mode                                 | Enum     | 否    | NEVER   | 
MySQL CDC 消费者的可选停止模式, 有效枚举值为 `never`, `latest` 和 `specific`. <br/> `never`: 
实时任务一直运行不停止.<br/> `latest`: 从最新的偏移量处停止. 
与需要执行快照的启动模式(`initial`/`earliest`)组合使用时, 停止偏移量在快照阶段完成后解析, 因此快照运行期间写入源表的变更仍会被 
binlog 阶段捕获, 不会丢失.<br/> `specific`: 从用户提供的特定偏移量处停止.                             
                                                            |
 | stop.specific-offset.file                 | String   | 否    | -       | 
从指定的binlog日志文件名停止. **注意, 当使用 `stop.mode` 选项为 `specific` 时,此选项为必填项.**            
                                                                                
                                                                             |
 | stop.specific-offset.pos                  | Long     | 否    | -       | 
从指定的binlog日志文件位置停止. **注意, 当使用 `stop.mode` 选项为 `specific` 时,此选项为必填项.**           
                                                                                
                                                                             |
 | snapshot.split.size                       | Integer  | 否    | 8096    | 
表快照的分割大小(行数),读取表的快照时,被捕获的表会被分割成多个分割块.                                           
                                                                                
                                                                             |
@@ -533,6 +533,25 @@ source {
 > **注意**:有界读取的终止行为目前仅在 **Zeta** 引擎上支持。
 > Flink 和 Spark 引擎暂不支持有界增量分片的终止。
 
+`stop.mode = "latest"` 同样会使作业变为有界读取:停止偏移量为快照阶段完成时的 binlog 位置
+(当 `startup.mode` 为 `initial`/`earliest` 时),或作业启动时的 binlog 位置(其他启动模式)。
+特别是与需要执行快照的启动模式组合使用时,**快照运行期间写入源表的变更不会丢失**——
+它们会在作业终止前被 binlog 阶段捕获。
+
+```hocon
+source {
+  MySQL-CDC {
+    server-id = 5654
+    username = "st_user_source"
+    password = "mysqlpw"
+    table-names = ["mysql_cdc.mysql_cdc_e2e_source_table"]
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    startup.mode = "initial"
+    stop.mode = "latest"
+  }
+}
+```
+
 ```hocon
 source {
   MySQL-CDC {
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssigner.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssigner.java
index a6188e9183..aa994efd4a 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssigner.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssigner.java
@@ -20,9 +20,11 @@ package 
org.apache.seatunnel.connectors.cdc.base.source.enumerator;
 import 
org.apache.seatunnel.shade.com.google.common.annotations.VisibleForTesting;
 
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.common.utils.SeaTunnelException;
 import org.apache.seatunnel.connectors.cdc.base.config.SourceConfig;
 import org.apache.seatunnel.connectors.cdc.base.config.StartupConfig;
 import org.apache.seatunnel.connectors.cdc.base.option.StartupMode;
+import org.apache.seatunnel.connectors.cdc.base.option.StopMode;
 import 
org.apache.seatunnel.connectors.cdc.base.source.enumerator.state.IncrementalPhaseState;
 import 
org.apache.seatunnel.connectors.cdc.base.source.event.SnapshotSplitWatermark;
 import org.apache.seatunnel.connectors.cdc.base.source.offset.Offset;
@@ -85,6 +87,12 @@ public class IncrementalSplitAssigner<C extends 
SourceConfig> implements SplitAs
      */
     private Offset startupOffset;
 
+    /**
+     * The stop offset resolved once when the snapshot phase completes ({@code 
stop.mode = latest}),
+     * reused after checkpoint restore so that a restart does not re-resolve 
(and drift) it.
+     */
+    private Offset resolvedStopOffset;
+
     private final boolean restoredFromCheckpoint;
 
     public IncrementalSplitAssigner(
@@ -113,6 +121,7 @@ public class IncrementalSplitAssigner<C extends 
SourceConfig> implements SplitAs
         this.offsetFactory = offsetFactory;
         this.restoredFromCheckpoint = true;
         this.startupOffset = checkpointState == null ? null : 
checkpointState.getStartupOffset();
+        this.resolvedStopOffset = checkpointState == null ? null : 
checkpointState.getStopOffset();
     }
 
     @Override
@@ -190,6 +199,21 @@ public class IncrementalSplitAssigner<C extends 
SourceConfig> implements SplitAs
                             if (this.startupOffset == null) {
                                 this.startupOffset = startupOffset;
                             }
+                            // Mirror the startupOffset restoration for the 
resolved latest
+                            // stop offset: a checkpoint written before the 
stopOffset field
+                            // existed has none, so on restore the re-created 
incremental
+                            // split carries the previously resolved stop 
offset. Reuse it
+                            // here instead of re-resolving (and drifting) at 
split creation.
+                            // If several splits with already-diverged stop 
offsets are handed
+                            // back in the same call, the first one processed 
wins (they can
+                            // only diverge in the same narrow 
legacy-checkpoint upgrade case).
+                            if (resolvedStopOffset == null
+                                    && context.getSourceConfig() != null
+                                    && 
context.getSourceConfig().getStopConfig().getStopMode()
+                                            == StopMode.LATEST
+                                    && incrementalSplit.getStopOffset() != 
null) {
+                                resolvedStopOffset = 
incrementalSplit.getStopOffset();
+                            }
                             checkpointTables = 
incrementalSplit.getCheckpointTables();
                             historyTableChanges = 
incrementalSplit.getHistoryTableChanges();
                         });
@@ -200,7 +224,7 @@ public class IncrementalSplitAssigner<C extends 
SourceConfig> implements SplitAs
 
     @Override
     public IncrementalPhaseState snapshotState(long checkpointId) {
-        return new IncrementalPhaseState(startupOffset);
+        return new IncrementalPhaseState(startupOffset, resolvedStopOffset);
     }
 
     @Override
@@ -296,11 +320,32 @@ public class IncrementalSplitAssigner<C extends 
SourceConfig> implements SplitAs
             startupOffset = 
sourceConfig.getStartupConfig().getStartupOffset(offsetFactory);
         }
         Offset incrementalSplitStartOffset = minOffset != null ? minOffset : 
startupOffset;
+        // stop.mode=latest: resolve the stop offset exactly once here, at the 
first
+        // incremental split creation, and reuse the same value for every 
subsequent split
+        // and for the checkpoint. This is the single authoritative resolution 
point (the
+        // split handed to the reader and the checkpointed value can never 
diverge, so a
+        // restore cannot silently move the stop boundary). Other stop modes 
keep the
+        // split's configured offset from StopConfig.
+        if (resolvedStopOffset == null
+                && sourceConfig.getStopConfig().getStopMode() == 
StopMode.LATEST) {
+            // The latest() resolution opens a fresh JDBC connection to query 
the current
+            // binlog position; a transient failure at this exact 
snapshot-to-incremental
+            // transition point must not fail the whole job, so retry with a 
short backoff
+            // (mirrors the connection-factory retry pattern).
+            resolvedStopOffset = 
resolveLatestStopOffsetWithRetry(sourceConfig);
+            LOG.info(
+                    "stop.mode=latest: resolved stop offset {} at incremental 
split creation",
+                    resolvedStopOffset);
+        }
+        Offset incrementalSplitStopOffset =
+                resolvedStopOffset != null
+                        ? resolvedStopOffset
+                        : 
sourceConfig.getStopConfig().getStopOffset(offsetFactory);
         return new IncrementalSplit(
                 String.format(INCREMENTAL_SPLIT_ID, index),
                 capturedTables,
                 incrementalSplitStartOffset,
-                sourceConfig.getStopConfig().getStopOffset(offsetFactory),
+                incrementalSplitStopOffset,
                 completedSnapshotSplitInfos,
                 checkpointTables,
                 historyTableChanges);
@@ -321,11 +366,50 @@ public class IncrementalSplitAssigner<C extends 
SourceConfig> implements SplitAs
                 
context.getSplitCompletedOffsets().remove(assignedSplit.splitId());
             }
         }
-        return context.getAssignedSnapshotSplit().isEmpty()
-                && context.getSplitCompletedOffsets().isEmpty();
+        boolean completed =
+                context.getAssignedSnapshotSplit().isEmpty()
+                        && context.getSplitCompletedOffsets().isEmpty();
+        return completed;
     }
 
     public boolean waitingForAssignedSplits() {
         return !(splitAssigned && noMoreSplits());
     }
+
+    /**
+     * Resolves the latest stop offset with retry, so a transient failure of 
the underlying JDBC
+     * query (e.g. a momentarily unreachable database at the 
snapshot-to-incremental transition)
+     * does not fail the whole job. Mirrors the retry pattern used by the CDC 
connection factory.
+     *
+     * <p>Threading note: this runs on the job's own split-enumerator 
coordinator thread (invoked
+     * via {@code IncrementalSourceEnumerator.assignSplits()} → {@code 
getNext()}), which is per-job
+     * and not shared across concurrently running sources; the bounded backoff 
(at most 300ms +
+     * 600ms) blocks only this job's enumerator, matching the 
connection-factory retry.
+     */
+    private Offset resolveLatestStopOffsetWithRetry(C sourceConfig) {
+        final int maxRetries = 3;
+        for (int attempt = 1; ; attempt++) {
+            try {
+                return 
sourceConfig.getStopConfig().getStopOffset(offsetFactory);
+            } catch (RuntimeException e) {
+                if (attempt < maxRetries) {
+                    try {
+                        Thread.sleep(300L * attempt);
+                    } catch (InterruptedException ie) {
+                        Thread.currentThread().interrupt();
+                        throw new SeaTunnelException(
+                                "Interrupted while retrying latest stop offset 
resolution", ie);
+                    }
+                    LOG.warn("Resolving latest stop offset failed, retry times 
{}", attempt, e);
+                } else {
+                    LOG.error("Resolving latest stop offset failed after {} 
attempts", attempt, e);
+                    throw new SeaTunnelException(
+                            "Failed to resolve the latest stop offset after "
+                                    + attempt
+                                    + " attempts",
+                            e);
+                }
+            }
+        }
+    }
 }
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseState.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseState.java
index 80d6ced1fc..d0b78ecb89 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseState.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/main/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseState.java
@@ -30,11 +30,24 @@ public class IncrementalPhaseState implements 
PendingSplitsState {
 
     private final Offset startupOffset;
 
+    /**
+     * The stop offset resolved once at the enumerator when the snapshot phase 
completes ({@code
+     * stop.mode = latest}). Stored in the checkpoint so that a restart reuses 
the same value
+     * instead of re-resolving (and drifting) it. {@code null} for other stop 
modes and for
+     * checkpoints written before this field existed.
+     */
+    private final Offset stopOffset;
+
     public IncrementalPhaseState() {
-        this(null);
+        this(null, null);
     }
 
     public IncrementalPhaseState(Offset startupOffset) {
+        this(startupOffset, null);
+    }
+
+    public IncrementalPhaseState(Offset startupOffset, Offset stopOffset) {
         this.startupOffset = startupOffset;
+        this.stopOffset = stopOffset;
     }
 }
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/HybridSplitAssignerTest.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/HybridSplitAssignerTest.java
index 2c931eb9e4..ebbbfc09fb 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/HybridSplitAssignerTest.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/HybridSplitAssignerTest.java
@@ -17,6 +17,9 @@
 
 package org.apache.seatunnel.connectors.cdc.base.source.enumerator;
 
+import org.apache.seatunnel.connectors.cdc.base.config.SourceConfig;
+import org.apache.seatunnel.connectors.cdc.base.config.StopConfig;
+import org.apache.seatunnel.connectors.cdc.base.option.StopMode;
 import 
org.apache.seatunnel.connectors.cdc.base.source.enumerator.state.HybridPendingSplitsState;
 import 
org.apache.seatunnel.connectors.cdc.base.source.enumerator.state.SnapshotPhaseState;
 import 
org.apache.seatunnel.connectors.cdc.base.source.event.SnapshotSplitWatermark;
@@ -34,6 +37,9 @@ import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 public class HybridSplitAssignerTest {
     @Test
     public void testCompletedSnapshotPhase() {
@@ -51,9 +57,16 @@ public class HybridSplitAssignerTest {
                         false);
         HybridPendingSplitsState checkpointState =
                 new HybridPendingSplitsState(snapshotPhaseState, null);
+        // This test only exercises split-tracking bookkeeping, so a minimal 
mocked
+        // SourceConfig (non-LATEST stop mode) keeps the Context well-formed. 
The
+        // stop.mode=latest resolution lives in 
IncrementalSplitAssigner.createIncrementalSplit
+        // and is not exercised here.
+        SourceConfig sourceConfig = mock(SourceConfig.class);
+        when(sourceConfig.getStopConfig())
+                .thenReturn(new StopConfig(StopMode.NEVER, null, null, null));
         SplitAssigner.Context context =
                 new SplitAssigner.Context<>(
-                        null,
+                        sourceConfig,
                         Collections.emptySet(),
                         
checkpointState.getSnapshotPhaseState().getAssignedSplits(),
                         
checkpointState.getSnapshotPhaseState().getSplitCompletedOffsets());
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssignerTest.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssignerTest.java
index c96248e498..c02c79bc1d 100644
--- 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssignerTest.java
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/IncrementalSplitAssignerTest.java
@@ -42,6 +42,8 @@ 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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -152,6 +154,87 @@ class IncrementalSplitAssignerTest {
         assertSame(startupOffset, reassignedSplit.get().getStartupOffset());
     }
 
+    @Test
+    void 
shouldResolveLatestStopOffsetOnceAtSplitCreationAndReuseAfterRestore() {
+        SourceConfig sourceConfig = mock(SourceConfig.class);
+        OffsetFactory offsetFactory = mock(OffsetFactory.class);
+        Offset committedOffset = mock(Offset.class);
+        Offset resolvedOffset = mock(Offset.class);
+        when(sourceConfig.getStartupConfig())
+                .thenReturn(new StartupConfig(StartupMode.COMMITTED_OFFSET, 
null, null, null));
+        when(sourceConfig.getStopConfig())
+                .thenReturn(new StopConfig(StopMode.LATEST, null, null, null));
+        when(offsetFactory.committedOffset()).thenReturn(committedOffset);
+        when(offsetFactory.latest()).thenReturn(resolvedOffset);
+
+        SplitAssigner.Context<SourceConfig> context = 
createContext(sourceConfig);
+        IncrementalSplitAssigner<SourceConfig> assigner =
+                new IncrementalSplitAssigner<>(context, 1, offsetFactory);
+
+        // The single authoritative resolution happens exactly once, when the 
first
+        // incremental split is created; the same value is used for the split 
handed to
+        // the reader and stored in the checkpoint, so they can never diverge.
+        assertSame(resolvedOffset, 
assigner.getNext().get().asIncrementalSplit().getStopOffset());
+        verify(offsetFactory, times(1)).latest();
+
+        // Snapshot phase completes: no second resolution, the resolved value 
is reused.
+        assertTrue(assigner.completedSnapshotPhase(Collections.emptyList()));
+        verify(offsetFactory, times(1)).latest();
+
+        // The checkpoint stores the resolved stop offset...
+        IncrementalPhaseState checkpoint = assigner.snapshotState(1L);
+
+        // ...and a restored assigner reuses it instead of re-resolving (no 
drift, and no
+        // boundary change on restore: the checkpointed value is the same one 
the running
+        // reader was already stopping at).
+        IncrementalSplitAssigner<SourceConfig> restoredAssigner =
+                new IncrementalSplitAssigner<>(
+                        createContext(sourceConfig), 1, offsetFactory, 
checkpoint);
+        assertSame(
+                resolvedOffset,
+                
restoredAssigner.getNext().get().asIncrementalSplit().getStopOffset());
+        verify(offsetFactory, times(1)).latest();
+    }
+
+    @Test
+    void 
shouldRestoreResolvedStopOffsetFromInFlightSplitOnLegacyCheckpointRestore() {
+        SourceConfig sourceConfig = mock(SourceConfig.class);
+        OffsetFactory offsetFactory = mock(OffsetFactory.class);
+        Offset committedOffset = mock(Offset.class);
+        Offset resolvedOffset = mock(Offset.class);
+        when(sourceConfig.getStartupConfig())
+                .thenReturn(new StartupConfig(StartupMode.COMMITTED_OFFSET, 
null, null, null));
+        when(sourceConfig.getStopConfig())
+                .thenReturn(new StopConfig(StopMode.LATEST, null, null, null));
+        when(offsetFactory.committedOffset()).thenReturn(committedOffset);
+
+        // A legacy checkpoint (written before the stopOffset field existed) 
restores the
+        // assigner with resolvedStopOffset == null.
+        IncrementalPhaseState legacyCheckpoint = new 
IncrementalPhaseState(committedOffset);
+        SplitAssigner.Context<SourceConfig> context = 
createContext(sourceConfig);
+        IncrementalSplitAssigner<SourceConfig> restoredAssigner =
+                new IncrementalSplitAssigner<>(context, 1, offsetFactory, 
legacyCheckpoint);
+
+        // An already-assigned incremental split is handed back to the 
enumerator via
+        // addSplits() (e.g. a reader task failure/retry within the same job 
run); it still
+        // carries the previously resolved stop offset.
+        IncrementalSplit inFlightSplit =
+                new IncrementalSplit(
+                        "incremental-1",
+                        
Collections.singletonList(TableId.parse("database.schema.table")),
+                        committedOffset,
+                        resolvedOffset,
+                        Collections.emptyList());
+        restoredAssigner.addSplits(Collections.singletonList(inFlightSplit));
+
+        // The assigner adopts the in-flight split's stop offset as its 
resolvedStopOffset,
+        // so the next split created reuses it instead of re-resolving 
latest() (and drifting
+        // the stop boundary after a restore).
+        IncrementalSplit nextSplit = 
restoredAssigner.getNext().get().asIncrementalSplit();
+        assertSame(resolvedOffset, nextSplit.getStopOffset());
+        verify(offsetFactory, never()).latest();
+    }
+
     private SplitAssigner.Context<SourceConfig> createContext(SourceConfig 
sourceConfig) {
         return createContext(
                 sourceConfig, 
Collections.singleton(TableId.parse("database.schema.table")));
diff --git 
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseStateTest.java
 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseStateTest.java
new file mode 100644
index 0000000000..d66850b01f
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-base/src/test/java/org/apache/seatunnel/connectors/cdc/base/source/enumerator/state/IncrementalPhaseStateTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.cdc.base.source.enumerator.state;
+
+import org.apache.seatunnel.connectors.cdc.base.source.offset.Offset;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+class IncrementalPhaseStateTest {
+
+    @Test
+    void shouldPreserveStopOffsetThroughJavaSerializationRoundTrip() throws 
Exception {
+        Offset startupOffset = new TestOffset("startup", 1L);
+        Offset stopOffset = new TestOffset("stop", 2L);
+        IncrementalPhaseState state = new IncrementalPhaseState(startupOffset, 
stopOffset);
+
+        IncrementalPhaseState restored = roundTrip(state);
+
+        // A real checkpoint round-trip (plain Java serialization, the 
mechanism used for
+        // enumerator state) must keep the resolved stop offset: otherwise a 
restore would
+        // fall back to re-resolving latest() and drift the stop boundary.
+        Assertions.assertEquals(startupOffset, restored.getStartupOffset());
+        Assertions.assertEquals(stopOffset, restored.getStopOffset());
+    }
+
+    @Test
+    void shouldDefaultStopOffsetToNullOnLegacySerializedState() throws 
Exception {
+        // Simulates a checkpoint written before the stopOffset field existed 
(the single-arg
+        // constructor is what old versions persisted): with the unchanged 
serialVersionUID,
+        // Java field-evolution defaults the missing field to null, which 
falls through to the
+        // pre-existing StopConfig resolution path.
+        Offset startupOffset = new TestOffset("startup", 1L);
+        IncrementalPhaseState legacy = new 
IncrementalPhaseState(startupOffset);
+
+        IncrementalPhaseState restored = roundTrip(legacy);
+
+        Assertions.assertEquals(startupOffset, restored.getStartupOffset());
+        Assertions.assertNull(restored.getStopOffset());
+    }
+
+    private static IncrementalPhaseState roundTrip(IncrementalPhaseState 
state) throws Exception {
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
+            output.writeObject(state);
+        }
+        try (ObjectInputStream input =
+                new ObjectInputStream(new 
ByteArrayInputStream(buffer.toByteArray()))) {
+            return (IncrementalPhaseState) input.readObject();
+        }
+    }
+
+    /** A minimal serializable {@link Offset} implementation for round-trip 
tests. */
+    private static class TestOffset extends Offset {
+        private static final long serialVersionUID = 1L;
+
+        TestOffset(String file, long position) {
+            Map<String, String> values = new HashMap<>();
+            values.put("file", file);
+            values.put("pos", String.valueOf(position));
+            this.offset = values;
+        }
+
+        @Override
+        public int compareTo(Offset that) {
+            return 
this.offset.get("pos").compareTo(that.getOffset().get("pos"));
+        }
+    }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCStopModeSpecificIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCStopModeSpecificIT.java
index 4877f46d99..d464a45029 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCStopModeSpecificIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCStopModeSpecificIT.java
@@ -309,6 +309,151 @@ public class MysqlCDCStopModeSpecificIT extends 
TestSuiteBase implements TestRes
                         });
     }
 
+    @TestTemplate
+    public void testMysqlCdcInitialStartupWithLatestStop(TestContainer 
container) throws Exception {
+        runLatestStopStartupMode(container, "initial");
+    }
+
+    @TestTemplate
+    public void testMysqlCdcEarliestStartupWithLatestStop(TestContainer 
container)
+            throws Exception {
+        runLatestStopStartupMode(container, "earliest");
+    }
+
+    @TestTemplate
+    public void testMysqlCdcLatestStartupWithLatestStop(TestContainer 
container) throws Exception {
+        runLatestStopStartupMode(container, "latest");
+    }
+
+    @TestTemplate
+    public void testMysqlCdcSpecificStartupWithLatestStop(TestContainer 
container)
+            throws Exception {
+        runLatestStopStartupMode(container, "specific");
+    }
+
+    @TestTemplate
+    public void testMysqlCdcTimestampStartupWithLatestStop(TestContainer 
container)
+            throws Exception {
+        runLatestStopStartupMode(container, "timestamp");
+    }
+
+    private void runLatestStopStartupMode(TestContainer container, String 
startupMode)
+            throws Exception {
+        clearTable(MYSQL_DATABASE, SOURCE_TABLE);
+        clearTable(MYSQL_DATABASE, SINK_TABLE);
+
+        // Bulk-insert rows before starting the job.
+        // - initial: 2000 rows so the snapshot phase spans many splits 
deterministically
+        //   (snapshot.split.size=20 -> 100 splits, parallelism=1, consumed in 
ascending-key
+        //   order): the readiness gate below (first row visible in the sink) 
then fires while
+        //   ~99 splits are still unread, so the UPDATE is guaranteed to land 
inside the
+        //   snapshot window. A smaller table could be snapshotted entirely 
within one polling
+        //   interval and complete before the gate is observed (the flaky race 
SEZ9 flagged).
+        // - earliest: keep 200 rows — there is no snapshot phase (binlog 
replay from the
+        //   earliest position), and a larger bulk would only slow the replay.
+        // - latest/specific/timestamp: 200 rows, no snapshot window involved.
+        int bulkRowCount = "initial".equals(startupMode) ? 2000 : 200;
+        StringBuilder bulkInsert =
+                new StringBuilder(
+                        String.format(
+                                "INSERT INTO %s.%s (id, f_varchar) VALUES ",
+                                MYSQL_DATABASE, SOURCE_TABLE));
+        for (int i = 1; i <= bulkRowCount; i++) {
+            if (i > 1) {
+                bulkInsert.append(", ");
+            }
+            bulkInsert.append("(").append(i).append(", 'bulk')");
+        }
+        executeSql(bulkInsert.toString());
+
+        List<String> variables = new ArrayList<>();
+        variables.add("startup_mode=" + startupMode);
+        String jobConfigFile = "/mysqlcdc_stop_mode_latest.conf";
+        if ("specific".equals(startupMode)) {
+            BinlogOffset startOffset = getCurrentBinlogOffset();
+            jobConfigFile = "/mysqlcdc_stop_mode_latest_specific.conf";
+            variables.add("specific_offset_file=" + startOffset.getFilename());
+            variables.add("specific_offset_pos=" + startOffset.getPosition());
+        }
+        if ("timestamp".equals(startupMode)) {
+            jobConfigFile = "/mysqlcdc_stop_mode_latest_timestamp.conf";
+            variables.add("timestamp=" + (getCurrentBinlogTimestamp() - 
1000L));
+        }
+        final String configFile = jobConfigFile;
+
+        String jobId = String.valueOf(JobIdGenerator.newJobId());
+        CompletableFuture<Container.ExecResult> jobFuture =
+                CompletableFuture.supplyAsync(
+                        () -> {
+                            try {
+                                return container.executeJob(
+                                        configFile, jobId, 
variables.toArray(new String[0]));
+                            } catch (Exception e) {
+                                throw new RuntimeException(e);
+                            }
+                        });
+
+        await().atMost(60, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () -> Assertions.assertEquals("RUNNING", 
container.getJobStatus(jobId)));
+
+        if ("initial".equals(startupMode)) {
+            // Structural readiness gate for snapshot-taking startups: wait 
until the first
+            // bulk row (id=1) has reached the sink. With 2000 rows across 
snapshot.split.size=20
+            // (100 splits, parallelism=1, ascending-key order), this signal 
fires while ~99
+            // splits are still unread — the UPDATE below is therefore 
guaranteed to land
+            // inside the snapshot window and be picked up by the binlog phase.
+            await().atMost(60, TimeUnit.SECONDS)
+                    .untilAsserted(
+                            () ->
+                                    Assertions.assertEquals(
+                                            "bulk",
+                                            queryVarcharById(1),
+                                            "snapshot phase must have started 
reading"));
+            // Defensive check for the residual race SEZ9 flagged: if the 
LAST-chunk row is
+            // already visible, the whole snapshot completed before the gate 
fired and the
+            // UPDATE below would land after the stop offset. Fail loudly 
instead of letting
+            // the post-UPDATE assertion mislead as data loss. (Row-lock 
stalls are not usable
+            // here: the snapshot reader issues a plain MVCC SELECT, so an 
open FOR UPDATE
+            // transaction on a trailing row does not block it.)
+            Assertions.assertNull(
+                    queryVarcharById(2000),
+                    "snapshot finished too early: last-chunk row already in 
sink; "
+                            + "increase the initial bulk row count");
+        }
+
+        // Issue a change while the job is running.
+        // - initial: the sink-readiness gate above guarantees the snapshot 
phase is still in
+        //   progress, so the update must be picked up by the binlog phase; 
with the stale
+        //   split-creation stop offset the binlog phase would terminate 
immediately past it
+        //   and this row would be silently dropped.
+        // - earliest: there is no snapshot phase (binlog replay starts from 
the earliest
+        //   position, completedSnapshotSplitInfos is empty), so whether the 
update is
+        //   captured depends on the enumerator's stop-offset resolution 
timing versus the
+        //   update — not a deterministic contract, therefore not asserted.
+        // - latest/specific/timestamp: no snapshot window, the update lands 
after the stop
+        //   offset is resolved and is not read — which is expected.
+        executeSql(
+                String.format(
+                        "UPDATE %s.%s SET f_varchar = 'latest-stop' WHERE id = 
21",
+                        MYSQL_DATABASE, SOURCE_TABLE));
+
+        if ("initial".equals(startupMode)) {
+            await().atMost(60, TimeUnit.SECONDS)
+                    .untilAsserted(
+                            () ->
+                                    Assertions.assertEquals(
+                                            "latest-stop",
+                                            queryVarcharById(21),
+                                            "post-snapshot update must be 
synced"));
+        }
+
+        await().atMost(120, TimeUnit.SECONDS)
+                .untilAsserted(
+                        () -> Assertions.assertEquals("FINISHED", 
container.getJobStatus(jobId)));
+        Assertions.assertEquals(0, jobFuture.get(30, 
TimeUnit.SECONDS).getExitCode());
+    }
+
     private long getCurrentBinlogTimestamp() {
         BinlogOffset binlogOffset = getCurrentBinlogOffset();
 
@@ -411,6 +556,23 @@ public class MysqlCDCStopModeSpecificIT extends 
TestSuiteBase implements TestRes
         }
     }
 
+    private String queryVarcharById(int id) {
+        try (Connection connection = getJdbcConnection();
+                Statement statement = connection.createStatement();
+                ResultSet resultSet =
+                        statement.executeQuery(
+                                String.format(
+                                        "select f_varchar from %s.%s where id 
= %d",
+                                        MYSQL_DATABASE, SINK_TABLE, id))) {
+            if (resultSet.next()) {
+                return resultSet.getString(1);
+            }
+            return null;
+        } catch (SQLException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     private void clearTable(String database, String tableName) {
         try (Connection connection = getJdbcConnection()) {
             connection
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest.conf
new file mode 100644
index 0000000000..e3fe04e49d
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest.conf
@@ -0,0 +1,56 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+env {
+  parallelism = 1
+  job.mode = "STREAMING"
+  checkpoint.interval = 5000
+}
+
+source {
+  MySQL-CDC {
+    plugin_output = "customers_mysql_cdc"
+    server-id = 5654
+    username = "st_user_source"
+    password = "mysqlpw"
+    table-names = ["mysql_cdc.mysql_cdc_e2e_source_table"]
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    startup.mode = ${startup_mode}
+    stop.mode = "latest"
+    # For snapshot-taking startups (initial), keep the snapshot phase 
multi-split so the
+    # sink-readiness signal in runLatestStopStartupMode (first bulk row 
visible) can only
+    # fire while the snapshot is still reading the remaining splits — 
guaranteeing the
+    # post-readiness UPDATE lands inside the snapshot window. With the default 
8096-row
+    # split size the 200-row table is read in a single split and the snapshot 
can
+    # complete before the signal is observed.
+    snapshot.split.size = 20
+  }
+}
+
+sink {
+  jdbc {
+    plugin_input = "customers_mysql_cdc"
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    driver = "com.mysql.cj.jdbc.Driver"
+    user = "st_user_sink"
+    password = "mysqlpw"
+    generate_sink_sql = true
+    database = mysql_cdc
+    table = mysql_cdc_e2e_sink_table
+    primary_keys = ["id"]
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest_specific.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest_specific.conf
new file mode 100644
index 0000000000..e7de2cfb5c
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest_specific.conf
@@ -0,0 +1,51 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+env {
+  parallelism = 1
+  job.mode = "STREAMING"
+  checkpoint.interval = 5000
+}
+
+source {
+  MySQL-CDC {
+    plugin_output = "customers_mysql_cdc"
+    server-id = 5654
+    username = "st_user_source"
+    password = "mysqlpw"
+    table-names = ["mysql_cdc.mysql_cdc_e2e_source_table"]
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    startup.mode = "specific"
+    startup.specific-offset.file = ${specific_offset_file}
+    startup.specific-offset.pos = ${specific_offset_pos}
+    stop.mode = "latest"
+  }
+}
+
+sink {
+  jdbc {
+    plugin_input = "customers_mysql_cdc"
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    driver = "com.mysql.cj.jdbc.Driver"
+    user = "st_user_sink"
+    password = "mysqlpw"
+    generate_sink_sql = true
+    database = mysql_cdc
+    table = mysql_cdc_e2e_sink_table
+    primary_keys = ["id"]
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest_timestamp.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest_timestamp.conf
new file mode 100644
index 0000000000..50108a3196
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_stop_mode_latest_timestamp.conf
@@ -0,0 +1,50 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+env {
+  parallelism = 1
+  job.mode = "STREAMING"
+  checkpoint.interval = 5000
+}
+
+source {
+  MySQL-CDC {
+    plugin_output = "customers_mysql_cdc"
+    server-id = 5655
+    username = "st_user_source"
+    password = "mysqlpw"
+    table-names = ["mysql_cdc.mysql_cdc_e2e_source_table"]
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    startup.mode = "timestamp"
+    startup.timestamp = ${timestamp}
+    stop.mode = "latest"
+  }
+}
+
+sink {
+  jdbc {
+    plugin_input = "customers_mysql_cdc"
+    url = "jdbc:mysql://mysql_cdc_e2e:3306/mysql_cdc"
+    driver = "com.mysql.cj.jdbc.Driver"
+    user = "st_user_sink"
+    password = "mysqlpw"
+    generate_sink_sql = true
+    database = mysql_cdc
+    table = mysql_cdc_e2e_sink_table
+    primary_keys = ["id"]
+  }
+}

Reply via email to