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

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


The following commit(s) were added to refs/heads/master by this push:
     new 3194adb15 [FLINK-39410][pipeline-connector][elasticsearch] The 
`flink-cdc-pipeline-connector-elasticsearch` module offers better compatibility 
with Flink 1.20 and Flink 2.x (#4368)
3194adb15 is described below

commit 3194adb15f6e3102bc5b30058baa921cadb7b299
Author: Pei Yu <[email protected]>
AuthorDate: Wed Sep 23 11:44:20 2026 +0800

    [FLINK-39410][pipeline-connector][elasticsearch] The 
`flink-cdc-pipeline-connector-elasticsearch` module offers better compatibility 
with Flink 1.20 and Flink 2.x (#4368)
---
 .../pipeline-connectors/elasticsearch.md           |   2 +-
 .../pipeline-connectors/elasticsearch.md           |   2 +-
 .../pom.xml                                        |  11 +-
 .../serializer/ElasticsearchEventSerializer.java   |   6 -
 .../sink/ElasticsearchDataSinkOptions.java         |   2 +-
 .../elasticsearch/v2/Elasticsearch8AsyncSink.java  |  42 ++-
 .../v2/Elasticsearch8AsyncWriter.java              |  39 ++-
 .../flink-cdc-pipeline-e2e-tests/pom.xml           |  21 ++
 .../stage2/MysqlToElasticsearch6E2eITCase.java     | 329 ++++++++++++++++++
 .../stage2/MysqlToElasticsearch7E2eITCase.java     | 329 ++++++++++++++++++
 .../stage2/MysqlToElasticsearch8E2eITCase.java     | 380 +++++++++++++++++++++
 .../connector/sink2/StatefulSinkWriterAdapter.java |  27 ++
 .../connector/sink2/WriterInitContextAdapter.java  |  94 +++++
 .../connector/base/sink/AsyncSinkBaseAdapter.java  |  68 ++++
 .../base/sink/writer/AsyncSinkWriterAdapter.java   |  42 +++
 .../connector/sink2/StatefulSinkWriterAdapter.java |  27 ++
 .../connector/sink2/WriterInitContextAdapter.java  |  94 +++++
 .../connector/base/sink/AsyncSinkBaseAdapter.java  |  69 ++++
 .../base/sink/writer/AsyncSinkWriterAdapter.java   |  64 ++++
 19 files changed, 1609 insertions(+), 39 deletions(-)

diff --git 
a/docs/content.zh/docs/connectors/pipeline-connectors/elasticsearch.md 
b/docs/content.zh/docs/connectors/pipeline-connectors/elasticsearch.md
index 0b260c761..203e6d29e 100644
--- a/docs/content.zh/docs/connectors/pipeline-connectors/elasticsearch.md
+++ b/docs/content.zh/docs/connectors/pipeline-connectors/elasticsearch.md
@@ -160,7 +160,7 @@ Pipeline Connector Options
     <tr>
       <td>record.size.max.bytes</td>
       <td>optional</td>
-      <td style="word-wrap: break-word;">10485760</td>
+      <td style="word-wrap: break-word;">5242880</td>
       <td>Long</td>
       <td>单个记录的最大大小(以byte为单位)。</td>
     </tr>
diff --git a/docs/content/docs/connectors/pipeline-connectors/elasticsearch.md 
b/docs/content/docs/connectors/pipeline-connectors/elasticsearch.md
index 3f3d693fe..60b316872 100644
--- a/docs/content/docs/connectors/pipeline-connectors/elasticsearch.md
+++ b/docs/content/docs/connectors/pipeline-connectors/elasticsearch.md
@@ -160,7 +160,7 @@ Pipeline Connector Options
     <tr>
       <td>record.size.max.bytes</td>
       <td>optional</td>
-      <td style="word-wrap: break-word;">10485760</td>
+      <td style="word-wrap: break-word;">5242880</td>
       <td>Long</td>
       <td>The maximum size of a single record in bytes.</td>
     </tr>
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/pom.xml
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/pom.xml
index de0061449..1cb38ef05 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/pom.xml
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/pom.xml
@@ -35,7 +35,7 @@ limitations under the License.
 
     <properties>
         <elasticsearch.version>8.12.1</elasticsearch.version>
-        
<flink.connector.elasticsearch.version>3.0.1-1.17</flink.connector.elasticsearch.version>
+        
<flink.connector.elasticsearch.version>3.1.0-1.20</flink.connector.elasticsearch.version>
         <httpclient.version>4.5.13</httpclient.version>
         <jakarta.json.version>2.0.2</jakarta.json.version>
     </properties>
@@ -195,4 +195,13 @@ limitations under the License.
             </plugin>
         </plugins>
     </build>
+
+    <profiles>
+        <profile>
+            <id>flink2</id>
+            <properties>
+                
<flink.connector.elasticsearch.version>4.0.0-2.0</flink.connector.elasticsearch.version>
+            </properties>
+        </profile>
+    </profiles>
 </project>
\ No newline at end of file
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/serializer/ElasticsearchEventSerializer.java
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/serializer/ElasticsearchEventSerializer.java
index 800c0f359..afae409b5 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/serializer/ElasticsearchEventSerializer.java
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/serializer/ElasticsearchEventSerializer.java
@@ -17,7 +17,6 @@
 
 package org.apache.flink.cdc.connectors.elasticsearch.serializer;
 
-import org.apache.flink.api.connector.sink2.Sink;
 import org.apache.flink.api.connector.sink2.SinkWriter;
 import org.apache.flink.cdc.common.data.RecordData;
 import org.apache.flink.cdc.common.event.AddColumnEvent;
@@ -251,9 +250,4 @@ public class ElasticsearchEventSerializer implements 
ElementConverter<Event, Bul
             throw new IndexOutOfBoundsException("Index: " + index + ", Size: " 
+ size);
         }
     }
