diff --git a/docs/en/connectors/sink/Assert.md b/docs/en/connectors/sink/Assert.md
index 40c143fd8972..3807e6880dda 100644
--- a/docs/en/connectors/sink/Assert.md
+++ b/docs/en/connectors/sink/Assert.md
@@ -17,8 +17,11 @@ Assert is a sink connector used to validate pipeline output. It checks row count
## Key Features
- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
-- [ ] [timer flush](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [x] [stream](../../introduction/concepts/connector-v2-features.md)
- [x] [support multiple table write](../../introduction/concepts/connector-v2-features.md)
+- [ ] [timer flush](../../introduction/concepts/connector-v2-features.md)
## Options
@@ -135,6 +138,16 @@ Sink plugin common parameters, please refer to [Sink Common Options](../common-o
- `tables_configs` is used for multi-table jobs. The `table_path` value must match the table path carried by the upstream source.
- `equals_to` compares the actual field value with the configured expected value. For complex values such as array, map, and row, use the same HOCON value shape as the source data.
+:::tip
+
+The Assert sink is a terminal sink — it has no external system to write to. Use it to validate intermediate results without needing a downstream database. The connector does not interpret `UPDATE` or `DELETE` row kinds as CDC operations; every received row is asserted against the configured rules. If the configured row range, field value, or catalog metadata check fails, the job fails with the matching error message.
+
+:::
+
+## Streaming Validation
+
+Assert works in both `BATCH` and `STREAMING` job modes. Field rules (`NOT_NULL`, `MIN_LENGTH`, `MAX_LENGTH`, etc.) are checked on every row as it arrives at the sink writer. Row count rules (`MIN_ROW` / `MAX_ROW`) are evaluated **exactly once** when the sink writer closes (at job shutdown, savepoint, or failure), against the cumulative row count observed by that writer instance since it was created — not per checkpoint window, and not reset between checkpoints. If you need per-checkpoint-window row-count validation, that requires a source code change (out of scope for a docs update).
+
## Example
### Simple
@@ -631,6 +644,67 @@ sink {
```
+### Stream Validation With Checkpoint Window
+
+The example below shows a streaming job that ends with the row count satisfying the cumulative `MIN_ROW` / `MAX_ROW` window (`50 ≤ total rows ≤ 5000`). The check runs once at writer close against the cumulative count, not per checkpoint window.
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 60000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema {
+ fields {
+ name = string
+ age = int
+ }
+ }
+ plugin_output = "stream_data"
+ }
+}
+
+sink {
+ Assert {
+ plugin_input = "stream_data"
+ rules =
+ {
+ row_rules = [
+ {
+ rule_type = MIN_ROW
+ rule_value = 50
+ },
+ {
+ rule_type = MAX_ROW
+ rule_value = 5000
+ }
+ ],
+ field_rules = [{
+ field_name = age
+ field_type = int
+ field_value = [
+ {
+ rule_type = NOT_NULL
+ },
+ {
+ rule_type = MIN
+ rule_value = 0
+ },
+ {
+ rule_type = MAX
+ rule_value = 150
+ }
+ ]
+ }]
+ }
+ }
+}
+```
+
## Changelog
diff --git a/docs/en/connectors/sink/GoogleBigtable.md b/docs/en/connectors/sink/GoogleBigtable.md
index a57905e2bcf8..8ff42164a50f 100644
--- a/docs/en/connectors/sink/GoogleBigtable.md
+++ b/docs/en/connectors/sink/GoogleBigtable.md
@@ -4,15 +4,21 @@ import ChangeLog from '../changelog/connector-google-bigtable.md';
> Google Bigtable sink connector
+## Support Those Engines
+
+> SeaTunnel Zeta
+
## Description
Writes data to Google Cloud Bigtable using the native Bigtable Data v2 Java client.
-## Key features
+## Key Features
- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
- [x] [support multiple table write](../../introduction/concepts/connector-v2-features.md)
+- [ ] [timer flush](../../introduction/concepts/connector-v2-features.md)
## Options
@@ -30,7 +36,7 @@ Writes data to Google Cloud Bigtable using the native Bigtable Data v2 Java clie
| batch_mutation_size | int | no | 100 |
| schema_save_mode | enum | no | RECREATE_SCHEMA |
| data_save_mode | enum | no | APPEND_DATA |
-| multi_table_sink_replica | int | no | - |
+| multi_table_sink_replica | int | no | 1 |
| common-options | | no | - |
### project_id [string]
@@ -43,13 +49,13 @@ Bigtable instance ID. Example: `"my-bigtable-instance"`
### table [string]
-The Bigtable table name to write to. Example: `"my-table"`
+The Bigtable table name to write to. Example: `"my-table"`. The connector does not create the Bigtable table; create it (with all required column families) before running the job.
### 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`.
+When multiple columns are specified they are joined with `rowkey_delimiter`. With a single row-key column, a null or empty value fails the job with `WRITE_FAILED`. With multiple row-key columns, a null value in any non-last column silently becomes an empty segment in the composed row key (joined by `rowkey_delimiter`); only when the entire composed key is empty does the job fail.
### column_family [config]
@@ -70,6 +76,8 @@ column_family {
}
```
+Field names that do not appear in the map fall back to the `all_columns` family, or to the default family `cf` if `all_columns` is not configured.
+
### credentials_path [string]
Path to the Google Cloud service account JSON key file.
@@ -109,13 +117,13 @@ Data save mode. Only `APPEND_DATA` is supported now.
### multi_table_sink_replica [int]
-The number of sink replicas used for multi-table writing. For details, see [Sink Common Options](../common-options/sink-common-options.md).
+The number of sink replicas used for multi-table writing. For details, see [Sink Common Options](../common-options/sink-common-options.md). `multi_table_sink_replica` increases the number of parallel writer replicas within a single sink instance; the target Bigtable table is fixed by the `table` option and is not derived per upstream table.
### common options
Sink plugin common parameters, please refer to [Sink Common Options](../common-options/sink-common-options.md) for details.
-## Data Types
+## Data Type Mapping
All SeaTunnel types are supported:
@@ -137,15 +145,20 @@ All SeaTunnel types are supported:
:::tip
-Bigtable does not have relational columns. The sink writes every non-row-key field as a Bigtable cell. The target column family is selected by `column_family`; the Bigtable qualifier is the SeaTunnel field name.
+Bigtable does not have relational columns. The sink writes every non-row-key field as a Bigtable cell. The target column family is selected by `column_family`; the Bigtable qualifier is the SeaTunnel field name. The sink treats every upstream row as an unconditional cell mutation, so `UPDATE` / `DELETE` row kinds are not interpreted as CDC operations and overwrite the previous cell under the same `(row key, column family, qualifier)` triple.
:::
-## Example
+## Task Example
### Basic — Application Default Credentials
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -162,6 +175,11 @@ sink {
### Service Account Key File
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -181,6 +199,11 @@ sink {
### Multiple Column Families
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -200,6 +223,11 @@ sink {
### Use a version column and empty null values
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -217,6 +245,52 @@ sink {
}
```
+### Streaming write with checkpoint flush
+
+In streaming mode, the writer flushes the in-memory mutation buffer at every checkpoint. The current `batch_mutation_size` still controls the in-task buffer; checkpoint frequency only affects how often already buffered mutations are sent to Bigtable.
+
+```hocon
+env {
+ parallelism = 2
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema {
+ fields {
+ tenant_id = string
+ event_id = string
+ event_ts = bigint
+ event_type = string
+ payload = string
+ }
+ }
+ plugin_output = "events_stream"
+ }
+}
+
+sink {
+ GoogleBigtable {
+ plugin_input = "events_stream"
+ 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 = "#"
+ version_column = "event_ts"
+ column_family {
+ all_columns = "data"
+ event_type = "meta"
+ }
+ batch_mutation_size = 200
+ }
+}
+```
+
## Changelog
diff --git a/docs/en/connectors/sink/GoogleFirestore.md b/docs/en/connectors/sink/GoogleFirestore.md
index 2b653a75a7b4..38f7232107fa 100644
--- a/docs/en/connectors/sink/GoogleFirestore.md
+++ b/docs/en/connectors/sink/GoogleFirestore.md
@@ -25,8 +25,9 @@ indexes in Google Cloud before running queries that need them.
- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
- [x] [batch](../../introduction/concepts/connector-v2-features.md)
-- [ ] [stream](../../introduction/concepts/connector-v2-features.md)
+- [x] [stream](../../introduction/concepts/connector-v2-features.md)
- [ ] [support multiple table write](../../introduction/concepts/connector-v2-features.md)
+- [ ] [timer flush](../../introduction/concepts/connector-v2-features.md)
## Supported DataSource Info
@@ -99,14 +100,16 @@ Sink plugin common parameters, please refer to [Sink Common Options](../common-o
## Notes
- The connector currently provides a sink only. There is no GoogleFirestore source connector.
-- Each sink block writes to one configured collection. It does not switch collections automatically for multi-table input.
+- Each sink block writes to one configured collection. It does not switch collections automatically for multi-table input; use one sink block per Firestore collection.
- Firestore document IDs are generated automatically. Use another connector or transform before this sink if you need deterministic document IDs.
-- The sink does not interpret `UPDATE` or `DELETE` row kinds as CDC operations.
+- The sink does not interpret `UPDATE` or `DELETE` row kinds as CDC operations — every row triggers a Firestore `add` call that produces a new document.
- Do not put raw service account JSON directly in `credentials`; encode it with Base64 first.
-- Field names in the upstream SeaTunnel schema become Firestore document field
- names.
+- Field names in the upstream SeaTunnel schema become Firestore document field names.
+- The connector works in both `BATCH` and `STREAMING` job modes. In the current implementation `FirestoreSinkWriter.write()` calls the Firestore client's `add(...)` once per row and does not buffer or batch rows, so there is no in-memory write buffer to flush at checkpoint boundaries; checkpoint completion does not imply that all previously written rows have reached Firestore.
-## Example
+## Task Example
+
+### Batch write of typed rows
```hocon
env {
@@ -153,6 +156,39 @@ sink {
}
```
+### Streaming write with checkpoint interval
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 100
+ schema = {
+ fields {
+ c_string = string
+ c_int = int
+ c_timestamp = timestamp
+ }
+ }
+ plugin_output = "firestore_stream"
+ }
+}
+
+sink {
+ GoogleFirestore {
+ plugin_input = "firestore_stream"
+ project_id = "my-gcp-project"
+ collection = "events"
+ credentials = "base64-service-account-json"
+ }
+}
+```
+
## Changelog
diff --git a/docs/en/connectors/sink/Lance.md b/docs/en/connectors/sink/Lance.md
index f32a6313f369..113240fc4aa2 100644
--- a/docs/en/connectors/sink/Lance.md
+++ b/docs/en/connectors/sink/Lance.md
@@ -13,7 +13,11 @@ import ChangeLog from '../changelog/connector-lance.md';
## Key Features
- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [x] [stream](../../introduction/concepts/connector-v2-features.md)
- [x] [support multiple table write](../../introduction/concepts/connector-v2-features.md)
+- [ ] [timer flush](../../introduction/concepts/connector-v2-features.md)
## Description
@@ -88,6 +92,10 @@ The target Lance table name. When it is not set, the connector uses the upstream
Controls how Lance writes the dataset. The default is `CREATE`. Use a value
supported by Lance `WriteParams.WriteMode`: `CREATE`, `APPEND`, or `OVERWRITE`.
+### lance.write.enable.stable.row.ids
+
+Whether to enable stable row IDs when writing Lance data. The connector reads this option into `LanceSinkConfig.enableStableRowIds` and exposes it via `getEnableStableRowIds()`, but **the value is currently parsed but not yet applied to the underlying Lance `WriteParams`** in `LanceSinkWriter.initializeDataset()` — toggling it has no observable effect on the write path today. This is a known gap pending a follow-up connector change.
+
### lance.write.storage.options
Passes extra storage parameters to Lance as key-value pairs.
@@ -101,9 +109,15 @@ lance.write.storage.options = {
}
```
+### multi_table_sink_replica
+
+Replica count used by the multi-table sink routing mechanism. Increase it when
+a single multi-table job writes to many Lance tables and the default single
+replica becomes a bottleneck. See [Sink Common Options](../common-options/sink-common-options.md).
+
## Data Type Mapping
-Lance uses the Apache Arrow type system. The sink creates the Lance schema from the incoming SeaTunnel schema.
+Lance uses the Apache Arrow type system. The sink creates the Lance schema from the incoming SeaTunnel schema. The current mapping narrows every integer SeaTunnel type (`TINYINT`, `SMALLINT`, `INT`, `BIGINT`) to Arrow `int32`, so `BIGINT` values outside the signed 32-bit range are truncated.
| SeaTunnel Data Type | Lance / Arrow Data Type |
|---------------------|-------------------------|
@@ -111,19 +125,25 @@ Lance uses the Apache Arrow type system. The sink creates the Lance schema from
| TINYINT | int32 |
| SMALLINT | int32 |
| INT | int32 |
-| BIGINT | int32 |
+| BIGINT | int32 (values outside the signed 32-bit range are truncated) |
| FLOAT | float32 |
| DOUBLE | float64 |
| DECIMAL | decimal128 |
| NULL | null |
| BYTES | binary |
| DATE | date32 |
-| TIME | time32 |
-| TIMESTAMP | timestamp |
+| TIME | time32 (millisecond precision) |
+| TIMESTAMP | timestamp (microsecond, Asia/Shanghai timezone) |
| STRING | utf8 |
| ARRAY | list |
| MAP | map |
+:::tip
+
+The sink does not interpret `UPDATE` / `DELETE` row kinds as CDC operations — every upstream row is appended to the Lance dataset using the configured `lance.write.mode`. In streaming mode, the writer flushes the in-memory row buffer to Lance on every checkpoint.
+
+:::
+
## Task Example
### Write FakeSource Data To Lance
@@ -167,6 +187,79 @@ sink {
}
```
+### Append Mode With Larger File Fragments
+
+`APPEND` mode keeps the existing dataset and adds new rows. Increase
+`lance.write.max-rows-per-file` and `lance.write.max-bytes-per-file` to reduce
+the number of Lance fragments when appending large batches.
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
+source {
+ FakeSource {
+ row.num = 1000000
+ schema = {
+ fields {
+ c_string = string
+ c_int = int
+ }
+ }
+ plugin_output = "fake"
+ }
+}
+
+sink {
+ Lance {
+ dataset_path = "/tmp/seatunnel_mnt/lanceTest/lance_sink_table"
+ namespace_type = "dir"
+ namespace_id = "root"
+ table = "lance_sink_table"
+ lance.write.mode = "APPEND"
+ lance.write.max-rows-per-file = 100000
+ lance.write.max-rows-per-group = 5000
+ lance.write.max-bytes-per-file = 134217728
+ }
+}
+```
+
+### Streaming Append With Checkpoint Flush
+
+```hocon
+env {
+ parallelism = 2
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema = {
+ fields {
+ c_string = string
+ c_int = int
+ }
+ }
+ plugin_output = "fake_stream"
+ }
+}
+
+sink {
+ Lance {
+ plugin_input = "fake_stream"
+ dataset_path = "/tmp/seatunnel_mnt/lanceTest/lance_sink_table"
+ namespace_type = "dir"
+ namespace_id = "root"
+ table = "lance_sink_table"
+ lance.write.mode = "APPEND"
+ }
+}
+```
+
## Changelog
diff --git a/docs/en/connectors/sink/Typesense.md b/docs/en/connectors/sink/Typesense.md
index bf3a2fc0d461..dcc538dafb75 100644
--- a/docs/en/connectors/sink/Typesense.md
+++ b/docs/en/connectors/sink/Typesense.md
@@ -2,6 +2,12 @@ import ChangeLog from '../changelog/connector-typesense.md';
# Typesense
+> Typesense sink connector
+
+## Support Those Engines
+
+> SeaTunnel Zeta
+
## Description
Writes SeaTunnel rows to a Typesense collection. The connector can create the target collection
@@ -13,6 +19,8 @@ or more primary key fields.
- [ ] [Exactly Once](../../introduction/concepts/connector-v2-features.md)
- [x] [CDC](../../introduction/concepts/connector-v2-features.md)
- [x] [Multiple Table Sink](../../introduction/concepts/connector-v2-features.md)
+- [x] [batch](../../introduction/concepts/connector-v2-features.md)
+- [x] [stream](../../introduction/concepts/connector-v2-features.md)
- [ ] [timer flush](../../introduction/concepts/connector-v2-features.md)
## Options
@@ -28,21 +36,21 @@ or more primary key fields.
| api_key | string | Yes | - | Typesense API key. |
| max_retry_count | int | No | 3 | Maximum retry count for one bulk request. |
| max_batch_size | int | No | 10 | Maximum number of documents sent in one bulk request. |
-| multi_table_sink_replica | int | No | - | Number of sink replicas used by the common multi-table sink routing mechanism. |
+| multi_table_sink_replica | int | No | 1 | Number of sink replicas used by the common multi-table sink routing mechanism. |
| common-options | | No | - | Common sink options. |
### hosts [array]
-The access address for Typesense, formatted as `host:port`, e.g., `["typesense-01:8108"]`.
+The access address for Typesense, formatted as `host:port`, e.g., `["typesense-01:8108"]`. When several nodes are configured, the sink keeps a single client per writer and does not balance writes across them.
### collection [string]
-The name of the collection to write to, e.g., "seatunnel".
+The name of the collection to write to, e.g., `"seatunnel"`. In multi-table jobs, every table routes to the same configured collection; configure one sink block per target collection if different tables need different destinations.
### primary_keys [array]
Primary key fields used to generate the document `id`. When more than one field is configured,
-the connector joins their values with `key_delimiter`.
+the connector joins their values with `key_delimiter`. Without `primary_keys`, Typesense assigns its own document IDs and the connector behaves as an append-only write.
### key_delimiter [string]
@@ -50,15 +58,15 @@ Sets the delimiter for composite keys (default is `_`).
### api_key [string]
-The `api_key` for secure access to Typesense.
+The `api_key` for secure access to Typesense. Treat this value as a secret and prefer passing it via a job secret or environment variable when running on shared infrastructure.
### max_retry_count [int]
-The maximum number of retry attempts for one batch request.
+The maximum number of retry attempts for one batch request. The retry predicate is `exception -> true`, so the connector retries on every exception thrown by `typesenseClient.insert(...)` (network errors, timeouts, and Typesense error responses alike) up to `max_retry_count` times with a fixed 200 ms backoff; it does not currently distinguish transient from permanent failures.
### max_batch_size [int]
-The maximum number of documents sent in one batch.
+The maximum number of documents sent in one batch. Typesense caps each request; keep this value below the Typesense server-side `per_page` limit.
### multi_table_sink_replica [int]
@@ -86,11 +94,17 @@ Choose how to handle existing data on the target side before starting the synchr
- `APPEND_DATA`: Retains both the database structure and the data.
- `ERROR_WHEN_DATA_EXISTS`: Throws an error if data exists.
+:::tip
+
+The connector uses Typesense's bulk import endpoint. `UPDATE` and `DELETE` row kinds are not interpreted as CDC operations — every upstream row is upserted into the target collection based on the generated document `id`. Use `data_save_mode = DROP_DATA` together with a stable `primary_keys` configuration to make repeated jobs behave like upserts rather than appends.
+
+:::
+
## Task Example
### Write Documents With Primary Keys
-```bash
+```hocon
env {
parallelism = 1
job.mode = "BATCH"
@@ -130,7 +144,7 @@ sink {
### Read From Typesense And Write To Another Collection
-```bash
+```hocon
env {
parallelism = 1
job.mode = "BATCH"
@@ -176,6 +190,47 @@ sink {
}
```
+### Streaming Upsert With Checkpoint Flush
+
+In streaming mode, the writer buffers up to `max_batch_size` rows or until the next checkpoint, then issues one bulk request. Pair `data_save_mode = DROP_DATA` with a stable `primary_keys` to make every checkpoint produce an idempotent upsert.
+
+```hocon
+env {
+ parallelism = 2
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema {
+ fields {
+ company_name = string
+ num = long
+ id = string
+ num_employees = int
+ flag = boolean
+ }
+ }
+ plugin_output = "typesense_stream"
+ }
+}
+
+sink {
+ Typesense {
+ plugin_input = "typesense_stream"
+ hosts = ["localhost:8108"]
+ collection = "typesense_stream_collection"
+ api_key = "xyz"
+ primary_keys = ["id"]
+ max_batch_size = 100
+ schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"
+ data_save_mode = "DROP_DATA"
+ }
+}
+```
+
## Changelog
diff --git a/docs/en/connectors/source/GoogleBigtable.md b/docs/en/connectors/source/GoogleBigtable.md
index 64e4d2d39c8b..f09218a8da75 100644
--- a/docs/en/connectors/source/GoogleBigtable.md
+++ b/docs/en/connectors/source/GoogleBigtable.md
@@ -4,20 +4,26 @@ import ChangeLog from '../changelog/connector-google-bigtable.md';
> Google Bigtable source connector
+## Support Those Engines
+
+> SeaTunnel Zeta
+
## Description
Reads data from Google Cloud Bigtable using the native Bigtable Data v2 Java client.
-## Key features
+## 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)
+- [ ] [column projection](../../introduction/concepts/connector-v2-features.md)
- [ ] [parallelism](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
:::tip
-The source is bounded. It creates one split for the configured table or row-key range, so increasing job parallelism does not split one Bigtable scan into multiple tablet-range reads.
+The source is bounded. It creates one split for the configured table or row-key range, so increasing job parallelism does not split one Bigtable scan into multiple tablet-range reads. Each scan reads every requested cell for the configured row range and emits one SeaTunnel row per Bigtable row.
:::
@@ -52,23 +58,27 @@ 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.
+Path to the Google Cloud service account JSON key file. If omitted, Application Default Credentials (ADC) are used. ADC works automatically on GCE/GKE nodes, in `gcloud` shell sessions, or when the `GOOGLE_APPLICATION_CREDENTIALS` environment variable points to a service account JSON file.
### rowkey_column [list]
-Optional list of field names that should receive the row key value. If this option is not set, the connector uses a schema field named `rowkey` as the row-key field. The row-key field can be declared as `BYTES` or `STRING`.
+Optional list of field names that should receive the row key value. If this option is not set, the connector uses a schema field named `rowkey` as the row-key field.
+
+Each listed field is decoded independently according to its own declared type in `schema.fields`: `BYTES` receives the raw row-key bytes; `STRING` receives a UTF-8 decoded view. Different row-key fields can therefore use different types in the same scan (for example one field exposing the raw key bytes for downstream binary processing, another exposing a UTF-8 view).
### start_rowkey [string]
Inclusive start row key for the scan. If not set, the scan starts from the beginning of the table.
+The connector passes the value to the Bigtable client as a UTF-8 string; only lexicographic comparison is supported. Use `BYTES` for binary row keys that do not encode as UTF-8.
+
### 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).
+Inclusive start timestamp filter (microseconds since epoch). Combined with `end_timestamp` and `max_versions`, this controls which cell versions Bigtable returns for each column qualifier.
### end_timestamp [long]
@@ -76,11 +86,11 @@ 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.
+Maximum number of cell versions to return per column qualifier. Default `1` returns only the latest version. Larger values expose historical cell versions; the source still emits one row per Bigtable row, so older versions of the same cell are flattened into the latest returned cell.
### scan_row_limit [int]
-Maximum number of rows to return. `-1` (default) means no limit.
+Maximum number of rows to return. `-1` (default) means no limit. Use this option together with `start_rowkey` / `end_rowkey` to do paginated full-table scans across multiple jobs.
### common options
@@ -102,11 +112,16 @@ The source reads the latest returned cell for each `family:qualifier` field. Use
:::
-## Example
+## Task Example
-### Read all rows — Application Default Credentials
+### Read all rows with Application Default Credentials
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
source {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -126,6 +141,11 @@ source {
### Scan a row-key range with a service account
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
source {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -149,6 +169,11 @@ source {
### Use a custom row-key field name
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
source {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -166,6 +191,38 @@ source {
}
```
+### Bounded streaming scan with cell-version filtering
+
+Use `STREAMING` job mode when you want the scan to run with checkpointing while still being a single bounded read. Combine `start_timestamp`, `end_timestamp`, and `max_versions` to restrict which cell versions Bigtable returns.
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 60000
+}
+
+source {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ start_timestamp = 1704067200000000
+ end_timestamp = 1735689600000000
+ max_versions = 3
+ scan_row_limit = 500000
+ schema {
+ fields {
+ rowkey = STRING
+ "cf:type" = STRING
+ "cf:data" = STRING
+ "cf:ts" = BIGINT
+ }
+ }
+ }
+}
+```
+
## Changelog
diff --git a/docs/en/connectors/source/Typesense.md b/docs/en/connectors/source/Typesense.md
index 715838f5c5a3..e1b24a8d51d2 100644
--- a/docs/en/connectors/source/Typesense.md
+++ b/docs/en/connectors/source/Typesense.md
@@ -4,16 +4,23 @@ import ChangeLog from '../changelog/connector-typesense.md';
> Typesense Source Connector
+## Support Those Engines
+
+> SeaTunnel Zeta
+
## Description
Reads documents from a Typesense collection. The source supports bounded batch reads and can pass
Typesense search parameters through `query`.
+The source is bounded. Each job reads every document that matches the configured `query` once and then completes. Use Typesense's own change tracking on the receiver side if you need change-data capture.
+
## Key Features
- [x] [Batch Processing](../../introduction/concepts/connector-v2-features.md)
- [ ] [Stream Processing](../../introduction/concepts/connector-v2-features.md)
- [ ] [Exactly-Once](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
- [x] [Schema](../../introduction/concepts/connector-v2-features.md)
- [x] [Parallelism](../../introduction/concepts/connector-v2-features.md)
- [ ] [User-Defined Splits Support](../../introduction/concepts/connector-v2-features.md)
@@ -34,7 +41,7 @@ Typesense search parameters through `query`.
### hosts [array]
The access address of Typesense. Use the `host:port` format, for example:
-`["typesense-01:8108"]`. Multiple hosts are supported.
+`["typesense-01:8108"]`. Multiple hosts are supported. When several nodes are configured, the source issues its search requests to the first reachable node; the list is not used for parallel scan sharding.
### collection [string]
@@ -46,7 +53,7 @@ The columns to be read from Typesense. For more information, please refer to the
### api_key [string]
-The `api_key` for Typesense security authentication.
+The `api_key` for Typesense security authentication. Treat this value as a secret and prefer passing it via a job secret or environment variable when running on shared infrastructure.
### protocol [string]
@@ -58,9 +65,11 @@ Typesense Cloud or other TLS-enabled endpoints.
Typesense search parameters, for example `q=*&filter_by=num_employees:>9000`. If it is not set,
the source reads all documents returned by the default search.
+Any valid Typesense search parameter can be appended, including `q`, `query_by`, `filter_by`, `sort_by`, `page`, and `per_page`. The connector forwards them to the Typesense search API unchanged.
+
### batch_size [int]
-The number of records to query per batch when reading data.
+The number of records to query per batch when reading data. Each request uses the Typesense `per_page` parameter, so the value must be between 1 and the Typesense server-side `per_page` limit (typically 250). Lower the value if you see truncated pages in the logs.
### Common Options
@@ -70,7 +79,7 @@ For common parameters of Source plugins, please refer to [Source Common Options]
### Read Documents With A Filter
-```bash
+```hocon
env {
parallelism = 1
job.mode = "BATCH"
@@ -105,6 +114,39 @@ sink {
}
```
+### Read A Subset With A Custom Query
+
+Combine `query_by` and `sort_by` to control which fields Typesense searches and how the result set is ordered.
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
+source {
+ Typesense {
+ hosts = ["localhost:8108"]
+ collection = "companies"
+ api_key = "xyz"
+ query = "q=acme&query_by=company_name&filter_by=country:=US&sort_by=num_employees:desc"
+ batch_size = 50
+ schema = {
+ fields {
+ company_name = string
+ num_employees = long
+ country = string
+ id = string
+ }
+ }
+ }
+}
+
+sink {
+ Console {}
+}
+```
+
## Changelog
diff --git a/docs/zh/connectors/sink/Assert.md b/docs/zh/connectors/sink/Assert.md
index 4565d908d960..ef5c5efea2f0 100644
--- a/docs/zh/connectors/sink/Assert.md
+++ b/docs/zh/connectors/sink/Assert.md
@@ -17,8 +17,11 @@ Assert 是一个用于校验任务输出结果的数据接收器。它可以按
## 主要特性
- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
-- [ ] [定时刷新](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
+- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
+- [x] [流处理](../../introduction/concepts/connector-v2-features.md)
- [x] [支持多表写入](../../introduction/concepts/connector-v2-features.md)
+- [ ] [定时刷新](../../introduction/concepts/connector-v2-features.md)
## 配置
@@ -136,6 +139,16 @@ Sink 插件的通用参数,请参考 [Sink Common Options](../common-options/s
- `tables_configs` 用于多表任务,`table_path` 必须和上游数据携带的表路径一致。
- `equals_to` 会比较实际字段值和配置的期望值。数组、Map、Row 这类复杂值需要使用和 source 数据一致的 HOCON 写法。
+:::tip
+
+Assert 是一个终端 sink —— 没有外部存储系统可以写入。它适合在不需要下游数据库的情况下校验中间结果。连接器不会按 `UPDATE` 或 `DELETE` 行类型执行 CDC 语义,每条收到的记录都会按配置的规则进行断言。行数、字段值或 catalog 元数据校验失败时,任务会以对应的错误信息直接失败。
+
+:::
+
+## 流式校验
+
+Assert 同时支持 `BATCH` 与 `STREAMING` 两种作业模式。字段规则(如 `NOT_NULL`、`MIN_LENGTH`、`MAX_LENGTH` 等)会在每一条记录到达 Sink Writer 时进行检查;行数规则(`MIN_ROW` / `MAX_ROW`)只在 Sink Writer 关闭时(作业关闭、savepoint 或失败时)执行一次,对比的是该 Writer 实例自创建以来累计接收的总行数,既不会按 checkpoint 窗口重复校验,也不会在 checkpoint 之间重置。如果需要真正按 checkpoint 窗口的行数校验,这超出了文档更新范围,需要改动源代码。
+
## 示例
### 简单
@@ -632,6 +645,67 @@ sink {
```
+### 流式校验并按 Checkpoint 窗口断言行数
+
+下面的示例演示一个流式作业:作业结束时累计行数满足 `MIN_ROW` / `MAX_ROW` 区间(`50 ≤ 总行数 ≤ 5000`)。该校验只在 Writer 关闭时执行一次,对比累计行数,并不是按 checkpoint 窗口重复执行。
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 60000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema = {
+ fields {
+ name = string
+ age = int
+ }
+ }
+ plugin_output = "stream_data"
+ }
+}
+
+sink {
+ Assert {
+ plugin_input = "stream_data"
+ rules =
+ {
+ row_rules = [
+ {
+ rule_type = MIN_ROW
+ rule_value = 50
+ },
+ {
+ rule_type = MAX_ROW
+ rule_value = 5000
+ }
+ ],
+ field_rules = [{
+ field_name = age
+ field_type = int
+ field_value = [
+ {
+ rule_type = NOT_NULL
+ },
+ {
+ rule_type = MIN
+ rule_value = 0
+ },
+ {
+ rule_type = MAX
+ rule_value = 150
+ }
+ ]
+ }]
+ }
+ }
+}
+```
+
## 变更日志
diff --git a/docs/zh/connectors/sink/GoogleBigtable.md b/docs/zh/connectors/sink/GoogleBigtable.md
index 4d05bc81bedf..ba3a7e020d7c 100644
--- a/docs/zh/connectors/sink/GoogleBigtable.md
+++ b/docs/zh/connectors/sink/GoogleBigtable.md
@@ -4,15 +4,21 @@ import ChangeLog from '../changelog/connector-google-bigtable.md';
> Google Bigtable Sink 连接器
+## 支持这些引擎
+
+> SeaTunnel Zeta
+
## 描述
使用原生 Bigtable Data v2 Java 客户端将数据写入 Google Cloud Bigtable。
## 主要特性
-- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
- [x] [支持多表写入](../../introduction/concepts/connector-v2-features.md)
+- [ ] [定时刷新](../../introduction/concepts/connector-v2-features.md)
## 参数
@@ -30,7 +36,7 @@ import ChangeLog from '../changelog/connector-google-bigtable.md';
| batch_mutation_size| int | 否 | 100 |
| schema_save_mode | enum | 否 | RECREATE_SCHEMA |
| data_save_mode | enum | 否 | APPEND_DATA |
-| multi_table_sink_replica | int | 否 | - |
+| multi_table_sink_replica | int | 否 | 1 |
| common-options | | 否 | - |
### project_id [string]
@@ -43,11 +49,11 @@ Bigtable 实例 ID,例如 `"my-bigtable-instance"`。
### table [string]
-写入的 Bigtable 表名,例如 `"my-table"`。
+写入的 Bigtable 表名,例如 `"my-table"`。连接器不会自动建表,需要先在 Bigtable 中创建好目标表以及会用到的列族。
### rowkey_column [list]
-用于构造行键的列名列表,例如 `["id"]` 或 `["tenant_id", "event_id"]`。多列时用 `rowkey_delimiter` 拼接。
+用于构造行键的列名列表,例如 `["id"]` 或 `["tenant_id", "event_id"]`。多列时用 `rowkey_delimiter` 拼接。当只有一个行键列时,值为 `null` 或空字符串会让作业直接以 `WRITE_FAILED` 失败;当配置了多个行键列时,非末尾列为 `null` 会被静默转成空串并通过 `rowkey_delimiter` 拼接到组合行键中,只有当整条组合行键最终为空时作业才会失败。
### column_family [config]
@@ -68,6 +74,8 @@ column_family {
}
```
+未在映射中出现的字段名会回退到 `all_columns` 指定的列族;如果也没有 `all_columns`,则使用默认列族 `cf`。
+
### credentials_path [string]
Google Cloud 服务账号 JSON 密钥文件路径。未设置时使用应用默认凭证(ADC)。
@@ -102,13 +110,13 @@ Schema 保存模式。当前只支持 `RECREATE_SCHEMA`。
### multi_table_sink_replica [int]
-多表写入时使用的 Sink 副本数。更多说明请参考 [Sink Common Options](../common-options/sink-common-options.md)。
+多表写入时使用的 Sink 副本数。`multi_table_sink_replica` 用于在单个 Sink 实例中增加并行写入副本数;目标 Bigtable 表由 `table` 选项固定,不会根据上游表名动态切换。更多说明请参考 [Sink Common Options](../common-options/sink-common-options.md)。
### common options
Sink 插件通用参数,详见 [Sink Common Options](../common-options/sink-common-options.md)。
-## 数据类型
+## 数据类型映射
Bigtable 没有关系型数据库那样的列类型。连接器会按如下格式把 SeaTunnel 字段写入 Bigtable Cell:
@@ -130,15 +138,20 @@ Bigtable 没有关系型数据库那样的列类型。连接器会按如下格
:::tip
-Sink 会把非行键字段写成 Bigtable Cell。目标列族由 `column_family` 决定,Bigtable 的列限定符使用 SeaTunnel 字段名。
+Sink 会把非行键字段写成 Bigtable Cell。目标列族由 `column_family` 决定,Bigtable 的列限定符使用 SeaTunnel 字段名。每条上游记录都会被当成无条件的 Cell 变更,所以 `UPDATE` / `DELETE` 类型的行不会被解释为 CDC 操作,而是直接覆盖相同 `(行键, 列族, 列限定符)` 下的旧 Cell。
:::
-## 示例
+## 任务示例
### 使用应用默认凭证写入
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -155,6 +168,11 @@ sink {
### 使用服务账号和复合行键写入
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -174,6 +192,11 @@ sink {
### 写入多个列族
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -193,6 +216,11 @@ sink {
### 使用版本列并把空值写成空字节
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
sink {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -210,6 +238,52 @@ sink {
}
```
+### 流式写入并按 Checkpoint 刷新
+
+在流式模式下,Writer 会在每次 checkpoint 触发时把本地 mutation 缓冲写入 Bigtable。`batch_mutation_size` 仍然控制任务内部缓冲,checkpoint 频率只会影响已缓冲的 mutation 多久被发送到 Bigtable。
+
+```hocon
+env {
+ parallelism = 2
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema {
+ fields {
+ tenant_id = string
+ event_id = string
+ event_ts = bigint
+ event_type = string
+ payload = string
+ }
+ }
+ plugin_output = "events_stream"
+ }
+}
+
+sink {
+ GoogleBigtable {
+ plugin_input = "events_stream"
+ 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 = "#"
+ version_column = "event_ts"
+ column_family {
+ all_columns = "data"
+ event_type = "meta"
+ }
+ batch_mutation_size = 200
+ }
+}
+```
+
## Changelog
diff --git a/docs/zh/connectors/sink/GoogleFirestore.md b/docs/zh/connectors/sink/GoogleFirestore.md
index 40133e5fb50d..2d948adddd18 100644
--- a/docs/zh/connectors/sink/GoogleFirestore.md
+++ b/docs/zh/connectors/sink/GoogleFirestore.md
@@ -24,8 +24,9 @@ GoogleFirestore Sink 用于将 SeaTunnel 数据写入 Google Cloud Firestore 集
- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
- [ ] [CDC](../../introduction/concepts/connector-v2-features.md)
- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
-- [ ] [流处理](../../introduction/concepts/connector-v2-features.md)
+- [x] [流处理](../../introduction/concepts/connector-v2-features.md)
- [ ] [支持多表写入](../../introduction/concepts/connector-v2-features.md)
+- [ ] [定时刷新](../../introduction/concepts/connector-v2-features.md)
## 支持的数据源信息
@@ -97,13 +98,16 @@ Sink 插件通用参数,请参考 [Sink 通用选项](../common-options/sink-c
## 注意事项
- 当前连接器只提供 sink,不提供 GoogleFirestore source。
-- 每个 sink 配置块只写入一个固定的 collection,不会按多表输入自动切换 collection。
+- 每个 sink 配置块只写入一个固定的 collection,不会按多表输入自动切换 collection;多 collection 场景需要为每个目标 collection 单独配置一个 sink 块。
- Firestore 文档 ID 会自动生成。如果需要固定文档 ID,请在写入前使用其他连接器或转换处理。
-- sink 不会按 `UPDATE` 或 `DELETE` 行类型执行 CDC 语义。
+- sink 不会按 `UPDATE` 或 `DELETE` 行类型执行 CDC 语义 —— 每条记录都会触发一次 Firestore `add` 调用生成新文档。
- `credentials` 不能直接填写服务账号 JSON 原文,需要先做 Base64 编码。
- 上游 SeaTunnel schema 中的字段名会作为 Firestore 文档字段名。
+- 连接器同时支持 `BATCH` 与 `STREAMING` 两种作业模式。在当前实现中,`FirestoreSinkWriter.write()` 对每一条记录直接调用一次 Firestore 客户端的 `add(...)`,并不会缓冲或批量写入,因此并没有“checkpoint 前把内存写缓冲刷到 Firestore”这一行为;checkpoint 完成并不意味着之前调用过的所有写入都已真正到达 Firestore。
-## 示例
+## 任务示例
+
+### 批量写入带类型的记录
```hocon
env {
@@ -150,6 +154,39 @@ sink {
}
```
+### 流式写入并启用 Checkpoint 间隔
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 100
+ schema = {
+ fields {
+ c_string = string
+ c_int = int
+ c_timestamp = timestamp
+ }
+ }
+ plugin_output = "firestore_stream"
+ }
+}
+
+sink {
+ GoogleFirestore {
+ plugin_input = "firestore_stream"
+ project_id = "my-gcp-project"
+ collection = "events"
+ credentials = "base64-service-account-json"
+ }
+}
+```
+
## 变更日志
diff --git a/docs/zh/connectors/sink/Lance.md b/docs/zh/connectors/sink/Lance.md
index 15350b09055e..59c478fbb8d0 100644
--- a/docs/zh/connectors/sink/Lance.md
+++ b/docs/zh/connectors/sink/Lance.md
@@ -13,7 +13,11 @@ import ChangeLog from '../changelog/connector-lance.md';
## 主要特性
- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
+- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
+- [x] [流处理](../../introduction/concepts/connector-v2-features.md)
- [x] [支持多表写入](../../introduction/concepts/connector-v2-features.md)
+- [ ] [定时刷新](../../introduction/concepts/connector-v2-features.md)
## 描述
@@ -82,6 +86,10 @@ Lance namespace 的根目录。SeaTunnel 运行用户需要有权限在该目录
控制 Lance 的写入方式。默认值是 `CREATE`。该值需要是 Lance `WriteParams.WriteMode` 支持的值:`CREATE`、`APPEND` 或 `OVERWRITE`。
+### lance.write.enable.stable.row.ids
+
+写入 Lance 时是否启用稳定的 row ID。连接器会把这个选项读入 `LanceSinkConfig.enableStableRowIds`,并通过 `getEnableStableRowIds()` 暴露,但**当前实现中该值仅被解析,还未真正传入底层的 Lance `WriteParams`**(`LanceSinkWriter.initializeDataset()` 构造的 `WriteParams` 不包含这个开关),目前切换它对写入路径没有可见效果。这是一项已知的缺口,需要后续连接器提交来补齐。
+
### lance.write.storage.options
以键值对形式传递额外的 Lance 存储参数。
@@ -95,9 +103,13 @@ lance.write.storage.options = {
}
```
+### multi_table_sink_replica
+
+多表写入时的 sink 并行副本数。当一个多表作业写入大量 Lance 表、单个副本成为瓶颈时调大该值。详见 [Sink 通用选项](../common-options/sink-common-options.md)。
+
## 数据类型映射
-Lance 使用 Apache Arrow 类型系统。sink 会根据上游 SeaTunnel 表结构创建 Lance schema。
+Lance 使用 Apache Arrow 类型系统。sink 会根据上游 SeaTunnel 表结构创建 Lance schema。当前映射会把所有整数类型(`TINYINT`、`SMALLINT`、`INT`、`BIGINT`)一律收窄为 Arrow `int32`,因此超出有符号 32 位范围的 `BIGINT` 值会被截断。
| SeaTunnel 数据类型 | Lance / Arrow 数据类型 |
|--------------------|------------------------|
@@ -105,19 +117,25 @@ Lance 使用 Apache Arrow 类型系统。sink 会根据上游 SeaTunnel 表结
| TINYINT | int32 |
| SMALLINT | int32 |
| INT | int32 |
-| BIGINT | int32 |
+| BIGINT | int32(超出有符号 32 位范围的值会被截断) |
| FLOAT | float32 |
| DOUBLE | float64 |
| DECIMAL | decimal128 |
| NULL | null |
| BYTES | binary |
| DATE | date32 |
-| TIME | time32 |
-| TIMESTAMP | timestamp |
+| TIME | time32(毫秒精度) |
+| TIMESTAMP | timestamp(微秒精度,Asia/Shanghai 时区) |
| STRING | utf8 |
| ARRAY | list |
| MAP | map |
+:::tip
+
+Sink 不会按 `UPDATE` / `DELETE` 行类型执行 CDC 语义 —— 每条上游记录都会按 `lance.write.mode` 追加到 Lance 数据集中。在流式模式下,Writer 会在每个 checkpoint 把内存中的行缓冲写入 Lance。
+
+:::
+
## 任务示例
### 写入 FakeSource 数据到 Lance
@@ -161,6 +179,77 @@ sink {
}
```
+### 使用 APPEND 模式并调大文件分片
+
+`APPEND` 模式会保留已有数据集并写入新行。把 `lance.write.max-rows-per-file` 和 `lance.write.max-bytes-per-file` 调大,可以减少追加大批量数据时产生的 Lance fragment 数量。
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
+source {
+ FakeSource {
+ row.num = 1000000
+ schema = {
+ fields {
+ c_string = string
+ c_int = int
+ }
+ }
+ plugin_output = "fake"
+ }
+}
+
+sink {
+ Lance {
+ dataset_path = "/tmp/seatunnel_mnt/lanceTest/lance_sink_table"
+ namespace_type = "dir"
+ namespace_id = "root"
+ table = "lance_sink_table"
+ lance.write.mode = "APPEND"
+ lance.write.max-rows-per-file = 100000
+ lance.write.max-rows-per-group = 5000
+ lance.write.max-bytes-per-file = 134217728
+ }
+}
+```
+
+### 流式追加并按 Checkpoint 刷新
+
+```hocon
+env {
+ parallelism = 2
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema = {
+ fields {
+ c_string = string
+ c_int = int
+ }
+ }
+ plugin_output = "fake_stream"
+ }
+}
+
+sink {
+ Lance {
+ plugin_input = "fake_stream"
+ dataset_path = "/tmp/seatunnel_mnt/lanceTest/lance_sink_table"
+ namespace_type = "dir"
+ namespace_id = "root"
+ table = "lance_sink_table"
+ lance.write.mode = "APPEND"
+ }
+}
+```
+
## 更新日志
diff --git a/docs/zh/connectors/sink/Typesense.md b/docs/zh/connectors/sink/Typesense.md
index faca40ae01c8..c9217c8dd3bb 100644
--- a/docs/zh/connectors/sink/Typesense.md
+++ b/docs/zh/connectors/sink/Typesense.md
@@ -2,6 +2,12 @@ import ChangeLog from '../changelog/connector-typesense.md';
# Typesense
+> Typesense sink 连接器
+
+## 支持的引擎
+
+> SeaTunnel Zeta
+
## 描述
将 SeaTunnel 数据写入 Typesense collection。该 connector 可以按配置创建目标 collection、
@@ -12,6 +18,8 @@ import ChangeLog from '../changelog/connector-typesense.md';
- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
- [x] [CDC](../../introduction/concepts/connector-v2-features.md)
- [x] [支持多表写入](../../introduction/concepts/connector-v2-features.md)
+- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
+- [x] [流处理](../../introduction/concepts/connector-v2-features.md)
- [ ] [定时刷新](../../introduction/concepts/connector-v2-features.md)
## 选项
@@ -27,20 +35,20 @@ import ChangeLog from '../changelog/connector-typesense.md';
| api_key | string | 是 | - | Typesense API Key。 |
| max_retry_count | int | 否 | 3 | 单个批量请求的最大重试次数。 |
| max_batch_size | int | 否 | 10 | 单个批量请求最多写入的文档数量。 |
-| multi_table_sink_replica | int | 否 | - | 通用多表写入路由机制使用的 Sink 副本数。 |
+| multi_table_sink_replica | int | 否 | 1 | 通用多表写入路由机制使用的 Sink 副本数。 |
| common-options | | 否 | - | 通用 Sink 选项。 |
### hosts [array]
-Typesense 的访问地址,格式为 `host:port`,例如:`["typesense-01:8108"]`。
+Typesense 的访问地址,格式为 `host:port`,例如:`["typesense-01:8108"]`。配置多个节点时,每个 Writer 只持有一个客户端,不会把写入请求在节点之间负载均衡。
### collection [string]
-要写入的 collection 名,例如:`seatunnel`。
+要写入的 collection 名,例如:`seatunnel`。在多表作业中,所有表都会路由到同一个 collection;如果不同表要写入不同目标,请为每个目标 collection 单独配置一个 sink 块。
### primary_keys [array]
-主键字段用于生成文档 `id`。配置多个字段时,connector 会用 `key_delimiter` 拼接这些字段值。
+主键字段用于生成文档 `id`。配置多个字段时,connector 会用 `key_delimiter` 拼接这些字段值。未配置 `primary_keys` 时,Typesense 会自行分配文档 ID,连接器退化为纯追加写入。
### key_delimiter [string]
@@ -48,15 +56,15 @@ Typesense 的访问地址,格式为 `host:port`,例如:`["typesense-01:810
### api_key [string]
-Typesense 安全认证的 `api_key`。
+Typesense 安全认证的 `api_key`。请把它当作敏感凭据处理;在共享环境运行时,建议通过作业密钥或环境变量注入。
### max_retry_count [int]
-单个批量请求的最大重试次数。
+单个批量请求的最大重试次数。重试谓词为 `exception -> true`,也就是说 `typesenseClient.insert(...)` 抛出的任何异常(网络错误、超时以及 Typesense 业务错误响应)都会被同样重试,最多执行 `max_retry_count` 次,每次间隔固定的 200 ms;当前实现并不会区分瞬时错误和永久错误。
### max_batch_size [int]
-每批最多写入的文档数量。
+每批最多写入的文档数量。Typesense 对单次请求有上限,请将该值保持在 Typesense 服务端 `per_page` 上限以下。
### multi_table_sink_replica [int]
@@ -85,11 +93,17 @@ Typesense collection 创建时会使用上游 SeaTunnel 表结构。如果希望
`APPEND_DATA`:保留数据库结构,保留数据
`ERROR_WHEN_DATA_EXISTS`:当有数据时抛出错误
+:::tip
+
+连接器使用 Typesense 的批量导入接口。`UPDATE` 和 `DELETE` 行类型不会被解释为 CDC 操作 —— 每条上游记录都会按生成的文档 `id` 被 upsert 到目标 collection。如果希望重复作业行为类似 upsert 而不是追加,可以把 `data_save_mode` 设为 `DROP_DATA`,并配置稳定的 `primary_keys`。
+
+:::
+
## 任务示例
### 使用主键写入文档
-```bash
+```hocon
env {
parallelism = 1
job.mode = "BATCH"
@@ -129,7 +143,7 @@ sink {
### 从 Typesense 读取并写入另一个 collection
-```bash
+```hocon
env {
parallelism = 1
job.mode = "BATCH"
@@ -175,6 +189,47 @@ sink {
}
```
+### 流式 Upsert 并按 Checkpoint 刷新
+
+在流式模式下,Writer 最多缓冲 `max_batch_size` 条记录,或者直到下一个 checkpoint,再发出一次批量请求。把 `data_save_mode = DROP_DATA` 与稳定的 `primary_keys` 组合起来,每个 checkpoint 都会产生幂等的 upsert。
+
+```hocon
+env {
+ parallelism = 2
+ job.mode = "STREAMING"
+ checkpoint.interval = 30000
+}
+
+source {
+ FakeSource {
+ row.num = 1000
+ schema {
+ fields {
+ company_name = string
+ num = long
+ id = string
+ num_employees = int
+ flag = boolean
+ }
+ }
+ plugin_output = "typesense_stream"
+ }
+}
+
+sink {
+ Typesense {
+ plugin_input = "typesense_stream"
+ hosts = ["localhost:8108"]
+ collection = "typesense_stream_collection"
+ api_key = "xyz"
+ primary_keys = ["id"]
+ max_batch_size = 100
+ schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"
+ data_save_mode = "DROP_DATA"
+ }
+}
+```
+
## 变更日志
diff --git a/docs/zh/connectors/source/GoogleBigtable.md b/docs/zh/connectors/source/GoogleBigtable.md
index b690d4496b2f..f0eebfe9cd83 100644
--- a/docs/zh/connectors/source/GoogleBigtable.md
+++ b/docs/zh/connectors/source/GoogleBigtable.md
@@ -4,6 +4,10 @@ import ChangeLog from '../changelog/connector-google-bigtable.md';
> Google Bigtable Source 连接器
+## 支持这些引擎
+
+> SeaTunnel Zeta
+
## 描述
使用原生 Bigtable Data v2 Java 客户端从 Google Cloud Bigtable 读取数据。
@@ -12,12 +16,14 @@ import ChangeLog from '../changelog/connector-google-bigtable.md';
- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
- [ ] [流处理](../../introduction/concepts/connector-v2-features.md)
-- [ ] [exactly-once](../../introduction/concepts/connector-v2-features.md)
+- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
+- [ ] [列投影](../../introduction/concepts/connector-v2-features.md)
- [ ] [并行度](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
:::tip
-该 Source 是有界读取。当前只会为配置的表或行键范围生成一个切分,所以提高作业并行度不会把一次 Bigtable 扫描拆成多个 tablet 范围并发读取。
+该 Source 是有界读取。当前只会为配置的表或行键范围生成一个切分,所以提高作业并行度不会把一次 Bigtable 扫描拆成多个 tablet 范围并发读取。每次扫描会读取所请求行范围内的全部 Cell,并为每个 Bigtable 行输出一条 SeaTunnel 记录。
:::
@@ -52,23 +58,27 @@ Bigtable 实例 ID。
### credentials_path [string]
-Google Cloud 服务账号 JSON 密钥文件路径。未设置时使用应用默认凭证(ADC)。
+Google Cloud 服务账号 JSON 密钥文件路径。未设置时使用应用默认凭证(ADC)。在 GCE/GKE 节点上、`gcloud` shell 会话中、或当 `GOOGLE_APPLICATION_CREDENTIALS` 环境变量指向服务账号 JSON 文件时,ADC 会自动生效。
### rowkey_column [list]
-用于接收 Bigtable 行键的字段名列表。未设置时,连接器默认把名为 `rowkey` 的字段当作行键字段。行键字段可以声明为 `BYTES` 或 `STRING`。
+用于接收 Bigtable 行键的字段名列表。未设置时,连接器默认把名为 `rowkey` 的字段当作行键字段。
+
+列出的每个字段会按照其在 `schema.fields` 中声明的类型独立解码:`BYTES` 接收原始行键字节;`STRING` 接收 UTF-8 解码后的视图。因此同一次扫描里的不同行键字段可以使用不同类型(例如一个字段把行键作为原始字节暴露给下游二进制处理,另一个字段同时暴露一个 UTF-8 可读视图)。
### start_rowkey [string]
扫描起始行键,包含该行键。未设置时从表起始位置读取。
+连接器会把该值原样以 UTF-8 字符串传给 Bigtable 客户端,只支持字典序比较。对于无法按 UTF-8 编码的二进制行键,请使用 `BYTES` 类型。
+
### end_rowkey [string]
扫描结束行键,不包含该行键。未设置时读取到表末尾。
### start_timestamp [long]
-Cell 时间戳过滤的起始值,包含该时间戳,单位是微秒。
+Cell 时间戳过滤的起始值,包含该时间戳,单位是微秒。与 `end_timestamp`、`max_versions` 配合,可以控制 Bigtable 对每个列限定符返回哪些版本的 Cell。
### end_timestamp [long]
@@ -76,11 +86,11 @@ Cell 时间戳过滤的结束值,不包含该时间戳,单位是微秒。
### max_versions [int]
-每个列限定符最多返回的 Cell 版本数。默认值 `1` 表示只读取最新版本。
+每个列限定符最多返回的 Cell 版本数。默认值 `1` 表示只读取最新版本。更大的值会暴露历史版本,但 Source 仍按 Bigtable 行聚合输出,同一 Cell 的旧版本会被合并到该行返回的最新版本。
### scan_row_limit [int]
-最多读取的行数。默认值 `-1` 表示不限制。
+最多读取的行数。默认值 `-1` 表示不限制。把 `scan_row_limit` 与 `start_rowkey` / `end_rowkey` 配合,可以在多个作业之间分页扫描整张表。
### common options
@@ -102,11 +112,16 @@ Source 会读取每个 `列族:列限定符` 字段返回的最新 Cell。可以
:::
-## 示例
+## 任务示例
### 使用应用默认凭证读取整张表
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
source {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -126,6 +141,11 @@ source {
### 使用服务账号扫描行键范围
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
source {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -149,6 +169,11 @@ source {
### 使用自定义行键字段名
```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
source {
GoogleBigtable {
project_id = "my-gcp-project"
@@ -166,6 +191,38 @@ source {
}
```
+### 有界流式扫描并按 Cell 版本过滤
+
+在 `STREAMING` 模式下,仍然只做单次有界扫描,但会按 checkpoint 推进。结合 `start_timestamp`、`end_timestamp` 和 `max_versions` 可以限制 Bigtable 返回的 Cell 版本。
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 60000
+}
+
+source {
+ GoogleBigtable {
+ project_id = "my-gcp-project"
+ instance_id = "my-bigtable-instance"
+ table = "events"
+ start_timestamp = 1704067200000000
+ end_timestamp = 1735689600000000
+ max_versions = 3
+ scan_row_limit = 500000
+ schema {
+ fields {
+ rowkey = STRING
+ "cf:type" = STRING
+ "cf:data" = STRING
+ "cf:ts" = BIGINT
+ }
+ }
+ }
+}
+```
+
## Changelog
diff --git a/docs/zh/connectors/source/Typesense.md b/docs/zh/connectors/source/Typesense.md
index 40937446ceab..dd412d3a46e5 100644
--- a/docs/zh/connectors/source/Typesense.md
+++ b/docs/zh/connectors/source/Typesense.md
@@ -4,16 +4,23 @@ import ChangeLog from '../changelog/connector-typesense.md';
> Typesense 源连接器
+## 支持的引擎
+
+> SeaTunnel Zeta
+
## 描述
从 Typesense collection 读取文档。该 source 支持有界批读取,也可以通过 `query` 传入
Typesense 查询参数。
+Source 是有界读取:每次作业只会读取一次匹配 `query` 的全部文档,然后结束。如果需要变更捕获,请在接收端借助 Typesense 自身的变化追踪机制实现。
+
## 主要特性
- [x] [批处理](../../introduction/concepts/connector-v2-features.md)
- [ ] [流处理](../../introduction/concepts/connector-v2-features.md)
- [ ] [精确一次](../../introduction/concepts/connector-v2-features.md)
+- [ ] [cdc](../../introduction/concepts/connector-v2-features.md)
- [x] [Schema](../../introduction/concepts/connector-v2-features.md)
- [x] [并行度](../../introduction/concepts/connector-v2-features.md)
- [ ] [支持用户定义的拆分](../../introduction/concepts/connector-v2-features.md)
@@ -33,7 +40,7 @@ Typesense 查询参数。
### hosts [array]
-Typesense 的访问地址,格式为 `host:port`,例如:`["typesense-01:8108"]`。支持配置多个地址。
+Typesense 的访问地址,格式为 `host:port`,例如:`["typesense-01:8108"]`。支持配置多个地址。配置多个节点时,Source 会把搜索请求发往第一个可达节点,列表本身不用于并行扫描分片。
### collection [string]
@@ -45,7 +52,7 @@ typesense 需要读取的列。有关更多信息,请参阅:[guide](../../in
### api_key [string]
-Typesense 安全认证的 `api_key`。
+Typesense 安全认证的 `api_key`。请把它当作敏感凭据处理;在共享环境运行时,建议通过作业密钥或环境变量注入。
### protocol [string]
@@ -56,9 +63,11 @@ Typesense 安全认证的 `api_key`。
Typesense 查询参数,例如 `q=*&filter_by=num_employees:>9000`。不配置时读取默认查询返回的文档。
+所有合法的 Typesense 搜索参数都可以追加进来,包括 `q`、`query_by`、`filter_by`、`sort_by`、`page` 和 `per_page`,连接器会原样转发给 Typesense 搜索接口。
+
### batch_size [int]
-读取数据时每批查询的文档数量。
+读取数据时每批查询的文档数量。每次请求使用 Typesense 的 `per_page` 参数,因此该值必须在 1 与 Typesense 服务端 `per_page` 上限(通常是 250)之间。如果日志中看到分页被截断,请调小该值。
### 常用选项
@@ -68,7 +77,7 @@ Source 插件常用参数,具体请参考 [Source 常用选项](../common-opti
### 带过滤条件读取文档
-```bash
+```hocon
env {
parallelism = 1
job.mode = "BATCH"
@@ -103,6 +112,39 @@ sink {
}
```
+### 使用自定义查询条件读取子集
+
+组合 `query_by` 与 `sort_by` 可以控制 Typesense 搜索的字段以及结果集的排序方式。
+
+```hocon
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
+source {
+ Typesense {
+ hosts = ["localhost:8108"]
+ collection = "companies"
+ api_key = "xyz"
+ query = "q=acme&query_by=company_name&filter_by=country:=US&sort_by=num_employees:desc"
+ batch_size = 50
+ schema = {
+ fields {
+ company_name = string
+ num_employees = long
+ country = string
+ id = string
+ }
+ }
+ }
+}
+
+sink {
+ Console {}
+}
+```
+
## 变更日志
diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormat.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormat.java
index 90dbe021fc62..b837827e5d10 100644
--- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormat.java
+++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormat.java
@@ -38,6 +38,7 @@
import java.io.IOException;
import java.io.Serializable;
+import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -57,6 +58,7 @@ public class JdbcInputFormat implements Serializable {
private final JdbcRowConverter jdbcRowConverter;
private final Map tables;
private final ChunkSplitter chunkSplitter;
+ private final boolean configuredAutoCommit;
private transient String splitTableId;
private transient TableSchema splitTableSchema;
@@ -65,23 +67,37 @@ public class JdbcInputFormat implements Serializable {
private volatile boolean hasNext;
public JdbcInputFormat(JdbcSourceConfig config, Map tables) {
- this.jdbcDialect =
+ this(
JdbcDialectLoader.load(
config.getJdbcConnectionConfig().getUrl(),
config.getJdbcConnectionConfig().getDialect(),
- config.getCompatibleMode());
- this.chunkSplitter = ChunkSplitter.create(config);
+ config.getCompatibleMode()),
+ ChunkSplitter.create(config),
+ tables,
+ config.getJdbcConnectionConfig().isAutoCommit());
+ }
+
+ JdbcInputFormat(
+ JdbcDialect jdbcDialect,
+ ChunkSplitter chunkSplitter,
+ Map tables,
+ boolean configuredAutoCommit) {
+ this.jdbcDialect = jdbcDialect;
+ this.chunkSplitter = chunkSplitter;
this.jdbcRowConverter = jdbcDialect.getRowConverter();
this.tables = tables;
+ this.configuredAutoCommit = configuredAutoCommit;
}
public void openInputFormat() {}
public void closeInputFormat() throws IOException {
- close();
-
- if (chunkSplitter != null) {
- chunkSplitter.close();
+ try {
+ close();
+ } finally {
+ if (chunkSplitter != null) {
+ chunkSplitter.close();
+ }
}
}
@@ -101,10 +117,31 @@ public void open(JdbcSourceSplit inputSplit) throws IOException {
resultSet = statement.executeQuery();
hasNext = resultSet.next();
} catch (SQLException se) {
+ cleanupAfterOpenFailure(se);
throw new JdbcConnectorException(
JdbcConnectorErrorCode.CONNECT_DATABASE_FAILED,
"open() failed." + se.getMessage(),
se);
+ } catch (RuntimeException runtimeException) {
+ cleanupAfterOpenFailure(runtimeException);
+ throw runtimeException;
+ }
+ }
+
+ private void cleanupAfterOpenFailure(Throwable openException) {
+ boolean shouldDiscardConnection = statement == null;
+ try {
+ close();
+ } catch (IOException cleanupException) {
+ openException.addSuppressed(cleanupException);
+ shouldDiscardConnection = true;
+ } finally {
+ if (shouldDiscardConnection) {
+ // Statement creation may establish or mutate the cached connection before failing
+ // without returning a statement. Discard it because close() cannot identify and
+ // finish that transaction safely.
+ chunkSplitter.close();
+ }
}
}
@@ -114,11 +151,14 @@ public void open(JdbcSourceSplit inputSplit) throws IOException {
* @throws IOException Indicates that a resource could not be closed.
*/
public void close() throws IOException {
+ Connection connection = getStatementConnection();
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
LOG.info("ResultSet couldn't be closed - " + e.getMessage());
+ } finally {
+ resultSet = null;
}
}
if (statement != null) {
@@ -126,7 +166,86 @@ public void close() throws IOException {
statement.close();
} catch (SQLException e) {
LOG.info("Statement couldn't be closed - " + e.getMessage());
+ } finally {
+ statement = null;
+ }
+ }
+
+ hasNext = false;
+ splitTableSchema = null;
+ splitTableId = null;
+ finishReadTransaction(connection);
+ }
+
+ private Connection getStatementConnection() {
+ if (statement == null) {
+ return null;
+ }
+ try {
+ Connection connection = statement.getConnection();
+ if (connection == null) {
+ LOG.warn(
+ "The JDBC source statement returned no connection. "
+ + "Closing the cached connection to avoid reusing an unknown "
+ + "transaction.");
+ chunkSplitter.close();
}
+ return connection;
+ } catch (SQLException e) {
+ LOG.warn(
+ "Failed to get the JDBC source connection from the current statement. "
+ + "Closing the cached connection to avoid reusing an unknown "
+ + "transaction.",
+ e);
+ chunkSplitter.close();
+ return null;
+ }
+ }
+
+ private void finishReadTransaction(Connection connection) {
+ try {
+ finishReadTransaction(connection, configuredAutoCommit);
+ } catch (SQLException e) {
+ LOG.warn(
+ "Failed to finish the JDBC source read transaction. "
+ + "Closing the connection to avoid leaving or reusing an idle "
+ + "transaction.",
+ e);
+ discardConnection(connection, e);
+ }
+ }
+
+ private void discardConnection(Connection connection, SQLException cleanupException) {
+ try {
+ if (connection != null) {
+ connection.close();
+ }
+ } catch (SQLException closeException) {
+ cleanupException.addSuppressed(closeException);
+ LOG.warn(
+ "Failed to close the JDBC source connection after transaction cleanup failed.",
+ cleanupException);
+ } finally {
+ // Clear the provider's cached reference and retry close for drivers whose first close
+ // attempt failed.
+ chunkSplitter.close();
+ }
+ }
+
+ static void finishReadTransaction(Connection connection, boolean configuredAutoCommit)
+ throws SQLException {
+ if (connection == null || connection.isClosed()) {
+ return;
+ }
+
+ boolean currentAutoCommit = connection.getAutoCommit();
+ if (!currentAutoCommit) {
+ // JDBC source reads do not have changes to commit. Rollback ends the server-side cursor
+ // transaction, releases its snapshot, and also recovers a transaction in failed state.
+ connection.rollback();
+ }
+ if (currentAutoCommit != configuredAutoCommit) {
+ connection.setAutoCommit(configuredAutoCommit);
}
}
diff --git a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormatTest.java b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormatTest.java
new file mode 100644
index 000000000000..13f5fb72f5e7
--- /dev/null
+++ b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/internal/JdbcInputFormatTest.java
@@ -0,0 +1,272 @@
+/*
+ * 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.jdbc.internal;
+
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.TablePath;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.connectors.seatunnel.jdbc.exception.JdbcConnectorException;
+import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.converter.JdbcRowConverter;
+import org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.JdbcDialect;
+import org.apache.seatunnel.connectors.seatunnel.jdbc.source.ChunkSplitter;
+import org.apache.seatunnel.connectors.seatunnel.jdbc.source.JdbcSourceSplit;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class JdbcInputFormatTest {
+
+ private static final TablePath TABLE_PATH = TablePath.of("test", "public", "source_table");
+ private static final TableSchema TABLE_SCHEMA = TableSchema.builder().build();
+ private static final JdbcSourceSplit SPLIT =
+ new JdbcSourceSplit(TABLE_PATH, "split-0", null, null, null, null, null);
+
+ @Test
+ void shouldRollbackAndRestoreConfiguredAutoCommitAfterClosingSplit() throws Exception {
+ TestContext context = createContext(true);
+ context.openEmptySplit();
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(false);
+
+ context.inputFormat.close();
+
+ InOrder inOrder = inOrder(context.statement, context.resultSet, context.connection);
+ inOrder.verify(context.statement).getConnection();
+ inOrder.verify(context.resultSet).close();
+ inOrder.verify(context.statement).close();
+ inOrder.verify(context.connection).rollback();
+ inOrder.verify(context.connection).setAutoCommit(true);
+
+ context.inputFormat.close();
+ verify(context.connection, times(1)).rollback();
+ }
+
+ @Test
+ void shouldRollbackAndKeepConfiguredManualCommit() throws Exception {
+ TestContext context = createContext(false);
+ context.openEmptySplit();
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(false);
+
+ context.inputFormat.close();
+
+ verify(context.connection).rollback();
+ verify(context.connection, never()).setAutoCommit(true);
+ }
+
+ @Test
+ void shouldNotRollbackAutoCommitConnection() throws Exception {
+ TestContext context = createContext(true);
+ context.openEmptySplit();
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(true);
+
+ context.inputFormat.close();
+
+ verify(context.connection, never()).rollback();
+ verify(context.connection, never()).setAutoCommit(true);
+ }
+
+ @Test
+ void shouldFinishTransactionWhenResourceCloseFails() throws Exception {
+ TestContext context = createContext(true);
+ context.openEmptySplit();
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(false);
+ doThrow(new SQLException("result set close failed")).when(context.resultSet).close();
+ doThrow(new SQLException("statement close failed")).when(context.statement).close();
+
+ context.inputFormat.close();
+
+ verify(context.connection).rollback();
+ verify(context.connection).setAutoCommit(true);
+ }
+
+ @Test
+ void shouldKeepConnectionReusableAcrossSuccessfulSplits() throws Exception {
+ TestContext context = createContext(true);
+ PreparedStatement secondStatement = mock(PreparedStatement.class);
+ ResultSet secondResultSet = mock(ResultSet.class);
+
+ when(context.chunkSplitter.generateSplitStatement(SPLIT, TABLE_SCHEMA))
+ .thenReturn(context.statement, secondStatement);
+ when(context.statement.executeQuery()).thenReturn(context.resultSet);
+ when(secondStatement.executeQuery()).thenReturn(secondResultSet);
+ when(context.resultSet.next()).thenReturn(false);
+ when(secondResultSet.next()).thenReturn(false);
+ when(context.statement.getConnection()).thenReturn(context.connection);
+ when(secondStatement.getConnection()).thenReturn(context.connection);
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(false);
+
+ context.inputFormat.open(SPLIT);
+ context.inputFormat.close();
+ context.inputFormat.open(SPLIT);
+ context.inputFormat.close();
+
+ verify(context.chunkSplitter, times(2)).generateSplitStatement(SPLIT, TABLE_SCHEMA);
+ verify(context.connection, times(2)).rollback();
+ verify(context.connection, times(2)).setAutoCommit(true);
+ verify(context.connection, never()).close();
+ verify(context.chunkSplitter, never()).close();
+ }
+
+ @Test
+ void shouldDiscardConnectionWhenTransactionCleanupFails() throws Exception {
+ TestContext context = createContext(true);
+ context.openEmptySplit();
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(false);
+ doThrow(new SQLException("rollback failed")).when(context.connection).rollback();
+
+ context.inputFormat.close();
+
+ verify(context.connection).close();
+ verify(context.chunkSplitter).close();
+ verify(context.connection, never()).setAutoCommit(true);
+ }
+
+ @Test
+ void shouldCloseCachedConnectionWhenStatementCannotExposeConnection() throws Exception {
+ TestContext context = createContext(true);
+ context.openEmptySplit();
+ when(context.statement.getConnection())
+ .thenThrow(new SQLException("get connection failed"));
+
+ context.inputFormat.close();
+
+ verify(context.chunkSplitter).close();
+ verify(context.resultSet).close();
+ verify(context.statement).close();
+ }
+
+ @Test
+ void shouldDiscardCachedConnectionWhenStatementCreationFails() throws Exception {
+ TestContext context = createContext(true);
+ when(context.chunkSplitter.generateSplitStatement(SPLIT, TABLE_SCHEMA))
+ .thenThrow(new SQLException("prepare failed"));
+
+ assertThrows(JdbcConnectorException.class, () -> context.inputFormat.open(SPLIT));
+
+ verify(context.chunkSplitter).close();
+ verify(context.statement, never()).executeQuery();
+ }
+
+ @Test
+ void shouldRollbackAndKeepConnectionWhenExecuteQueryFails() throws Exception {
+ TestContext context = createContext(true);
+ when(context.chunkSplitter.generateSplitStatement(SPLIT, TABLE_SCHEMA))
+ .thenReturn(context.statement);
+ when(context.statement.getConnection()).thenReturn(context.connection);
+ when(context.statement.executeQuery()).thenThrow(new SQLException("execute failed"));
+ when(context.connection.isClosed()).thenReturn(false);
+ when(context.connection.getAutoCommit()).thenReturn(false);
+
+ assertThrows(JdbcConnectorException.class, () -> context.inputFormat.open(SPLIT));
+
+ verify(context.statement).close();
+ verify(context.connection).rollback();
+ verify(context.connection).setAutoCommit(true);
+ verify(context.connection, never()).close();
+ verify(context.chunkSplitter, never()).close();
+ }
+
+ @Test
+ void shouldAlwaysCloseCachedConnectionWhenInputFormatCloses() throws Exception {
+ TestContext context = createContext(true);
+
+ context.inputFormat.closeInputFormat();
+
+ verify(context.chunkSplitter).close();
+ }
+
+ @Test
+ void shouldIgnoreClosedConnection() throws SQLException {
+ Connection connection = mock(Connection.class);
+ when(connection.isClosed()).thenReturn(true);
+
+ JdbcInputFormat.finishReadTransaction(connection, true);
+
+ verify(connection, never()).getAutoCommit();
+ verify(connection, never()).rollback();
+ }
+
+ private static TestContext createContext(boolean configuredAutoCommit) throws SQLException {
+ JdbcDialect dialect = mock(JdbcDialect.class);
+ JdbcRowConverter rowConverter = mock(JdbcRowConverter.class);
+ ChunkSplitter chunkSplitter = mock(ChunkSplitter.class);
+ PreparedStatement statement = mock(PreparedStatement.class);
+ ResultSet resultSet = mock(ResultSet.class);
+ Connection connection = mock(Connection.class);
+ CatalogTable catalogTable = mock(CatalogTable.class);
+ Map tables = Collections.singletonMap(TABLE_PATH, catalogTable);
+
+ when(dialect.getRowConverter()).thenReturn(rowConverter);
+ when(catalogTable.getTableSchema()).thenReturn(TABLE_SCHEMA);
+
+ JdbcInputFormat inputFormat =
+ new JdbcInputFormat(dialect, chunkSplitter, tables, configuredAutoCommit);
+ return new TestContext(inputFormat, chunkSplitter, statement, resultSet, connection);
+ }
+
+ private static final class TestContext {
+ private final JdbcInputFormat inputFormat;
+ private final ChunkSplitter chunkSplitter;
+ private final PreparedStatement statement;
+ private final ResultSet resultSet;
+ private final Connection connection;
+
+ private TestContext(
+ JdbcInputFormat inputFormat,
+ ChunkSplitter chunkSplitter,
+ PreparedStatement statement,
+ ResultSet resultSet,
+ Connection connection) {
+ this.inputFormat = inputFormat;
+ this.chunkSplitter = chunkSplitter;
+ this.statement = statement;
+ this.resultSet = resultSet;
+ this.connection = connection;
+ }
+
+ private void openEmptySplit() throws IOException, SQLException {
+ when(chunkSplitter.generateSplitStatement(SPLIT, TABLE_SCHEMA)).thenReturn(statement);
+ when(statement.executeQuery()).thenReturn(resultSet);
+ when(resultSet.next()).thenReturn(false);
+ when(statement.getConnection()).thenReturn(connection);
+ inputFormat.open(SPLIT);
+ }
+ }
+}