This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new be5d0f0279 [Feature][Connector-V2] Add Google Cloud Bigtable Source
and Sink con… (#10849)
be5d0f0279 is described below
commit be5d0f0279d72ba6f6b64e22f44bfc4391117816
Author: 雷炯 <[email protected]>
AuthorDate: Sat Jun 20 13:04:10 2026 +0800
[Feature][Connector-V2] Add Google Cloud Bigtable Source and Sink con…
(#10849)
Co-authored-by: leijiong <[email protected]>
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
Co-authored-by: Cursor <[email protected]>
---
.dlc.json | 3 +-
config/plugin_config | 1 +
.../changelog/connector-google-bigtable.md | 7 +
docs/en/connectors/sink/GoogleBigtable.md | 176 ++++++++++++
docs/en/connectors/source/GoogleBigtable.md | 139 ++++++++++
.../changelog/connector-google-bigtable.md | 7 +
docs/zh/connectors/sink/GoogleBigtable.md | 105 ++++++++
docs/zh/connectors/source/GoogleBigtable.md | 59 ++++
plugin-mapping.properties | 2 +
.../connector-google-bigtable/pom.xml | 64 +++++
.../seatunnel/bigtable/client/BigtableClient.java | 173 ++++++++++++
.../bigtable/config/BigtableBaseOptions.java | 61 +++++
.../bigtable/config/BigtableParameters.java | 94 +++++++
.../bigtable/config/BigtableSinkOptions.java | 90 +++++++
.../bigtable/config/BigtableSourceOptions.java | 64 +++++
.../bigtable/constant/BigtableIdentifier.java | 24 ++
.../exception/BigtableConnectorErrorCode.java | 51 ++++
.../exception/BigtableConnectorException.java | 32 +++
.../format/BigtableDeserializationFormat.java | 119 ++++++++
.../seatunnel/bigtable/sink/BigtableSink.java | 127 +++++++++
.../bigtable/sink/BigtableSinkFactory.java | 67 +++++
.../bigtable/sink/BigtableSinkWriter.java | 298 +++++++++++++++++++++
.../seatunnel/bigtable/source/BigtableSource.java | 81 ++++++
.../bigtable/source/BigtableSourceFactory.java | 78 ++++++
.../bigtable/source/BigtableSourceReader.java | 254 ++++++++++++++++++
.../bigtable/source/BigtableSourceSplit.java | 60 +++++
.../source/BigtableSourceSplitEnumerator.java | 176 ++++++++++++
.../bigtable/source/BigtableSourceState.java | 36 +++
.../state/BigtableAggregatedCommitInfo.java | 24 ++
.../bigtable/state/BigtableCommitInfo.java | 24 ++
.../bigtable/state/BigtableSinkState.java | 24 ++
.../bigtable/sink/BigtableSinkSaveModeTest.java | 153 +++++++++++
.../bigtable/sink/BigtableSinkWriterTest.java | 129 +++++++++
.../bigtable/source/BigtableSourceReaderTest.java | 267 ++++++++++++++++++
.../source/BigtableSourceSplitEnumeratorTest.java | 40 +++
seatunnel-connectors-v2/pom.xml | 1 +
seatunnel-dist/pom.xml | 6 +
37 files changed, 3115 insertions(+), 1 deletion(-)
diff --git a/.dlc.json b/.dlc.json
index b25f2512f2..a43af66641 100644
--- a/.dlc.json
+++ b/.dlc.json
@@ -42,6 +42,7 @@
0,
200,
401,
- 403
+ 403,
+ 406
]
}
diff --git a/config/plugin_config b/config/plugin_config
index fc6bf5c7c2..5f560a4a46 100644
--- a/config/plugin_config
+++ b/config/plugin_config
@@ -47,6 +47,7 @@ connector-file-sftp
connector-file-obs
connector-google-sheets
connector-google-firestore
+connector-google-bigtable
connector-graphql
connector-hive
connector-http-base
diff --git a/docs/en/connectors/changelog/connector-google-bigtable.md
b/docs/en/connectors/changelog/connector-google-bigtable.md
new file mode 100644
index 0000000000..3fc3a897f9
--- /dev/null
+++ b/docs/en/connectors/changelog/connector-google-bigtable.md
@@ -0,0 +1,7 @@
+<details><summary> Change Log </summary>
+
+| Change | Commit | Version |
+| --- | --- | --- |
+|[Feature][Connector-V2] Add Google Cloud Bigtable Source and Sink
connector|https://github.com/apache/seatunnel/commit/8e57c04|dev|
+
+</details>
diff --git a/docs/en/connectors/sink/GoogleBigtable.md
b/docs/en/connectors/sink/GoogleBigtable.md
new file mode 100644
index 0000000000..55b96cf398
--- /dev/null
+++ b/docs/en/connectors/sink/GoogleBigtable.md
@@ -0,0 +1,176 @@
+import ChangeLog from '../changelog/connector-google-bigtable.md';
+
+# GoogleBigtable
+
+> Google Bigtable sink connector
+
+## Description
+
+Writes data to Google Cloud Bigtable using the native Bigtable Data v2 Java
client.
+
+## Key features
+
+- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+
+## Options
+
+| name | type | required | default value |
+|---------------------|---------|----------|---------------|
+| project_id | string | yes | - |
+| instance_id | string | yes | - |
+| table | string | yes | - |
+| rowkey_column | list | yes | - |
+| column_family | config | yes | - |
+| credentials_path | string | no | - |
+| rowkey_delimiter | string | no | "" |
+| version_column | string | no | - |
+| null_mode | string | no | skip |
+| batch_mutation_size | int | no | 100 |
+| common-options | | no | - |
+
+### project_id [string]
+
+Google Cloud project ID. Example: `"my-gcp-project"`
+
+### instance_id [string]
+
+Bigtable instance ID. Example: `"my-bigtable-instance"`
+
+### table [string]
+
+The Bigtable table name to write to. Example: `"my-table"`
+
+### rowkey_column [list]
+
+Column names used to compose the Bigtable row key. Example: `["id"]` or
`["tenant", "id"]`.
+
+When multiple columns are specified they are joined with `rowkey_delimiter`.
+
+### column_family [config]
+
+Mapping from column name to column family name. Use `all_columns` as key to
set a default family for all unmapped columns.
+
+```hocon
+column_family {
+ name = "info"
+ age = "stats"
+}
+```
+
+or to put everything in one family:
+
+```hocon
+column_family {
+ all_columns = "cf"
+}
+```
+
+### credentials_path [string]
+
+Path to the Google Cloud service account JSON key file.
+
+If not set, [Application Default Credentials
(ADC)](https://cloud.google.com/docs/authentication/application-default-credentials)
will be used — this works automatically on GCE/GKE or when
`GOOGLE_APPLICATION_CREDENTIALS` is set in the environment.
+
+### rowkey_delimiter [string]
+
+Delimiter used to join multiple row-key column values. Default is `""` (empty
string, no delimiter).
+
+### version_column [string]
+
+Column name whose `BIGINT` value is used as the Bigtable cell timestamp
(microseconds since epoch). If not set, the current system time is used.
+
+### null_mode [string]
+
+How to handle `null` field values. Supported: `skip` (default), `empty`.
+
+- `skip` — the cell is omitted from the mutation
+- `empty` — an empty byte array is written to the cell
+
+### batch_mutation_size [int]
+
+Number of row mutations to accumulate before sending a BulkMutation to
Bigtable. Default is `100`. Increase for higher throughput at the cost of
higher per-task memory usage.
+
+### common options
+
+Sink plugin common parameters, please refer to [Sink Common
Options](../common-options/sink-common-options.md) for details.
+
+## Data Types
+
+All SeaTunnel types are supported:
+
+| SeaTunnel type | Storage format in Bigtable |
+|------------------------------|---------------------------------|
+| TINYINT | 1-byte binary |
+| SMALLINT | 2-byte big-endian binary |
+| INT | 4-byte big-endian binary |
+| BIGINT | 8-byte big-endian binary |
+| FLOAT | 4-byte IEEE 754 big-endian |
+| DOUBLE | 8-byte IEEE 754 big-endian |
+| BOOLEAN | 1-byte (1 = true, 0 = false) |
+| BYTES | Raw bytes |
+| STRING | UTF-8 text |
+| DECIMAL | UTF-8 plain string |
+| DATE | UTF-8 `yyyy-MM-dd` |
+| TIME | UTF-8 `HH:mm:ss` |
+| TIMESTAMP | UTF-8 `yyyy-MM-dd HH:mm:ss` |
+
+## Example
+
+### Basic — Application Default Credentials
+
+```hocon
+sink {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ rowkey_column = ["event_id"]
+ column_family {
+ all_columns = "cf"
+ }
+ }
+}
+```
+
+### Service Account Key File
+
+```hocon
+sink {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ credentials_path = "/secrets/sa-key.json"
+ rowkey_column = ["tenant_id", "event_id"]
+ rowkey_delimiter = "#"
+ column_family {
+ all_columns = "data"
+ }
+ batch_mutation_size = 500
+ }
+}
+```
+
+### Multiple Column Families
+
+```hocon
+sink {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "user_profile"
+ rowkey_column = ["user_id"]
+ column_family {
+ name = "identity"
+ email = "identity"
+ age = "stats"
+ last_login = "stats"
+ }
+ }
+}
+```
+
+## Changelog
+
+<ChangeLog />
diff --git a/docs/en/connectors/source/GoogleBigtable.md
b/docs/en/connectors/source/GoogleBigtable.md
new file mode 100644
index 0000000000..b511aa38c7
--- /dev/null
+++ b/docs/en/connectors/source/GoogleBigtable.md
@@ -0,0 +1,139 @@
+import ChangeLog from '../changelog/connector-google-bigtable.md';
+
+# GoogleBigtable
+
+> Google Bigtable source connector
+
+## Description
+
+Reads data from Google Cloud Bigtable using the native Bigtable Data v2 Java
client.
+
+## Key features
+
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [ ] [stream](../../introduction/concepts/connector-v2-features.md)
+- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+- [ ] [parallelism](../../introduction/concepts/connector-v2-features.md)
+
+## Options
+
+| name | type | required | default value |
+|------------------|--------|----------|---------------|
+| project_id | string | yes | - |
+| instance_id | string | yes | - |
+| table | string | yes | - |
+| credentials_path | string | no | - |
+| rowkey_column | list | no | - |
+| start_rowkey | string | no | - |
+| end_rowkey | string | no | - |
+| start_timestamp | long | no | - |
+| end_timestamp | long | no | - |
+| max_versions | int | no | 1 |
+| scan_row_limit | int | no | -1 |
+| common-options | | no | - |
+
+### project_id [string]
+
+Google Cloud project ID.
+
+### instance_id [string]
+
+Bigtable instance ID.
+
+### table [string]
+
+Bigtable table name to read from.
+
+### credentials_path [string]
+
+Path to the Google Cloud service account JSON key file. If omitted,
Application Default Credentials (ADC) are used.
+
+### rowkey_column [list]
+
+Optional list of field names that should receive the row key value. Declare a
field named `rowkey` in your schema to capture the raw row key bytes as a
`BYTES` or `STRING` field.
+
+### start_rowkey [string]
+
+Inclusive start row key for the scan. If not set, the scan starts from the
beginning of the table.
+
+### end_rowkey [string]
+
+Exclusive end row key for the scan. If not set, the scan reads to the end of
the table.
+
+### start_timestamp [long]
+
+Inclusive start timestamp filter (microseconds since epoch).
+
+### end_timestamp [long]
+
+Exclusive end timestamp filter (microseconds since epoch).
+
+### max_versions [int]
+
+Maximum number of cell versions to return per column qualifier. Default `1`
returns only the latest version.
+
+### scan_row_limit [int]
+
+Maximum number of rows to return. `-1` (default) means no limit.
+
+### common options
+
+Source plugin common parameters, please refer to [Source Common
Options](../common-options/source-common-options.md) for details.
+
+## Schema Mapping
+
+Field names in the SeaTunnel schema must follow the pattern
`familyName:qualifier`, for example `cf:name` or `stats:age`. The special field
name `rowkey` maps to the Bigtable row key.
+
+| Schema field name | Mapped Bigtable cell |
+|-------------------|-----------------------------|
+| `rowkey` | Row key |
+| `cf:name` | Column family `cf`, qualifier `name` |
+| `stats:age` | Column family `stats`, qualifier `age` |
+
+## Example
+
+### Read all rows — Application Default Credentials
+
+```hocon
+source {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ schema {
+ fields {
+ rowkey = BYTES
+ "cf:type" = STRING
+ "cf:ts" = BIGINT
+ }
+ }
+ }
+}
+```
+
+### Scan a row-key range with a service account
+
+```hocon
+source {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ credentials_path = "/secrets/sa-key.json"
+ start_rowkey = "2024-01-01#"
+ end_rowkey = "2024-02-01#"
+ max_versions = 1
+ schema {
+ fields {
+ rowkey = STRING
+ "cf:type" = STRING
+ "cf:data" = STRING
+ }
+ }
+ }
+}
+```
+
+## Changelog
+
+<ChangeLog />
diff --git a/docs/zh/connectors/changelog/connector-google-bigtable.md
b/docs/zh/connectors/changelog/connector-google-bigtable.md
new file mode 100644
index 0000000000..3fc3a897f9
--- /dev/null
+++ b/docs/zh/connectors/changelog/connector-google-bigtable.md
@@ -0,0 +1,7 @@
+<details><summary> Change Log </summary>
+
+| Change | Commit | Version |
+| --- | --- | --- |
+|[Feature][Connector-V2] Add Google Cloud Bigtable Source and Sink
connector|https://github.com/apache/seatunnel/commit/8e57c04|dev|
+
+</details>
diff --git a/docs/zh/connectors/sink/GoogleBigtable.md
b/docs/zh/connectors/sink/GoogleBigtable.md
new file mode 100644
index 0000000000..65fd4f927d
--- /dev/null
+++ b/docs/zh/connectors/sink/GoogleBigtable.md
@@ -0,0 +1,105 @@
+import ChangeLog from '../changelog/connector-google-bigtable.md';
+
+# GoogleBigtable
+
+> Google Bigtable Sink 连接器
+
+## 描述
+
+使用原生 Bigtable Data v2 Java 客户端将数据写入 Google Cloud Bigtable。
+
+## 主要特性
+
+- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+
+## 参数
+
+| 参数名 | 类型 | 是否必填 | 默认值 |
+|--------------------|---------|--------|------|
+| project_id | string | 是 | - |
+| instance_id | string | 是 | - |
+| table | string | 是 | - |
+| rowkey_column | list | 是 | - |
+| column_family | config | 是 | - |
+| credentials_path | string | 否 | - |
+| rowkey_delimiter | string | 否 | "" |
+| version_column | string | 否 | - |
+| null_mode | string | 否 | skip |
+| batch_mutation_size| int | 否 | 100 |
+| common-options | | 否 | - |
+
+### project_id [string]
+
+Google Cloud 项目 ID,例如 `"my-gcp-project"`。
+
+### instance_id [string]
+
+Bigtable 实例 ID,例如 `"my-bigtable-instance"`。
+
+### table [string]
+
+写入的 Bigtable 表名,例如 `"my-table"`。
+
+### rowkey_column [list]
+
+用于构造行键的列名列表,例如 `["id"]` 或 `["tenant_id", "event_id"]`。多列时用 `rowkey_delimiter`
拼接。
+
+### column_family [config]
+
+列名到列族的映射配置。可使用 `all_columns` 作为默认列族:
+
+```hocon
+column_family {
+ all_columns = "cf"
+}
+```
+
+也可以为不同列指定不同列族:
+
+```hocon
+column_family {
+ name = "info"
+ age = "stats"
+}
+```
+
+### credentials_path [string]
+
+Google Cloud 服务账号 JSON 密钥文件路径。未设置时使用应用默认凭证(ADC)。
+
+### rowkey_delimiter [string]
+
+多列行键的拼接分隔符,默认为空字符串 `""`。
+
+### version_column [string]
+
+用作 Bigtable Cell 时间戳(微秒)的 BIGINT 列名。未设置时使用当前系统时间。
+
+### null_mode [string]
+
+空值写入策略:`skip`(默认,跳过该 Cell)或 `empty`(写入空字节数组)。
+
+### batch_mutation_size [int]
+
+每次批量提交的行数,默认 `100`。
+
+## 示例
+
+```hocon
+sink {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ rowkey_column = ["event_id"]
+ column_family {
+ all_columns = "cf"
+ }
+ }
+}
+```
+
+## Changelog
+
+<ChangeLog />
diff --git a/docs/zh/connectors/source/GoogleBigtable.md
b/docs/zh/connectors/source/GoogleBigtable.md
new file mode 100644
index 0000000000..ea1b4e8190
--- /dev/null
+++ b/docs/zh/connectors/source/GoogleBigtable.md
@@ -0,0 +1,59 @@
+import ChangeLog from '../changelog/connector-google-bigtable.md';
+
+# GoogleBigtable
+
+> Google Bigtable Source 连接器
+
+## 描述
+
+使用原生 Bigtable Data v2 Java 客户端从 Google Cloud Bigtable 读取数据。
+
+## 主要特性
+
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [ ] [parallelism](../../introduction/concepts/connector-v2-features.md)
+- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+
+## 参数
+
+| 参数名 | 类型 | 是否必填 | 默认值 |
+|-----------------|--------|--------|------|
+| project_id | string | 是 | - |
+| instance_id | string | 是 | - |
+| table | string | 是 | - |
+| credentials_path| string | 否 | - |
+| rowkey_column | list | 否 | - |
+| start_rowkey | string | 否 | - |
+| end_rowkey | string | 否 | - |
+| start_timestamp | long | 否 | - |
+| end_timestamp | long | 否 | - |
+| max_versions | int | 否 | 1 |
+| scan_row_limit | int | 否 | -1 |
+| common-options | | 否 | - |
+
+### Schema 映射
+
+字段名须使用 `列族:列限定符` 格式,例如 `cf:name`、`stats:age`。特殊字段名 `rowkey` 映射到行键。
+
+## 示例
+
+```hocon
+source {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ schema {
+ fields {
+ rowkey = BYTES
+ "cf:type" = STRING
+ "cf:ts" = BIGINT
+ }
+ }
+ }
+}
+```
+
+## Changelog
+
+<ChangeLog />
diff --git a/plugin-mapping.properties b/plugin-mapping.properties
index 800f50b7be..5e0a4460ce 100644
--- a/plugin-mapping.properties
+++ b/plugin-mapping.properties
@@ -90,6 +90,8 @@ seatunnel.source.MyHours = connector-http-myhours
seatunnel.sink.InfluxDB = connector-influxdb
seatunnel.source.GoogleSheets = connector-google-sheets
seatunnel.sink.GoogleFirestore = connector-google-firestore
+seatunnel.source.GoogleBigtable = connector-google-bigtable
+seatunnel.sink.GoogleBigtable = connector-google-bigtable
seatunnel.sink.Tablestore = connector-tablestore
seatunnel.source.Tablestore = connector-tablestore
seatunnel.source.Lemlist = connector-http-lemlist
diff --git a/seatunnel-connectors-v2/connector-google-bigtable/pom.xml
b/seatunnel-connectors-v2/connector-google-bigtable/pom.xml
new file mode 100644
index 0000000000..5f6e03aaaa
--- /dev/null
+++ b/seatunnel-connectors-v2/connector-google-bigtable/pom.xml
@@ -0,0 +1,64 @@
+<?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">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.seatunnel</groupId>
+ <artifactId>seatunnel-connectors-v2</artifactId>
+ <version>${revision}</version>
+ </parent>
+
+ <artifactId>connector-google-bigtable</artifactId>
+ <name>SeaTunnel : Connectors V2 : Google Bigtable</name>
+
+ <properties>
+ <bigtable.version>2.39.1</bigtable.version>
+ </properties>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.apache.seatunnel</groupId>
+ <artifactId>connector-common</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.cloud</groupId>
+ <artifactId>google-cloud-bigtable</artifactId>
+ <version>${bigtable.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.seatunnel</groupId>
+ <artifactId>seatunnel-format-json</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-junit-jupiter</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/client/BigtableClient.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/client/BigtableClient.java
new file mode 100644
index 0000000000..d4d21d9718
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/client/BigtableClient.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.client;
+
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorErrorCode;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorException;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auth.oauth2.ServiceAccountCredentials;
+import com.google.cloud.bigtable.data.v2.BigtableDataClient;
+import com.google.cloud.bigtable.data.v2.BigtableDataSettings;
+import com.google.cloud.bigtable.data.v2.models.BulkMutation;
+import com.google.cloud.bigtable.data.v2.models.Mutation;
+import com.google.cloud.bigtable.data.v2.models.RowMutation;
+import com.google.protobuf.ByteString;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Wrapper around the native Google Cloud Bigtable Java client.
+ *
+ * <p>Provides basic CRUD operations and batched mutation support used by both
the Sink and Source
+ * connectors.
+ */
+@Slf4j
+public class BigtableClient implements Serializable, AutoCloseable {
+
+ private transient BigtableDataClient dataClient;
+ private final BigtableParameters parameters;
+
+ private BigtableClient(BigtableDataClient dataClient, BigtableParameters
parameters) {
+ this.dataClient = dataClient;
+ this.parameters = parameters;
+ }
+
+ /**
+ * Creates a new BigtableClient instance using the provided parameters.
+ *
+ * @param parameters Bigtable connection parameters
+ * @return a connected BigtableClient
+ */
+ public static BigtableClient createInstance(BigtableParameters parameters)
{
+ try {
+ BigtableDataSettings.Builder settingsBuilder =
+ BigtableDataSettings.newBuilder()
+ .setProjectId(parameters.getProjectId())
+ .setInstanceId(parameters.getInstanceId());
+
+ if (parameters.getCredentialsPath() != null
+ && !parameters.getCredentialsPath().isEmpty()) {
+ try (FileInputStream credStream =
+ new FileInputStream(parameters.getCredentialsPath())) {
+ GoogleCredentials credentials =
+ ServiceAccountCredentials.fromStream(credStream);
+ settingsBuilder.stubSettings().setCredentialsProvider(()
-> credentials);
+ }
+ }
+
+ BigtableDataClient client =
BigtableDataClient.create(settingsBuilder.build());
+ return new BigtableClient(client, parameters);
+ } catch (IOException e) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.CONNECTION_FAILED,
+ "Failed to create Bigtable client for project="
+ + parameters.getProjectId()
+ + ", instance="
+ + parameters.getInstanceId(),
+ e);
+ }
+ }
+
+ /**
+ * Applies a single row mutation to Bigtable.
+ *
+ * @param rowMutation the row mutation to apply
+ */
+ public void mutateRow(RowMutation rowMutation) {
+ try {
+ dataClient.mutateRow(rowMutation);
+ } catch (Exception e) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.WRITE_FAILED,
+ "Failed to mutate row in table " + parameters.getTable(),
+ e);
+ }
+ }
+
+ /**
+ * Applies a batch of row mutations to Bigtable using BulkMutation for
efficiency.
+ *
+ * @param mutations list of (rowKey, Mutation) pairs to apply
+ */
+ public void bulkMutate(List<RowKeyMutation> mutations) {
+ if (mutations.isEmpty()) {
+ return;
+ }
+ try {
+ BulkMutation bulk = BulkMutation.create(parameters.getTable());
+ for (RowKeyMutation entry : mutations) {
+ bulk.add(entry.getRowKey(), entry.getMutation());
+ }
+ dataClient.bulkMutateRows(bulk);
+ } catch (Exception e) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.WRITE_FAILED,
+ "Failed to bulk mutate "
+ + mutations.size()
+ + " rows in table "
+ + parameters.getTable(),
+ e);
+ }
+ }
+
+ /**
+ * Returns the underlying data client (for use in Source reader streaming).
+ *
+ * @return BigtableDataClient
+ */
+ public BigtableDataClient getDataClient() {
+ return dataClient;
+ }
+
+ @Override
+ public void close() {
+ if (dataClient != null) {
+ try {
+ dataClient.close();
+ dataClient = null;
+ } catch (Exception e) {
+ log.error("Failed to close Bigtable data client", e);
+ }
+ }
+ }
+
+ /** Holds a row key and its associated Mutation for bulk operations. */
+ public static class RowKeyMutation implements Serializable {
+ private final ByteString rowKey;
+ private final Mutation mutation;
+
+ public RowKeyMutation(ByteString rowKey, Mutation mutation) {
+ this.rowKey = rowKey;
+ this.mutation = mutation;
+ }
+
+ public ByteString getRowKey() {
+ return rowKey;
+ }
+
+ public Mutation getMutation() {
+ return mutation;
+ }
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableBaseOptions.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableBaseOptions.java
new file mode 100644
index 0000000000..01850ee698
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableBaseOptions.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.config;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+import org.apache.seatunnel.api.options.ConnectorCommonOptions;
+
+import java.util.List;
+
+public class BigtableBaseOptions extends ConnectorCommonOptions {
+
+ public static final Option<String> PROJECT_ID =
+ Options.key("project_id")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("Google Cloud project ID");
+
+ public static final Option<String> INSTANCE_ID =
+ Options.key("instance_id")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("Bigtable instance ID");
+
+ public static final Option<String> TABLE =
+ Options.key("table")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("Bigtable table name");
+
+ public static final Option<List<String>> ROWKEY_COLUMNS =
+ Options.key("rowkey_column")
+ .listType()
+ .noDefaultValue()
+ .withDescription(
+ "Column names used to compose the Bigtable row
key. "
+ + "If multiple columns are specified they
are joined with rowkey_delimiter.");
+
+ public static final Option<String> CREDENTIALS_PATH =
+ Options.key("credentials_path")
+ .stringType()
+ .noDefaultValue()
+ .withDescription(
+ "Path to the Google Cloud service account JSON key
file. "
+ + "If not set, Application Default
Credentials (ADC) will be used.");
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableParameters.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableParameters.java
new file mode 100644
index 0000000000..69a005976a
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableParameters.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.config;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+@Builder
+@Getter
+public class BigtableParameters implements Serializable {
+
+ private String projectId;
+ private String instanceId;
+ private String table;
+ private List<String> rowkeyColumns;
+ private String credentialsPath;
+
+ // Sink-specific
+ private Map<String, String> columnFamily;
+ @Builder.Default private String rowkeyDelimiter = "";
+ private String versionColumn;
+
+ @Builder.Default
+ private BigtableSinkOptions.NullMode nullMode =
BigtableSinkOptions.NullMode.SKIP;
+
+ @Builder.Default private int batchMutationSize = 100;
+
+ // Source-specific
+ private String startRowkey;
+ private String endRowkey;
+ private Long startTimestamp;
+ private Long endTimestamp;
+ @Builder.Default private int maxVersions = 1;
+ @Builder.Default private int scanRowLimit = -1;
+
+ public static BigtableParameters buildWithConfig(ReadonlyConfig config) {
+ BigtableParametersBuilder builder = BigtableParameters.builder();
+ builder.projectId(config.get(BigtableBaseOptions.PROJECT_ID));
+ builder.instanceId(config.get(BigtableBaseOptions.INSTANCE_ID));
+ builder.table(config.get(BigtableBaseOptions.TABLE));
+ builder.rowkeyColumns(config.get(BigtableBaseOptions.ROWKEY_COLUMNS));
+
+ config.getOptional(BigtableBaseOptions.CREDENTIALS_PATH)
+ .ifPresent(builder::credentialsPath);
+
config.getOptional(BigtableSinkOptions.COLUMN_FAMILY).ifPresent(builder::columnFamily);
+ config.getOptional(BigtableSinkOptions.ROWKEY_DELIMITER)
+ .ifPresent(builder::rowkeyDelimiter);
+
config.getOptional(BigtableSinkOptions.VERSION_COLUMN).ifPresent(builder::versionColumn);
+
config.getOptional(BigtableSinkOptions.NULL_MODE).ifPresent(builder::nullMode);
+ config.getOptional(BigtableSinkOptions.BATCH_MUTATION_SIZE)
+ .ifPresent(builder::batchMutationSize);
+ return builder.build();
+ }
+
+ public static BigtableParameters buildWithSourceConfig(ReadonlyConfig
config) {
+ BigtableParametersBuilder builder = BigtableParameters.builder();
+ builder.projectId(config.get(BigtableBaseOptions.PROJECT_ID));
+ builder.instanceId(config.get(BigtableBaseOptions.INSTANCE_ID));
+ builder.table(config.get(BigtableBaseOptions.TABLE));
+
+
config.getOptional(BigtableBaseOptions.ROWKEY_COLUMNS).ifPresent(builder::rowkeyColumns);
+ config.getOptional(BigtableBaseOptions.CREDENTIALS_PATH)
+ .ifPresent(builder::credentialsPath);
+
config.getOptional(BigtableSourceOptions.START_ROW_KEY).ifPresent(builder::startRowkey);
+
config.getOptional(BigtableSourceOptions.END_ROW_KEY).ifPresent(builder::endRowkey);
+ config.getOptional(BigtableSourceOptions.START_TIMESTAMP)
+ .ifPresent(builder::startTimestamp);
+
config.getOptional(BigtableSourceOptions.END_TIMESTAMP).ifPresent(builder::endTimestamp);
+
config.getOptional(BigtableSourceOptions.MAX_VERSIONS).ifPresent(builder::maxVersions);
+
config.getOptional(BigtableSourceOptions.SCAN_ROW_LIMIT).ifPresent(builder::scanRowLimit);
+ return builder.build();
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableSinkOptions.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableSinkOptions.java
new file mode 100644
index 0000000000..1c3b4bb0b6
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableSinkOptions.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.config;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+import org.apache.seatunnel.api.sink.DataSaveMode;
+import org.apache.seatunnel.api.sink.SchemaSaveMode;
+
+import java.util.Map;
+
+import static org.apache.seatunnel.api.sink.DataSaveMode.APPEND_DATA;
+
+public class BigtableSinkOptions extends BigtableBaseOptions {
+
+ public static final Option<Map<String, String>> COLUMN_FAMILY =
+ Options.key("column_family")
+ .mapType()
+ .noDefaultValue()
+ .withDescription(
+ "Mapping from column name to column family. "
+ + "Use \"all_columns\" as key to set a
default family for unmapped columns.");
+
+ public static final Option<String> ROWKEY_DELIMITER =
+ Options.key("rowkey_delimiter")
+ .stringType()
+ .defaultValue("")
+ .withDescription(
+ "Delimiter used to join multiple rowkey column
values. Default is empty string.");
+
+ public static final Option<String> VERSION_COLUMN =
+ Options.key("version_column")
+ .stringType()
+ .noDefaultValue()
+ .withDescription(
+ "Column name whose long value is used as the
Bigtable cell timestamp. "
+ + "If not set, the current system time is
used.");
+
+ public static final Option<NullMode> NULL_MODE =
+ Options.key("null_mode")
+ .enumType(NullMode.class)
+ .defaultValue(NullMode.SKIP)
+ .withDescription(
+ "How to handle null field values: SKIP (default)
omits the cell; EMPTY writes an empty byte array.");
+
+ public static final Option<Integer> BATCH_MUTATION_SIZE =
+ Options.key("batch_mutation_size")
+ .intType()
+ .defaultValue(100)
+ .withDescription(
+ "Number of mutations to accumulate before flushing
to Bigtable. Default is 100.");
+
+ public static final Option<SchemaSaveMode> SCHEMA_SAVE_MODE =
+ Options.key("schema_save_mode")
+ .singleChoice(
+ SchemaSaveMode.class,
+
java.util.Arrays.asList(SchemaSaveMode.RECREATE_SCHEMA))
+ .defaultValue(SchemaSaveMode.RECREATE_SCHEMA)
+ .withDescription(
+ "Schema save mode. Only RECREATE_SCHEMA is
currently supported. "
+ + "Table and column families must be
created manually before running the job.");
+
+ public static final Option<DataSaveMode> DATA_SAVE_MODE =
+ Options.key("data_save_mode")
+ .singleChoice(DataSaveMode.class,
java.util.Arrays.asList(APPEND_DATA))
+ .defaultValue(APPEND_DATA)
+ .withDescription(
+ "Data save mode. Only APPEND_DATA is currently
supported. "
+ + "DROP_DATA and ERROR_WHEN_DATA_EXISTS
are not yet implemented.");
+
+ public enum NullMode {
+ SKIP,
+ EMPTY;
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableSourceOptions.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableSourceOptions.java
new file mode 100644
index 0000000000..9ef2f88a37
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/config/BigtableSourceOptions.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.config;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+
+public class BigtableSourceOptions extends BigtableBaseOptions {
+
+ public static final Option<String> START_ROW_KEY =
+ Options.key("start_rowkey")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("Bigtable scan start row key
(inclusive).");
+
+ public static final Option<String> END_ROW_KEY =
+ Options.key("end_rowkey")
+ .stringType()
+ .noDefaultValue()
+ .withDescription("Bigtable scan end row key (exclusive).");
+
+ public static final Option<Long> START_TIMESTAMP =
+ Options.key("start_timestamp")
+ .longType()
+ .noDefaultValue()
+ .withDescription(
+ "Start timestamp (inclusive) for scan time range
in microseconds since epoch.");
+
+ public static final Option<Long> END_TIMESTAMP =
+ Options.key("end_timestamp")
+ .longType()
+ .noDefaultValue()
+ .withDescription(
+ "End timestamp (exclusive) for scan time range in
microseconds since epoch.");
+
+ public static final Option<Integer> MAX_VERSIONS =
+ Options.key("max_versions")
+ .intType()
+ .defaultValue(1)
+ .withDescription(
+ "Maximum number of cell versions to return per
column. Default is 1 (latest only).");
+
+ public static final Option<Integer> SCAN_ROW_LIMIT =
+ Options.key("scan_row_limit")
+ .intType()
+ .defaultValue(-1)
+ .withDescription(
+ "Maximum number of rows to scan. -1 (default)
means no limit.");
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/constant/BigtableIdentifier.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/constant/BigtableIdentifier.java
new file mode 100644
index 0000000000..6bd287b27a
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/constant/BigtableIdentifier.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.constant;
+
+public class BigtableIdentifier {
+ public static final String IDENTIFIER_NAME = "GoogleBigtable";
+
+ private BigtableIdentifier() {}
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/exception/BigtableConnectorErrorCode.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/exception/BigtableConnectorErrorCode.java
new file mode 100644
index 0000000000..0f16864b72
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/exception/BigtableConnectorErrorCode.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.exception;
+
+import org.apache.seatunnel.common.exception.SeaTunnelErrorCode;
+
+public enum BigtableConnectorErrorCode implements SeaTunnelErrorCode {
+ CONNECTION_FAILED("Bigtable-01", "Build Bigtable connection failed"),
+ TABLE_NOT_FOUND("Bigtable-02", "Bigtable table not found"),
+ TABLE_CREATE_FAILED("Bigtable-03", "Bigtable table create failed"),
+ TABLE_DELETE_FAILED("Bigtable-04", "Bigtable table delete failed"),
+ TABLE_TRUNCATE_FAILED("Bigtable-05", "Bigtable table truncate failed"),
+ TABLE_QUERY_FAILED("Bigtable-06", "Bigtable table query failed"),
+ WRITE_FAILED("Bigtable-07", "Bigtable write failed"),
+ READ_FAILED("Bigtable-08", "Bigtable read failed"),
+ CREDENTIALS_FAILED("Bigtable-09", "Failed to load Bigtable credentials"),
+ ;
+
+ private final String code;
+ private final String description;
+
+ BigtableConnectorErrorCode(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ @Override
+ public String getCode() {
+ return code;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/exception/BigtableConnectorException.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/exception/BigtableConnectorException.java
new file mode 100644
index 0000000000..d9b73e77f3
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/exception/BigtableConnectorException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.exception;
+
+import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
+
+public class BigtableConnectorException extends SeaTunnelRuntimeException {
+
+ public BigtableConnectorException(BigtableConnectorErrorCode errorCode,
String errorMessage) {
+ super(errorCode, errorMessage);
+ }
+
+ public BigtableConnectorException(
+ BigtableConnectorErrorCode errorCode, String errorMessage,
Throwable cause) {
+ super(errorCode, errorMessage, cause);
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/format/BigtableDeserializationFormat.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/format/BigtableDeserializationFormat.java
new file mode 100644
index 0000000000..5a7254ba3e
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/format/BigtableDeserializationFormat.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.format;
+
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.common.utils.DateTimeUtils;
+import org.apache.seatunnel.common.utils.DateUtils;
+import org.apache.seatunnel.common.utils.TimeUtils;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorErrorCode;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorException;
+
+import com.google.protobuf.ByteString;
+
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Deserializes raw {@link ByteString} cell values returned by Bigtable into
typed {@link
+ * SeaTunnelRow} fields.
+ *
+ * <p>Numeric types are stored as big-endian binary; string-like types (DATE,
TIME, TIMESTAMP,
+ * DECIMAL, STRING) are stored as UTF-8 text, matching the encoding used by
{@code
+ * BigtableSinkWriter}.
+ */
+public class BigtableDeserializationFormat {
+
+ private final DateUtils.Formatter dateFormat =
DateUtils.Formatter.YYYY_MM_DD;
+ private final DateTimeUtils.Formatter datetimeFormat =
+ DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS;
+ private final TimeUtils.Formatter timeFormat =
TimeUtils.Formatter.HH_MM_SS;
+
+ /**
+ * Deserializes an array of raw cell bytes into a {@link SeaTunnelRow}.
+ *
+ * @param rawCells one entry per field in {@code rowType}; may be {@code
null} for absent cells
+ * @param rowType the target row schema
+ * @return the deserialized row
+ */
+ public SeaTunnelRow deserialize(ByteString[] rawCells, SeaTunnelRowType
rowType) {
+ SeaTunnelRow row = new SeaTunnelRow(rowType.getTotalFields());
+ for (int i = 0; i < rowType.getTotalFields(); i++) {
+ SeaTunnelDataType<?> fieldType = rowType.getFieldType(i);
+ row.setField(i, deserializeCell(fieldType, rawCells[i]));
+ }
+ return row;
+ }
+
+ private Object deserializeCell(SeaTunnelDataType<?> fieldType, ByteString
cell) {
+ if (cell == null || cell.isEmpty()) {
+ return null;
+ }
+ byte[] bytes = cell.toByteArray();
+ switch (fieldType.getSqlType()) {
+ case TINYINT:
+ return bytes[0];
+ case SMALLINT:
+ return (short) ((bytes[0] & 0xFF) << 8 | (bytes[1] & 0xFF));
+ case INT:
+ return ByteBuffer.wrap(bytes).getInt();
+ case BIGINT:
+ return ByteBuffer.wrap(bytes).getLong();
+ case FLOAT:
+ return ByteBuffer.wrap(bytes).getFloat();
+ case DOUBLE:
+ return ByteBuffer.wrap(bytes).getDouble();
+ case BOOLEAN:
+ return bytes[0] != 0;
+ case BYTES:
+ return bytes;
+ case DECIMAL:
+ String decStr = new String(bytes, StandardCharsets.UTF_8);
+ try {
+ return new BigDecimal(decStr);
+ } catch (NumberFormatException e) {
+ return new BigDecimal(ByteBuffer.wrap(bytes).getFloat());
+ }
+ case DATE:
+ return LocalDate.parse(
+ new String(bytes, StandardCharsets.UTF_8),
+ DateTimeFormatter.ofPattern(dateFormat.getValue()));
+ case TIME:
+ return LocalTime.parse(
+ new String(bytes, StandardCharsets.UTF_8),
+ DateTimeFormatter.ofPattern(timeFormat.getValue()));
+ case TIMESTAMP:
+ return LocalDateTime.parse(
+ new String(bytes, StandardCharsets.UTF_8),
+
DateTimeFormatter.ofPattern(datetimeFormat.getValue()));
+ case STRING:
+ return new String(bytes, StandardCharsets.UTF_8);
+ default:
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.READ_FAILED,
+ "Unsupported data type: " + fieldType.getSqlType());
+ }
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSink.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSink.java
new file mode 100644
index 0000000000..468d0f8c52
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSink.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.sink.DataSaveMode;
+import org.apache.seatunnel.api.sink.SchemaSaveMode;
+import org.apache.seatunnel.api.sink.SeaTunnelSink;
+import org.apache.seatunnel.api.sink.SinkWriter;
+import org.apache.seatunnel.api.sink.SupportMultiTableSink;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableSinkOptions;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.constant.BigtableIdentifier;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorErrorCode;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorException;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.state.BigtableAggregatedCommitInfo;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.state.BigtableCommitInfo;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.state.BigtableSinkState;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+@Slf4j
+public class BigtableSink
+ implements SeaTunnelSink<
+ SeaTunnelRow,
+ BigtableSinkState,
+ BigtableCommitInfo,
+ BigtableAggregatedCommitInfo>,
+ SupportMultiTableSink {
+
+ private final ReadonlyConfig config;
+ private final CatalogTable catalogTable;
+ private final BigtableParameters parameters;
+ private final SeaTunnelRowType rowType;
+ private final List<Integer> rowkeyColumnIndexes = new ArrayList<>();
+ private int versionColumnIndex = -1;
+ private final SchemaSaveMode schemaSaveMode;
+ private final DataSaveMode dataSaveMode;
+
+ public BigtableSink(ReadonlyConfig config, CatalogTable catalogTable) {
+ this.config = config;
+ this.catalogTable = catalogTable;
+ this.parameters = BigtableParameters.buildWithConfig(config);
+ this.rowType = catalogTable.getSeaTunnelRowType();
+ if (parameters.getVersionColumn() != null) {
+ this.versionColumnIndex =
rowType.indexOf(parameters.getVersionColumn());
+ }
+ this.schemaSaveMode = config.get(BigtableSinkOptions.SCHEMA_SAVE_MODE);
+ this.dataSaveMode = config.get(BigtableSinkOptions.DATA_SAVE_MODE);
+ }
+
+ @Override
+ public String getPluginName() {
+ return BigtableIdentifier.IDENTIFIER_NAME;
+ }
+
+ @Override
+ public BigtableSinkWriter createWriter(SinkWriter.Context context) throws
IOException {
+ for (String rowkeyColumn : parameters.getRowkeyColumns()) {
+ rowkeyColumnIndexes.add(rowType.indexOf(rowkeyColumn));
+ }
+ if (parameters.getVersionColumn() != null) {
+ this.versionColumnIndex =
rowType.indexOf(parameters.getVersionColumn());
+ }
+ handleSaveMode();
+ return new BigtableSinkWriter(rowType, parameters,
rowkeyColumnIndexes, versionColumnIndex);
+ }
+
+ /**
+ * Validates and applies the configured {@link SchemaSaveMode} and {@link
DataSaveMode}.
+ *
+ * <p>Only {@link SchemaSaveMode#RECREATE_SCHEMA} and {@link
DataSaveMode#APPEND_DATA} are
+ * currently supported. Unsupported modes throw immediately so users are
never misled by
+ * accepted-but-no-op settings. Full Admin API support (table creation /
truncation) can be
+ * added in a follow-up once a BigtableCatalog is available.
+ */
+ private void handleSaveMode() {
+ if (schemaSaveMode == SchemaSaveMode.CREATE_SCHEMA_WHEN_NOT_EXIST) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.TABLE_CREATE_FAILED,
+ "schema_save_mode=CREATE_SCHEMA_WHEN_NOT_EXIST is not yet
supported by the "
+ + "Bigtable connector. Please create the table and
column families "
+ + "manually and set
schema_save_mode=RECREATE_SCHEMA.");
+ }
+ if (dataSaveMode == DataSaveMode.DROP_DATA) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.TABLE_TRUNCATE_FAILED,
+ "data_save_mode=DROP_DATA is not yet supported by the
Bigtable connector. "
+ + "Please truncate the table manually or use
data_save_mode=APPEND_DATA.");
+ }
+ if (dataSaveMode == DataSaveMode.ERROR_WHEN_DATA_EXISTS) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.TABLE_QUERY_FAILED,
+ "data_save_mode=ERROR_WHEN_DATA_EXISTS is not yet
supported by the Bigtable "
+ + "connector. Please use
data_save_mode=APPEND_DATA.");
+ }
+ log.info("Bigtable sink save mode: schema={}, data={}",
schemaSaveMode, dataSaveMode);
+ }
+
+ @Override
+ public Optional<CatalogTable> getWriteCatalogTable() {
+ return Optional.ofNullable(catalogTable);
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkFactory.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkFactory.java
new file mode 100644
index 0000000000..aed77c0729
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkFactory.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.seatunnel.connectors.seatunnel.bigtable.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.options.SinkConnectorCommonOptions;
+import org.apache.seatunnel.api.table.connector.TableSink;
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.TableSinkFactory;
+import org.apache.seatunnel.api.table.factory.TableSinkFactoryContext;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableBaseOptions;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableSinkOptions;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.constant.BigtableIdentifier;
+
+import com.google.auto.service.AutoService;
+
+@AutoService(Factory.class)
+public class BigtableSinkFactory implements TableSinkFactory {
+
+ @Override
+ public String factoryIdentifier() {
+ return BigtableIdentifier.IDENTIFIER_NAME;
+ }
+
+ @Override
+ public OptionRule optionRule() {
+ return OptionRule.builder()
+ .required(
+ BigtableBaseOptions.PROJECT_ID,
+ BigtableBaseOptions.INSTANCE_ID,
+ BigtableBaseOptions.TABLE,
+ BigtableBaseOptions.ROWKEY_COLUMNS,
+ BigtableSinkOptions.COLUMN_FAMILY)
+ .optional(
+ BigtableBaseOptions.CREDENTIALS_PATH,
+ BigtableSinkOptions.ROWKEY_DELIMITER,
+ BigtableSinkOptions.VERSION_COLUMN,
+ BigtableSinkOptions.NULL_MODE,
+ BigtableSinkOptions.BATCH_MUTATION_SIZE,
+ BigtableSinkOptions.SCHEMA_SAVE_MODE,
+ BigtableSinkOptions.DATA_SAVE_MODE,
+ SinkConnectorCommonOptions.MULTI_TABLE_SINK_REPLICA)
+ .build();
+ }
+
+ @Override
+ public TableSink createSink(TableSinkFactoryContext context) {
+ ReadonlyConfig readonlyConfig = context.getOptions();
+ return () -> new BigtableSink(readonlyConfig,
context.getCatalogTable());
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkWriter.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkWriter.java
new file mode 100644
index 0000000000..8ca5312e19
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkWriter.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.sink;
+
+import org.apache.seatunnel.api.sink.SinkWriter;
+import org.apache.seatunnel.api.sink.SupportMultiTableSinkWriter;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.common.utils.DateTimeUtils;
+import org.apache.seatunnel.common.utils.DateUtils;
+import org.apache.seatunnel.common.utils.TimeUtils;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.client.BigtableClient;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.client.BigtableClient.RowKeyMutation;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableSinkOptions;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorErrorCode;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorException;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.state.BigtableCommitInfo;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.state.BigtableSinkState;
+
+import com.google.cloud.bigtable.data.v2.models.Mutation;
+import com.google.protobuf.ByteString;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ * Writes {@link SeaTunnelRow} records to Google Cloud Bigtable.
+ *
+ * <p>Each row is converted to a Bigtable mutation using the configured
column-family mapping and
+ * row-key strategy. Mutations are accumulated in a local buffer and flushed
in bulk when the buffer
+ * reaches {@code batchMutationSize}.
+ */
+@Slf4j
+public class BigtableSinkWriter
+ implements SinkWriter<SeaTunnelRow, BigtableCommitInfo,
BigtableSinkState>,
+ SupportMultiTableSinkWriter<Void> {
+
+ private static final String ALL_COLUMNS_KEY = "all_columns";
+ private static final String DEFAULT_FAMILY = "cf";
+
+ private final BigtableClient bigtableClient;
+ private final SeaTunnelRowType rowType;
+ private final BigtableParameters parameters;
+ private final List<Integer> rowkeyColumnIndexes;
+ private final int versionColumnIndex;
+ private final String defaultFamily;
+
+ /** Buffer of pending mutations, flushed in batches. */
+ private final List<RowKeyMutation> buffer;
+
+ public BigtableSinkWriter(
+ SeaTunnelRowType rowType,
+ BigtableParameters parameters,
+ List<Integer> rowkeyColumnIndexes,
+ int versionColumnIndex) {
+ this(rowType, parameters, rowkeyColumnIndexes, versionColumnIndex,
null);
+ }
+
+ BigtableSinkWriter(
+ SeaTunnelRowType rowType,
+ BigtableParameters parameters,
+ List<Integer> rowkeyColumnIndexes,
+ int versionColumnIndex,
+ BigtableClient bigtableClient) {
+ this.rowType = rowType;
+ this.parameters = parameters;
+ this.rowkeyColumnIndexes = rowkeyColumnIndexes;
+ this.versionColumnIndex = versionColumnIndex;
+ this.buffer = new ArrayList<>(parameters.getBatchMutationSize());
+
+ Map<String, String> familyMap = parameters.getColumnFamily();
+ if (familyMap != null && familyMap.containsKey(ALL_COLUMNS_KEY)) {
+ this.defaultFamily = familyMap.get(ALL_COLUMNS_KEY);
+ } else {
+ this.defaultFamily = DEFAULT_FAMILY;
+ }
+
+ this.bigtableClient =
+ bigtableClient != null ? bigtableClient :
BigtableClient.createInstance(parameters);
+ }
+
+ @Override
+ public void write(SeaTunnelRow element) throws IOException {
+ RowKeyMutation entry = convertRowToMutation(element); // outside lock:
no shared state
+ synchronized (buffer) {
+ buffer.add(entry);
+ if (buffer.size() >= parameters.getBatchMutationSize()) {
+ flush();
+ }
+ }
+ }
+
+ @Override
+ public Optional<BigtableCommitInfo> prepareCommit() throws IOException {
+ synchronized (buffer) {
+ flush();
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public void abortPrepare() {}
+
+ @Override
+ public void close() throws IOException {
+ try {
+ synchronized (buffer) {
+ flush();
+ }
+ } finally {
+ if (bigtableClient != null) {
+ bigtableClient.close();
+ }
+ }
+ }
+
+ private void flush() {
+ if (buffer.isEmpty()) {
+ return;
+ }
+ List<RowKeyMutation> toFlush = new ArrayList<>(buffer);
+ buffer.clear(); // clear first: prevents re-sending if bulkMutate
throws
+ bigtableClient.bulkMutate(toFlush);
+ }
+
+ private RowKeyMutation convertRowToMutation(SeaTunnelRow row) {
+ ByteString rowKey = buildRowKey(row);
+ if (rowKey.isEmpty()) {
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.WRITE_FAILED,
+ "Row key cannot be empty. Check rowkey_column
configuration.");
+ }
+
+ long timestamp = System.currentTimeMillis() * 1000L; // Bigtable uses
microseconds
+ if (versionColumnIndex != -1) {
+ Object versionField = row.getField(versionColumnIndex);
+ if (versionField instanceof Long) {
+ timestamp = (Long) versionField;
+ }
+ }
+
+ Mutation mutation = Mutation.create();
+
+ List<Integer> writeColumnIndexes =
+ IntStream.range(0, row.getArity())
+ .boxed()
+ .filter(idx -> !rowkeyColumnIndexes.contains(idx))
+ .filter(idx -> idx != versionColumnIndex)
+ .collect(Collectors.toList());
+
+ for (Integer idx : writeColumnIndexes) {
+ String fieldName = rowType.getFieldName(idx);
+ String family = resolveFamily(fieldName);
+ Object fieldValue = row.getField(idx);
+ ByteString qualifier = ByteString.copyFromUtf8(fieldName);
+
+ if (fieldValue == null) {
+ if (parameters.getNullMode() ==
BigtableSinkOptions.NullMode.EMPTY) {
+ mutation.setCell(family, qualifier, timestamp,
ByteString.EMPTY);
+ }
+ // SKIP: do nothing
+ } else {
+ ByteString valueBytes = convertToByteString(row, idx);
+ mutation.setCell(family, qualifier, timestamp, valueBytes);
+ }
+ }
+
+ return new RowKeyMutation(rowKey, mutation);
+ }
+
+ private ByteString buildRowKey(SeaTunnelRow row) {
+ if (rowkeyColumnIndexes.size() == 1) {
+ return fieldToByteString(row, rowkeyColumnIndexes.get(0));
+ }
+ String delimiter = parameters.getRowkeyDelimiter();
+ List<String> parts = new ArrayList<>();
+ for (Integer idx : rowkeyColumnIndexes) {
+ Object field = row.getField(idx);
+ parts.add(field == null ? "" : field.toString());
+ }
+ return ByteString.copyFromUtf8(String.join(delimiter, parts));
+ }
+
+ private ByteString fieldToByteString(SeaTunnelRow row, int index) {
+ Object field = row.getField(index);
+ if (field == null) {
+ return ByteString.EMPTY;
+ }
+ SeaTunnelDataType<?> fieldType = rowType.getFieldType(index);
+ if (fieldType.getSqlType() ==
org.apache.seatunnel.api.table.type.SqlType.BYTES) {
+ return ByteString.copyFrom((byte[]) field);
+ }
+ return ByteString.copyFromUtf8(field.toString());
+ }
+
+ private String resolveFamily(String fieldName) {
+ Map<String, String> familyMap = parameters.getColumnFamily();
+ if (familyMap == null) {
+ return defaultFamily;
+ }
+ return familyMap.getOrDefault(fieldName, defaultFamily);
+ }
+
+ private ByteString convertToByteString(SeaTunnelRow row, int index) {
+ Object field = row.getField(index);
+ SeaTunnelDataType<?> fieldType = rowType.getFieldType(index);
+ switch (fieldType.getSqlType()) {
+ case TINYINT:
+ return ByteString.copyFrom(new byte[] {(Byte) field});
+ case SMALLINT:
+ short sv = (Short) field;
+ return ByteString.copyFrom(new byte[] {(byte) (sv >> 8),
(byte) sv});
+ case INT:
+ int iv = (Integer) field;
+ return
ByteString.copyFrom(ByteBuffer.allocate(4).putInt(iv).array());
+ case BIGINT:
+ long lv = (Long) field;
+ return
ByteString.copyFrom(ByteBuffer.allocate(8).putLong(lv).array());
+ case FLOAT:
+ float fv = (Float) field;
+ return
ByteString.copyFrom(ByteBuffer.allocate(4).putFloat(fv).array());
+ case DOUBLE:
+ double dv = (Double) field;
+ return
ByteString.copyFrom(ByteBuffer.allocate(8).putDouble(dv).array());
+ case BOOLEAN:
+ return ByteString.copyFrom(new byte[] {(byte) ((Boolean) field
? 1 : 0)});
+ case BYTES:
+ return ByteString.copyFrom((byte[]) field);
+ case DECIMAL:
+ BigDecimal bd =
+ field instanceof BigDecimal
+ ? (BigDecimal) field
+ : new BigDecimal(field.toString());
+ return
ByteString.copyFrom(bd.toPlainString().getBytes(StandardCharsets.UTF_8));
+ case DATE:
+ LocalDate date =
+ field instanceof LocalDate
+ ? (LocalDate) field
+ : DateUtils.parse(field.toString());
+ return ByteString.copyFrom(
+ DateUtils.toString(date,
DateUtils.Formatter.YYYY_MM_DD)
+ .getBytes(StandardCharsets.UTF_8));
+ case TIME:
+ LocalTime time =
+ field instanceof LocalTime
+ ? (LocalTime) field
+ : TimeUtils.parse(field.toString());
+ return ByteString.copyFrom(
+ TimeUtils.toString(time, TimeUtils.Formatter.HH_MM_SS)
+ .getBytes(StandardCharsets.UTF_8));
+ case TIMESTAMP:
+ LocalDateTime ts =
+ field instanceof LocalDateTime
+ ? (LocalDateTime) field
+ : DateTimeUtils.parse(field.toString());
+ return ByteString.copyFrom(
+ DateTimeUtils.toString(ts,
DateTimeUtils.Formatter.YYYY_MM_DD_HH_MM_SS)
+ .getBytes(StandardCharsets.UTF_8));
+ case STRING:
+ return ByteString.copyFromUtf8(field.toString());
+ default:
+ throw new BigtableConnectorException(
+ BigtableConnectorErrorCode.WRITE_FAILED,
+ String.format(
+ "Bigtable connector does not support column
type [%s]",
+ fieldType.getSqlType()));
+ }
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSource.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSource.java
new file mode 100644
index 0000000000..9f9135f68e
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSource.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.apache.seatunnel.shade.com.google.common.collect.Lists;
+
+import org.apache.seatunnel.api.source.Boundedness;
+import org.apache.seatunnel.api.source.SeaTunnelSource;
+import org.apache.seatunnel.api.source.SourceReader;
+import org.apache.seatunnel.api.source.SourceSplitEnumerator;
+import org.apache.seatunnel.api.source.SupportParallelism;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.constant.BigtableIdentifier;
+
+import java.util.List;
+
+public class BigtableSource
+ implements SeaTunnelSource<SeaTunnelRow, BigtableSourceSplit,
BigtableSourceState>,
+ SupportParallelism {
+
+ private final CatalogTable catalogTable;
+ private final BigtableParameters parameters;
+
+ BigtableSource(BigtableParameters parameters, CatalogTable catalogTable) {
+ this.parameters = parameters;
+ this.catalogTable = catalogTable;
+ }
+
+ @Override
+ public String getPluginName() {
+ return BigtableIdentifier.IDENTIFIER_NAME;
+ }
+
+ @Override
+ public Boundedness getBoundedness() {
+ return Boundedness.BOUNDED;
+ }
+
+ @Override
+ public List<CatalogTable> getProducedCatalogTables() {
+ return Lists.newArrayList(catalogTable);
+ }
+
+ @Override
+ public SourceReader<SeaTunnelRow, BigtableSourceSplit> createReader(
+ SourceReader.Context readerContext) throws Exception {
+ return new BigtableSourceReader(
+ parameters, readerContext, catalogTable.getSeaTunnelRowType());
+ }
+
+ @Override
+ public SourceSplitEnumerator<BigtableSourceSplit, BigtableSourceState>
createEnumerator(
+ SourceSplitEnumerator.Context<BigtableSourceSplit>
enumeratorContext) throws Exception {
+ return new BigtableSourceSplitEnumerator(enumeratorContext,
parameters);
+ }
+
+ @Override
+ public SourceSplitEnumerator<BigtableSourceSplit, BigtableSourceState>
restoreEnumerator(
+ SourceSplitEnumerator.Context<BigtableSourceSplit>
enumeratorContext,
+ BigtableSourceState checkpointState)
+ throws Exception {
+ return new BigtableSourceSplitEnumerator(enumeratorContext,
parameters, checkpointState);
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceFactory.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceFactory.java
new file mode 100644
index 0000000000..bb4f4d2ad4
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.source.SeaTunnelSource;
+import org.apache.seatunnel.api.source.SourceSplit;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.connector.TableSource;
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.TableSourceFactory;
+import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableBaseOptions;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableSourceOptions;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.constant.BigtableIdentifier;
+
+import com.google.auto.service.AutoService;
+
+import java.io.Serializable;
+
+@AutoService(Factory.class)
+public class BigtableSourceFactory implements TableSourceFactory {
+
+ @Override
+ public String factoryIdentifier() {
+ return BigtableIdentifier.IDENTIFIER_NAME;
+ }
+
+ @Override
+ public OptionRule optionRule() {
+ return OptionRule.builder()
+ .required(
+ BigtableBaseOptions.PROJECT_ID,
+ BigtableBaseOptions.INSTANCE_ID,
+ BigtableBaseOptions.TABLE)
+ .optional(
+ BigtableBaseOptions.CREDENTIALS_PATH,
+ BigtableBaseOptions.ROWKEY_COLUMNS,
+ BigtableSourceOptions.START_ROW_KEY,
+ BigtableSourceOptions.END_ROW_KEY,
+ BigtableSourceOptions.START_TIMESTAMP,
+ BigtableSourceOptions.END_TIMESTAMP,
+ BigtableSourceOptions.MAX_VERSIONS,
+ BigtableSourceOptions.SCAN_ROW_LIMIT)
+ .build();
+ }
+
+ @Override
+ public Class<? extends SeaTunnelSource> getSourceClass() {
+ return BigtableSource.class;
+ }
+
+ @Override
+ public <T, SplitT extends SourceSplit, StateT extends Serializable>
+ TableSource<T, SplitT, StateT>
createSource(TableSourceFactoryContext context) {
+ return () ->
+ (SeaTunnelSource<T, SplitT, StateT>)
+ new BigtableSource(
+
BigtableParameters.buildWithSourceConfig(context.getOptions()),
+
CatalogTableUtil.buildWithConfig(context.getOptions()));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceReader.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceReader.java
new file mode 100644
index 0000000000..17e41107aa
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceReader.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.apache.seatunnel.api.source.Collector;
+import org.apache.seatunnel.api.source.SourceReader;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.client.BigtableClient;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.format.BigtableDeserializationFormat;
+
+import com.google.cloud.bigtable.data.v2.models.Filters;
+import com.google.cloud.bigtable.data.v2.models.Query;
+import com.google.cloud.bigtable.data.v2.models.Row;
+import com.google.cloud.bigtable.data.v2.models.RowCell;
+import com.google.protobuf.ByteString;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+/**
+ * Reads rows from a single {@link BigtableSourceSplit} and emits {@link
SeaTunnelRow} records.
+ *
+ * <p>The column schema is derived from the {@link SeaTunnelRowType}. Each
field name must follow
+ * the pattern {@code columnFamily:qualifier}, except the special name {@code
rowkey} which maps to
+ * the Bigtable row key.
+ */
+@Slf4j
+public class BigtableSourceReader implements SourceReader<SeaTunnelRow,
BigtableSourceSplit> {
+
+ private static final String ROW_KEY_FIELD = "rowkey";
+
+ private final Deque<BigtableSourceSplit> pendingSplits = new
ConcurrentLinkedDeque<>();
+ private final Context context;
+ private final SeaTunnelRowType rowType;
+ private final BigtableParameters parameters;
+ private final BigtableDeserializationFormat deserializationFormat;
+ private volatile boolean noMoreSplits = false;
+ private BigtableClient bigtableClient;
+
+ /** The split currently being read. Persisted in checkpoint to survive
failover. */
+ private volatile BigtableSourceSplit currentSplit = null;
+
+ /**
+ * Set of field names that map to the Bigtable row key. Populated from
{@code rowkey_column}
+ * config; falls back to the literal field name {@value ROW_KEY_FIELD}
when not configured.
+ */
+ private final java.util.Set<String> rowKeyFieldNames;
+
+ public BigtableSourceReader(
+ BigtableParameters parameters, Context context, SeaTunnelRowType
rowType) {
+ this(parameters, context, rowType, null);
+ }
+
+ BigtableSourceReader(
+ BigtableParameters parameters,
+ Context context,
+ SeaTunnelRowType rowType,
+ BigtableClient bigtableClient) {
+ this.parameters = parameters;
+ this.context = context;
+ this.rowType = rowType;
+ this.bigtableClient = bigtableClient;
+ this.deserializationFormat = new BigtableDeserializationFormat();
+ if (parameters.getRowkeyColumns() != null &&
!parameters.getRowkeyColumns().isEmpty()) {
+ this.rowKeyFieldNames = new
java.util.HashSet<>(parameters.getRowkeyColumns());
+ } else {
+ this.rowKeyFieldNames =
java.util.Collections.singleton(ROW_KEY_FIELD);
+ }
+ }
+
+ @Override
+ public void open() throws Exception {
+ if (bigtableClient == null) {
+ bigtableClient = BigtableClient.createInstance(parameters);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (bigtableClient != null) {
+ bigtableClient.close();
+ bigtableClient = null;
+ }
+ }
+
+ @Override
+ public void pollNext(Collector<SeaTunnelRow> output) throws Exception {
+ final BigtableSourceSplit split = pendingSplits.poll();
+ if (Objects.nonNull(split)) {
+ // Assign currentSplit before reading so checkpoints taken during
readSplit()
+ // include it and can re-enqueue it on restore.
+ currentSplit = split;
+ readSplit(split, output);
+ currentSplit = null;
+ } else if (noMoreSplits && pendingSplits.isEmpty()) {
+ log.info("Closed the bounded Bigtable source");
+ context.signalNoMoreElement();
+ } else {
+ log.warn("Waiting for Bigtable split, sleeping 1s");
+ Thread.sleep(1000L);
+ }
+ }
+
+ private void readSplit(BigtableSourceSplit split, Collector<SeaTunnelRow>
output) {
+ Query query = buildQuery(split);
+ // Stream rows one at a time to avoid buffering the full result in
memory.
+ // Each collect() acquires the checkpoint lock only for the emit, not
for the network read.
+ bigtableClient
+ .getDataClient()
+ .readRows(query)
+ .forEach(
+ bigtableRow -> {
+ SeaTunnelRow seaTunnelRow =
convertRow(bigtableRow);
+ synchronized (output.getCheckpointLock()) {
+ output.collect(seaTunnelRow);
+ }
+ });
+ }
+
+ private Query buildQuery(BigtableSourceSplit split) {
+ Query query = Query.create(parameters.getTable());
+
+ String startKey = split.getStartRowKey();
+ String endKey = split.getEndRowKey();
+ if (!startKey.isEmpty() && !endKey.isEmpty()) {
+ query.range(startKey, endKey);
+ } else if (!startKey.isEmpty()) {
+ query.range(startKey, null);
+ } else if (!endKey.isEmpty()) {
+ query.range(null, endKey);
+ }
+
+ if (parameters.getScanRowLimit() > 0) {
+ query.limit(parameters.getScanRowLimit());
+ }
+
+ Filters.Filter filter = buildFilter();
+ if (filter != null) {
+ query.filter(filter);
+ }
+
+ return query;
+ }
+
+ private Filters.Filter buildFilter() {
+ List<Filters.Filter> filters = new ArrayList<>();
+
+ if (parameters.getMaxVersions() > 0) {
+
filters.add(Filters.FILTERS.limit().cellsPerColumn(parameters.getMaxVersions()));
+ }
+
+ Long startTs = parameters.getStartTimestamp();
+ Long endTs = parameters.getEndTimestamp();
+ if (startTs != null && endTs != null) {
+ filters.add(Filters.FILTERS.timestamp().range().of(startTs,
endTs));
+ } else if (startTs != null) {
+
filters.add(Filters.FILTERS.timestamp().range().startClosed(startTs));
+ } else if (endTs != null) {
+ filters.add(Filters.FILTERS.timestamp().range().endOpen(endTs));
+ }
+
+ if (filters.isEmpty()) {
+ return null;
+ }
+ if (filters.size() == 1) {
+ return filters.get(0);
+ }
+ Filters.ChainFilter chain = Filters.FILTERS.chain();
+ for (Filters.Filter f : filters) {
+ chain = chain.filter(f);
+ }
+ return chain;
+ }
+
+ /**
+ * Converts a Bigtable {@link Row} into a {@link SeaTunnelRow}.
+ *
+ * <p>Field names drive the mapping:
+ *
+ * <ul>
+ * <li>Fields listed in {@code rowkey_column} config (or the literal
{@value ROW_KEY_FIELD}
+ * when the option is absent) → the row key bytes
+ * <li>{@code familyName:qualifier} → the latest cell value for that
column
+ * </ul>
+ */
+ private SeaTunnelRow convertRow(Row bigtableRow) {
+ // Build a flat lookup: "family:qualifier" -> latest cell value
+ Map<String, ByteString> cellMap = new HashMap<>();
+ for (RowCell cell : bigtableRow.getCells()) {
+ String key = cell.getFamily() + ":" +
cell.getQualifier().toStringUtf8();
+ cellMap.putIfAbsent(key, cell.getValue()); // first cell = latest
(sorted desc by ts)
+ }
+
+ String[] fieldNames = rowType.getFieldNames();
+ ByteString[] rawCells = new ByteString[fieldNames.length];
+ for (int i = 0; i < fieldNames.length; i++) {
+ String fieldName = fieldNames[i];
+ if (rowKeyFieldNames.contains(fieldName)) {
+ rawCells[i] = bigtableRow.getKey();
+ } else {
+ rawCells[i] = cellMap.get(fieldName);
+ }
+ }
+ return deserializationFormat.deserialize(rawCells, rowType);
+ }
+
+ @Override
+ public List<BigtableSourceSplit> snapshotState(long checkpointId) {
+ List<BigtableSourceSplit> state = new ArrayList<>();
+ // Include the split currently being read so it can be re-enqueued on
restore.
+ if (currentSplit != null) {
+ state.add(currentSplit);
+ }
+ state.addAll(pendingSplits);
+ return state;
+ }
+
+ @Override
+ public void addSplits(List<BigtableSourceSplit> splits) {
+ pendingSplits.addAll(splits);
+ }
+
+ @Override
+ public void handleNoMoreSplits() {
+ noMoreSplits = true;
+ }
+
+ @Override
+ public void notifyCheckpointComplete(long checkpointId) {}
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplit.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplit.java
new file mode 100644
index 0000000000..99e6782960
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplit.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.apache.seatunnel.api.source.SourceSplit;
+
+import java.io.Serializable;
+
+public class BigtableSourceSplit implements SourceSplit, Serializable {
+
+ private static final long serialVersionUID = 1L;
+ public static final String SPLIT_PREFIX = "bigtable_source_split_";
+
+ private final String splitId;
+ /** Inclusive start row key (empty means the table start). */
+ private final String startRowKey;
+ /** Exclusive end row key (empty means the table end). */
+ private final String endRowKey;
+
+ public BigtableSourceSplit(int splitIndex, String startRowKey, String
endRowKey) {
+ this.splitId = SPLIT_PREFIX + splitIndex;
+ this.startRowKey = startRowKey;
+ this.endRowKey = endRowKey;
+ }
+
+ @Override
+ public String splitId() {
+ return splitId;
+ }
+
+ public String getStartRowKey() {
+ return startRowKey;
+ }
+
+ public String getEndRowKey() {
+ return endRowKey;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "{\"split_id\":\"%s\", \"start\":\"%s\", \"end\":\"%s\"}",
+ splitId, startRowKey, endRowKey);
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplitEnumerator.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplitEnumerator.java
new file mode 100644
index 0000000000..3d41a2ad39
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplitEnumerator.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.apache.seatunnel.api.source.SourceSplitEnumerator;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Enumerates {@link BigtableSourceSplit} instances for parallel reading.
+ *
+ * <p>Currently produces a single split covering the full table (or the
user-defined row-key range).
+ * The split is assigned to whichever reader registers first. Future work can
partition by Bigtable
+ * tablet boundaries using the Admin API.
+ */
+@Slf4j
+public class BigtableSourceSplitEnumerator
+ implements SourceSplitEnumerator<BigtableSourceSplit,
BigtableSourceState> {
+
+ private final Context<BigtableSourceSplit> context;
+ private final BigtableParameters parameters;
+ private final Set<BigtableSourceSplit> assignedSplits;
+ private Set<BigtableSourceSplit> pendingSplits;
+ private boolean initialized = false;
+
+ public BigtableSourceSplitEnumerator(
+ Context<BigtableSourceSplit> context, BigtableParameters
parameters) {
+ this(context, parameters, new HashSet<>());
+ }
+
+ public BigtableSourceSplitEnumerator(
+ Context<BigtableSourceSplit> context,
+ BigtableParameters parameters,
+ BigtableSourceState sourceState) {
+ this(context, parameters, sourceState.getAssignedSplits());
+ }
+
+ private BigtableSourceSplitEnumerator(
+ Context<BigtableSourceSplit> context,
+ BigtableParameters parameters,
+ Set<BigtableSourceSplit> assignedSplits) {
+ this.context = context;
+ this.parameters = parameters;
+ this.assignedSplits = new HashSet<>(assignedSplits);
+ }
+
+ @Override
+ public void open() {
+ this.pendingSplits = new HashSet<>();
+ this.initialized = false;
+ }
+
+ @Override
+ public void run() throws Exception {
+ // Splits are assigned lazily when readers register.
+ }
+
+ @Override
+ public void close() throws IOException {
+ // Nothing to close – no persistent connection held here.
+ }
+
+ @Override
+ public void addSplitsBack(List<BigtableSourceSplit> splits, int subtaskId)
{
+ if (!splits.isEmpty()) {
+ pendingSplits.addAll(splits);
+ if (context.registeredReaders().contains(subtaskId)) {
+ assignSplit(subtaskId);
+ }
+ }
+ }
+
+ @Override
+ public int currentUnassignedSplitSize() {
+ return pendingSplits.size();
+ }
+
+ @Override
+ public void registerReader(int subtaskId) {
+ initializePendingSplits();
+ assignSplit(subtaskId);
+ }
+
+ @Override
+ public BigtableSourceState snapshotState(long checkpointId) throws
Exception {
+ return new BigtableSourceState(assignedSplits);
+ }
+
+ @Override
+ public void notifyCheckpointComplete(long checkpointId) throws Exception {}
+
+ @Override
+ public void handleSplitRequest(int subtaskId) {}
+
+ private void initializePendingSplits() {
+ if (initialized) {
+ return;
+ }
+ Set<BigtableSourceSplit> tableSplits = buildSplits();
+ Set<String> existingIds =
+ pendingSplits.stream()
+ .map(BigtableSourceSplit::splitId)
+ .collect(Collectors.toSet());
+ existingIds.addAll(
+ assignedSplits.stream()
+ .map(BigtableSourceSplit::splitId)
+ .collect(Collectors.toSet()));
+ tableSplits.stream()
+ .filter(s -> !existingIds.contains(s.splitId()))
+ .forEach(pendingSplits::add);
+ initialized = true;
+ }
+
+ /**
+ * Builds the set of splits.
+ *
+ * <p>For now a single split spanning the requested row-key range is
produced. This is
+ * sufficient for bounded batch reads. Parallel multi-split support can be
added later by
+ * querying Bigtable tablet boundary information.
+ */
+ private Set<BigtableSourceSplit> buildSplits() {
+ String startKey = parameters.getStartRowkey() != null ?
parameters.getStartRowkey() : "";
+ String endKey = parameters.getEndRowkey() != null ?
parameters.getEndRowkey() : "";
+ return Collections.singleton(new BigtableSourceSplit(0, startKey,
endKey));
+ }
+
+ private void assignSplit(int taskId) {
+ List<BigtableSourceSplit> toAssign = new ArrayList<>();
+ if (context.currentParallelism() == 1) {
+ toAssign.addAll(pendingSplits);
+ } else {
+ for (BigtableSourceSplit split : pendingSplits) {
+ int owner =
+ (split.splitId().hashCode() & Integer.MAX_VALUE)
+ % context.currentParallelism();
+ if (owner == taskId) {
+ toAssign.add(split);
+ }
+ }
+ }
+ context.assignSplit(taskId, toAssign);
+ assignedSplits.addAll(toAssign);
+ toAssign.forEach(pendingSplits::remove);
+ log.info(
+ "SubTask {} assigned [{}]",
+ taskId,
+ toAssign.stream()
+ .map(BigtableSourceSplit::splitId)
+ .collect(Collectors.joining(",")));
+ context.signalNoMoreSplits(taskId);
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceState.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceState.java
new file mode 100644
index 0000000000..e44fd024a9
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceState.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import java.io.Serializable;
+import java.util.Set;
+
+public class BigtableSourceState implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private final Set<BigtableSourceSplit> assignedSplits;
+
+ public BigtableSourceState(Set<BigtableSourceSplit> assignedSplits) {
+ this.assignedSplits = assignedSplits;
+ }
+
+ public Set<BigtableSourceSplit> getAssignedSplits() {
+ return assignedSplits;
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableAggregatedCommitInfo.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableAggregatedCommitInfo.java
new file mode 100644
index 0000000000..ca9858ae71
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableAggregatedCommitInfo.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.state;
+
+import java.io.Serializable;
+
+public class BigtableAggregatedCommitInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableCommitInfo.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableCommitInfo.java
new file mode 100644
index 0000000000..98b10945bd
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableCommitInfo.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.state;
+
+import java.io.Serializable;
+
+public class BigtableCommitInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableSinkState.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableSinkState.java
new file mode 100644
index 0000000000..e4d636e7f9
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/main/java/org/apache/seatunnel/connectors/seatunnel/bigtable/state/BigtableSinkState.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.state;
+
+import java.io.Serializable;
+
+public class BigtableSinkState implements Serializable {
+ private static final long serialVersionUID = 1L;
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkSaveModeTest.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkSaveModeTest.java
new file mode 100644
index 0000000000..9d3bd33ab3
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkSaveModeTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.sink.DataSaveMode;
+import org.apache.seatunnel.api.sink.SchemaSaveMode;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.api.table.type.BasicType;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.exception.BigtableConnectorException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Unit tests for {@link BigtableSink#handleSaveMode()}.
+ *
+ * <p>Verifies that unsupported save modes fail fast with a {@link
BigtableConnectorException}
+ * instead of silently no-opping (Issue 3 fix).
+ */
+class BigtableSinkSaveModeTest {
+
+ private CatalogTable catalogTable;
+
+ @BeforeEach
+ void setUp() {
+ catalogTable =
+ CatalogTable.of(
+ TableIdentifier.of("catalog", "database", "table"),
+ TableSchema.builder()
+ .column(
+ PhysicalColumn.of(
+ "id",
+ BasicType.STRING_TYPE,
+ (Long) null,
+ false,
+ null,
+ "row key"))
+ .column(
+ PhysicalColumn.of(
+ "cf:name",
+ BasicType.STRING_TYPE,
+ (Long) null,
+ true,
+ null,
+ ""))
+ .build(),
+ Collections.emptyMap(),
+ Collections.emptyList(),
+ "");
+ }
+
+ private ReadonlyConfig buildConfig(String schemaSaveMode, String
dataSaveMode) {
+ Map<String, Object> map = new HashMap<>();
+ map.put("project_id", "p");
+ map.put("instance_id", "i");
+ map.put("table", "t");
+ map.put("rowkey_column", Arrays.asList("id"));
+ map.put("column_family", Collections.singletonMap("all_columns",
"cf"));
+ if (schemaSaveMode != null) {
+ map.put("schema_save_mode", schemaSaveMode);
+ }
+ if (dataSaveMode != null) {
+ map.put("data_save_mode", dataSaveMode);
+ }
+ return ReadonlyConfig.fromMap(map);
+ }
+
+ private BigtableSink newSink() {
+ ReadonlyConfig config = buildConfig("RECREATE_SCHEMA", "APPEND_DATA");
+ return new BigtableSink(config, catalogTable);
+ }
+
+ private static void setField(Object target, String fieldName, Object
value) throws Exception {
+ Field field = BigtableSink.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(target, value);
+ }
+
+ /** Invokes the private handleSaveMode guard without creating a writer or
Bigtable client. */
+ private static void invokeHandleSaveMode(BigtableSink sink) throws
Exception {
+ Method method = BigtableSink.class.getDeclaredMethod("handleSaveMode");
+ method.setAccessible(true);
+ try {
+ method.invoke(sink);
+ } catch (InvocationTargetException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof Exception) {
+ throw (Exception) cause;
+ }
+ if (cause instanceof Error) {
+ throw (Error) cause;
+ }
+ throw e;
+ }
+ }
+
+ @Test
+ void testSupportedModeDoesNotThrow() {
+ BigtableSink sink = newSink();
+ assertDoesNotThrow(() -> invokeHandleSaveMode(sink));
+ }
+
+ @Test
+ void testDropDataThrows() throws Exception {
+ BigtableSink sink = newSink();
+ setField(sink, "dataSaveMode", DataSaveMode.DROP_DATA);
+ assertThrows(BigtableConnectorException.class, () ->
invokeHandleSaveMode(sink));
+ }
+
+ @Test
+ void testErrorWhenDataExistsThrows() throws Exception {
+ BigtableSink sink = newSink();
+ setField(sink, "dataSaveMode", DataSaveMode.ERROR_WHEN_DATA_EXISTS);
+ assertThrows(BigtableConnectorException.class, () ->
invokeHandleSaveMode(sink));
+ }
+
+ @Test
+ void testCreateSchemaWhenNotExistThrows() throws Exception {
+ BigtableSink sink = newSink();
+ setField(sink, "schemaSaveMode",
SchemaSaveMode.CREATE_SCHEMA_WHEN_NOT_EXIST);
+ assertThrows(BigtableConnectorException.class, () ->
invokeHandleSaveMode(sink));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkWriterTest.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkWriterTest.java
new file mode 100644
index 0000000000..1d8432190d
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/sink/BigtableSinkWriterTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.sink;
+
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.client.BigtableClient;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableSinkOptions;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class BigtableSinkWriterTest {
+
+ private BigtableClient mockClient;
+ private SeaTunnelRowType rowType;
+ private BigtableParameters parameters;
+
+ @BeforeEach
+ void setUp() {
+ mockClient = Mockito.mock(BigtableClient.class);
+
+ rowType =
+ new SeaTunnelRowType(
+ new String[] {"id", "name", "age"},
+ new
org.apache.seatunnel.api.table.type.SeaTunnelDataType[] {
+ BasicType.STRING_TYPE, BasicType.STRING_TYPE,
BasicType.INT_TYPE
+ });
+
+ Map<String, String> familyMap = new HashMap<>();
+ familyMap.put("all_columns", "cf");
+
+ parameters =
+ BigtableParameters.builder()
+ .projectId("test-project")
+ .instanceId("test-instance")
+ .table("test-table")
+ .rowkeyColumns(Arrays.asList("id"))
+ .columnFamily(familyMap)
+ .batchMutationSize(10)
+ .nullMode(BigtableSinkOptions.NullMode.SKIP)
+ .build();
+ }
+
+ @Test
+ void testWriteFlushesOnBatchSize() throws IOException {
+ List<Integer> rowkeyIndexes = Arrays.asList(0);
+ BigtableSinkWriter writer =
+ new BigtableSinkWriter(rowType, parameters, rowkeyIndexes, -1,
mockClient);
+
+ // Write batchMutationSize rows to trigger a flush
+ for (int i = 0; i < parameters.getBatchMutationSize(); i++) {
+ SeaTunnelRow row = new SeaTunnelRow(3);
+ row.setField(0, "key-" + i);
+ row.setField(1, "name-" + i);
+ row.setField(2, i);
+ writer.write(row);
+ }
+
+ verify(mockClient, times(1)).bulkMutate(anyList());
+ }
+
+ @Test
+ void testCloseFlushesRemainingRows() throws IOException {
+ List<Integer> rowkeyIndexes = Arrays.asList(0);
+ BigtableSinkWriter writer =
+ new BigtableSinkWriter(rowType, parameters, rowkeyIndexes, -1,
mockClient);
+
+ // Write fewer rows than batch size
+ SeaTunnelRow row = new SeaTunnelRow(3);
+ row.setField(0, "key-1");
+ row.setField(1, "alice");
+ row.setField(2, 30);
+ writer.write(row);
+
+ // No flush yet
+ verify(mockClient, times(0)).bulkMutate(anyList());
+
+ // Close should flush
+ writer.close();
+ verify(mockClient, times(1)).bulkMutate(anyList());
+ verify(mockClient, times(1)).close();
+ }
+
+ @Test
+ void testNullFieldSkipped() throws IOException {
+ List<Integer> rowkeyIndexes = Arrays.asList(0);
+ BigtableSinkWriter writer =
+ new BigtableSinkWriter(rowType, parameters, rowkeyIndexes, -1,
mockClient);
+
+ SeaTunnelRow row = new SeaTunnelRow(3);
+ row.setField(0, "key-null");
+ row.setField(1, null); // should be skipped
+ row.setField(2, 25);
+ writer.write(row);
+ writer.close();
+
+ // Verify a mutation was still produced (null field just omitted)
+ verify(mockClient, times(1)).bulkMutate(anyList());
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceReaderTest.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceReaderTest.java
new file mode 100644
index 0000000000..d1e8852028
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceReaderTest.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.apache.seatunnel.api.source.Collector;
+import org.apache.seatunnel.api.source.SourceReader;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.client.BigtableClient;
+import
org.apache.seatunnel.connectors.seatunnel.bigtable.config.BigtableParameters;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import com.google.api.gax.rpc.ServerStream;
+import com.google.cloud.bigtable.data.v2.BigtableDataClient;
+import com.google.cloud.bigtable.data.v2.models.Query;
+import com.google.cloud.bigtable.data.v2.models.Row;
+import com.google.cloud.bigtable.data.v2.models.RowCell;
+import com.google.protobuf.ByteString;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for {@link BigtableSourceReader}.
+ *
+ * <p>Covers:
+ *
+ * <ul>
+ * <li>Checkpoint state includes the in-flight split (Issue 1 fix)
+ * <li>Streaming read path — rows emitted via forEach, not buffered (Issue 2
fix)
+ * <li>rowkey_column config drives row-key field mapping (Issue 3 fix)
+ * </ul>
+ */
+class BigtableSourceReaderTest {
+
+ private BigtableClient mockClient;
+ private BigtableDataClient mockDataClient;
+ private SourceReader.Context mockContext;
+ private SeaTunnelRowType rowType;
+ private BigtableParameters parameters;
+
+ @BeforeEach
+ void setUp() {
+ mockClient = mock(BigtableClient.class);
+ mockDataClient = mock(BigtableDataClient.class);
+ when(mockClient.getDataClient()).thenReturn(mockDataClient);
+
+ mockContext = mock(SourceReader.Context.class);
+
+ rowType =
+ new SeaTunnelRowType(
+ new String[] {"rowkey", "cf:name"},
+ new
org.apache.seatunnel.api.table.type.SeaTunnelDataType[] {
+ BasicType.STRING_TYPE, BasicType.STRING_TYPE
+ });
+
+ parameters =
BigtableParameters.builder().projectId("p").instanceId("i").table("t").build();
+ }
+
+ /** Matches production lifecycle: open() initializes the client before
pollNext(). */
+ private BigtableSourceReader createOpenedReader(
+ BigtableParameters params, SeaTunnelRowType type) throws Exception
{
+ BigtableSourceReader reader =
+ new BigtableSourceReader(params, mockContext, type,
mockClient);
+ reader.open();
+ return reader;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void mockReadStream(Runnable duringForEach) {
+ ServerStream<Row> fakeStream = mock(ServerStream.class);
+ Mockito.doAnswer(
+ invocation -> {
+ if (duringForEach != null) {
+ duringForEach.run();
+ }
+ return null;
+ })
+ .when(fakeStream)
+ .forEach(any());
+ when(mockDataClient.readRows(any(Query.class))).thenReturn(fakeStream);
+ }
+
+ //
-------------------------------------------------------------------------
+ // Issue 1: snapshotState must include the currently-being-read split
+ //
-------------------------------------------------------------------------
+
+ /**
+ * When a split is being read (between addSplits and end of readSplit),
snapshotState must
+ * include it so that a failover can re-enqueue it.
+ */
+ @Test
+ void testSnapshotStateIncludesInFlightSplit() throws Exception {
+ BigtableSourceSplit split = new BigtableSourceSplit(0, "a", "z");
+ final List<BigtableSourceSplit>[] capturedState = new List[1];
+
+ BigtableSourceReader reader = createOpenedReader(parameters, rowType);
+ reader.addSplits(Collections.singletonList(split));
+
+ mockReadStream(() -> capturedState[0] = reader.snapshotState(1L));
+
+ Collector<SeaTunnelRow> collector = mock(Collector.class);
+ when(collector.getCheckpointLock()).thenReturn(new Object());
+
+ reader.pollNext(collector);
+
+ assertTrue(
+ capturedState[0].stream().anyMatch(s ->
s.splitId().equals(split.splitId())),
+ "snapshotState taken during readSplit() must include the
in-flight split");
+ }
+
+ /**
+ * After readSplit() completes, currentSplit is cleared. A snapshot taken
after that must NOT
+ * re-include the already-finished split.
+ */
+ @Test
+ void testSnapshotStateAfterReadDoesNotDuplicateSplit() throws Exception {
+ BigtableSourceSplit split = new BigtableSourceSplit(0, "", "");
+
+ BigtableSourceReader reader = createOpenedReader(parameters, rowType);
+ reader.addSplits(Collections.singletonList(split));
+
+ mockReadStream(null);
+
+ Collector<SeaTunnelRow> collector = mock(Collector.class);
+ when(collector.getCheckpointLock()).thenReturn(new Object());
+
+ reader.pollNext(collector);
+
+ List<BigtableSourceSplit> state = reader.snapshotState(2L);
+ assertTrue(state.isEmpty(), "State after completed read must be
empty");
+ }
+
+ //
-------------------------------------------------------------------------
+ // Issue 2: rows must be emitted via streaming forEach, not buffered
+ //
-------------------------------------------------------------------------
+
+ /**
+ * Verifies that each row is emitted individually via output.collect()
inside the forEach
+ * lambda, rather than being buffered first.
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ void testRowsEmittedStreamingNotBuffered() throws Exception {
+ BigtableSourceSplit split = new BigtableSourceSplit(0, "", "");
+
+ Row fakeRow = mock(Row.class);
+ RowCell cell = mock(RowCell.class);
+ when(cell.getFamily()).thenReturn("cf");
+ when(cell.getQualifier()).thenReturn(ByteString.copyFromUtf8("name"));
+ when(cell.getValue()).thenReturn(ByteString.copyFromUtf8("alice"));
+ when(fakeRow.getCells()).thenReturn(Collections.singletonList(cell));
+ when(fakeRow.getKey()).thenReturn(ByteString.copyFromUtf8("row-1"));
+
+ ServerStream<Row> fakeStream = mock(ServerStream.class);
+ Mockito.doAnswer(
+ invocation -> {
+ Consumer<Row> action = invocation.getArgument(0);
+ action.accept(fakeRow);
+ return null;
+ })
+ .when(fakeStream)
+ .forEach(any());
+ when(mockDataClient.readRows(any(Query.class))).thenReturn(fakeStream);
+
+ BigtableSourceReader reader = createOpenedReader(parameters, rowType);
+ reader.addSplits(Collections.singletonList(split));
+
+ Object lock = new Object();
+ Collector<SeaTunnelRow> collector = mock(Collector.class);
+ when(collector.getCheckpointLock()).thenReturn(lock);
+
+ reader.pollNext(collector);
+
+ ArgumentCaptor<SeaTunnelRow> captor =
ArgumentCaptor.forClass(SeaTunnelRow.class);
+ verify(collector).collect(captor.capture());
+ assertEquals("alice", captor.getValue().getField(1));
+ }
+
+ //
-------------------------------------------------------------------------
+ // Issue 3: rowkey_column config drives field mapping
+ //
-------------------------------------------------------------------------
+
+ /**
+ * When rowkey_column is configured, the named field should receive the
row key value, not the
+ * default literal "rowkey".
+ */
+ @SuppressWarnings("unchecked")
+ @Test
+ void testRowkeyColumnConfigMapsCorrectField() throws Exception {
+ SeaTunnelRowType customRowType =
+ new SeaTunnelRowType(
+ new String[] {"id", "cf:value"},
+ new
org.apache.seatunnel.api.table.type.SeaTunnelDataType[] {
+ BasicType.STRING_TYPE, BasicType.STRING_TYPE
+ });
+
+ BigtableParameters paramsWithRowkeyCol =
+ BigtableParameters.builder()
+ .projectId("p")
+ .instanceId("i")
+ .table("t")
+ .rowkeyColumns(Collections.singletonList("id"))
+ .build();
+
+ Row fakeRow = mock(Row.class);
+ RowCell cell = mock(RowCell.class);
+ when(cell.getFamily()).thenReturn("cf");
+ when(cell.getQualifier()).thenReturn(ByteString.copyFromUtf8("value"));
+ when(cell.getValue()).thenReturn(ByteString.copyFromUtf8("hello"));
+ when(fakeRow.getCells()).thenReturn(Collections.singletonList(cell));
+ when(fakeRow.getKey()).thenReturn(ByteString.copyFromUtf8("my-key"));
+
+ ServerStream<Row> fakeStream = mock(ServerStream.class);
+ Mockito.doAnswer(
+ invocation -> {
+ Consumer<Row> action = invocation.getArgument(0);
+ action.accept(fakeRow);
+ return null;
+ })
+ .when(fakeStream)
+ .forEach(any());
+ when(mockDataClient.readRows(any(Query.class))).thenReturn(fakeStream);
+
+ BigtableSourceReader reader = createOpenedReader(paramsWithRowkeyCol,
customRowType);
+ reader.addSplits(Collections.singletonList(new BigtableSourceSplit(0,
"", "")));
+
+ Object lock = new Object();
+ Collector<SeaTunnelRow> collector = mock(Collector.class);
+ when(collector.getCheckpointLock()).thenReturn(lock);
+
+ reader.pollNext(collector);
+
+ ArgumentCaptor<SeaTunnelRow> captor =
ArgumentCaptor.forClass(SeaTunnelRow.class);
+ verify(collector).collect(captor.capture());
+ assertEquals("my-key", captor.getValue().getField(0));
+ assertEquals("hello", captor.getValue().getField(1));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplitEnumeratorTest.java
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplitEnumeratorTest.java
new file mode 100644
index 0000000000..b9d764b34a
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-google-bigtable/src/test/java/org/apache/seatunnel/connectors/seatunnel/bigtable/source/BigtableSourceSplitEnumeratorTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.bigtable.source;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class BigtableSourceSplitEnumeratorTest {
+
+ @Test
+ void testSingleSplitFullRange() {
+ BigtableSourceSplit split = new BigtableSourceSplit(0, "", "");
+ assertEquals("bigtable_source_split_0", split.splitId());
+ assertEquals("", split.getStartRowKey());
+ assertEquals("", split.getEndRowKey());
+ }
+
+ @Test
+ void testSplitWithRowKeyRange() {
+ BigtableSourceSplit split = new BigtableSourceSplit(0, "aaa", "zzz");
+ assertEquals("aaa", split.getStartRowKey());
+ assertEquals("zzz", split.getEndRowKey());
+ }
+}
diff --git a/seatunnel-connectors-v2/pom.xml b/seatunnel-connectors-v2/pom.xml
index 6e0266fa6d..bb47dfb588 100644
--- a/seatunnel-connectors-v2/pom.xml
+++ b/seatunnel-connectors-v2/pom.xml
@@ -77,6 +77,7 @@
<module>connector-tdengine</module>
<module>connector-selectdb-cloud</module>
<module>connector-hbase</module>
+ <module>connector-google-bigtable</module>
<module>connector-rocketmq</module>
<module>connector-amazonsqs</module>
<module>connector-paimon</module>
diff --git a/seatunnel-dist/pom.xml b/seatunnel-dist/pom.xml
index ec8fd7a951..92d25d8aa8 100644
--- a/seatunnel-dist/pom.xml
+++ b/seatunnel-dist/pom.xml
@@ -418,6 +418,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>org.apache.seatunnel</groupId>
+ <artifactId>connector-google-bigtable</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.seatunnel</groupId>
<artifactId>connector-datahub</artifactId>