<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Apache SeaTunnel</title>
    <description>The latest articles on DEV Community by Apache SeaTunnel (@seatunnel).</description>
    <link>https://dev.to/seatunnel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg</url>
      <title>DEV Community: Apache SeaTunnel</title>
      <link>https://dev.to/seatunnel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/seatunnel"/>
    <language>en</language>
    <item>
      <title>Why I Reworked a BigQuery Sink from Pending Streams to Buffered Streams</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:00:24 +0000</pubDate>
      <link>https://dev.to/seatunnel/why-i-reworked-a-bigquery-sink-from-pending-streams-to-buffered-streams-ja6</link>
      <guid>https://dev.to/seatunnel/why-i-reworked-a-bigquery-sink-from-pending-streams-to-buffered-streams-ja6</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6sm2jh0qfr9fpwbd5v5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6sm2jh0qfr9fpwbd5v5.jpg" width="720" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While working on the BigQuery Sink Connector for Apache SeaTunnel, I initially thought the main challenge would be straightforward: write rows into BigQuery correctly.&lt;/p&gt;

&lt;p&gt;But as the implementation evolved, I realized the real problem was deeper than just calling the BigQuery API. The hard part was aligning BigQuery’s external write visibility model with SeaTunnel’s checkpoint and restore lifecycle.&lt;/p&gt;

&lt;p&gt;This post summarizes the design issue I encountered while using the BigQuery Storage Write API, why the initial pending-stream design was not strong enough for checkpoint recovery, and why I eventually redesigned the batch write path around buffered streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial Design: Pending Streams for Batch Writes
&lt;/h2&gt;

&lt;p&gt;BigQuery Storage Write API provides several write stream types. At first, Pending Stream looked like a natural fit for batch writes.&lt;/p&gt;

&lt;p&gt;The flow is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create PENDING stream
AppendRows
FinalizeWriteStream
BatchCommitWriteStreams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data written to a pending stream is not visible to readers until &lt;code&gt;BatchCommitWriteStreams&lt;/code&gt; is called. Since BigQuery can commit multiple pending streams atomically, this looked like a good match for a checkpoint-based sink commit protocol.&lt;/p&gt;

