You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@flink.apache.org by re...@apache.org on 2024/04/24 04:11:10 UTC
(flink-cdc) branch master updated: [pipeline-connector][paimon] add paimon pipeline data sink connector. (#2916)
This is an automated email from the ASF dual-hosted git repository.
renqs 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 ef2eece25 [pipeline-connector][paimon] add paimon pipeline data sink connector. (#2916)
ef2eece25 is described below
commit ef2eece256f35f3d7928f2c4e3407c049f895909
Author: Kunni <lv...@alibaba-inc.com>
AuthorDate: Wed Apr 24 12:11:05 2024 +0800
[pipeline-connector][paimon] add paimon pipeline data sink connector. (#2916)
---
docs/content.zh/docs/connectors/paimon.md | 232 +++++++++
docs/content/docs/connectors/paimon.md | 232 +++++++++
.../cdc/common/types/utils/DataTypeUtils.java | 92 +++-
.../cdc/common/types/utils/DataTypeUtilsTest.java | 5 +-
.../flink-cdc-pipeline-connector-paimon/pom.xml | 284 +++++++++++
.../cdc/connectors/paimon/sink/PaimonDataSink.java | 72 +++
.../paimon/sink/PaimonDataSinkFactory.java | 126 +++++
.../paimon/sink/PaimonDataSinkOptions.java | 67 +++
.../paimon/sink/PaimonMetadataApplier.java | 203 ++++++++
.../v2/MultiTableCommittableChannelComputer.java | 59 +++
.../connectors/paimon/sink/v2/PaimonCommitter.java | 68 +++
.../cdc/connectors/paimon/sink/v2/PaimonEvent.java | 70 +++
.../sink/v2/PaimonRecordEventSerializer.java | 87 ++++
.../paimon/sink/v2/PaimonRecordSerializer.java | 26 ++
.../cdc/connectors/paimon/sink/v2/PaimonSink.java | 98 ++++
.../connectors/paimon/sink/v2/PaimonWriter.java | 197 ++++++++
.../paimon/sink/v2/PaimonWriterHelper.java | 171 +++++++
.../paimon/sink/v2/PreCommitOperator.java | 97 ++++
.../paimon/sink/v2/StoreSinkWriteImpl.java | 223 +++++++++
.../connectors/paimon/sink/v2/TableSchemaInfo.java | 45 ++
.../org.apache.flink.cdc.common.factories.Factory | 16 +
.../paimon/sink/PaimonDataSinkFactoryTest.java | 63 +++
.../paimon/sink/PaimonMetadataApplierTest.java | 336 +++++++++++++
.../paimon/sink/v2/PaimonSinkITCase.java | 519 +++++++++++++++++++++
.../paimon/sink/v2/PaimonWriterHelperTest.java | 175 +++++++
.../TestMultiTableCommittableChannelComputer.java | 72 +++
.../test/resources/hadoop-conf-dir/core-site.xml | 36 ++
.../src/test/resources/hive-conf-dir/hive-site.xml | 47 ++
.../src/test/resources/log4j2-test.properties | 25 +
.../flink-cdc-pipeline-connectors/pom.xml | 1 +
30 files changed, 3720 insertions(+), 24 deletions(-)
diff --git a/docs/content.zh/docs/connectors/paimon.md b/docs/content.zh/docs/connectors/paimon.md
new file mode 100644
index 000000000..26cec4806
--- /dev/null
+++ b/docs/content.zh/docs/connectors/paimon.md
@@ -0,0 +1,232 @@
+---
+title: "Paimon"
+weight: 6
+type: docs
+aliases:
+- /connectors/paimon
+---
+<!--
+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.
+-->
+
+# Paimon Pipeline 连接器
+
+Paimon Pipeline 连接器可以用作 Pipeline 的 *Data Sink*,将数据写入[Paimon](https://paimon.apache.org)。 本文档介绍如何设置 Paimon Pipeline 连接器。
+
+## 连接器的功能
+* 自动建表
+* 表结构变更同步
+* 数据实时同步
+
+如何创建 Pipeline
+----------------
+
+从 MySQL 读取数据同步到 Paimon 的 Pipeline 可以定义如下:
+
+```yaml
+source:
+ type: mysql
+ name: MySQL Source
+ hostname: 127.0.0.1
+ port: 3306
+ username: admin
+ password: pass
+ tables: adb.\.*, bdb.user_table_[0-9]+, [app|web].order_\.*
+ server-id: 5401-5404
+
+sink:
+ type: paimon
+ name: Paimon Sink
+ metastore: filesystem
+ warehouse: /path/warehouse
+
+pipeline:
+ name: MySQL to Paimon Pipeline
+ parallelism: 2
+```
+
+Pipeline 连接器配置项
+----------------
+<div class="highlight">
+<table class="colwidths-auto docutils">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 25%">Option</th>
+ <th class="text-left" style="width: 8%">Required</th>
+ <th class="text-left" style="width: 7%">Default</th>
+ <th class="text-left" style="width: 10%">Type</th>
+ <th class="text-left" style="width: 50%">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>type</td>
+ <td>required</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>指定要使用的连接器, 这里需要设置成 <code>'paimon'</code>.</td>
+ </tr>
+ <tr>
+ <td>name</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Sink 的名称.</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.metastore</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;"><code>"filesystem"</code></td>
+ <td>String</td>
+ <td>用于构建 Paimon Catalog 的类型。可选填值 filesystem 或者 hive。</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.warehouse</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Paimon 仓库存储数据的根目录。</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.uri</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Hive metastore 的 uri,在 metastore 设置为 hive 的时候需要。</td>
+ </tr>
+ <tr>
+ <td>commit.user</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;"><code>admin</code></td>
+ <td>String</td>
+ <td>提交数据文件时的用户名。</td>
+ </tr>
+ <tr>
+ <td>partition.key</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>设置每个分区表的分区字段,允许填写成多个分区表的多个分区字段。 不同的表使用 ';'分割, 而不同的字段则使用 ','分割。举个例子, 我们可以为两张表的不同分区字段作如下的设置 'testdb.table1:id1,id2;testdb.table2:name'。</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.*</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>将 Paimon catalog 支持的参数传递给 pipeline,参考 <a href="https://paimon.apache.org/docs/master/maintenance/configurations/#catalogoptions">Paimon catalog options</a>。 </td>
+ </tr>
+ <tr>
+ <td>table.properties.*</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>将 Paimon table 支持的参数传递给 pipeline,参考 <a href="https://paimon.apache.org/docs/master/maintenance/configurations/#coreoptions">Paimon table options</a>。 </td>
+ </tr>
+ </tbody>
+</table>
+</div>
+
+使用说明
+--------
+
+* 只支持主键表,因此源表必须有主键
+
+* 暂不支持 exactly-once,连接器 通过 at-least-once 和主键表实现幂等写
+
+数据类型映射
+----------------
+<div class="wy-table-responsive">
+<table class="colwidths-auto docutils">
+ <thead>
+ <tr>
+ <th class="text-left">CDC type</th>
+ <th class="text-left">Paimon type</th>
+ <th class="text-left" style="width:60%;">NOTE</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>TINYINT</td>
+ <td>TINYINT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>SMALLINT</td>
+ <td>SMALLINT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>INT</td>
+ <td>INT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>BIGINT</td>
+ <td>BIGINT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>FLOAT</td>
+ <td>FLOAT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>DOUBLE</td>
+ <td>DOUBLE</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>DECIMAL(p, s)</td>
+ <td>DECIMAL(p, s)</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>BOOLEAN</td>
+ <td>BOOLEAN</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>DATE</td>
+ <td>DATE</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>TIMESTAMP</td>
+ <td>DATETIME</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>TIMESTAMP_LTZ</td>
+ <td>TIMESTAMP_LTZ</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>CHAR(n)</td>
+ <td>CHAR(n)</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>VARCHAR(n)</td>
+ <td>VARCHAR(n)</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</div>
+
+{{< top >}}
\ No newline at end of file
diff --git a/docs/content/docs/connectors/paimon.md b/docs/content/docs/connectors/paimon.md
new file mode 100644
index 000000000..f8d030779
--- /dev/null
+++ b/docs/content/docs/connectors/paimon.md
@@ -0,0 +1,232 @@
+---
+title: "Paimon"
+weight: 6
+type: docs
+aliases:
+- /connectors/paimon
+---
+<!--
+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.
+-->
+
+# Paimon Pipeline Connector
+
+The Paimon Pipeline connector can be used as the *Data Sink* of the pipeline, and write data to [Paimon](https://paimon.apache.org). This document describes how to set up the Paimon Pipeline connector.
+
+## What can the connector do?
+* Create table automatically if not exist
+* Schema change synchronization
+* Data synchronization
+
+How to create Pipeline
+----------------
+
+The pipeline for reading data from MySQL and sink to Paimon can be defined as follows:
+
+```yaml
+source:
+ type: mysql
+ name: MySQL Source
+ hostname: 127.0.0.1
+ port: 3306
+ username: admin
+ password: pass
+ tables: adb.\.*, bdb.user_table_[0-9]+, [app|web].order_\.*
+ server-id: 5401-5404
+
+sink:
+ type: paimon
+ name: Paimon Sink
+ metastore: filesystem
+ warehouse: /path/warehouse
+
+pipeline:
+ name: MySQL to Paimon Pipeline
+ parallelism: 2
+```
+
+Pipeline Connector Options
+----------------
+<div class="highlight">
+<table class="colwidths-auto docutils">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 25%">Option</th>
+ <th class="text-left" style="width: 8%">Required</th>
+ <th class="text-left" style="width: 7%">Default</th>
+ <th class="text-left" style="width: 10%">Type</th>
+ <th class="text-left" style="width: 50%">Description</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>type</td>
+ <td>required</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Specify what connector to use, here should be <code>'paimon'</code>.</td>
+ </tr>
+ <tr>
+ <td>name</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>The name of the sink.</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.metastore</td>
+ <td>required</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Metastore of paimon catalog, supports filesystem and hive.</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.warehouse</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>The warehouse root path of catalog.</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.uri</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Uri of metastore server.</td>
+ </tr>
+ <tr>
+ <td>commit.user</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;"><code>"admin"</code></td>
+ <td>String</td>
+ <td>User name for committing data files.</td>
+ </tr>
+ <tr>
+ <td>partition.key</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Partition keys for each partitioned table, allow setting multiple primary keys for multiTables. Each table are separated by ';', and each partition key are separated by ','. For example, we can set partition.key of two tables by 'testdb.table1:id1,id2;testdb.table2:name'.</td>
+ </tr>
+ <tr>
+ <td>catalog.properties.*</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Pass options of Paimon catalog to pipeline,See <a href="https://paimon.apache.org/docs/master/maintenance/configurations/#catalogoptions">Paimon catalog options</a>。 </td>
+ </tr>
+ <tr>
+ <td>table.properties.*</td>
+ <td>optional</td>
+ <td style="word-wrap: break-word;">(none)</td>
+ <td>String</td>
+ <td>Pass options of Paimon table to pipeline,See <a href="https://paimon.apache.org/docs/master/maintenance/configurations/#coreoptions">Paimon table options</a>。 </td>
+ </tr>
+ </tbody>
+</table>
+</div>
+
+Usage Notes
+--------
+
+* Only support Paimon primary key table, so the source table must have primary keys.
+
+* Not support exactly-once. The connector uses at-least-once + primary key table for idempotent writing.
+
+Data Type Mapping
+----------------
+<div class="wy-table-responsive">
+<table class="colwidths-auto docutils">
+ <thead>
+ <tr>
+ <th class="text-left">CDC type</th>
+ <th class="text-left">Paimon type</th>
+ <th class="text-left" style="width:60%;">NOTE</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>TINYINT</td>
+ <td>TINYINT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>SMALLINT</td>
+ <td>SMALLINT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>INT</td>
+ <td>INT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>BIGINT</td>
+ <td>BIGINT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>FLOAT</td>
+ <td>FLOAT</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>DOUBLE</td>
+ <td>DOUBLE</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>DECIMAL(p, s)</td>
+ <td>DECIMAL(p, s)</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>BOOLEAN</td>
+ <td>BOOLEAN</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>DATE</td>
+ <td>DATE</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>TIMESTAMP</td>
+ <td>TIMESTAMP</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>TIMESTAMP_LTZ</td>
+ <td>TIMESTAMP_LTZ</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>CHAR(n)</td>
+ <td>CHAR(n)</td>
+ <td></td>
+ </tr>
+ <tr>
+ <td>VARCHAR(n)</td>
+ <td>VARCHAR(n)</td>
+ <td></td>
+ </tr>
+ </tbody>
+</table>
+</div>
+
+{{< top >}}
\ No newline at end of file
diff --git a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/types/utils/DataTypeUtils.java b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/types/utils/DataTypeUtils.java
index cc3405008..f54fba2ff 100644
--- a/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/types/utils/DataTypeUtils.java
+++ b/flink-cdc-common/src/main/java/org/apache/flink/cdc/common/types/utils/DataTypeUtils.java
@@ -95,46 +95,92 @@ public class DataTypeUtils {
int scale = DataTypes.getScale(type).orElse(0);
switch (type.getTypeRoot()) {
case CHAR:
- return org.apache.flink.table.api.DataTypes.CHAR(length);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.CHAR(length)
+ : org.apache.flink.table.api.DataTypes.CHAR(length).notNull();
case VARCHAR:
- return org.apache.flink.table.api.DataTypes.VARCHAR(length);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.VARCHAR(length)
+ : org.apache.flink.table.api.DataTypes.VARCHAR(length).notNull();
case BOOLEAN:
- return org.apache.flink.table.api.DataTypes.BOOLEAN();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.BOOLEAN()
+ : org.apache.flink.table.api.DataTypes.BOOLEAN().notNull();
case BINARY:
- return org.apache.flink.table.api.DataTypes.BINARY(length);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.BINARY(length)
+ : org.apache.flink.table.api.DataTypes.BINARY(length).notNull();
case VARBINARY:
- return org.apache.flink.table.api.DataTypes.VARBINARY(length);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.VARBINARY(length)
+ : org.apache.flink.table.api.DataTypes.VARBINARY(length).notNull();
case DECIMAL:
- return org.apache.flink.table.api.DataTypes.DECIMAL(precision, scale);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.DECIMAL(precision, scale)
+ : org.apache.flink.table.api.DataTypes.DECIMAL(precision, scale).notNull();
case TINYINT:
- return org.apache.flink.table.api.DataTypes.TINYINT();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.TINYINT()
+ : org.apache.flink.table.api.DataTypes.TINYINT().notNull();
case SMALLINT:
- return org.apache.flink.table.api.DataTypes.SMALLINT();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.SMALLINT()
+ : org.apache.flink.table.api.DataTypes.SMALLINT().notNull();
case INTEGER:
- return org.apache.flink.table.api.DataTypes.INT();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.INT()
+ : org.apache.flink.table.api.DataTypes.INT().notNull();
case DATE:
- return org.apache.flink.table.api.DataTypes.DATE();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.DATE()
+ : org.apache.flink.table.api.DataTypes.DATE().notNull();
case TIME_WITHOUT_TIME_ZONE:
- return org.apache.flink.table.api.DataTypes.TIME(precision);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.TIME(precision)
+ : org.apache.flink.table.api.DataTypes.TIME(precision).notNull();
case BIGINT:
- return org.apache.flink.table.api.DataTypes.BIGINT();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.BIGINT()
+ : org.apache.flink.table.api.DataTypes.BIGINT().notNull();
case FLOAT:
- return org.apache.flink.table.api.DataTypes.FLOAT();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.FLOAT()
+ : org.apache.flink.table.api.DataTypes.FLOAT().notNull();
case DOUBLE:
- return org.apache.flink.table.api.DataTypes.DOUBLE();
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.DOUBLE()
+ : org.apache.flink.table.api.DataTypes.DOUBLE().notNull();
case TIMESTAMP_WITHOUT_TIME_ZONE:
- case TIMESTAMP_WITH_TIME_ZONE:
- return org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_TIME_ZONE(precision);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.TIMESTAMP(precision)
+ : org.apache.flink.table.api.DataTypes.TIMESTAMP(precision).notNull();
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
- return org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(
- precision);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(
+ precision)
+ : org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(
+ precision)
+ .notNull();
+ case TIMESTAMP_WITH_TIME_ZONE:
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_TIME_ZONE(precision)
+ : org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_TIME_ZONE(precision)
+ .notNull();
case ARRAY:
Preconditions.checkState(children != null && !children.isEmpty());
- return org.apache.flink.table.api.DataTypes.ARRAY(toFlinkDataType(children.get(0)));
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.ARRAY(
+ toFlinkDataType(children.get(0)))
+ : org.apache.flink.table.api.DataTypes.ARRAY(
+ toFlinkDataType(children.get(0)).notNull());
case MAP:
Preconditions.checkState(children != null && children.size() > 1);
- return org.apache.flink.table.api.DataTypes.MAP(
- toFlinkDataType(children.get(0)), toFlinkDataType(children.get(1)));
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.MAP(
+ toFlinkDataType(children.get(0)), toFlinkDataType(children.get(1)))
+ : org.apache.flink.table.api.DataTypes.MAP(
+ toFlinkDataType(children.get(0)),
+ toFlinkDataType(children.get(1)).notNull());
case ROW:
Preconditions.checkState(!CollectionUtil.isNullOrEmpty(children));
RowType rowType = (RowType) type;
@@ -142,7 +188,9 @@ public class DataTypeUtils {
rowType.getFields().stream()
.map(DataField::toFlinkDataTypeField)
.collect(Collectors.toList());
- return org.apache.flink.table.api.DataTypes.ROW(fields);
+ return type.isNullable()
+ ? org.apache.flink.table.api.DataTypes.ROW(fields)
+ : org.apache.flink.table.api.DataTypes.ROW(fields).notNull();
default:
throw new IllegalArgumentException("Illegal type: " + type);
}
diff --git a/flink-cdc-common/src/test/java/org/apache/flink/cdc/common/types/utils/DataTypeUtilsTest.java b/flink-cdc-common/src/test/java/org/apache/flink/cdc/common/types/utils/DataTypeUtilsTest.java
index 5853f5769..2807cd326 100644
--- a/flink-cdc-common/src/test/java/org/apache/flink/cdc/common/types/utils/DataTypeUtilsTest.java
+++ b/flink-cdc-common/src/test/java/org/apache/flink/cdc/common/types/utils/DataTypeUtilsTest.java
@@ -45,6 +45,7 @@ import static org.apache.flink.table.api.DataTypes.ROW;
import static org.apache.flink.table.api.DataTypes.SMALLINT;
import static org.apache.flink.table.api.DataTypes.STRING;
import static org.apache.flink.table.api.DataTypes.TIME;
+import static org.apache.flink.table.api.DataTypes.TIMESTAMP;
import static org.apache.flink.table.api.DataTypes.TIMESTAMP_LTZ;
import static org.apache.flink.table.api.DataTypes.TIMESTAMP_WITH_TIME_ZONE;
import static org.apache.flink.table.api.DataTypes.TINYINT;
@@ -116,8 +117,8 @@ class DataTypeUtilsTest {
FIELD("f14", DATE()),
FIELD("f15", TIME()),
FIELD("f16", TIME(6)),
- FIELD("f17", TIMESTAMP_WITH_TIME_ZONE()),
- FIELD("f18", TIMESTAMP_WITH_TIME_ZONE(6)),
+ FIELD("f17", TIMESTAMP()),
+ FIELD("f18", TIMESTAMP(6)),
FIELD("f19", TIMESTAMP_LTZ()),
FIELD("f20", TIMESTAMP_LTZ(6)),
FIELD("f21", TIMESTAMP_WITH_TIME_ZONE()),
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/pom.xml b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/pom.xml
new file mode 100644
index 000000000..2c80c8aa9
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/pom.xml
@@ -0,0 +1,284 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>flink-cdc-pipeline-connectors</artifactId>
+ <groupId>org.apache.flink</groupId>
+ <version>${revision}</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>flink-cdc-pipeline-connector-paimon</artifactId>
+
+ <properties>
+ <paimon.version>0.7.0-incubating</paimon.version>
+ <hadoop.version>2.8.5</hadoop.version>
+ <hive.version>2.3.9</hive.version>
+ <mockito.version>3.4.6</mockito.version>
+ </properties>
+
+ <dependencies>
+ <!-- paimon dependency -->
+ <dependency>
+ <groupId>org.apache.paimon</groupId>
+ <artifactId>paimon-flink-${flink.major.version}</artifactId>
+ <version>${paimon.version}</version>
+ </dependency>
+
+ <!-- test -->
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-cdc-composer</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-clients</artifactId>
+ <version>${flink.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-table-planner_2.12</artifactId>
+ <version>${flink.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-connector-files</artifactId>
+ <version>${flink.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-inline</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <version>${mockito.version}</version>
+ <type>jar</type>
+ <scope>test</scope>
+ </dependency>
+
+ <!-- hadoop dependency -->
+
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-common</artifactId>
+ <version>${hadoop.version}</version>
+ <scope>provided</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.avro</groupId>
+ <artifactId>avro</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>jdk.tools</groupId>
+ <artifactId>jdk.tools</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.hadoop</groupId>
+ <artifactId>hadoop-hdfs</artifactId>
+ <version>${hadoop.version}</version>
+ <scope>provided</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>jdk.tools</groupId>
+ <artifactId>jdk.tools</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- hive dependency -->
+
+ <dependency>
+ <groupId>org.apache.hive</groupId>
+ <artifactId>hive-metastore</artifactId>
+ <version>${hive.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.orc</groupId>
+ <artifactId>orc-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>jdk.tools</groupId>
+ <artifactId>jdk.tools</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.google.protobuf</groupId>
+ <artifactId>protobuf-java</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.hive</groupId>
+ <artifactId>hive-exec</artifactId>
+ <version>${hive.version}</version>
+ <scope>provided</scope>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-log4j12</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-annotations</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-databind</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.orc</groupId>
+ <artifactId>orc-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.pentaho</groupId>
+ <artifactId>*</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.calcite</groupId>
+ <artifactId>calcite-core</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.calcite</groupId>
+ <artifactId>calcite-druid</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.calcite.avatica</groupId>
+ <artifactId>avatica</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.calcite</groupId>
+ <artifactId>calcite-avatica</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>3.1.1</version>
+ <executions>
+ <execution>
+ <id>shade-flink</id>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <shadeTestJar>false</shadeTestJar>
+ <artifactSet>
+ <includes>
+ <include>org.apache.paimon:*</include>
+ </includes>
+ </artifactSet>
+ <relocations>
+ <relocation>
+ <pattern>org.apache.kafka</pattern>
+ <shadedPattern>shaded.paimon.org.apache.kafka</shadedPattern>
+ </relocation>
+ </relocations>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSink.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSink.java
new file mode 100644
index 000000000..6c94a3a74
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSink.java
@@ -0,0 +1,72 @@
+/*
+ * 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.connectors.paimon.sink;
+
+import org.apache.flink.cdc.common.event.Event;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.sink.DataSink;
+import org.apache.flink.cdc.common.sink.EventSinkProvider;
+import org.apache.flink.cdc.common.sink.FlinkSinkProvider;
+import org.apache.flink.cdc.common.sink.MetadataApplier;
+import org.apache.flink.cdc.connectors.paimon.sink.v2.PaimonRecordSerializer;
+import org.apache.flink.cdc.connectors.paimon.sink.v2.PaimonSink;
+
+import org.apache.paimon.options.Options;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/** A {@link DataSink} for Paimon connector that supports schema evolution. */
+public class PaimonDataSink implements DataSink, Serializable {
+
+ // options for creating Paimon catalog.
+ private final Options options;
+
+ // options for creating Paimon table.
+ private final Map<String, String> tableOptions;
+
+ private final String commitUser;
+
+ private final Map<TableId, List<String>> partitionMaps;
+
+ private final PaimonRecordSerializer<Event> serializer;
+
+ public PaimonDataSink(
+ Options options,
+ Map<String, String> tableOptions,
+ String commitUser,
+ Map<TableId, List<String>> partitionMaps,
+ PaimonRecordSerializer<Event> serializer) {
+ this.options = options;
+ this.tableOptions = tableOptions;
+ this.commitUser = commitUser;
+ this.partitionMaps = partitionMaps;
+ this.serializer = serializer;
+ }
+
+ @Override
+ public EventSinkProvider getEventSinkProvider() {
+ return FlinkSinkProvider.of(new PaimonSink<>(options, commitUser, serializer));
+ }
+
+ @Override
+ public MetadataApplier getMetadataApplier() {
+ return new PaimonMetadataApplier(options, tableOptions, partitionMaps);
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkFactory.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkFactory.java
new file mode 100644
index 000000000..3927fbc73
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkFactory.java
@@ -0,0 +1,126 @@
+/*
+ * 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.connectors.paimon.sink;
+
+import org.apache.flink.cdc.common.configuration.ConfigOption;
+import org.apache.flink.cdc.common.event.Event;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.factories.DataSinkFactory;
+import org.apache.flink.cdc.common.pipeline.PipelineOptions;
+import org.apache.flink.cdc.common.sink.DataSink;
+import org.apache.flink.cdc.common.utils.Preconditions;
+import org.apache.flink.cdc.connectors.paimon.sink.v2.PaimonRecordEventSerializer;
+import org.apache.flink.cdc.connectors.paimon.sink.v2.PaimonRecordSerializer;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.options.Options;
+
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** A {@link DataSinkFactory} to create {@link PaimonDataSink}. */
+public class PaimonDataSinkFactory implements DataSinkFactory {
+
+ public static final String IDENTIFIER = "paimon";
+
+ @Override
+ public DataSink createDataSink(Context context) {
+ Map<String, String> allOptions = context.getFactoryConfiguration().toMap();
+ Map<String, String> catalogOptions = new HashMap<>();
+ Map<String, String> tableOptions = new HashMap<>();
+ allOptions.forEach(
+ (key, value) -> {
+ if (key.startsWith(PaimonDataSinkOptions.PREFIX_TABLE_PROPERTIES)) {
+ tableOptions.put(
+ key.substring(
+ PaimonDataSinkOptions.PREFIX_TABLE_PROPERTIES.length()),
+ value);
+ } else if (key.startsWith(PaimonDataSinkOptions.PREFIX_CATALOG_PROPERTIES)) {
+ catalogOptions.put(
+ key.substring(
+ PaimonDataSinkOptions.PREFIX_CATALOG_PROPERTIES.length()),
+ value);
+ }
+ });
+ Options options = Options.fromMap(catalogOptions);
+ try (Catalog catalog = FlinkCatalogFactory.createPaimonCatalog(options)) {
+ Preconditions.checkNotNull(
+ catalog.listDatabases(), "catalog option of Paimon is invalid.");
+ } catch (Exception e) {
+ throw new RuntimeException("failed to create or use paimon catalog", e);
+ }
+ ZoneId zoneId = ZoneId.systemDefault();
+ if (!Objects.equals(
+ context.getPipelineConfiguration().get(PipelineOptions.PIPELINE_LOCAL_TIME_ZONE),
+ PipelineOptions.PIPELINE_LOCAL_TIME_ZONE.defaultValue())) {
+ zoneId =
+ ZoneId.of(
+ context.getPipelineConfiguration()
+ .get(PipelineOptions.PIPELINE_LOCAL_TIME_ZONE));
+ }
+ String commitUser =
+ context.getFactoryConfiguration().get(PaimonDataSinkOptions.COMMIT_USER);
+ String partitionKey =
+ context.getFactoryConfiguration().get(PaimonDataSinkOptions.PARTITION_KEY);
+ Map<TableId, List<String>> partitionMaps = new HashMap<>();
+ if (!partitionKey.isEmpty()) {
+ for (String tables : partitionKey.split(";")) {
+ String[] splits = tables.split(":");
+ if (splits.length == 2) {
+ TableId tableId = TableId.parse(splits[0]);
+ List<String> partitions = Arrays.asList(splits[1].split(","));
+ partitionMaps.put(tableId, partitions);
+ } else {
+ throw new IllegalArgumentException(
+ PaimonDataSinkOptions.PARTITION_KEY.key()
+ + " is malformed, please refer to the documents");
+ }
+ }
+ }
+ PaimonRecordSerializer<Event> serializer = new PaimonRecordEventSerializer(zoneId);
+ return new PaimonDataSink(options, tableOptions, commitUser, partitionMaps, serializer);
+ }
+
+ @Override
+ public String identifier() {
+ return IDENTIFIER;
+ }
+
+ @Override
+ public Set<ConfigOption<?>> requiredOptions() {
+ Set<ConfigOption<?>> options = new HashSet<>();
+ options.add(PaimonDataSinkOptions.METASTORE);
+ return options;
+ }
+
+ @Override
+ public Set<ConfigOption<?>> optionalOptions() {
+ Set<ConfigOption<?>> options = new HashSet<>();
+ options.add(PaimonDataSinkOptions.URI);
+ options.add(PaimonDataSinkOptions.WAREHOUSE);
+ options.add(PaimonDataSinkOptions.COMMIT_USER);
+ return options;
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkOptions.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkOptions.java
new file mode 100644
index 000000000..018920d09
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkOptions.java
@@ -0,0 +1,67 @@
+/*
+ * 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.connectors.paimon.sink;
+
+import org.apache.flink.cdc.common.configuration.ConfigOption;
+
+import org.apache.paimon.options.CatalogOptions;
+
+import static org.apache.flink.cdc.common.configuration.ConfigOptions.key;
+
+/** copy from {@link CatalogOptions}. Options for {@link PaimonDataSink}. */
+public class PaimonDataSinkOptions {
+
+ // prefix for passing properties for table creation.
+ public static final String PREFIX_TABLE_PROPERTIES = "table.properties.";
+
+ // prefix for passing properties for catalog creation.
+ public static final String PREFIX_CATALOG_PROPERTIES = "catalog.properties.";
+
+ public static final ConfigOption<String> COMMIT_USER =
+ key("commit.user")
+ .stringType()
+ .defaultValue("admin")
+ .withDescription("User name for committing data files.");
+
+ public static final ConfigOption<String> WAREHOUSE =
+ key("catalog.properties.warehouse")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("The warehouse root path of catalog.");
+
+ public static final ConfigOption<String> METASTORE =
+ key("catalog.properties.metastore")
+ .stringType()
+ .defaultValue("filesystem")
+ .withDescription("Metastore of paimon catalog, supports filesystem and hive.");
+
+ public static final ConfigOption<String> URI =
+ key("catalog.properties.uri")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("Uri of metastore server.");
+
+ public static final ConfigOption<String> PARTITION_KEY =
+ key("partition.key")
+ .stringType()
+ .defaultValue("")
+ .withDescription(
+ "Partition keys for each partitioned table, allow setting multiple primary keys for multiTables. "
+ + "Tables are separated by ';', and partition keys are separated by ','. "
+ + "For example, we can set partition.key of two tables by 'testdb.table1:id1,id2;testdb.table2:name'.");
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonMetadataApplier.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonMetadataApplier.java
new file mode 100644
index 000000000..9253108ca
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonMetadataApplier.java
@@ -0,0 +1,203 @@
+/*
+ * 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.connectors.paimon.sink;
+
+import org.apache.flink.cdc.common.event.AddColumnEvent;
+import org.apache.flink.cdc.common.event.AlterColumnTypeEvent;
+import org.apache.flink.cdc.common.event.CreateTableEvent;
+import org.apache.flink.cdc.common.event.DropColumnEvent;
+import org.apache.flink.cdc.common.event.RenameColumnEvent;
+import org.apache.flink.cdc.common.event.SchemaChangeEvent;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.sink.MetadataApplier;
+import org.apache.flink.cdc.common.types.utils.DataTypeUtils;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.flink.LogicalTypeConversion;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.SchemaChange;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A {@code MetadataApplier} that applies metadata changes to Paimon. Support primary key table
+ * only.
+ */
+public class PaimonMetadataApplier implements MetadataApplier {
+
+ // Catalog is unSerializable.
+ private transient Catalog catalog;
+
+ // currently, we set table options for all tables using the same options.
+ private final Map<String, String> tableOptions;
+
+ private final Options catalogOptions;
+
+ private final Map<TableId, List<String>> partitionMaps;
+
+ public PaimonMetadataApplier(Options catalogOptions) {
+ this.catalogOptions = catalogOptions;
+ this.tableOptions = new HashMap<>();
+ this.partitionMaps = new HashMap<>();
+ }
+
+ public PaimonMetadataApplier(
+ Options catalogOptions,
+ Map<String, String> tableOptions,
+ Map<TableId, List<String>> partitionMaps) {
+ this.catalogOptions = catalogOptions;
+ this.tableOptions = tableOptions;
+ this.partitionMaps = partitionMaps;
+ }
+
+ @Override
+ public void applySchemaChange(SchemaChangeEvent schemaChangeEvent) {
+ if (catalog == null) {
+ catalog = FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
+ }
+ try {
+ if (schemaChangeEvent instanceof CreateTableEvent) {
+ applyCreateTable((CreateTableEvent) schemaChangeEvent);
+ } else if (schemaChangeEvent instanceof AddColumnEvent) {
+ applyAddColumn((AddColumnEvent) schemaChangeEvent);
+ } else if (schemaChangeEvent instanceof DropColumnEvent) {
+ applyDropColumn((DropColumnEvent) schemaChangeEvent);
+ } else if (schemaChangeEvent instanceof RenameColumnEvent) {
+ applyRenameColumn((RenameColumnEvent) schemaChangeEvent);
+ } else if (schemaChangeEvent instanceof AlterColumnTypeEvent) {
+ applyAlterColumn((AlterColumnTypeEvent) schemaChangeEvent);
+ } else {
+ throw new UnsupportedOperationException(
+ "PaimonDataSink doesn't support schema change event " + schemaChangeEvent);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void applyCreateTable(CreateTableEvent event)
+ throws Catalog.DatabaseAlreadyExistException, Catalog.TableAlreadyExistException,
+ Catalog.DatabaseNotExistException {
+ if (!catalog.databaseExists(event.tableId().getSchemaName())) {
+ catalog.createDatabase(event.tableId().getSchemaName(), true);
+ }
+ Schema schema = event.getSchema();
+ org.apache.paimon.schema.Schema.Builder builder =
+ new org.apache.paimon.schema.Schema.Builder();
+ schema.getColumns()
+ .forEach(
+ (column) ->
+ builder.column(
+ column.getName(),
+ LogicalTypeConversion.toDataType(
+ DataTypeUtils.toFlinkDataType(column.getType())
+ .getLogicalType())));
+ builder.primaryKey(schema.primaryKeys().toArray(new String[0]));
+ if (partitionMaps.containsKey(event.tableId())) {
+ builder.partitionKeys(partitionMaps.get(event.tableId()));
+ } else if (schema.partitionKeys() != null && !schema.partitionKeys().isEmpty()) {
+ builder.partitionKeys(schema.partitionKeys());
+ }
+ builder.options(tableOptions);
+ catalog.createTable(
+ new Identifier(event.tableId().getSchemaName(), event.tableId().getTableName()),
+ builder.build(),
+ true);
+ }
+
+ private void applyAddColumn(AddColumnEvent event)
+ throws Catalog.TableNotExistException, Catalog.ColumnAlreadyExistException,
+ Catalog.ColumnNotExistException {
+ List<SchemaChange> tableChangeList = new ArrayList<>();
+ event.getAddedColumns()
+ .forEach(
+ (column) -> {
+ SchemaChange tableChange =
+ SchemaChange.addColumn(
+ column.getAddColumn().getName(),
+ LogicalTypeConversion.toDataType(
+ DataTypeUtils.toFlinkDataType(
+ column.getAddColumn().getType())
+ .getLogicalType()));
+ tableChangeList.add(tableChange);
+ });
+ catalog.alterTable(
+ new Identifier(event.tableId().getSchemaName(), event.tableId().getTableName()),
+ tableChangeList,
+ true);
+ }
+
+ private void applyDropColumn(DropColumnEvent event)
+ throws Catalog.ColumnAlreadyExistException, Catalog.TableNotExistException,
+ Catalog.ColumnNotExistException {
+ List<SchemaChange> tableChangeList = new ArrayList<>();
+ event.getDroppedColumnNames()
+ .forEach(
+ (column) -> {
+ SchemaChange tableChange = SchemaChange.dropColumn(column);
+ tableChangeList.add(tableChange);
+ });
+ catalog.alterTable(
+ new Identifier(event.tableId().getSchemaName(), event.tableId().getTableName()),
+ tableChangeList,
+ true);
+ }
+
+ private void applyRenameColumn(RenameColumnEvent event)
+ throws Catalog.ColumnAlreadyExistException, Catalog.TableNotExistException,
+ Catalog.ColumnNotExistException {
+ List<SchemaChange> tableChangeList = new ArrayList<>();
+ event.getNameMapping()
+ .forEach(
+ (oldName, newName) -> {
+ SchemaChange tableChange = SchemaChange.renameColumn(oldName, newName);
+ tableChangeList.add(tableChange);
+ });
+ catalog.alterTable(
+ new Identifier(event.tableId().getSchemaName(), event.tableId().getTableName()),
+ tableChangeList,
+ true);
+ }
+
+ private void applyAlterColumn(AlterColumnTypeEvent event)
+ throws Catalog.ColumnAlreadyExistException, Catalog.TableNotExistException,
+ Catalog.ColumnNotExistException {
+ List<SchemaChange> tableChangeList = new ArrayList<>();
+ event.getTypeMapping()
+ .forEach(
+ (oldName, newType) -> {
+ SchemaChange tableChange =
+ SchemaChange.updateColumnType(
+ oldName,
+ LogicalTypeConversion.toDataType(
+ DataTypeUtils.toFlinkDataType(newType)
+ .getLogicalType()));
+ tableChangeList.add(tableChange);
+ });
+ catalog.alterTable(
+ new Identifier(event.tableId().getSchemaName(), event.tableId().getTableName()),
+ tableChangeList,
+ true);
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/MultiTableCommittableChannelComputer.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/MultiTableCommittableChannelComputer.java
new file mode 100644
index 000000000..6694b4074
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/MultiTableCommittableChannelComputer.java
@@ -0,0 +1,59 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+import org.apache.paimon.table.sink.ChannelComputer;
+
+import java.util.Objects;
+
+/**
+ * {@link ChannelComputer} for {@link MultiTableCommittable}. Shuffle by database and table, make
+ * sure that MultiTableCommittable from the same table will be sent to the same channel.
+ */
+public class MultiTableCommittableChannelComputer
+ implements ChannelComputer<CommittableMessage<MultiTableCommittable>> {
+
+ private static final long serialVersionUID = 1L;
+
+ private transient int numChannels;
+
+ @Override
+ public void setup(int numChannels) {
+ this.numChannels = numChannels;
+ }
+
+ @Override
+ public int channel(CommittableMessage<MultiTableCommittable> committableMessage) {
+ if (committableMessage instanceof CommittableWithLineage) {
+ MultiTableCommittable multiTableCommittable =
+ ((CommittableWithLineage<MultiTableCommittable>) committableMessage)
+ .getCommittable();
+ return Math.floorMod(
+ Objects.hash(
+ multiTableCommittable.getDatabase(), multiTableCommittable.getTable()),
+ numChannels);
+ } else {
+ // CommittableSummary is inaccurate after shuffling, need to be recreated.
+ return Math.floorMod(Objects.hash(committableMessage), numChannels);
+ }
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonCommitter.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonCommitter.java
new file mode 100644
index 000000000..d0db819e1
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonCommitter.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.cdc.connectors.paimon.sink.v2;
+
+import org.apache.flink.api.connector.sink2.Committer;
+
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+import org.apache.paimon.flink.sink.StoreMultiCommitter;
+import org.apache.paimon.manifest.WrappedManifestCommittable;
+import org.apache.paimon.options.Options;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** A {@link Committer} to commit write results for multiple tables. */
+public class PaimonCommitter implements Committer<MultiTableCommittable> {
+
+ private final StoreMultiCommitter storeMultiCommitter;
+
+ public PaimonCommitter(Options catalogOptions, String commitUser) {
+ // flinkMetricGroup could be passed after FLIP-371.
+ storeMultiCommitter =
+ new StoreMultiCommitter(
+ () -> FlinkCatalogFactory.createPaimonCatalog(catalogOptions),
+ commitUser,
+ null);
+ }
+
+ @Override
+ public void commit(Collection<CommitRequest<MultiTableCommittable>> commitRequests)
+ throws IOException, InterruptedException {
+ if (commitRequests.isEmpty()) {
+ return;
+ }
+ List<MultiTableCommittable> committables =
+ commitRequests.stream()
+ .map(CommitRequest::getCommittable)
+ .collect(Collectors.toList());
+ long checkpointId = committables.get(0).checkpointId();
+ WrappedManifestCommittable wrappedManifestCommittable =
+ storeMultiCommitter.combine(checkpointId, 1L, committables);
+ storeMultiCommitter.commit(Collections.singletonList(wrappedManifestCommittable));
+ }
+
+ @Override
+ public void close() throws Exception {
+ storeMultiCommitter.close();
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonEvent.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonEvent.java
new file mode 100644
index 000000000..eb136aae7
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonEvent.java
@@ -0,0 +1,70 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.GenericRow;
+
+/** Contains the data to be written for {@link PaimonWriter}. */
+public class PaimonEvent {
+
+ // Identifier for the Paimon table to be written.
+ Identifier tableId;
+
+ // The actual record to be written to Paimon table.
+ GenericRow genericRow;
+
+ // if true, means that table schema has changed right before this genericRow.
+ boolean shouldRefreshSchema;
+
+ public PaimonEvent(Identifier tableId, GenericRow genericRow) {
+ this.tableId = tableId;
+ this.genericRow = genericRow;
+ this.shouldRefreshSchema = false;
+ }
+
+ public PaimonEvent(Identifier tableId, GenericRow genericRow, boolean shouldRefreshSchema) {
+ this.tableId = tableId;
+ this.genericRow = genericRow;
+ this.shouldRefreshSchema = shouldRefreshSchema;
+ }
+
+ public Identifier getTableId() {
+ return tableId;
+ }
+
+ public void setTableId(Identifier tableId) {
+ this.tableId = tableId;
+ }
+
+ public boolean isShouldRefreshSchema() {
+ return shouldRefreshSchema;
+ }
+
+ public void setShouldRefreshSchema(boolean shouldRefreshSchema) {
+ this.shouldRefreshSchema = shouldRefreshSchema;
+ }
+
+ public GenericRow getGenericRow() {
+ return genericRow;
+ }
+
+ public void setGenericRow(GenericRow genericRow) {
+ this.genericRow = genericRow;
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonRecordEventSerializer.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonRecordEventSerializer.java
new file mode 100644
index 000000000..53b63f3b5
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonRecordEventSerializer.java
@@ -0,0 +1,87 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.cdc.common.event.ChangeEvent;
+import org.apache.flink.cdc.common.event.CreateTableEvent;
+import org.apache.flink.cdc.common.event.DataChangeEvent;
+import org.apache.flink.cdc.common.event.Event;
+import org.apache.flink.cdc.common.event.SchemaChangeEvent;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.utils.SchemaUtils;
+
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.GenericRow;
+
+import java.time.ZoneId;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A {@link PaimonRecordSerializer} for converting {@link Event} into {@link PaimonEvent} for {@link
+ * PaimonWriter}.
+ */
+public class PaimonRecordEventSerializer implements PaimonRecordSerializer<Event> {
+
+ // maintain the latest schema of tableId.
+ private final Map<TableId, TableSchemaInfo> schemaMaps;
+
+ // ZoneId for converting relevant type.
+ private final ZoneId zoneId;
+
+ public PaimonRecordEventSerializer(ZoneId zoneId) {
+ schemaMaps = new HashMap<>();
+ this.zoneId = zoneId;
+ }
+
+ @Override
+ public PaimonEvent serialize(Event event) {
+ Identifier tableId =
+ Identifier.create(
+ ((ChangeEvent) event).tableId().getSchemaName(),
+ ((ChangeEvent) event).tableId().getTableName());
+ if (event instanceof SchemaChangeEvent) {
+ if (event instanceof CreateTableEvent) {
+ CreateTableEvent createTableEvent = (CreateTableEvent) event;
+ schemaMaps.put(
+ createTableEvent.tableId(),
+ new TableSchemaInfo(createTableEvent.getSchema(), zoneId));
+ } else {
+ SchemaChangeEvent schemaChangeEvent = (SchemaChangeEvent) event;
+ schemaMaps.put(
+ schemaChangeEvent.tableId(),
+ new TableSchemaInfo(
+ SchemaUtils.applySchemaChangeEvent(
+ schemaMaps.get(schemaChangeEvent.tableId()).getSchema(),
+ schemaChangeEvent),
+ zoneId));
+ }
+ return new PaimonEvent(tableId, null, true);
+ } else if (event instanceof DataChangeEvent) {
+ DataChangeEvent dataChangeEvent = (DataChangeEvent) event;
+ GenericRow genericRow =
+ PaimonWriterHelper.convertEventToGenericRow(
+ dataChangeEvent,
+ schemaMaps.get(dataChangeEvent.tableId()).getFieldGetters());
+ return new PaimonEvent(tableId, genericRow);
+ } else {
+ throw new IllegalArgumentException(
+ "failed to convert Input into PaimonEvent, unsupported event: " + event);
+ }
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonRecordSerializer.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonRecordSerializer.java
new file mode 100644
index 000000000..b4f30cc31
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonRecordSerializer.java
@@ -0,0 +1,26 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import java.io.IOException;
+import java.io.Serializable;
+
+/** Converting Input into {@link PaimonEvent} for {@link PaimonWriter}. */
+public interface PaimonRecordSerializer<Input> extends Serializable {
+ PaimonEvent serialize(Input t) throws IOException;
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSink.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSink.java
new file mode 100644
index 000000000..24002bd24
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSink.java
@@ -0,0 +1,98 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.TwoPhaseCommittingSink;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessageTypeInfo;
+import org.apache.flink.streaming.api.connector.sink2.WithPreCommitTopology;
+import org.apache.flink.streaming.api.datastream.DataStream;
+
+import org.apache.paimon.flink.sink.FlinkStreamPartitioner;
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+import org.apache.paimon.flink.sink.MultiTableCommittableSerializer;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.table.sink.CommitMessageSerializer;
+
+/**
+ * A {@link Sink} for Paimon. Maintain this package until Paimon has it own sinkV2 implementation.
+ */
+public class PaimonSink<InputT>
+ implements TwoPhaseCommittingSink<InputT, MultiTableCommittable>,
+ WithPreCommitTopology<InputT, MultiTableCommittable> {
+
+ // provided a default commit user.
+ public static final String DEFAULT_COMMIT_USER = "admin";
+
+ private final Options catalogOptions;
+
+ private final String commitUser;
+
+ private final PaimonRecordSerializer<InputT> serializer;
+
+ public PaimonSink(Options catalogOptions, PaimonRecordSerializer<InputT> serializer) {
+ this.catalogOptions = catalogOptions;
+ this.serializer = serializer;
+ commitUser = DEFAULT_COMMIT_USER;
+ }
+
+ public PaimonSink(
+ Options catalogOptions, String commitUser, PaimonRecordSerializer<InputT> serializer) {
+ this.catalogOptions = catalogOptions;
+ this.commitUser = commitUser;
+ this.serializer = serializer;
+ }
+
+ @Override
+ public PaimonWriter<InputT> createWriter(InitContext context) {
+ return new PaimonWriter<>(catalogOptions, context.metricGroup(), commitUser, serializer);
+ }
+
+ @Override
+ public Committer<MultiTableCommittable> createCommitter() {
+ return new PaimonCommitter(catalogOptions, commitUser);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<MultiTableCommittable> getCommittableSerializer() {
+ CommitMessageSerializer fileSerializer = new CommitMessageSerializer();
+ return new MultiTableCommittableSerializer(fileSerializer);
+ }
+
+ @Override
+ public DataStream<CommittableMessage<MultiTableCommittable>> addPreCommitTopology(
+ DataStream<CommittableMessage<MultiTableCommittable>> committables) {
+ TypeInformation<CommittableMessage<MultiTableCommittable>> typeInformation =
+ CommittableMessageTypeInfo.of(this::getCommittableSerializer);
+ // shuffle MultiTableCommittable by tables
+ DataStream<CommittableMessage<MultiTableCommittable>> partitioned =
+ FlinkStreamPartitioner.partition(
+ committables,
+ new MultiTableCommittableChannelComputer(),
+ committables.getParallelism());
+
+ // add correct checkpointId to MultiTableCommittable and recreate CommittableSummary.
+ return partitioned
+ .transform("preCommit", typeInformation, new PreCommitOperator())
+ .setParallelism(committables.getParallelism());
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriter.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriter.java
new file mode 100644
index 000000000..1b51535b2
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriter.java
@@ -0,0 +1,197 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.api.connector.sink2.TwoPhaseCommittingSink;
+import org.apache.flink.cdc.common.event.DataChangeEvent;
+import org.apache.flink.cdc.common.event.FlushEvent;
+import org.apache.flink.metrics.MetricGroup;
+import org.apache.flink.runtime.io.disk.iomanager.IOManager;
+import org.apache.flink.runtime.io.disk.iomanager.IOManagerAsync;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+import org.apache.paimon.flink.sink.StoreSinkWrite;
+import org.apache.paimon.memory.HeapMemorySegmentPool;
+import org.apache.paimon.memory.MemoryPoolFactory;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.utils.ExecutorThreadFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+/** A {@link Sink} to write {@link DataChangeEvent} to Paimon storage. */
+public class PaimonWriter<InputT>
+ implements TwoPhaseCommittingSink.PrecommittingSinkWriter<InputT, MultiTableCommittable> {
+
+ // use `static` because Catalog is unSerializable.
+ private static Catalog catalog;
+ private final IOManager ioManager;
+
+ // Each job can only have one user name and this name must be consistent across restarts.
+ private final String commitUser;
+ // all table write should share one write buffer so that writers can preempt memory
+ // from those of other tables
+ private MemoryPoolFactory memoryPoolFactory;
+
+ // deserializer that converts Input into PaimonEvent.
+ private final PaimonRecordSerializer<InputT> serializer;
+ private final Map<Identifier, FileStoreTable> tables;
+ private final Map<Identifier, StoreSinkWrite> writes;
+ private final ExecutorService compactExecutor;
+ private final MetricGroup metricGroup;
+ private final List<MultiTableCommittable> committables;
+
+ public PaimonWriter(
+ Options catalogOptions,
+ MetricGroup metricGroup,
+ String commitUser,
+ PaimonRecordSerializer<InputT> serializer) {
+ catalog = FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
+ this.metricGroup = metricGroup;
+ this.commitUser = commitUser;
+ this.tables = new HashMap<>();
+ this.writes = new HashMap<>();
+ this.committables = new ArrayList<>();
+ this.ioManager = new IOManagerAsync();
+ this.compactExecutor =
+ Executors.newSingleThreadScheduledExecutor(
+ new ExecutorThreadFactory(
+ Thread.currentThread().getName() + "-CdcMultiWrite-Compaction"));
+ this.serializer = serializer;
+ }
+
+ @Override
+ public Collection<MultiTableCommittable> prepareCommit() {
+ Collection<MultiTableCommittable> allCommittables = new ArrayList<>(committables);
+ committables.clear();
+ return allCommittables;
+ }
+
+ @Override
+ public void write(InputT event, Context context) throws IOException {
+ PaimonEvent paimonEvent = serializer.serialize(event);
+ Identifier tableId = paimonEvent.getTableId();
+ if (paimonEvent.isShouldRefreshSchema()) {
+ // remove the table temporarily, then add the table with latest schema when received
+ // DataChangeEvent.
+ writes.remove(tableId);
+ tables.remove(tableId);
+ }
+ if (paimonEvent.getGenericRow() != null) {
+ FileStoreTable table;
+ table = getTable(tableId);
+ if (memoryPoolFactory == null) {
+ memoryPoolFactory =
+ new MemoryPoolFactory(
+ // currently, the options of all tables are the same in CDC
+ new HeapMemorySegmentPool(
+ table.coreOptions().writeBufferSize(),
+ table.coreOptions().pageSize()));
+ }
+ StoreSinkWrite write =
+ writes.computeIfAbsent(
+ tableId,
+ id -> {
+ StoreSinkWriteImpl storeSinkWrite =
+ new StoreSinkWriteImpl(
+ table,
+ commitUser,
+ ioManager,
+ false,
+ false,
+ true,
+ memoryPoolFactory,
+ metricGroup);
+ storeSinkWrite.withCompactExecutor(compactExecutor);
+ return storeSinkWrite;
+ });
+ try {
+ write.write(paimonEvent.getGenericRow());
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+ }
+
+ private FileStoreTable getTable(Identifier tableId) {
+ FileStoreTable table =
+ tables.computeIfAbsent(
+ tableId,
+ id -> {
+ try {
+ return (FileStoreTable) catalog.getTable(tableId);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ if (table.bucketMode() != BucketMode.FIXED) {
+ throw new UnsupportedOperationException(
+ "Unified Sink only supports FIXED bucket mode, but is " + table.bucketMode());
+ }
+ return table;
+ }
+
+ /**
+ * Called on checkpoint or end of input so that the writer to flush all pending data for
+ * at-least-once.
+ *
+ * <p>this method will also be called when receiving {@link FlushEvent}, but we don't need to
+ * commit the MultiTableCommittables immediately in this case, because {@link PaimonCommitter}
+ * support committing data of different schemas.
+ */
+ @Override
+ public void flush(boolean endOfInput) throws IOException {
+ for (Map.Entry<Identifier, StoreSinkWrite> entry : writes.entrySet()) {
+ Identifier key = entry.getKey();
+ StoreSinkWrite write = entry.getValue();
+ boolean waitCompaction = false;
+ // checkpointId will be updated correctly by PreCommitOperator.
+ long checkpointId = 1L;
+ committables.addAll(
+ write.prepareCommit(waitCompaction, checkpointId).stream()
+ .map(
+ committable ->
+ MultiTableCommittable.fromCommittable(key, committable))
+ .collect(Collectors.toList()));
+ }
+ }
+
+ @Override
+ public void close() throws Exception {
+ for (StoreSinkWrite write : writes.values()) {
+ write.close();
+ }
+ if (compactExecutor != null) {
+ compactExecutor.shutdownNow();
+ }
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriterHelper.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriterHelper.java
new file mode 100644
index 000000000..a2df17455
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriterHelper.java
@@ -0,0 +1,171 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.cdc.common.data.DecimalData;
+import org.apache.flink.cdc.common.data.RecordData;
+import org.apache.flink.cdc.common.event.DataChangeEvent;
+import org.apache.flink.cdc.common.schema.Column;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataType;
+import org.apache.flink.cdc.common.types.DataTypeChecks;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.types.RowKind;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.apache.flink.cdc.common.types.DataTypeChecks.getFieldCount;
+
+/** A helper class for {@link PaimonWriter} to create FieldGetter and GenericRow. */
+public class PaimonWriterHelper {
+
+ /** create a list of {@link RecordData.FieldGetter} for {@link PaimonWriter}. */
+ public static List<RecordData.FieldGetter> createFieldGetters(Schema schema, ZoneId zoneId) {
+ List<Column> columns = schema.getColumns();
+ List<RecordData.FieldGetter> fieldGetters = new ArrayList<>(columns.size());
+ for (int i = 0; i < columns.size(); i++) {
+ fieldGetters.add(createFieldGetter(columns.get(i).getType(), i, zoneId));
+ }
+ return fieldGetters;
+ }
+
+ private static RecordData.FieldGetter createFieldGetter(
+ DataType fieldType, int fieldPos, ZoneId zoneId) {
+ final RecordData.FieldGetter fieldGetter;
+ // ordered by type root definition
+ switch (fieldType.getTypeRoot()) {
+ case CHAR:
+ case VARCHAR:
+ fieldGetter = row -> BinaryString.fromString(row.getString(fieldPos).toString());
+ break;
+ case BOOLEAN:
+ fieldGetter = row -> row.getBoolean(fieldPos);
+ break;
+ case BINARY:
+ case VARBINARY:
+ fieldGetter = row -> row.getBinary(fieldPos);
+ break;
+ case DECIMAL:
+ final int decimalPrecision = DataTypeChecks.getPrecision(fieldType);
+ final int decimalScale = DataTypeChecks.getScale(fieldType);
+ fieldGetter =
+ row -> {
+ DecimalData decimalData =
+ row.getDecimal(fieldPos, decimalPrecision, decimalScale);
+ return Decimal.fromBigDecimal(
+ decimalData.toBigDecimal(), decimalPrecision, decimalScale);
+ };
+ break;
+ case TINYINT:
+ fieldGetter = row -> row.getByte(fieldPos);
+ break;
+ case SMALLINT:
+ fieldGetter = row -> row.getShort(fieldPos);
+ break;
+ case BIGINT:
+ fieldGetter = row -> row.getLong(fieldPos);
+ break;
+ case FLOAT:
+ fieldGetter = row -> row.getFloat(fieldPos);
+ break;
+ case DOUBLE:
+ fieldGetter = row -> row.getDouble(fieldPos);
+ break;
+ case INTEGER:
+ case DATE:
+ case TIME_WITHOUT_TIME_ZONE:
+ fieldGetter = row -> row.getInt(fieldPos);
+ break;
+ case TIMESTAMP_WITHOUT_TIME_ZONE:
+ fieldGetter =
+ row ->
+ Timestamp.fromSQLTimestamp(
+ row.getTimestamp(
+ fieldPos,
+ DataTypeChecks.getPrecision(fieldType))
+ .toTimestamp());
+ break;
+ case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+ case TIMESTAMP_WITH_TIME_ZONE:
+ fieldGetter =
+ row ->
+ Timestamp.fromLocalDateTime(
+ ZonedDateTime.ofInstant(
+ row.getLocalZonedTimestampData(
+ fieldPos,
+ DataTypeChecks.getPrecision(
+ fieldType))
+ .toInstant(),
+ zoneId)
+ .toLocalDateTime());
+ break;
+ case ROW:
+ final int rowFieldCount = getFieldCount(fieldType);
+ fieldGetter = row -> row.getRow(fieldPos, rowFieldCount);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "don't support type of " + fieldType.getTypeRoot());
+ }
+ if (!fieldType.isNullable()) {
+ return fieldGetter;
+ }
+ return row -> {
+ if (row.isNullAt(fieldPos)) {
+ return null;
+ }
+ return fieldGetter.getFieldOrNull(row);
+ };
+ }
+
+ /** create a {@link GenericRow} from a {@link DataChangeEvent} for {@link PaimonWriter}. */
+ public static GenericRow convertEventToGenericRow(
+ DataChangeEvent dataChangeEvent, List<RecordData.FieldGetter> fieldGetters) {
+ GenericRow genericRow;
+ RecordData recordData;
+ switch (dataChangeEvent.op()) {
+ case INSERT:
+ case UPDATE:
+ case REPLACE:
+ {
+ recordData = dataChangeEvent.after();
+ genericRow = new GenericRow(RowKind.INSERT, recordData.getArity());
+ break;
+ }
+ case DELETE:
+ {
+ recordData = dataChangeEvent.before();
+ genericRow = new GenericRow(RowKind.DELETE, recordData.getArity());
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("don't support type of " + dataChangeEvent.op());
+ }
+ for (int i = 0; i < recordData.getArity(); i++) {
+ genericRow.setField(i, fieldGetters.get(i).getFieldOrNull(recordData));
+ }
+ return genericRow;
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PreCommitOperator.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PreCommitOperator.java
new file mode 100644
index 000000000..07a7e0cd7
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PreCommitOperator.java
@@ -0,0 +1,97 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.streaming.api.connector.sink2.CommittableMessage;
+import org.apache.flink.streaming.api.connector.sink2.CommittableSummary;
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** An Operator to add checkpointId to MultiTableCommittable and generate CommittableSummary. */
+public class PreCommitOperator
+ extends AbstractStreamOperator<CommittableMessage<MultiTableCommittable>>
+ implements OneInputStreamOperator<
+ CommittableMessage<MultiTableCommittable>,
+ CommittableMessage<MultiTableCommittable>> {
+
+ /** store a list of MultiTableCommittable in one checkpoint. */
+ private final List<MultiTableCommittable> results;
+
+ public PreCommitOperator() {
+ results = new ArrayList<>();
+ }
+
+ @Override
+ public void open() throws Exception {
+ super.open();
+ }
+
+ @Override
+ public void processElement(StreamRecord<CommittableMessage<MultiTableCommittable>> element) {
+ if (element.getValue() instanceof CommittableWithLineage) {
+ results.add(
+ ((CommittableWithLineage<MultiTableCommittable>) element.getValue())
+ .getCommittable());
+ }
+ }
+
+ @Override
+ public void finish() {
+ prepareSnapshotPreBarrier(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) {
+ // CommittableSummary should be sent before all CommittableWithLineage.
+ CommittableMessage<MultiTableCommittable> summary =
+ new CommittableSummary<>(
+ getRuntimeContext().getIndexOfThisSubtask(),
+ getRuntimeContext().getNumberOfParallelSubtasks(),
+ checkpointId,
+ results.size(),
+ results.size(),
+ 0);
+ output.collect(new StreamRecord<>(summary));
+
+ results.forEach(
+ committable -> {
+ // update the right checkpointId for MultiTableCommittable
+ MultiTableCommittable committableWithCheckPointId =
+ new MultiTableCommittable(
+ committable.getDatabase(),
+ committable.getTable(),
+ checkpointId,
+ committable.kind(),
+ committable.wrappedCommittable());
+ CommittableMessage<MultiTableCommittable> message =
+ new CommittableWithLineage<>(
+ committableWithCheckPointId,
+ checkpointId,
+ getRuntimeContext().getIndexOfThisSubtask());
+ output.collect(new StreamRecord<>(message));
+ });
+ results.clear();
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/StoreSinkWriteImpl.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/StoreSinkWriteImpl.java
new file mode 100644
index 000000000..3d49086b2
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/StoreSinkWriteImpl.java
@@ -0,0 +1,223 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.metrics.MetricGroup;
+import org.apache.flink.runtime.io.disk.iomanager.IOManager;
+import org.apache.flink.runtime.state.StateInitializationContext;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.disk.IOManagerImpl;
+import org.apache.paimon.flink.metrics.FlinkMetricRegistry;
+import org.apache.paimon.flink.sink.Committable;
+import org.apache.paimon.flink.sink.StoreSinkWrite;
+import org.apache.paimon.flink.sink.StoreSinkWriteState;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.memory.HeapMemorySegmentPool;
+import org.apache.paimon.memory.MemoryPoolFactory;
+import org.apache.paimon.memory.MemorySegmentPool;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.CommitMessage;
+import org.apache.paimon.table.sink.SinkRecord;
+import org.apache.paimon.table.sink.TableWriteImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
+/**
+ * copy from {@link org.apache.paimon.flink.sink.StoreSinkWriteImpl}. remove {@link
+ * StoreSinkWriteState} because we can't create it using {@link StateInitializationContext}.
+ */
+public class StoreSinkWriteImpl implements StoreSinkWrite {
+
+ private static final Logger LOG =
+ LoggerFactory.getLogger(org.apache.paimon.flink.sink.StoreSinkWriteImpl.class);
+
+ protected final String commitUser;
+ private final IOManagerImpl paimonIOManager;
+ private final boolean ignorePreviousFiles;
+ private final boolean waitCompaction;
+ private final boolean isStreamingMode;
+ @Nullable private final MemorySegmentPool memoryPool;
+ @Nullable private final MemoryPoolFactory memoryPoolFactory;
+
+ protected TableWriteImpl<?> write;
+
+ @Nullable private final MetricGroup metricGroup;
+
+ public StoreSinkWriteImpl(
+ FileStoreTable table,
+ String commitUser,
+ IOManager ioManager,
+ boolean ignorePreviousFiles,
+ boolean waitCompaction,
+ boolean isStreamingMode,
+ MemoryPoolFactory memoryPoolFactory,
+ @Nullable MetricGroup metricGroup) {
+ this(
+ table,
+ commitUser,
+ ioManager,
+ ignorePreviousFiles,
+ waitCompaction,
+ isStreamingMode,
+ null,
+ memoryPoolFactory,
+ metricGroup);
+ }
+
+ private StoreSinkWriteImpl(
+ FileStoreTable table,
+ String commitUser,
+ IOManager ioManager,
+ boolean ignorePreviousFiles,
+ boolean waitCompaction,
+ boolean isStreamingMode,
+ @Nullable MemorySegmentPool memoryPool,
+ @Nullable MemoryPoolFactory memoryPoolFactory,
+ @Nullable MetricGroup metricGroup) {
+ this.commitUser = commitUser;
+ this.paimonIOManager = new IOManagerImpl(ioManager.getSpillingDirectoriesPaths());
+ this.ignorePreviousFiles = ignorePreviousFiles;
+ this.waitCompaction = waitCompaction;
+ this.isStreamingMode = isStreamingMode;
+ this.memoryPool = memoryPool;
+ this.memoryPoolFactory = memoryPoolFactory;
+ this.metricGroup = metricGroup;
+ this.write = newTableWrite(table);
+ }
+
+ private TableWriteImpl<?> newTableWrite(FileStoreTable table) {
+ checkArgument(
+ !(memoryPool != null && memoryPoolFactory != null),
+ "memoryPool and memoryPoolFactory cannot be set at the same time.");
+
+ TableWriteImpl<?> tableWrite =
+ table.newWrite(commitUser, (part, bucket) -> true)
+ .withIOManager(paimonIOManager)
+ .withIgnorePreviousFiles(ignorePreviousFiles);
+
+ if (metricGroup != null) {
+ tableWrite.withMetricRegistry(new FlinkMetricRegistry(metricGroup));
+ }
+
+ if (memoryPoolFactory != null) {
+ return tableWrite.withMemoryPoolFactory(memoryPoolFactory);
+ } else {
+ return tableWrite.withMemoryPool(
+ memoryPool != null
+ ? memoryPool
+ : new HeapMemorySegmentPool(
+ table.coreOptions().writeBufferSize(),
+ table.coreOptions().pageSize()));
+ }
+ }
+
+ public void withCompactExecutor(ExecutorService compactExecutor) {
+ write.withCompactExecutor(compactExecutor);
+ }
+
+ @Override
+ public SinkRecord write(InternalRow internalRow) throws Exception {
+ return write.writeAndReturn(internalRow);
+ }
+
+ @Override
+ public SinkRecord write(InternalRow internalRow, int i) throws Exception {
+ return write.writeAndReturn(internalRow);
+ }
+
+ @Override
+ public SinkRecord toLogRecord(SinkRecord record) {
+ return write.toLogRecord(record);
+ }
+
+ @Override
+ public void compact(BinaryRow partition, int bucket, boolean fullCompaction) throws Exception {
+ write.compact(partition, bucket, fullCompaction);
+ }
+
+ @Override
+ public void notifyNewFiles(
+ long snapshotId, BinaryRow partition, int bucket, List<DataFileMeta> files) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug(
+ "Receive {} new files from snapshot {}, partition {}, bucket {}",
+ files.size(),
+ snapshotId,
+ partition,
+ bucket);
+ }
+ write.notifyNewFiles(snapshotId, partition, bucket, files);
+ }
+
+ @Override
+ public List<Committable> prepareCommit(boolean waitCompaction, long checkpointId)
+ throws IOException {
+ List<Committable> committables = new ArrayList<>();
+ if (write != null) {
+ try {
+ for (CommitMessage committable :
+ write.prepareCommit(this.waitCompaction || waitCompaction, checkpointId)) {
+ committables.add(
+ new Committable(checkpointId, Committable.Kind.FILE, committable));
+ }
+ } catch (Exception e) {
+ throw new IOException(e);
+ }
+ }
+ return committables;
+ }
+
+ @Override
+ public void snapshotState() {
+ // do nothing
+ }
+
+ @Override
+ public boolean streamingMode() {
+ return isStreamingMode;
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (write != null) {
+ write.close();
+ }
+
+ paimonIOManager.close();
+ }
+
+ @Override
+ public void replace(FileStoreTable newTable) throws Exception {
+ if (commitUser == null) {
+ return;
+ }
+ write.close();
+ write = newTableWrite(newTable);
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/TableSchemaInfo.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/TableSchemaInfo.java
new file mode 100644
index 000000000..26b4a1b12
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/java/org/apache/flink/cdc/connectors/paimon/sink/v2/TableSchemaInfo.java
@@ -0,0 +1,45 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.cdc.common.data.RecordData;
+import org.apache.flink.cdc.common.schema.Schema;
+
+import java.time.ZoneId;
+import java.util.List;
+
+/** Keep a list of {@link RecordData.FieldGetter} for a specific {@link Schema}. */
+public class TableSchemaInfo {
+
+ private final Schema schema;
+
+ private final List<RecordData.FieldGetter> fieldGetters;
+
+ public TableSchemaInfo(Schema schema, ZoneId zoneId) {
+ this.schema = schema;
+ this.fieldGetters = PaimonWriterHelper.createFieldGetters(schema, zoneId);
+ }
+
+ public Schema getSchema() {
+ return schema;
+ }
+
+ public List<RecordData.FieldGetter> getFieldGetters() {
+ return fieldGetters;
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/resources/META-INF/services/org.apache.flink.cdc.common.factories.Factory b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/resources/META-INF/services/org.apache.flink.cdc.common.factories.Factory
new file mode 100644
index 000000000..6ad058535
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/main/resources/META-INF/services/org.apache.flink.cdc.common.factories.Factory
@@ -0,0 +1,16 @@
+# 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.
+
+org.apache.flink.cdc.connectors.paimon.sink.PaimonDataSinkFactory
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkFactoryTest.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkFactoryTest.java
new file mode 100644
index 000000000..f2ab4a51b
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonDataSinkFactoryTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.connectors.paimon.sink;
+
+import org.apache.flink.cdc.common.configuration.Configuration;
+import org.apache.flink.cdc.common.factories.DataSinkFactory;
+import org.apache.flink.cdc.common.factories.FactoryHelper;
+import org.apache.flink.cdc.common.sink.DataSink;
+import org.apache.flink.cdc.composer.utils.FactoryDiscoveryUtils;
+
+import org.apache.flink.shaded.guava31.com.google.common.collect.ImmutableMap;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.File;
+import java.util.UUID;
+
+/** Tests for {@link PaimonDataSinkFactory}. */
+public class PaimonDataSinkFactoryTest {
+
+ @TempDir public static java.nio.file.Path temporaryFolder;
+
+ @Test
+ public void testCreateDataSink() {
+ DataSinkFactory sinkFactory =
+ FactoryDiscoveryUtils.getFactoryByIdentifier("paimon", DataSinkFactory.class);
+ Assertions.assertInstanceOf(PaimonDataSinkFactory.class, sinkFactory);
+
+ Configuration conf =
+ Configuration.fromMap(
+ ImmutableMap.<String, String>builder()
+ .put(PaimonDataSinkOptions.METASTORE.key(), "filesystem")
+ .put(
+ PaimonDataSinkOptions.WAREHOUSE.key(),
+ new File(
+ temporaryFolder.toFile(),
+ UUID.randomUUID().toString())
+ .toString())
+ .build());
+ DataSink dataSink =
+ sinkFactory.createDataSink(
+ new FactoryHelper.DefaultContext(
+ conf, conf, Thread.currentThread().getContextClassLoader()));
+ Assertions.assertInstanceOf(PaimonDataSink.class, dataSink);
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonMetadataApplierTest.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonMetadataApplierTest.java
new file mode 100644
index 000000000..dc24ff37f
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/PaimonMetadataApplierTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.connectors.paimon.sink;
+
+import org.apache.flink.cdc.common.event.AddColumnEvent;
+import org.apache.flink.cdc.common.event.AlterColumnTypeEvent;
+import org.apache.flink.cdc.common.event.CreateTableEvent;
+import org.apache.flink.cdc.common.event.DropColumnEvent;
+import org.apache.flink.cdc.common.event.RenameColumnEvent;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.schema.Column;
+import org.apache.flink.cdc.common.sink.MetadataApplier;
+import org.apache.flink.cdc.common.types.DataType;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+
+/** Tests for {@link PaimonMetadataApplier}. */
+public class PaimonMetadataApplierTest {
+
+ @TempDir public static java.nio.file.Path temporaryFolder;
+
+ private Catalog catalog;
+
+ private Options catalogOptions;
+
+ public static final String TEST_DATABASE = "test";
+
+ private static final String HADOOP_CONF_DIR =
+ Objects.requireNonNull(
+ Thread.currentThread()
+ .getContextClassLoader()
+ .getResource("hadoop-conf-dir"))
+ .getPath();
+
+ private static final String HIVE_CONF_DIR =
+ Objects.requireNonNull(
+ Thread.currentThread()
+ .getContextClassLoader()
+ .getResource("hive-conf-dir"))
+ .getPath();
+
+ private void initialize(String metastore)
+ throws Catalog.DatabaseNotEmptyException, Catalog.DatabaseNotExistException {
+ catalogOptions = new Options();
+ String warehouse =
+ new File(temporaryFolder.toFile(), UUID.randomUUID().toString()).toString();
+ if ("hive".equals(metastore)) {
+ catalogOptions.setString("hadoop-conf-dir", HADOOP_CONF_DIR);
+ catalogOptions.setString("hive-conf-dir", HIVE_CONF_DIR);
+ }
+ catalogOptions.setString("metastore", metastore);
+ catalogOptions.setString("warehouse", warehouse);
+ this.catalog = FlinkCatalogFactory.createPaimonCatalog(catalogOptions);
+ this.catalog.dropDatabase(TEST_DATABASE, true, true);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"filesystem", "hive"})
+ public void testApplySchemaChange(String metastore)
+ throws Catalog.TableNotExistException, Catalog.DatabaseNotEmptyException,
+ Catalog.DatabaseNotExistException {
+ initialize(metastore);
+ MetadataApplier metadataApplier = new PaimonMetadataApplier(catalogOptions);
+ CreateTableEvent createTableEvent =
+ new CreateTableEvent(
+ TableId.parse("test.table1"),
+ org.apache.flink.cdc.common.schema.Schema.newBuilder()
+ .physicalColumn(
+ "col1",
+ org.apache.flink.cdc.common.types.DataTypes.STRING()
+ .notNull())
+ .physicalColumn(
+ "col2", org.apache.flink.cdc.common.types.DataTypes.INT())
+ .primaryKey("col1")
+ .build());
+ metadataApplier.applySchemaChange(createTableEvent);
+ RowType tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(1, "col2", DataTypes.INT())));
+ Assertions.assertEquals(
+ tableSchema, catalog.getTable(Identifier.fromString("test.table1")).rowType());
+
+ List<AddColumnEvent.ColumnWithPosition> addedColumns = new ArrayList<>();
+ addedColumns.add(
+ new AddColumnEvent.ColumnWithPosition(
+ Column.physicalColumn(
+ "col3", org.apache.flink.cdc.common.types.DataTypes.STRING())));
+ AddColumnEvent addColumnEvent =
+ new AddColumnEvent(TableId.parse("test.table1"), addedColumns);
+ metadataApplier.applySchemaChange(addColumnEvent);
+ tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(1, "col2", DataTypes.INT()),
+ new DataField(2, "col3", DataTypes.STRING())));
+ Assertions.assertEquals(
+ tableSchema, catalog.getTable(Identifier.fromString("test.table1")).rowType());
+
+ Map<String, String> nameMapping = new HashMap<>();
+ nameMapping.put("col2", "newcol2");
+ nameMapping.put("col3", "newcol3");
+ RenameColumnEvent renameColumnEvent =
+ new RenameColumnEvent(TableId.parse("test.table1"), nameMapping);
+ metadataApplier.applySchemaChange(renameColumnEvent);
+ tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(1, "newcol2", DataTypes.INT()),
+ new DataField(2, "newcol3", DataTypes.STRING())));
+ Assertions.assertEquals(
+ tableSchema, catalog.getTable(Identifier.fromString("test.table1")).rowType());
+
+ Map<String, DataType> typeMapping = new HashMap<>();
+ typeMapping.put("newcol2", org.apache.flink.cdc.common.types.DataTypes.STRING());
+ AlterColumnTypeEvent alterColumnTypeEvent =
+ new AlterColumnTypeEvent(TableId.parse("test.table1"), typeMapping);
+ metadataApplier.applySchemaChange(alterColumnTypeEvent);
+ tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(1, "newcol2", DataTypes.STRING()),
+ new DataField(2, "newcol3", DataTypes.STRING())));
+ Assertions.assertEquals(
+ tableSchema, catalog.getTable(Identifier.fromString("test.table1")).rowType());
+
+ DropColumnEvent dropColumnEvent =
+ new DropColumnEvent(
+ TableId.parse("test.table1"), Collections.singletonList("newcol2"));
+ metadataApplier.applySchemaChange(dropColumnEvent);
+ // id of DataField should keep the same as before dropping column
+ tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(2, "newcol3", DataTypes.STRING())));
+ Assertions.assertEquals(
+ tableSchema, catalog.getTable(Identifier.fromString("test.table1")).rowType());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"filesystem", "hive"})
+ public void testCreateTableWithOptions(String metastore)
+ throws Catalog.TableNotExistException, Catalog.DatabaseNotEmptyException,
+ Catalog.DatabaseNotExistException {
+ initialize(metastore);
+ Map<String, String> tableOptions = new HashMap<>();
+ tableOptions.put("bucket", "-1");
+ Map<TableId, List<String>> partitionMaps = new HashMap<>();
+ partitionMaps.put(TableId.parse("test.table1"), Arrays.asList("col3", "col4"));
+ MetadataApplier metadataApplier =
+ new PaimonMetadataApplier(catalogOptions, tableOptions, partitionMaps);
+ CreateTableEvent createTableEvent =
+ new CreateTableEvent(
+ TableId.parse("test.table1"),
+ org.apache.flink.cdc.common.schema.Schema.newBuilder()
+ .physicalColumn(
+ "col1",
+ org.apache.flink.cdc.common.types.DataTypes.STRING()
+ .notNull())
+ .physicalColumn(
+ "col2",
+ org.apache.flink.cdc.common.types.DataTypes.STRING())
+ .physicalColumn(
+ "col3",
+ org.apache.flink.cdc.common.types.DataTypes.STRING())
+ .physicalColumn(
+ "col4",
+ org.apache.flink.cdc.common.types.DataTypes.STRING())
+ .primaryKey("col1")
+ .build());
+ metadataApplier.applySchemaChange(createTableEvent);
+ Table table = catalog.getTable(Identifier.fromString("test.table1"));
+ RowType tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(1, "col2", DataTypes.STRING()),
+ new DataField(2, "col3", DataTypes.STRING()),
+ new DataField(3, "col4", DataTypes.STRING())));
+ Assertions.assertEquals(tableSchema, table.rowType());
+ Assertions.assertEquals(Collections.singletonList("col1"), table.primaryKeys());
+ Assertions.assertEquals(Arrays.asList("col3", "col4"), table.partitionKeys());
+ Assertions.assertEquals("-1", table.options().get("bucket"));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"filesystem", "hive"})
+ public void testCreateTableWithAllDataTypes(String metastore)
+ throws Catalog.TableNotExistException, Catalog.DatabaseNotEmptyException,
+ Catalog.DatabaseNotExistException {
+ initialize(metastore);
+ MetadataApplier metadataApplier = new PaimonMetadataApplier(catalogOptions);
+ CreateTableEvent createTableEvent =
+ new CreateTableEvent(
+ TableId.parse("test.table1"),
+ org.apache.flink.cdc.common.schema.Schema.newBuilder()
+ .physicalColumn(
+ "col1",
+ org.apache.flink.cdc.common.types.DataTypes.STRING()
+ .notNull())
+ .physicalColumn(
+ "boolean",
+ org.apache.flink.cdc.common.types.DataTypes.BOOLEAN())
+ .physicalColumn(
+ "binary",
+ org.apache.flink.cdc.common.types.DataTypes.BINARY(3))
+ .physicalColumn(
+ "varbinary",
+ org.apache.flink.cdc.common.types.DataTypes.VARBINARY(10))
+ .physicalColumn(
+ "bytes",
+ org.apache.flink.cdc.common.types.DataTypes.BYTES())
+ .physicalColumn(
+ "tinyint",
+ org.apache.flink.cdc.common.types.DataTypes.TINYINT())
+ .physicalColumn(
+ "smallint",
+ org.apache.flink.cdc.common.types.DataTypes.SMALLINT())
+ .physicalColumn(
+ "int", org.apache.flink.cdc.common.types.DataTypes.INT())
+ .physicalColumn(
+ "float",
+ org.apache.flink.cdc.common.types.DataTypes.FLOAT())
+ .physicalColumn(
+ "double",
+ org.apache.flink.cdc.common.types.DataTypes.DOUBLE())
+ .physicalColumn(
+ "decimal",
+ org.apache.flink.cdc.common.types.DataTypes.DECIMAL(6, 3))
+ .physicalColumn(
+ "char", org.apache.flink.cdc.common.types.DataTypes.CHAR(5))
+ .physicalColumn(
+ "varchar",
+ org.apache.flink.cdc.common.types.DataTypes.VARCHAR(10))
+ .physicalColumn(
+ "string",
+ org.apache.flink.cdc.common.types.DataTypes.STRING())
+ .physicalColumn(
+ "date", org.apache.flink.cdc.common.types.DataTypes.DATE())
+ .physicalColumn(
+ "time", org.apache.flink.cdc.common.types.DataTypes.TIME())
+ .physicalColumn(
+ "time_with_precision",
+ org.apache.flink.cdc.common.types.DataTypes.TIME(6))
+ .physicalColumn(
+ "timestamp",
+ org.apache.flink.cdc.common.types.DataTypes.TIMESTAMP())
+ .physicalColumn(
+ "timestamp_with_precision",
+ org.apache.flink.cdc.common.types.DataTypes.TIMESTAMP(3))
+ .physicalColumn(
+ "timestamp_ltz",
+ org.apache.flink.cdc.common.types.DataTypes.TIMESTAMP_LTZ())
+ .physicalColumn(
+ "timestamp_ltz_with_precision",
+ org.apache.flink.cdc.common.types.DataTypes.TIMESTAMP_LTZ(
+ 3))
+ .primaryKey("col1")
+ .build());
+ metadataApplier.applySchemaChange(createTableEvent);
+ RowType tableSchema =
+ new RowType(
+ Arrays.asList(
+ new DataField(0, "col1", DataTypes.STRING().notNull()),
+ new DataField(1, "boolean", DataTypes.BOOLEAN()),
+ new DataField(2, "binary", DataTypes.BINARY(3)),
+ new DataField(3, "varbinary", DataTypes.VARBINARY(10)),
+ new DataField(4, "bytes", DataTypes.BYTES()),
+ new DataField(5, "tinyint", DataTypes.TINYINT()),
+ new DataField(6, "smallint", DataTypes.SMALLINT()),
+ new DataField(7, "int", DataTypes.INT()),
+ new DataField(8, "float", DataTypes.FLOAT()),
+ new DataField(9, "double", DataTypes.DOUBLE()),
+ new DataField(10, "decimal", DataTypes.DECIMAL(6, 3)),
+ new DataField(11, "char", DataTypes.CHAR(5)),
+ new DataField(12, "varchar", DataTypes.VARCHAR(10)),
+ new DataField(13, "string", DataTypes.STRING()),
+ new DataField(14, "date", DataTypes.DATE()),
+ new DataField(15, "time", DataTypes.TIME(0)),
+ new DataField(16, "time_with_precision", DataTypes.TIME(6)),
+ new DataField(17, "timestamp", DataTypes.TIMESTAMP(6)),
+ new DataField(
+ 18, "timestamp_with_precision", DataTypes.TIMESTAMP(3)),
+ new DataField(
+ 19,
+ "timestamp_ltz",
+ DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(6)),
+ new DataField(
+ 20,
+ "timestamp_ltz_with_precision",
+ DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3))));
+ Assertions.assertEquals(
+ tableSchema, catalog.getTable(Identifier.fromString("test.table1")).rowType());
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java
new file mode 100644
index 000000000..edc785e89
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonSinkITCase.java
@@ -0,0 +1,519 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.api.common.JobID;
+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.api.connector.sink2.Committer;
+import org.apache.flink.api.connector.sink2.Sink;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.event.AddColumnEvent;
+import org.apache.flink.cdc.common.event.CreateTableEvent;
+import org.apache.flink.cdc.common.event.DataChangeEvent;
+import org.apache.flink.cdc.common.event.DropColumnEvent;
+import org.apache.flink.cdc.common.event.Event;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.schema.Column;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.common.types.RowType;
+import org.apache.flink.cdc.connectors.paimon.sink.PaimonMetadataApplier;
+import org.apache.flink.cdc.runtime.typeutils.BinaryRecordDataGenerator;
+import org.apache.flink.metrics.MetricGroup;
+import org.apache.flink.metrics.groups.SinkWriterMetricGroup;
+import org.apache.flink.streaming.runtime.operators.sink.committables.CommitRequestImpl;
+import org.apache.flink.table.api.EnvironmentSettings;
+import org.apache.flink.table.api.TableEnvironment;
+import org.apache.flink.types.Row;
+import org.apache.flink.types.RowKind;
+import org.apache.flink.util.UserCodeClassLoader;
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.flink.FlinkCatalogFactory;
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+import org.apache.paimon.options.Options;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.OptionalLong;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/** An ITCase for {@link PaimonWriter} and {@link PaimonCommitter}. */
+public class PaimonSinkITCase {
+
+ @TempDir public static java.nio.file.Path temporaryFolder;
+
+ private Options catalogOptions;
+
+ private TableEnvironment tEnv;
+
+ private String warehouse;
+
+ private TableId table1;
+
+ private BinaryRecordDataGenerator generator;
+
+ public static final String TEST_DATABASE = "test";
+ private static final String HADOOP_CONF_DIR =
+ Objects.requireNonNull(
+ Thread.currentThread()
+ .getContextClassLoader()
+ .getResource("hadoop-conf-dir"))
+ .getPath();
+
+ private static final String HIVE_CONF_DIR =
+ Objects.requireNonNull(
+ Thread.currentThread()
+ .getContextClassLoader()
+ .getResource("hive-conf-dir"))
+ .getPath();
+
+ private void initialize(String metastore)
+ throws Catalog.DatabaseNotEmptyException, Catalog.DatabaseNotExistException {
+ tEnv = TableEnvironment.create(EnvironmentSettings.newInstance().inBatchMode().build());
+ warehouse = new File(temporaryFolder.toFile(), UUID.randomUUID().toString()).toString();
+ catalogOptions = new Options();
+ catalogOptions.setString("metastore", metastore);
+ catalogOptions.setString("warehouse", warehouse);
+ table1 = TableId.tableId("test", "table1");
+ if ("hive".equals(metastore)) {
+ catalogOptions.setString("hadoop-conf-dir", HADOOP_CONF_DIR);
+ catalogOptions.setString("hive-conf-dir", HIVE_CONF_DIR);
+ tEnv.executeSql(
+ String.format(
+ "CREATE CATALOG paimon_catalog WITH ("
+ + "'type'='paimon', "
+ + "'warehouse'='%s', "
+ + "'metastore'='hive', "
+ + "'hadoop-conf-dir'='%s', "
+ + "'hive-conf-dir'='%s' "
+ + ")",
+ warehouse, HADOOP_CONF_DIR, HIVE_CONF_DIR));
+ } else {
+ tEnv.executeSql(
+ String.format(
+ "CREATE CATALOG paimon_catalog WITH ('type'='paimon', 'warehouse'='%s')",
+ warehouse));
+ }
+ FlinkCatalogFactory.createPaimonCatalog(catalogOptions)
+ .dropDatabase(TEST_DATABASE, true, true);
+ }
+
+ private List<Event> createTestEvents() {
+ List<Event> testEvents = new ArrayList<>();
+ // create table
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("col1", DataTypes.STRING())
+ .physicalColumn("col2", DataTypes.STRING())
+ .primaryKey("col1")
+ .build();
+ CreateTableEvent createTableEvent = new CreateTableEvent(table1, schema);
+ testEvents.add(createTableEvent);
+ PaimonMetadataApplier metadataApplier = new PaimonMetadataApplier(catalogOptions);
+ metadataApplier.applySchemaChange(createTableEvent);
+
+ generator =
+ new BinaryRecordDataGenerator(RowType.of(DataTypes.STRING(), DataTypes.STRING()));
+ // insert
+ DataChangeEvent insertEvent1 =
+ DataChangeEvent.insertEvent(
+ table1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("1"),
+ BinaryStringData.fromString("1")
+ }));
+ testEvents.add(insertEvent1);
+ DataChangeEvent insertEvent2 =
+ DataChangeEvent.insertEvent(
+ table1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("2"),
+ BinaryStringData.fromString("2")
+ }));
+ testEvents.add(insertEvent2);
+ return testEvents;
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"filesystem", "hive"})
+ public void testSinkWithDataChange(String metastore)
+ throws IOException, InterruptedException, Catalog.DatabaseNotEmptyException,
+ Catalog.DatabaseNotExistException {
+ initialize(metastore);
+ PaimonSink<Event> paimonSink =
+ new PaimonSink<>(
+ catalogOptions, new PaimonRecordEventSerializer(ZoneId.systemDefault()));
+ PaimonWriter<Event> writer = paimonSink.createWriter(new MockInitContext());
+ Committer<MultiTableCommittable> committer = paimonSink.createCommitter();
+
+ // insert
+ for (Event event : createTestEvents()) {
+ writer.write(event, null);
+ }
+ writer.flush(false);
+ Collection<Committer.CommitRequest<MultiTableCommittable>> commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ List<Row> result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Arrays.asList(
+ Row.ofKind(RowKind.INSERT, "1", "1"), Row.ofKind(RowKind.INSERT, "2", "2")),
+ result);
+
+ // delete
+ Event event =
+ DataChangeEvent.deleteEvent(
+ TableId.tableId("test", "table1"),
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("1"),
+ BinaryStringData.fromString("1")
+ }));
+ writer.write(event, null);
+ writer.flush(false);
+ commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Collections.singletonList(Row.ofKind(RowKind.INSERT, "2", "2")), result);
+
+ // update
+ event =
+ DataChangeEvent.updateEvent(
+ TableId.tableId("test", "table1"),
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("2"),
+ BinaryStringData.fromString("2")
+ }),
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("2"),
+ BinaryStringData.fromString("x")
+ }));
+ writer.write(event, null);
+ writer.flush(false);
+ commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Collections.singletonList(Row.ofKind(RowKind.INSERT, "2", "x")), result);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"filesystem", "hive"})
+ public void testSinkWithSchemaChange(String metastore)
+ throws IOException, InterruptedException, Catalog.DatabaseNotEmptyException,
+ Catalog.DatabaseNotExistException {
+ initialize(metastore);
+ PaimonSink<Event> paimonSink =
+ new PaimonSink(
+ catalogOptions, new PaimonRecordEventSerializer(ZoneId.systemDefault()));
+ PaimonWriter<Event> writer = paimonSink.createWriter(new MockInitContext());
+ Committer<MultiTableCommittable> committer = paimonSink.createCommitter();
+
+ // 1. receive only DataChangeEvents during one checkpoint
+ for (Event event : createTestEvents()) {
+ writer.write(event, null);
+ }
+ writer.flush(false);
+ Collection<Committer.CommitRequest<MultiTableCommittable>> commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ List<Row> result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Arrays.asList(
+ Row.ofKind(RowKind.INSERT, "1", "1"), Row.ofKind(RowKind.INSERT, "2", "2")),
+ result);
+
+ // 2. receive DataChangeEvents and SchemaChangeEvents during one checkpoint
+ DataChangeEvent insertEvent3 =
+ DataChangeEvent.insertEvent(
+ table1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("3"),
+ BinaryStringData.fromString("3")
+ }));
+ writer.write(insertEvent3, null);
+ writer.flush(false);
+
+ // add column
+ AddColumnEvent.ColumnWithPosition columnWithPosition =
+ new AddColumnEvent.ColumnWithPosition(
+ Column.physicalColumn("col3", DataTypes.STRING()));
+ AddColumnEvent addColumnEvent =
+ new AddColumnEvent(table1, Collections.singletonList(columnWithPosition));
+ PaimonMetadataApplier metadataApplier = new PaimonMetadataApplier(catalogOptions);
+ writer.write(addColumnEvent, null);
+ metadataApplier.applySchemaChange(addColumnEvent);
+ generator =
+ new BinaryRecordDataGenerator(
+ RowType.of(DataTypes.STRING(), DataTypes.STRING(), DataTypes.STRING()));
+ DataChangeEvent insertEvent4 =
+ DataChangeEvent.insertEvent(
+ table1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("4"),
+ BinaryStringData.fromString("4"),
+ BinaryStringData.fromString("4")
+ }));
+ writer.write(insertEvent4, null);
+ writer.flush(false);
+ commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Arrays.asList(
+ Row.ofKind(RowKind.INSERT, "1", "1", null),
+ Row.ofKind(RowKind.INSERT, "2", "2", null),
+ Row.ofKind(RowKind.INSERT, "3", "3", null),
+ Row.ofKind(RowKind.INSERT, "4", "4", "4")),
+ result);
+
+ // 2. receive DataChangeEvents and SchemaChangeEvents during one checkpoint
+ DataChangeEvent insertEvent5 =
+ DataChangeEvent.insertEvent(
+ table1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("5"),
+ BinaryStringData.fromString("5"),
+ BinaryStringData.fromString("5")
+ }));
+ writer.write(insertEvent5, null);
+ writer.flush(false);
+ // drop column
+ DropColumnEvent dropColumnEvent =
+ new DropColumnEvent(table1, Collections.singletonList("col2"));
+ metadataApplier.applySchemaChange(dropColumnEvent);
+ writer.write(dropColumnEvent, null);
+ generator =
+ new BinaryRecordDataGenerator(RowType.of(DataTypes.STRING(), DataTypes.STRING()));
+ DataChangeEvent insertEvent6 =
+ DataChangeEvent.insertEvent(
+ table1,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("6"),
+ BinaryStringData.fromString("6")
+ }));
+ writer.write(insertEvent6, null);
+ writer.flush(false);
+ commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Arrays.asList(
+ Row.ofKind(RowKind.INSERT, "1", null),
+ Row.ofKind(RowKind.INSERT, "2", null),
+ Row.ofKind(RowKind.INSERT, "3", null),
+ Row.ofKind(RowKind.INSERT, "4", "4"),
+ Row.ofKind(RowKind.INSERT, "5", "5"),
+ Row.ofKind(RowKind.INSERT, "6", "6")),
+ result);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"filesystem", "hive"})
+ public void testSinkWithMultiTables(String metastore)
+ throws IOException, InterruptedException, Catalog.DatabaseNotEmptyException,
+ Catalog.DatabaseNotExistException {
+ initialize(metastore);
+ PaimonSink<Event> paimonSink =
+ new PaimonSink<>(
+ catalogOptions, new PaimonRecordEventSerializer(ZoneId.systemDefault()));
+ PaimonWriter<Event> writer = paimonSink.createWriter(new MockInitContext());
+ Committer<MultiTableCommittable> committer = paimonSink.createCommitter();
+ List<Event> testEvents = createTestEvents();
+ // create table
+ TableId table2 = TableId.tableId("test", "table2");
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("col1", DataTypes.STRING())
+ .physicalColumn("col2", DataTypes.STRING())
+ .primaryKey("col1")
+ .build();
+ CreateTableEvent createTableEvent = new CreateTableEvent(table2, schema);
+ testEvents.add(createTableEvent);
+ PaimonMetadataApplier metadataApplier = new PaimonMetadataApplier(catalogOptions);
+ metadataApplier.applySchemaChange(createTableEvent);
+ // insert
+ DataChangeEvent insertEvent1 =
+ DataChangeEvent.insertEvent(
+ table2,
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("1"),
+ BinaryStringData.fromString("1")
+ }));
+ testEvents.add(insertEvent1);
+
+ // insert
+ for (Event event : testEvents) {
+ writer.write(event, null);
+ }
+ writer.flush(false);
+ Collection<Committer.CommitRequest<MultiTableCommittable>> commitRequests =
+ writer.prepareCommit().stream()
+ .map(MockCommitRequestImpl::new)
+ .collect(Collectors.toList());
+ committer.commit(commitRequests);
+ List<Row> result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table1")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Arrays.asList(
+ Row.ofKind(RowKind.INSERT, "1", "1"), Row.ofKind(RowKind.INSERT, "2", "2")),
+ result);
+ result = new ArrayList<>();
+ tEnv.sqlQuery("select * from paimon_catalog.test.table2")
+ .execute()
+ .collect()
+ .forEachRemaining(result::add);
+ Assertions.assertEquals(
+ Collections.singletonList(Row.ofKind(RowKind.INSERT, "1", "1")), result);
+ }
+
+ private static class MockCommitRequestImpl<CommT> extends CommitRequestImpl<CommT> {
+
+ protected MockCommitRequestImpl(CommT committable) {
+ super(committable);
+ }
+ }
+
+ private static class MockInitContext
+ implements Sink.InitContext, SerializationSchema.InitializationContext {
+
+ private MockInitContext() {}
+
+ public UserCodeClassLoader getUserCodeClassLoader() {
+ return null;
+ }
+
+ public MailboxExecutor getMailboxExecutor() {
+ return null;
+ }
+
+ public ProcessingTimeService getProcessingTimeService() {
+ return null;
+ }
+
+ public int getSubtaskId() {
+ return 0;
+ }
+
+ public int getNumberOfParallelSubtasks() {
+ return 0;
+ }
+
+ public int getAttemptNumber() {
+ return 0;
+ }
+
+ public SinkWriterMetricGroup metricGroup() {
+ return null;
+ }
+
+ public MetricGroup getMetricGroup() {
+ return null;
+ }
+
+ public OptionalLong getRestoredCheckpointId() {
+ return OptionalLong.empty();
+ }
+
+ public SerializationSchema.InitializationContext
+ asSerializationSchemaInitializationContext() {
+ return this;
+ }
+
+ public boolean isObjectReuseEnabled() {
+ return false;
+ }
+
+ public <IN> TypeSerializer<IN> createInputSerializer() {
+ return null;
+ }
+
+ public JobID getJobId() {
+ return null;
+ }
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriterHelperTest.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriterHelperTest.java
new file mode 100644
index 000000000..d432e3fe2
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/PaimonWriterHelperTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.cdc.common.data.DecimalData;
+import org.apache.flink.cdc.common.data.LocalZonedTimestampData;
+import org.apache.flink.cdc.common.data.RecordData;
+import org.apache.flink.cdc.common.data.TimestampData;
+import org.apache.flink.cdc.common.data.binary.BinaryRecordData;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.event.DataChangeEvent;
+import org.apache.flink.cdc.common.event.TableId;
+import org.apache.flink.cdc.common.schema.Schema;
+import org.apache.flink.cdc.common.types.DataTypes;
+import org.apache.flink.cdc.common.types.RowType;
+import org.apache.flink.cdc.runtime.typeutils.BinaryRecordDataGenerator;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.types.RowKind;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.util.List;
+
+/** Tests for {@link PaimonWriterHelper}. */
+public class PaimonWriterHelperTest {
+
+ @Test
+ public void testConvertEventToGenericRowOfAllDataTypes() {
+ RowType rowType =
+ RowType.of(
+ DataTypes.BOOLEAN(),
+ DataTypes.BINARY(3),
+ DataTypes.VARBINARY(10),
+ DataTypes.BYTES(),
+ DataTypes.TINYINT(),
+ DataTypes.SMALLINT(),
+ DataTypes.INT(),
+ DataTypes.BIGINT(),
+ DataTypes.FLOAT(),
+ DataTypes.DOUBLE(),
+ DataTypes.DECIMAL(6, 3),
+ DataTypes.CHAR(5),
+ DataTypes.VARCHAR(10),
+ DataTypes.STRING(),
+ DataTypes.DATE(),
+ DataTypes.TIME(),
+ DataTypes.TIME(6),
+ DataTypes.TIMESTAMP(),
+ DataTypes.TIMESTAMP(3),
+ DataTypes.TIMESTAMP_LTZ(),
+ DataTypes.TIMESTAMP_LTZ(3),
+ DataTypes.STRING());
+ Object[] testData =
+ new Object[] {
+ true,
+ new byte[] {1, 2},
+ new byte[] {3, 4},
+ new byte[] {5, 6, 7},
+ (byte) 1,
+ (short) 2,
+ 3,
+ 4L,
+ 5.1f,
+ 6.2,
+ DecimalData.fromBigDecimal(new BigDecimal("7.123"), 6, 3),
+ BinaryStringData.fromString("test1"),
+ BinaryStringData.fromString("test2"),
+ BinaryStringData.fromString("test3"),
+ 100,
+ 200,
+ 300,
+ TimestampData.fromTimestamp(
+ java.sql.Timestamp.valueOf("2023-01-01 00:00:00.000")),
+ TimestampData.fromTimestamp(java.sql.Timestamp.valueOf("2023-01-01 00:00:00")),
+ LocalZonedTimestampData.fromInstant(Instant.parse("2023-01-01T00:00:00.000Z")),
+ LocalZonedTimestampData.fromInstant(Instant.parse("2023-01-01T00:00:00.000Z")),
+ null
+ };
+ BinaryRecordData recordData = new BinaryRecordDataGenerator(rowType).generate(testData);
+ Schema schema = Schema.newBuilder().fromRowDataType(rowType).build();
+ List<RecordData.FieldGetter> fieldGetters =
+ PaimonWriterHelper.createFieldGetters(schema, ZoneId.of("UTC+8"));
+ DataChangeEvent dataChangeEvent =
+ DataChangeEvent.insertEvent(TableId.parse("database.table"), recordData);
+ GenericRow genericRow =
+ PaimonWriterHelper.convertEventToGenericRow(dataChangeEvent, fieldGetters);
+ Assertions.assertEquals(
+ GenericRow.ofKind(
+ RowKind.INSERT,
+ true,
+ new byte[] {1, 2},
+ new byte[] {3, 4},
+ new byte[] {5, 6, 7},
+ (byte) 1,
+ (short) 2,
+ 3,
+ 4L,
+ 5.1f,
+ 6.2,
+ Decimal.fromBigDecimal(new BigDecimal("7.123"), 6, 3),
+ BinaryString.fromString("test1"),
+ BinaryString.fromString("test2"),
+ BinaryString.fromString("test3"),
+ 100,
+ 200,
+ 300,
+ Timestamp.fromSQLTimestamp(
+ java.sql.Timestamp.valueOf("2023-01-01 00:00:00.000")),
+ Timestamp.fromSQLTimestamp(
+ java.sql.Timestamp.valueOf("2023-01-01 00:00:00")),
+ // plus 8 hours.
+ Timestamp.fromInstant(Instant.parse("2023-01-01T08:00:00.000Z")),
+ Timestamp.fromInstant(Instant.parse("2023-01-01T08:00:00.000Z")),
+ null),
+ genericRow);
+ }
+
+ @Test
+ public void testConvertEventToGenericRowOfDataChangeTypes() {
+ Schema schema =
+ Schema.newBuilder()
+ .physicalColumn("col1", DataTypes.STRING())
+ .physicalColumn("col2", DataTypes.STRING())
+ .build();
+ List<RecordData.FieldGetter> fieldGetters =
+ PaimonWriterHelper.createFieldGetters(schema, ZoneId.systemDefault());
+ TableId tableId = TableId.parse("database.table");
+ BinaryRecordDataGenerator generator =
+ new BinaryRecordDataGenerator(RowType.of(DataTypes.STRING(), DataTypes.STRING()));
+ BinaryRecordData recordData =
+ generator.generate(
+ new Object[] {
+ BinaryStringData.fromString("1"), BinaryStringData.fromString("1")
+ });
+
+ DataChangeEvent dataChangeEvent = DataChangeEvent.insertEvent(tableId, recordData);
+ GenericRow genericRow =
+ PaimonWriterHelper.convertEventToGenericRow(dataChangeEvent, fieldGetters);
+ Assertions.assertEquals(genericRow.getRowKind(), RowKind.INSERT);
+
+ dataChangeEvent = DataChangeEvent.deleteEvent(tableId, recordData);
+ genericRow = PaimonWriterHelper.convertEventToGenericRow(dataChangeEvent, fieldGetters);
+ Assertions.assertEquals(genericRow.getRowKind(), RowKind.DELETE);
+
+ dataChangeEvent = DataChangeEvent.updateEvent(tableId, recordData, recordData);
+ genericRow = PaimonWriterHelper.convertEventToGenericRow(dataChangeEvent, fieldGetters);
+ Assertions.assertEquals(genericRow.getRowKind(), RowKind.INSERT);
+
+ dataChangeEvent = DataChangeEvent.replaceEvent(tableId, recordData, null);
+ genericRow = PaimonWriterHelper.convertEventToGenericRow(dataChangeEvent, fieldGetters);
+ Assertions.assertEquals(genericRow.getRowKind(), RowKind.INSERT);
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/TestMultiTableCommittableChannelComputer.java b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/TestMultiTableCommittableChannelComputer.java
new file mode 100644
index 000000000..2c0a9b4cb
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/java/org/apache/flink/cdc/connectors/paimon/sink/v2/TestMultiTableCommittableChannelComputer.java
@@ -0,0 +1,72 @@
+/*
+ * 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.connectors.paimon.sink.v2;
+
+import org.apache.flink.streaming.api.connector.sink2.CommittableWithLineage;
+
+import org.apache.paimon.flink.sink.MultiTableCommittable;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** A test for {@link MultiTableCommittableChannelComputer}. */
+public class TestMultiTableCommittableChannelComputer {
+
+ @Test
+ public void testChannel() {
+ MultiTableCommittableChannelComputer computer = new MultiTableCommittableChannelComputer();
+ computer.setup(4);
+ List<MultiTableCommittable> commits =
+ Arrays.asList(
+ new MultiTableCommittable("database", "table1", 1L, null, null),
+ new MultiTableCommittable("database", "table2", 1L, null, null),
+ new MultiTableCommittable("database", "table1", 1L, null, null),
+ new MultiTableCommittable("database", "table5", 1L, null, null),
+ new MultiTableCommittable("database", "table3", 1L, null, null),
+ new MultiTableCommittable("database", "table8", 1L, null, null),
+ new MultiTableCommittable("database", "table5", 1L, null, null),
+ new MultiTableCommittable("database", "table1", 1L, null, null),
+ new MultiTableCommittable("database", "table9", 1L, null, null),
+ new MultiTableCommittable("database", "table5", 1L, null, null),
+ new MultiTableCommittable("database", "table3", 1L, null, null),
+ new MultiTableCommittable("database", "table8", 1L, null, null));
+ Map<Integer, Set<String>> map = new HashMap<>();
+ commits.forEach(
+ (commit) -> {
+ int channel = computer.channel(new CommittableWithLineage<>(commit, 1L, 0));
+ Set<String> set = map.getOrDefault(channel, new HashSet<>());
+ set.add(commit.getTable());
+ map.put(channel, set);
+ });
+ Set<String> actualtables = new HashSet<>();
+ for (Map.Entry<Integer, Set<String>> entry : map.entrySet()) {
+ actualtables.addAll(entry.getValue());
+ }
+ Set<String> expectedTables =
+ new HashSet<>(
+ Arrays.asList("table1", "table2", "table3", "table5", "table8", "table9"));
+ // Not a table is appeared in more than one channel.
+ Assertions.assertEquals(actualtables, expectedTables);
+ }
+}
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/hadoop-conf-dir/core-site.xml b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/hadoop-conf-dir/core-site.xml
new file mode 100644
index 000000000..38b1c4202
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/hadoop-conf-dir/core-site.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<!--Autogenerated by Cloudera Manager-->
+<configuration>
+ <property>
+ <name>fs.defaultFS</name>
+ <value>file:///</value>
+ </property>
+ <property>
+ <name>fs.trash.interval</name>
+ <value>1</value>
+ </property>
+ <property>
+ <name>io.compression.codecs</name>
+ <value>org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.BZip2Codec,org.apache.hadoop.io.compress.DeflateCodec,org.apache.hadoop.io.compress.SnappyCodec,org.apache.hadoop.io.compress.Lz4Codec</value>
+ </property>
+ <property>
+ <name>hadoop.security.authentication</name>
+ <value>none</value>
+ </property>
+</configuration>
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/hive-conf-dir/hive-site.xml b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/hive-conf-dir/hive-site.xml
new file mode 100644
index 000000000..845ff9f62
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/hive-conf-dir/hive-site.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
+<!--
+ 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.
+-->
+
+<configuration>
+
+ <property>
+ <name>hive.metastore.schema.verification</name>
+ <value>false</value>
+ </property>
+
+ <property>
+ <name>hive.metastore.client.capability.check</name>
+ <value>false</value>
+ </property>
+
+ <property>
+ <name>datanucleus.schema.autoCreateTables</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>datanucleus.schema.autoCreateAll</name>
+ <value>true</value>
+ </property>
+
+ <property>
+ <name>javax.jdo.option.ConnectionURL</name>
+ <value>jdbc:derby:memory:test;create=true</value>
+ </property>
+
+</configuration>
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/log4j2-test.properties b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/log4j2-test.properties
new file mode 100644
index 000000000..8f6c07473
--- /dev/null
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/flink-cdc-pipeline-connector-paimon/src/test/resources/log4j2-test.properties
@@ -0,0 +1,25 @@
+################################################################################
+# 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.
+################################################################################
+
+# Set root logger level to OFF to not flood build logs
+# set manually to INFO for debugging purposes
+rootLogger.level=OFF
+rootLogger.appenderRef.test.ref = TestLogger
+
+appender.testlogger.name = TestLogger
+appender.testlogger.type = CONSOLE
+appender.testlogger.target = SYSTEM_ERR
+appender.testlogger.layout.type = PatternLayout
+appender.testlogger.layout.pattern = %-4r [%t] %-5p %c - %m%n
diff --git a/flink-cdc-connect/flink-cdc-pipeline-connectors/pom.xml b/flink-cdc-connect/flink-cdc-pipeline-connectors/pom.xml
index c5265af9f..424372f4c 100644
--- a/flink-cdc-connect/flink-cdc-pipeline-connectors/pom.xml
+++ b/flink-cdc-connect/flink-cdc-pipeline-connectors/pom.xml
@@ -33,6 +33,7 @@ limitations under the License.
<module>flink-cdc-pipeline-connector-doris</module>
<module>flink-cdc-pipeline-connector-starrocks</module>
<module>flink-cdc-pipeline-connector-kafka</module>
+ <module>flink-cdc-pipeline-connector-paimon</module>
</modules>
<dependencies>