sql2sqlx 0.1.0__tar.gz → 0.1.1__tar.gz
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-0.1.0 → sql2sqlx-0.1.1}/MANIFEST.in +0 -1
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/PKG-INFO +2 -3
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/README.md +1 -1
- sql2sqlx-0.1.1/docs/api.md +129 -0
- sql2sqlx-0.1.1/docs/architecture.md +170 -0
- sql2sqlx-0.1.1/docs/cli.md +211 -0
- sql2sqlx-0.1.1/docs/concepts.md +255 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/conversion_rules.md +151 -20
- sql2sqlx-0.1.1/docs/dependencies.md +236 -0
- sql2sqlx-0.1.1/docs/examples.md +99 -0
- sql2sqlx-0.1.1/docs/index.md +81 -0
- sql2sqlx-0.1.1/docs/installation.md +93 -0
- sql2sqlx-0.1.1/docs/limitations.md +103 -0
- sql2sqlx-0.1.1/docs/migration_guide.md +173 -0
- sql2sqlx-0.1.1/docs/quickstart.md +145 -0
- sql2sqlx-0.1.1/docs/report.md +205 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/pyproject.toml +0 -1
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/converter.py +16 -4
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/emitter.py +59 -21
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/version.py +1 -1
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx.egg-info/PKG-INFO +2 -3
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx.egg-info/SOURCES.txt +6 -3
- sql2sqlx-0.1.1/tests/fixtures/dataform_compile/13_sqlx_lexical_safety.sql +12 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/test_correctness_regressions.py +50 -1
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/test_parser_refs.py +1 -1
- sql2sqlx-0.1.0/CHANGELOG.md +0 -12
- sql2sqlx-0.1.0/docs/api.md +0 -78
- sql2sqlx-0.1.0/docs/architecture.md +0 -105
- sql2sqlx-0.1.0/docs/changelog.md +0 -2
- sql2sqlx-0.1.0/docs/cli.md +0 -59
- sql2sqlx-0.1.0/docs/examples.md +0 -27
- sql2sqlx-0.1.0/docs/index.md +0 -43
- sql2sqlx-0.1.0/docs/installation.md +0 -30
- sql2sqlx-0.1.0/docs/limitations.md +0 -56
- sql2sqlx-0.1.0/docs/quickstart.md +0 -75
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/CITATION.cff +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/CONTRIBUTING.md +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/LICENSE +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/Makefile +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/conf.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/contributing.md +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/notebooks/01_in_memory_and_file_conversion.ipynb +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/notebooks/02_directory_options_and_dependencies.ipynb +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/notebooks/03_reports_benchmark_and_review_gates.ipynb +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/docs/requirements.txt +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/01_convert_string.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/02_convert_file.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/03_convert_directory.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/04_custom_options.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/05_programmatic_report.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/06_benchmark.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/README.md +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/_notebook_benchmark_helper.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/notebooks/01_in_memory_and_file_conversion.ipynb +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/notebooks/02_directory_options_and_dependencies.ipynb +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/notebooks/03_reports_benchmark_and_review_gates.ipynb +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/maintenance/backfill_script.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/maintenance/cleanup.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/maintenance/dedupe_merge.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/marts/customer_orders.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/marts/order_facts.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/sources/raw_events.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/staging/stg_customers.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/examples/sql/staging/stg_orders.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/setup.cfg +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/__init__.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/__main__.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/cli.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/errors.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/keywords.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/lexer.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/model.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/parser.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/py.typed +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/refs.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx/splitter.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx.egg-info/dependency_links.txt +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx.egg-info/entry_points.txt +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx.egg-info/requires.txt +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/src/sql2sqlx.egg-info/top_level.txt +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/01_source.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/02_insert.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/03_merge.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/04_transaction.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/05_procedure.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/06_cycle_a.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/07_cycle_b.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/08_plain_create.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/09_guarded_view.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/10_cross_project_dependency.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/11_clone.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/fixtures/dataform_compile/12_literal_sqlx.sql +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/test_end_to_end.py +0 -0
- {sql2sqlx-0.1.0 → sql2sqlx-0.1.1}/tests/test_lexer_splitter.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql2sqlx
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Convert BigQuery SQL into Dataform SQLX with conservative, dependency-aware migration tooling.
|
|
5
5
|
Author-email: Soumyadip Sarkar <soumyadip@soumyadipsarkar.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -8,7 +8,6 @@ Project-URL: Homepage, https://github.com/neuralsorcerer/sql2sqlx
|
|
|
8
8
|
Project-URL: Documentation, https://neuralsorcerer.github.io/sql2sqlx/
|
|
9
9
|
Project-URL: Source, https://github.com/neuralsorcerer/sql2sqlx
|
|
10
10
|
Project-URL: Issues, https://github.com/neuralsorcerer/sql2sqlx/issues
|
|
11
|
-
Project-URL: Changelog, https://github.com/neuralsorcerer/sql2sqlx/blob/main/CHANGELOG.md
|
|
12
11
|
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
12
|
Classifier: Development Status :: 4 - Beta
|
|
14
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -56,13 +55,13 @@ BigQuery SQL -> Dataform SQLX migration tooling.
|
|
|
56
55
|
|
|
57
56
|
<div align="center">
|
|
58
57
|
|
|
59
|
-
[](https://github.com/neuralsorcerer/sql2sqlx/releases)
|
|
60
58
|
[](https://pypi.org/project/sql2sqlx/)
|
|
61
59
|
[](https://www.python.org/downloads/)
|
|
62
60
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml?query=branch%3Amain)
|
|
63
61
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml?query=branch%3Amain)
|
|
64
62
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml?query=branch%3Amain)
|
|
65
63
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml?query=branch%3Amain)
|
|
64
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/codeql.yml?query=branch%3Amain)
|
|
66
65
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml?query=branch%3Amain)
|
|
67
66
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml?query=branch%3Amain)
|
|
68
67
|
[](https://neuralsorcerer.github.io/sql2sqlx/)
|
|
@@ -9,13 +9,13 @@ BigQuery SQL -> Dataform SQLX migration tooling.
|
|
|
9
9
|
|
|
10
10
|
<div align="center">
|
|
11
11
|
|
|
12
|
-
[](https://github.com/neuralsorcerer/sql2sqlx/releases)
|
|
13
12
|
[](https://pypi.org/project/sql2sqlx/)
|
|
14
13
|
[](https://www.python.org/downloads/)
|
|
15
14
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_ubuntu.yml?query=branch%3Amain)
|
|
16
15
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_windows.yml?query=branch%3Amain)
|
|
17
16
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/test_macos.yml?query=branch%3Amain)
|
|
18
17
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/lints.yml?query=branch%3Amain)
|
|
18
|
+
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/codeql.yml?query=branch%3Amain)
|
|
19
19
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/dataform_compile.yml?query=branch%3Amain)
|
|
20
20
|
[](https://github.com/neuralsorcerer/sql2sqlx/actions/workflows/build.yml?query=branch%3Amain)
|
|
21
21
|
[](https://neuralsorcerer.github.io/sql2sqlx/)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Everything in this section is generated from the package's docstrings. The
|
|
4
|
+
narrative below orients you before the autodoc entries.
|
|
5
|
+
|
|
6
|
+
The public surface is small and stable. Everything importable from the
|
|
7
|
+
top-level `sql2sqlx` package (listed in its `__all__`) is what semantic
|
|
8
|
+
versioning covers.
|
|
9
|
+
|
|
10
|
+
## Choosing an entry point
|
|
11
|
+
|
|
12
|
+
Three functions convert; two helpers support them.
|
|
13
|
+
|
|
14
|
+
:::{list-table}
|
|
15
|
+
:header-rows: 1
|
|
16
|
+
:widths: 26 30 44
|
|
17
|
+
|
|
18
|
+
* - Function
|
|
19
|
+
- Input
|
|
20
|
+
- Writes to disk?
|
|
21
|
+
* - `convert_string(sql, options=None, name="input.sql")`
|
|
22
|
+
- a SQL string (one or many statements)
|
|
23
|
+
- No - returns a result you can inspect or `write_result`.
|
|
24
|
+
* - `convert_file(path, options=None)`
|
|
25
|
+
- one `.sql` file
|
|
26
|
+
- No - returns a result.
|
|
27
|
+
* - `convert_directory(input_dir, output_dir=None, options=None)`
|
|
28
|
+
- a directory tree (scanned by `include_glob`)
|
|
29
|
+
- **Only if** `output_dir` is given.
|
|
30
|
+
:::
|
|
31
|
+
|
|
32
|
+
`parse_source(...)` exposes just Phase 1 (lex + split + classify) for one
|
|
33
|
+
source, returning a `ParsedFile`; `write_result(result, output_dir)` writes a
|
|
34
|
+
`ConversionResult` to disk (and refuses paths that would escape the
|
|
35
|
+
destination). All three converters run the same three-phase pipeline described
|
|
36
|
+
in [architecture](architecture.md); `convert_directory` additionally
|
|
37
|
+
parallelizes parsing across a process pool.
|
|
38
|
+
|
|
39
|
+
```{eval-rst}
|
|
40
|
+
.. autofunction:: sql2sqlx.convert_string
|
|
41
|
+
.. autofunction:: sql2sqlx.convert_file
|
|
42
|
+
.. autofunction:: sql2sqlx.convert_directory
|
|
43
|
+
.. autofunction:: sql2sqlx.write_result
|
|
44
|
+
.. autofunction:: sql2sqlx.parse_source
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Options and results
|
|
48
|
+
|
|
49
|
+
`ConversionOptions` holds every knob; its defaults preserve semantics (see the
|
|
50
|
+
[CLI reference](cli.md) for the flag-to-field mapping). Its `__post_init__`
|
|
51
|
+
**validates and normalizes** on construction: the string enum values
|
|
52
|
+
(`"incremental"`, `"mirror"`, ...) are accepted as well as the enum members, so
|
|
53
|
+
options are easy to build from JSON or config files, and invalid values raise
|
|
54
|
+
`ValueError` immediately rather than silently mis-selecting a strategy.
|
|
55
|
+
|
|
56
|
+
A conversion returns a `ConversionResult` with two attributes: `files` (the
|
|
57
|
+
generated `SqlxFile` objects, in deterministic sorted-path order) and `report`
|
|
58
|
+
(a [`ConversionReport`](report.md)). All of these are plain, picklable
|
|
59
|
+
dataclasses.
|
|
60
|
+
|
|
61
|
+
```{eval-rst}
|
|
62
|
+
.. autoclass:: sql2sqlx.ConversionOptions
|
|
63
|
+
:members:
|
|
64
|
+
|
|
65
|
+
.. autoclass:: sql2sqlx.InsertStrategy
|
|
66
|
+
:members:
|
|
67
|
+
.. autoclass:: sql2sqlx.MergeStrategy
|
|
68
|
+
:members:
|
|
69
|
+
.. autoclass:: sql2sqlx.PlainCreateStrategy
|
|
70
|
+
:members:
|
|
71
|
+
.. autoclass:: sql2sqlx.IfNotExistsStrategy
|
|
72
|
+
:members:
|
|
73
|
+
.. autoclass:: sql2sqlx.Layout
|
|
74
|
+
:members:
|
|
75
|
+
|
|
76
|
+
.. autoclass:: sql2sqlx.ConversionResult
|
|
77
|
+
:members:
|
|
78
|
+
.. autoclass:: sql2sqlx.ConversionReport
|
|
79
|
+
:members:
|
|
80
|
+
.. autoclass:: sql2sqlx.ReportWarning
|
|
81
|
+
:members:
|
|
82
|
+
.. autoclass:: sql2sqlx.SqlxFile
|
|
83
|
+
:members:
|
|
84
|
+
.. autoclass:: sql2sqlx.ActionType
|
|
85
|
+
:members:
|
|
86
|
+
.. autoclass:: sql2sqlx.TableName
|
|
87
|
+
:members:
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Errors
|
|
91
|
+
|
|
92
|
+
Every exception raised deliberately by the library derives from
|
|
93
|
+
`Sql2SqlxError`, so a single `except Sql2SqlxError` catches them all.
|
|
94
|
+
Location-aware errors (`LexError`, `SplitError`) carry 1-based `line` and
|
|
95
|
+
`column` attributes. Note that *unsupported-but-valid* SQL never raises - it
|
|
96
|
+
falls back to an `operations` action and a warning; the exceptions are for
|
|
97
|
+
genuinely broken input (unreadable files, lexer failures). During directory
|
|
98
|
+
conversion, a per-file failure is captured in `report.failures` rather than
|
|
99
|
+
aborting the run.
|
|
100
|
+
|
|
101
|
+
```{eval-rst}
|
|
102
|
+
.. automodule:: sql2sqlx.errors
|
|
103
|
+
:members:
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Internals
|
|
107
|
+
|
|
108
|
+
The internal modules are stable enough to build on, but the public surface
|
|
109
|
+
above is what semantic versioning covers.
|
|
110
|
+
|
|
111
|
+
```{eval-rst}
|
|
112
|
+
.. automodule:: sql2sqlx.lexer
|
|
113
|
+
:members: tokenize, comment_spans, unquote_identifier, Token, LineIndex
|
|
114
|
+
|
|
115
|
+
.. automodule:: sql2sqlx.splitter
|
|
116
|
+
:members:
|
|
117
|
+
|
|
118
|
+
.. automodule:: sql2sqlx.parser
|
|
119
|
+
:members: classify_statement
|
|
120
|
+
|
|
121
|
+
.. automodule:: sql2sqlx.refs
|
|
122
|
+
:members: scan_ref_sites, parse_table_path, PathMatch
|
|
123
|
+
|
|
124
|
+
.. automodule:: sql2sqlx.emitter
|
|
125
|
+
:members:
|
|
126
|
+
|
|
127
|
+
.. automodule:: sql2sqlx.cli
|
|
128
|
+
:members: main, build_arg_parser
|
|
129
|
+
```
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
sql2sqlx is a three-phase pipeline built on one invariant:
|
|
4
|
+
|
|
5
|
+
> **Untouched SQL is preserved character-for-character after decoding.** Output
|
|
6
|
+
> bodies are produced by applying *span edits* to slices of the original text -
|
|
7
|
+
> tokens are never re-serialized, so formatting, casing and inline comments
|
|
8
|
+
> survive exactly.
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
.sql files
|
|
12
|
+
|
|
|
13
|
+
v
|
|
14
|
+
Phase 1: parse each file, in parallel when possible
|
|
15
|
+
- lexer: tokens and comment spans
|
|
16
|
+
- splitter: top-level statements
|
|
17
|
+
- classifier: action drafts and metadata
|
|
18
|
+
- reference scanner: candidate table read sites
|
|
19
|
+
|
|
|
20
|
+
v
|
|
21
|
+
Phase 2: link corpus metadata
|
|
22
|
+
- creators and duplicate demotion
|
|
23
|
+
- writer chains and ordering dependencies
|
|
24
|
+
- hasOutput election
|
|
25
|
+
- ref resolution and declaration synthesis
|
|
26
|
+
- action names and output paths
|
|
27
|
+
|
|
|
28
|
+
v
|
|
29
|
+
Phase 3: emit SQLX
|
|
30
|
+
- config blocks
|
|
31
|
+
- source-span edits
|
|
32
|
+
- provenance and original comments
|
|
33
|
+
- .sqlx files
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The phases map onto the module layout: `lexer`, `splitter`, `parser` and
|
|
37
|
+
`refs` do Phase 1; `converter._Linker` does Phase 2; `emitter` does Phase 3.
|
|
38
|
+
The shared, picklable data model lives in `model` so parsed results can cross
|
|
39
|
+
process boundaries.
|
|
40
|
+
|
|
41
|
+
## Phase 1 - lexing, splitting, classifying
|
|
42
|
+
|
|
43
|
+
This phase is **per file** and has no cross-file dependencies, which is what
|
|
44
|
+
makes it parallelizable.
|
|
45
|
+
|
|
46
|
+
### Lexer (`sql2sqlx.lexer`)
|
|
47
|
+
|
|
48
|
+
A single compiled regex alternation, executed by CPython's C regex engine,
|
|
49
|
+
implements the GoogleSQL lexical specification. Every alternative is written to
|
|
50
|
+
match in strictly linear time (no ambiguous nested quantifiers, hence no
|
|
51
|
+
catastrophic backtracking), giving high throughput while remaining exactly
|
|
52
|
+
character-accurate. It recognizes:
|
|
53
|
+
|
|
54
|
+
- line comments (`--` and `#`) and non-nested block comments (`/* ... */`,
|
|
55
|
+
ending at the first `*/`, exactly as GoogleSQL defines them);
|
|
56
|
+
- all string/bytes/raw literal forms, including triple-quoted strings and
|
|
57
|
+
every valid `r`/`b` prefix combination and case, with GoogleSQL's exact
|
|
58
|
+
backslash-escape rules (a raw literal can never end in an odd number of
|
|
59
|
+
backslashes, and so on);
|
|
60
|
+
- backtick-quoted identifiers with escapes and embedded dots;
|
|
61
|
+
- numeric literals, named/positional query parameters and system variables,
|
|
62
|
+
and every GoogleSQL operator and punctuation token.
|
|
63
|
+
|
|
64
|
+
Tokens carry `(kind, text, start, end)` with character offsets into the
|
|
65
|
+
original text - the anchor for span-exact emission later. Unterminated strings,
|
|
66
|
+
backtick identifiers and block comments raise a `LexError` with an exact
|
|
67
|
+
1-based line and column. Comment spans are captured in the **same pass** (via
|
|
68
|
+
an output list), so no file is ever lexed twice.
|
|
69
|
+
|
|
70
|
+
### Splitter (`sql2sqlx.splitter`)
|
|
71
|
+
|
|
72
|
+
Splits the token stream into top-level statements on `;`, but only at
|
|
73
|
+
parenthesis depth 0 **and** with an empty *block-frame stack*. Frames model
|
|
74
|
+
BigQuery scripting constructs - `BEGIN ... END`, `IF ... END IF`, loops, and
|
|
75
|
+
`CASE` - and openers are recognized only in statement position. A bounded
|
|
76
|
+
lookahead distinguishes the scripting `IF cond THEN` from the `IF(a, b, c)`
|
|
77
|
+
function, and `BEGIN [TRANSACTION];` is treated as a statement, not a block
|
|
78
|
+
opener. The splitter is deliberately lenient: a mismatched `END` is tolerated
|
|
79
|
+
rather than fatal, so odd-but-valid scripts still split sensibly.
|
|
80
|
+
|
|
81
|
+
### Classifier (`sql2sqlx.parser`)
|
|
82
|
+
|
|
83
|
+
Maps each statement to an `ActionDraft`: its action type, target table,
|
|
84
|
+
already-known `config` entries, the metadata extracted from
|
|
85
|
+
`PARTITION BY`/`CLUSTER BY`/`OPTIONS`, and any classification warnings. This is
|
|
86
|
+
where the opt-in analyses live:
|
|
87
|
+
|
|
88
|
+
- the select-list aliasing analysis for `INSERT -> incremental` (proving each
|
|
89
|
+
output name is exactly determinable, or refusing);
|
|
90
|
+
- the MERGE shape proof (all six obligations) plus its target-schema
|
|
91
|
+
precondition warning;
|
|
92
|
+
- detection of the forms that must fall back (`LIKE`/`CLONE`/`COPY`, column-list
|
|
93
|
+
CTAS, temp/external/snapshot tables, guarded creates, `INSERT ... VALUES`).
|
|
94
|
+
|
|
95
|
+
### Reference scanner (`sql2sqlx.refs`)
|
|
96
|
+
|
|
97
|
+
A context-tracked scan for physical table paths in read positions - after
|
|
98
|
+
`FROM`, `JOIN`, from-list commas and `MERGE ... USING` - carrying per-query
|
|
99
|
+
CTE and range-variable scopes. It models recursive versus ordered CTE
|
|
100
|
+
visibility, correlated scalar subqueries versus non-lateral `FROM` subqueries,
|
|
101
|
+
set-operation branches, implicit aliases and `UNNEST` aliases; `EXTRACT(x FROM
|
|
102
|
+
col)` stays inert, and dashed-project adjacency and target skip-spans (e.g. the
|
|
103
|
+
target of `DELETE FROM x`) are handled without ever re-serializing SQL. The
|
|
104
|
+
output is a list of candidate `RefSite`s (character spans plus the decoded,
|
|
105
|
+
still-unresolved `TableName`).
|
|
106
|
+
|
|
107
|
+
### Whole-file scripts
|
|
108
|
+
|
|
109
|
+
Files that use transactions, temporary objects, variables, procedural control
|
|
110
|
+
flow, or dynamic side effects cannot be safely split into independent Dataform
|
|
111
|
+
actions without changing control-flow or scope semantics, so they collapse
|
|
112
|
+
into a **single whole-file `operations` draft**. A conservative nested scan
|
|
113
|
+
still harvests persistent reference sites and write targets from inside the
|
|
114
|
+
script, so dependency wiring works, while temporary names are kept from
|
|
115
|
+
leaking into the corpus.
|
|
116
|
+
|
|
117
|
+
## Phase 2 - the linker
|
|
118
|
+
|
|
119
|
+
`_Linker` runs once, single-process, over the lightweight drafts that Phase 1
|
|
120
|
+
produced (parallel workers ship back picklable `ParsedFile`/`ActionDraft`
|
|
121
|
+
objects). In order:
|
|
122
|
+
|
|
123
|
+
1. one **creator** per resolved target `(project, dataset, table)`; a second
|
|
124
|
+
producer of the same target is demoted to a verbatim `operations` action
|
|
125
|
+
(`DUPLICATE_TARGET`), because Dataform allows exactly one owner per target;
|
|
126
|
+
2. **writer chains** per table in corpus order (sorted relative path, then
|
|
127
|
+
statement position): every writer depends on its predecessor, and a
|
|
128
|
+
same-file writer also waits for intervening readers, so
|
|
129
|
+
`DROP x; CREATE x; UPDATE x` keeps its order;
|
|
130
|
+
3. **`hasOutput` election**: an ownerless `operations` action that actually
|
|
131
|
+
creates its configured output (and only such an action - never mutating
|
|
132
|
+
DML) is elected as a producer, gaining `hasOutput: true` and a `${self()}`
|
|
133
|
+
rewrite of its own target path;
|
|
134
|
+
4. **reference resolution** against the `--default-project/--default-dataset`
|
|
135
|
+
values, rewriting resolved read sites to `${ref(...)}` and adding reader
|
|
136
|
+
dependencies on each table's latest preceding writer;
|
|
137
|
+
5. **future-owner protection** that leaves an earlier read literal
|
|
138
|
+
(`FUTURE_CREATOR`) rather than reversing corpus order, plus **cycle
|
|
139
|
+
detection** that omits an unsafe edge (`DEPENDENCY_CYCLE`) rather than
|
|
140
|
+
emitting an uncompilable graph;
|
|
141
|
+
6. unique action naming and collision-free output paths - deterministic, so
|
|
142
|
+
two runs produce identical bytes.
|
|
143
|
+
|
|
144
|
+
See the [dependency model](dependencies.md) for the full semantics of each
|
|
145
|
+
edge and the warnings they raise.
|
|
146
|
+
|
|
147
|
+
## Phase 3 - emission
|
|
148
|
+
|
|
149
|
+
`sql2sqlx.emitter` renders each linked draft to a `.sqlx` file: it builds the
|
|
150
|
+
canonical `config { ... }` block (fixed key order), applies the accumulated
|
|
151
|
+
span edits to a slice of the original source text, emits literal `${` and
|
|
152
|
+
SQLX-unsafe comments through constant placeholders (while inserted
|
|
153
|
+
`${ref()}`/`${self()}` stay active), strips one trailing semicolon, and
|
|
154
|
+
attaches the provenance comment plus the statement's own leading comments. The
|
|
155
|
+
result is UTF-8 with `\n` newlines. See [core concepts](concepts.md) for the
|
|
156
|
+
output anatomy.
|
|
157
|
+
|
|
158
|
+
## Performance model
|
|
159
|
+
|
|
160
|
+
Lexing dominates runtime, and it runs at C speed via the master regex pattern.
|
|
161
|
+
Directory conversion parallelizes Phase 1 across a `ProcessPoolExecutor` sized
|
|
162
|
+
by `--jobs` (`0` = one worker per CPU); Phase 2 and Phase 3 are a fast
|
|
163
|
+
single-process metadata pass. Because parsing is embarrassingly parallel and
|
|
164
|
+
linking is cheap, throughput scales with core count on large corpora.
|
|
165
|
+
|
|
166
|
+
Measured on this package's benchmark (`examples/06_benchmark.py`, 120 files /
|
|
167
|
+
1,056,600 lines / 31.7 MB, single core): **43.8 s ~ 24,100 lines/s**, with
|
|
168
|
+
~96k actions emitted and ~96k references rewritten. Memory is proportional to
|
|
169
|
+
corpus size, because the original text of each file is retained for span-exact
|
|
170
|
+
emission.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
sql2sqlx INPUT [-o DIR] [options]
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
The CLI is a thin veneer over [`convert_file`](api.md) and
|
|
8
|
+
[`convert_directory`](api.md). It can also be run as a module:
|
|
9
|
+
`python -m sql2sqlx ...`.
|
|
10
|
+
|
|
11
|
+
## Input and output modes
|
|
12
|
+
|
|
13
|
+
`INPUT` is either a single `.sql` **file** or a **directory** of `.sql` files.
|
|
14
|
+
What happens next depends on the input kind and whether `--output` is given:
|
|
15
|
+
|
|
16
|
+
:::{list-table}
|
|
17
|
+
:header-rows: 1
|
|
18
|
+
:widths: 22 20 58
|
|
19
|
+
|
|
20
|
+
* - Input
|
|
21
|
+
- `--output`
|
|
22
|
+
- Behavior
|
|
23
|
+
* - file
|
|
24
|
+
- omitted
|
|
25
|
+
- The generated SQLX is printed to **stdout**; the run summary goes to
|
|
26
|
+
**stderr**, so `sql2sqlx model.sql > out.sqlx` is clean.
|
|
27
|
+
* - file
|
|
28
|
+
- given
|
|
29
|
+
- The generated file(s) are written beneath the output directory.
|
|
30
|
+
* - directory
|
|
31
|
+
- given
|
|
32
|
+
- The converted tree is written beneath the output directory.
|
|
33
|
+
* - directory
|
|
34
|
+
- omitted
|
|
35
|
+
- **Error**, unless `--dry-run` is passed (a directory has no meaningful
|
|
36
|
+
stdout form).
|
|
37
|
+
:::
|
|
38
|
+
|
|
39
|
+
When a single file produces **multiple** actions and is printed to stdout, the
|
|
40
|
+
files are separated by a header line so the stream stays parseable:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
-- ===== active.sqlx =====
|
|
44
|
+
config { ... }
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
-- ===== customers.sqlx =====
|
|
48
|
+
config { ... }
|
|
49
|
+
...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
:::{list-table}
|
|
55
|
+
:header-rows: 1
|
|
56
|
+
:widths: 34 14 52
|
|
57
|
+
|
|
58
|
+
* - Flag
|
|
59
|
+
- Default
|
|
60
|
+
- Meaning
|
|
61
|
+
* - `-o, --output DIR`
|
|
62
|
+
- -
|
|
63
|
+
- Where to write `.sqlx` files (typically your Dataform `definitions/`).
|
|
64
|
+
* - `--report FILE`
|
|
65
|
+
- -
|
|
66
|
+
- Write the full JSON [conversion report](report.md) to `FILE`.
|
|
67
|
+
* - `--default-project ID`
|
|
68
|
+
- -
|
|
69
|
+
- Project assumed for unqualified table paths (reference resolution only).
|
|
70
|
+
* - `--default-dataset ID`
|
|
71
|
+
- -
|
|
72
|
+
- Dataset assumed for unqualified paths; also resolves single-part
|
|
73
|
+
references to qualified producers.
|
|
74
|
+
* - `--default-location LOCATION`
|
|
75
|
+
- `US`
|
|
76
|
+
- Location written into a scaffolded `workflow_settings.yaml`.
|
|
77
|
+
* - `--layout {mirror,flat}`
|
|
78
|
+
- `mirror`
|
|
79
|
+
- Mirror the input directory tree, or flatten all output into the root.
|
|
80
|
+
* - `--insert-strategy {incremental,operations}`
|
|
81
|
+
- `operations`
|
|
82
|
+
- How `INSERT ... SELECT` converts.
|
|
83
|
+
* - `--merge-strategy {operations,incremental-when-safe}`
|
|
84
|
+
- `operations`
|
|
85
|
+
- How `MERGE` converts.
|
|
86
|
+
* - `--plain-create {operations,declaration}`
|
|
87
|
+
- `operations`
|
|
88
|
+
- How `CREATE TABLE` **without** `AS SELECT` converts.
|
|
89
|
+
* - `--if-not-exists {table,operations}`
|
|
90
|
+
- `operations`
|
|
91
|
+
- How guarded `CREATE TABLE/VIEW IF NOT EXISTS ... AS` converts.
|
|
92
|
+
* - `--declare-external`
|
|
93
|
+
- off
|
|
94
|
+
- Emit `declaration` actions under `sources/` for referenced-but-not-produced
|
|
95
|
+
tables, and `ref()` them too.
|
|
96
|
+
* - `--no-protected`
|
|
97
|
+
- off
|
|
98
|
+
- Do **not** mark converted incrementals `protected: true`.
|
|
99
|
+
* - `--no-annotate`
|
|
100
|
+
- off
|
|
101
|
+
- Omit the `-- source: file:line` provenance comments.
|
|
102
|
+
* - `--tags A,B`
|
|
103
|
+
- -
|
|
104
|
+
- Comma-separated Dataform tags added to every generated action.
|
|
105
|
+
* - `--include GLOB`
|
|
106
|
+
- `*.sql`
|
|
107
|
+
- Filename glob for directory scans (must stay within the input directory).
|
|
108
|
+
* - `--encoding ENC`
|
|
109
|
+
- `utf-8`
|
|
110
|
+
- Encoding used to read input files (must be a text codec).
|
|
111
|
+
* - `-j, --jobs N`
|
|
112
|
+
- `0` (auto)
|
|
113
|
+
- Parser worker processes; `0` = one per CPU, `1` = no multiprocessing.
|
|
114
|
+
* - `--dry-run`
|
|
115
|
+
- off
|
|
116
|
+
- Convert and report, but write no `.sqlx` files.
|
|
117
|
+
* - `--overwrite`
|
|
118
|
+
- off
|
|
119
|
+
- Allow writing into an output directory that already contains `.sqlx` files.
|
|
120
|
+
* - `--init-project`
|
|
121
|
+
- off
|
|
122
|
+
- Also scaffold a `workflow_settings.yaml`.
|
|
123
|
+
* - `-q, --quiet`
|
|
124
|
+
- -
|
|
125
|
+
- Suppress the summary.
|
|
126
|
+
* - `-v, --verbose`
|
|
127
|
+
- -
|
|
128
|
+
- Also list every warning and generated file.
|
|
129
|
+
* - `--version`
|
|
130
|
+
- -
|
|
131
|
+
- Print the version and exit.
|
|
132
|
+
:::
|
|
133
|
+
|
|
134
|
+
### Strategy flags in depth
|
|
135
|
+
|
|
136
|
+
The four strategy flags each default to the **semantics-preserving** option
|
|
137
|
+
and let you opt into more idiomatic Dataform structure where you accept the
|
|
138
|
+
trade-off. Their full rules, proof obligations and the warnings they emit are
|
|
139
|
+
documented in [conversion rules](conversion_rules.md):
|
|
140
|
+
|
|
141
|
+
- `--insert-strategy incremental` produces `type: "incremental"` from
|
|
142
|
+
`INSERT ... SELECT`, marks it `protected: true` (unless `--no-protected`),
|
|
143
|
+
and reports `INSERT_INCREMENTAL`.
|
|
144
|
+
- `--merge-strategy incremental-when-safe` converts only MERGEs whose shape is
|
|
145
|
+
*provably* equivalent to an incremental upsert; everything else stays
|
|
146
|
+
`operations` with a `MERGE_FALLBACK` reason.
|
|
147
|
+
- `--plain-create declaration` turns bodiless `CREATE TABLE (schema)` DDL into
|
|
148
|
+
a `declaration` (drops the DDL; use for externally managed tables).
|
|
149
|
+
- `--if-not-exists table` converts guarded `... IF NOT EXISTS ... AS`
|
|
150
|
+
statements to typed actions and reports the lost create-if-absent guard.
|
|
151
|
+
|
|
152
|
+
### The `--init-project` scaffold
|
|
153
|
+
|
|
154
|
+
`--init-project` writes a Dataform-core 3.x `workflow_settings.yaml`. If the
|
|
155
|
+
output directory is named `definitions`, the file is placed in its **parent**
|
|
156
|
+
(the project root); otherwise it is placed inside the output directory. It is
|
|
157
|
+
**never** silently overwritten - pass `--overwrite` to replace an existing one.
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
defaultProject: "my-gcp-project" # from --default-project, else "your-gcp-project"
|
|
161
|
+
defaultLocation: "US" # from --default-location
|
|
162
|
+
defaultDataset: "analytics" # from --default-dataset, else "dataform"
|
|
163
|
+
defaultAssertionDataset: "dataform_assertions"
|
|
164
|
+
dataformCoreVersion: "3.0.61"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### The overwrite guard
|
|
168
|
+
|
|
169
|
+
When writing to a directory, if the output path already contains any `.sqlx`
|
|
170
|
+
files and `--overwrite` is not set, `sql2sqlx` stops with exit code `2` rather
|
|
171
|
+
than risk clobbering hand-written actions. Because conversion is deterministic,
|
|
172
|
+
re-running with `--overwrite` in CI produces the same tree and a clean diff.
|
|
173
|
+
|
|
174
|
+
## Exit codes
|
|
175
|
+
|
|
176
|
+
:::{list-table}
|
|
177
|
+
:header-rows: 1
|
|
178
|
+
:widths: 12 88
|
|
179
|
+
|
|
180
|
+
* - Code
|
|
181
|
+
- Meaning
|
|
182
|
+
* - `0`
|
|
183
|
+
- Converted successfully (there may still be warnings).
|
|
184
|
+
* - `1`
|
|
185
|
+
- One or more input files failed to parse (see `FAILED` lines and the
|
|
186
|
+
report's `failures`).
|
|
187
|
+
* - `2`
|
|
188
|
+
- Usage error: bad arguments, a directory input without `--output`/`--dry-run`,
|
|
189
|
+
the output-directory guard, or an output-write error.
|
|
190
|
+
:::
|
|
191
|
+
|
|
192
|
+
## Examples
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Inspect a single file without writing anything
|
|
196
|
+
sql2sqlx model.sql | less
|
|
197
|
+
|
|
198
|
+
# Full migration with shape-checked MERGE conversion and source declarations
|
|
199
|
+
sql2sqlx ./sql -o ./definitions \
|
|
200
|
+
--merge-strategy incremental-when-safe \
|
|
201
|
+
--declare-external --tags migrated --report report.json
|
|
202
|
+
|
|
203
|
+
# CI-style dry run that only produces the report
|
|
204
|
+
sql2sqlx ./sql --dry-run --report report.json -q
|
|
205
|
+
|
|
206
|
+
# Regenerate an existing project deterministically, verbosely
|
|
207
|
+
sql2sqlx ./sql -o ./definitions --overwrite -v
|
|
208
|
+
|
|
209
|
+
# Convert only staging models, single-process
|
|
210
|
+
sql2sqlx ./sql -o ./definitions --include "stg_*.sql" -j 1
|
|
211
|
+
```
|