&lt;p&gt;The initial implementation looked roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Override
public Optional&amp;lt;BigQueryCommitInfo&amp;gt; prepareCommit() {
    flush();
    streamWriter.finalizeStream();
    return Optional.of(new BigQueryCommitInfo(streamWriter.getStreamName()));
}
@Override
public List&amp;lt;Void&amp;gt; snapshotState(long checkpointId) {
    this.streamWriter.close();
    this.streamWriter = BigQueryBatchWriter.of(client, config);
    return Collections.emptyList();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looked similar to a two-phase commit protocol.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepareCommit()  -&amp;gt; finalize stream
commit()         -&amp;gt; BatchCommitWriteStreams
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, during review, a subtle but important problem surfaced.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Ownership Gap After a Failed Checkpoint
&lt;/h2&gt;

&lt;p&gt;In SeaTunnel’s sink lifecycle, &lt;code&gt;prepareCommit()&lt;/code&gt; is called before the checkpoint is fully completed. The issue is that &lt;code&gt;prepareCommit()&lt;/code&gt; already performs an external side effect by calling &lt;code&gt;FinalizeWriteStream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Consider this scenario:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Checkpoint N starts.
2. prepareCommit() finalizes stream A.
3. commitInfo(stream A) is created.
4. The job fails before checkpoint N completes.
5. The job restores from the last 
completed checkpoint N-1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After restore, &lt;code&gt;restoreWriter(states)&lt;/code&gt;receives only the state of the last completed checkpoint. Failed checkpoint state is discarded.&lt;/p&gt;

&lt;p&gt;This means stream A, which was finalized during the failed checkpoint N, is not part of the restored state.&lt;/p&gt;

&lt;p&gt;That is the core issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FinalizeWriteStream has already been called,
but the checkpoint does not contain a durable decision
about whether this finalized stream should be committed,
abandoned, or cleaned up.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first, I thought that since pending-stream data is invisible before &lt;code&gt;BatchCommitWriteStreams&lt;/code&gt;, abandoning the stream might be acceptable. However, that explanation was not strong enough. To claim clear checkpoint recovery semantics, the connector needs a more explicit story about external side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why restoreWriter() Should Not Commit Failed-Checkpoint Streams
&lt;/h2&gt;

&lt;p&gt;One seemingly simple solution is to discover and commit the finalized pending stream during restore.&lt;/p&gt;

&lt;p&gt;But this is unsafe.&lt;/p&gt;

&lt;p&gt;Remember that restore starts from the last completed checkpoint, N-1. Therefore, records that were processed during checkpoint N may be replayed after restore.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;restoreWriter()&lt;/code&gt; discovers stream A and commits it, the following can happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;records in stream A become visible in BigQuery
+
the same records are replayed after restore from checkpoint N-1
+
the replayed records are written again and committed later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This could produce duplicate rows if the connector commits a stream that belongs to a failed checkpoint.&lt;/p&gt;

&lt;p&gt;So the important point is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;restoreWriter() receives only successful checkpoint state.
A stream finalized by a failed checkpoint is not part of that state.
If we try to commit such an external stream during restore,
we may publish records that the engine is about to replay.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Therefore, streams finalized by failed checkpoints should not be blindly committed during restore.&lt;/p&gt;

&lt;p&gt;The issue is not that Pending Streams always cause duplicates; the issue is that the connector has no durable decision for finalized-but-uncommitted streams created by failed checkpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redesign: Switching to Buffered Streams
&lt;/h2&gt;

&lt;p&gt;To avoid this ownership gap, I redesigned the batch write path to use Buffered Streams instead of Pending Streams.&lt;/p&gt;

&lt;p&gt;Buffered Streams do not commit the whole stream through a finalize-and-batch-commit protocol. Instead, they make data visible up to a specific stream offset.&lt;/p&gt;

&lt;p&gt;The flow looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create BUFFERED stream
AppendRows(offset=N)
FlushRows(offset=M)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Data appended to a buffered stream is not immediately visible. It becomes visible only after &lt;code&gt;FlushRows&lt;/code&gt; advances visibility up to a specific offset.&lt;/p&gt;

&lt;p&gt;This model maps much better to checkpoint-based recovery.&lt;/p&gt;

&lt;p&gt;A checkpoint represents the processing position of the engine. A buffered stream offset represents the external write position in BigQuery.&lt;/p&gt;

&lt;p&gt;The new design looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;writer state:
  streamName
  nextOffset
  checkpointId
prepareCommit(checkpointId):
  flush()
  return BigQueryCommitInfo(streamName, flushOffset = nextOffset - 1)
snapshotState(checkpointId):
  return BigQuerySinkState(streamName, nextOffset, checkpointId)
commit(commitInfo):
  FlushRows(streamName, flushOffset)
restoreWriter(states):
  select the latest completed checkpoint state
  restore writer with streamName + nextOffset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the external write position is represented by:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;streamName + nextOffset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This position is stored in checkpoint state. After restore, the writer can resume from the last completed checkpoint’s external write position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding State and CommitInfo
&lt;/h2&gt;

&lt;p&gt;One important lesson was that writer state and commit info are related, but they mean different things.&lt;/p&gt;

&lt;p&gt;Write on Medium&lt;br&gt;
The writer state represents where the writer should resume appending after restore.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BigQuerySinkState {
    String streamName;
    long nextOffset;
    long checkpointId;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The commit info represents what should become visible in BigQuery after the checkpoint completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BigQueryCommitInfo {
    String streamName;
    long flushOffset;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;writer state:
  streamName = S
  nextOffset = 100
commit info:
  streamName = S
  flushOffset = 99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The writer has appended rows up to offset 99.
If the checkpoint completes, BigQuery should flush visibility up to offset 99.
If the job restores from this checkpoint, the writer should resume from offset 100.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation made the recovery model much clearer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing Offsets Correctly
&lt;/h2&gt;

&lt;p&gt;The most important detail in the buffered-stream design is offset management.&lt;/p&gt;

&lt;p&gt;The offset is not a record identifier. It is not a primary key. It is also not a checkpoint id.&lt;/p&gt;

&lt;p&gt;The offset is the append position inside a specific BigQuery write stream.&lt;/p&gt;

&lt;p&gt;If there are multiple parallel writers, each writer should own its own stream, and each stream should manage its offset independently.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;writer-0:
  stream S0
  nextOffset = 100
writer-1:
  stream S1
  nextOffset = 250
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if the checkpoint id is 10, the BigQuery append offset should not be 10. The checkpoint id is only metadata. The BigQuery offset must represent the append position inside that stream.&lt;/p&gt;

&lt;p&gt;So the model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checkpointId:
  used to identify and select checkpoint state
nextOffset:
  used as the next append position in the BigQuery stream
flushOffset:
  nextOffset - 1, used by FlushRows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is important, especially when the sink runs with parallelism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Committer Uses FlushRows Instead of BatchCommitWriteStreams
&lt;/h2&gt;

&lt;p&gt;With Pending Streams, the committer used &lt;code&gt;BatchCommitWriteStreams&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With Buffered Streams, the committer uses &lt;code&gt;FlushRows&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FlushRowsRequest request =
        FlushRowsRequest.newBuilder()
                .setWriteStream(info.getStreamName())
                .setOffset(Int64Value.of(info.getFlushOffset()))
                .build();
FlushRowsResponse response = client.flushRows(request);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes rows visible up to the requested offset.&lt;/p&gt;

&lt;p&gt;So after a checkpoint completes, the committer advances BigQuery visibility up to the checkpoint’s &lt;code&gt;flushOffset&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping CDC Separate
&lt;/h2&gt;

&lt;p&gt;This redesign only applies to the batch write path.&lt;/p&gt;

&lt;p&gt;I did not change the CDC path to use Buffered Streams. BigQuery CDC ingestion has its own semantics around &lt;code&gt;_CHANGE_TYPE&lt;/code&gt;, &lt;code&gt;_CHANGE_SEQUENCE_NUMBER&lt;/code&gt;, and primary keys. It should not be forced into the same checkpoint-offset model as batch writes.&lt;/p&gt;

&lt;p&gt;The resulting structure is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;batch mode:
  Buffered Stream
  streamName + nextOffset in checkpoint state
  FlushRows on commit
cdc / streaming mode:
  existing streaming path
  BigQuery CDC semantics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the batch recovery model explicit without changing CDC behavior unnecessarily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Challenges
&lt;/h2&gt;

&lt;p&gt;This change depends on real BigQuery Storage Write API behavior, especially Buffered Streams, explicit offsets, and &lt;code&gt;FlushRows&lt;/code&gt;. A local emulator was not sufficient to validate these semantics reliably.&lt;/p&gt;

&lt;p&gt;I tested the updated batch path against a real BigQuery environment.&lt;/p&gt;

&lt;p&gt;The verified scenarios included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- creating a Buffered Stream
- appending rows with explicit offsets
- running a batch write with checkpoint enabled
- verifying that rows become visible in BigQuery after FlushRows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fully deterministic failure-recovery E2E test is much harder. It would require controlling checkpoint barrier timing, injecting failure at a precise point, restoring the job, and verifying BigQuery visibility and duplicates.&lt;/p&gt;

&lt;p&gt;For automated tests, a more practical approach is to cover the recoverable metadata path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- BigQuerySinkState serialization
- selecting the latest state by checkpointId
- advancing nextOffset only after append success
- creating FlushRows commit info
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not replace a full failure-recovery E2E test, but it validates the most important internal recovery metadata.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson I learned is that connector exactly-once semantics are not just about calling a commit API.&lt;/p&gt;

&lt;p&gt;A connector must answer questions like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. When does data become visible in the external system?
2. What happens to external side effects if a checkpoint fails?
3. What external write position should the writer restore from?
4. What does writer state mean?
5. What does commitInfo mean?
6. How should offsets behave when append fails and retries happen?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initial Pending Stream design looked natural from BigQuery’s batch commit perspective. But when combined with SeaTunnel’s checkpoint and restore lifecycle, it created an ownership gap for finalized streams after failed checkpoints.&lt;/p&gt;

&lt;p&gt;The Buffered Stream design is more complex, but it gives the connector a clearer recovery model by storing the external write position as &lt;code&gt;streamName + offset&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;This work was not just about adding a BigQuery Sink Connector.&lt;/p&gt;

&lt;p&gt;It was about aligning an external system’s write visibility model with a stream processing engine’s checkpoint lifecycle.&lt;/p&gt;

&lt;p&gt;The first design was not perfect. Review revealed an important failure scenario. But by understanding the issue, revisiting BigQuery stream types, and redesigning the batch path around Buffered Streams, the connector’s recovery semantics became much clearer.&lt;/p&gt;

&lt;p&gt;Open source review can be painful, but it often forces us to think beyond whether the code works in the happy path. It pushes us to reason about boundaries, failure windows, and system guarantees.&lt;/p&gt;

&lt;p&gt;For me, this BigQuery Sink work was exactly that kind of experience.&lt;/p&gt;

</description>
      <category>bigquery</category>
      <category>apacheseatunnel</category>
      <category>datascience</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How Apache SeaTunnel Handles Kafka Topic Partition Expansion Without a Job Restart</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Fri, 18 Sep 2026 09:16:22 +0000</pubDate>
      <link>https://dev.to/seatunnel/how-apache-seatunnel-handles-kafka-topic-partition-expansion-without-a-job-restart-44a0</link>
      <guid>https://dev.to/seatunnel/how-apache-seatunnel-handles-kafka-topic-partition-expansion-without-a-job-restart-44a0</guid>
      <description>&lt;h2&gt;
  
  
  1. Scenario
&lt;/h2&gt;

&lt;p&gt;When using an Apache SeaTunnel streaming job to consume data from a Kafka topic, what happens if the topic is expanded from 2 partitions to 4?&lt;/p&gt;

&lt;p&gt;By default, the new partitions are not consumed, and none of the data written to them will be processed unless the SeaTunnel job is restarted.&lt;/p&gt;

&lt;p&gt;The reason is that SeaTunnel’s Kafka Source scans the available partitions when the job starts. Without additional configuration, it does not automatically detect partitions added later.&lt;/p&gt;

&lt;p&gt;Restarting the job can force SeaTunnel to scan the topic again, but this approach introduces several issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The entire job is paused during the restart. If checkpoints are used for recovery, there is also a risk of missing data depending on the recovery and offset state.&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;start_mode = EARLIEST&lt;/code&gt;, restarting may cause the newly assigned partitions to be consumed from the beginning, which can replay historical data and result in duplicate records.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SeaTunnel provides the &lt;code&gt;partition-discovery.interval-millis&lt;/code&gt; parameter specifically for this scenario. It periodically scans Kafka for newly added partitions and automatically adds them to the consumer, so the streaming job does not need to be restarted.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hands-On Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A Kafka cluster&lt;/li&gt;
&lt;li&gt;Apache SeaTunnel 2.3.12&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.2 Create a Topic with Two Partitions
&lt;/h3&gt;

&lt;p&gt;Create a Kafka topic named &lt;code&gt;ksource&lt;/code&gt; with two partitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/kafka-topics.sh &lt;span class="nt"&gt;--create&lt;/span&gt; &lt;span class="nt"&gt;--topic&lt;/span&gt; ksource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; ip:9092 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partitions&lt;/span&gt; 2 &lt;span class="nt"&gt;--replication-factor&lt;/span&gt; 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2.3 Configure the SeaTunnel Streaming Job
&lt;/h3&gt;

&lt;p&gt;Create a SeaTunnel configuration file at &lt;code&gt;job/k.conf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are three key settings to pay attention to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;job.mode&lt;/code&gt; to &lt;code&gt;STREAMING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enable checkpointing explicitly with &lt;code&gt;checkpoint.interval&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;partition-discovery.interval-millis&lt;/code&gt; to a positive value.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;parallelism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;job.mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STREAMING"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;checkpoint.interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;Kafka&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;bootstrap.servers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ip:9092"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;topic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ksource"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;consumer.group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"seatunnel_k_group"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;partition-discovery.interval-millis&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;start_mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EARLIEST"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"json"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;fields&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;INT&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;cusname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;STRING&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;DOUBLE&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="nl"&gt;sink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;Console&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key settings are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;partition-discovery.interval-millis = 5000&lt;/code&gt;: SeaTunnel scans Kafka for new partitions every 5 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start_mode = EARLIEST&lt;/code&gt;: When a new partition is discovered, SeaTunnel starts consuming from the earliest available offset.&lt;/li&gt;
&lt;li&gt;Schema field types such as &lt;code&gt;INT&lt;/code&gt;, &lt;code&gt;STRING&lt;/code&gt;, and &lt;code&gt;DOUBLE&lt;/code&gt; are basic types and can be specified directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.4 Start the Job and Verify Normal Consumption
&lt;/h3&gt;

&lt;p&gt;Start the SeaTunnel job in local mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/seatunnel.sh &lt;span class="nt"&gt;--config&lt;/span&gt; job/k.conf &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nb"&gt;local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open another terminal and produce three messages to &lt;code&gt;ksource&lt;/code&gt;. Because the topic currently has only two partitions and no key is specified, the messages will be distributed across partitions 0 and 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/kafka-console-producer.sh &lt;span class="nt"&gt;--broker-list&lt;/span&gt; ip:9092 &lt;span class="nt"&gt;--topic&lt;/span&gt; ksource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the following messages, one per line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Zhang San"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;100.50&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Li Si"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;200.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Wang Wu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;300.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding records should appear in the SeaTunnel job console, confirming that the source is consuming data normally.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.5 Expand the Topic from 2 Partitions to 4
&lt;/h3&gt;

&lt;p&gt;Now increase the number of partitions from 2 to 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/kafka-topics.sh &lt;span class="nt"&gt;--alter&lt;/span&gt; &lt;span class="nt"&gt;--topic&lt;/span&gt; ksource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; ip:9092 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partitions&lt;/span&gt; 4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/kafka-topics.sh &lt;span class="nt"&gt;--describe&lt;/span&gt; &lt;span class="nt"&gt;--topic&lt;/span&gt; ksource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap-server&lt;/span&gt; ip:9092
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The topic should now contain four partitions: 0, 1, 2, and 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.6 Produce More Data and Check the New Partitions
&lt;/h3&gt;

&lt;p&gt;Continue producing six messages to &lt;code&gt;ksource&lt;/code&gt;. Because no key is specified, Kafka's default partitioning behavior distributes the records across all four partitions. Some of them should therefore be written to the newly added partitions 2 and 3.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/kafka-console-producer.sh &lt;span class="nt"&gt;--broker-list&lt;/span&gt; ip:9092 &lt;span class="nt"&gt;--topic&lt;/span&gt; ksource
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Zhao Liu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;400.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Sun Qi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;500.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Zhou Ba"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;600.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Wu Jiu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;700.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Zheng Shi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;800.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"cusname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Qian Shiyi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;900.00&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The six records should gradually appear in the SeaTunnel console, including those written to the newly added partitions 2 and 3.&lt;/p&gt;

&lt;p&gt;The job continues running without a restart, and data from the new partitions is consumed automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.7 Comparison: What Happens Without Dynamic Partition Discovery?
&lt;/h3&gt;

&lt;p&gt;Now remove &lt;code&gt;partition-discovery.interval-millis&lt;/code&gt; from the configuration and repeat Steps 2.4 through 2.6.&lt;/p&gt;

&lt;p&gt;The results are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Messages produced before the topic expansion: consumed normally.&lt;/li&gt;
&lt;li&gt;Messages produced after the expansion and written to new partitions 2 and 3: not consumed.&lt;/li&gt;
&lt;li&gt;Restarting the SeaTunnel job: the new partitions are detected and their data can then be consumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference between the two configurations is summarized below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5mlh2uvz5mnw7xhuu6v2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5mlh2uvz5mnw7xhuu6v2.jpg" width="798" height="267"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Important Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 The Starting Offset of New Partitions Depends on &lt;code&gt;start_mode&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;New partitions do not have existing checkpoint offsets. The starting position therefore depends on the configured &lt;code&gt;start_mode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;EARLIEST&lt;/code&gt;, SeaTunnel consumes the new partition from its earliest available offset. If the partition already contains historical data, that data will be consumed as well.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;LATEST&lt;/code&gt;, SeaTunnel only consumes new records written after the partition is discovered.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2 Avoid Setting the Discovery Interval Too Low
&lt;/h3&gt;

&lt;p&gt;Each discovery scan sends metadata requests to the Kafka cluster. A shorter interval allows SeaTunnel to detect partition changes more quickly, but it also increases the request frequency and therefore the load on Kafka.&lt;/p&gt;

&lt;p&gt;A 5-second interval is a reasonable starting point. If partition changes are infrequent, you can consider increasing the interval to 30–60 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Kafka Partitions Can Only Be Increased, Not Decreased
&lt;/h3&gt;

&lt;p&gt;Kafka supports increasing the number of partitions for a topic, but does not support reducing the partition count. Reducing partitions could result in data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 Partition Expansion Triggers Consumer Group Rebalancing
&lt;/h3&gt;

&lt;p&gt;When the number of partitions changes, Kafka rebalances the consumer group and redistributes partitions among consumers.&lt;/p&gt;

&lt;p&gt;A brief fluctuation in consumption during the rebalance is expected behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.5 Existing Partitions Are Not Affected
&lt;/h3&gt;

&lt;p&gt;Dynamic partition discovery only handles newly added partitions.&lt;/p&gt;

&lt;p&gt;Existing partitions continue consuming from their checkpointed offsets, so enabling partition discovery does not cause SeaTunnel to reprocess data from existing partitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.6 Types with Precision or Generic Parameters Must Be Quoted
&lt;/h3&gt;

&lt;p&gt;Types containing commas or angle brackets must be enclosed in double quotes, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="s2"&gt;"decimal(10,2)"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"array&amp;lt;int&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"map&amp;lt;string, int&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Otherwise, the HOCON parser will report a syntax error.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Summary
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;partition-discovery.interval-millis&lt;/code&gt; parameter solves a specific problem: automatically detecting newly added Kafka partitions while a SeaTunnel streaming job is already running.&lt;/p&gt;

&lt;p&gt;Set the parameter to a positive interval, and SeaTunnel periodically checks Kafka for new partitions and automatically starts consuming from them without requiring a job restart.&lt;/p&gt;

&lt;p&gt;There are three key points to remember:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming mode + checkpointing + a positive partition discovery interval.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The starting offset for a newly discovered partition depends on &lt;code&gt;start_mode&lt;/code&gt;, while the discovery interval should be chosen based on the balance between detection latency and Kafka cluster load.&lt;/p&gt;

&lt;p&gt;With this configuration in place, expanding a Kafka topic no longer requires manually restarting the SeaTunnel job just to make the new partitions visible to the consumer.&lt;/p&gt;

</description>
      <category>apacheseatunnel</category>
      <category>kafka</category>
      <category>beginners</category>
      <category>datascience</category>
    </item>
    <item>
      <title>🚀 Move Redis data without writing migration scripts! Explore how Apache SeaTunnel handles String, Hash, Set, and ZSet with practical Redis-to-Redis configs. #ApacheSeaTunnel #Redis #DataMigration #DataEngineering</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Fri, 11 Sep 2026 07:28:22 +0000</pubDate>
      <link>https://dev.to/seatunnel/move-redis-data-without-writing-migration-scripts-explore-how-apache-seatunnel-handles-string-15kf</link>
      <guid>https://dev.to/seatunnel/move-redis-data-without-writing-migration-scripts-explore-how-apache-seatunnel-handles-string-15kf</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/seatunnel/redis-data-migration-without-scripts-how-apache-seatunnel-handles-string-hash-set-and-zset-3pio" class="crayons-story__hidden-navigation-link"&gt;Redis Data Migration Without Scripts: How Apache SeaTunnel Handles String, Hash, Set, and ZSet&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/seatunnel" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" alt="seatunnel profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/seatunnel" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Apache SeaTunnel
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Apache SeaTunnel
                
                
              
              &lt;div id="story-author-preview-content-4629457" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/seatunnel" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Apache SeaTunnel&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/seatunnel/redis-data-migration-without-scripts-how-apache-seatunnel-handles-string-hash-set-and-zset-3pio" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/seatunnel/redis-data-migration-without-scripts-how-apache-seatunnel-handles-string-hash-set-and-zset-3pio" id="article-link-4629457"&gt;
          Redis Data Migration Without Scripts: How Apache SeaTunnel Handles String, Hash, Set, and ZSet
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/apacheseatunnel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;apacheseatunnel&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/redis"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;redis&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/seatunnel/redis-data-migration-without-scripts-how-apache-seatunnel-handles-string-hash-set-and-zset-3pio#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Apache SeaTunnel had a busy August: 195 PRs merged, 13 new connectors added, and major upgrades to Zeta observability, CDC, Checkpoint recovery, and CI. #ApacheSeaTunnel #DataEngineering #OpenSource</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Fri, 11 Sep 2026 07:27:38 +0000</pubDate>
      <link>https://dev.to/seatunnel/apache-seatunnel-had-a-busy-august-195-prs-merged-13-new-connectors-added-and-major-upgrades-to-1ep1</link>
      <guid>https://dev.to/seatunnel/apache-seatunnel-had-a-busy-august-195-prs-merged-13-new-connectors-added-and-major-upgrades-to-1ep1</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/seatunnel/195-merged-prs-13-new-connectors-whats-new-in-apache-seatunnel-this-august-17of" class="crayons-story__hidden-navigation-link"&gt;195 Merged PRs, 13 New Connectors: What’s New in Apache SeaTunnel This August?&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/seatunnel" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" alt="seatunnel profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/seatunnel" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Apache SeaTunnel
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Apache SeaTunnel
                
                
              
              &lt;div id="story-author-preview-content-4629676" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/seatunnel" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Apache SeaTunnel&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/seatunnel/195-merged-prs-13-new-connectors-whats-new-in-apache-seatunnel-this-august-17of" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/seatunnel/195-merged-prs-13-new-connectors-whats-new-in-apache-seatunnel-this-august-17of" id="article-link-4629676"&gt;
          195 Merged PRs, 13 New Connectors: What’s New in Apache SeaTunnel This August?
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/apacheseatunnel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;apacheseatunnel&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/bigdata"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;bigdata&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/seatunnel/195-merged-prs-13-new-connectors-whats-new-in-apache-seatunnel-this-august-17of#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            10 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>195 Merged PRs, 13 New Connectors: What’s New in Apache SeaTunnel This August?</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Fri, 11 Sep 2026 07:19:40 +0000</pubDate>
      <link>https://dev.to/seatunnel/195-merged-prs-13-new-connectors-whats-new-in-apache-seatunnel-this-august-17of</link>
      <guid>https://dev.to/seatunnel/195-merged-prs-13-new-connectors-whats-new-in-apache-seatunnel-this-august-17of</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtvgwpavvw1chn4yywby.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhtvgwpavvw1chn4yywby.jpg" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Apache SeaTunnel continued its rapid pace of development in August! The community merged a total of 195 PRs, with contributions from 60 contributors and 13 new connectors added to further expand the data source ecosystem. Zeta observability also received a major upgrade, with continued improvements to dynamic logging, Worker resource monitoring, Job status tracking, and more. Meanwhile, several capabilities, including Checkpoint recovery, CDC Schema Change, CI Merge Queue, and declarative OptionRule validation, were optimized to deliver greater stability, observability, and operational efficiency for data integration workloads.&lt;/p&gt;

&lt;p&gt;Let’s take a closer look at the updates that stood out in Apache SeaTunnel this August!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Merged PRs This Month
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1.1 Overall Statistics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Successful Merge Commits&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;195&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Includes automatic merges by Merge Queue and manual merges, consistent with independent verification via &lt;code&gt;git log --oneline&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Estimated Merged PRs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;180+&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Some PRs contain multiple commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Average Merges per Day&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;28 merge days in the month; 195 / 28 = 6.96 ≈ 7.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Daily Peak Merges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Monthly peak reached on August 23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Net Code Changes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+114,181&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;+132,340 additions / -18,159 deletions; reporting convention retained. Independent recalculation: 134,165 / 18,143 / net +116,022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unique Files Changed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1,687&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  1.2 PR Classification
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffx7qywqqfhe7poncnols.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffx7qywqqfhe7poncnols.jpg" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What the numbers tell us:&lt;/strong&gt; Bug fixes accounted for the largest share of PRs at 35.7%, reflecting the community’s continued focus on stability. New feature development contributed the largest volume of code changes, with &lt;strong&gt;+76,352 net lines added&lt;/strong&gt;, accounting for approximately 66.9% of the total. Much of this came from 13 new connectors and enhancements to core APIs. The 37 documentation PRs (18.9%) also marked a new monthly high for the year.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  2. Top 25 Contributors
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4svsxgp0lcqggjco8kx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4svsxgp0lcqggjco8kx.jpg" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Code Change Analysis
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Overall Code Volume
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total lines added&lt;/td&gt;
&lt;td&gt;+132,340&lt;/td&gt;
&lt;td&gt;Independently recalculated with `git log --numstat \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total lines deleted&lt;/td&gt;
&lt;td&gt;-18,159&lt;/td&gt;
&lt;td&gt;Same as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Net lines added&lt;/td&gt;
&lt;td&gt;+114,181&lt;/td&gt;
&lt;td&gt;Feature: +76,352; other categories: +37,829&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File change references&lt;/td&gt;
&lt;td&gt;2,187&lt;/td&gt;
&lt;td&gt;Includes duplicates; files modified by multiple commits are counted multiple times&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique files changed&lt;/td&gt;
&lt;td&gt;1,687&lt;/td&gt;
&lt;td&gt;Independently verified with {% raw %}`sort -u \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average changes per commit&lt;/td&gt;
&lt;td&gt;+675/-93 lines, 11.2 files&lt;/td&gt;
&lt;td&gt;The average was pushed up by large PRs such as #11413 HugeGraph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3.2 Distribution by Technical Area
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Based on actual net lines added by category&lt;/em&gt;&lt;br&gt;
{% raw %}&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────┐
│ New Features [Feature]   ████████████████████████████████ 66.9% │ +76,352 net
│ Bug Fixes [Fix]/[Bug]    ████████████████                  14.8% │ +16,859 net
│ Improvements [Improve]   ████████                           6.6% │  +7,478 net
│ Documentation [Doc]/[Docs] ██████████                       9.5% │ +10,846 net
│ Tests/CI                  ██                                0.4% │    +472 net
│ Other                     ██                                1.9% │  +2,174 net
└──────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.3 Major Refactoring and Architectural Changes
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Files Affected&lt;/th&gt;
&lt;th&gt;Change Volume&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Risk Level&lt;/th&gt;
&lt;th&gt;Verified Contributors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;seatunnel-shade module refactoring (#9993)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;116&lt;/td&gt;
&lt;td&gt;+1,042/-5,890&lt;/td&gt;
&lt;td&gt;Third-party dependency shading and uber JAR build process across all Shade packaging&lt;/td&gt;
&lt;td&gt;⚠️ &lt;strong&gt;Medium-High&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;hawk9821&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Declarative OptionRule migration across 10+ connectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~150 (estimated)&lt;/td&gt;
&lt;td&gt;~+3,500/-1,500 (estimated)&lt;/td&gt;
&lt;td&gt;Configuration validation for Pulsar/MongoDB/HDFS/S3/Iceberg/HBase/Cassandra/ActiveMQ connectors&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Low&lt;/strong&gt; (backward-compatible and non-breaking; validation is stricter, so previously accepted boundary configurations may now fail at startup instead of failing silently)&lt;/td&gt;
&lt;td&gt;Multi-contributor effort (itzrohan007/amanbbdniit.mishra/Linz1248/nikk4645/zhang-arvin/claire040217, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zeta REST API v2 expansion (#11984/#11909/#11982)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;57 (estimated across 3 PRs)&lt;/td&gt;
&lt;td&gt;+3,102/-62 (estimated)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/v2/jobs&lt;/code&gt;, &lt;code&gt;/api/v2/nodes&lt;/code&gt;, &lt;code&gt;/api/v2/logs&lt;/code&gt;, &lt;code&gt;/api/v2/nodes/{id}/resources&lt;/code&gt;, and other endpoints&lt;/td&gt;
&lt;td&gt;⚠️ &lt;strong&gt;Medium&lt;/strong&gt; (response bodies add new fields such as &lt;code&gt;stateTransitions&lt;/code&gt;; clients using strict JSON Schema may need &lt;code&gt;ignoreUnknown=true&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;SEZ9 + goutamadwant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jackson Relocate added and later reverted (#11673 → #11851)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3 (related changes)&lt;/td&gt;
&lt;td&gt;Corresponding additions/removals&lt;/td&gt;
&lt;td&gt;Jackson version conflicts between &lt;code&gt;hadoop-aws&lt;/code&gt; and the uber JAR (#11673 original commit: dev.loustler; #11851 revert: hawk9821)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Stable after revert&lt;/strong&gt;; current state is consistent with the reverted version&lt;/td&gt;
&lt;td&gt;#11673 + #11851 (#11522 shenghang’s openGauss anti-hijacking fix was related)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Markdown RAG metadata alignment (#10990/#11740)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;41 (2 commits combined)&lt;/td&gt;
&lt;td&gt;+2,286/-72&lt;/td&gt;
&lt;td&gt;File Source + Knowledge Sync end-to-end field contract&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Low&lt;/strong&gt; (new fields are optional)&lt;/td&gt;
&lt;td&gt;yzeng1618&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Top 7 Updates Users Will Notice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✨ TOP 1: 13 New Connectors Expand the Data Source Ecosystem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Estimated total change volume:&lt;/strong&gt; approximately +40,000 lines / 500+ files&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connector&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Contributor&lt;/th&gt;
&lt;th&gt;Primary Use Case&lt;/th&gt;
&lt;th&gt;Estimated Maturity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BosFile (Baidu Intelligent Cloud BOS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source + Sink&lt;/td&gt;
&lt;td&gt;programmerloverun&lt;/td&gt;
&lt;td&gt;Multi-cloud object storage coverage, aligned with S3/Azure/OSS&lt;/td&gt;
&lt;td&gt;✅ Full E2E-level support, consistent with other File connectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure Cosmos DB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;ilovezeri333 (first-time contributor)&lt;/td&gt;
&lt;td&gt;Azure NoSQL migration into data lakes&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NebulaGraph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vertex Sink&lt;/td&gt;
&lt;td&gt;goutamadwant&lt;/td&gt;
&lt;td&gt;Writing vertices to graph databases; Edge support is planned&lt;/td&gt;
&lt;td&gt;⚠️ Beta (Vertex only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SNMP (v2c/v3)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;goutamadwant&lt;/td&gt;
&lt;td&gt;Collecting network device monitoring data&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostHog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;goutamadwant&lt;/td&gt;
&lt;td&gt;Extracting user behavior data from product analytics platforms&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Pub/Sub (Source + Sink)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source + Sink&lt;/td&gt;
&lt;td&gt;goutamadwant&lt;/td&gt;
&lt;td&gt;GCP messaging events into data lakes and write-back workflows&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure Queue Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sink&lt;/td&gt;
&lt;td&gt;goutamadwant&lt;/td&gt;
&lt;td&gt;Cloud-native asynchronous message delivery to Azure&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Couchbase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sink&lt;/td&gt;
&lt;td&gt;srijan-singh&lt;/td&gt;
&lt;td&gt;Distributed document database integration&lt;/td&gt;
&lt;td&gt;⚠️ Beta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB2 CDC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source (CDC)&lt;/td&gt;
&lt;td&gt;davidzollo365&lt;/td&gt;
&lt;td&gt;Incremental synchronization from IBM DB2&lt;/td&gt;
&lt;td&gt;✅ Incremental support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PythonSource&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source&lt;/td&gt;
&lt;td&gt;escheduler&lt;/td&gt;
&lt;td&gt;User-defined Python-script-based data sources&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NATS JetStream&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sink&lt;/td&gt;
&lt;td&gt;rucciva (first-time contributor)&lt;/td&gt;
&lt;td&gt;Cloud-native messaging and cloud-edge collaboration scenarios&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HugeGraph (Source + Sink refactoring)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Source + Sink&lt;/td&gt;
&lt;td&gt;liu7777jx (first-time contributor)&lt;/td&gt;
&lt;td&gt;Multi-mapping support for the Baidu open-source graph database&lt;/td&gt;
&lt;td&gt;✅ Largest single PR this month at 15,358 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;BigQuery enhancements (Multi-Table + UniverseDomain)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sink enhancement&lt;/td&gt;
&lt;td&gt;merlin.launay (first-time contributor)&lt;/td&gt;
&lt;td&gt;Writing multiple source tables to a GCP data warehouse&lt;/td&gt;
&lt;td&gt;✅ See TOP 3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What this means for users:&lt;/strong&gt; Cloud storage coverage now spans &lt;strong&gt;five major storage ecosystems&lt;/strong&gt;: S3, Azure, OSS, BOS, and HDFS. For example, in an overseas advertising scenario, the Cosmos DB → BosFile combination can directly support compliant cross-cloud synchronization from Azure public cloud environments to domestic BOS storage. PythonSource also expands the flexibility available for custom data source scenarios.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✨ TOP 2: Major Observability Upgrades for Zeta
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;REST v2 + Dynamic Log Levels + Worker Resource APIs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New and enhanced endpoints:&lt;/strong&gt; approximately 10+ REST endpoints, all covered by independent E2E tests.&lt;/p&gt;

&lt;p&gt;Key improvements include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dynamic log levels:&lt;/strong&gt; &lt;code&gt;PUT /api/v2/logs/level&lt;/code&gt; accepts a JSON body such as &lt;code&gt;logger=xxx, level=DEBUG&lt;/code&gt;, with the new level taking effect within 30 seconds. Production troubleshooting no longer requires a cluster restart. &lt;strong&gt;This behavior is deterministic based on the implementation.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fine-grained Worker resources:&lt;/strong&gt; &lt;code&gt;GET /api/v2/nodes/{id}/resources&lt;/code&gt; provides real-time monitoring of CPU, heap, and thread-pool utilization.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Job state transition tracking:&lt;/strong&gt; The response now includes a &lt;code&gt;stateTransitions&lt;/code&gt; array, making it possible to trace the source and trigger time of each failover or manual restart.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Metrics isolation fix (#11492):&lt;/strong&gt; In multi-Worker deployments, metrics were previously written to a global Registry, which could cause Prometheus/Grafana dashboards to display values under the wrong Worker. This has been fixed, with expected dashboard accuracy improving from approximately 70% to &lt;strong&gt;95%+&lt;/strong&gt; (estimate).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Schema-first multi-table sink metrics alignment (#12001):&lt;/strong&gt; Metrics from individual tables in multi-table synchronization no longer interfere with one another, which is expected to significantly improve the accuracy of per-tenant table-level metrics.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Operational impact:&lt;/strong&gt; Combining online DEBUG-level adjustment with state-transition tracing is estimated to reduce the average time required to diagnose typical production issues by approximately &lt;strong&gt;30%–60%&lt;/strong&gt;. This is a conservative estimate based on the workflow change from “restart first, then inspect logs” to “inspect signals online and adjust logging levels,” rather than a benchmark result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✨ TOP 3: BigQuery Sink Adds Multi-Table and UniverseDomain Support
&lt;/h3&gt;

&lt;p&gt;Key improvements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A single SeaTunnel job can now map &lt;strong&gt;N source tables to N corresponding BigQuery tables&lt;/strong&gt;, eliminating the need to split the workload into N independent SeaTunnel jobs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;bigquery-universe-domain&lt;/code&gt; configuration supports different Google API domains, including the standard Google domain (&lt;code&gt;googleapis.com&lt;/code&gt;), EU domains, and private/custom domains.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Combined with Flink/Spark CDC upstream pipelines, this can provide a practical configuration for a &lt;strong&gt;multi-database, multi-table MySQL → BigQuery&lt;/strong&gt; pipeline with end-to-end latency measured in minutes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Value for users:&lt;/strong&gt; For typical overseas retail and advertising workloads, moving from N jobs to a single multi-table job can reduce resource consumption by an estimated &lt;strong&gt;30%–40%&lt;/strong&gt; under comparable throughput. This has not been validated against a standardized benchmark and is provided for reference only. Operational complexity is reduced accordingly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✨ TOP 4: LATEST_COMPLETED Checkpoint Recovery + Prometheus Checkpoint Flush Alignment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;LATEST_COMPLETED recovery strategy (#11421):&lt;/strong&gt; A new &lt;code&gt;restore.strategy = LATEST_COMPLETED&lt;/code&gt; option avoids the risk of recovering from a Checkpoint that was still in progress when a failure occurred, reducing the possibility of reading incomplete state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prometheus Checkpoint Flush (#11827):&lt;/strong&gt; This addresses occasional loss of data points from the final few seconds when Spark/Flink timer-driven flushes are not aligned with the Checkpoint Barrier. An enhancement in #11778 further moved the &lt;code&gt;PrometheusWriter&lt;/code&gt; to an engine-level &lt;code&gt;FlushSignal&lt;/code&gt;, binding the flush timing to the Checkpoint barrier.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt; For high-consistency workloads such as financial payments and order reconciliation, the risk of data loss during Checkpoint recovery moves from an &lt;strong&gt;occasional issue reported by the community&lt;/strong&gt; toward &lt;strong&gt;theoretical zero data loss&lt;/strong&gt;, significantly increasing confidence in recovery consistency. This is a qualitative statement rather than a benchmark result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✨ TOP 5: CDC COMMENT Schema Change Support + Stronger Recovery Stability
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Enhancement&lt;/th&gt;
&lt;th&gt;Problem Solved&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;COMMENT Schema Change event support&lt;/td&gt;
&lt;td&gt;Changes such as &lt;code&gt;ALTER TABLE ... MODIFY COLUMN ... COMMENT 'xxx'&lt;/code&gt; in MySQL/PostgreSQL/Oracle can now be correctly propagated through the CDC pipeline to downstream Iceberg/Paimon/Hive metadata, addressing delays in synchronizing column comments with downstream BI data dictionaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event routing fix #11837&lt;/td&gt;
&lt;td&gt;COMMENT events were previously routed incorrectly into the DML pipeline, causing parsing errors; this has been fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL CDC long-running E2E&lt;/td&gt;
&lt;td&gt;Added a 12-hour fault injection → recovery → data consistency regression test case (E2E #11946, with 192 new lines)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB snapshot failure stack preservation&lt;/td&gt;
&lt;td&gt;Previously, the exception stack could be lost; it can now be used directly to identify the root cause without rerunning the job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server CDC Resume LSN&lt;/td&gt;
&lt;td&gt;Fixed a 0–3 record offset issue in the resume position after restart (#11410)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle startup by SCN #11171&lt;/td&gt;
&lt;td&gt;Added an option to specify the starting SCN for incremental recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Enterprise CDC impact:&lt;/strong&gt; Schema evolution now covers the “last mile” for column comments, allowing metadata changes to propagate correctly through the CDC pipeline. For long-running CDC workloads, the estimated recovery success rate after 12-hour fault-injection tests improved from approximately &lt;strong&gt;85%+ to over 95%&lt;/strong&gt;. This is a conservative estimate based on CI pass-rate improvements.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✨ TOP 6: CI Merge Queue + E2E Shard Rebalancing + Hung Job Protection
&lt;/h3&gt;

&lt;p&gt;Key mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Merge Queue:&lt;/strong&gt; Changes are first placed into a temporary queue, where the full CI suite, including high-risk E2E tests, is executed. Once all checks pass, changes are merged linearly into &lt;code&gt;dev&lt;/code&gt;. This avoids the historical problem of the &lt;code&gt;dev&lt;/code&gt; branch remaining red for days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;E2E shard rebalancing:&lt;/strong&gt; The test suite was rebalanced from &lt;strong&gt;7 shards to 12&lt;/strong&gt;, reducing the longest shard from more than 90 minutes to an estimated &lt;strong&gt;25–40 minutes&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic timeout for hung jobs (#11718):&lt;/strong&gt; Added protection for hung CI jobs, including the 66-minute boundary observed with Paimon S3 hung jobs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt; Conservatively estimated, the time contributors spend waiting for CI results is reduced by approximately &lt;strong&gt;25%–35% on average&lt;/strong&gt;. The Merge Queue design also provides a theoretical baseline stability of &lt;strong&gt;95%+&lt;/strong&gt; for the &lt;code&gt;dev&lt;/code&gt; branch by preventing prolonged red states.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ✨ TOP 7: Declarative OptionRule Validation Expands Across 10 Connector Migration PRs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PR list: 10 independent migrations + 1 additional Iceberg alignment enhancement:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pulsar (#11985 Linz1248) — First-time contribution&lt;/li&gt;
&lt;li&gt;MongoDB fetch-size (#11886 amanbbdniit.mishra) — First-time contribution&lt;/li&gt;
&lt;li&gt;HDFS + S3 sink (#11881 itzrohan007) — First-time contribution&lt;/li&gt;
&lt;li&gt;HBase timestamp (#11803 goutamadwant)&lt;/li&gt;
&lt;li&gt;Iceberg (#11921 claire040217) — First-time contribution + Iceberg alignment enhancement (#11675 claire040217)&lt;/li&gt;
&lt;li&gt;Cassandra (#11964 Aryadeepta) — First-time contribution&lt;/li&gt;
&lt;li&gt;ActiveMQ (#12004 nikk4645) — First-time contribution&lt;/li&gt;
&lt;li&gt;RabbitMQ source (#11795)&lt;/li&gt;
&lt;li&gt;IoTDBv2 SQL dialect (#11839)&lt;/li&gt;
&lt;li&gt;Email (#11817)&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Before: Traditional Procedural Validation&lt;/th&gt;
&lt;th&gt;After: Declarative OptionRule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Validation code location&lt;/td&gt;
&lt;td&gt;Distributed across multiple Factory/constructor classes, typically around 100–200 lines&lt;/td&gt;
&lt;td&gt;Centralized in OptionRule definitions, typically around 30–80 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error messages&lt;/td&gt;
&lt;td&gt;Mixed Chinese/English, NPEs, or raw stack traces&lt;/td&gt;
&lt;td&gt;Standardized &lt;code&gt;OptionMessage&lt;/code&gt; JSON: &lt;code&gt;{field, required, range...}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL vs. API consistency&lt;/td&gt;
&lt;td&gt;Implemented separately in two places&lt;/td&gt;
&lt;td&gt;One shared OptionRule implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Development cost for new contributors (estimated)&lt;/td&gt;
&lt;td&gt;Requires understanding and modifying multiple locations, with a higher risk of mistakes&lt;/td&gt;
&lt;td&gt;Follow the template and fill in the required rules; estimated effort reduction of ≈30%–40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What users will notice:&lt;/strong&gt; Configuration errors can now produce clear validation messages instead of occasionally surfacing as difficult-to-understand NPE stack traces. For example, &lt;code&gt;partitionDiscoveryIntervalMillis=-1&lt;/code&gt; in Pulsar now returns an explicit validation error indicating that the field value is &lt;code&gt;-1&lt;/code&gt; and must be greater than &lt;code&gt;0&lt;/code&gt;. This is a deterministic improvement in configuration validation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Appendix: Quick Reference for Key Changes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Commit Hash&lt;/th&gt;
&lt;th&gt;PR #&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;96048a7b8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11167&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Azure Cosmos DB Source&lt;/strong&gt; (ilovezeri333, first-time contributor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a9f69848a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#10780&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;DB2 CDC Source&lt;/strong&gt; (davidzollo365)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;99e53aad8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11952&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;BosFile Source &amp;amp; Sink&lt;/strong&gt; (programmerloverun)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;9f229ce3e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11865&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;NebulaGraph Vertex Sink&lt;/strong&gt; (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3534417f6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11968&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;SNMP Source&lt;/strong&gt; (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e352beba2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11998&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;PostHog Source&lt;/strong&gt; (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c5b58851b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11989&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Google Pub/Sub Source&lt;/strong&gt; (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e97555a62&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11877&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Google Pub/Sub Sink&lt;/strong&gt; (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;8725e1e13&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11939&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Azure Queue Storage Sink&lt;/strong&gt; (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ead62107e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11198&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Couchbase Sink&lt;/strong&gt; (srijan-singh)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;4ebb87b8f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11337&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;PythonSource connector&lt;/strong&gt; (escheduler)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a6beccf8b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11460&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;NATS JetStream Sink&lt;/strong&gt; (rucciva, first-time contributor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;543f2a5a6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11413&lt;/td&gt;
&lt;td&gt;Feature-New&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;HugeGraph Source + Sink multi-mapping&lt;/strong&gt; (liu7777jx, first-time contributor; largest PR this month, +14,135/-1,223)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f0046a002&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11649&lt;/td&gt;
&lt;td&gt;Feature-Enhancement&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;BigQuery Multi-Table + UniverseDomain&lt;/strong&gt; (merlin.launay, first-time contributor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;9ec9c4fe8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11984&lt;/td&gt;
&lt;td&gt;Feature-Zeta&lt;/td&gt;
&lt;td&gt;Zeta dynamic log level REST v2 (SEZ9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0071e8fa6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11909&lt;/td&gt;
&lt;td&gt;Feature-Zeta&lt;/td&gt;
&lt;td&gt;Zeta Worker Resource REST API (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;7c73866c7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11957&lt;/td&gt;
&lt;td&gt;Feature-Zeta&lt;/td&gt;
&lt;td&gt;Zeta Job Detail real-time metrics charts (lindaluo83)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f9b1b330f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11963&lt;/td&gt;
&lt;td&gt;Feature-CI&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;CI Merge Queue introduced&lt;/strong&gt; (zniu70696)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c44f5b338&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11982&lt;/td&gt;
&lt;td&gt;Improve-Zeta&lt;/td&gt;
&lt;td&gt;Zeta Job &lt;code&gt;stateTransitions&lt;/code&gt; exposure (SEZ9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2b51d3614&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11421&lt;/td&gt;
&lt;td&gt;Feature-Zeta&lt;/td&gt;
&lt;td&gt;Zeta &lt;code&gt;LATEST_COMPLETED&lt;/code&gt; Checkpoint recovery (JeremyXin)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;425d4ac57&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11983&lt;/td&gt;
&lt;td&gt;Bug-Zeta&lt;/td&gt;
&lt;td&gt;Reject invalid log level input (SEZ9)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f13b610ef&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11653&lt;/td&gt;
&lt;td&gt;Fix-Zeta&lt;/td&gt;
&lt;td&gt;Prevent duplicate Pending scheduling after failover (shenghang)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1ff72ceb3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11812&lt;/td&gt;
&lt;td&gt;Fix-Zeta&lt;/td&gt;
&lt;td&gt;ClassLoader leak after task deployment failure (goutamadwant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2a739a94a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11492&lt;/td&gt;
&lt;td&gt;Fix-Zeta&lt;/td&gt;
&lt;td&gt;Isolate cross-Worker metric Registry interference (dybyte)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b03cf1f16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#12001&lt;/td&gt;
&lt;td&gt;Fix-Zeta&lt;/td&gt;
&lt;td&gt;Fix metric misalignment for Schema-first multi-table sinks (BinTaoMa, first-time contributor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;be265d45c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11986&lt;/td&gt;
&lt;td&gt;Feature-Benchmark&lt;/td&gt;
&lt;td&gt;Zeta intermediate queue comparison benchmark (zniu70696)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ba55ef965&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11025&lt;/td&gt;
&lt;td&gt;Feature-CDC&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;CDC COMMENT Schema Change event support&lt;/strong&gt; (cloverdue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;339fb88cce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11811&lt;/td&gt;
&lt;td&gt;Improve-Core&lt;/td&gt;
&lt;td&gt;Expanded configuration log redaction coverage (shenghang)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e46a7106c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11827&lt;/td&gt;
&lt;td&gt;Improve-Connector&lt;/td&gt;
&lt;td&gt;Prometheus Checkpoint flush alignment (surafel58)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;4ba289595&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#9993&lt;/td&gt;
&lt;td&gt;Refactor&lt;/td&gt;
&lt;td&gt;seatunnel-shade module refactoring (hawk9821)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f1a1a0abb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11851&lt;/td&gt;
&lt;td&gt;Revert&lt;/td&gt;
&lt;td&gt;Revert Jackson relocate (restoring the stable version) (hawk9821)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;13d3977f8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#11870&lt;/td&gt;
&lt;td&gt;Docs-Cookbook&lt;/td&gt;
&lt;td&gt;Zeta slow-operation troubleshooting cookbook (zhang-arvin)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>apacheseatunnel</category>
      <category>programming</category>
      <category>datascience</category>
      <category>bigdata</category>
    </item>
    <item>
      <title>Redis Data Migration Without Scripts: How Apache SeaTunnel Handles String, Hash, Set, and ZSet</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Fri, 11 Sep 2026 06:52:56 +0000</pubDate>
      <link>https://dev.to/seatunnel/redis-data-migration-without-scripts-how-apache-seatunnel-handles-string-hash-set-and-zset-3pio</link>
      <guid>https://dev.to/seatunnel/redis-data-migration-without-scripts-how-apache-seatunnel-handles-string-hash-set-and-zset-3pio</guid>
      <description>&lt;p&gt;Apache SeaTunnel’s Redis connector supports reading and writing four Redis data types: &lt;strong&gt;string, hash, set, and zset&lt;/strong&gt;. This means you can move data from one Redis instance to another without writing custom migration scripts. This article provides a complete synchronization configuration for each data type, based on the connector behavior in &lt;strong&gt;SeaTunnel 2.3.13&lt;/strong&gt;, as a practical reference for developers and data engineers working with Redis data migration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Data Type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Read Behavior&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Typical Write Method&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Reads the entire value; when &lt;code&gt;format=json&lt;/code&gt;, fields are parsed according to the schema&lt;/td&gt;
&lt;td&gt;Writes by key; later writes overwrite earlier values for the same key&lt;/td&gt;
&lt;td&gt;Key templates support &lt;code&gt;{field}&lt;/code&gt; placeholders for dynamic key rewriting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hash&lt;/td&gt;
&lt;td&gt;Without a schema, the entire hash is serialized as a single JSON row&lt;/td&gt;
&lt;td&gt;Appends to a list&lt;/td&gt;
&lt;td&gt;The hash key itself is not included in the data row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;Each member is emitted as a separate row&lt;/td&gt;
&lt;td&gt;Appends to a list&lt;/td&gt;
&lt;td&gt;Unordered and does not deduplicate across source sets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zset&lt;/td&gt;
&lt;td&gt;Each member is emitted as a separate row&lt;/td&gt;
&lt;td&gt;Appends to a list&lt;/td&gt;
&lt;td&gt;Scores are not read and will be lost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;String: Structured Pass-Through&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;data_type = string&lt;/code&gt;, the connector reads the complete value of each Redis key. When &lt;code&gt;format = json&lt;/code&gt; is configured, the value is parsed field by field according to the schema.&lt;/p&gt;

&lt;p&gt;On the sink side, &lt;code&gt;data_type = key&lt;/code&gt; writes each record back using a Redis key. If the same key is written multiple times, the later value overwrites the earlier one. The &lt;code&gt;{uid}&lt;/code&gt; placeholder in the key template is replaced with the value of the corresponding field in each record.&lt;/p&gt;

&lt;p&gt;This setup works well for scenarios such as &lt;strong&gt;cache migration and Redis key prefix changes&lt;/strong&gt;, where you want to preserve the original data structure while rewriting the destination key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "user:profile:*"
    data_type = string
    batch_size = 200
    read_key_enabled = true
    key_field_name = key
    single_field_name = value
    format = json
    schema = {
      table = "UserDB.UserProfile"
      columns = [
        { name = "key",       type = "string" },
        { name = "uid",       type = "bigint" },
        { name = "nickname",  type = "string" },
        { name = "age",       type = "int" }
      ]
    }
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "user:profile:v2:{uid}"
    support_custom_key = true
    data_type = key
    batch_size = 200
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SET user:profile:1001 '{"uid":1001,"nickname":"xiaoma","age":30}'
SET user:profile:1002 '{"uid":1002,"nickname":"candy","age":25}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the job finishes, verify the result with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET user:profile:v2:1001
"{\"key\":\"user:profile:1001\",\"uid\":1001,\"nickname\":\"xiaoma\",\"age\":30}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data is preserved while the key is rewritten with the &lt;code&gt;v2&lt;/code&gt; prefix.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;value_field&lt;/code&gt; is not configured, the entire row is serialized as JSON before being written. With &lt;code&gt;read_key_enabled = true&lt;/code&gt;, the original Redis key is also included as a field in the output record.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Hash: Flatten and Merge into a List&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When no schema is configured, the connector serializes all field-value pairs in each hash into a single JSON object and sends it as one row. The values remain strings, while the hash key itself is not included in the data row.&lt;/p&gt;

&lt;p&gt;If the sink is configured with &lt;code&gt;data_type = list&lt;/code&gt;, records from multiple hashes can be appended to the same Redis list. This is useful when you need to &lt;strong&gt;aggregate hashes with different field structures into a single downstream stream&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "user:ext:*"
    data_type = hash
    batch_size = 200
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "audit:user:ext:queue"
    data_type = list
    batch_size = 200
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HSET user:ext:1001 city Shanghai level gold
HSET user:ext:1002 city Beijing level silver device iOS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LRANGE audit:user:ext:queue 0 -1
1) "{\"city\":\"Shanghai\",\"level\":\"gold\"}"
2) "{\"city\":\"Beijing\",\"level\":\"silver\",\"device\":\"iOS\"}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two hashes have different field structures, but both are flattened into the same Redis list.&lt;/p&gt;

&lt;p&gt;If you want to split the hash into individual columns instead of keeping the entire hash as a JSON object, configure a schema and set &lt;code&gt;hash_key_parse_mode = kv&lt;/code&gt;. In this mode, the first field in the schema is used to store the original hash key, and each key-value pair is emitted as a separate row.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Set: Expand Each Member into a List&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For Redis sets, each member is emitted as an individual row. In other words, the data granularity changes from a collection to a stream of individual values.&lt;/p&gt;

&lt;p&gt;Configure the sink with &lt;code&gt;data_type = list&lt;/code&gt; to append these values to a Redis list one by one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "tag:members:*"
    data_type = set
    batch_size = 200
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "tag:members:merged"
    data_type = list
    batch_size = 200
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SADD tag:members:vip     1001 1003 1007
SADD tag:members:newuser 1002 1003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LRANGE tag:members:merged 0 -1
1) "1001"
2) "1003"
3) "1007"
4) "1002"
5) "1003"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two things to keep in mind.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;Redis sets are unordered&lt;/strong&gt;. The order shown above is only an example and should not be treated as meaningful business ordering.&lt;/p&gt;

&lt;p&gt;Second, the list does not deduplicate values across different source sets. Since &lt;code&gt;1003&lt;/code&gt; exists in both source sets, it appears twice in the destination list.&lt;/p&gt;

&lt;p&gt;If you want to merge the data and remove duplicates, change the sink to &lt;code&gt;data_type = set&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;ZSet: Expand Each Member, but Scores Are Lost&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A Redis sorted set (&lt;code&gt;zset&lt;/code&gt;) behaves similarly to a set in this connector scenario: each member is emitted as a separate row. However, the &lt;strong&gt;score is not read and is not included in the output record&lt;/strong&gt;, so the downstream system receives only the member values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    keys = "rank:board:*"
    data_type = zset
    batch_size = 200
  }
}

sink {
  Redis {
    host = "redis-prod-01"
    port = 6379
    auth = "redis-demo-pass"
    key = "rank:members:merged"
    data_type = list
    batch_size = 200
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZADD rank:board:day  9800 1001 9500 1003
ZADD rank:board:week 7200 1002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the result with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LRANGE rank:members:merged 0 -1
1) "1001"
2) "1003"
3) "1002"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scores of the three members, &lt;code&gt;9800&lt;/code&gt;, &lt;code&gt;9500&lt;/code&gt;, and &lt;code&gt;7200&lt;/code&gt;, are all lost during the migration.&lt;/p&gt;

&lt;p&gt;The same limitation applies when writing to a zset: the sink uses a fixed score of &lt;code&gt;1&lt;/code&gt; rather than preserving the original score. In other words, &lt;strong&gt;neither the source read nor the destination write carries the actual zset score&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Therefore, this approach should not be used for use cases such as &lt;strong&gt;leaderboards or priority queues&lt;/strong&gt;, where the score is part of the business logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Summary&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The four Redis data types correspond to three different data transformation patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;String:&lt;/strong&gt; structured pass-through&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hash:&lt;/strong&gt; flatten and merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set/ZSet:&lt;/strong&gt; expand each member into an individual record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many Redis-to-Redis migration scenarios, Apache SeaTunnel can handle the data transfer directly through connector configuration, without requiring custom migration scripts.&lt;/p&gt;

&lt;p&gt;At the same time, the connector's boundaries are clear. By default, the &lt;strong&gt;hash key, set ordering, and zset scores&lt;/strong&gt; are not preserved.&lt;/p&gt;

&lt;p&gt;If your application depends on any of these pieces of metadata, you will need to consider a custom script or another migration approach.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>apacheseatunnel</category>
      <category>redis</category>
      <category>datascience</category>
    </item>
    <item>
      <title>🚀 Need to sync Amazon DynamoDB data to Redis? Apache SeaTunnel makes it simple—configure once, run anywhere, and scale with ease. Thanks @Ricardo Ferreira for the great guide!</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Fri, 04 Sep 2026 03:30:52 +0000</pubDate>
      <link>https://dev.to/seatunnel/need-to-sync-amazon-dynamodb-data-to-redis-apache-seatunnel-makes-it-simple-configure-once-run-24om</link>
      <guid>https://dev.to/seatunnel/need-to-sync-amazon-dynamodb-data-to-redis-apache-seatunnel-makes-it-simple-configure-once-run-24om</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/redis/syncing-data-from-amazon-dynamodb-to-redis-with-apache-seatunnel-4njn" class="crayons-story__hidden-navigation-link"&gt;Syncing Data from Amazon DynamoDB to Redis with Apache SeaTunnel&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/redis"&gt;
            &lt;img alt="Redis logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F1284%2Fe3860d38-3900-46c6-a051-1fb66704157c.png" class="crayons-logo__image" width="800" height="800"&gt;
          &lt;/a&gt;

          &lt;a href="/riferrei" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F515691%2Fa664c2f5-cf2c-494a-995c-ab1eb023ff73.png" alt="riferrei profile" class="crayons-avatar__image" width="800" height="654"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/riferrei" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Ricardo Ferreira
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Ricardo Ferreira
                
                
              
              &lt;div id="story-author-preview-content-3161104" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/riferrei" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F515691%2Fa664c2f5-cf2c-494a-995c-ab1eb023ff73.png" class="crayons-avatar__image" alt="" width="800" height="654"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Ricardo Ferreira&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/redis" class="crayons-story__secondary fw-medium"&gt;Redis&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/redis/syncing-data-from-amazon-dynamodb-to-redis-with-apache-seatunnel-4njn" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 9&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/redis/syncing-data-from-amazon-dynamodb-to-redis-with-apache-seatunnel-4njn" id="article-link-3161104"&gt;
          Syncing Data from Amazon DynamoDB to Redis with Apache SeaTunnel
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/aws"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;aws&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dynamodb"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dynamodb&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/redis"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;redis&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/seatunnel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;seatunnel&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/redis/syncing-data-from-amazon-dynamodb-to-redis-with-apache-seatunnel-4njn" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;8&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/redis/syncing-data-from-amazon-dynamodb-to-redis-with-apache-seatunnel-4njn#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            25 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>🚀 Migrating from PostgreSQL to TiDB Cloud? See how Apache SeaTunnel enables reliable full-load + CDC with zero data loss. 🔄📊 Read the full guide! #ApacheSeaTunnel #TiDB #DataMigration</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:52:56 +0000</pubDate>
      <link>https://dev.to/seatunnel/migrating-from-postgresql-to-tidb-cloud-see-how-apache-seatunnel-enables-reliable-full-load--epk</link>
      <guid>https://dev.to/seatunnel/migrating-from-postgresql-to-tidb-cloud-see-how-apache-seatunnel-enables-reliable-full-load--epk</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/seatunnel/from-postgresql-to-tidb-cloud-a-hands-on-data-migration-with-apache-seatunnel-31ji" class="crayons-story__hidden-navigation-link"&gt;From PostgreSQL to TiDB Cloud: A Hands-On Data Migration with Apache SeaTunnel&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/seatunnel" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" alt="seatunnel profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/seatunnel" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Apache SeaTunnel
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Apache SeaTunnel
                
                
              
              &lt;div id="story-author-preview-content-4564247" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/seatunnel" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Apache SeaTunnel&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/seatunnel/from-postgresql-to-tidb-cloud-a-hands-on-data-migration-with-apache-seatunnel-31ji" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 3&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/seatunnel/from-postgresql-to-tidb-cloud-a-hands-on-data-migration-with-apache-seatunnel-31ji" id="article-link-4564247"&gt;
          From PostgreSQL to TiDB Cloud: A Hands-On Data Migration with Apache SeaTunnel
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/postgressql"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;postgressql&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/apacheseatunnel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;apacheseatunnel&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/database"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;database&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/seatunnel/from-postgresql-to-tidb-cloud-a-hands-on-data-migration-with-apache-seatunnel-31ji#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>From PostgreSQL to TiDB Cloud: A Hands-On Data Migration with Apache SeaTunnel</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Thu, 03 Sep 2026 09:52:43 +0000</pubDate>
      <link>https://dev.to/seatunnel/from-postgresql-to-tidb-cloud-a-hands-on-data-migration-with-apache-seatunnel-31ji</link>
      <guid>https://dev.to/seatunnel/from-postgresql-to-tidb-cloud-a-hands-on-data-migration-with-apache-seatunnel-31ji</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv44e4y2o56mxioeigvfk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv44e4y2o56mxioeigvfk.jpg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why Migrate the Database?
&lt;/h2&gt;

&lt;p&gt;In the early stages of our business, we used PostgreSQL as both an OLTP and analytical database, deployed and maintained on our own infrastructure. In day-to-day operations, however, maintaining high availability, scheduling regular backups, and handling failure recovery required significant engineering effort. The resulting operational costs and complexity were high, and occasional operational oversights introduced a certain level of risk to business continuity.&lt;/p&gt;

&lt;p&gt;Given that &lt;strong&gt;TiDB Cloud&lt;/strong&gt;, PingCAP's cloud database service, eliminates the need for on-premises deployment and maintenance while offering convenient usage and flexible, controllable pricing, we decided to migrate our PostgreSQL database to &lt;strong&gt;TiDB Cloud&lt;/strong&gt;, with business continuity and stability as our top priorities. We also saw this migration as an opportunity to explore a more diversified data storage and analytics architecture that would better fit our business needs.&lt;/p&gt;

&lt;p&gt;Our core business focuses on &lt;strong&gt;road traffic checkpoint data analytics&lt;/strong&gt;. Based on massive volumes of vehicle passage records—including license plates, vehicle speeds, locations, and driving behavior characteristics—we provide data support for traffic management and safety oversight.&lt;/p&gt;

&lt;p&gt;The core table, &lt;code&gt;driving_data_jsonb&lt;/code&gt;, uses a JSONB + partitioned-table design. It processes approximately &lt;strong&gt;5 million vehicle passage records per day&lt;/strong&gt;, while a single partition stores around &lt;strong&gt;150 million vehicle passage records (approximately 60 GB)&lt;/strong&gt;. It is the largest and most frequently queried table in our business.&lt;/p&gt;

&lt;p&gt;Typical analytical scenarios include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Road congestion analysis:&lt;/strong&gt; Aggregating traffic volume and average vehicle speed by time period and road segment to assess current road traffic conditions in real time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Section speed-limit violation detection:&lt;/strong&gt; Calculating a vehicle's average speed over a road section based on the travel time and distance between checkpoints, and identifying speeding violations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Driving trajectory analysis:&lt;/strong&gt; Reconstructing vehicle routes to analyze driving habits and patterns of abnormal behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Continuous driving duration monitoring:&lt;/strong&gt; Tracking the continuous driving time of individual vehicles and generating alerts based on traffic regulations, such as the requirement to take a mandatory break after four hours of continuous driving&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These analytical queries involve large-scale data aggregation and multidimensional calculations, placing relatively high demands on the database's OLAP capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Migration Environment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Source Environment
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL 18.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Local virtual machine (test environment)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;traffic&lt;/code&gt; (traffic data database)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core table&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;driving_data_jsonb&lt;/code&gt; (partitioned table, approximately 150 million rows / 60 GB per partition)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network egress&lt;/td&gt;
&lt;td&gt;500 Mbps broadband connection with direct public Internet access&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2.2 Target Environment
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Platform&lt;/td&gt;
&lt;td&gt;TiDB Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cn-shanghai&lt;/code&gt; (Shanghai)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage consumption&lt;/td&gt;
&lt;td&gt;Approximately 62.62 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  2.3 Network Topology
&lt;/h3&gt;

&lt;p&gt;The source environment connects directly to &lt;strong&gt;TiDB Cloud&lt;/strong&gt; over the public Internet through a 500 Mbps broadband connection. No dedicated private connection or VPN was deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Migration Tool Selection
&lt;/h2&gt;

&lt;p&gt;During the migration, we primarily evaluated two commonly used data migration tools: DataX and SeaTunnel.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DataX&lt;/th&gt;
&lt;th&gt;SeaTunnel ✅&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;• No version updates for a long time&lt;/td&gt;
&lt;td&gt;• Active open-source project with timely iterative updates (currently using 2.3.13)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;• Some plugins rely on Python 2.x helper scripts, leading to poor compatibility with modern tech stacks&lt;/td&gt;
&lt;td&gt;• Rich variety of supported data sources with a mature connector ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;• Limited extensibility with a relatively sparse connector ecosystem&lt;/td&gt;
&lt;td&gt;• High community activity and fast response times to issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;• Supports generic JDBC connectors, fully compatible with PG (PostgreSQL) and TiDB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Given our core requirements for &lt;strong&gt;timely synchronization, stability, and zero data loss&lt;/strong&gt;, we ultimately selected &lt;strong&gt;SeaTunnel 2.3.13&lt;/strong&gt; as the migration tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Migrating Data with SeaTunnel
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Migration Strategy: Full Load + Incremental Sync
&lt;/h3&gt;

&lt;p&gt;Because the PG-CDC connector in SeaTunnel 2.3.13 had a known bug in the &lt;strong&gt;combined full-load + incremental synchronization mode&lt;/strong&gt; (which we reported to the community and helped fix), we adopted a two-step approach: &lt;strong&gt;full-load initialization followed by CDC-based incremental synchronization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; Perform a full data initialization using the JDBC PostgreSQL connector.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Use the CDC incremental connector to synchronize data changes in real time.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Full-Load Configuration (JDBC Source → JDBC Sink)
&lt;/h3&gt;

&lt;p&gt;The following is the core configuration used during the full-load phase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;parallelism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;checkpoint.interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;pipeline.name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PG_TO_TiDB_DRIVING"&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;flink.execution.checkpointing.mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXACTLY_ONCE"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;flink.execution.checkpointing.timeout&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;Jdbc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jdbc:postgresql://&amp;lt;source_host&amp;gt;:5432/traffic"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"org.postgresql.Driver"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"******"&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SELECT record_id, pass_time, plate_no, plate_color,
             vehicle_type, speed, speed_limit, status,
             longitude, latitude, speed_variance,
             lane_change_count, raw_behavior_features,
             create_time, update_time
             FROM driving_data_jsonb"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;column.filters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update_time"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;start.time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-01 00:00:00"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;fetch.size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;parallelism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;sink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;Jdbc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jdbc:mysql://&amp;lt;tidb_cloud_host&amp;gt;:4000/traffic?sslMode=VERIFY_IDENTITY"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.mysql.cj.jdbc.Driver"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"******"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"******"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;database&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"traffic"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"driving_data_jsonb"&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;generate_sink_sql&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;save_mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upsert"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;unique_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"record_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;batch.size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;batch.interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;parallelism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.3 Incremental Synchronization Configuration (Postgres-CDC Source → JDBC Sink)
&lt;/h3&gt;

&lt;p&gt;Once the full load was completed, we started the CDC incremental connector to continuously synchronize changes from the source database in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hocon"&gt;&lt;code&gt;&lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;execution.parallelism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;job.mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STREAMING"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;checkpoint.interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;Postgres-CDC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"******"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;database-names&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"traffic"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;schema-names&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;table-names&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"traffic.public.driving_data_jsonb"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jdbc:postgresql://&amp;lt;source_host&amp;gt;:5432/traffic"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;decoding.plugin.name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pgoutput"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;slot.name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"final_slot"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;startup.mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;plugin_output&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"out"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;sink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;Jdbc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jdbc:mysql://&amp;lt;tidb_cloud_host&amp;gt;:4000/traffic?sslMode=VERIFY_IDENTITY"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;driver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.mysql.cj.jdbc.Driver"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"******"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"******"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;database&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"traffic"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"driving_data_jsonb"&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;generate_sink_sql&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;save_mode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"upsert"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;unique_key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"record_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;batch.size&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;batch.interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;parallelism&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.4 Key Configuration Details
&lt;/h3&gt;

&lt;p&gt;The following table highlights the key configuration options used during full-load and incremental synchronization:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;parallelism = 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets the parallelism to 2 to match the available resources on the source side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkpoint.mode = EXACTLY_ONCE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Provides exactly-once semantics to help ensure data consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkpoint.interval = 10000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sets the checkpoint interval to 10 seconds, balancing performance and fault tolerance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;save_mode = upsert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes data using Upsert based on &lt;code&gt;unique_key&lt;/code&gt; to prevent duplicate records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unique_key = ["record_id"]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;record_id&lt;/code&gt; as the deduplication key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sslMode = VERIFY_IDENTITY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses a TLS-encrypted connection, as required by TiDB Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;batch.size / batch.interval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Writes 1,000 records per batch or flushes every 1 second, whichever comes first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;decoding.plugin.name = pgoutput&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uses PostgreSQL's native logical decoding plugin for CDC incremental synchronization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;slot.name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Specifies the logical replication slot to ensure incremental data is not lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startup.mode = latest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starts consuming incremental changes from the latest position to avoid duplicate synchronization&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.5 Migration Results
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full-load duration&lt;/td&gt;
&lt;td&gt;Approximately 12 hours per partition (the source was a virtual machine in a test environment)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental synchronization latency&lt;/td&gt;
&lt;td&gt;The migration proceeded smoothly with no noticeable latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data consistency&lt;/td&gt;
&lt;td&gt;Every record contains the unique primary key &lt;code&gt;record_id&lt;/code&gt;. The Sink uses &lt;code&gt;upsert&lt;/code&gt; mode and deduplicates records based on the primary key, ensuring that data is neither lost nor duplicated. After migration, the final validation was performed by comparing row counts between the source and target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema compatibility&lt;/td&gt;
&lt;td&gt;No data type mapping issues were encountered when migrating from PostgreSQL to TiDB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The migration first focused on the largest core table, &lt;code&gt;driving_data_jsonb&lt;/code&gt;. The remaining tables contain significantly less data and will be synchronized as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Challenges and Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 PG-CDC Connector Bug
&lt;/h3&gt;

&lt;p&gt;The PG-CDC connector in SeaTunnel 2.3.13 had a bug in the combined full-load + incremental synchronization mode, which prevented the workflow from completing successfully. We reported the issue to the community, worked with developers to identify and fix the problem, and ultimately avoided it by running the full-load and incremental synchronization stages separately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; When using SeaTunnel for PostgreSQL migration, we recommend adopting a &lt;strong&gt;two-step full-load + incremental synchronization strategy&lt;/strong&gt; first, as it provides better stability. If you need to use the integrated mode, make sure the SeaTunnel version you are using contains the fix for this issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2 The Impact of Network Bandwidth
&lt;/h3&gt;

&lt;p&gt;The source database was deployed on a local virtual machine and connected to &lt;strong&gt;TiDB Cloud&lt;/strong&gt; over the public Internet through a 500 Mbps broadband connection. Synchronizing 60 GB of data from a single partition took approximately 12 hours during the full-load phase.&lt;/p&gt;

&lt;p&gt;If the source database is deployed in the same cloud region or connected through a dedicated private connection, the synchronization time is expected to be significantly shorter.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Business Performance After Migration
&lt;/h2&gt;

&lt;p&gt;The most noticeable improvement after migrating to &lt;strong&gt;TiDB Cloud&lt;/strong&gt; was &lt;strong&gt;business performance in OLAP workloads&lt;/strong&gt;. Statistical analysis and complex queries became noticeably more efficient, reducing query wait times for business users and improving the overall query experience.&lt;/p&gt;

&lt;p&gt;Going forward, we plan to explore enabling &lt;strong&gt;TiFlash&lt;/strong&gt;, the columnar storage engine, based on specific business scenarios. This will allow us to further optimize analytical query performance and make better use of TiDB's HTAP architecture for workloads that combine row- and column-oriented processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; We have not yet completed a detailed analysis of application performance after the migration. Specific query performance comparisons will be added in a future update.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Conclusion and Recommendations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overall Assessment
&lt;/h3&gt;

&lt;p&gt;Overall, the migration from PostgreSQL to &lt;strong&gt;TiDB Cloud&lt;/strong&gt; achieved the expected results.&lt;/p&gt;

&lt;p&gt;SeaTunnel performed reliably as the migration tool. Both the full-load and incremental synchronization stages were completed successfully, with no data loss observed. &lt;strong&gt;TiDB Cloud&lt;/strong&gt; delivered noticeable performance improvements for OLAP analytical workloads while significantly reducing the operational burden of database management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration Recommendations for Other PostgreSQL Users
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tool selection:&lt;/strong&gt; We recommend SeaTunnel for its rich connector ecosystem and active community. Its JDBC connector can quickly adapt to both PostgreSQL and TiDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration strategy:&lt;/strong&gt; We recommend using a two-step full-load + incremental synchronization strategy, which provides better stability than the integrated mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data consistency:&lt;/strong&gt; Be sure to enable the &lt;code&gt;EXACTLY_ONCE&lt;/code&gt; checkpoint mode and use &lt;code&gt;upsert&lt;/code&gt; writes to prevent duplicate records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network planning:&lt;/strong&gt; Network quality between the source environment and &lt;strong&gt;TiDB Cloud&lt;/strong&gt; has a significant impact on full-load synchronization time. Where possible, prioritize deployment within the same cloud environment or use a dedicated private connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure connections:&lt;/strong&gt; &lt;strong&gt;TiDB Cloud&lt;/strong&gt; requires TLS. Configure &lt;code&gt;sslMode=VERIFY_IDENTITY&lt;/code&gt; on the Sink side to ensure encrypted data transmission.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>postgressql</category>
      <category>apacheseatunnel</category>
      <category>database</category>
      <category>datascience</category>
    </item>
    <item>
      <title>🔍 A successful pipeline doesn’t guarantee trusted data. See how Apache SeaTunnel enables reliable movement, recovery, and schema evolution.</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:50:12 +0000</pubDate>
      <link>https://dev.to/seatunnel/a-successful-pipeline-doesnt-guarantee-trusted-data-see-how-apache-seatunnel-enables-reliable-541g</link>
      <guid>https://dev.to/seatunnel/a-successful-pipeline-doesnt-guarantee-trusted-data-see-how-apache-seatunnel-enables-reliable-541g</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/seatunnel/pipeline-green-isnt-data-correct-4m7p" class="crayons-story__hidden-navigation-link"&gt;Pipeline Green Isn’t Data Correct&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/seatunnel" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" alt="seatunnel profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/seatunnel" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Apache SeaTunnel
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Apache SeaTunnel
                
                
              
              &lt;div id="story-author-preview-content-4501945" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/seatunnel" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Apache SeaTunnel&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/seatunnel/pipeline-green-isnt-data-correct-4m7p" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/seatunnel/pipeline-green-isnt-data-correct-4m7p" id="article-link-4501945"&gt;
          Pipeline Green Isn’t Data Correct
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/apacheseatunnel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;apacheseatunnel&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/seatunnel/pipeline-green-isnt-data-correct-4m7p#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            11 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>🔄 A financial tech company migrated 200+ ETL workflows from Informatica to Apache SeaTunnel, cutting infrastructure costs 60% and runtimes 40%!</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:49:43 +0000</pubDate>
      <link>https://dev.to/seatunnel/a-financial-tech-company-migrated-200-etl-workflows-from-informatica-to-apache-seatunnel-2oi2</link>
      <guid>https://dev.to/seatunnel/a-financial-tech-company-migrated-200-etl-workflows-from-informatica-to-apache-seatunnel-2oi2</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/seatunnel/from-informatica-to-apache-seatunnel-a-financial-grade-etl-migration-in-practice-2pfk" class="crayons-story__hidden-navigation-link"&gt;From Informatica to Apache SeaTunnel: A Financial-Grade ETL Migration in Practice&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/seatunnel" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" alt="seatunnel profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/seatunnel" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Apache SeaTunnel
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Apache SeaTunnel
                
                
              
              &lt;div id="story-author-preview-content-4502239" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/seatunnel" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F844122%2Fc6155eb3-df58-448b-8d88-36865c4f1d84.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Apache SeaTunnel&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/seatunnel/from-informatica-to-apache-seatunnel-a-financial-grade-etl-migration-in-practice-2pfk" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 27&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/seatunnel/from-informatica-to-apache-seatunnel-a-financial-grade-etl-migration-in-practice-2pfk" id="article-link-4502239"&gt;
          From Informatica to Apache SeaTunnel: A Financial-Grade ETL Migration in Practice
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/apacheseatunnel"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;apacheseatunnel&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/etl"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;etl&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/dataengineering"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;dataengineering&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/seatunnel/from-informatica-to-apache-seatunnel-a-financial-grade-etl-migration-in-practice-2pfk#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>From Informatica to Apache SeaTunnel: A Financial-Grade ETL Migration in Practice</title>
      <dc:creator>Apache SeaTunnel</dc:creator>
      <pubDate>Thu, 27 Aug 2026 07:31:23 +0000</pubDate>
      <link>https://dev.to/seatunnel/from-informatica-to-apache-seatunnel-a-financial-grade-etl-migration-in-practice-2pfk</link>
      <guid>https://dev.to/seatunnel/from-informatica-to-apache-seatunnel-a-financial-grade-etl-migration-in-practice-2pfk</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmanoxdj7yy0fww41h11.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjmanoxdj7yy0fww41h11.jpg" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Project Background and Challenges
&lt;/h2&gt;

&lt;p&gt;As the head of data architecture at a financial technology company, I led the migration of our core ETL system from Informatica PowerCenter to a domestic ETL platform last year. The migration covered more than 200 workflows and a core system processing terabytes of data every day. It took five months from start to finish, and we ultimately completed the transition smoothly with zero data incidents. Today, I’d like to share the key decisions, technical details, and lessons learned from the project.&lt;/p&gt;

&lt;p&gt;As a long-standing leader in enterprise data integration, Informatica has dominated the traditional ETL market for more than 20 years. Its visual development environment, reliable scheduling engine, and comprehensive metadata management have made it a de facto standard in industries such as financial services and telecommunications. However, with the changing international landscape and the growing demand for domestic technology adoption, we had to confront three practical challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;High licensing costs:&lt;/strong&gt; Annual maintenance fees running into millions of RMB were a significant burden for a mid-sized enterprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A closed technology stack:&lt;/strong&gt; It was difficult to integrate deeply with emerging real-time computing and AI platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow response to customization needs:&lt;/strong&gt; Custom requirements often required cross-border collaboration, resulting in delivery cycles that could stretch to several months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After years of development, domestic ETL platforms such as Kettle, DataX, and SeaTunnel have become viable alternatives for core ETL capabilities. Our technical evaluation showed that, in batch-processing scenarios, domestic platforms could cover approximately 85% of Informatica’s functionality while costing only one-third as much and offering the flexibility for secondary development.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Migration Strategy and Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Technology Selection
&lt;/h3&gt;

&lt;p&gt;We conducted an in-depth evaluation of three mainstream domestic ETL tools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flw4gl25oqcxhf23mnhu2.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flw4gl25oqcxhf23mnhu2.jpg" width="799" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We ultimately selected Apache SeaTunnel as the primary migration platform for three main reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Support for both Spark and Flink engines&lt;/strong&gt;, making it a good fit for our future real-time data warehouse strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A plugin-based architecture&lt;/strong&gt; that makes it easier to extend support for custom data sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An active Chinese-language community&lt;/strong&gt; that enables us to resolve technical issues quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.2 Migration Strategy
&lt;/h3&gt;

&lt;p&gt;We adopted a hybrid approach combining &lt;strong&gt;phased migration with parallel-run validation&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decouple the components:&lt;/strong&gt; Break each Informatica workflow into three independent modules for extraction, transformation, and loading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Map the functionality:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Source data extraction → SeaTunnel Source plugins&lt;/li&gt;
&lt;li&gt;Complex transformation logic → Rebuild with Spark SQL&lt;/li&gt;
&lt;li&gt;Scheduling dependencies → Orchestrate with Apache DolphinScheduler

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate the data:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Use a combination of CRC32 and sample-based comparison for validation
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_df&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;source_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;target_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;sample_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;
    &lt;span class="n"&gt;source_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;source_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;target_sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;source_sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exceptAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_sample&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isEmpty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key lesson:&lt;/strong&gt; Don’t try to replicate Informatica workflows 1:1. Use the migration as an opportunity to optimize the data flows. We redesigned 30% of the transformation logic that had performance bottlenecks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. Core Migration Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Metadata Migration
&lt;/h3&gt;

&lt;p&gt;The Informatica Repository contained thousands of metadata objects. We developed a metadata parsing tool to automate the migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export XML metadata through the PowerCenter CLI.&lt;/li&gt;
&lt;li&gt;Use XSLT to transform key attributes:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Example of mapping transformation --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;xsl:template&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"SOURCE"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;connector&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"jdbc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"url"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{@DBSERVER}"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;property&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"table"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{@OBJECTNAME}"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/connector&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/xsl:template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Generate SeaTunnel configuration file templates.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.2 Refactoring Complex Transformations
&lt;/h3&gt;

&lt;p&gt;Informatica components such as Expression and Aggregator required special handling during the migration.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Conditional routing:&lt;/strong&gt; The original workflows used the Router component.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Reimplemented with Spark SQL&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createTempView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="nv"&gt;
  SELECT *, 
    CASE 
      WHEN amount &amp;gt; 10000 THEN 'VIP' 
      ELSE 'NORMAL' 
    END AS customer_level
  FROM source
&lt;/span&gt;&lt;span class="se"&gt;""&lt;/span&gt;&lt;span class="nv"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Slowly Changing Dimensions (SCD):&lt;/strong&gt; The original implementation relied on the Slowly Changing Dimension wizard.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Implement Type 2 SCD using MERGE INTO&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;dim_customer&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;stage_customer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Y'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'N'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.3 Performance Tuning in Practice
&lt;/h3&gt;

&lt;p&gt;The most challenging issue we encountered was an ETL job containing 20 joins that ran into an OOM error on SeaTunnel. We resolved it through the following optimizations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Analyze the execution plan:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="o"&gt;#&lt;/span&gt; &lt;span class="k"&gt;Get&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;Spark&lt;/span&gt; &lt;span class="n"&gt;physical&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt; &lt;span class="n"&gt;plan&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="n"&gt;EXTENDED&lt;/span&gt; 
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;fact&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dim1&lt;/span&gt; &lt;span class="n"&gt;d1&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Optimization measures:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Enable dynamic partition pruning: &lt;code&gt;spark.sql.optimizer.dynamicPartitionPruning=true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Adjust the broadcast threshold: &lt;code&gt;spark.sql.autoBroadcastJoinThreshold=20MB&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Force broadcast joins for dimension tables: &lt;code&gt;/*+ BROADCAST(dim1) */&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Parameter comparison:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nzhpvnimlm8sc297c34.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nzhpvnimlm8sc297c34.jpg" width="800" height="196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Validation and Cutover
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Ensuring Data Consistency
&lt;/h3&gt;

&lt;p&gt;We established a three-level validation framework:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Record-level validation:&lt;/strong&gt; Generate a CRC32 fingerprint for the entire table.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CRC32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONCAT_WS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'|'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;col1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;col2&lt;/span&gt;&lt;span class="p"&gt;,...))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;checksum&lt;/span&gt; 
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Business metric comparison:&lt;/strong&gt; Keep month-over-month fluctuations in key KPIs below 1%.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User acceptance testing:&lt;/strong&gt; Have business teams validate the data in their reports.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4.2 Gradual Rollout
&lt;/h3&gt;

&lt;p&gt;We migrated workloads incrementally by business line:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, migrate non-core marketing analytics workloads.&lt;/li&gt;
&lt;li&gt;Next, migrate the risk management system.&lt;/li&gt;
&lt;li&gt;Finally, migrate the financial settlement system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We monitored each phase for one week, with a particular focus on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data latency&lt;/li&gt;
&lt;li&gt;Resource utilization&lt;/li&gt;
&lt;li&gt;Error logs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  5.1 Key Success Factors
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Team training:&lt;/strong&gt; Two months before the migration, we organized training sessions for Informatica developers to learn Spark and SeaTunnel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A complete toolchain:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Developed an auxiliary workflow conversion tool.&lt;/li&gt;
&lt;li&gt;Built an automated data comparison platform.

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vendor support:&lt;/strong&gt; Established a direct communication channel with the SeaTunnel core team.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.2 Lessons Learned the Hard Way
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Time zone issues:&lt;/strong&gt; Informatica uses the server's time zone by default, while Spark uses UTC. We therefore needed to explicitly handle time zone conversion for all relevant time fields:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;FROM_UTC_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CAST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'Asia/Shanghai'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Character encoding pitfalls:&lt;/strong&gt; The ZHS16GBK encoding used by the Oracle source database had to be explicitly configured:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jdbc&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;connection_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oracle.jdbc.convertNlsStrings=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Transaction semantics:&lt;/strong&gt; Informatica uses auto-commit by default, while Spark requires transaction behavior to be controlled explicitly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isolationLevel&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;READ_COMMITTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quantified Results After the Migration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;60% reduction in infrastructure costs&lt;/strong&gt;, from eight physical servers to a Kubernetes cluster&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;40% reduction in average job execution time&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New capabilities for real-time data processing&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest takeaway from this migration is that domestic infrastructure software has reached a level where it can serve as a viable alternative to traditional enterprise platforms. But successful migration requires more than simply replacing one tool with another. It requires a shift in technical mindset.&lt;/p&gt;

&lt;p&gt;Instead of treating migration as a straightforward tool replacement exercise, we used it as an opportunity to rethink and modernize our data architecture, laying the groundwork for the next stage of real-time processing and intelligent data operations.&lt;/p&gt;

</description>
      <category>apacheseatunnel</category>
      <category>etl</category>
      <category>datascience</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