-
-    @Override
-    public void open(Sink.InitContext context) {
-        ElementConverter.super.open(context);
-    }
 }
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/sink/ElasticsearchDataSinkOptions.java
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/sink/ElasticsearchDataSinkOptions.java
index 213b446c8..45183a187 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/sink/ElasticsearchDataSinkOptions.java
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/sink/ElasticsearchDataSinkOptions.java
@@ -74,7 +74,7 @@ public class ElasticsearchDataSinkOptions {
     public static final ConfigOption<Long> MAX_RECORD_SIZE_IN_BYTES =
             ConfigOptions.key("record.size.max.bytes")
                     .longType()
-                    .defaultValue(10L * 1024L * 1024L)
+                    .defaultValue(5L * 1024L * 1024L)
                     .withDescription("The maximum size of a single record in 
bytes.");
 
     /** The version of Elasticsearch to connect to. */
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncSink.java
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncSink.java
index 4a237e3a3..1250c94b4 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncSink.java
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncSink.java
@@ -20,14 +20,16 @@
 package org.apache.flink.cdc.connectors.elasticsearch.v2;
 
 import org.apache.flink.annotation.VisibleForTesting;
-import org.apache.flink.connector.base.sink.AsyncSinkBase;
+import org.apache.flink.api.connector.sink2.InitContextAdapter;
+import org.apache.flink.api.connector.sink2.SinkWriter;
+import org.apache.flink.api.connector.sink2.StatefulSinkWriterAdapter;
+import org.apache.flink.api.connector.sink2.WriterInitContext;
+import org.apache.flink.connector.base.sink.AsyncSinkBaseAdapter;
 import org.apache.flink.connector.base.sink.writer.BufferedRequestState;
 import org.apache.flink.connector.base.sink.writer.ElementConverter;
 import org.apache.flink.core.io.SimpleVersionedSerializer;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Collections;
 
@@ -38,8 +40,7 @@ import java.util.Collections;
  * @param <InputT> type of records that will be converted into {@link 
Operation}. See {@link
  *     Elasticsearch8AsyncSinkBuilder} on how to construct valid instances.
  */
-public class Elasticsearch8AsyncSink<InputT> extends AsyncSinkBase<InputT, 
Operation> {
-    private static final Logger LOG = 
LoggerFactory.getLogger(Elasticsearch8AsyncSink.class);
+public class Elasticsearch8AsyncSink<InputT> extends 
AsyncSinkBaseAdapter<InputT, Operation> {
 
     @VisibleForTesting protected final NetworkConfig networkConfig;
 
@@ -78,14 +79,13 @@ public class Elasticsearch8AsyncSink<InputT> extends 
AsyncSinkBase<InputT, Opera
     }
 
     /**
-     * Creates a new {@link StatefulSinkWriter} for writing elements to 
Elasticsearch.
+     * Creates a new {@link SinkWriter} for writing elements to Elasticsearch.
      *
      * @param context the initialization context.
      * @return a new instance of {@link Elasticsearch8AsyncWriter}.
      */
     @Override
-    public StatefulSinkWriter<InputT, BufferedRequestState<Operation>> 
createWriter(
-            InitContext context) {
+    public SinkWriter<InputT> createWriter(InitContext context) {
         return new Elasticsearch8AsyncWriter<>(
                 getElementConverter(),
                 context,
@@ -99,19 +99,35 @@ public class Elasticsearch8AsyncSink<InputT> extends 
AsyncSinkBase<InputT, Opera
                 Collections.emptyList());
     }
 
+    @Override
+    public SinkWriter<InputT> createWriter(WriterInitContext context) throws 
IOException {
+        return new Elasticsearch8AsyncWriter<>(
+                getElementConverter(),
+                new InitContextAdapter(context),
+                getMaxBatchSize(),
+                getMaxInFlightRequests(),
+                getMaxBufferedRequests(),
+                getMaxBatchSizeInBytes(),
+                getMaxTimeInBufferMS(),
+                getMaxRecordSizeInBytes(),
+                networkConfig,
+                Collections.emptyList());
+    }
+
     /**
-     * Restores a {@link StatefulSinkWriter} from a previously saved state.
+     * Restores a {@link StatefulSinkWriterAdapter} from a previously saved 
state.
      *
      * @param context the initialization context.
      * @param recoveredState the recovered state.
      * @return a restored instance of {@link Elasticsearch8AsyncWriter}.
      */
     @Override
-    public StatefulSinkWriter<InputT, BufferedRequestState<Operation>> 
restoreWriter(
-            InitContext context, Collection<BufferedRequestState<Operation>> 
recoveredState) {
+    public StatefulSinkWriterAdapter<InputT, BufferedRequestState<Operation>> 
restoreWriterAdapter(
+            WriterInitContext context, 
Collection<BufferedRequestState<Operation>> recoveredState)
+            throws IOException {
         return new Elasticsearch8AsyncWriter<>(
                 getElementConverter(),
-                context,
+                new InitContextAdapter(context),
                 getMaxBatchSize(),
                 getMaxInFlightRequests(),
                 getMaxBufferedRequests(),
diff --git 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncWriter.java
 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncWriter.java
index 86d081697..b3246b1aa 100644
--- 
a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncWriter.java
+++ 
b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-elasticsearch/src/main/java/org/apache/flink/cdc/connectors/elasticsearch/v2/Elasticsearch8AsyncWriter.java
@@ -20,10 +20,12 @@
 package org.apache.flink.cdc.connectors.elasticsearch.v2;
 
 import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.StatefulSinkWriterAdapter;
 import org.apache.flink.connector.base.sink.throwable.FatalExceptionClassifier;
-import org.apache.flink.connector.base.sink.writer.AsyncSinkWriter;
+import org.apache.flink.connector.base.sink.writer.AsyncSinkWriterAdapter;
 import org.apache.flink.connector.base.sink.writer.BufferedRequestState;
 import org.apache.flink.connector.base.sink.writer.ElementConverter;
+import org.apache.flink.connector.base.sink.writer.ResultHandler;
 import 
org.apache.flink.connector.base.sink.writer.config.AsyncSinkWriterConfiguration;
 import org.apache.flink.metrics.Counter;
 import org.apache.flink.metrics.groups.SinkWriterMetricGroup;
@@ -41,9 +43,7 @@ import java.net.ConnectException;
 import java.net.NoRouteToHostException;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
-import java.util.function.Consumer;
 
 import static org.apache.flink.util.Preconditions.checkNotNull;
 
@@ -53,7 +53,8 @@ import static 
org.apache.flink.util.Preconditions.checkNotNull;
  *
  * @param <InputT> type of Operations
  */
-public class Elasticsearch8AsyncWriter<InputT> extends AsyncSinkWriter<InputT, 
Operation> {
+public class Elasticsearch8AsyncWriter<InputT> extends 
AsyncSinkWriterAdapter<InputT, Operation>
+        implements StatefulSinkWriterAdapter<InputT, 
BufferedRequestState<Operation>> {
     private static final Logger LOG = 
LoggerFactory.getLogger(Elasticsearch8AsyncWriter.class);
 
     private final ElasticsearchAsyncClient esClient;
@@ -118,7 +119,7 @@ public class Elasticsearch8AsyncWriter<InputT> extends 
AsyncSinkWriter<InputT, O
 
     @Override
     protected void submitRequestEntries(
-            List<Operation> requestEntries, Consumer<List<Operation>> 
requestResult) {
+            List<Operation> requestEntries, ResultHandler<Operation> 
resultHandler) {
         numRequestSubmittedCounter.inc();
         LOG.debug("submitRequestEntries with {} items", requestEntries.size());
 
@@ -136,7 +137,7 @@ public class Elasticsearch8AsyncWriter<InputT> extends 
AsyncSinkWriter<InputT, O
             LOG.debug(
                     "Skipping empty BulkRequest, all {} operation(s) have null 
BulkOperationVariant",
                     requestEntries.size());
-            requestResult.accept(Collections.emptyList());
+            resultHandler.complete();
             return;
         }
 
@@ -144,19 +145,19 @@ public class Elasticsearch8AsyncWriter<InputT> extends 
AsyncSinkWriter<InputT, O
                 .whenComplete(
                         (response, error) -> {
                             if (error != null) {
-                                handleFailedRequest(requestEntries, 
requestResult, error);
+                                handleFailedRequest(requestEntries, 
resultHandler, error);
                             } else if (response.errors()) {
                                 handlePartiallyFailedRequest(
-                                        requestEntries, requestResult, 
response);
+                                        requestEntries, resultHandler, 
response);
                             } else {
-                                handleSuccessfulRequest(requestResult, 
response);
+                                handleSuccessfulRequest(resultHandler, 
response);
                             }
                         });
     }
 
     private void handleFailedRequest(
             List<Operation> requestEntries,
-            Consumer<List<Operation>> requestResult,
+            ResultHandler<Operation> resultHandler,
             Throwable error) {
         LOG.warn(
                 "The BulkRequest of {} operation(s) has failed due to: {}",
@@ -165,14 +166,20 @@ public class Elasticsearch8AsyncWriter<InputT> extends 
AsyncSinkWriter<InputT, O
         LOG.debug("The BulkRequest has failed", error);
         numRecordsOutErrorsCounter.inc(requestEntries.size());
 
-        if (isRetryable(error.getCause())) {
-            requestResult.accept(requestEntries);
+        Throwable retryableError = error.getCause() != null ? error.getCause() 
: error;
+        if (isRetryable(retryableError)) {
+            resultHandler.retryForEntries(requestEntries);
+        } else {
+            resultHandler.completeExceptionally(
+                    retryableError instanceof Exception
+                            ? (Exception) retryableError
+                            : new FlinkRuntimeException(retryableError));
         }
     }
 
     private void handlePartiallyFailedRequest(
             List<Operation> requestEntries,
-            Consumer<List<Operation>> requestResult,
+            ResultHandler<Operation> resultHandler,
             BulkResponse response) {
         LOG.debug("The BulkRequest has failed partially. Response: {}", 
response);
         ArrayList<Operation> failedItems = new ArrayList<>();
@@ -192,16 +199,16 @@ public class Elasticsearch8AsyncWriter<InputT> extends 
AsyncSinkWriter<InputT, O
                 requestEntries.size(),
                 failedItems.size(),
                 response.took());
-        requestResult.accept(failedItems);
+        resultHandler.retryForEntries(failedItems);
     }
 
     private void handleSuccessfulRequest(
-            Consumer<List<Operation>> requestResult, BulkResponse response) {
+            ResultHandler<Operation> resultHandler, BulkResponse response) {
         LOG.debug(
                 "The BulkRequest of {} operation(s) completed successfully. It 
took {}ms",
                 response.items().size(),
                 response.took());
-        requestResult.accept(Collections.emptyList());
+        resultHandler.complete();
     }
 
     private boolean isRetryable(Throwable error) {
diff --git a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml
index 2619583c3..c97125726 100644
--- a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml
+++ b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/pom.xml
@@ -205,6 +205,12 @@ limitations under the License.
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.apache.flink</groupId>
+            <artifactId>flink-cdc-pipeline-connector-elasticsearch</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.flink</groupId>
             <artifactId>flink-connector-postgres-cdc</artifactId>
@@ -271,6 +277,12 @@ limitations under the License.
             <version>${testcontainers.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.testcontainers</groupId>
+            <artifactId>elasticsearch</artifactId>
+            <version>${testcontainers.version}</version>
+            <scope>test</scope>
+        </dependency>
 
         <!-- benchmark -->
         <dependency>
@@ -763,6 +775,15 @@ limitations under the License.
                             
<outputDirectory>${project.build.directory}/dependencies
                             </outputDirectory>
                         </artifactItem>
+                        <artifactItem>
+                            <groupId>org.apache.flink</groupId>
+                            
<artifactId>flink-cdc-pipeline-connector-elasticsearch</artifactId>
+                            <version>${project.version}</version>
+                            
<destFileName>elasticsearch-cdc-pipeline-connector.jar</destFileName>
+                            <type>jar</type>
+                            
<outputDirectory>${project.build.directory}/dependencies
+                            </outputDirectory>
+                        </artifactItem>
                         <artifactItem>
                             <groupId>org.apache.flink</groupId>
                             <artifactId>flink-parquet</artifactId>
diff --git 
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch6E2eITCase.java
 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch6E2eITCase.java
new file mode 100644
index 000000000..77a05c524
--- /dev/null
+++ 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch6E2eITCase.java
@@ -0,0 +1,329 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.pipeline.tests.stage2;
+
+import org.apache.flink.cdc.common.test.utils.TestUtils;
+import org.apache.flink.cdc.connectors.mysql.testutils.UniqueDatabase;
+import org.apache.flink.cdc.pipeline.tests.utils.PipelineTestEnvironment;
+import org.apache.flink.elasticsearch6.shaded.org.apache.http.HttpHost;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.action.get.GetRequest;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.action.get.GetResponse;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.client.RequestOptions;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.client.RestClient;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.client.RestHighLevelClient;
+import 
org.apache.flink.elasticsearch6.shaded.org.elasticsearch.common.xcontent.XContentType;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.lifecycle.Startables;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.time.Duration;
+import java.util.stream.Stream;
+
+@ParameterizedClass
+@ValueSource(ints = {1, 4})
+class MysqlToElasticsearch6E2eITCase extends PipelineTestEnvironment {
+    private static final Logger LOG = 
LoggerFactory.getLogger(MysqlToElasticsearch6E2eITCase.class);
+
+    protected final UniqueDatabase inventoryDatabase =
+            new UniqueDatabase(MYSQL, "mysql_inventory", MYSQL_TEST_USER, 
MYSQL_TEST_PASSWORD);
+
+    private static final String ELASTICSEARCH_VERSION = "6.8.20";
+    private static final String INTER_CONTAINER_ES_ALIAS = "elasticsearch";
+
+    private RestHighLevelClient client;
+
+    @Container
+    private static final ElasticsearchContainer ELASTICSEARCH_CONTAINER =
+            createElasticsearchContainer();
+
+    MysqlToElasticsearch6E2eITCase(int parallelism) {
+        super(parallelism);
+    }
+
+    @BeforeAll
+    static void initializeContainers() {
+        LOG.info("Starting containers...");
+        Startables.deepStart(Stream.of(MYSQL, ELASTICSEARCH_CONTAINER)).join();
+        LOG.info("Containers are started.");
+    }
+
+    @BeforeEach
+    public void before() throws Exception {
+        super.before();
+        inventoryDatabase.createAndInitialize();
+        client = createElasticsearchClient();
+        initEsData();
+    }
+
+    @AfterEach
+    public void after() {
+        super.after();
+        inventoryDatabase.dropDatabase();
+        if (client != null) {
+            try {
+                client.close();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    @Test
+    void testSyncWholeDatabase() throws Exception {
+        String databaseName = inventoryDatabase.getDatabaseName();
+        String pipelineJob =
+                String.format(
+                        "source:\n"
+                                + "  type: mysql\n"
+                                + "  hostname: %s\n"
+                                + "  port: 3306\n"
+                                + "  username: %s\n"
+                                + "  password: %s\n"
+                                + "  tables: %s.\\.*\n"
+                                + "  server-id: 5400-5404\n"
+                                + "  server-time-zone: UTC\n"
+                                + "\n"
+                                + "sink:\n"
+                                + "  type: elasticsearch\n"
+                                + "  hosts: %s:9200\n"
+                                + "  version: 6\n"
+                                + "\n"
+                                + "pipeline:\n"
+                                + "  parallelism: %d",
+                        INTER_CONTAINER_MYSQL_ALIAS,
+                        MYSQL_TEST_USER,
+                        MYSQL_TEST_PASSWORD,
+                        databaseName,
+                        INTER_CONTAINER_ES_ALIAS,
+                        parallelism);
+        Path esConnectorJar = 
TestUtils.getResource("elasticsearch-cdc-pipeline-connector.jar");
+        submitPipelineJob(pipelineJob, esConnectorJar);
+        waitUntilJobRunning(Duration.ofSeconds(30));
+        LOG.info("Pipeline job is running");
+
+        verifySnapshotData(databaseName);
+        verifyIncrementalData(databaseName);
+    }
+
+    private void verifySnapshotData(String databaseName) throws Exception {
+        String productsIndex = databaseName + ".products";
+        String customersIndex = databaseName + ".customers";
+
+        // products id=101 (all fields populated)
+        waitForEsDocument(productsIndex, "101");
+        GetResponse resp =
+                client.get(new GetRequest(productsIndex).id("101"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "scooter")
+                .containsEntry("description", "Small 2-wheel scooter")
+                .containsEntry("weight", 3.14)
+                .containsEntry("enum_c", "red")
+                .containsEntry("json_c", "{\"key1\": \"value1\"}")
+                .containsEntry("point_c", 
"{\"coordinates\":[1,1],\"type\":\"Point\",\"srid\":0}");
+
+        // products id=106 (enum_c, json_c, point_c are null)
+        waitForEsDocument(productsIndex, "106");
+        resp = client.get(new GetRequest(productsIndex).id("106"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "hammer")
+                .containsEntry("description", "16oz carpenter's hammer")
+                .containsEntry("weight", 1.0);
+
+        // products id=109 (last snapshot row)
+        waitForEsDocument(productsIndex, "109");
+        resp = client.get(new GetRequest(productsIndex).id("109"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "spare tire")
+                .containsEntry("description", "24 inch spare tire")
+                .containsEntry("weight", 22.2);
+
+        // customers id=101
+        waitForEsDocument(customersIndex, "101");
+        resp = client.get(new GetRequest(customersIndex).id("101"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "user_1")
+                .containsEntry("address", "Shanghai")
+                .containsEntry("phone_number", "123567891234");
+    }
+
+    private void verifyIncrementalData(String databaseName) throws Exception {
+        LOG.info("Begin incremental reading stage.");
+        String productsIndex = databaseName + ".products";
+        String mysqlJdbcUrl =
+                String.format(
+                        "jdbc:mysql://%s:%s/%s",
+                        MYSQL.getHost(), MYSQL.getDatabasePort(), 
databaseName);
+        try (Connection conn =
+                        DriverManager.getConnection(
+                                mysqlJdbcUrl, MYSQL_TEST_USER, 
MYSQL_TEST_PASSWORD);
+                Statement stat = conn.createStatement()) {
+
+            // INSERT
+            stat.execute(
+                    "INSERT INTO products VALUES "
+                            + "(default,'jacket','water resistent white wind 
breaker',0.2, null, null, null);");
+            waitForEsDocument(productsIndex, "110");
+            GetResponse resp =
+                    client.get(new GetRequest(productsIndex).id("110"), 
RequestOptions.DEFAULT);
+            Assertions.assertThat(resp.getSource())
+                    .containsEntry("name", "jacket")
+                    .containsEntry("description", "water resistent white wind 
breaker")
+                    .containsEntry("weight", 0.2);
+
+            // UPDATE
+            stat.execute("UPDATE products SET description='18oz carpenter 
hammer' WHERE id=106;");
+            stat.execute("UPDATE products SET weight='5.1' WHERE id=107;");
+            waitForEsDocumentField(productsIndex, "106", "description", "18oz 
carpenter hammer");
+            waitForEsDocumentField(productsIndex, "107", "weight", 5.1);
+
+            // DELETE
+            stat.execute("DELETE FROM products WHERE id=101;");
+            waitForEsDocumentDeleted(productsIndex, "101");
+        }
+    }
+
+    private static ElasticsearchContainer createElasticsearchContainer() {
+        DockerImageName imageName =
+                DockerImageName.parse(
+                                
"docker.elastic.co/elasticsearch/elasticsearch:"
+                                        + ELASTICSEARCH_VERSION)
+                        
.asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch");
+        ElasticsearchContainer esContainer = new 
ElasticsearchContainer(imageName);
+        esContainer
+                .withNetwork(NETWORK)
+                .withNetworkAliases(INTER_CONTAINER_ES_ALIAS)
+                .withEnv("xpack.security.enabled", "false")
+                .withEnv("ES_JAVA_OPTS", "-Xms2g -Xmx2g")
+                .withLogConsumer(new Slf4jLogConsumer(LOG));
+        return esContainer;
+    }
+
+    private RestHighLevelClient createElasticsearchClient() {
+        return new RestHighLevelClient(
+                RestClient.builder(
+                        new HttpHost(
+                                ELASTICSEARCH_CONTAINER.getHost(),
+                                ELASTICSEARCH_CONTAINER.getFirstMappedPort(),
+                                "http")));
+    }
+
+    private void initEsData() throws IOException {
+        String dbName = inventoryDatabase.getDatabaseName();
+        createProductsIndex(dbName);
+        createCustomersIndex(dbName);
+    }
+
+    private void createProductsIndex(String dbName) throws IOException {
+        String indexName = dbName + ".products";
+        String source =
+                "{\"mappings\":{\"_doc\":{\"properties\":{"
+                        + "\"id\":{\"type\":\"integer\"},"
+                        + "\"name\":{\"type\":\"keyword\"},"
+                        + "\"description\":{\"type\":\"text\"},"
+                        + "\"weight\":{\"type\":\"float\"},"
+                        + "\"enum_c\":{\"type\":\"keyword\"},"
+                        + "\"json_c\":{\"type\":\"keyword\"},"
+                        + "\"point_c\":{\"type\":\"keyword\"}"
+                        + "}}}}";
+        client.indices()
+                .create(
+                        new CreateIndexRequest(indexName).source(source, 
XContentType.JSON),
+                        RequestOptions.DEFAULT);
+    }
+
+    private void createCustomersIndex(String dbName) throws IOException {
+        String indexName = dbName + ".customers";
+        String source =
+                "{\"mappings\":{\"_doc\":{\"properties\":{"
+                        + "\"id\":{\"type\":\"integer\"},"
+                        + "\"name\":{\"type\":\"keyword\"},"
+                        + "\"address\":{\"type\":\"text\"},"
+                        + "\"phone_number\":{\"type\":\"keyword\"}"
+                        + "}}}}";
+        client.indices()
+                .create(
+                        new CreateIndexRequest(indexName).source(source, 
XContentType.JSON),
+                        RequestOptions.DEFAULT);
+    }
+
+    private void waitForEsDocument(String indexName, String docId) throws 
Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetRequest getRequest = new GetRequest(indexName).id(docId);
+            GetResponse response = client.get(getRequest, 
RequestOptions.DEFAULT);
+            if (response.isExists()) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail("Timed out waiting for ES document: " + indexName + 
"/" + docId);
+    }
+
+    private void waitForEsDocumentField(
+            String indexName, String docId, String field, Object 
expectedValue) throws Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetRequest getRequest = new GetRequest(indexName).id(docId);
+            GetResponse response = client.get(getRequest, 
RequestOptions.DEFAULT);
+            if (response.isExists() && 
expectedValue.equals(response.getSource().get(field))) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail(
+                "Timed out waiting for ES document field: "
+                        + indexName
+                        + "/"
+                        + docId
+                        + " "
+                        + field
+                        + "="
+                        + expectedValue);
+    }
+
+    private void waitForEsDocumentDeleted(String indexName, String docId) 
throws Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetRequest getRequest = new GetRequest(indexName).id(docId);
+            GetResponse response = client.get(getRequest, 
RequestOptions.DEFAULT);
+            if (!response.isExists()) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail("Document was not deleted within timeout: " + 
indexName + "/" + docId);
+    }
+}
diff --git 
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch7E2eITCase.java
 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch7E2eITCase.java
new file mode 100644
index 000000000..78b46894c
--- /dev/null
+++ 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch7E2eITCase.java
@@ -0,0 +1,329 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.pipeline.tests.stage2;
+
+import org.apache.flink.cdc.common.test.utils.TestUtils;
+import org.apache.flink.cdc.connectors.mysql.testutils.UniqueDatabase;
+import org.apache.flink.cdc.pipeline.tests.utils.PipelineTestEnvironment;
+import org.apache.flink.elasticsearch7.shaded.org.apache.http.HttpHost;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.action.get.GetRequest;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.action.get.GetResponse;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.client.RequestOptions;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.client.RestClient;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.client.RestHighLevelClient;
+import 
org.apache.flink.elasticsearch7.shaded.org.elasticsearch.common.xcontent.XContentType;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.lifecycle.Startables;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.time.Duration;
+import java.util.stream.Stream;
+
+@ParameterizedClass
+@ValueSource(ints = {1, 4})
+class MysqlToElasticsearch7E2eITCase extends PipelineTestEnvironment {
+    private static final Logger LOG = 
LoggerFactory.getLogger(MysqlToElasticsearch7E2eITCase.class);
+
+    protected final UniqueDatabase inventoryDatabase =
+            new UniqueDatabase(MYSQL, "mysql_inventory", MYSQL_TEST_USER, 
MYSQL_TEST_PASSWORD);
+
+    private static final String ELASTICSEARCH_VERSION = "7.10.2";
+    private static final String INTER_CONTAINER_ES_ALIAS = "elasticsearch7";
+
+    private RestHighLevelClient client;
+
+    @Container
+    private static final ElasticsearchContainer ELASTICSEARCH_CONTAINER =
+            createElasticsearchContainer();
+
+    MysqlToElasticsearch7E2eITCase(int parallelism) {
+        super(parallelism);
+    }
+
+    @BeforeAll
+    static void initializeContainers() {
+        LOG.info("Starting containers...");
+        Startables.deepStart(Stream.of(MYSQL, ELASTICSEARCH_CONTAINER)).join();
+        LOG.info("Containers are started.");
+    }
+
+    @BeforeEach
+    public void before() throws Exception {
+        super.before();
+        inventoryDatabase.createAndInitialize();
+        client = createElasticsearchClient();
+        initEsData();
+    }
+
+    @AfterEach
+    public void after() {
+        super.after();
+        inventoryDatabase.dropDatabase();
+        if (client != null) {
+            try {
+                client.close();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    @Test
+    void testSyncWholeDatabase() throws Exception {
+        String databaseName = inventoryDatabase.getDatabaseName();
+        String pipelineJob =
+                String.format(
+                        "source:\n"
+                                + "  type: mysql\n"
+                                + "  hostname: %s\n"
+                                + "  port: 3306\n"
+                                + "  username: %s\n"
+                                + "  password: %s\n"
+                                + "  tables: %s.\\.*\n"
+                                + "  server-id: 5400-5404\n"
+                                + "  server-time-zone: UTC\n"
+                                + "\n"
+                                + "sink:\n"
+                                + "  type: elasticsearch\n"
+                                + "  hosts: %s:9200\n"
+                                + "  version: 7\n"
+                                + "\n"
+                                + "pipeline:\n"
+                                + "  parallelism: %d",
+                        INTER_CONTAINER_MYSQL_ALIAS,
+                        MYSQL_TEST_USER,
+                        MYSQL_TEST_PASSWORD,
+                        databaseName,
+                        INTER_CONTAINER_ES_ALIAS,
+                        parallelism);
+        Path esConnectorJar = 
TestUtils.getResource("elasticsearch-cdc-pipeline-connector.jar");
+        submitPipelineJob(pipelineJob, esConnectorJar);
+        waitUntilJobRunning(Duration.ofSeconds(30));
+        LOG.info("Pipeline job is running");
+
+        verifySnapshotData(databaseName);
+        verifyIncrementalData(databaseName);
+    }
+
+    private void verifySnapshotData(String databaseName) throws Exception {
+        String productsIndex = databaseName + ".products";
+        String customersIndex = databaseName + ".customers";
+
+        // products id=101 (all fields populated)
+        waitForEsDocument(productsIndex, "101");
+        GetResponse resp =
+                client.get(new GetRequest(productsIndex).id("101"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "scooter")
+                .containsEntry("description", "Small 2-wheel scooter")
+                .containsEntry("weight", 3.14)
+                .containsEntry("enum_c", "red")
+                .containsEntry("json_c", "{\"key1\": \"value1\"}")
+                .containsEntry("point_c", 
"{\"coordinates\":[1,1],\"type\":\"Point\",\"srid\":0}");
+
+        // products id=106 (enum_c, json_c, point_c are null)
+        waitForEsDocument(productsIndex, "106");
+        resp = client.get(new GetRequest(productsIndex).id("106"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "hammer")
+                .containsEntry("description", "16oz carpenter's hammer")
+                .containsEntry("weight", 1.0);
+
+        // products id=109 (last snapshot row)
+        waitForEsDocument(productsIndex, "109");
+        resp = client.get(new GetRequest(productsIndex).id("109"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "spare tire")
+                .containsEntry("description", "24 inch spare tire")
+                .containsEntry("weight", 22.2);
+
+        // customers id=101
+        waitForEsDocument(customersIndex, "101");
+        resp = client.get(new GetRequest(customersIndex).id("101"), 
RequestOptions.DEFAULT);
+        Assertions.assertThat(resp.getSource())
+                .containsEntry("name", "user_1")
+                .containsEntry("address", "Shanghai")
+                .containsEntry("phone_number", "123567891234");
+    }
+
+    private void verifyIncrementalData(String databaseName) throws Exception {
+        LOG.info("Begin incremental reading stage.");
+        String productsIndex = databaseName + ".products";
+        String mysqlJdbcUrl =
+                String.format(
+                        "jdbc:mysql://%s:%s/%s",
+                        MYSQL.getHost(), MYSQL.getDatabasePort(), 
databaseName);
+        try (Connection conn =
+                        DriverManager.getConnection(
+                                mysqlJdbcUrl, MYSQL_TEST_USER, 
MYSQL_TEST_PASSWORD);
+                Statement stat = conn.createStatement()) {
+
+            // INSERT
+            stat.execute(
+                    "INSERT INTO products VALUES "
+                            + "(default,'jacket','water resistent white wind 
breaker',0.2, null, null, null);");
+            waitForEsDocument(productsIndex, "110");
+            GetResponse resp =
+                    client.get(new GetRequest(productsIndex).id("110"), 
RequestOptions.DEFAULT);
+            Assertions.assertThat(resp.getSource())
+                    .containsEntry("name", "jacket")
+                    .containsEntry("description", "water resistent white wind 
breaker")
+                    .containsEntry("weight", 0.2);
+
+            // UPDATE
+            stat.execute("UPDATE products SET description='18oz carpenter 
hammer' WHERE id=106;");
+            stat.execute("UPDATE products SET weight='5.1' WHERE id=107;");
+            waitForEsDocumentField(productsIndex, "106", "description", "18oz 
carpenter hammer");
+            waitForEsDocumentField(productsIndex, "107", "weight", 5.1);
+
+            // DELETE
+            stat.execute("DELETE FROM products WHERE id=101;");
+            waitForEsDocumentDeleted(productsIndex, "101");
+        }
+    }
+
+    private static ElasticsearchContainer createElasticsearchContainer() {
+        DockerImageName imageName =
+                DockerImageName.parse(
+                                
"docker.elastic.co/elasticsearch/elasticsearch:"
+                                        + ELASTICSEARCH_VERSION)
+                        
.asCompatibleSubstituteFor("docker.elastic.co/elasticsearch/elasticsearch");
+        ElasticsearchContainer esContainer = new 
ElasticsearchContainer(imageName);
+        esContainer
+                .withNetwork(NETWORK)
+                .withNetworkAliases(INTER_CONTAINER_ES_ALIAS)
+                .withEnv("xpack.security.enabled", "false")
+                .withEnv("ES_JAVA_OPTS", "-Xms2g -Xmx2g")
+                .withLogConsumer(new Slf4jLogConsumer(LOG));
+        return esContainer;
+    }
+
+    private RestHighLevelClient createElasticsearchClient() {
+        return new RestHighLevelClient(
+                RestClient.builder(
+                        new HttpHost(
+                                ELASTICSEARCH_CONTAINER.getHost(),
+                                ELASTICSEARCH_CONTAINER.getFirstMappedPort(),
+                                "http")));
+    }
+
+    private void initEsData() throws IOException {
+        String dbName = inventoryDatabase.getDatabaseName();
+        createProductsIndex(dbName);
+        createCustomersIndex(dbName);
+    }
+
+    private void createProductsIndex(String dbName) throws IOException {
+        String indexName = dbName + ".products";
+        String source =
+                "{\"mappings\":{\"_doc\":{\"properties\":{"
+                        + "\"id\":{\"type\":\"integer\"},"
+                        + "\"name\":{\"type\":\"keyword\"},"
+                        + "\"description\":{\"type\":\"text\"},"
+                        + "\"weight\":{\"type\":\"float\"},"
+                        + "\"enum_c\":{\"type\":\"keyword\"},"
+                        + "\"json_c\":{\"type\":\"keyword\"},"
+                        + "\"point_c\":{\"type\":\"keyword\"}"
+                        + "}}}}";
+        client.indices()
+                .create(
+                        new CreateIndexRequest(indexName).source(source, 
XContentType.JSON),
+                        RequestOptions.DEFAULT);
+    }
+
+    private void createCustomersIndex(String dbName) throws IOException {
+        String indexName = dbName + ".customers";
+        String source =
+                "{\"mappings\":{\"_doc\":{\"properties\":{"
+                        + "\"id\":{\"type\":\"integer\"},"
+                        + "\"name\":{\"type\":\"keyword\"},"
+                        + "\"address\":{\"type\":\"text\"},"
+                        + "\"phone_number\":{\"type\":\"keyword\"}"
+                        + "}}}}";
+        client.indices()
+                .create(
+                        new CreateIndexRequest(indexName).source(source, 
XContentType.JSON),
+                        RequestOptions.DEFAULT);
+    }
+
+    private void waitForEsDocument(String indexName, String docId) throws 
Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetRequest getRequest = new GetRequest(indexName).id(docId);
+            GetResponse response = client.get(getRequest, 
RequestOptions.DEFAULT);
+            if (response.isExists()) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail("Timed out waiting for ES document: " + indexName + 
"/" + docId);
+    }
+
+    private void waitForEsDocumentField(
+            String indexName, String docId, String field, Object 
expectedValue) throws Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetRequest getRequest = new GetRequest(indexName).id(docId);
+            GetResponse response = client.get(getRequest, 
RequestOptions.DEFAULT);
+            if (response.isExists() && 
expectedValue.equals(response.getSource().get(field))) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail(
+                "Timed out waiting for ES document field: "
+                        + indexName
+                        + "/"
+                        + docId
+                        + " "
+                        + field
+                        + "="
+                        + expectedValue);
+    }
+
+    private void waitForEsDocumentDeleted(String indexName, String docId) 
throws Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetRequest getRequest = new GetRequest(indexName).id(docId);
+            GetResponse response = client.get(getRequest, 
RequestOptions.DEFAULT);
+            if (!response.isExists()) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail("Document was not deleted within timeout: " + 
indexName + "/" + docId);
+    }
+}
diff --git 
a/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch8E2eITCase.java
 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch8E2eITCase.java
new file mode 100644
index 000000000..fb96c6d7f
--- /dev/null
+++ 
b/flink-cdc-e2e-tests/flink-cdc-pipeline-e2e-tests/src/test/java/org/apache/flink/cdc/pipeline/tests/stage2/MysqlToElasticsearch8E2eITCase.java
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.flink.cdc.pipeline.tests.stage2;
+
+import org.apache.flink.cdc.common.test.utils.TestUtils;
+import org.apache.flink.cdc.connectors.mysql.testutils.UniqueDatabase;
+import org.apache.flink.cdc.pipeline.tests.utils.PipelineTestEnvironment;
+
+import co.elastic.clients.elasticsearch.ElasticsearchClient;
+import co.elastic.clients.elasticsearch.core.GetRequest;
+import co.elastic.clients.elasticsearch.core.GetResponse;
+import co.elastic.clients.json.jackson.JacksonJsonpMapper;
+import co.elastic.clients.transport.rest_client.RestClientTransport;
+import org.apache.http.HttpHost;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.assertj.core.api.Assertions;
+import org.elasticsearch.client.RestClient;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedClass;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.output.Slf4jLogConsumer;
+import org.testcontainers.elasticsearch.ElasticsearchContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.lifecycle.Startables;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.Statement;
+import java.time.Duration;
+import java.util.Map;
+import java.util.stream.Stream;
+
+@ParameterizedClass
+@ValueSource(ints = {1, 4})
+class MysqlToElasticsearch8E2eITCase extends PipelineTestEnvironment {
+    private static final Logger LOG = 
LoggerFactory.getLogger(MysqlToElasticsearch8E2eITCase.class);
+
+    protected final UniqueDatabase inventoryDatabase =
+            new UniqueDatabase(MYSQL, "mysql_inventory", MYSQL_TEST_USER, 
MYSQL_TEST_PASSWORD);
+
+    private static final String ELASTICSEARCH_VERSION = "8.12.1";
+    private static final String INTER_CONTAINER_ES_ALIAS = "elasticsearch8";
+    private static final String DEFAULT_USERNAME = "elastic";
+    private static final String DEFAULT_PASSWORD = "123456";
+
+    private ElasticsearchClient client;
+
+    @Container
+    private static final ElasticsearchContainer ELASTICSEARCH_CONTAINER =
+            createElasticsearchContainer();
+
+    MysqlToElasticsearch8E2eITCase(int parallelism) {
+        super(parallelism);
+    }
+
+    @BeforeAll
+    static void initializeContainers() {
+        LOG.info("Starting containers...");
+        Startables.deepStart(Stream.of(MYSQL, ELASTICSEARCH_CONTAINER)).join();
+        LOG.info("Containers are started.");
+    }
+
+    @BeforeEach
+    public void before() throws Exception {
+        super.before();
+        inventoryDatabase.createAndInitialize();
+        client = createElasticsearchClient();
+        initEsData();
+    }
+
+    @AfterEach
+    public void after() {
+        super.after();
+        inventoryDatabase.dropDatabase();
+        if (client != null) {
+            try {
+                client.shutdown();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    @Test
+    void testSyncWholeDatabase() throws Exception {
+        String databaseName = inventoryDatabase.getDatabaseName();
+        String pipelineJob =
+                String.format(
+                        "source:\n"
+                                + "  type: mysql\n"
+                                + "  hostname: %s\n"
+                                + "  port: 3306\n"
+                                + "  username: %s\n"
+                                + "  password: %s\n"
+                                + "  tables: %s.\\.*\n"
+                                + "  server-id: 5400-5404\n"
+                                + "  server-time-zone: UTC\n"
+                                + "\n"
+                                + "sink:\n"
+                                + "  type: elasticsearch\n"
+                                + "  hosts: %s:9200\n"
+                                + "  version: 8\n"
+                                + "  username: %s\n"
+                                + "  password: %s\n"
+                                + "\n"
+                                + "pipeline:\n"
+                                + "  parallelism: %d",
+                        INTER_CONTAINER_MYSQL_ALIAS,
+                        MYSQL_TEST_USER,
+                        MYSQL_TEST_PASSWORD,
+                        databaseName,
+                        INTER_CONTAINER_ES_ALIAS,
+                        DEFAULT_USERNAME,
+                        DEFAULT_PASSWORD,
+                        parallelism);
+        Path esConnectorJar = 
TestUtils.getResource("elasticsearch-cdc-pipeline-connector.jar");
+        submitPipelineJob(pipelineJob, esConnectorJar);
+        waitUntilJobRunning(Duration.ofSeconds(30));
+        LOG.info("Pipeline job is running");
+
+        verifySnapshotData(databaseName);
+        verifyIncrementalData(databaseName);
+    }
+
+    private void verifySnapshotData(String databaseName) throws Exception {
+        String productsIndex = databaseName + ".products";
+        String customersIndex = databaseName + ".customers";
+
+        // products id=101 (all fields populated)
+        waitForEsDocument(productsIndex, "101");
+        GetResponse<Map> resp =
+                client.get(
+                        new 
GetRequest.Builder().index(productsIndex).id("101").build(), Map.class);
+        Assertions.assertThat(resp.source())
+                .containsEntry("name", "scooter")
+                .containsEntry("description", "Small 2-wheel scooter")
+                .containsEntry("weight", 3.14)
+                .containsEntry("enum_c", "red")
+                .containsEntry("json_c", "{\"key1\": \"value1\"}")
+                .containsEntry("point_c", 
"{\"coordinates\":[1,1],\"type\":\"Point\",\"srid\":0}");
+
+        // products id=106 (enum_c, json_c, point_c are null)
+        waitForEsDocument(productsIndex, "106");
+        resp =
+                client.get(
+                        new 
GetRequest.Builder().index(productsIndex).id("106").build(), Map.class);
+        Assertions.assertThat(resp.source())
+                .containsEntry("name", "hammer")
+                .containsEntry("description", "16oz carpenter's hammer")
+                .containsEntry("weight", 1.0);
+
+        // products id=109 (last snapshot row)
+        waitForEsDocument(productsIndex, "109");
+        resp =
+                client.get(
+                        new 
GetRequest.Builder().index(productsIndex).id("109").build(), Map.class);
+        Assertions.assertThat(resp.source())
+                .containsEntry("name", "spare tire")
+                .containsEntry("description", "24 inch spare tire")
+                .containsEntry("weight", 22.2);
+
+        // customers id=101
+        waitForEsDocument(customersIndex, "101");
+        resp =
+                client.get(
+                        new 
GetRequest.Builder().index(customersIndex).id("101").build(),
+                        Map.class);
+        Assertions.assertThat(resp.source())
+                .containsEntry("name", "user_1")
+                .containsEntry("address", "Shanghai")
+                .containsEntry("phone_number", "123567891234");
+    }
+
+    private void verifyIncrementalData(String databaseName) throws Exception {
+        LOG.info("Begin incremental reading stage.");
+        String productsIndex = databaseName + ".products";
+        String mysqlJdbcUrl =
+                String.format(
+                        "jdbc:mysql://%s:%s/%s",
+                        MYSQL.getHost(), MYSQL.getDatabasePort(), 
databaseName);
+        try (Connection conn =
+                        DriverManager.getConnection(
+                                mysqlJdbcUrl, MYSQL_TEST_USER, 
MYSQL_TEST_PASSWORD);
+                Statement stat = conn.createStatement()) {
+
+            // INSERT
+            stat.execute(
+                    "INSERT INTO products VALUES "
+                            + "(default,'jacket','water resistent white wind 
breaker',0.2, null, null, null);");
+            waitForEsDocument(productsIndex, "110");
+            GetResponse<Map> resp =
+                    client.get(
+                            new 
GetRequest.Builder().index(productsIndex).id("110").build(),
+                            Map.class);
+            Assertions.assertThat(resp.source())
+                    .containsEntry("name", "jacket")
+                    .containsEntry("description", "water resistent white wind 
breaker")
+                    .containsEntry("weight", 0.2);
+
+            // UPDATE
+            stat.execute("UPDATE products SET description='18oz carpenter 
hammer' WHERE id=106;");
+            stat.execute("UPDATE products SET weight='5.1' WHERE id=107;");
+            waitForEsDocumentField(productsIndex, "106", "description", "18oz 
carpenter hammer");
+            waitForEsDocumentField(productsIndex, "107", "weight", 5.1);
+
+            // DELETE
+            stat.execute("DELETE FROM products WHERE id=101;");
+            waitForEsDocumentDeleted(productsIndex, "101");
+        }
+    }
+
+    private static ElasticsearchContainer createElasticsearchContainer() {
+        DockerImageName imageName =
+                DockerImageName.parse(
+                        "docker.elastic.co/elasticsearch/elasticsearch:" + 
ELASTICSEARCH_VERSION);
+        ElasticsearchContainer esContainer = new 
ElasticsearchContainer(imageName);
+        esContainer
+                .withNetwork(NETWORK)
+                .withNetworkAliases(INTER_CONTAINER_ES_ALIAS)
+                .withEnv("discovery.type", "single-node")
+                .withEnv("xpack.security.enabled", "true")
+                .withEnv("xpack.security.http.ssl.enabled", "false")
+                .withEnv("ES_JAVA_OPTS", "-Xms2g -Xmx2g")
+                .withPassword(DEFAULT_PASSWORD)
+                .withLogConsumer(new Slf4jLogConsumer(LOG));
+        return esContainer;
+    }
+
+    private ElasticsearchClient createElasticsearchClient() {
+        BasicCredentialsProvider credentialsProvider = new 
BasicCredentialsProvider();
+        credentialsProvider.setCredentials(
+                AuthScope.ANY, new 
UsernamePasswordCredentials(DEFAULT_USERNAME, DEFAULT_PASSWORD));
+        RestClientTransport transport =
+                new RestClientTransport(
+                        RestClient.builder(
+                                        new HttpHost(
+                                                
ELASTICSEARCH_CONTAINER.getHost(),
+                                                
ELASTICSEARCH_CONTAINER.getFirstMappedPort(),
+                                                "http"))
+                                .setHttpClientConfigCallback(
+                                        httpAsyncClientBuilder ->
+                                                httpAsyncClientBuilder
+                                                        
.setDefaultCredentialsProvider(
+                                                                
credentialsProvider))
+                                .build(),
+                        new JacksonJsonpMapper());
+        return new ElasticsearchClient(transport);
+    }
+
+    private void initEsData() throws IOException {
+        String dbName = inventoryDatabase.getDatabaseName();
+        createProductsIndex(dbName);
+        createCustomersIndex(dbName);
+    }
+
+    private void createProductsIndex(String dbName) throws IOException {
+        String indexName = dbName + ".products";
+        client.indices()
+                .create(
+                        c ->
+                                c.index(indexName)
+                                        .mappings(
+                                                m ->
+                                                        m.properties("id", p 
-> p.integer(i -> i))
+                                                                .properties(
+                                                                        "name",
+                                                                        p -> 
p.keyword(k -> k))
+                                                                .properties(
+                                                                        
"description",
+                                                                        p -> 
p.text(t -> t))
+                                                                .properties(
+                                                                        
"weight",
+                                                                        p -> 
p.float_(f -> f))
+                                                                .properties(
+                                                                        
"enum_c",
+                                                                        p -> 
p.keyword(k -> k))
+                                                                .properties(
+                                                                        
"json_c",
+                                                                        p -> 
p.keyword(k -> k))
+                                                                .properties(
+                                                                        
"point_c",
+                                                                        p -> 
p.keyword(k -> k))));
+    }
+
+    private void createCustomersIndex(String dbName) throws IOException {
+        String indexName = dbName + ".customers";
+        client.indices()
+                .create(
+                        c ->
+                                c.index(indexName)
+                                        .mappings(
+                                                m ->
+                                                        m.properties("id", p 
-> p.integer(i -> i))
+                                                                .properties(
+                                                                        "name",
+                                                                        p -> 
p.keyword(k -> k))
+                                                                .properties(
+                                                                        
"address",
+                                                                        p -> 
p.text(t -> t))
+                                                                .properties(
+                                                                        
"phone_number",
+                                                                        p -> 
p.keyword(k -> k))));
+    }
+
+    private void waitForEsDocument(String indexName, String docId) throws 
Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetResponse<Map> response =
+                    client.get(
+                            new 
GetRequest.Builder().index(indexName).id(docId).build(), Map.class);
+            if (response.source() != null) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail("Timed out waiting for ES document: " + indexName + 
"/" + docId);
+    }
+
+    private void waitForEsDocumentField(
+            String indexName, String docId, String field, Object 
expectedValue) throws Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetResponse<Map> response =
+                    client.get(
+                            new 
GetRequest.Builder().index(indexName).id(docId).build(), Map.class);
+            if (response.source() != null && 
expectedValue.equals(response.source().get(field))) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail(
+                "Timed out waiting for ES document field: "
+                        + indexName
+                        + "/"
+                        + docId
+                        + " "
+                        + field
+                        + "="
+                        + expectedValue);
+    }
+
+    private void waitForEsDocumentDeleted(String indexName, String docId) 
throws Exception {
+        long deadline = System.currentTimeMillis() + 
EVENT_WAITING_TIMEOUT.toMillis();
+        while (System.currentTimeMillis() < deadline) {
+            GetResponse<Map> response =
+                    client.get(
+                            new 
GetRequest.Builder().index(indexName).id(docId).build(), Map.class);
+            if (response.source() == null) {
+                return;
+            }
+            Thread.sleep(1000L);
+        }
+        Assertions.fail("Document was not deleted within timeout: " + 
indexName + "/" + docId);
+    }
+}
diff --git 
a/flink-cdc-flink1-compat/src/main/java/org/apache/flink/api/connector/sink2/StatefulSinkWriterAdapter.java
 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/api/connector/sink2/StatefulSinkWriterAdapter.java
new file mode 100644
index 000000000..70e8034e3
--- /dev/null
+++ 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/api/connector/sink2/StatefulSinkWriterAdapter.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.api.connector.sink2;
+
+/**
+ * Compatibility adapter for Flink 1.20. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public interface StatefulSinkWriterAdapter<InputT, WriterStateT>
+        extends StatefulSink.StatefulSinkWriter<InputT, WriterStateT> {}
diff --git 
a/flink-cdc-flink1-compat/src/main/java/org/apache/flink/api/connector/sink2/WriterInitContextAdapter.java
 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/api/connector/sink2/WriterInitContextAdapter.java
new file mode 100644
index 000000000..04d8cde07
--- /dev/null
+++ 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/api/connector/sink2/WriterInitContextAdapter.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.api.connector.sink2;
+
+import org.apache.flink.api.common.JobInfo;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.operators.MailboxExecutor;
+import org.apache.flink.api.common.operators.ProcessingTimeService;
+import org.apache.flink.api.common.serialization.SerializationSchema;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.metrics.groups.SinkWriterMetricGroup;
+import org.apache.flink.util.UserCodeClassLoader;
+
+import java.util.OptionalLong;
+
+/**
+ * Compatibility adapter for Flink 1.20. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public class WriterInitContextAdapter implements WriterInitContext {
+
+    private final Sink.InitContext context;
+
+    public WriterInitContextAdapter(Sink.InitContext context) {
+        this.context = context;
+    }
+
+    @Override
+    public UserCodeClassLoader getUserCodeClassLoader() {
+        return this.context.getUserCodeClassLoader();
+    }
+
+    @Override
+    public MailboxExecutor getMailboxExecutor() {
+        return this.context.getMailboxExecutor();
+    }
+
+    @Override
+    public ProcessingTimeService getProcessingTimeService() {
+        return this.context.getProcessingTimeService();
+    }
+
+    @Override
+    public SinkWriterMetricGroup metricGroup() {
+        return this.context.metricGroup();
+    }
+
+    @Override
+    public SerializationSchema.InitializationContext 
asSerializationSchemaInitializationContext() {
+        return this.context.asSerializationSchemaInitializationContext();
+    }
+
+    @Override
+    public boolean isObjectReuseEnabled() {
+        return this.context.isObjectReuseEnabled();
+    }
+
+    @Override
+    public <IN> TypeSerializer<IN> createInputSerializer() {
+        return this.context.createInputSerializer();
+    }
+
+    @Override
+    public OptionalLong getRestoredCheckpointId() {
+        return this.context.getRestoredCheckpointId();
+    }
+
+    @Override
+    public JobInfo getJobInfo() {
+        return this.context.getJobInfo();
+    }
+
+    @Override
+    public TaskInfo getTaskInfo() {
+        return this.context.getTaskInfo();
+    }
+}
diff --git 
a/flink-cdc-flink1-compat/src/main/java/org/apache/flink/connector/base/sink/AsyncSinkBaseAdapter.java
 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/connector/base/sink/AsyncSinkBaseAdapter.java
new file mode 100644
index 000000000..881a2ff39
--- /dev/null
+++ 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/connector/base/sink/AsyncSinkBaseAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.connector.base.sink;
+
+import org.apache.flink.api.connector.sink2.StatefulSinkWriterAdapter;
+import org.apache.flink.api.connector.sink2.WriterInitContext;
+import org.apache.flink.connector.base.sink.writer.BufferedRequestState;
+import org.apache.flink.connector.base.sink.writer.ElementConverter;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Compatibility adapter for Flink 1.20. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public abstract class AsyncSinkBaseAdapter<InputT, RequestEntryT extends 
Serializable>
+        extends AsyncSinkBase<InputT, RequestEntryT> {
+    protected AsyncSinkBaseAdapter(
+            ElementConverter<InputT, RequestEntryT> elementConverter,
+            int maxBatchSize,
+            int maxInFlightRequests,
+            int maxBufferedRequests,
+            long maxBatchSizeInBytes,
+            long maxTimeInBufferMS,
+            long maxRecordSizeInBytes) {
+        super(
+                elementConverter,
+                maxBatchSize,
+                maxInFlightRequests,
+                maxBufferedRequests,
+                maxBatchSizeInBytes,
+                maxTimeInBufferMS,
+                maxRecordSizeInBytes);
+    }
+
+    @Override
+    public StatefulSinkWriter<InputT, BufferedRequestState<RequestEntryT>> 
restoreWriter(
+            WriterInitContext context,
+            Collection<BufferedRequestState<RequestEntryT>> recoveredState)
+            throws IOException {
+        return restoreWriterAdapter(context, recoveredState);
+    }
+
+    public abstract StatefulSinkWriterAdapter<InputT, 
BufferedRequestState<RequestEntryT>>
+            restoreWriterAdapter(
+                    WriterInitContext context,
+                    Collection<BufferedRequestState<RequestEntryT>> 
recoveredState)
+                    throws IOException;
+}
diff --git 
a/flink-cdc-flink1-compat/src/main/java/org/apache/flink/connector/base/sink/writer/AsyncSinkWriterAdapter.java
 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/connector/base/sink/writer/AsyncSinkWriterAdapter.java
new file mode 100644
index 000000000..6d5b2d7e1
--- /dev/null
+++ 
b/flink-cdc-flink1-compat/src/main/java/org/apache/flink/connector/base/sink/writer/AsyncSinkWriterAdapter.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.connector.base.sink.writer;
+
+import org.apache.flink.api.connector.sink2.Sink;
+import 
org.apache.flink.connector.base.sink.writer.config.AsyncSinkWriterConfiguration;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Compatibility adapter for Flink 1.20. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public abstract class AsyncSinkWriterAdapter<InputT, RequestEntryT extends 
Serializable>
+        extends AsyncSinkWriter<InputT, RequestEntryT> {
+
+    public AsyncSinkWriterAdapter(
+            ElementConverter<InputT, RequestEntryT> elementConverter,
+            Sink.InitContext context,
+            AsyncSinkWriterConfiguration configuration,
+            Collection<BufferedRequestState<RequestEntryT>> 
bufferedRequestStates) {
+        super(elementConverter, context, configuration, bufferedRequestStates);
+    }
+}
diff --git 
a/flink-cdc-flink2-compat/src/main/java/org/apache/flink/api/connector/sink2/StatefulSinkWriterAdapter.java
 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/api/connector/sink2/StatefulSinkWriterAdapter.java
new file mode 100644
index 000000000..8d7fbcb33
--- /dev/null
+++ 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/api/connector/sink2/StatefulSinkWriterAdapter.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.api.connector.sink2;
+
+/**
+ * Compatibility adapter for Flink 2.2. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public interface StatefulSinkWriterAdapter<InputT, WriterStateT>
+        extends StatefulSinkWriter<InputT, WriterStateT> {}
diff --git 
a/flink-cdc-flink2-compat/src/main/java/org/apache/flink/api/connector/sink2/WriterInitContextAdapter.java
 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/api/connector/sink2/WriterInitContextAdapter.java
new file mode 100644
index 000000000..1aa5588e1
--- /dev/null
+++ 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/api/connector/sink2/WriterInitContextAdapter.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.api.connector.sink2;
+
+import org.apache.flink.api.common.JobInfo;
+import org.apache.flink.api.common.TaskInfo;
+import org.apache.flink.api.common.operators.MailboxExecutor;
+import org.apache.flink.api.common.operators.ProcessingTimeService;
+import org.apache.flink.api.common.serialization.SerializationSchema;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.metrics.groups.SinkWriterMetricGroup;
+import org.apache.flink.util.UserCodeClassLoader;
+
+import java.util.OptionalLong;
+
+/**
+ * Compatibility adapter for Flink 2.2. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public class WriterInitContextAdapter implements WriterInitContext {
+
+    private final Sink.InitContext context;
+
+    public WriterInitContextAdapter(Sink.InitContext context) {
+        this.context = context;
+    }
+
+    @Override
+    public UserCodeClassLoader getUserCodeClassLoader() {
+        return this.context.getUserCodeClassLoader();
+    }
+
+    @Override
+    public MailboxExecutor getMailboxExecutor() {
+        return this.context.getMailboxExecutor();
+    }
+
+    @Override
+    public ProcessingTimeService getProcessingTimeService() {
+        return this.context.getProcessingTimeService();
+    }
+
+    @Override
+    public SinkWriterMetricGroup metricGroup() {
+        return this.context.metricGroup();
+    }
+
+    @Override
+    public SerializationSchema.InitializationContext 
asSerializationSchemaInitializationContext() {
+        return this.context.asSerializationSchemaInitializationContext();
+    }
+
+    @Override
+    public boolean isObjectReuseEnabled() {
+        return this.context.isObjectReuseEnabled();
+    }
+
+    @Override
+    public <IN> TypeSerializer<IN> createInputSerializer() {
+        return this.context.createInputSerializer();
+    }
+
+    @Override
+    public OptionalLong getRestoredCheckpointId() {
+        return this.context.getRestoredCheckpointId();
+    }
+
+    @Override
+    public JobInfo getJobInfo() {
+        return this.context.getJobInfo();
+    }
+
+    @Override
+    public TaskInfo getTaskInfo() {
+        return this.context.getTaskInfo();
+    }
+}
diff --git 
a/flink-cdc-flink2-compat/src/main/java/org/apache/flink/connector/base/sink/AsyncSinkBaseAdapter.java
 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/connector/base/sink/AsyncSinkBaseAdapter.java
new file mode 100644
index 000000000..c4841ef0c
--- /dev/null
+++ 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/connector/base/sink/AsyncSinkBaseAdapter.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.connector.base.sink;
+
+import org.apache.flink.api.connector.sink2.StatefulSinkWriter;
+import org.apache.flink.api.connector.sink2.StatefulSinkWriterAdapter;
+import org.apache.flink.api.connector.sink2.WriterInitContext;
+import org.apache.flink.connector.base.sink.writer.BufferedRequestState;
+import org.apache.flink.connector.base.sink.writer.ElementConverter;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Compatibility adapter for Flink 2.2. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public abstract class AsyncSinkBaseAdapter<InputT, RequestEntryT extends 
Serializable>
+        extends AsyncSinkBase<InputT, RequestEntryT> {
+    protected AsyncSinkBaseAdapter(
+            ElementConverter<InputT, RequestEntryT> elementConverter,
+            int maxBatchSize,
+            int maxInFlightRequests,
+            int maxBufferedRequests,
+            long maxBatchSizeInBytes,
+            long maxTimeInBufferMS,
+            long maxRecordSizeInBytes) {
+        super(
+                elementConverter,
+                maxBatchSize,
+                maxInFlightRequests,
+                maxBufferedRequests,
+                maxBatchSizeInBytes,
+                maxTimeInBufferMS,
+                maxRecordSizeInBytes);
+    }
+
+    @Override
+    public StatefulSinkWriter<InputT, BufferedRequestState<RequestEntryT>> 
restoreWriter(
+            WriterInitContext context,
+            Collection<BufferedRequestState<RequestEntryT>> recoveredState)
+            throws IOException {
+        return restoreWriterAdapter(context, recoveredState);
+    }
+
+    public abstract StatefulSinkWriterAdapter<InputT, 
BufferedRequestState<RequestEntryT>>
+            restoreWriterAdapter(
+                    WriterInitContext context,
+                    Collection<BufferedRequestState<RequestEntryT>> 
recoveredState)
+                    throws IOException;
+}
diff --git 
a/flink-cdc-flink2-compat/src/main/java/org/apache/flink/connector/base/sink/writer/AsyncSinkWriterAdapter.java
 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/connector/base/sink/writer/AsyncSinkWriterAdapter.java
new file mode 100644
index 000000000..87f02eb1a
--- /dev/null
+++ 
b/flink-cdc-flink2-compat/src/main/java/org/apache/flink/connector/base/sink/writer/AsyncSinkWriterAdapter.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.flink.connector.base.sink.writer;
+
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.WriterInitContext;
+import org.apache.flink.api.connector.sink2.WriterInitContextAdapter;
+import 
org.apache.flink.connector.base.sink.writer.config.AsyncSinkWriterConfiguration;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Compatibility adapter for Flink 2.2. This class is part of the 
multi-version compatibility layer
+ * that allows Flink CDC to work across different Flink versions.
+ */
+public abstract class AsyncSinkWriterAdapter<InputT, RequestEntryT extends 
Serializable>
+        extends AsyncSinkWriter<InputT, RequestEntryT> {
+
+    public AsyncSinkWriterAdapter(
+            ElementConverter<InputT, RequestEntryT> elementConverter,
+            Sink.InitContext context,
+            AsyncSinkWriterConfiguration configuration,
+            Collection<BufferedRequestState<RequestEntryT>> 
bufferedRequestStates) {
+        super(
+                elementConverter,
+                new WriterInitContextAdapter(context),
+                configuration,
+                bufferedRequestStates);
+    }
+
+    public AsyncSinkWriterAdapter(
+            ElementConverter<InputT, RequestEntryT> elementConverter,
+            WriterInitContext context,
+            AsyncSinkWriterConfiguration configuration,
+            Collection<BufferedRequestState<RequestEntryT>> 
bufferedRequestStates,
+            BatchCreator<RequestEntryT> batchCreator,
+            RequestBuffer<RequestEntryT> bufferedRequestEntries) {
+        super(
+                elementConverter,
+                context,
+                configuration,
+                bufferedRequestStates,
+                batchCreator,
+                bufferedRequestEntries);
+    }
+}

Reply via email to