The conversion report

Every conversion produces a ConversionReport alongside the generated files. It is the machine-readable record of what happened and why - the artifact you triage after a migration and the one you gate CI on. This page documents its structure, how to obtain it, and how to act on it.

Getting the report

From the CLI, write it to a file with --report:

sql2sqlx ./sql -o ./definitions --report report.json

From Python it is always attached to the result:

from sql2sqlx import convert_directory

result = convert_directory("sql", "definitions")
report = result.report
print(report.actions_by_type)
report_dict = report.to_dict()   # JSON-serializable

Report fields

ConversionReport is a dataclass with the following fields (see ConversionReport for the autodoc entry):

Field

Meaning

files_read

Number of input files parsed, including files that failed.

statements

Number of top-level statements found across all files, counted before any whole-file script collapsing.

actions_by_type

Mapping of Dataform action type to count, e.g. {"table": 12, "view": 4, "operations": 3}.

refs_rewritten

Number of table reference sites rewritten to ${ref(...)}.

refs_unresolved

Distinct referenced-but-not-produced table paths that could be dataset-resolved and were left as literals. These are your external sources; --declare-external turns them into declarations.

warnings

The list of every non-fatal finding (see below).

failures

Mapping of source path -> error message for files that could not be parsed at all (e.g. a lexer error). Other files are unaffected.

elapsed_seconds

Wall-clock duration of the run, rounded to milliseconds.

input_bytes

Total size of the SQL read, in bytes.

Each entry in warnings is a ReportWarning with four fields:

code

A stable, machine-readable identifier such as FALLBACK_OPERATIONS or MERGE_INCREMENTAL. Gate and filter on this, not on the message text.

message

A human-readable explanation, including the specific table or reason.

path

The source file the finding relates to (empty for synthesized actions).

line

The 1-based source line (0 when not applicable).

JSON shape

report.to_dict() (and the CLI’s --report file) produces this structure:

{
  "files_read": 1,
  "statements": 3,
  "actions_by_type": {
    "table": 1,
    "operations": 2
  },
  "refs_rewritten": 1,
  "refs_unresolved": [
    "raw.src"
  ],
  "warnings": [
    {
      "code": "INSERT_VALUES",
      "message": "INSERT ... VALUES has no query body; kept as operations.",
      "path": "input.sql",
      "line": 2
    },
    {
      "code": "ORPHAN_SELECT",
      "message": "Standalone query converted to an operations action; review whether it should be a table, view or assertion instead.",
      "path": "input.sql",
      "line": 3
    }
  ],
  "failures": {},
  "elapsed_seconds": 0.0,
  "input_bytes": 124
}

Key ordering in the JSON is stable (it follows the dataclass field order, not alphabetical), so the file diffs cleanly between runs.

The CLI summary

Unless you pass --quiet, the CLI prints a compact summary to stderr (generated SQLX for a single-file input goes to stdout, so piping stays clean):

sql2sqlx v0.1.0
  files read:     1
  statements:     3
  actions:        operations=2 table=1
  refs rewritten: 1 (1 external left as literals)
  warnings:       2
  failures:       0
  elapsed:        0.0s (0.0 MB)

Add -v/--verbose to also list every warning ([CODE] path:line: message) and every generated file. Any failures are always listed, regardless of verbosity.

Triage workflow

The warnings fall into three broad severities. None of them stop a build - the tool never emits SQL that changes behavior on its own - but they tell you where human review pays off.

1. Review the choice you opted into. When you select a non-default strategy, the tool tells you exactly what changed:

  • INSERT_INCREMENTAL, MERGE_INCREMENTAL, TARGET_SCHEMA_REQUIRED - a typed incremental was produced; verify the existing target’s schema matches the query’s output before the first incremental run.

  • CREATE_REPLACE_SEMANTICS - a non-OR REPLACE create became a typed action that rebuilds on later runs instead of erroring on an existing object.

  • IF_NOT_EXISTS, DECLARATION_DROPPED_DDL - a guard was dropped or DDL was replaced by a declaration per your option.

2. Understand a fallback. These explain why a statement stayed verbatim, so you can decide whether to restructure the SQL or accept the operations action:

  • FALLBACK_OPERATIONS, FALLBACK_SELECT_ALIAS, MERGE_FALLBACK, COLUMN_DDL, INSERT_VALUES, TEMP_TABLE, EXTERNAL_TABLE, SNAPSHOT_TABLE, SCRIPT_FILE, PROCEDURE_PRESERVED.

3. Check an inferred edge or an ambiguous shape. These flag places where static analysis was conservative:

  • ORDER_ASSUMED, FUTURE_CREATOR, DEPENDENCY_CYCLE, SELF_REFERENCE, DUPLICATE_TARGET - ordering/reference decisions to sanity-check.

  • ORPHAN_SELECT, DYNAMIC_SIDE_EFFECTS, RERUN_RISK, SCRIPT_WRITES - statements whose intent or hidden effects need a human eye.

The full meaning of every code lives in the warning code reference.

Gating CI

failures and specific codes make good CI gates. For example, fail a build on any parse failure and on a code you have decided to disallow:

import sys
from sql2sqlx import convert_directory

result = convert_directory("sql", "definitions")
report = result.report

blocking = {"DEPENDENCY_CYCLE"}
hits = [w for w in report.warnings if w.code in blocking]

if report.failures or hits:
    for path, err in report.failures.items():
        print(f"FAILED {path}: {err}", file=sys.stderr)
    for w in hits:
        print(f"[{w.code}] {w.path}:{w.line}: {w.message}", file=sys.stderr)
    sys.exit(1)

The CLI already exits 1 when any file failed to convert (and 2 on a usage error), so sql2sqlx ... --report report.json is itself a usable gate; parse report.json for finer-grained policy.

Tip

dataform compile is the final gate. sql2sqlx produces a project that is intended to compile, but only Dataform can confirm the whole graph resolves against your warehouse’s real schemas. Run it after every conversion.