Hard sources — not covered by free tools
Also parsed — certify what free tools miss
Runtimes
After migration
Parser-driven modernization of BTEQ scripts and load utilities — QUALIFY, primary index design, FastLoad, MultiLoad, and TPump. Full lineage, automated conversion, validated parity.
BTEQ script parsed into interactive lineage graph
One source, every target. Deterministic parsers read the estate and emit native code for the platform you pick — not BTEQ replayed through a Teradata-compatible shim.
Teradata BTEQ estate → MigryX parser → native platforms
Deterministic parseAI where it helpsThe parser is deterministic: the same input produces the same output on every run. Every output is validated against the original, row by row, before go-live.
Growth means a node addition or a platform refresh, negotiated years ahead of the demand it serves. Warehouses that bill for compute while it runs remove that planning cycle entirely.
A script interleaves DDL, DML, session settings, conditional logic and error handling. No modern client runs them, so every script is a small program that has to be re-expressed rather than simply re-pointed.
FastLoad, MultiLoad, TPump and FastExport were built around Teradata's own protocols and locking. Cloud platforms load through object storage instead, which changes the shape of the job rather than just its syntax.
Session settings, conditional error handling with .IF ERRORCODE, explicit transaction boundaries and Teradata-only syntax such as QUALIFY. Re-pointing this at another database does not work, because the control flow is BTEQ's, not SQL's.
-- daily_sales_rank.bteq
.LOGON $TDHOST/$TDUSER,$TDPWD;
.SET WIDTH 500;
.SET ERROROUT STDOUT;
DELETE FROM sales_rank_stg ALL;
.IF ERRORCODE <> 0 THEN .QUIT 8;
INSERT INTO sales_rank_stg
SELECT store_id,
product_id,
SUM(amount) AS total_amt
FROM sales_fact
WHERE sale_dt BETWEEN DATE '2026-01-01' AND DATE
'2026-01-31'
GROUP BY 1,2
QUALIFY RANK() OVER (PARTITION BY store_id
ORDER BY SUM(amount) DESC) <= 10;
.IF ERRORCODE <> 0 THEN .QUIT 8;
.IF ACTIVITYCOUNT = 0 THEN .GOTO NODATA;
COLLECT STATISTICS ON sales_rank_stg COLUMN(store_id);
.LABEL NODATA
.LOGOFF;
.QUIT 0;
# BTEQ control flow → explicit steps
import snowflake.connector
TOP_N_SALES = """
INSERT INTO sales_rank_stg
SELECT store_id, product_id, total_amt FROM (
SELECT store_id, product_id,
SUM(amount) AS total_amt,
RANK() OVER (PARTITION BY store_id
ORDER BY SUM(amount) DESC) AS rnk
FROM sales_fact
WHERE sale_dt BETWEEN '2026-01-01' AND '2026-01-31'
GROUP BY store_id, product_id
) WHERE rnk <= 10;
"""
with snowflake.connector.connect(**cfg) as cx:
cur = cx.cursor()
cur.execute("TRUNCATE TABLE sales_rank_stg") # DELETE ALL
cur.execute(TOP_N_SALES) # QUALIFY rewritten
if cur.rowcount == 0: # ACTIVITYCOUNT
log.warning("no rows loaded; skipping stats")
# .IF ERRORCODE handling is exception handling here;
# COLLECT STATISTICS has no equivalent and is dropped.
QUALIFY becomes a ranked subquery with an outer filter, DELETE ALL becomes TRUNCATE, ACTIVITYCOUNT becomes a row count check, and .IF ERRORCODE becomes real exception handling. Statistics collection is dropped because the target does not need it.
Every Teradata construct in your scripts maps to a defined target equivalent, recorded in the lineage report.
| Teradata Construct | Target Equivalent | Notes |
|---|---|---|
| BTEQ script | SQL steps with a driver | Control flow made explicit |
| .IF ERRORCODE / .QUIT | Exception handling and exit codes | Failure branches preserved |
| .LABEL / .GOTO | Structured control flow | Jumps rewritten as conditionals |
| ACTIVITYCOUNT | Row count check | Zero-row branches kept |
| QUALIFY clause | Ranked subquery with filter | Window function semantics preserved |
| PRIMARY INDEX | Clustering or partitioning | Distribution intent mapped to target |
| PARTITION BY (PPI) | Table partitioning | Range and case partitioning translated |
| MULTISET / SET tables | Table with dedupe on load | SET table uniqueness enforced explicitly |
| Teradata-only functions | Target dialect equivalents | OREPLACE, INDEX, ZEROIFNULL and similar |
| FastLoad | Bulk load from stage | Empty-table requirement no longer applies |
| MultiLoad | MERGE or upsert | Multi-table operations separated |
| TPump | Streaming or micro-batch load | Row-rate throttling no longer required |
| FastExport | Bulk unload to storage | Export sessions become file writes |
| Stored procedure / macro | Stored procedure or view | Macros usually become views |
| COLLECT STATISTICS | Not required | Target optimizers maintain their own statistics |
MigryX Data Matching compares Teradata output against the new warehouse output, row by row and column by column, with configurable tolerance rules and mismatch drill-down.
See how Data Matching works →The BTEQ scripts and DDL, plus any load utility control files such as FastLoad or MultiLoad scripts. Those files describe the batch estate. A database connection is only needed later, when validating that converted output matches.
It becomes a subquery that computes the window function and an outer filter on its result. Snowflake and Databricks now support QUALIFY directly, so on those targets the clause is often kept as-is rather than rewritten unnecessarily.
Distribution intent is mapped to the target's own mechanism, such as clustering keys or partitioning. This is treated as a design decision rather than a syntax translation, because a primary index choice that suited Teradata's hashing is not automatically the right clustering choice elsewhere.
Their control files are parsed to recover the target table, the field layout and the load semantics, which then become a bulk load from object storage or a MERGE. The result is a different shape of job, since cloud platforms do not use Teradata's session-based load protocols.