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.
- sql2sqlx/__init__.py +83 -0
- sql2sqlx/__main__.py +14 -0
- sql2sqlx/cli.py +323 -0
- sql2sqlx/converter.py +1241 -0
- sql2sqlx/emitter.py +275 -0
- sql2sqlx/errors.py +93 -0
- sql2sqlx/keywords.py +169 -0
- sql2sqlx/lexer.py +494 -0
- sql2sqlx/model.py +487 -0
- sql2sqlx/parser.py +2193 -0
- sql2sqlx/py.typed +2 -0
- sql2sqlx/refs.py +902 -0
- sql2sqlx/splitter.py +360 -0
- sql2sqlx/version.py +15 -0
- sql2sqlx-0.1.0.dist-info/METADATA +733 -0
- sql2sqlx-0.1.0.dist-info/RECORD +20 -0
- sql2sqlx-0.1.0.dist-info/WHEEL +5 -0
- sql2sqlx-0.1.0.dist-info/entry_points.txt +2 -0
- sql2sqlx-0.1.0.dist-info/licenses/LICENSE +202 -0
- sql2sqlx-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/releases)
|
|
60
|
+
[](https://pypi.org/project/sql2sqlx/)
|
|
61
|
+
[](https://www.python.org/downloads/)
|
|
62
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml?query=branch%3Amain)
|
|
63
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml?query=branch%3Amain)
|
|
64
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml?query=branch%3Amain)
|
|
65
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml?query=branch%3Amain)
|
|
66
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml?query=branch%3Amain)
|
|
67
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml?query=branch%3Amain)
|
|
68
|
+
[](https://neuralsorcerer.github.io/sql2sqlx/)
|
|
69
|
+
[](LICENSE)
|
|
70
|
+
[](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)
|