@sqlanvil/cli 1.21.0 → 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/bundle.js +633 -5
- package/package.json +1 -1
package/bundle.js
CHANGED
|
@@ -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) {
|
|
@@ -53541,7 +53541,7 @@ class Runner {
|
|
|
53541
53541
|
return actionResult;
|
|
53542
53542
|
}
|
|
53543
53543
|
async executeTask(client, task, parentAction, options, action) {
|
|
53544
|
-
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;
|
|
53545
53545
|
const timer = Timer.start();
|
|
53546
53546
|
const taskResult = {
|
|
53547
53547
|
status: sqlanvil.TaskResult.ExecutionStatus.RUNNING,
|
|
@@ -53613,7 +53613,14 @@ class Runner {
|
|
|
53613
53613
|
}
|
|
53614
53614
|
else if (task.type === "extract") {
|
|
53615
53615
|
try {
|
|
53616
|
-
|
|
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"
|
|
53617
53624
|
? this.executionOptions.mysqlExtract || runMysqlExtract
|
|
53618
53625
|
: this.executionOptions.bigQueryExtract || runBigQueryExtract;
|
|
53619
53626
|
await extractor({
|
|
@@ -53643,7 +53650,7 @@ class Runner {
|
|
|
53643
53650
|
}), task.type === "operation" ? 1 : options.bigquery.actionRetryLimit + 1 || 1);
|
|
53644
53651
|
taskResult.metadata = metadata;
|
|
53645
53652
|
if (task.type === "assertion") {
|
|
53646
|
-
const rowCount = (
|
|
53653
|
+
const rowCount = (_o = rows[0]) === null || _o === void 0 ? void 0 : _o[Object.keys(rows[0])[0]];
|
|
53647
53654
|
if (rowCount > 0) {
|
|
53648
53655
|
throw new Error(`Assertion failed: query returned ${rowCount} row(s).`);
|
|
53649
53656
|
}
|
|
@@ -53655,7 +53662,7 @@ class Runner {
|
|
|
53655
53662
|
? sqlanvil.TaskResult.ExecutionStatus.CANCELLED
|
|
53656
53663
|
: sqlanvil.TaskResult.ExecutionStatus.FAILED;
|
|
53657
53664
|
taskResult.errorMessage = `${this.graph.projectConfig.warehouse} error: ${e.message}`;
|
|
53658
|
-
if ((
|
|
53665
|
+
if ((_q = (_p = e.metadata) === null || _p === void 0 ? void 0 : _p.bigquery) === null || _q === void 0 ? void 0 : _q.jobId) {
|
|
53659
53666
|
taskResult.metadata = {
|
|
53660
53667
|
bigquery: {
|
|
53661
53668
|
jobId: e.metadata.bigquery.jobId
|
|
@@ -54175,6 +54182,596 @@ ${columnRows || '<tr><td colspan="3" class="meta">No documented columns.</td></t
|
|
|
54175
54182
|
`;
|
|
54176
54183
|
}
|
|
54177
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
|
+
|
|
54178
54775
|
const CHECKER_SOURCE = `
|
|
54179
54776
|
import json, os, sys
|
|
54180
54777
|
|
|
@@ -56843,6 +57440,37 @@ function runCli() {
|
|
|
56843
57440
|
}
|
|
56844
57441
|
return 0;
|
|
56845
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
|
+
}
|
|
56846
57474
|
}
|
|
56847
57475
|
]
|
|
56848
57476
|
})
|