Analyze a DataStage flow and rewrite it as a Full Pushdown flow — all computation moves inside the database engine as a single SQL statement, leaving exactly 2 stages: one connector + one sink.
Scope: Both Target Full Pushdown (ELT) and Source Full Pushdown (TEL) are supported, subject to the following constraints:
| Mode | Condition |
|---|---|
| Target Full Pushdown (ELT) | All connectors (source + target) share the same connection; flow has a write target |
| Source Full Pushdown (TEL) | All source connectors share the same connection; sink is Sequential File, PxCopy, or PxPeek (no write target DB connector) |
Not supported:
- Partial pushdown (any intermediate stage not convertible to SQL)
- TEL with a different-connection DB as sink (sink config cannot be preserved through the pipeline) — future work
If the flow does not meet either mode's conditions, stop and report the reason clearly; do not attempt any partial conversion.
Inputs
Ask for these if not already provided:
flow_idorflow_name— the DataStage flow to analyzeproject_idorproject_name— the watsonx project
If names are given instead of IDs, resolve them first:
project_name→get_projects(name=<name>)to getproject_idflow_name→list_datastage_flows(entity_name=<name>, project_id=...)to getflow_id
Workflow
1. Retrieve the flow
Call retrieve_datastage_flow_code. Read the full sdk_code and extract:
- All connector stages — identify every
flow.add_stage(...)call whose type is a database connector (not Sequential File, Filter, Sort, Aggregator, etc.). For each:- Connection name — the string passed to
project.create_connection(name=...)orproject.connections.get(name=...)used by that stage. - Connection ID — call
list_connections(project_id=<project_id>, entity_name=<connection_name>)and record the returnedidUUID. Resolve every connector's connection separately; do not assume all connectors share the same connection until Step 2 verifies it.
- Connection name — the string passed to
- Local parameters — any
flow.add_local_parameter(...)calls; record name and default value.
The resolved connection UUIDs per connector are the inputs to the same-connection check
in Step 2. The single canonical connection_id used in Step 5 is only determined after
Step 2 confirms all connectors share the same UUID.
2. Classify pushdown eligibility
First verify the connector type is in the ELT-supported list. Only the following connectors support full pushdown (ELT run mode): Amazon RDS for PostgreSQL, Amazon Redshift, Google BigQuery, IBM Cloud Databases for PostgreSQL, IBM Db2, IBM Db2 for DataStage, IBM Db2 on Cloud, IBM Db2 Warehouse, Oracle, PostgreSQL, Snowflake, Teradata, Teradata database for DataStage, watsonx.data.
Cross-database and cross-schema operations are not supported in either source or target full pushdown — all tables referenced in the flow must belong to the same database and schema.
Classify the flow into one of the supported modes. Apply in order (first match wins):
| Priority | Mode | Condition |
|---|---|---|
| 1 | Target Full Pushdown (ELT) | All connectors (source + target) from the supported list, same connection, same database/schema, all intermediate stages convertible to SQL, flow has a write target connector |
| 2 | Source Full Pushdown (TEL) | All source connectors from the supported list, same connection, same database/schema, all intermediate stages convertible to SQL, sink is Sequential File / PxCopy / PxPeek (no write target DB connector) |
| 3 | Not supported | Any other case — stop and report |
Common preconditions for both modes (check first):
| Condition | Check |
|---|---|
| All connectors are from the supported list | See connector list above |
| All connectors share the same connection | Same connection UUID |
| All tables are in the same database/schema | No cross-database or cross-schema reads/writes |
| All intermediate stages have a SQL equivalent | See stage mapping in Step 3 |
If any condition fails, stop immediately and report:
| Failure | Report |
|---|---|
| Connector not in supported list | "Connector <type> does not support ELT run mode." |
| Connectors on different connections | "Connectors use different connections — Full Pushdown requires the same connection." |
| Cross-database or cross-schema | "Cross-database/schema operations are not supported in pushdown mode." |
| Any intermediate stage not convertible to SQL | "Stage <name> (<type>) cannot be translated to SQL — Partial Pushdown is not supported. Blocking stage(s): <list>." |
| TEL with a different-connection DB sink | "Sink is a DB connector on a different connection — TEL with a different-connection DB sink is not supported yet." |
3. Translate stages to SQL
Use the stage-to-SQL mapping in
references/stage-to-sql-mapping.md
to translate each DataStage stage. That file covers: supported stage translations,
target connector write-mode prepend (table_action / write_mode), stages that
block full pushdown, Mixed-mode stage limitations, and the runtime parameter
preservation rule.
4. Present SQL and confirm
Show the user:
- Pushdown classification and reasoning
- Each stage → SQL mapping
- Any items requiring manual verification (case sensitivity, dialect-specific functions)
- The complete optimized SQL
The "complete optimized SQL" for Target Full Pushdown is the full
workload block — i.e. the TRUNCATE/DELETE/CREATE prepend (from the
table_action table above) followed by the INSERT … SELECT. Do not
add a trailing SELECT here; the observability SELECT is appended
temporarily in Step 5 only when passing the SQL to di-agent-query-optimization.
For Source Full Pushdown, the complete SQL is just the single
workload SELECT.
Wait for explicit user confirmation before creating the flow.
5. Create the optimized flow
Observability SELECT rule (applies to BOTH paths below)
For Target Full Pushdown, the observability SELECT must be:
SELECT <col1>, <col2>, … FROM <target_table>
Where <col1>, <col2>, … are exactly the output columns of the target connector
(taken from the output link schema in Step 3), and <target_table> is the
fully-qualified target table name.
Hard constraints — never violate:
- No aggregation functions (
COUNT,SUM,AVG,MIN,MAX, etc.) — the target table already holds aggregated data; re-aggregating produces wrong results (e.g.COUNT(*) AS int2over a pre-aggregated table returns1per row, not the original counts) - No
GROUP BY - No subqueries, CTEs, or JOINs
- No reuse of any expression from the workload
INSERT … SELECT— derive the observability SELECT only from the target table's column list, never by copying or adapting the workload SELECT - The observability SELECT is read-only; it must not modify any data
Primary path: delegate to di-agent-query-optimization + di-agent-flow-substrait-datastage
This is the mandatory first attempt. Do not skip it. Do not use any other tool
(e.g. substrait_to_flow_code, compile_substrait_dsl, or direct SDK generation)
as a shortcut before trying this path.
Execute in strict order:
Step 5a — Invoke di-agent-query-optimization (Path 2 — SQL text).
di-agent-query-optimization is a skill, not an MCP tool. If no MCP tool with
that name exists, that is expected — do not treat it as unavailability and do not use
it as a reason to enter the fallback path. Activate the skill and execute its workflow
inline to produce an optimized pushdown Substrait plan. The primary path is considered
attempted only after a Substrait plan has been produced and handed to Step 5b, or after
the skill workflow returns an explicit failure (optimized: false or an error).
Proceeding directly to SDK code generation without producing a plan does not count
as attempting the primary path.
Pass to the skill:
- the canonical
connection_idconfirmed in Step 2 - the SQL constructed as follows:
- Target Full Pushdown — append the observability SELECT (per the rule above)
as a trailing statement after the workload block from Step 4:
The optimizer's script-trailing classifier lifts this SELECT as the observability SELECT automatically — noTRUNCATE/DELETE ...; INSERT INTO target ...; SELECT <output_cols> FROM <target_table>; ← appended trailing SELECThintsneeded. - Source Full Pushdown — pass the workload SELECT from Step 4 as-is.
- Target Full Pushdown — append the observability SELECT (per the rule above)
as a trailing statement after the workload block from Step 4:
- the suggested optimized flow name derived from the original flow name per the rule below, and local parameter defaults from Step 1 (when available)
Optimized flow naming rule:
- Always create a new flow — never overwrite the original flow or ask the user.
- The new flow name is
<original_flow_name>_optimized. - If
<original_flow_name>_optimizedalready exists, try<original_flow_name>_optimized_1, then<original_flow_name>_optimized_2, and so on until a free name is found. - Pass the resolved name as
flow_metadata.suggested_flow_nametodi-agent-flow-substrait-datastage.
Step 5b — Pass the resulting Substrait plan to di-agent-flow-substrait-datastage
to generate and call create_datastage_flow.
di-agent-flow-substrait-datastage is also a skill, not an MCP tool. If no MCP
tool with that name exists, that is expected — activate the skill and execute its
workflow inline.
The primary path has failed — and only then may you proceed to the fallback — when any of the following occurs:
di-agent-query-optimizationreturnsoptimized: falseor an explicit errordi-agent-flow-substrait-datastagereturns an error that cannot be fixed by correcting the SDK code (e.g. a skill or tool is unavailable)create_datastage_flowreturns an SDK-code error that persists after one corrective retry
A tool call returning a retryable error (e.g. a fixable SDK-code validation error) is not a primary-path failure — fix the error and retry within the same path.
Fallback path: generate SDK code directly
Only enter this path after the primary path has failed per the definition above.
Do not enter it preemptively, speculatively, or because an intermediate tool
(e.g. substrait_to_flow_code) that is not part of the primary path returned an error.
In the fallback path, do not invoke substrait_to_flow_code or compile_substrait_dsl.
Generate the sdk_code and call create_datastage_flow directly. Apply the same
Optimized flow naming rule as the primary path (always create new, _optimized suffix,
then _optimized_1, _optimized_2, … if needed). When generating SDK code, apply these rules:
enable_before_sql= the workload block only (TRUNCATE/DELETE + INSERT) — no SELECTselect_statement= the observability SELECT per the rule above — a plainSELECT <output_cols> FROM <target_table>reading the target table post-write- For Source Full Pushdown:
select_statement= the workload SELECT; noenable_before_sql
6. Summarize
Report: flow link, optimized flow name, pushdown mode (Target Full Pushdown or Source Full Pushdown), stage count (original → 2), parameters preserved, and (for target pushdown) the observability SELECT used. Do not report the original flow's execution mode. Note any items the user should verify before running.
Scope and limits
- Target Full Pushdown (ELT) — all connectors share the same
connection; flow has a write target; entire logic becomes
INSERT … SELECTinside the target database. - Source Full Pushdown (TEL) — all source connectors share the
same connection; sink is Sequential File, PxCopy, or PxPeek;
entire logic becomes a single
SELECT; sink is automatically rebuilt bydi-agent-flow-substrait-datastage(original sink configuration is not preserved). - TEL with a different-connection DB sink is not supported — the sink connector configuration cannot be preserved through the current pipeline. Future work.
- Partial Pushdown is not supported — if any intermediate stage cannot be translated to SQL, the skill stops and reports the blocking stage(s). Future work.
di-agent-query-optimizationis only invoked after Step 3 confirms that all stages have been successfully translated into a single complete SQL statement. It is never called for partial conversions.