sql2sqlx 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,733 @@
1
+ Metadata-Version: 2.4
2
+ Name: sql2sqlx
3
+ Version: 0.1.0
4
+ Summary: Convert BigQuery SQL into Dataform SQLX with conservative, dependency-aware migration tooling.
5
+ Author-email: Soumyadip Sarkar <soumyadip@soumyadipsarkar.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/neuralsorcerer/sql2sqlx
8
+ Project-URL: Documentation, https://neuralsorcerer.github.io/sql2sqlx/
9
+ Project-URL: Source, https://github.com/neuralsorcerer/sql2sqlx
10
+ Project-URL: Issues, https://github.com/neuralsorcerer/sql2sqlx/issues
11
+ Project-URL: Changelog, https://github.com/neuralsorcerer/sql2sqlx/blob/main/CHANGELOG.md
12
+ Keywords: analytics-engineering,bigquery,bigquery-sql,code-generation,data-engineering,dataform,dataform-sqlx,ddl,dependency-graph,dml,elt,etl,gcp,google-cloud,googlesql,migration,sql,sql-converter,sql-migration,sql-parser,sqlx,warehouse-migration
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: SQL
24
+ Classifier: Topic :: Database
25
+ Classifier: Topic :: Software Development
26
+ Classifier: Topic :: Text Processing
27
+ Classifier: Topic :: Utilities
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=7.0; extra == "dev"
34
+ Requires-Dist: black[jupyter]>=26.3.1; extra == "dev"
35
+ Requires-Dist: isort>=5.13; extra == "dev"
36
+ Requires-Dist: mypy<2.0,>=1.10; extra == "dev"
37
+ Requires-Dist: pre-commit>=3.5; extra == "dev"
38
+ Provides-Extra: docs
39
+ Requires-Dist: sphinx>=7.0; extra == "docs"
40
+ Requires-Dist: furo>=2024.1.29; extra == "docs"
41
+ Requires-Dist: myst-parser>=2.0; extra == "docs"
42
+ Requires-Dist: myst-nb>=1.1; extra == "docs"
43
+ Provides-Extra: examples
44
+ Requires-Dist: jupyter>=1.0; extra == "examples"
45
+ Requires-Dist: matplotlib>=3.8; extra == "examples"
46
+ Dynamic: license-file
47
+
48
+ <h1 align="center">
49
+ sql2sqlx
50
+ </h1>
51
+ <h3 align="center">
52
+ BigQuery SQL -> Dataform SQLX migration tooling.
53
+ </h3>
54
+
55
+ ---
56
+
57
+ <div align="center">
58
+
59
+ [![Current Release](https://img.shields.io/github/release/neuralsorcerer/sql2sqlx.svg)](https://github.com/neuralsorcerer/sql2sqlx/releases)
60
+ [![PyPI](https://img.shields.io/pypi/v/sql2sqlx.svg?logo=pypi&logoColor=white)](https://pypi.org/project/sql2sqlx/)
61
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-fcbc2c.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
62
+ [![Test Ubuntu](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml?query=branch%3Amain)
63
+ [![Test Windows](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml?query=branch%3Amain)
64
+ [![Test MacOS](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml?query=branch%3Amain)
65
+ [![Lints](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml?query=branch%3Amain)
66
+ [![Dataform Compile](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml?query=branch%3Amain)
67
+ [![Build](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml/badge.svg)](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml?query=branch%3Amain)
68
+ [![Documentation](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/docs.yml/badge.svg)](https://neuralsorcerer.github.io/sql2sqlx/)
69
+ [![License](https://img.shields.io/badge/License-Apache%202.0-3c60b1.svg?logo=opensourceinitiative&logoColor=white)](LICENSE)
70
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/sql2sqlx?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=GREEN&left_text=downloads)](https://pepy.tech/projects/sql2sqlx)
71
+
72
+ </div>
73
+
74
+ `sql2sqlx` converts plain BigQuery/GoogleSQL `.sql` estates into
75
+ [Dataform](https://cloud.google.com/dataform) `.sqlx` actions. It is built
76
+ for real migrations: large repositories, mixed DDL/DML/script workloads,
77
+ legacy scheduler ordering, comments and formatting that must survive review,
78
+ and teams that need a machine-readable audit trail rather than a black-box
79
+ rewrite.
80
+
81
+ At a high level it turns this:
82
+
83
+ ```sql
84
+ CREATE OR REPLACE TABLE marts.customer_orders
85
+ PARTITION BY DATE(order_ts)
86
+ CLUSTER BY customer_id
87
+ OPTIONS(description = "Customer order facts") AS
88
+ SELECT customer_id, order_id, order_ts
89
+ FROM staging.orders;
90
+ ```
91
+
92
+ into this:
93
+
94
+ ```sqlx
95
+ config {
96
+ type: "table",
97
+ schema: "marts",
98
+ name: "customer_orders",
99
+ description: "Customer order facts",
100
+ bigquery: {
101
+ partitionBy: "DATE(order_ts)",
102
+ clusterBy: ["customer_id"]
103
+ }
104
+ }
105
+
106
+ -- source: path/to/file.sql:1 (CREATE TABLE converted by sql2sqlx v0.1.0)
107
+ SELECT customer_id, order_id, order_ts
108
+ FROM ${ref("staging", "orders")}
109
+ ```
110
+
111
+ The project philosophy is deliberately conservative:
112
+
113
+ > **Convert only when the tool can prove that the generated SQLX preserves the
114
+ > intended behavior. Otherwise, keep the original SQL verbatim as a Dataform
115
+ > `operations` action and report exactly why.**
116
+
117
+ That policy is what makes the tool suitable for production migrations. It
118
+ prefers an auditable fallback over a clever but unsafe guess.
119
+
120
+ ---
121
+
122
+ ## What this project solves
123
+
124
+ Migrating a BigQuery warehouse to Dataform usually involves several risky,
125
+ manual steps:
126
+
127
+ 1. Translating `CREATE TABLE ... AS SELECT` and `CREATE VIEW ... AS SELECT`
128
+ statements into Dataform `config {}` blocks.
129
+ 2. Replacing raw table names with `${ref(...)}` calls without accidentally
130
+ rewriting CTEs, aliases, table functions, `UNNEST`, or self-references.
131
+ 3. Preserving ordering for legacy DML such as `INSERT`, `MERGE`, `UPDATE`,
132
+ `DELETE`, `DROP`, and `ALTER` statements.
133
+ 4. Handling scripts that rely on BigQuery session state: variables,
134
+ transactions, temporary tables, procedural blocks, procedure calls, and
135
+ dynamic SQL.
136
+ 5. Keeping enough provenance to review the migration and gate it in CI.
137
+
138
+ Regular expressions are not enough for this. Semicolons appear inside strings,
139
+ comments, and scripts. Identifiers may be backticked, escaped, dotted, or use
140
+ BigQuery dashed project syntax. Query-local names can shadow table names.
141
+ Dataform can own exactly one output per action name, while SQL files can have
142
+ many writers to the same physical table.
143
+
144
+ `sql2sqlx` addresses those problems with a tokenizer, statement splitter,
145
+ classifier, reference scanner, cross-file linker, and SQLX emitter that all
146
+ operate on source spans rather than re-serializing SQL from an AST. The result
147
+ is deterministic, reviewable SQLX with conservative fallback behavior.
148
+
149
+ ---
150
+
151
+ ## Core capabilities
152
+
153
+ ### Safe SQLX action generation
154
+
155
+ - Converts safe `CREATE TABLE ... AS SELECT` statements to `type: "table"`.
156
+ - Converts safe `CREATE VIEW ... AS SELECT` statements to `type: "view"`.
157
+ - Preserves unsupported or unsafe statements as `type: "operations"`.
158
+ - Can emit `type: "declaration"` for externally managed plain tables when
159
+ explicitly requested.
160
+ - Can convert eligible `INSERT ... SELECT` and shape-proven `MERGE` statements
161
+ to `type: "incremental"` when the user opts into those strategies.
162
+
163
+ ### BigQuery-aware parsing foundations
164
+
165
+ - Handles GoogleSQL strings, raw strings, bytes literals, triple-quoted
166
+ strings, comments, backtick identifiers, parameters, numeric literals,
167
+ operators, and dashed project identifiers.
168
+ - Splits statements without being fooled by semicolons in strings, comments,
169
+ expressions, or scripting blocks.
170
+ - Understands BigQuery scripting constructs such as `BEGIN ... END`,
171
+ `IF ... END IF`, `LOOP`, `WHILE`, `REPEAT`, `FOR`, scripting `CASE`, and
172
+ transaction boundaries.
173
+
174
+ ### Dependency-aware migration
175
+
176
+ - Rewrites produced table reads to `${ref(...)}`.
177
+ - Builds writer chains so mutating statements keep their corpus order.
178
+ - Adds dependencies on the latest preceding writer when a reader consumes a
179
+ table that was mutated earlier.
180
+ - Avoids dependency edges that would reorder same-file future creators before
181
+ earlier reads.
182
+ - Detects duplicate owners and demotes later owners to safe operations.
183
+ - Optionally creates declarations for referenced-but-not-produced external
184
+ tables.
185
+
186
+ ### Metadata preservation
187
+
188
+ - Maps `PARTITION BY` to `bigquery.partitionBy`.
189
+ - Maps `CLUSTER BY` to `bigquery.clusterBy`.
190
+ - Maps simple `OPTIONS(description=...)` to `description`.
191
+ - Maps labels and selected BigQuery table options to Dataform BigQuery config.
192
+ - Preserves unrecognized or complex options as raw `bigquery.additionalOptions`.
193
+
194
+ ### Auditability and scale
195
+
196
+ - Emits provenance comments such as `-- source: file.sql:42` by default.
197
+ - Carries leading and trailing SQL comments into generated files where safe.
198
+ - Produces a JSON report with files read, statement counts, action counts,
199
+ rewritten refs, unresolved refs, warnings, failures, and elapsed time.
200
+ - Uses stable warning codes so migrations can be reviewed and gated in CI.
201
+ - Runs per-file parsing in parallel for directory conversions.
202
+ - Has no runtime third-party dependencies.
203
+ - Ships type information via `py.typed`.
204
+
205
+ ---
206
+
207
+ ## Installation
208
+
209
+ Install from PyPI:
210
+
211
+ ```bash
212
+ pip install sql2sqlx
213
+ ```
214
+
215
+ The package provides both:
216
+
217
+ - the `sql2sqlx` command-line program, and
218
+ - the importable `sql2sqlx` Python package.
219
+
220
+ Runtime support targets Python **3.10 through 3.14**.
221
+
222
+ For local development from a repository checkout:
223
+
224
+ ```bash
225
+ git clone https://github.com/neuralsorcerer/sql2sqlx.git
226
+ cd sql2sqlx
227
+ python -m pip install -e ".[dev,docs,examples]"
228
+ python -m pytest
229
+ ```
230
+
231
+ If you only want to run tests from a checkout, the repository's pytest
232
+ configuration includes the `src` package root, so `python -m pytest` works
233
+ without manually setting `PYTHONPATH` or performing an editable install.
234
+
235
+ ---
236
+
237
+ ## Quick start
238
+
239
+ ### Convert a directory into a Dataform definitions tree
240
+
241
+ ```bash
242
+ sql2sqlx ./legacy_sql \
243
+ -o ./dataform/definitions \
244
+ --default-project my-gcp-project \
245
+ --default-dataset analytics \
246
+ --report ./conversion-report.json \
247
+ --init-project
248
+ ```
249
+
250
+ This command:
251
+
252
+ 1. scans `./legacy_sql` for `*.sql` files,
253
+ 2. converts each SQL file into one or more `.sqlx` actions,
254
+ 3. writes the generated files below `./dataform/definitions`,
255
+ 4. writes a JSON conversion report, and
256
+ 5. scaffolds a `workflow_settings.yaml` next to the `definitions` folder.
257
+
258
+ ### Inspect a single SQL file
259
+
260
+ ```bash
261
+ sql2sqlx ./models/orders.sql
262
+ ```
263
+
264
+ For a single input file with no `--output`, generated SQLX is printed to
265
+ stdout. The summary is printed to stderr.
266
+
267
+ ### Dry-run a migration
268
+
269
+ ```bash
270
+ sql2sqlx ./legacy_sql --dry-run --report report.json --verbose
271
+ ```
272
+
273
+ Dry runs perform parsing, classification, linking, warning generation, and
274
+ report generation without writing SQLX files.
275
+
276
+ ---
277
+
278
+ ## Command-line usage
279
+
280
+ ```text
281
+ sql2sqlx INPUT [OPTIONS]
282
+ ```
283
+
284
+ `INPUT` may be either a `.sql` file or a directory. Directory conversions
285
+ normally require `--output`, unless `--dry-run` is supplied.
286
+
287
+ ### Common options
288
+
289
+ | Flag | Default | Description |
290
+ | --- | --- | --- |
291
+ | `-o, --output DIR` | none | Output directory for generated `.sqlx` files, usually Dataform `definitions/`. |
292
+ | `--report FILE` | none | Write the machine-readable JSON conversion report. |
293
+ | `--default-project ID` | none | Project used to resolve unqualified table paths for linking. |
294
+ | `--default-dataset ID` | none | Dataset used to resolve unqualified table paths for linking. |
295
+ | `--default-location LOCATION` | `US` | Location written into generated workflow settings. |
296
+ | `--layout {mirror,flat}` | `mirror` | Preserve input directory structure or emit all SQLX files in one folder. |
297
+ | `--include GLOB` | `*.sql` | Glob used when scanning directories. |
298
+ | `--encoding ENCODING` | `utf-8` | Input file encoding. |
299
+ | `-j, --jobs N` | `0` | Parser worker processes; `0` means auto-detect. |
300
+ | `--dry-run` | off | Convert and report without writing output files. |
301
+ | `--overwrite` | off | Permit writing into an output directory that already contains `.sqlx` files. |
302
+ | `--init-project` | off | Scaffold `workflow_settings.yaml`. |
303
+ | `-q, --quiet` | off | Suppress the run summary. |
304
+ | `-v, --verbose` | off | Print warnings and generated files in the summary. |
305
+ | `--version` | n/a | Print the installed version. |
306
+
307
+ ### Strategy options
308
+
309
+ The defaults are intentionally conservative.
310
+
311
+ | Flag | Default | Other values | Meaning |
312
+ | --- | --- | --- | --- |
313
+ | `--insert-strategy` | `operations` | `incremental` | Keep `INSERT ... SELECT` verbatim, or convert safe shapes to Dataform incrementals. |
314
+ | `--merge-strategy` | `operations` | `incremental-when-safe` | Keep `MERGE` verbatim, or convert shape-proven MERGEs to incrementals with `uniqueKey`. |
315
+ | `--plain-create` | `operations` | `declaration` | Keep schema-only `CREATE TABLE` DDL, or emit a declaration and drop the DDL. |
316
+ | `--if-not-exists` | `operations` | `table` | Preserve guarded CTAS/view DDL, or lift it with a warning about changed rerun behavior. |
317
+ | `--declare-external` | off | on | Emit declarations for external tables that are read but not produced by the corpus. |
318
+ | `--no-protected` | off | on | Disable `protected: true` on converted incrementals. |
319
+ | `--no-annotate` | off | on | Disable generated provenance comments. |
320
+ | `--tags TAG[,TAG...]` | none | n/a | Add Dataform tags to every action. |
321
+
322
+ ### Exit codes
323
+
324
+ | Code | Meaning |
325
+ | --- | --- |
326
+ | `0` | Conversion completed without per-file failures. |
327
+ | `1` | Conversion completed, but one or more input files failed to read or lex. |
328
+ | `2` | Usage or output-writing error. |
329
+
330
+ ---
331
+
332
+ ## Python API usage
333
+
334
+ The public API mirrors the CLI.
335
+
336
+ ### Convert a string
337
+
338
+ ```python
339
+ from sql2sqlx import convert_string
340
+
341
+ result = convert_string("""
342
+ CREATE OR REPLACE TABLE analytics.daily AS
343
+ SELECT CURRENT_DATE() AS d;
344
+ """)
345
+
346
+ for file in result.files:
347
+ print(file.relpath)
348
+ print(file.content)
349
+ ```
350
+
351
+ ### Convert one file
352
+
353
+ ```python
354
+ from sql2sqlx import convert_file
355
+
356
+ result = convert_file("legacy_sql/orders.sql")
357
+ print(result.report.to_dict())
358
+ ```
359
+
360
+ ### Convert a directory
361
+
362
+ ```python
363
+ from sql2sqlx import ConversionOptions, convert_directory
364
+
365
+ options = ConversionOptions(
366
+ default_project="my-gcp-project",
367
+ default_dataset="analytics",
368
+ insert_strategy="incremental",
369
+ declare_external=True,
370
+ jobs=4,
371
+ )
372
+
373
+ result = convert_directory(
374
+ "legacy_sql",
375
+ "dataform/definitions",
376
+ options,
377
+ )
378
+
379
+ print(result.report.actions_by_type)
380
+ for warning in result.report.warnings:
381
+ print(warning.code, f"{warning.path}:{warning.line}", warning.message)
382
+ ```
383
+
384
+ ### Write an existing result
385
+
386
+ ```python
387
+ from sql2sqlx import convert_directory, write_result
388
+
389
+ result = convert_directory("legacy_sql")
390
+ write_result(result, "dataform/definitions")
391
+ ```
392
+
393
+ ---
394
+
395
+ ## Conversion model
396
+
397
+ The converter produces Dataform actions from SQL statements. Some statements
398
+ map directly to typed Dataform actions. Others must remain operations because
399
+ Dataform cannot represent the original statement without changing semantics.
400
+
401
+ | Input SQL | Default output | Notes |
402
+ | --- | --- | --- |
403
+ | `CREATE [OR REPLACE] TABLE ... AS SELECT ...` | `table` | Metadata is mapped when safely understood. |
404
+ | `CREATE TABLE IF NOT EXISTS ... AS SELECT ...` | `operations` | `--if-not-exists table` converts with a warning because Dataform reruns rebuild targets. |
405
+ | `CREATE [MATERIALIZED] VIEW ... AS SELECT ...` | `view` | Materialized views receive `materialized: true`. |
406
+ | View column lists | `view` or `operations` | Converted by safely pushing aliases into the select list; ambiguous lists fall back. |
407
+ | `INSERT INTO t SELECT ...` | `operations` | `--insert-strategy incremental` converts eligible shapes. |
408
+ | `INSERT INTO t (cols) SELECT ...` | `operations` | Incremental conversion aliases the select list only when arity and names are provably safe. |
409
+ | `INSERT ... VALUES` | `operations` | Preserved verbatim. |
410
+ | `MERGE` | `operations` | `--merge-strategy incremental-when-safe` converts only shape-proven MERGEs. |
411
+ | Plain `CREATE TABLE t (...)` | `operations` | `--plain-create declaration` emits a source declaration instead. |
412
+ | `CREATE TEMP TABLE` | `operations` | Temporary object scope must remain inside BigQuery script/session semantics. |
413
+ | `CREATE EXTERNAL TABLE` | `operations` | External table DDL is preserved; declarations may be a better manual target. |
414
+ | `CREATE SNAPSHOT TABLE` | `operations` | Preserved verbatim. |
415
+ | `CREATE TABLE ... LIKE/CLONE/COPY` | `operations` | No typed Dataform equivalent. |
416
+ | `UPDATE`, `DELETE`, `TRUNCATE`, `DROP`, `ALTER`, `LOAD DATA` | `operations` | Write targets are still tracked for dependency ordering. |
417
+ | Standalone `SELECT` or `WITH` | `operations` | Reported with `ORPHAN_SELECT` for manual review. |
418
+ | Scripts using variables, transactions, temp objects, dynamic SQL, calls, or procedural control flow | one script `operations` action | Keeps shared BigQuery context intact. |
419
+ | Anything unsupported or unknown | `operations` | Original SQL is preserved and warning codes explain why. |
420
+
421
+ ### Incremental conversion details
422
+
423
+ `INSERT ... SELECT` conversion is opt-in because an append operation is not the
424
+ same as a rebuildable table by default. When enabled, `sql2sqlx` emits a
425
+ Dataform incremental action and, unless disabled, marks it `protected: true` to
426
+ reduce full-refresh risk.
427
+
428
+ For `INSERT INTO target (a, b) SELECT x, y ...`, the converter must preserve
429
+ output column names. It rewrites the select list to `SELECT x AS a, y AS b ...`
430
+ only when each item can be reasoned about exactly. It falls back for `*`,
431
+ `SELECT AS STRUCT`, arity mismatches, unsafe aliases, and ambiguous constructs.
432
+
433
+ ### MERGE conversion details
434
+
435
+ `MERGE` statements are preserved by default. With
436
+ `--merge-strategy incremental-when-safe`, only restricted MERGE shapes are
437
+ converted. The converter must be able to identify target keys, update columns,
438
+ insert columns, and a source query that Dataform can represent as an
439
+ incremental body. If any proof fails, the original MERGE remains an operation.
440
+
441
+ ### Metadata mapping details
442
+
443
+ The converter maps BigQuery table metadata when Dataform has a direct field or
444
+ when a raw option can be preserved safely:
445
+
446
+ | BigQuery SQL | SQLX config |
447
+ | --- | --- |
448
+ | `PARTITION BY expr` | `bigquery.partitionBy: "expr"` |
449
+ | `CLUSTER BY a, b` | `bigquery.clusterBy: ["a", "b"]` |
450
+ | `OPTIONS(description = "...")` | `description: "..."` |
451
+ | `OPTIONS(labels = [("k", "v")])` | `bigquery.labels` |
452
+ | `OPTIONS(partition_expiration_days = N)` | `bigquery.partitionExpirationDays` |
453
+ | `OPTIONS(require_partition_filter = true)` | `bigquery.requirePartitionFilter` |
454
+ | Other options | `bigquery.additionalOptions` with raw SQL values |
455
+
456
+ ---
457
+
458
+ ## Dependency linking
459
+
460
+ Dataform dependency correctness is the heart of this project. The linker does
461
+ more than replace strings; it builds a corpus-level model of producers,
462
+ writers, readers, and action names.
463
+
464
+ ### Producer and writer tracking
465
+
466
+ For each table identity, the linker determines:
467
+
468
+ - the action that owns the Dataform output, if any,
469
+ - all statements that write to the table,
470
+ - corpus order of those writers,
471
+ - reads that can be safely rewritten to refs, and
472
+ - reads that must remain literal to avoid changing execution order.
473
+
474
+ Corpus order is deterministic: sorted file path, then statement position within
475
+ the file.
476
+
477
+ ### Reference rewriting rules
478
+
479
+ A table reference is eligible for `${ref(...)}` only when it is a real table
480
+ read and the referenced table is owned or declared in the generated project.
481
+ The scanner avoids rewriting:
482
+
483
+ - CTE names within their visibility scope,
484
+ - range variables and aliases,
485
+ - table-valued function calls,
486
+ - `UNNEST(...)`,
487
+ - target tables of mutating DML,
488
+ - self-references inside a defining query,
489
+ - future same-file creators that would be pulled before earlier reads,
490
+ - wildcard or decorated table expressions that cannot be Dataform
491
+ declarations, and
492
+ - known metadata pseudo-schemas such as `INFORMATION_SCHEMA`.
493
+
494
+ ### Ordering rules
495
+
496
+ Generated dependencies preserve the most important legacy ordering properties:
497
+
498
+ 1. A reader depends on the producer of a table when a safe producer exists.
499
+ 2. A reader also depends on the latest preceding writer when it reads a table
500
+ that has been mutated earlier in corpus order.
501
+ 3. Writers to the same table depend on the previous writer.
502
+ 4. Later same-file writes wait for intervening readers so mutations do not race
503
+ ahead of reads that originally came first.
504
+ 5. Cross-file writer chains are flagged with `ORDER_ASSUMED` because sorted
505
+ file-path order is deterministic but may not match the legacy scheduler.
506
+ 6. Duplicate producers are demoted to operations because Dataform allows one
507
+ owner per target.
508
+ 7. Cycle-producing dependency edges are omitted conservatively and reported.
509
+
510
+ ---
511
+
512
+ ## Conversion report and review workflow
513
+
514
+ Every conversion returns a `ConversionReport`; the CLI can write it with
515
+ `--report report.json`.
516
+
517
+ The report includes:
518
+
519
+ - `files_read`,
520
+ - `statements`,
521
+ - `input_bytes`,
522
+ - `actions_by_type`,
523
+ - `refs_rewritten`,
524
+ - unresolved external references,
525
+ - per-file failures,
526
+ - elapsed seconds, and
527
+ - warnings with `{code, message, path, line}`.
528
+
529
+ Stable warning codes are intended for automation. A production migration can
530
+ fail CI on unexpected new warning codes while allowing reviewed warnings.
531
+
532
+ Recommended migration workflow:
533
+
534
+ 1. Run with conservative defaults and generate a report.
535
+ 2. Review every warning by code and source location.
536
+ 3. Run `dataform compile` on the generated project.
537
+ 4. Compare compiled SQL or BigQuery dry-run plans for critical jobs.
538
+ 5. Opt into incremental, merge, guarded-create, or declaration strategies only
539
+ when the documented preconditions match your warehouse.
540
+ 6. Keep the report as a CI artifact for future regression checks.
541
+
542
+ Important warning categories include:
543
+
544
+ | Code | Meaning |
545
+ | --- | --- |
546
+ | `FALLBACK_OPERATIONS` | Statement could not be safely converted to a typed action. |
547
+ | `CREATE_REPLACE_SEMANTICS` | A create statement was lifted into Dataform rebuild semantics. |
548
+ | `IF_NOT_EXISTS` | Guarded create semantics were preserved or explicitly changed. |
549
+ | `INSERT_INCREMENTAL` | An `INSERT ... SELECT` was converted to incremental. |
550
+ | `MERGE_INCREMENTAL` | A safe MERGE shape was converted to incremental. |
551
+ | `TARGET_SCHEMA_REQUIRED` | Converted incremental logic assumes target schema compatibility. |
552
+ | `DUPLICATE_TARGET` | A later producer was demoted because the target already had an owner. |
553
+ | `ORDER_ASSUMED` | Multiple files write the same table; sorted-path order was used. |
554
+ | `SELF_REFERENCE` | A defining query reads its own target and was left literal. |
555
+ | `FUTURE_CREATOR` | A ref was not generated because the owner appears later in corpus order. |
556
+ | `SCRIPT_FILE` | A file was preserved as a whole script operation. |
557
+ | `DYNAMIC_SIDE_EFFECTS` | Calls or dynamic SQL may hide dependencies. |
558
+ | `ORPHAN_SELECT` | A standalone query needs manual review. |
559
+
560
+ See [`docs/conversion_rules.md`](docs/conversion_rules.md) for the detailed
561
+ rulebook.
562
+
563
+ ---
564
+
565
+ ## Project architecture
566
+
567
+ `sql2sqlx` is organized as a three-phase pipeline.
568
+
569
+ ```text
570
+ SQL files
571
+ |
572
+ v
573
+ Phase 1: per-file parsing
574
+ - lexer: tokens and comment spans
575
+ - splitter: top-level statements
576
+ - parser: action drafts, metadata, warnings
577
+ - reference scanner: candidate table read sites
578
+ |
579
+ v
580
+ Phase 2: corpus linker
581
+ - resolve table identities
582
+ - elect creators and demote duplicates
583
+ - build writer chains
584
+ - choose hasOutput operations
585
+ - rewrite safe refs
586
+ - synthesize external declarations
587
+ - assign action names and dependencies
588
+ |
589
+ v
590
+ Phase 3: emitter
591
+ - render Dataform config blocks
592
+ - apply source-span edits
593
+ - preserve comments and annotations
594
+ - write deterministic .sqlx files
595
+ ```
596
+
597
+ ### Source fidelity invariant
598
+
599
+ Generated SQL bodies are built by applying non-overlapping span edits to slices
600
+ of the original source text. The converter does not reformat or reconstruct SQL
601
+ from tokens. This preserves casing, whitespace, comments, and dialect-specific
602
+ syntax outside explicit edits.
603
+
604
+ SQLX interpolation safety is handled explicitly. Literal `${` inside SQL
605
+ strings or quoted identifiers is emitted through a constant JavaScript
606
+ placeholder so it cannot be interpreted as user-controlled SQLX code, while
607
+ converter-generated `${ref(...)}` and `${self()}` remain active.
608
+
609
+ ---
610
+
611
+ ## Correctness, safety, and limitations
612
+
613
+ ### Guarantees
614
+
615
+ - **No silent best-effort rewrites.** Unsafe statements remain operations with
616
+ warnings.
617
+ - **Character fidelity outside explicit edits.** Untouched SQL text is copied
618
+ from source spans.
619
+ - **Deterministic output.** File order, action naming, and path collision
620
+ handling are deterministic.
621
+ - **Per-file failure isolation.** One bad file is reported without aborting an
622
+ entire directory conversion.
623
+ - **Typed API surface.** The package includes type hints and `py.typed`.
624
+ - **No runtime dependencies.** Deployment into migration tooling remains simple.
625
+
626
+ ### Important limitations
627
+
628
+ - Input is assumed to be BigQuery Standard SQL / GoogleSQL. Other dialects may
629
+ lex or convert incorrectly.
630
+ - Some valid GoogleSQL forms have no Dataform typed equivalent and intentionally
631
+ remain operations.
632
+ - Whole-file scripts stay whole when statement splitting would break shared
633
+ BigQuery context.
634
+ - Cross-file writer order is inferred from sorted paths, not from an external
635
+ scheduler. Review every `ORDER_ASSUMED` warning.
636
+ - `--insert-strategy incremental`, `--merge-strategy incremental-when-safe`,
637
+ and `--if-not-exists table` are explicit migration decisions. Review their
638
+ warnings before using them in production.
639
+ - `--declare-external` can generate Dataform declarations for many external
640
+ tables, but it intentionally skips metadata schemas and table expressions
641
+ that are not valid standalone declarations.
642
+ - The generated project should always be validated with `dataform compile` and,
643
+ for critical paths, BigQuery dry runs or controlled execution.
644
+
645
+ See [`docs/limitations.md`](docs/limitations.md) for more detail.
646
+
647
+ ---
648
+
649
+ ## Performance and scale
650
+
651
+ The converter is designed for large repositories:
652
+
653
+ - lexing is linear-time,
654
+ - per-file phase 1 work can run in multiple processes,
655
+ - linker output is deterministic regardless of worker count,
656
+ - failures are captured per file,
657
+ - source text is retained so emission can remain span-exact.
658
+
659
+ Use `--jobs N` to control parsing parallelism. `--jobs 0` selects an automatic
660
+ worker count based on available CPUs.
661
+
662
+ A benchmark script is included:
663
+
664
+ ```bash
665
+ python examples/06_benchmark.py --files 120 --statements 800
666
+ ```
667
+
668
+ Benchmark results vary by hardware, Python version, and corpus shape. Use the
669
+ included script with representative SQL to size migration runs for your own
670
+ environment.
671
+
672
+ ---
673
+
674
+ ## Development
675
+
676
+ Install development dependencies:
677
+
678
+ ```bash
679
+ python -m pip install -e ".[dev,docs,examples]"
680
+ ```
681
+
682
+ Run the main checks:
683
+
684
+ ```bash
685
+ python -m pytest
686
+ python -m mypy src/sql2sqlx
687
+ python -m black --check .
688
+ python -m isort --check-only .
689
+ ```
690
+
691
+ Build documentation locally:
692
+
693
+ ```bash
694
+ python -m pip install -e ".[docs]"
695
+ make -C docs html
696
+ ```
697
+
698
+ Contribution expectations:
699
+
700
+ - preserve the conservative conversion policy,
701
+ - add or update tests for every conversion rule change,
702
+ - keep warning codes stable unless a breaking change is intentional,
703
+ - prefer source-span edits over SQL reserialization,
704
+ - document new strategies and warnings in both README and `docs/`, and
705
+ - run the full local check suite before opening a PR.
706
+
707
+ ---
708
+
709
+ ## Citation
710
+
711
+ If you use `sql2sqlx` in migration work, publications, benchmarks, or internal
712
+ technical reports, please cite the project metadata in [`CITATION.cff`](CITATION.cff).
713
+
714
+ ```bibtex
715
+ @software{sarkar_sql2sqlx,
716
+ title = {sql2sqlx: Conservative BigQuery SQL to Dataform SQLX migration tooling},
717
+ author = {Soumyadip Sarkar},
718
+ url = {https://github.com/neuralsorcerer/sql2sqlx},
719
+ license = {Apache-2.0}
720
+ }
721
+ ```
722
+
723
+ ## License and trademarks
724
+
725
+ `sql2sqlx` is licensed under the [Apache License 2.0](LICENSE).
726
+
727
+ This project is not affiliated with or endorsed by Google. BigQuery,
728
+ GoogleSQL, and Dataform are trademarks of Google LLC.
729
+
730
+ ## References
731
+
732
+ - [Dataform documentation](https://cloud.google.com/dataform/docs)
733
+ - [GoogleSQL for BigQuery reference](https://cloud.google.com/bigquery/docs/reference/standard-sql)