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 5050fbaa34 [Improve][CLI] Add conditional-routing skill, 
PostgreSQL-CDC prerequisites, and wide-DAG wiring rules (#11659)
5050fbaa34 is described below

commit 5050fbaa345b48a3ef4aa81c777ff8f8b3c69056
Author: SEZ <[email protected]>
AuthorDate: Tue Aug 11 23:26:49 2026 +0900

    [Improve][CLI] Add conditional-routing skill, PostgreSQL-CDC prerequisites, 
and wide-DAG wiring rules (#11659)
    
    Co-authored-by: SEZ9 <[email protected]>
---
 seatunnel-cli/seatunnel_cli/agents.py              |  10 ++
 seatunnel-cli/seatunnel_cli/skills/cdc_realtime.md |  39 +++++-
 .../seatunnel_cli/skills/conditional_routing.md    | 135 +++++++++++++++++++++
 .../seatunnel_cli/skills/multi_pipeline.md         |  77 ++++++++++++
 4 files changed, 257 insertions(+), 4 deletions(-)

diff --git a/seatunnel-cli/seatunnel_cli/agents.py 
b/seatunnel-cli/seatunnel_cli/agents.py
index 07adfc6670..1a5085c7b5 100644
--- a/seatunnel-cli/seatunnel_cli/agents.py
+++ b/seatunnel-cli/seatunnel_cli/agents.py
@@ -1108,6 +1108,16 @@ When generating configs with **multiple source/sink 
blocks**:
 - ALL source blocks go inside ONE `source {{ }}` section
 - ALL sink blocks go inside ONE `sink {{ }}` section
 
+Chained/wide DAGs (transforms in the middle, 5+ blocks):
+- Wire every hop explicitly: source → transform → sink each consume the
+  upstream label and emit their own (`a_raw` → `a_filtered` → sink).
+- After inserting a transform, re-point the downstream consumer to the
+  transform's OUTPUT label — a sink still reading the source label silently
+  bypasses the transform.
+- To SPLIT one stream by condition: several transforms may consume the SAME
+  source label (each with its own predicate + distinct output label). Two
+  different sources must never emit the same label.
+
 Example — Two Jdbc sources routed to Console and Assert:
 ```hocon
 env {{
diff --git a/seatunnel-cli/seatunnel_cli/skills/cdc_realtime.md 
b/seatunnel-cli/seatunnel_cli/skills/cdc_realtime.md
index 825d82e618..29901e2c5e 100644
--- a/seatunnel-cli/seatunnel_cli/skills/cdc_realtime.md
+++ b/seatunnel-cli/seatunnel_cli/skills/cdc_realtime.md
@@ -54,12 +54,42 @@ Data flows as a never-ending stream of insert/update/delete 
events.
 - For multi-table CDC, use `table-name` regex or create multiple CDC source 
blocks.
 - CDC sources need database permissions: REPLICATION SLAVE, REPLICATION CLIENT 
for MySQL.
 
+### PostgreSQL-CDC specifics (differs from MySQL-CDC)
+
+- PostgreSQL-CDC reads the logical replication stream (WAL), which requires
+  server-side prerequisites: `wal_level = logical` on the server, and a user
+  with the REPLICATION attribute.
+- **`slot.name` should be set explicitly** — a stable replication slot name
+  (e.g. `"seatunnel_slot"`). Each concurrent CDC job needs its own slot.
+- **`decoding.plugin.name`** selects the logical decoding plugin. Use
+  `"pgoutput"` for PostgreSQL 10+ (built-in, no server install needed);
+  `"decoderbufs"` only when that plugin is installed on the server.
+- `table-names` (plural, list-typed) takes schema-qualified entries. The
+  canonical form is TWO-part `"schema.table"` (e.g. `["inventory.orders"]`)
+  — the connector's own error message calls it `schemaName.tableName`.
+  A three-part `"database.schema.table"` entry is also accepted, but the
+  leading database segment is ignored (it does NOT filter by database).
+  Database selection comes from `database-names` (plural, list-typed);
+  the connector connects to the first listed database. Always confirm
+  exact keys with `get_connector_info`.
+- Debezium engine properties (e.g. a custom publication name) are passed
+  through the nested `debezium { }` map option, such as
+  `debezium { publication.name = "my_pub" }` — never as top-level options.
+
+| Option | MySQL-CDC | PostgreSQL-CDC |
+|---|---|---|
+| stream source | binlog | logical replication (WAL) |
+| `slot.name` | n/a | recommended, one per job |
+| `decoding.plugin.name` | n/a | `pgoutput` (PG 10+) |
+| `table-names` entry format | `database.table` | `schema.table` (canonical) |
+| server prerequisite | binlog ROW mode | `wal_level = logical` |
+
 ## SOP
 
 1. **Identify CDC source connector** (MySQL-CDC, PostgreSQL-CDC, etc.) from 
the plan.
 2. **Identify sink connector** — verify it supports upsert semantics for CDC 
correctness.
 3. **Set env block**: `job.mode = "STREAMING"`, `checkpoint.interval = 10000`.
-4. **Fill CDC source options**: hostname, port, username, password, 
database-name, table-name. Use `get_connector_info` for exact option keys.
+4. **Fill CDC source options**: hostname, port, username, password, and the 
database/table selection keys. Use `get_connector_info` for exact option keys — 
they vary by connector (e.g. PostgreSQL-CDC uses plural 
`database-names`/`table-names`).
 5. **Fill sink options**: fetch connector metadata, ensure primary key / 
upsert config is set.
 6. **Set routing**: `plugin_output` on source, `plugin_input` on sink.
 7. **Apply golden example** if available for this (source, sink) pair.
@@ -69,7 +99,7 @@ Data flows as a never-ending stream of insert/update/delete 
events.
 
 - MUST use `job.mode = "STREAMING"` — never BATCH for CDC.
 - MUST include `checkpoint.interval` in env block.
-- CDC source option keys use hyphens: `database-name`, `table-name` — NOT 
underscores.
+- CDC source option keys use hyphens (e.g. `database-name`/`database-names`) — 
NOT underscores.
 - Do NOT add `query` to CDC sources — they capture changelogs, not arbitrary 
SQL.
 - Credentials always as `${ENV_VAR}`.
 
@@ -88,8 +118,9 @@ source {
     port = <port>
     username = "${DB_USER}"
     password = "${DB_PASSWORD}"
-    database-name = "<database>"
-    table-name = "<table>"
+    database-names = ["<database>"]
+    # MySQL-CDC entries: "<database>.<table>"; PostgreSQL-CDC: 
"<schema>.<table>"
+    table-names = ["<database-or-schema-qualified-table>"]
     plugin_output = "<routing_label>"
   }
 }
diff --git a/seatunnel-cli/seatunnel_cli/skills/conditional_routing.md 
b/seatunnel-cli/seatunnel_cli/skills/conditional_routing.md
new file mode 100644
index 0000000000..eed870a82d
--- /dev/null
+++ b/seatunnel-cli/seatunnel_cli/skills/conditional_routing.md
@@ -0,0 +1,135 @@
+<!--
+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.
+-->
+
+---
+name: conditional_routing
+description: Split one source stream into multiple sinks by row-level 
conditions (predicate routing)
+triggers:
+  - split
+  - route
+  - routing
+  - partition by
+  - separate
+  - filter into
+  - based on
+  - by condition
+  - by status
+  - by level
+  - greater than
+  - less than
+  - 拆分
+  - 分流
+  - 路由
+  - 大于
+  - 小于
+tools:
+  - get_connector_info
+composable: true
+---
+
+## When to Use
+
+User wants rows from ONE source delivered to DIFFERENT sinks depending on a
+row-level condition. Examples: "route amount >= 1000 to files, the rest to
+console", "ERROR logs to console, everything else to archive", "PAID orders
+here, unpaid there".
+
+This is NOT multi-pipeline (independent sources) and NOT fan-out (same rows
+to all sinks) — it is one stream split by mutually exclusive predicates.
+
+## Domain Knowledge
+
+- The split is built from **parallel SQL transforms consuming the SAME source
+  output**: each transform declares `plugin_input = "<source_label>"` and its
+  own WHERE predicate, then emits a distinct `plugin_output`.
+- SeaTunnel duplicates the source stream to every transform that references
+  its label — no extra config needed on the source.
+- Each branch then needs its own sink consuming that branch's label.
+- The Zeta SQL transform supports projection + WHERE only (no GROUP BY,
+  JOIN, or ORDER BY). Keep predicates simple comparisons/boolean logic.
+- In the SQL transform, `table_name` must reference the INPUT label (the
+  source's plugin_output), and the FROM clause uses that same name.
+
+## SOP
+
+1. **Identify the split condition** and the number of branches (usually 2).
+2. **Make predicates mutually exclusive and complete**: `amount > 100` pairs
+   with `amount <= 100` (not `amount < 100`, which silently drops equality).
+   If the user's wording leaves a gap, close it and note the choice.
+3. **One source block** with a single `plugin_output` label.
+4. **One SQL transform per branch**, all with `plugin_input` = source label,
+   each with a distinct `plugin_output` and its WHERE predicate.
+5. **One sink per branch**, each consuming its branch label.
+6. **Validate wiring**: every label consumed exists; branch labels are
+   unique; no branch left without a sink.
+
+## Constraints
+
+- Do NOT read the source twice — one source block, shared by transforms.
+- Do NOT chain the transforms (both consume the SOURCE label, not each other).
+- Predicates MUST be mutually exclusive and SHOULD cover all rows.
+- Only WHERE-level SQL (Zeta SQL transform limitation).
+
+## Pattern
+
+Split `transactions` by amount into two destinations:
+
+```hocon
+env {
+  parallelism = 2
+  job.mode = "BATCH"
+}
+
+source {
+  Jdbc {
+    url = "jdbc:mysql://<host>:3306/<db>"
+    driver = "com.mysql.cj.jdbc.Driver"
+    user = "${MYSQL_USER}"
+    password = "${MYSQL_PASSWORD}"
+    query = "SELECT * FROM transactions"
+    plugin_output = "tx_all"
+  }
+}
+
+transform {
+  Sql {
+    plugin_input = "tx_all"
+    plugin_output = "tx_large"
+    query = "SELECT * FROM tx_all WHERE amount >= 1000"
+  }
+  Sql {
+    plugin_input = "tx_all"
+    plugin_output = "tx_small"
+    query = "SELECT * FROM tx_all WHERE amount < 1000"
+  }
+}
+
+sink {
+  LocalFile {
+    plugin_input = "tx_large"
+    path = "/data/tx_large"
+    file_format_type = "json"
+  }
+  Console {
+    plugin_input = "tx_small"
+  }
+}
+```
+
+Wiring summary: `source(tx_all) ⇒ [Sql→tx_large, Sql→tx_small] ⇒ sinks`.
+Both transforms consume `tx_all`; the two predicates are exclusive
+(`>= 1000` / `< 1000`) and together cover every row.
diff --git a/seatunnel-cli/seatunnel_cli/skills/multi_pipeline.md 
b/seatunnel-cli/seatunnel_cli/skills/multi_pipeline.md
index d5d3efe77e..1bddb69875 100644
--- a/seatunnel-cli/seatunnel_cli/skills/multi_pipeline.md
+++ b/seatunnel-cli/seatunnel_cli/skills/multi_pipeline.md
@@ -64,6 +64,83 @@ Expansion is NOT needed when:
 - Sink supports multi-table (e.g., Jdbc with `generate_sink_sql = true`)
 - Source uses multi-table syntax natively (e.g., CDC with regex `table-name`)
 
+### Wide-DAG Wiring (5+ blocks, transforms in the middle)
+
+When a job mixes pipelines WITH transforms and pipelines WITHOUT them, wire
+each hop explicitly — every block consumes exactly one upstream label and
+emits its own. Always produce ONE complete config with single `source { }`,
+`transform { }`, and `sink { }` sections (never separate fragments per block).
+
+Complete example — chain `source → Sql → FieldMapper → sink` plus an
+independent second pipeline in the same job:
+
+```hocon
+env {
+  parallelism = 2
+  job.mode = "BATCH"
+}
+
+source {
+  Jdbc {
+    url = "jdbc:mysql://host:3306/shop"
+    driver = "com.mysql.cj.jdbc.Driver"
+    user = "${MYSQL_USER}"
+    password = "${MYSQL_PASSWORD}"
+    query = "SELECT * FROM orders"
+    plugin_output = "orders_raw"
+  }
+  Jdbc {
+    url = "jdbc:mysql://host:3306/shop"
+    driver = "com.mysql.cj.jdbc.Driver"
+    user = "${MYSQL_USER}"
+    password = "${MYSQL_PASSWORD}"
+    query = "SELECT * FROM audit_log"
+    plugin_output = "audit_raw"
+  }
+}
+
+transform {
+  Sql {
+    plugin_input = "orders_raw"
+    plugin_output = "orders_filtered"
+    query = "SELECT * FROM orders_raw WHERE amount > 10"
+  }
+  FieldMapper {
+    plugin_input = "orders_filtered"
+    plugin_output = "orders_final"
+    field_mapper = {
+      id = order_id
+      amount = total
+    }
+  }
+}
+
+sink {
+  Console {
+    plugin_input = "orders_final"
+  }
+  Console {
+    plugin_input = "audit_raw"
+  }
+}
+```
+
+The chain hops are: `orders_raw` (source) → `orders_filtered` (Sql) →
+`orders_final` (FieldMapper) → first sink; the second pipeline goes
+`audit_raw` → second sink directly.
+
+Common wiring mistakes to avoid:
+- **Label reuse across chains**: one label may feed multiple parallel
+  branches (intentional split), but two different SOURCES must never emit
+  the same label.
+- **Broken chain**: a transform emitting `orders_filtered` while the sink
+  still consumes `orders_raw` silently bypasses the transform. After adding
+  a transform, re-point the downstream consumer to the transform's output
+  label.
+- **Transform applied to the wrong pipeline**: transforms only apply to the
+  chain whose label they consume — placement inside `transform { }` does not
+  scope them; only `plugin_input` does.
+
 ## SOP
 
 1. **Count pipelines** from the structured plan.

Reply via email to