Limitations and guarantees

This page states precisely what sql2sqlx promises, and where static analysis of SQL text is fundamentally not enough - so you know exactly what to review.

Guarantees

  1. Semantics-preserving defaults. Statements that cannot be converted provably safely become verbatim operations actions - the SQL that runs is the SQL you wrote. Explicitly selected migration strategies report their changed first-run/guard behavior and runtime preconditions.

  2. Character-for-character source fidelity for all SQL outside explicit rewrites (references, select-list aliases, ${self()}, and the constant SQLX placeholders used to preserve literal ${ text and neutralize SQLX-unsafe comments). Generated SQLX is UTF-8 with \n newlines.

  3. Deterministic output: identical inputs and options yield byte-identical trees, whatever the worker count.

  4. Failure isolation: a broken file is reported and skipped; the rest of the corpus converts.

These follow directly from the design - see core concepts for the span-edit model and architecture for how determinism is maintained across parallel workers.

Known limitations

Input must be valid GoogleSQL

The converter is not a validator. Clearly broken input either raises a located LexError (reported per file in failures) or, if it lexes but is not recognized, passes through inside an operations body. It does not attempt to diagnose semantic SQL errors - dataform compile and BigQuery are the authorities on validity.

Cross-file writer ordering is heuristic

When several files write one table, their chain follows sorted file paths (ORDER_ASSUMED), because cross-file execution order is not encoded in the SQL. Within a single file, statically identified read/write conflicts preserve original statement order; hidden effects from dynamic SQL or procedures cannot be seen and so cannot be ordered.

Single-part name resolution

Unqualified FROM t matches an unqualified producer t, or a qualified producer via --default-dataset. Query-scoped CTE and range-variable analysis removes relational aliases, but physical-name ambiguity that a default cannot resolve is left literal; supply an appropriate default dataset or qualify the source path.

TABLE table_path arguments inside table functions

Arguments of the form TABLE d.t inside a table-valued function call (e.g. ML.PREDICT(MODEL m, TABLE d.t)) are not rewritten to ref().

Scripts stay whole

Files that use transactions, temporary objects, variables, procedural control flow, calls or dynamic SQL become one operations action; their internal statements are not individually lifted, because Dataform runs the body in a single BigQuery script context and splitting would change scope and control-flow semantics. Reference sites and write targets inside the script are still harvested for dependency wiring.

Dynamic side effects require review

SQL inside EXECUTE IMMEDIATE, and writes performed by a called procedure, cannot be inferred statically (DYNAMIC_SIDE_EFFECTS). The statements remain verbatim, but their hidden read/write dependencies are not synthesized - add any needed dependencies by hand.

Some DDL has no Dataform config equivalent

CREATE TABLE AS column lists (COLUMN_DDL) and DEFAULT COLLATE cannot be represented in a typed action’s config and fall back to operations. Special create forms (LIKE/CLONE/COPY, temp/external/snapshot) likewise stay verbatim.

Opt-in incrementals require schema review

Dataform derives incremental INSERT/MERGE projections from the existing target metadata. SQL text alone cannot prove that a pre-existing target’s complete column set matches the converted query, so INSERT_INCREMENTAL and TARGET_SCHEMA_REQUIRED explicitly flag that operator check. The defaults keep both statement classes verbatim.

Wildcard tables and decorators are not sources

Wildcard tables (for example `ds.events_*`) and table decorators (`ds.events$20240101`) are table expressions, not declarable objects, and are left as literals even under --declare-external.

Future owners never reverse source order

An earlier read of a table whose owner appears later in corpus order stays literal (FUTURE_CREATOR) rather than reordering your pipeline; any remaining cycle-producing edge is omitted with DEPENDENCY_CYCLE.

The final gate

Dataform compiles and validates the final project. Run dataform compile after conversion as the last gate - it is the only thing that can confirm the entire dependency graph resolves against your warehouse’s real schemas.