@sqlanvil/cli 1.20.1 → 1.22.0
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.
- package/README.md +59 -39
- package/bundle.js +786 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**SQL workflow tool for BigQuery, Postgres, Supabase, and MySQL/MariaDB.**
|
|
4
4
|
|
|
5
|
-
SQLAnvil is an open-source fork of [Dataform OSS](https://github.com/dataform-co/dataform) (Apache 2.0), extended with first-class PostgreSQL
|
|
5
|
+
SQLAnvil is an open-source fork of [Dataform OSS](https://github.com/dataform-co/dataform) (Apache 2.0), extended with first-class PostgreSQL, Supabase, and MySQL/MariaDB support. Define your data transformations in SQLX, have SQLAnvil compile them to idiomatic SQL for your warehouse, and run, validate, and document the whole DAG from one CLI.
|
|
6
6
|
|
|
7
7
|
> **SQLAnvil is not affiliated with or endorsed by Google.** The Dataform name and related marks are trademarks of Google LLC. See [NOTICE](NOTICE) for attribution.
|
|
8
8
|
|
|
@@ -10,10 +10,21 @@ SQLAnvil is an open-source fork of [Dataform OSS](https://github.com/dataform-co
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
13
|
+
**Four first-class warehouses** — each adapter generates its warehouse's own idiomatic SQL, not translated BigQuery:
|
|
14
|
+
|
|
15
|
+
- **PostgreSQL** — native partitioning (RANGE/LIST/HASH), `INSERT … ON CONFLICT` upserts, btree/hash/gin/gist/brin indexes (unique, partial, `INCLUDE`, opclasses), tablespaces, fillfactor, materialized views with in-place refresh policies
|
|
16
|
+
- **Supabase** — everything Postgres, plus RLS policies, Realtime publications, pgvector indexes, and Supabase Wrappers (FDW) as declarative actions
|
|
17
|
+
- **MySQL / MariaDB** — one adapter validated against both engines: CTAS tables, `ON DUPLICATE KEY UPDATE` incremental upserts, the full `mysql: {}` config block (engine/charset/collation/rowFormat, secondary indexes incl. `FULLTEXT`/`SPATIAL` and prefix lengths, native partitioning), materialized-view emulation
|
|
18
|
+
- **BigQuery** — full upstream support: partitioning, clustering, labels, materialized views, `MERGE`-based incremental upserts
|
|
19
|
+
|
|
20
|
+
**Beyond transformation** — the pieces most SQL workflow tools leave outside the pipeline:
|
|
21
|
+
|
|
22
|
+
- **Cross-warehouse sources** — read BigQuery or MySQL/MariaDB from a Postgres/Supabase warehouse via named `connections:`: a live FDW bridge (Supabase Wrappers) or **runner-extract** (the CLI reads the source at run time and materializes a plain `ref()`-able table — keyless BigQuery supported). `sqlanvil introspect` scaffolds the declarations
|
|
23
|
+
- **File imports & exports** — `type: "import"` loads Parquet/CSV/JSON from `s3://`/`gs://`/local into a `ref()`-able table (DuckDB bridge on Postgres/Supabase, native `LOAD DATA` on BigQuery); `type: "export"` writes query results back out to files
|
|
24
|
+
- **Python script actions** — `python:` in `actions.yaml` runs file-staging and glue scripts as first-class DAG nodes: ordered by dependencies, covered by run history, no warehouse credentials injected (scripts stage files; `import` loads them). SQLAnvil validates the declared environment — interpreter version, `requirements.txt` vs installed packages, syntax — and never installs anything
|
|
25
|
+
- **`sqlanvil validate`** — EXPLAIN/dry-run the *whole DAG* against the live warehouse without executing: an isolated shadow schema of empty stubs lets every downstream `ref()` resolve; results classify as PASS / FAIL / BLOCKED so one real error doesn't cascade
|
|
26
|
+
- **Queryable artifacts** — every compile/run writes a Parquet catalog + run history under `target/`; `sqlanvil query` runs SQL over them, `sqlanvil inspect` summarizes, `sqlanvil docs` renders a self-contained HTML catalog
|
|
27
|
+
- **Release-artifact runs** — `sqlanvil run --graph <file>` executes a stored `compile --json` output exactly as compiled; **named environments** (`--environment`) give per-env schema suffixes and vars
|
|
17
28
|
- **SQLX + YAML + JS** — three authoring modes: SQL with config blocks, `actions.yaml` bulk definitions, or the JavaScript API
|
|
18
29
|
|
|
19
30
|
---
|
|
@@ -24,23 +35,32 @@ SQLAnvil is an open-source fork of [Dataform OSS](https://github.com/dataform-co
|
|
|
24
35
|
npm install -g @sqlanvil/cli
|
|
25
36
|
sqlanvil init my-project
|
|
26
37
|
cd my-project
|
|
27
|
-
# edit workflow_settings.yaml
|
|
38
|
+
# edit workflow_settings.yaml + .df-credentials.json (see below)
|
|
28
39
|
sqlanvil compile
|
|
29
40
|
sqlanvil run
|
|
30
41
|
```
|
|
31
42
|
|
|
32
|
-
**`workflow_settings.yaml`
|
|
43
|
+
**`workflow_settings.yaml` (Postgres/Supabase):** the warehouse is a flat string; connection
|
|
44
|
+
secrets never live here.
|
|
33
45
|
|
|
34
46
|
```yaml
|
|
35
|
-
warehouse:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
warehouse: postgres # bigquery | postgres | supabase | mysql
|
|
48
|
+
defaultDataset: public # the Postgres schema
|
|
49
|
+
defaultAssertionDataset: sqlanvil_assertions
|
|
50
|
+
sqlanvilCoreVersion: 1.20.1
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**`.df-credentials.json`** (gitignored) holds the connection:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"host": "localhost",
|
|
58
|
+
"port": 5432,
|
|
59
|
+
"database": "analytics",
|
|
60
|
+
"user": "sqlanvil_writer",
|
|
61
|
+
"password": "...",
|
|
62
|
+
"sslMode": "disable"
|
|
63
|
+
}
|
|
44
64
|
```
|
|
45
65
|
|
|
46
66
|
**First action (`definitions/my_view.sqlx`):**
|
|
@@ -58,23 +78,29 @@ SELECT 1 AS id, 'hello' AS greeting
|
|
|
58
78
|
|
|
59
79
|
## Documentation
|
|
60
80
|
|
|
61
|
-
Full documentation at **[sqlanvil.com](https://sqlanvil.com)**.
|
|
81
|
+
Full documentation at **[sqlanvil.com/docs](https://sqlanvil.com/docs/)**.
|
|
82
|
+
|
|
83
|
+
- [Getting Started](https://sqlanvil.com/docs/getting-started/)
|
|
84
|
+
- Warehouses: [PostgreSQL](https://sqlanvil.com/docs/guides/postgres/) · [Supabase](https://sqlanvil.com/docs/guides/supabase/) · [MySQL/MariaDB](https://sqlanvil.com/docs/guides/mysql/) · [BigQuery](https://sqlanvil.com/docs/guides/bigquery/)
|
|
85
|
+
- [Cross-warehouse sources](https://sqlanvil.com/docs/guides/foreign-wrappers/) · [File Imports](https://sqlanvil.com/docs/guides/imports/) · [File Exports](https://sqlanvil.com/docs/guides/exports/) · [Python Script Actions](https://sqlanvil.com/docs/guides/python-actions/)
|
|
86
|
+
- [Validate](https://sqlanvil.com/docs/guides/validate/) · [Named Environments](https://sqlanvil.com/docs/guides/environments/) · [Artifacts & Catalog](https://sqlanvil.com/docs/guides/artifacts/)
|
|
87
|
+
- [Configs Reference](https://sqlanvil.com/docs/reference/configs/) · [What's New](https://sqlanvil.com/docs/whats-new/)
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
- [Supabase Guide](https://sqlanvil.com/guides/supabase/)
|
|
67
|
-
- [Configs Reference](https://sqlanvil.com/reference/configs/)
|
|
89
|
+
Writing sqlanvil projects with an AI agent? Point it at [`AGENTS.md`](AGENTS.md) — it corrects
|
|
90
|
+
the Dataform/BigQuery priors that otherwise produce broken sqlanvil code. Contributor/design
|
|
91
|
+
docs live in the separate [`sqlanvil/docs`](https://github.com/sqlanvil/docs) repo.
|
|
68
92
|
|
|
69
93
|
---
|
|
70
94
|
|
|
71
95
|
## Project layout
|
|
72
96
|
|
|
73
97
|
```
|
|
74
|
-
core/ Compiler + action types (table/view/incremental/assertion/operation/
|
|
75
|
-
cli/ CLI entrypoint and per-adapter glue (cli/api/dbadapters/)
|
|
98
|
+
core/ Compiler + action types (table/view/incremental/assertion/operation/declaration/import/export/script/…)
|
|
99
|
+
cli/ CLI entrypoint and per-adapter glue (cli/api/dbadapters/ — bigquery, postgres, supabase, mysql)
|
|
76
100
|
protos/ Protobuf definitions for core/configs/execution/db_adapter
|
|
77
|
-
|
|
101
|
+
tests/ Integration specs against real warehouses (Postgres, Supabase, MySQL/MariaDB, BigQuery)
|
|
102
|
+
tools/ Bazel rules + Docker fixtures for the integration databases
|
|
103
|
+
examples/ Sample SQLAnvil projects (see examples/supabase_bigquery_mailing_list)
|
|
78
104
|
scripts/ ./scripts/run is the CLI entrypoint wrapper
|
|
79
105
|
```
|
|
80
106
|
|
|
@@ -82,28 +108,22 @@ scripts/ ./scripts/run is the CLI entrypoint wrapper
|
|
|
82
108
|
|
|
83
109
|
## Building from source
|
|
84
110
|
|
|
85
|
-
SQLAnvil uses [Bazel](https://bazel.build) (via Bazelisk). Native Bazel works on
|
|
86
|
-
and Linux
|
|
111
|
+
SQLAnvil uses [Bazel](https://bazel.build) 7 with bzlmod (via Bazelisk). Native Bazel works on
|
|
112
|
+
macOS and Linux:
|
|
87
113
|
|
|
88
114
|
```bash
|
|
89
|
-
# Build the proto layer
|
|
90
|
-
bazel build //protos:sqlanvil_proto
|
|
91
|
-
|
|
92
115
|
# Run the CLI
|
|
93
116
|
./scripts/run help
|
|
94
117
|
|
|
95
118
|
# Run tests
|
|
96
119
|
bazel test //core/... //cli/...
|
|
97
|
-
```
|
|
98
120
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## Documentation
|
|
121
|
+
# Integration tests against a local Docker Postgres
|
|
122
|
+
./tools/postgres/run-postgres-db.sh
|
|
123
|
+
```
|
|
105
124
|
|
|
106
|
-
|
|
125
|
+
A Docker-based build container (`./scripts/docker-bazel`) remains available as an optional
|
|
126
|
+
hermetic environment. See [contributing.md](contributing.md) for full instructions.
|
|
107
127
|
|
|
108
128
|
---
|
|
109
129
|
|
|
@@ -111,4 +131,4 @@ Reference docs and design documents live in the separate [`sqlanvil/docs`](https
|
|
|
111
131
|
|
|
112
132
|
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
113
133
|
|
|
114
|
-
SQLAnvil is a derivative of Dataform OSS (originally developed by Dataform Co and contributed to by Google LLC). It has been fully renamed and extended with first-class PostgreSQL/Supabase support. Licensed under Apache License 2.0.
|
|
134
|
+
SQLAnvil is a derivative of Dataform OSS (originally developed by Dataform Co and contributed to by Google LLC). It has been fully renamed and extended with first-class PostgreSQL/Supabase/MySQL support. Licensed under Apache License 2.0.
|
package/bundle.js
CHANGED
|
@@ -49392,11 +49392,11 @@ function setOrValidateTableEnumType(table) {
|
|
|
49392
49392
|
function prune(compiledGraph, runConfig) {
|
|
49393
49393
|
compiledGraph.tables.forEach(setOrValidateTableEnumType);
|
|
49394
49394
|
const includedActionNames = computeIncludedActionNames(compiledGraph, runConfig);
|
|
49395
|
-
return Object.assign(Object.assign({}, compiledGraph), { tables: compiledGraph.tables.filter(action => includedActionNames.has(targetAsReadableString(action.target))), assertions: compiledGraph.assertions.filter(action => includedActionNames.has(targetAsReadableString(action.target))), operations: compiledGraph.operations.filter(action => includedActionNames.has(targetAsReadableString(action.target))), exports: compiledGraph.exports.filter(action => includedActionNames.has(targetAsReadableString(action.target))), imports: compiledGraph.imports.filter(action => includedActionNames.has(targetAsReadableString(action.target))), scripts: (compiledGraph.scripts || []).filter(action => includedActionNames.has(targetAsReadableString(action.target))) });
|
|
49395
|
+
return Object.assign(Object.assign({}, compiledGraph), { tables: compiledGraph.tables.filter(action => includedActionNames.has(targetAsReadableString(action.target))), assertions: compiledGraph.assertions.filter(action => includedActionNames.has(targetAsReadableString(action.target))), operations: compiledGraph.operations.filter(action => includedActionNames.has(targetAsReadableString(action.target))), exports: compiledGraph.exports.filter(action => includedActionNames.has(targetAsReadableString(action.target))), imports: compiledGraph.imports.filter(action => includedActionNames.has(targetAsReadableString(action.target))), extracts: (compiledGraph.extracts || []).filter(action => includedActionNames.has(targetAsReadableString(action.target))), scripts: (compiledGraph.scripts || []).filter(action => includedActionNames.has(targetAsReadableString(action.target))) });
|
|
49396
49396
|
}
|
|
49397
49397
|
function computeIncludedActionNames(compiledGraph, runConfig) {
|
|
49398
49398
|
var _a, _b, _c;
|
|
49399
|
-
const allActions = [].concat(compiledGraph.tables, compiledGraph.operations, compiledGraph.assertions, compiledGraph.exports, compiledGraph.imports, compiledGraph.scripts || []);
|
|
49399
|
+
const allActions = [].concat(compiledGraph.tables, compiledGraph.operations, compiledGraph.assertions, compiledGraph.exports, compiledGraph.imports, compiledGraph.extracts || [], compiledGraph.scripts || []);
|
|
49400
49400
|
const allActionNames = new Set(allActions.map(action => targetAsReadableString(action.target)));
|
|
49401
49401
|
const allActionsByName = new Map(allActions.map(action => [targetAsReadableString(action.target), action]));
|
|
49402
49402
|
const hasActionSelector = ((_a = runConfig.actions) === null || _a === void 0 ? void 0 : _a.length) > 0;
|
|
@@ -50606,7 +50606,7 @@ function collectEvaluationQueries(queryOrAction, concatenate, queryModifier = (q
|
|
|
50606
50606
|
.filter(validationQuery => !!validationQuery.query);
|
|
50607
50607
|
}
|
|
50608
50608
|
|
|
50609
|
-
const version = "1.
|
|
50609
|
+
const version = "1.22.0";
|
|
50610
50610
|
const dataformVersion = "3.0.61";
|
|
50611
50611
|
|
|
50612
50612
|
async function build(compiledGraph, runConfig, dbadapter) {
|
|
@@ -50995,10 +50995,10 @@ node_modules/
|
|
|
50995
50995
|
function postgresCredentialsTemplate(warehouse) {
|
|
50996
50996
|
const isSupabase = warehouse === "supabase";
|
|
50997
50997
|
const template = {
|
|
50998
|
-
host: isSupabase ? "
|
|
50998
|
+
host: isSupabase ? "aws-1-<region>.pooler.supabase.com" : "localhost",
|
|
50999
50999
|
port: 5432,
|
|
51000
51000
|
database: "postgres",
|
|
51001
|
-
user: "postgres",
|
|
51001
|
+
user: isSupabase ? "postgres.<your-project-ref>" : "postgres",
|
|
51002
51002
|
password: "",
|
|
51003
51003
|
sslMode: isSupabase ? "require" : "disable",
|
|
51004
51004
|
defaultSchema: "public"
|
|
@@ -51041,7 +51041,9 @@ async function init(projectDir, projectConfig) {
|
|
|
51041
51041
|
workflowSettings.defaultProject = projectConfig.defaultDatabase;
|
|
51042
51042
|
workflowSettings.defaultLocation = projectConfig.defaultLocation;
|
|
51043
51043
|
}
|
|
51044
|
-
|
|
51044
|
+
const isPostgresLike = warehouse === "postgres" || warehouse === "supabase";
|
|
51045
|
+
workflowSettings.defaultDataset =
|
|
51046
|
+
projectConfig.defaultSchema || (isPostgresLike ? "public" : "sqlanvil");
|
|
51045
51047
|
workflowSettings.defaultAssertionDataset =
|
|
51046
51048
|
projectConfig.assertionSchema || "sqlanvil_assertions";
|
|
51047
51049
|
if (isBigQuery && projectConfig.defaultIcebergConfig) {
|
|
@@ -51062,6 +51064,17 @@ async function init(projectDir, projectConfig) {
|
|
|
51062
51064
|
if (projectConfig.builtinAssertionNamePrefix) {
|
|
51063
51065
|
workflowSettings.builtinAssertionNamePrefix = projectConfig.builtinAssertionNamePrefix;
|
|
51064
51066
|
}
|
|
51067
|
+
if (isPostgresLike) {
|
|
51068
|
+
workflowSettings.connections = {
|
|
51069
|
+
bigquery_public: sqlanvil.ConnectionConfig.create({
|
|
51070
|
+
platform: "bigquery",
|
|
51071
|
+
project: "bigquery-public-data",
|
|
51072
|
+
dataset: "geo_us_boundaries",
|
|
51073
|
+
billingProject: "REPLACE_WITH_YOUR_GCP_PROJECT",
|
|
51074
|
+
mode: "runner-extract"
|
|
51075
|
+
})
|
|
51076
|
+
};
|
|
51077
|
+
}
|
|
51065
51078
|
fs__namespace$1.writeFileSync(workflowSettingsYamlPath, jsYaml.dump(workflowSettings));
|
|
51066
51079
|
filesWritten.push(workflowSettingsYamlPath);
|
|
51067
51080
|
fs__namespace$1.writeFileSync(gitignorePath, gitIgnoreContents);
|
|
@@ -51070,39 +51083,140 @@ async function init(projectDir, projectConfig) {
|
|
|
51070
51083
|
fs__namespace$1.writeFileSync(path__namespace.join(projectDir, CREDENTIALS_FILENAME), warehouse === "mysql" ? mysqlCredentialsTemplate() : postgresCredentialsTemplate(warehouse));
|
|
51071
51084
|
filesWritten.push(path__namespace.join(projectDir, CREDENTIALS_FILENAME));
|
|
51072
51085
|
}
|
|
51073
|
-
const
|
|
51074
|
-
|
|
51075
|
-
|
|
51076
|
-
|
|
51077
|
-
|
|
51078
|
-
|
|
51079
|
-
const
|
|
51080
|
-
|
|
51081
|
-
|
|
51082
|
-
|
|
51083
|
-
|
|
51084
|
-
|
|
51085
|
-
const
|
|
51086
|
-
|
|
51087
|
-
|
|
51088
|
-
const
|
|
51089
|
-
|
|
51090
|
-
|
|
51091
|
-
const
|
|
51092
|
-
|
|
51093
|
-
|
|
51094
|
-
|
|
51095
|
-
|
|
51096
|
-
|
|
51097
|
-
|
|
51098
|
-
|
|
51099
|
-
|
|
51100
|
-
|
|
51101
|
-
|
|
51102
|
-
|
|
51103
|
-
|
|
51104
|
-
|
|
51105
|
-
|
|
51086
|
+
const mkdir = (...segments) => {
|
|
51087
|
+
const dir = path__namespace.join(projectDir, ...segments);
|
|
51088
|
+
fs__namespace$1.mkdirSync(dir, { recursive: true });
|
|
51089
|
+
dirsCreated.push(dir);
|
|
51090
|
+
return dir;
|
|
51091
|
+
};
|
|
51092
|
+
const write = (filePath, contents) => {
|
|
51093
|
+
fs__namespace$1.writeFileSync(filePath, contents);
|
|
51094
|
+
filesWritten.push(filePath);
|
|
51095
|
+
};
|
|
51096
|
+
const keep = (dir) => write(path__namespace.join(dir, ".gitkeep"), "");
|
|
51097
|
+
mkdir("definitions");
|
|
51098
|
+
const sourcesDir = mkdir("definitions", "sources");
|
|
51099
|
+
const intermediateDir = mkdir("definitions", "intermediate");
|
|
51100
|
+
const salesDir = mkdir("definitions", "outputs", "sales");
|
|
51101
|
+
const reportingDir = mkdir("definitions", "outputs", "reporting");
|
|
51102
|
+
const testDir = mkdir("definitions", "test");
|
|
51103
|
+
keep(mkdir("includes"));
|
|
51104
|
+
const withBigQuerySource = warehouse !== "mysql";
|
|
51105
|
+
write(path__namespace.join(sourcesDir, "app_orders.sqlx"), `-- A declaration: a table that already exists in your warehouse (created by your
|
|
51106
|
+
-- application, an import, or another pipeline). Point name/dataset at a real table —
|
|
51107
|
+
-- downstream models read it with \${ref("app_orders")}.
|
|
51108
|
+
config {
|
|
51109
|
+
type: "declaration",
|
|
51110
|
+
name: "app_orders",
|
|
51111
|
+
description: "Raw orders table owned by the application — replace with one of your tables."
|
|
51112
|
+
}
|
|
51113
|
+
`);
|
|
51114
|
+
write(path__namespace.join(intermediateDir, "stg_app_orders.sqlx"), `config {
|
|
51115
|
+
type: "view",
|
|
51116
|
+
description: "Staging view over the raw orders: rename, cast, and filter here — downstream models only read staged data."
|
|
51117
|
+
}
|
|
51118
|
+
|
|
51119
|
+
SELECT
|
|
51120
|
+
order_id,
|
|
51121
|
+
product,
|
|
51122
|
+
quantity,
|
|
51123
|
+
amount,
|
|
51124
|
+
postal_code,
|
|
51125
|
+
ordered_at
|
|
51126
|
+
FROM \${ref("app_orders")}
|
|
51127
|
+
WHERE amount IS NOT NULL
|
|
51128
|
+
`);
|
|
51129
|
+
write(path__namespace.join(salesDir, "daily_sales.sqlx"), `config {
|
|
51130
|
+
type: "table",
|
|
51131
|
+
description: "Sales by day and product, built from the staged orders."
|
|
51132
|
+
}
|
|
51133
|
+
|
|
51134
|
+
SELECT
|
|
51135
|
+
CAST(ordered_at AS DATE) AS sale_date,
|
|
51136
|
+
product,
|
|
51137
|
+
SUM(quantity) AS units_sold,
|
|
51138
|
+
SUM(amount) AS revenue
|
|
51139
|
+
FROM \${ref("stg_app_orders")}
|
|
51140
|
+
GROUP BY CAST(ordered_at AS DATE), product
|
|
51141
|
+
`);
|
|
51142
|
+
write(path__namespace.join(reportingDir, "product_revenue.sqlx"), `config {
|
|
51143
|
+
type: "view",
|
|
51144
|
+
description: "Revenue by product, straight off the staged orders."
|
|
51145
|
+
}
|
|
51146
|
+
|
|
51147
|
+
SELECT
|
|
51148
|
+
product,
|
|
51149
|
+
SUM(quantity) AS units_sold,
|
|
51150
|
+
SUM(amount) AS revenue
|
|
51151
|
+
FROM \${ref("stg_app_orders")}
|
|
51152
|
+
GROUP BY product
|
|
51153
|
+
`);
|
|
51154
|
+
write(path__namespace.join(testDir, "assert_sales_amounts_positive.sqlx"), `config {
|
|
51155
|
+
type: "assertion",
|
|
51156
|
+
description: "Business rule: every sales row has positive units and revenue."
|
|
51157
|
+
}
|
|
51158
|
+
|
|
51159
|
+
-- An assertion FAILS when its query returns rows — select the violations.
|
|
51160
|
+
SELECT *
|
|
51161
|
+
FROM \${ref("daily_sales")}
|
|
51162
|
+
WHERE units_sold <= 0 OR revenue <= 0
|
|
51163
|
+
`);
|
|
51164
|
+
if (withBigQuerySource) {
|
|
51165
|
+
write(path__namespace.join(sourcesDir, "bigquery_zip_codes.sqlx"), isBigQuery
|
|
51166
|
+
? `-- A declaration for a table in another project: Google's public ZIP geo data.
|
|
51167
|
+
-- BigQuery reads it natively — \${ref("zip_codes")} resolves to the full name.
|
|
51168
|
+
config {
|
|
51169
|
+
type: "declaration",
|
|
51170
|
+
database: "bigquery-public-data",
|
|
51171
|
+
schema: "geo_us_boundaries",
|
|
51172
|
+
name: "zip_codes",
|
|
51173
|
+
description: "US ZIP codes from BigQuery public data."
|
|
51174
|
+
}
|
|
51175
|
+
`
|
|
51176
|
+
: `-- A cross-warehouse source: Google's public ZIP geo data in BigQuery, read through
|
|
51177
|
+
-- the "bigquery_public" connection (see workflow_settings.yaml). With
|
|
51178
|
+
-- mode: runner-extract the CLI reads BigQuery at run time and materializes the rows
|
|
51179
|
+
-- as a plain table here — \${ref("zip_codes")} works like any other source.
|
|
51180
|
+
-- Scaffold real sources with: sqlanvil introspect bigquery_public <dataset.table>
|
|
51181
|
+
config {
|
|
51182
|
+
type: "declaration",
|
|
51183
|
+
connection: "bigquery_public",
|
|
51184
|
+
name: "zip_codes",
|
|
51185
|
+
description: "US ZIP codes from BigQuery public data, materialized by runner-extract.",
|
|
51186
|
+
columnTypes: {
|
|
51187
|
+
zip_code: "text",
|
|
51188
|
+
city: "text",
|
|
51189
|
+
state_code: "text"
|
|
51190
|
+
}
|
|
51191
|
+
}
|
|
51192
|
+
`);
|
|
51193
|
+
write(path__namespace.join(intermediateDir, "stg_zip_codes.sqlx"), `config {
|
|
51194
|
+
type: "view",
|
|
51195
|
+
description: "Staged ZIP geography from the BigQuery source."
|
|
51196
|
+
}
|
|
51197
|
+
|
|
51198
|
+
SELECT
|
|
51199
|
+
zip_code,
|
|
51200
|
+
city,
|
|
51201
|
+
state_code
|
|
51202
|
+
FROM \${ref("zip_codes")}
|
|
51203
|
+
`);
|
|
51204
|
+
write(path__namespace.join(reportingDir, "orders_by_region.sqlx"), `config {
|
|
51205
|
+
type: "table",
|
|
51206
|
+
description: "Orders and revenue by state/city — joins both staged sources."
|
|
51207
|
+
}
|
|
51208
|
+
|
|
51209
|
+
SELECT
|
|
51210
|
+
z.state_code,
|
|
51211
|
+
z.city,
|
|
51212
|
+
COUNT(*) AS orders,
|
|
51213
|
+
SUM(o.amount) AS revenue
|
|
51214
|
+
FROM \${ref("stg_app_orders")} o
|
|
51215
|
+
JOIN \${ref("stg_zip_codes")} z
|
|
51216
|
+
ON z.zip_code = o.postal_code
|
|
51217
|
+
GROUP BY z.state_code, z.city
|
|
51218
|
+
`);
|
|
51219
|
+
}
|
|
51106
51220
|
return {
|
|
51107
51221
|
filesWritten,
|
|
51108
51222
|
dirsCreated
|
|
@@ -53427,7 +53541,7 @@ class Runner {
|
|
|
53427
53541
|
return actionResult;
|
|
53428
53542
|
}
|
|
53429
53543
|
async executeTask(client, task, parentAction, options, action) {
|
|
53430
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
53544
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
53431
53545
|
const timer = Timer.start();
|
|
53432
53546
|
const taskResult = {
|
|
53433
53547
|
status: sqlanvil.TaskResult.ExecutionStatus.RUNNING,
|
|
@@ -53499,7 +53613,14 @@ class Runner {
|
|
|
53499
53613
|
}
|
|
53500
53614
|
else if (task.type === "extract") {
|
|
53501
53615
|
try {
|
|
53502
|
-
|
|
53616
|
+
if (Object.keys((_e = (_d = action === null || action === void 0 ? void 0 : action.extract) === null || _d === void 0 ? void 0 : _d.columnTypes) !== null && _e !== void 0 ? _e : {}).length === 0) {
|
|
53617
|
+
const ds = ((_f = action === null || action === void 0 ? void 0 : action.extract) === null || _f === void 0 ? void 0 : _f.platform) === "mysql"
|
|
53618
|
+
? (_g = action === null || action === void 0 ? void 0 : action.extract) === null || _g === void 0 ? void 0 : _g.database : (_h = action === null || action === void 0 ? void 0 : action.extract) === null || _h === void 0 ? void 0 : _h.dataset;
|
|
53619
|
+
throw new Error(`declaration "${(_j = action === null || action === void 0 ? void 0 : action.target) === null || _j === void 0 ? void 0 : _j.name}" has no columnTypes — scaffold them with: ` +
|
|
53620
|
+
`sqlanvil introspect ${(_k = action === null || action === void 0 ? void 0 : action.extract) === null || _k === void 0 ? void 0 : _k.connectionName} ${ds}.${(_l = action === null || action === void 0 ? void 0 : action.extract) === null || _l === void 0 ? void 0 : _l.sourceName} ` +
|
|
53621
|
+
`--output <its .sqlx file>`);
|
|
53622
|
+
}
|
|
53623
|
+
const extractor = ((_m = action === null || action === void 0 ? void 0 : action.extract) === null || _m === void 0 ? void 0 : _m.platform) === "mysql"
|
|
53503
53624
|
? this.executionOptions.mysqlExtract || runMysqlExtract
|
|
53504
53625
|
: this.executionOptions.bigQueryExtract || runBigQueryExtract;
|
|
53505
53626
|
await extractor({
|
|
@@ -53529,7 +53650,7 @@ class Runner {
|
|
|
53529
53650
|
}), task.type === "operation" ? 1 : options.bigquery.actionRetryLimit + 1 || 1);
|
|
53530
53651
|
taskResult.metadata = metadata;
|
|
53531
53652
|
if (task.type === "assertion") {
|
|
53532
|
-
const rowCount = (
|
|
53653
|
+
const rowCount = (_o = rows[0]) === null || _o === void 0 ? void 0 : _o[Object.keys(rows[0])[0]];
|
|
53533
53654
|
if (rowCount > 0) {
|
|
53534
53655
|
throw new Error(`Assertion failed: query returned ${rowCount} row(s).`);
|
|
53535
53656
|
}
|
|
@@ -53541,7 +53662,7 @@ class Runner {
|
|
|
53541
53662
|
? sqlanvil.TaskResult.ExecutionStatus.CANCELLED
|
|
53542
53663
|
: sqlanvil.TaskResult.ExecutionStatus.FAILED;
|
|
53543
53664
|
taskResult.errorMessage = `${this.graph.projectConfig.warehouse} error: ${e.message}`;
|
|
53544
|
-
if ((
|
|
53665
|
+
if ((_q = (_p = e.metadata) === null || _p === void 0 ? void 0 : _p.bigquery) === null || _q === void 0 ? void 0 : _q.jobId) {
|
|
53545
53666
|
taskResult.metadata = {
|
|
53546
53667
|
bigquery: {
|
|
53547
53668
|
jobId: e.metadata.bigquery.jobId
|
|
@@ -54061,6 +54182,596 @@ ${columnRows || '<tr><td colspan="3" class="meta">No documented columns.</td></t
|
|
|
54061
54182
|
`;
|
|
54062
54183
|
}
|
|
54063
54184
|
|
|
54185
|
+
function containsPath(parent, child) {
|
|
54186
|
+
const rel = path__namespace.relative(parent, child);
|
|
54187
|
+
return rel === "" || (!rel.startsWith("..") && !path__namespace.isAbsolute(rel));
|
|
54188
|
+
}
|
|
54189
|
+
class OutWriter {
|
|
54190
|
+
constructor(outDir) {
|
|
54191
|
+
this.outDir = outDir;
|
|
54192
|
+
}
|
|
54193
|
+
write(relPath, contents) {
|
|
54194
|
+
const full = path__namespace.resolve(this.outDir, relPath);
|
|
54195
|
+
if (!containsPath(path__namespace.resolve(this.outDir), full)) {
|
|
54196
|
+
throw new Error(`Refusing to write outside the output directory: ${relPath}`);
|
|
54197
|
+
}
|
|
54198
|
+
fs__namespace$1.mkdirSync(path__namespace.dirname(full), { recursive: true });
|
|
54199
|
+
fs__namespace$1.writeFileSync(full, contents);
|
|
54200
|
+
}
|
|
54201
|
+
}
|
|
54202
|
+
const SAFETY_SKIP = [
|
|
54203
|
+
/^\.git(\/|$)/,
|
|
54204
|
+
/(^|\/)node_modules(\/|$)/,
|
|
54205
|
+
/(^|\/)\.df-credentials[^/]*\.json$/,
|
|
54206
|
+
/service_account[^/]*\.json$/i,
|
|
54207
|
+
/(^|\/)\.env([^/]*)$/,
|
|
54208
|
+
/\.pem$/,
|
|
54209
|
+
/(^|\/)compile_output\.json$/,
|
|
54210
|
+
/(^|\/)\.DS_Store$/,
|
|
54211
|
+
/(^|\/)target(\/|$)/,
|
|
54212
|
+
];
|
|
54213
|
+
function walkFiles(root) {
|
|
54214
|
+
const out = [];
|
|
54215
|
+
const walk = (rel) => {
|
|
54216
|
+
for (const entry of fs__namespace$1.readdirSync(path__namespace.join(root, rel), { withFileTypes: true })) {
|
|
54217
|
+
const relPath = rel ? `${rel}/${entry.name}` : entry.name;
|
|
54218
|
+
if (SAFETY_SKIP.some(re => re.test(relPath) || re.test(relPath + "/"))) {
|
|
54219
|
+
skippedForSafety.add(relPath);
|
|
54220
|
+
continue;
|
|
54221
|
+
}
|
|
54222
|
+
if (entry.isDirectory()) {
|
|
54223
|
+
walk(relPath);
|
|
54224
|
+
}
|
|
54225
|
+
else if (entry.isFile()) {
|
|
54226
|
+
out.push(relPath);
|
|
54227
|
+
}
|
|
54228
|
+
}
|
|
54229
|
+
};
|
|
54230
|
+
walk("");
|
|
54231
|
+
return out;
|
|
54232
|
+
}
|
|
54233
|
+
let skippedForSafety;
|
|
54234
|
+
function findConfigBlock(source) {
|
|
54235
|
+
const m = /(^|\n)\s*config\s*\{/.exec(source);
|
|
54236
|
+
if (!m)
|
|
54237
|
+
return null;
|
|
54238
|
+
const open = source.indexOf("{", m.index + m[0].indexOf("config"));
|
|
54239
|
+
let depth = 0;
|
|
54240
|
+
let inString = null;
|
|
54241
|
+
for (let i = open; i < source.length; i++) {
|
|
54242
|
+
const ch = source[i];
|
|
54243
|
+
const prev = source[i - 1];
|
|
54244
|
+
if (inString) {
|
|
54245
|
+
if (ch === inString && prev !== "\\")
|
|
54246
|
+
inString = null;
|
|
54247
|
+
continue;
|
|
54248
|
+
}
|
|
54249
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
54250
|
+
inString = ch;
|
|
54251
|
+
continue;
|
|
54252
|
+
}
|
|
54253
|
+
if (ch === "{")
|
|
54254
|
+
depth++;
|
|
54255
|
+
if (ch === "}") {
|
|
54256
|
+
depth--;
|
|
54257
|
+
if (depth === 0) {
|
|
54258
|
+
return {
|
|
54259
|
+
start: m.index + (m[1] ? m[1].length : 0),
|
|
54260
|
+
open,
|
|
54261
|
+
end: i + 1,
|
|
54262
|
+
body: source.slice(open + 1, i),
|
|
54263
|
+
};
|
|
54264
|
+
}
|
|
54265
|
+
}
|
|
54266
|
+
}
|
|
54267
|
+
return null;
|
|
54268
|
+
}
|
|
54269
|
+
function jsBlockLines(source) {
|
|
54270
|
+
const lines = new Set();
|
|
54271
|
+
const re = /(^|\n)\s*js\s*\{/g;
|
|
54272
|
+
let m;
|
|
54273
|
+
while ((m = re.exec(source))) {
|
|
54274
|
+
const open = source.indexOf("{", m.index + m[0].length - 1);
|
|
54275
|
+
let depth = 0;
|
|
54276
|
+
let end = -1;
|
|
54277
|
+
for (let i = open; i < source.length; i++) {
|
|
54278
|
+
if (source[i] === "{")
|
|
54279
|
+
depth++;
|
|
54280
|
+
if (source[i] === "}") {
|
|
54281
|
+
depth--;
|
|
54282
|
+
if (depth === 0) {
|
|
54283
|
+
end = i;
|
|
54284
|
+
break;
|
|
54285
|
+
}
|
|
54286
|
+
}
|
|
54287
|
+
}
|
|
54288
|
+
if (end < 0)
|
|
54289
|
+
end = source.length - 1;
|
|
54290
|
+
const startLine = source.slice(0, m.index + (m[1] ? m[1].length : 0)).split("\n").length - 1;
|
|
54291
|
+
const endLine = source.slice(0, end).split("\n").length - 1;
|
|
54292
|
+
for (let l = startLine; l <= endLine; l++)
|
|
54293
|
+
lines.add(l);
|
|
54294
|
+
}
|
|
54295
|
+
return lines;
|
|
54296
|
+
}
|
|
54297
|
+
const str = (field, body) => {
|
|
54298
|
+
const re = new RegExp(`${field}\\s*:\\s*["'\`]([^"'\`]+)["'\`]`);
|
|
54299
|
+
const m = re.exec(body);
|
|
54300
|
+
return m ? m[1] : null;
|
|
54301
|
+
};
|
|
54302
|
+
function parseSqlxConfig(body) {
|
|
54303
|
+
let database = str("database", body);
|
|
54304
|
+
if (!database && /database\s*:\s*(dataform|sqlanvil)\.projectConfig\.defaultDatabase/.test(body)) {
|
|
54305
|
+
database = "<defaultDatabase>";
|
|
54306
|
+
}
|
|
54307
|
+
return {
|
|
54308
|
+
type: str("type", body),
|
|
54309
|
+
schema: str("schema", body),
|
|
54310
|
+
database,
|
|
54311
|
+
name: str("name", body),
|
|
54312
|
+
description: str("description", body),
|
|
54313
|
+
};
|
|
54314
|
+
}
|
|
54315
|
+
const GLOBAL_RULES = [
|
|
54316
|
+
{
|
|
54317
|
+
pattern: /\bdataform\.projectConfig\b/g,
|
|
54318
|
+
replacement: "sqlanvil.projectConfig",
|
|
54319
|
+
kind: "rewrite",
|
|
54320
|
+
note: "dataform.projectConfig → sqlanvil.projectConfig (compile global renamed)",
|
|
54321
|
+
},
|
|
54322
|
+
];
|
|
54323
|
+
const SQL_RULES = [
|
|
54324
|
+
{
|
|
54325
|
+
pattern: /\bSAFE_CAST\s*\(/g,
|
|
54326
|
+
replacement: "CAST(",
|
|
54327
|
+
kind: "rewrite",
|
|
54328
|
+
note: "SAFE_CAST → CAST — Postgres CAST raises on bad input; verify the data or guard it",
|
|
54329
|
+
},
|
|
54330
|
+
{
|
|
54331
|
+
pattern: /\bCURRENT_DATE\s*\(\s*\)/g,
|
|
54332
|
+
replacement: "CURRENT_DATE",
|
|
54333
|
+
kind: "rewrite",
|
|
54334
|
+
note: "CURRENT_DATE() → CURRENT_DATE",
|
|
54335
|
+
},
|
|
54336
|
+
{
|
|
54337
|
+
pattern: /\bCURRENT_DATE\s*\(\s*['"]/g,
|
|
54338
|
+
kind: "flag",
|
|
54339
|
+
note: "CURRENT_DATE('<tz>') — Postgres: (CURRENT_TIMESTAMP AT TIME ZONE '<tz>')::date",
|
|
54340
|
+
},
|
|
54341
|
+
{
|
|
54342
|
+
pattern: /\bSAFE_DIVIDE\s*\(/g,
|
|
54343
|
+
kind: "flag",
|
|
54344
|
+
note: "SAFE_DIVIDE(a, b) — Postgres: a / NULLIF(b, 0)",
|
|
54345
|
+
},
|
|
54346
|
+
{
|
|
54347
|
+
pattern: /\bSAFE_OFFSET\s*\(/g,
|
|
54348
|
+
kind: "flag",
|
|
54349
|
+
note: "array[SAFE_OFFSET(n)] — Postgres arrays are 1-based; use (split_part / arr[n+1])",
|
|
54350
|
+
},
|
|
54351
|
+
{ pattern: /\bQUALIFY\b/gi, kind: "flag", note: "QUALIFY — rewrite as a subquery/CTE filter on the window function" },
|
|
54352
|
+
{ pattern: /\bUNNEST\s*\(/g, kind: "flag", note: "UNNEST — Postgres unnest() works on arrays only (no STRUCT arrays); verify semantics" },
|
|
54353
|
+
{ pattern: /\bSTRUCT\s*[(<]/g, kind: "flag", note: "STRUCT — no Postgres equivalent; consider jsonb or a composite type" },
|
|
54354
|
+
{ pattern: /\bARRAY\s*</g, kind: "flag", note: "ARRAY<T> type syntax — Postgres uses T[]" },
|
|
54355
|
+
{ pattern: /\bFORMAT_DATE\s*\(/g, kind: "flag", note: "FORMAT_DATE → TO_CHAR (different format tokens)" },
|
|
54356
|
+
{ pattern: /\bPARSE_DATE\s*\(/g, kind: "flag", note: "PARSE_DATE → TO_DATE (different format tokens)" },
|
|
54357
|
+
{ pattern: /\bFORMAT_TIMESTAMP\s*\(/g, kind: "flag", note: "FORMAT_TIMESTAMP → TO_CHAR (different format tokens)" },
|
|
54358
|
+
{
|
|
54359
|
+
pattern: /\b(DATE_ADD|DATE_SUB|TIMESTAMP_ADD|TIMESTAMP_SUB|DATETIME_ADD|DATETIME_SUB)\s*\(/g,
|
|
54360
|
+
kind: "flag",
|
|
54361
|
+
note: "BigQuery date arithmetic — Postgres uses interval arithmetic (x + INTERVAL '1 day')",
|
|
54362
|
+
},
|
|
54363
|
+
{ pattern: /\bDATE_DIFF\s*\(/g, kind: "flag", note: "DATE_DIFF — Postgres: subtraction / EXTRACT(EPOCH FROM ...) depending on the part" },
|
|
54364
|
+
{ pattern: /_PARTITIONTIME|_PARTITIONDATE|_TABLE_SUFFIX/g, kind: "flag", note: "BigQuery pseudo-column has no Postgres equivalent" },
|
|
54365
|
+
{ pattern: /\bEXPORT\s+DATA\b/gi, kind: "flag", note: "EXPORT DATA — use a sqlanvil `type: \"export\"` action instead" },
|
|
54366
|
+
{ pattern: /\bEXECUTE\s+IMMEDIATE\b/gi, kind: "flag", note: "BigQuery scripting (EXECUTE IMMEDIATE) — rewrite as a PL/pgSQL DO block or operations" },
|
|
54367
|
+
{ pattern: /\bGENERATE_UUID\s*\(\s*\)/g, kind: "flag", note: "GENERATE_UUID() → gen_random_uuid()" },
|
|
54368
|
+
{
|
|
54369
|
+
pattern: /`[A-Za-z0-9_-]+\.[A-Za-z0-9_$]+\.[A-Za-z0-9_$]+`/g,
|
|
54370
|
+
kind: "flag",
|
|
54371
|
+
note: "backticked BigQuery FQN — use ${ref(...)} (declare it as a source) or a quoted Postgres name",
|
|
54372
|
+
},
|
|
54373
|
+
];
|
|
54374
|
+
const CONFIG_BQ_KEYS = /^(\s*)(bigquery\s*:|partitionBy\s*:|clusterBy\s*:|requirePartitionFilter\s*:|partitionExpirationDays\s*:|database\s*:)/;
|
|
54375
|
+
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
54376
|
+
const connectionNameFor = (project) => `bq_${sanitize(project)}`;
|
|
54377
|
+
function convertDeclaration(source, parsed, connection, sourceDataset, filePath) {
|
|
54378
|
+
const span = findConfigBlock(source);
|
|
54379
|
+
const description = parsed.description
|
|
54380
|
+
? ` description: ${JSON.stringify(parsed.description)},\n`
|
|
54381
|
+
: "";
|
|
54382
|
+
const newConfig = `config {
|
|
54383
|
+
type: "declaration",
|
|
54384
|
+
connection: ${JSON.stringify(connection)},
|
|
54385
|
+
schema: ${JSON.stringify(sourceDataset)},
|
|
54386
|
+
name: ${JSON.stringify(parsed.name)},
|
|
54387
|
+
${description} columnTypes: {
|
|
54388
|
+
// SQLANVIL-MIGRATE(TODO): scaffold the columns from the live source:
|
|
54389
|
+
// sqlanvil introspect ${connection} ${sourceDataset}.${parsed.name} --output ${filePath}
|
|
54390
|
+
},
|
|
54391
|
+
}`;
|
|
54392
|
+
const content = source.slice(0, span.start) + newConfig + source.slice(span.end);
|
|
54393
|
+
return {
|
|
54394
|
+
content,
|
|
54395
|
+
findings: [
|
|
54396
|
+
{
|
|
54397
|
+
line: 1,
|
|
54398
|
+
kind: "rewrite",
|
|
54399
|
+
note: `declaration now reads BigQuery through connection "${connection}" (runner-extract); columnTypes need introspect`,
|
|
54400
|
+
},
|
|
54401
|
+
],
|
|
54402
|
+
};
|
|
54403
|
+
}
|
|
54404
|
+
function convertTarget(source, jsMode) {
|
|
54405
|
+
const span = jsMode ? null : findConfigBlock(source);
|
|
54406
|
+
const findings = [];
|
|
54407
|
+
const lines = source.split("\n");
|
|
54408
|
+
const jsLines = jsMode ? new Set() : jsBlockLines(source);
|
|
54409
|
+
let configStartLine = -1;
|
|
54410
|
+
let configEndLine = -1;
|
|
54411
|
+
if (span) {
|
|
54412
|
+
configStartLine = source.slice(0, span.start).split("\n").length - 1;
|
|
54413
|
+
configEndLine = source.slice(0, span.end).split("\n").length - 1;
|
|
54414
|
+
}
|
|
54415
|
+
const out = [];
|
|
54416
|
+
let commentingConfigBlockDepth = 0;
|
|
54417
|
+
let swallowLeadingComma = false;
|
|
54418
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54419
|
+
let line = lines[i];
|
|
54420
|
+
const inConfig = i >= configStartLine && i <= configEndLine && configStartLine >= 0;
|
|
54421
|
+
const markersSafe = !jsMode && !jsLines.has(i);
|
|
54422
|
+
const marker = (note) => inConfig ? `// SQLANVIL-MIGRATE: ${note}` : `-- SQLANVIL-MIGRATE: ${note}`;
|
|
54423
|
+
const lineNo = i + 1;
|
|
54424
|
+
const notesForLine = [];
|
|
54425
|
+
for (const rule of GLOBAL_RULES) {
|
|
54426
|
+
rule.pattern.lastIndex = 0;
|
|
54427
|
+
if (rule.pattern.test(line)) {
|
|
54428
|
+
line = line.replace(rule.pattern, rule.replacement);
|
|
54429
|
+
findings.push({ line: lineNo, kind: "rewrite", note: rule.note });
|
|
54430
|
+
}
|
|
54431
|
+
}
|
|
54432
|
+
if (swallowLeadingComma) {
|
|
54433
|
+
swallowLeadingComma = false;
|
|
54434
|
+
if (/^\s*,\s*$/.test(line)) {
|
|
54435
|
+
out.push(`// ${line}`);
|
|
54436
|
+
continue;
|
|
54437
|
+
}
|
|
54438
|
+
if (/^\s*,/.test(line)) {
|
|
54439
|
+
line = line.replace(/,/, " ");
|
|
54440
|
+
}
|
|
54441
|
+
}
|
|
54442
|
+
if (inConfig && commentingConfigBlockDepth === 0) {
|
|
54443
|
+
const m = CONFIG_BQ_KEYS.exec(line);
|
|
54444
|
+
if (m) {
|
|
54445
|
+
const note = `BigQuery-only setting — not applicable on Postgres (partitioning/clustering → postgres: {}; project qualifiers drop)`;
|
|
54446
|
+
findings.push({ line: lineNo, kind: "flag", note });
|
|
54447
|
+
out.push(marker(note));
|
|
54448
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
54449
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
54450
|
+
commentingConfigBlockDepth = Math.max(0, opens - closes);
|
|
54451
|
+
if (commentingConfigBlockDepth === 0)
|
|
54452
|
+
swallowLeadingComma = !/,\s*$/.test(line) ? true : false;
|
|
54453
|
+
out.push(`// ${line}`);
|
|
54454
|
+
continue;
|
|
54455
|
+
}
|
|
54456
|
+
}
|
|
54457
|
+
else if (commentingConfigBlockDepth > 0) {
|
|
54458
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
54459
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
54460
|
+
commentingConfigBlockDepth = Math.max(0, commentingConfigBlockDepth + opens - closes);
|
|
54461
|
+
if (commentingConfigBlockDepth === 0 && !/,\s*$/.test(line))
|
|
54462
|
+
swallowLeadingComma = true;
|
|
54463
|
+
out.push(`// ${line}`);
|
|
54464
|
+
continue;
|
|
54465
|
+
}
|
|
54466
|
+
if (!inConfig || jsMode) {
|
|
54467
|
+
for (const rule of SQL_RULES) {
|
|
54468
|
+
rule.pattern.lastIndex = 0;
|
|
54469
|
+
if (!rule.pattern.test(line))
|
|
54470
|
+
continue;
|
|
54471
|
+
if (rule.replacement !== undefined) {
|
|
54472
|
+
rule.pattern.lastIndex = 0;
|
|
54473
|
+
line = line.replace(rule.pattern, rule.replacement);
|
|
54474
|
+
findings.push({ line: lineNo, kind: "rewrite", note: rule.note });
|
|
54475
|
+
if (markersSafe)
|
|
54476
|
+
notesForLine.push(rule.note);
|
|
54477
|
+
}
|
|
54478
|
+
else {
|
|
54479
|
+
findings.push({ line: lineNo, kind: "flag", note: rule.note });
|
|
54480
|
+
if (markersSafe)
|
|
54481
|
+
notesForLine.push(rule.note);
|
|
54482
|
+
}
|
|
54483
|
+
}
|
|
54484
|
+
}
|
|
54485
|
+
for (const note of notesForLine) {
|
|
54486
|
+
out.push(marker(note));
|
|
54487
|
+
}
|
|
54488
|
+
out.push(line);
|
|
54489
|
+
}
|
|
54490
|
+
let content = out.join("\n");
|
|
54491
|
+
if (jsMode && findings.length > 0) {
|
|
54492
|
+
const noted = findings.slice(0, 12).map(f => ` * L${f.line}: ${f.note}`);
|
|
54493
|
+
const more = findings.length > 12 ? [` * …and ${findings.length - 12} more (see migration-report.json)`] : [];
|
|
54494
|
+
content =
|
|
54495
|
+
["/* SQLANVIL-MIGRATE — findings in this file (inline markers are unsafe in JS templates):", ...noted, ...more, " */"].join("\n") +
|
|
54496
|
+
"\n" +
|
|
54497
|
+
content;
|
|
54498
|
+
}
|
|
54499
|
+
return { content, findings };
|
|
54500
|
+
}
|
|
54501
|
+
function readSourceConfig(srcDir) {
|
|
54502
|
+
var _a, _b, _c, _d, _e;
|
|
54503
|
+
const wsPath = path__namespace.join(srcDir, "workflow_settings.yaml");
|
|
54504
|
+
const djPath = path__namespace.join(srcDir, "dataform.json");
|
|
54505
|
+
let raw;
|
|
54506
|
+
if (fs__namespace$1.existsSync(wsPath)) {
|
|
54507
|
+
raw = jsYaml.load(fs__namespace$1.readFileSync(wsPath, "utf8")) || {};
|
|
54508
|
+
}
|
|
54509
|
+
else if (fs__namespace$1.existsSync(djPath)) {
|
|
54510
|
+
raw = JSON.parse(fs__namespace$1.readFileSync(djPath, "utf8"));
|
|
54511
|
+
}
|
|
54512
|
+
else {
|
|
54513
|
+
throw new Error(`${srcDir} does not look like a Dataform project (no workflow_settings.yaml or dataform.json).`);
|
|
54514
|
+
}
|
|
54515
|
+
const s = (k) => (typeof raw[k] === "string" ? raw[k] : null);
|
|
54516
|
+
return {
|
|
54517
|
+
defaultProject: (_a = s("defaultProject")) !== null && _a !== void 0 ? _a : s("defaultDatabase"),
|
|
54518
|
+
defaultDataset: (_c = (_b = s("defaultDataset")) !== null && _b !== void 0 ? _b : s("defaultSchema")) !== null && _c !== void 0 ? _c : "dataform",
|
|
54519
|
+
defaultAssertionDataset: (_d = s("defaultAssertionDataset")) !== null && _d !== void 0 ? _d : s("assertionSchema"),
|
|
54520
|
+
vars: (_e = raw.vars) !== null && _e !== void 0 ? _e : null,
|
|
54521
|
+
raw,
|
|
54522
|
+
};
|
|
54523
|
+
}
|
|
54524
|
+
async function migrateDataform(opts) {
|
|
54525
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
54526
|
+
const srcDir = path__namespace.resolve(opts.srcDir);
|
|
54527
|
+
const outDir = path__namespace.resolve(opts.outDir);
|
|
54528
|
+
if (containsPath(srcDir, outDir)) {
|
|
54529
|
+
throw new Error("The output directory must not be inside the source project.");
|
|
54530
|
+
}
|
|
54531
|
+
if (containsPath(outDir, srcDir)) {
|
|
54532
|
+
throw new Error("The source project must not be inside the output directory.");
|
|
54533
|
+
}
|
|
54534
|
+
if (fs__namespace$1.existsSync(outDir) && fs__namespace$1.readdirSync(outDir).length > 0) {
|
|
54535
|
+
throw new Error(`Output directory ${outDir} is not empty.`);
|
|
54536
|
+
}
|
|
54537
|
+
const sourceConfig = readSourceConfig(srcDir);
|
|
54538
|
+
const writer = new OutWriter(outDir);
|
|
54539
|
+
skippedForSafety = new Set();
|
|
54540
|
+
const files = walkFiles(srcDir);
|
|
54541
|
+
const report = {
|
|
54542
|
+
generator: `sqlanvil migrate-dataform ${version}`,
|
|
54543
|
+
sourceDir: srcDir,
|
|
54544
|
+
sourceConfig: sourceConfig.raw,
|
|
54545
|
+
inventory: { sqlxFiles: 0, jsFiles: 0, byType: {} },
|
|
54546
|
+
connections: [],
|
|
54547
|
+
overlappingSchemas: [],
|
|
54548
|
+
files: [],
|
|
54549
|
+
skippedForSafety: [],
|
|
54550
|
+
warnings: [],
|
|
54551
|
+
};
|
|
54552
|
+
const sqlxFiles = [];
|
|
54553
|
+
for (const rel of files) {
|
|
54554
|
+
if (!rel.endsWith(".sqlx"))
|
|
54555
|
+
continue;
|
|
54556
|
+
const source = fs__namespace$1.readFileSync(path__namespace.join(srcDir, rel), "utf8");
|
|
54557
|
+
const span = findConfigBlock(source);
|
|
54558
|
+
const parsed = span
|
|
54559
|
+
? parseSqlxConfig(span.body)
|
|
54560
|
+
: { type: null, schema: null, database: null, name: null, description: null };
|
|
54561
|
+
sqlxFiles.push({ rel, source, parsed });
|
|
54562
|
+
report.inventory.sqlxFiles++;
|
|
54563
|
+
const t = (_a = parsed.type) !== null && _a !== void 0 ? _a : "(none)";
|
|
54564
|
+
report.inventory.byType[t] = ((_b = report.inventory.byType[t]) !== null && _b !== void 0 ? _b : 0) + 1;
|
|
54565
|
+
}
|
|
54566
|
+
const resolveDatabase = (db) => { var _a; return !db || db === "<defaultDatabase>" ? (_a = sourceConfig.defaultProject) !== null && _a !== void 0 ? _a : "UNKNOWN_PROJECT" : db; };
|
|
54567
|
+
const declarationSchemas = new Set();
|
|
54568
|
+
const targetSchemas = new Set();
|
|
54569
|
+
const connectionMap = new Map();
|
|
54570
|
+
for (const f of sqlxFiles) {
|
|
54571
|
+
if (f.parsed.type === "declaration") {
|
|
54572
|
+
const dataset = (_c = f.parsed.schema) !== null && _c !== void 0 ? _c : sourceConfig.defaultDataset;
|
|
54573
|
+
const project = resolveDatabase(f.parsed.database);
|
|
54574
|
+
declarationSchemas.add(dataset);
|
|
54575
|
+
const name = connectionNameFor(project);
|
|
54576
|
+
const existing = connectionMap.get(name);
|
|
54577
|
+
if (existing) {
|
|
54578
|
+
existing.declarationCount++;
|
|
54579
|
+
if (!existing.datasets.includes(dataset))
|
|
54580
|
+
existing.datasets.push(dataset);
|
|
54581
|
+
}
|
|
54582
|
+
else {
|
|
54583
|
+
connectionMap.set(name, {
|
|
54584
|
+
name,
|
|
54585
|
+
project,
|
|
54586
|
+
datasets: [dataset],
|
|
54587
|
+
declarationCount: 1,
|
|
54588
|
+
introspectExample: `sqlanvil introspect ${name} <dataset>.<table> --output <declaration.sqlx>`,
|
|
54589
|
+
});
|
|
54590
|
+
}
|
|
54591
|
+
}
|
|
54592
|
+
else if (f.parsed.type) {
|
|
54593
|
+
targetSchemas.add((_d = f.parsed.schema) !== null && _d !== void 0 ? _d : sourceConfig.defaultDataset);
|
|
54594
|
+
}
|
|
54595
|
+
}
|
|
54596
|
+
report.connections = [...connectionMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
54597
|
+
report.overlappingSchemas = [...declarationSchemas].filter(s => targetSchemas.has(s)).sort();
|
|
54598
|
+
for (const rel of files) {
|
|
54599
|
+
const abs = path__namespace.join(srcDir, rel);
|
|
54600
|
+
if (rel === "workflow_settings.yaml" || rel === "dataform.json") {
|
|
54601
|
+
continue;
|
|
54602
|
+
}
|
|
54603
|
+
if (rel.endsWith(".sqlx")) {
|
|
54604
|
+
const info = sqlxFiles.find(f => f.rel === rel);
|
|
54605
|
+
if (info.parsed.type === "declaration") {
|
|
54606
|
+
const dataset = (_e = info.parsed.schema) !== null && _e !== void 0 ? _e : sourceConfig.defaultDataset;
|
|
54607
|
+
const project = resolveDatabase(info.parsed.database);
|
|
54608
|
+
const connection = connectionNameFor(project);
|
|
54609
|
+
const { content, findings } = convertDeclaration(info.source, info.parsed, connection, dataset, rel);
|
|
54610
|
+
writer.write(rel, content);
|
|
54611
|
+
report.files.push({
|
|
54612
|
+
file: rel,
|
|
54613
|
+
action: "declaration",
|
|
54614
|
+
type: "declaration",
|
|
54615
|
+
status: "rewritten",
|
|
54616
|
+
findings,
|
|
54617
|
+
connection,
|
|
54618
|
+
});
|
|
54619
|
+
}
|
|
54620
|
+
else {
|
|
54621
|
+
const { content, findings } = convertTarget(info.source, false);
|
|
54622
|
+
writer.write(rel, content);
|
|
54623
|
+
report.files.push({
|
|
54624
|
+
file: rel,
|
|
54625
|
+
action: "target",
|
|
54626
|
+
type: (_f = info.parsed.type) !== null && _f !== void 0 ? _f : "(none)",
|
|
54627
|
+
status: findings.length === 0
|
|
54628
|
+
? "clean"
|
|
54629
|
+
: findings.some(f => f.kind === "flag")
|
|
54630
|
+
? "flagged"
|
|
54631
|
+
: "rewritten",
|
|
54632
|
+
findings,
|
|
54633
|
+
});
|
|
54634
|
+
}
|
|
54635
|
+
continue;
|
|
54636
|
+
}
|
|
54637
|
+
if (rel.endsWith(".js") && (rel.startsWith("definitions/") || rel.startsWith("includes/"))) {
|
|
54638
|
+
report.inventory.jsFiles++;
|
|
54639
|
+
const source = fs__namespace$1.readFileSync(abs, "utf8");
|
|
54640
|
+
const { content, findings } = convertTarget(source, true);
|
|
54641
|
+
writer.write(rel, content);
|
|
54642
|
+
const isIncludes = rel.startsWith("includes/");
|
|
54643
|
+
if (isIncludes && findings.length > 0) {
|
|
54644
|
+
report.warnings.push(`includes helper ${rel} emits BigQuery-flavored SQL — review its ${findings.length} finding(s)`);
|
|
54645
|
+
}
|
|
54646
|
+
report.files.push({
|
|
54647
|
+
file: rel,
|
|
54648
|
+
action: isIncludes ? "includes" : "target",
|
|
54649
|
+
type: "js",
|
|
54650
|
+
status: findings.length === 0
|
|
54651
|
+
? "clean"
|
|
54652
|
+
: findings.some(f => f.kind === "flag")
|
|
54653
|
+
? "flagged"
|
|
54654
|
+
: "rewritten",
|
|
54655
|
+
findings,
|
|
54656
|
+
});
|
|
54657
|
+
continue;
|
|
54658
|
+
}
|
|
54659
|
+
writer.write(rel, fs__namespace$1.readFileSync(abs));
|
|
54660
|
+
report.files.push({ file: rel, action: "copied", status: "clean", findings: [] });
|
|
54661
|
+
}
|
|
54662
|
+
const settings = {
|
|
54663
|
+
warehouse: "supabase",
|
|
54664
|
+
defaultDataset: sourceConfig.defaultDataset.toLowerCase(),
|
|
54665
|
+
defaultAssertionDataset: ((_g = sourceConfig.defaultAssertionDataset) !== null && _g !== void 0 ? _g : "sqlanvil_assertions").toLowerCase(),
|
|
54666
|
+
sqlanvilCoreVersion: (_h = opts.coreVersion) !== null && _h !== void 0 ? _h : version,
|
|
54667
|
+
};
|
|
54668
|
+
if (sourceConfig.vars)
|
|
54669
|
+
settings.vars = sourceConfig.vars;
|
|
54670
|
+
if (report.connections.length > 0) {
|
|
54671
|
+
settings.connections = Object.fromEntries(report.connections.map(c => {
|
|
54672
|
+
var _a;
|
|
54673
|
+
return [
|
|
54674
|
+
c.name,
|
|
54675
|
+
{
|
|
54676
|
+
platform: "bigquery",
|
|
54677
|
+
project: c.project,
|
|
54678
|
+
billingProject: (_a = sourceConfig.defaultProject) !== null && _a !== void 0 ? _a : "REPLACE_WITH_YOUR_GCP_PROJECT",
|
|
54679
|
+
mode: "runner-extract",
|
|
54680
|
+
},
|
|
54681
|
+
];
|
|
54682
|
+
}));
|
|
54683
|
+
}
|
|
54684
|
+
writer.write("workflow_settings.yaml", jsYaml.dump(settings));
|
|
54685
|
+
report.skippedForSafety = [...skippedForSafety].sort();
|
|
54686
|
+
if (skippedForSafety.size > 0) {
|
|
54687
|
+
report.warnings.push(`Skipped ${skippedForSafety.size} file(s)/dir(s) for safety (credentials, build artifacts, VCS) — see skippedForSafety`);
|
|
54688
|
+
}
|
|
54689
|
+
writer.write("migration-report.json", JSON.stringify(report, null, 2));
|
|
54690
|
+
writer.write("migration-report.md", renderReportMd(report));
|
|
54691
|
+
return report;
|
|
54692
|
+
}
|
|
54693
|
+
function renderReportMd(r) {
|
|
54694
|
+
const targets = r.files.filter(f => f.action === "target");
|
|
54695
|
+
const flagged = targets.filter(f => f.status === "flagged");
|
|
54696
|
+
const rewritten = targets.filter(f => f.status === "rewritten");
|
|
54697
|
+
const clean = targets.filter(f => f.status === "clean");
|
|
54698
|
+
const declarations = r.files.filter(f => f.action === "declaration");
|
|
54699
|
+
const lines = [];
|
|
54700
|
+
lines.push(`# Dataform → SQLAnvil migration report`);
|
|
54701
|
+
lines.push("");
|
|
54702
|
+
lines.push(`Generated by ${r.generator} from \`${r.sourceDir}\` (source untouched).`);
|
|
54703
|
+
lines.push("");
|
|
54704
|
+
lines.push(`## Inventory`);
|
|
54705
|
+
lines.push("");
|
|
54706
|
+
lines.push(`- ${r.inventory.sqlxFiles} .sqlx, ${r.inventory.jsFiles} .js`);
|
|
54707
|
+
for (const [t, n] of Object.entries(r.inventory.byType).sort((a, b) => b[1] - a[1])) {
|
|
54708
|
+
lines.push(` - ${t}: ${n}`);
|
|
54709
|
+
}
|
|
54710
|
+
lines.push("");
|
|
54711
|
+
lines.push(`## Source connections (${r.connections.length})`);
|
|
54712
|
+
lines.push("");
|
|
54713
|
+
lines.push(`Declarations keep reading BigQuery through named \`connections:\` in workflow_settings.yaml ` +
|
|
54714
|
+
`(mode: runner-extract — no Vault, no wrappers). Each declaration needs \`columnTypes\` ` +
|
|
54715
|
+
`before it can extract; scaffold them with \`sqlanvil introspect\`:`);
|
|
54716
|
+
lines.push("");
|
|
54717
|
+
for (const c of r.connections) {
|
|
54718
|
+
lines.push(`- **${c.name}** — project \`${c.project}\`, ${c.declarationCount} declaration(s) across ` +
|
|
54719
|
+
`${c.datasets.length} dataset(s): ${c.datasets.slice(0, 8).map(d => `\`${d}\``).join(", ")}` +
|
|
54720
|
+
(c.datasets.length > 8 ? ", …" : ""));
|
|
54721
|
+
lines.push(` - \`${c.introspectExample}\``);
|
|
54722
|
+
}
|
|
54723
|
+
if (r.overlappingSchemas.length > 0) {
|
|
54724
|
+
lines.push("");
|
|
54725
|
+
lines.push(`## ⚠ Schemas that are BOTH source and target`);
|
|
54726
|
+
lines.push("");
|
|
54727
|
+
lines.push(`These schemas have declarations AND materializing actions: ${r.overlappingSchemas
|
|
54728
|
+
.map(s => `\`${s}\``)
|
|
54729
|
+
.join(", ")}. ` +
|
|
54730
|
+
`Their declared tables are probably produced by this pipeline (or a sibling) — once those ` +
|
|
54731
|
+
`producers move to Postgres, convert the declarations to plain local declarations instead ` +
|
|
54732
|
+
`of BigQuery connections.`);
|
|
54733
|
+
}
|
|
54734
|
+
lines.push("");
|
|
54735
|
+
lines.push(`## Target files (${targets.length})`);
|
|
54736
|
+
lines.push("");
|
|
54737
|
+
lines.push(`- clean: ${clean.length}`);
|
|
54738
|
+
lines.push(`- rewritten (safe lexical changes only): ${rewritten.length}`);
|
|
54739
|
+
lines.push(`- **flagged (need dialect review): ${flagged.length}** — every location is marked ` +
|
|
54740
|
+
`inline with \`SQLANVIL-MIGRATE:\``);
|
|
54741
|
+
lines.push("");
|
|
54742
|
+
for (const f of flagged) {
|
|
54743
|
+
lines.push(`- \`${f.file}\` (${f.type})`);
|
|
54744
|
+
for (const finding of f.findings.filter(x => x.kind === "flag").slice(0, 6)) {
|
|
54745
|
+
lines.push(` - L${finding.line}: ${finding.note}`);
|
|
54746
|
+
}
|
|
54747
|
+
const more = f.findings.filter(x => x.kind === "flag").length - 6;
|
|
54748
|
+
if (more > 0)
|
|
54749
|
+
lines.push(` - …and ${more} more (see migration-report.json)`);
|
|
54750
|
+
}
|
|
54751
|
+
lines.push("");
|
|
54752
|
+
lines.push(`## Declarations (${declarations.length})`);
|
|
54753
|
+
lines.push("");
|
|
54754
|
+
lines.push(`Every declaration was rewritten to ride its connection. The BigQuery data does not move.`);
|
|
54755
|
+
if (r.warnings.length > 0) {
|
|
54756
|
+
lines.push("");
|
|
54757
|
+
lines.push(`## Warnings`);
|
|
54758
|
+
lines.push("");
|
|
54759
|
+
for (const w of r.warnings)
|
|
54760
|
+
lines.push(`- ${w}`);
|
|
54761
|
+
}
|
|
54762
|
+
lines.push("");
|
|
54763
|
+
lines.push(`## Finish the migration`);
|
|
54764
|
+
lines.push("");
|
|
54765
|
+
lines.push(`1. \`sqlanvil compile\` — should already succeed.`);
|
|
54766
|
+
lines.push(`2. Scaffold \`columnTypes\` for the declarations you actually read (see connections above).`);
|
|
54767
|
+
lines.push(`3. \`sqlanvil validate\` against your Postgres/Supabase warehouse — PASS/FAILURE/BLOCKED per ` +
|
|
54768
|
+
`model is the migration to-do list. Work the flagged files until validate is green.`);
|
|
54769
|
+
lines.push(`4. This report is machine-readable (migration-report.json) — pointing an AI agent at it plus ` +
|
|
54770
|
+
`the \`SQLANVIL-MIGRATE:\` markers is the fastest path through the dialect work.`);
|
|
54771
|
+
lines.push("");
|
|
54772
|
+
return lines.join("\n");
|
|
54773
|
+
}
|
|
54774
|
+
|
|
54064
54775
|
const CHECKER_SOURCE = `
|
|
54065
54776
|
import json, os, sys
|
|
54066
54777
|
|
|
@@ -56125,7 +56836,7 @@ function runCli() {
|
|
|
56125
56836
|
{
|
|
56126
56837
|
format: `init [${projectDirOption.name}] [${ProjectConfigOptions.defaultDatabase.name}]` +
|
|
56127
56838
|
` [${ProjectConfigOptions.defaultLocation.name}]`,
|
|
56128
|
-
description: "Create a new sqlanvil project (BigQuery, Postgres, or
|
|
56839
|
+
description: "Create a new sqlanvil project (BigQuery, Postgres, Supabase, or MySQL/MariaDB).",
|
|
56129
56840
|
positionalOptions: [
|
|
56130
56841
|
projectDirOption,
|
|
56131
56842
|
positionalOption(ProjectConfigOptions.defaultDatabase.name, {
|
|
@@ -56729,6 +57440,37 @@ function runCli() {
|
|
|
56729
57440
|
}
|
|
56730
57441
|
return 0;
|
|
56731
57442
|
}
|
|
57443
|
+
},
|
|
57444
|
+
{
|
|
57445
|
+
format: "migrate-dataform <source-dir> <out-dir>",
|
|
57446
|
+
description: "Convert a Dataform/BigQuery project into a sqlanvil-on-Postgres project. The source " +
|
|
57447
|
+
"directory is READ-ONLY; the converted project + migration-report.{md,json} land in " +
|
|
57448
|
+
"<out-dir> (must be empty). Sources stay in BigQuery as named connections; target SQL " +
|
|
57449
|
+
"gets safe rewrites + inline SQLANVIL-MIGRATE markers for dialect review.",
|
|
57450
|
+
positionalOptions: [
|
|
57451
|
+
positionalOption("source-dir", {
|
|
57452
|
+
describe: "The Dataform project to convert (never modified)."
|
|
57453
|
+
}),
|
|
57454
|
+
positionalOption("out-dir", {
|
|
57455
|
+
describe: "Where the converted sqlanvil project is written (created; must be empty)."
|
|
57456
|
+
})
|
|
57457
|
+
],
|
|
57458
|
+
options: [],
|
|
57459
|
+
processFn: async (argv) => {
|
|
57460
|
+
const report = await migrateDataform({
|
|
57461
|
+
srcDir: argv["source-dir"],
|
|
57462
|
+
outDir: argv["out-dir"]
|
|
57463
|
+
});
|
|
57464
|
+
const targets = report.files.filter(f => f.action === "target");
|
|
57465
|
+
const flagged = targets.filter(f => f.status === "flagged").length;
|
|
57466
|
+
printSuccess(`Converted ${report.inventory.sqlxFiles} .sqlx file(s): ` +
|
|
57467
|
+
`${report.connections.length} source connection(s) over ` +
|
|
57468
|
+
`${report.files.filter(f => f.action === "declaration").length} declaration(s); ` +
|
|
57469
|
+
`${targets.length} target file(s), ${flagged} flagged for dialect review.`);
|
|
57470
|
+
print(`Report: ${path__namespace.join(argv["out-dir"], "migration-report.md")}`);
|
|
57471
|
+
print(`Next: sqlanvil compile ${argv["out-dir"]}`);
|
|
57472
|
+
return 0;
|
|
57473
|
+
}
|
|
56732
57474
|
}
|
|
56733
57475
|
]
|
|
56734
57476
|
})
|