@sqlanvil/cli 1.8.3 → 1.10.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 +892 -44
- package/package.json +1 -1
package/bundle.js
CHANGED
|
@@ -42996,6 +42996,24 @@ from (${query}) as insertions`;
|
|
|
42996
42996
|
`) AS\n${exp.query}`;
|
|
42997
42997
|
return [sqlanvil.ExecutionTask.create({ type: "statement", statement })];
|
|
42998
42998
|
}
|
|
42999
|
+
validationStubSql(table) {
|
|
43000
|
+
const target = this.resolveTarget(table.target);
|
|
43001
|
+
if (table.enumType === sqlanvil.TableType.VIEW) {
|
|
43002
|
+
return `create view ${target} as ${table.query}`;
|
|
43003
|
+
}
|
|
43004
|
+
return `create table ${target} as select * from (${table.query}) limit 0`;
|
|
43005
|
+
}
|
|
43006
|
+
createSchemaSql(schema) {
|
|
43007
|
+
return `create schema if not exists ${this.qualifiedSchema(schema)}`;
|
|
43008
|
+
}
|
|
43009
|
+
dropSchemaCascadeSql(schema) {
|
|
43010
|
+
return `drop schema if exists ${this.qualifiedSchema(schema)} cascade`;
|
|
43011
|
+
}
|
|
43012
|
+
qualifiedSchema(schema) {
|
|
43013
|
+
return this.project.defaultDatabase
|
|
43014
|
+
? `\`${this.project.defaultDatabase}.${schema}\``
|
|
43015
|
+
: `\`${schema}\``;
|
|
43016
|
+
}
|
|
42999
43017
|
buildIncrementalSchemaChangeTasks(tasks, table) {
|
|
43000
43018
|
const uniqueId = this.uniqueIdGenerator();
|
|
43001
43019
|
const emptyTempTableTarget = Object.assign(Object.assign({}, table.target), { name: `${table.target.name}_sa_temp_${uniqueId}_empty` });
|
|
@@ -43206,6 +43224,19 @@ class MysqlExecutionSql {
|
|
|
43206
43224
|
createExportTasks(exp) {
|
|
43207
43225
|
throw new Error("type: \"export\" is not supported on MySQL/MariaDB yet.");
|
|
43208
43226
|
}
|
|
43227
|
+
validationStubSql(table) {
|
|
43228
|
+
const target = this.resolveTarget(table.target);
|
|
43229
|
+
if (table.enumType === sqlanvil.TableType.VIEW) {
|
|
43230
|
+
return `create or replace view ${target} as ${table.query}`;
|
|
43231
|
+
}
|
|
43232
|
+
return `create table ${target} as select * from (${table.query}) as _sa_stub limit 0`;
|
|
43233
|
+
}
|
|
43234
|
+
createSchemaSql(schema) {
|
|
43235
|
+
return `create database if not exists \`${schema}\``;
|
|
43236
|
+
}
|
|
43237
|
+
dropSchemaCascadeSql(schema) {
|
|
43238
|
+
return `drop database if exists \`${schema}\``;
|
|
43239
|
+
}
|
|
43209
43240
|
publishTasks(table, runConfig, tableMetadata) {
|
|
43210
43241
|
const tasks = new Tasks();
|
|
43211
43242
|
const target = this.resolveTarget(table.target);
|
|
@@ -43370,6 +43401,19 @@ class PostgresExecutionSql {
|
|
|
43370
43401
|
const kind = type === sqlanvil.TableMetadata.Type.VIEW ? "view" : "table";
|
|
43371
43402
|
return `drop ${kind} if exists ${this.resolveTarget(target)} cascade`;
|
|
43372
43403
|
}
|
|
43404
|
+
validationStubSql(table) {
|
|
43405
|
+
const target = this.resolveTarget(table.target);
|
|
43406
|
+
if (table.enumType === sqlanvil.TableType.VIEW) {
|
|
43407
|
+
return `create view ${target} as ${table.query}`;
|
|
43408
|
+
}
|
|
43409
|
+
return `create table ${target} as ${table.query} with no data`;
|
|
43410
|
+
}
|
|
43411
|
+
createSchemaSql(schema) {
|
|
43412
|
+
return `create schema if not exists "${schema}"`;
|
|
43413
|
+
}
|
|
43414
|
+
dropSchemaCascadeSql(schema) {
|
|
43415
|
+
return `drop schema if exists "${schema}" cascade`;
|
|
43416
|
+
}
|
|
43373
43417
|
createExportTasks(exp) {
|
|
43374
43418
|
return [sqlanvil.ExecutionTask.create({ type: "export", statement: exp.query })];
|
|
43375
43419
|
}
|
|
@@ -43682,6 +43726,15 @@ class ExecutionSql {
|
|
|
43682
43726
|
dropIfExists(target, type) {
|
|
43683
43727
|
return this.delegate.dropIfExists(target, type);
|
|
43684
43728
|
}
|
|
43729
|
+
validationStubSql(table) {
|
|
43730
|
+
return this.delegate.validationStubSql(table);
|
|
43731
|
+
}
|
|
43732
|
+
createSchemaSql(schema) {
|
|
43733
|
+
return this.delegate.createSchemaSql(schema);
|
|
43734
|
+
}
|
|
43735
|
+
dropSchemaCascadeSql(schema) {
|
|
43736
|
+
return this.delegate.dropSchemaCascadeSql(schema);
|
|
43737
|
+
}
|
|
43685
43738
|
}
|
|
43686
43739
|
function collectEvaluationQueries(queryOrAction, concatenate, queryModifier = (q) => q) {
|
|
43687
43740
|
const validationQueries = new Array();
|
|
@@ -43739,7 +43792,7 @@ function collectEvaluationQueries(queryOrAction, concatenate, queryModifier = (q
|
|
|
43739
43792
|
.filter(validationQuery => !!validationQuery.query);
|
|
43740
43793
|
}
|
|
43741
43794
|
|
|
43742
|
-
const version = "1.
|
|
43795
|
+
const version = "1.10.0";
|
|
43743
43796
|
const dataformVersion = "3.0.60";
|
|
43744
43797
|
|
|
43745
43798
|
async function build(compiledGraph, runConfig, dbadapter) {
|
|
@@ -44406,6 +44459,53 @@ function assertConnectionCredentialsAvailable(graph, connections) {
|
|
|
44406
44459
|
}));
|
|
44407
44460
|
}
|
|
44408
44461
|
|
|
44462
|
+
function loadDuckdb() {
|
|
44463
|
+
try {
|
|
44464
|
+
return nativeRequire("@duckdb/node-api");
|
|
44465
|
+
}
|
|
44466
|
+
catch (e) {
|
|
44467
|
+
throw new Error(`This feature requires the optional "@duckdb/node-api" dependency, which failed to load: ` +
|
|
44468
|
+
`${e.message}`);
|
|
44469
|
+
}
|
|
44470
|
+
}
|
|
44471
|
+
async function runAsync(conn, sql) {
|
|
44472
|
+
await conn.run(sql);
|
|
44473
|
+
}
|
|
44474
|
+
async function allAsync(conn, sql) {
|
|
44475
|
+
const reader = await conn.runAndReadAll(sql);
|
|
44476
|
+
return reader.getRowObjects();
|
|
44477
|
+
}
|
|
44478
|
+
async function withDuckdb(fn, dbPath = ":memory:") {
|
|
44479
|
+
const { DuckDBInstance } = loadDuckdb();
|
|
44480
|
+
const instance = await DuckDBInstance.create(dbPath);
|
|
44481
|
+
const conn = await instance.connect();
|
|
44482
|
+
const done = () => {
|
|
44483
|
+
var _a, _b;
|
|
44484
|
+
try {
|
|
44485
|
+
(_a = conn.closeSync) === null || _a === void 0 ? void 0 : _a.call(conn);
|
|
44486
|
+
}
|
|
44487
|
+
catch (e) {
|
|
44488
|
+
}
|
|
44489
|
+
try {
|
|
44490
|
+
(_b = instance.closeSync) === null || _b === void 0 ? void 0 : _b.call(instance);
|
|
44491
|
+
}
|
|
44492
|
+
catch (e) {
|
|
44493
|
+
}
|
|
44494
|
+
};
|
|
44495
|
+
try {
|
|
44496
|
+
if (!process.env.HOME) {
|
|
44497
|
+
await runAsync(conn, `SET home_directory='${os__namespace.tmpdir()}'`);
|
|
44498
|
+
}
|
|
44499
|
+
const result = await fn(conn);
|
|
44500
|
+
done();
|
|
44501
|
+
return result;
|
|
44502
|
+
}
|
|
44503
|
+
catch (e) {
|
|
44504
|
+
done();
|
|
44505
|
+
throw e;
|
|
44506
|
+
}
|
|
44507
|
+
}
|
|
44508
|
+
|
|
44409
44509
|
const PG_ATTACH_ALIAS = "pg";
|
|
44410
44510
|
const SECRET_NAME = "sa_export";
|
|
44411
44511
|
function buildAttachSql(pg) {
|
|
@@ -44469,48 +44569,6 @@ function buildCopySql(selectSql, uri, format, options = {}) {
|
|
|
44469
44569
|
const optionList = [`FORMAT ${fmt}`, ...extraOptions].join(", ");
|
|
44470
44570
|
return `COPY (SELECT * FROM postgres_query('${PG_ATTACH_ALIAS}', $sa$${selectSql}$sa$)) TO '${target}' (${optionList})`;
|
|
44471
44571
|
}
|
|
44472
|
-
function loadDuckdb() {
|
|
44473
|
-
try {
|
|
44474
|
-
return nativeRequire("@duckdb/node-api");
|
|
44475
|
-
}
|
|
44476
|
-
catch (e) {
|
|
44477
|
-
throw new Error(`Exporting on Postgres/Supabase requires the optional "@duckdb/node-api" dependency, which ` +
|
|
44478
|
-
`failed to load: ${e.message}`);
|
|
44479
|
-
}
|
|
44480
|
-
}
|
|
44481
|
-
async function runAsync(conn, sql) {
|
|
44482
|
-
await conn.run(sql);
|
|
44483
|
-
}
|
|
44484
|
-
async function withConnection(fn) {
|
|
44485
|
-
const { DuckDBInstance } = loadDuckdb();
|
|
44486
|
-
const instance = await DuckDBInstance.create(":memory:");
|
|
44487
|
-
const conn = await instance.connect();
|
|
44488
|
-
const done = () => {
|
|
44489
|
-
var _a, _b;
|
|
44490
|
-
try {
|
|
44491
|
-
(_a = conn.closeSync) === null || _a === void 0 ? void 0 : _a.call(conn);
|
|
44492
|
-
}
|
|
44493
|
-
catch (e) {
|
|
44494
|
-
}
|
|
44495
|
-
try {
|
|
44496
|
-
(_b = instance.closeSync) === null || _b === void 0 ? void 0 : _b.call(instance);
|
|
44497
|
-
}
|
|
44498
|
-
catch (e) {
|
|
44499
|
-
}
|
|
44500
|
-
};
|
|
44501
|
-
try {
|
|
44502
|
-
if (!process.env.HOME) {
|
|
44503
|
-
await runAsync(conn, `SET home_directory='${os__namespace.tmpdir()}'`);
|
|
44504
|
-
}
|
|
44505
|
-
const result = await fn(conn);
|
|
44506
|
-
done();
|
|
44507
|
-
return result;
|
|
44508
|
-
}
|
|
44509
|
-
catch (e) {
|
|
44510
|
-
done();
|
|
44511
|
-
throw e;
|
|
44512
|
-
}
|
|
44513
|
-
}
|
|
44514
44572
|
async function runDuckdbExport(args) {
|
|
44515
44573
|
const { spec, selectSql, pg, storage, actionName } = args;
|
|
44516
44574
|
const uri = resolveExportUri(spec, actionName, { wildcard: false });
|
|
@@ -44519,7 +44577,7 @@ async function runDuckdbExport(args) {
|
|
|
44519
44577
|
throw new Error(`No "${scheme}" storage credentials found in .df-credentials.json (storage.${scheme}) for ` +
|
|
44520
44578
|
`export to ${uri}.`);
|
|
44521
44579
|
}
|
|
44522
|
-
return
|
|
44580
|
+
return withDuckdb(async (conn) => {
|
|
44523
44581
|
await runAsync(conn, "INSTALL postgres; LOAD postgres; INSTALL httpfs; LOAD httpfs;");
|
|
44524
44582
|
await runAsync(conn, buildAttachSql(pg));
|
|
44525
44583
|
if (scheme !== "local") {
|
|
@@ -45151,6 +45209,520 @@ function resolveCredentials(envCredentials, cliCredentials, defaultFilename) {
|
|
|
45151
45209
|
return envCredentials || defaultFilename;
|
|
45152
45210
|
}
|
|
45153
45211
|
|
|
45212
|
+
function key(target) {
|
|
45213
|
+
return targetStringifier.stringify(target);
|
|
45214
|
+
}
|
|
45215
|
+
function enumName(enumObject, value) {
|
|
45216
|
+
if (value === undefined || value === null) {
|
|
45217
|
+
return "UNKNOWN";
|
|
45218
|
+
}
|
|
45219
|
+
const match = Object.keys(enumObject).find(k => enumObject[k] === value);
|
|
45220
|
+
return match || String(value);
|
|
45221
|
+
}
|
|
45222
|
+
function tableType$1(enumType) {
|
|
45223
|
+
switch (enumType) {
|
|
45224
|
+
case sqlanvil.TableType.VIEW:
|
|
45225
|
+
return "view";
|
|
45226
|
+
case sqlanvil.TableType.INCREMENTAL:
|
|
45227
|
+
return "incremental";
|
|
45228
|
+
default:
|
|
45229
|
+
return "table";
|
|
45230
|
+
}
|
|
45231
|
+
}
|
|
45232
|
+
function toMillis(value) {
|
|
45233
|
+
if (value === undefined || value === null) {
|
|
45234
|
+
return 0;
|
|
45235
|
+
}
|
|
45236
|
+
if (typeof value === "number") {
|
|
45237
|
+
return value;
|
|
45238
|
+
}
|
|
45239
|
+
if (typeof value.toNumber === "function") {
|
|
45240
|
+
return value.toNumber();
|
|
45241
|
+
}
|
|
45242
|
+
return Number(value) || 0;
|
|
45243
|
+
}
|
|
45244
|
+
function actionRow(action, type) {
|
|
45245
|
+
const descriptor = action.actionDescriptor || {};
|
|
45246
|
+
return {
|
|
45247
|
+
target_key: key(action.target),
|
|
45248
|
+
database: action.target.database || "",
|
|
45249
|
+
schema: action.target.schema || "",
|
|
45250
|
+
name: action.target.name || "",
|
|
45251
|
+
readable_name: targetAsReadableString(action.target),
|
|
45252
|
+
type,
|
|
45253
|
+
tags: JSON.stringify(action.tags || []),
|
|
45254
|
+
disabled: !!action.disabled,
|
|
45255
|
+
file_name: action.fileName || "",
|
|
45256
|
+
description: descriptor.description || ""
|
|
45257
|
+
};
|
|
45258
|
+
}
|
|
45259
|
+
function pushDeps(action, out) {
|
|
45260
|
+
for (const dep of action.dependencyTargets || []) {
|
|
45261
|
+
out.push({
|
|
45262
|
+
from_target_key: key(action.target),
|
|
45263
|
+
to_target_key: key(dep),
|
|
45264
|
+
from_readable: targetAsReadableString(action.target),
|
|
45265
|
+
to_readable: targetAsReadableString(dep)
|
|
45266
|
+
});
|
|
45267
|
+
}
|
|
45268
|
+
}
|
|
45269
|
+
function pushColumns(action, out) {
|
|
45270
|
+
const columns = (action.actionDescriptor && action.actionDescriptor.columns) || [];
|
|
45271
|
+
for (const column of columns) {
|
|
45272
|
+
out.push({
|
|
45273
|
+
target_key: key(action.target),
|
|
45274
|
+
readable_name: targetAsReadableString(action.target),
|
|
45275
|
+
column_name: (column.path || []).join("."),
|
|
45276
|
+
description: column.description || ""
|
|
45277
|
+
});
|
|
45278
|
+
}
|
|
45279
|
+
}
|
|
45280
|
+
function catalogRows(compiledGraph) {
|
|
45281
|
+
const actions = [];
|
|
45282
|
+
const dependencies = [];
|
|
45283
|
+
const columns = [];
|
|
45284
|
+
const add = (action, type) => {
|
|
45285
|
+
actions.push(actionRow(action, type));
|
|
45286
|
+
pushDeps(action, dependencies);
|
|
45287
|
+
pushColumns(action, columns);
|
|
45288
|
+
};
|
|
45289
|
+
for (const table of compiledGraph.tables || []) {
|
|
45290
|
+
add(table, tableType$1(table.enumType));
|
|
45291
|
+
}
|
|
45292
|
+
for (const operation of compiledGraph.operations || []) {
|
|
45293
|
+
add(operation, "operation");
|
|
45294
|
+
}
|
|
45295
|
+
for (const assertion of compiledGraph.assertions || []) {
|
|
45296
|
+
add(assertion, "assertion");
|
|
45297
|
+
}
|
|
45298
|
+
for (const exp of compiledGraph.exports || []) {
|
|
45299
|
+
add(exp, "export");
|
|
45300
|
+
}
|
|
45301
|
+
for (const declaration of compiledGraph.declarations || []) {
|
|
45302
|
+
add(declaration, "declaration");
|
|
45303
|
+
}
|
|
45304
|
+
return { actions, dependencies, columns };
|
|
45305
|
+
}
|
|
45306
|
+
function runRows(runResult, runId) {
|
|
45307
|
+
const runStatus = enumName(sqlanvil.RunResult.ExecutionStatus, runResult.status);
|
|
45308
|
+
return (runResult.actions || []).map(action => {
|
|
45309
|
+
const start = toMillis(action.timing && action.timing.startTimeMillis);
|
|
45310
|
+
const end = toMillis(action.timing && action.timing.endTimeMillis);
|
|
45311
|
+
const failedTask = (action.tasks || []).find(t => !!t.errorMessage);
|
|
45312
|
+
return {
|
|
45313
|
+
run_id: runId,
|
|
45314
|
+
run_status: runStatus,
|
|
45315
|
+
target_key: key(action.target),
|
|
45316
|
+
readable_name: targetAsReadableString(action.target),
|
|
45317
|
+
status: enumName(sqlanvil.ActionResult.ExecutionStatus, action.status),
|
|
45318
|
+
start_millis: start,
|
|
45319
|
+
end_millis: end,
|
|
45320
|
+
duration_millis: start && end ? end - start : 0,
|
|
45321
|
+
error_message: (failedTask && failedTask.errorMessage) || ""
|
|
45322
|
+
};
|
|
45323
|
+
});
|
|
45324
|
+
}
|
|
45325
|
+
|
|
45326
|
+
let tmpCounter = 0;
|
|
45327
|
+
async function writeParquet(rows, outPath, columns) {
|
|
45328
|
+
await fs__namespace.ensureDir(path__namespace.dirname(outPath));
|
|
45329
|
+
const tmp = path__namespace.join(os__namespace.tmpdir(), `sa_artifact_${process.pid}_${Date.now()}_${tmpCounter++}.json`);
|
|
45330
|
+
const payload = rows.length > 0 ? rows : [columns.reduce((o, c) => ((o[c] = null), o), {})];
|
|
45331
|
+
const whereFalse = rows.length > 0 ? "" : " WHERE false";
|
|
45332
|
+
await fs__namespace.writeFile(tmp, JSON.stringify(payload));
|
|
45333
|
+
try {
|
|
45334
|
+
await withDuckdb(async (conn) => {
|
|
45335
|
+
await runAsync(conn, `COPY (SELECT * FROM read_json_auto('${tmp}')${whereFalse}) TO '${outPath}' (FORMAT parquet)`);
|
|
45336
|
+
});
|
|
45337
|
+
}
|
|
45338
|
+
finally {
|
|
45339
|
+
await fs__namespace.remove(tmp).catch(() => undefined);
|
|
45340
|
+
}
|
|
45341
|
+
}
|
|
45342
|
+
async function queryParquet(sql, views) {
|
|
45343
|
+
return withDuckdb(async (conn) => {
|
|
45344
|
+
for (const view of views) {
|
|
45345
|
+
await runAsync(conn, `create view ${view.name} as select * from read_parquet('${view.glob}')`);
|
|
45346
|
+
}
|
|
45347
|
+
return allAsync(conn, sql);
|
|
45348
|
+
});
|
|
45349
|
+
}
|
|
45350
|
+
|
|
45351
|
+
const TARGET_DIR = "target";
|
|
45352
|
+
const ACTION_COLUMNS = [
|
|
45353
|
+
"target_key",
|
|
45354
|
+
"database",
|
|
45355
|
+
"schema",
|
|
45356
|
+
"name",
|
|
45357
|
+
"readable_name",
|
|
45358
|
+
"type",
|
|
45359
|
+
"tags",
|
|
45360
|
+
"disabled",
|
|
45361
|
+
"file_name",
|
|
45362
|
+
"description"
|
|
45363
|
+
];
|
|
45364
|
+
const DEPENDENCY_COLUMNS = ["from_target_key", "to_target_key", "from_readable", "to_readable"];
|
|
45365
|
+
const COLUMN_COLUMNS = ["target_key", "readable_name", "column_name", "description"];
|
|
45366
|
+
const RUN_COLUMNS = [
|
|
45367
|
+
"run_id",
|
|
45368
|
+
"run_status",
|
|
45369
|
+
"target_key",
|
|
45370
|
+
"readable_name",
|
|
45371
|
+
"status",
|
|
45372
|
+
"start_millis",
|
|
45373
|
+
"end_millis",
|
|
45374
|
+
"duration_millis",
|
|
45375
|
+
"error_message"
|
|
45376
|
+
];
|
|
45377
|
+
async function writeArtifacts(compiledGraph, projectDir, options = {}) {
|
|
45378
|
+
const targetDir = path__namespace.join(projectDir, TARGET_DIR);
|
|
45379
|
+
const catalogDir = path__namespace.join(targetDir, "catalog");
|
|
45380
|
+
const { actions, dependencies, columns } = catalogRows(compiledGraph);
|
|
45381
|
+
await writeParquet(actions, path__namespace.join(catalogDir, "actions.parquet"), ACTION_COLUMNS);
|
|
45382
|
+
await writeParquet(dependencies, path__namespace.join(catalogDir, "dependencies.parquet"), DEPENDENCY_COLUMNS);
|
|
45383
|
+
await writeParquet(columns, path__namespace.join(catalogDir, "columns.parquet"), COLUMN_COLUMNS);
|
|
45384
|
+
if (options.runResult) {
|
|
45385
|
+
const runId = options.runId !== undefined ? options.runId : Date.now();
|
|
45386
|
+
await writeParquet(runRows(options.runResult, runId), path__namespace.join(targetDir, "runs", `run_${runId}.parquet`), RUN_COLUMNS);
|
|
45387
|
+
}
|
|
45388
|
+
return { targetDir };
|
|
45389
|
+
}
|
|
45390
|
+
async function safeWriteArtifacts(compiledGraph, projectDir, options = {}) {
|
|
45391
|
+
try {
|
|
45392
|
+
await writeArtifacts(compiledGraph, projectDir, options);
|
|
45393
|
+
}
|
|
45394
|
+
catch (e) {
|
|
45395
|
+
if (options.warn) {
|
|
45396
|
+
options.warn(`Artifacts skipped: ${e.message}`);
|
|
45397
|
+
}
|
|
45398
|
+
}
|
|
45399
|
+
}
|
|
45400
|
+
|
|
45401
|
+
function escapeHtml(value) {
|
|
45402
|
+
return String(value === null || value === undefined ? "" : value)
|
|
45403
|
+
.replace(/&/g, "&")
|
|
45404
|
+
.replace(/</g, "<")
|
|
45405
|
+
.replace(/>/g, ">")
|
|
45406
|
+
.replace(/"/g, """);
|
|
45407
|
+
}
|
|
45408
|
+
async function buildDocsModel(views, generatedAt) {
|
|
45409
|
+
const hasRuns = views.some(v => v.name === "runs");
|
|
45410
|
+
const actions = await queryParquet("select readable_name, type, tags, description from actions order by type, readable_name", views);
|
|
45411
|
+
const dependencies = await queryParquet("select from_readable, to_readable from dependencies", views);
|
|
45412
|
+
const columns = await queryParquet("select readable_name, column_name, description from columns order by readable_name, column_name", views);
|
|
45413
|
+
let latestRun;
|
|
45414
|
+
const statusByModel = new Map();
|
|
45415
|
+
if (hasRuns) {
|
|
45416
|
+
const head = await queryParquet("select max(run_id) as run_id from runs", views);
|
|
45417
|
+
const runId = head[0] && head[0].run_id !== null ? Number(head[0].run_id) : undefined;
|
|
45418
|
+
if (runId !== undefined) {
|
|
45419
|
+
const overall = await queryParquet(`select run_status from runs where run_id = ${runId} limit 1`, views);
|
|
45420
|
+
latestRun = { runId, status: overall[0] ? overall[0].run_status : "UNKNOWN" };
|
|
45421
|
+
const statuses = await queryParquet(`select readable_name, status from runs where run_id = ${runId}`, views);
|
|
45422
|
+
for (const row of statuses) {
|
|
45423
|
+
statusByModel.set(row.readable_name, row.status);
|
|
45424
|
+
}
|
|
45425
|
+
}
|
|
45426
|
+
}
|
|
45427
|
+
const dependsOn = new Map();
|
|
45428
|
+
for (const dep of dependencies) {
|
|
45429
|
+
dependsOn.set(dep.from_readable, (dependsOn.get(dep.from_readable) || []).concat(dep.to_readable));
|
|
45430
|
+
}
|
|
45431
|
+
const byTypeMap = new Map();
|
|
45432
|
+
const models = actions.map(a => {
|
|
45433
|
+
byTypeMap.set(a.type, (byTypeMap.get(a.type) || 0) + 1);
|
|
45434
|
+
let tags = [];
|
|
45435
|
+
try {
|
|
45436
|
+
tags = JSON.parse(a.tags || "[]");
|
|
45437
|
+
}
|
|
45438
|
+
catch (e) {
|
|
45439
|
+
tags = [];
|
|
45440
|
+
}
|
|
45441
|
+
return {
|
|
45442
|
+
readable: a.readable_name,
|
|
45443
|
+
type: a.type,
|
|
45444
|
+
tags,
|
|
45445
|
+
description: a.description || "",
|
|
45446
|
+
status: statusByModel.get(a.readable_name),
|
|
45447
|
+
dependsOn: dependsOn.get(a.readable_name) || []
|
|
45448
|
+
};
|
|
45449
|
+
});
|
|
45450
|
+
return {
|
|
45451
|
+
generatedAt,
|
|
45452
|
+
summary: {
|
|
45453
|
+
total: models.length,
|
|
45454
|
+
byType: Array.from(byTypeMap.entries())
|
|
45455
|
+
.map(([type, n]) => ({ type, n }))
|
|
45456
|
+
.sort((x, y) => x.type.localeCompare(y.type))
|
|
45457
|
+
},
|
|
45458
|
+
latestRun,
|
|
45459
|
+
models,
|
|
45460
|
+
columns: columns.map(c => ({
|
|
45461
|
+
readable: c.readable_name,
|
|
45462
|
+
column: c.column_name,
|
|
45463
|
+
description: c.description || ""
|
|
45464
|
+
}))
|
|
45465
|
+
};
|
|
45466
|
+
}
|
|
45467
|
+
function renderDocsHtml(model) {
|
|
45468
|
+
const summaryLine = `${model.summary.total} models — ` +
|
|
45469
|
+
model.summary.byType.map(t => `${t.n} ${t.type}`).join(", ");
|
|
45470
|
+
const runLine = model.latestRun
|
|
45471
|
+
? `Last run: <strong>${escapeHtml(model.latestRun.status)}</strong> (run ${model.latestRun.runId})`
|
|
45472
|
+
: "No runs recorded yet.";
|
|
45473
|
+
const statusBadge = (status) => {
|
|
45474
|
+
if (!status) {
|
|
45475
|
+
return "";
|
|
45476
|
+
}
|
|
45477
|
+
const cls = status === "SUCCESSFUL" ? "ok" : status === "FAILED" ? "fail" : "muted";
|
|
45478
|
+
return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
|
|
45479
|
+
};
|
|
45480
|
+
const modelRows = model.models
|
|
45481
|
+
.map(m => `<tr data-search="${escapeHtml((m.readable + " " + m.type + " " + m.tags.join(" ")).toLowerCase())}">
|
|
45482
|
+
<td><code>${escapeHtml(m.readable)}</code></td>
|
|
45483
|
+
<td>${escapeHtml(m.type)}</td>
|
|
45484
|
+
<td>${m.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join(" ")}</td>
|
|
45485
|
+
<td>${statusBadge(m.status)}</td>
|
|
45486
|
+
<td>${m.dependsOn.map(d => `<code>${escapeHtml(d)}</code>`).join("<br>")}</td>
|
|
45487
|
+
<td>${escapeHtml(m.description)}</td>
|
|
45488
|
+
</tr>`)
|
|
45489
|
+
.join("\n");
|
|
45490
|
+
const columnRows = model.columns
|
|
45491
|
+
.map(c => `<tr><td><code>${escapeHtml(c.readable)}</code></td><td><code>${escapeHtml(c.column)}</code></td><td>${escapeHtml(c.description)}</td></tr>`)
|
|
45492
|
+
.join("\n");
|
|
45493
|
+
return `<!doctype html>
|
|
45494
|
+
<html lang="en">
|
|
45495
|
+
<head>
|
|
45496
|
+
<meta charset="utf-8">
|
|
45497
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
45498
|
+
<title>SQLAnvil catalog</title>
|
|
45499
|
+
<style>
|
|
45500
|
+
:root { color-scheme: light dark; }
|
|
45501
|
+
body { font: 14px/1.5 -apple-system, system-ui, sans-serif; margin: 2rem; max-width: 1100px; }
|
|
45502
|
+
h1 { margin: 0 0 .25rem; }
|
|
45503
|
+
.meta { color: #888; margin-bottom: 1.5rem; }
|
|
45504
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0 2rem; }
|
|
45505
|
+
th, td { text-align: left; padding: .4rem .6rem; border-bottom: 1px solid #8884; vertical-align: top; }
|
|
45506
|
+
th { font-weight: 600; }
|
|
45507
|
+
code { font-size: 12px; }
|
|
45508
|
+
.tag { background: #8883; border-radius: 4px; padding: 0 .35rem; font-size: 12px; }
|
|
45509
|
+
.badge { border-radius: 4px; padding: 0 .4rem; font-size: 12px; font-weight: 600; }
|
|
45510
|
+
.badge.ok { background: #1a7f37; color: #fff; }
|
|
45511
|
+
.badge.fail { background: #b62324; color: #fff; }
|
|
45512
|
+
.badge.muted { background: #8884; }
|
|
45513
|
+
#q { padding: .4rem .6rem; width: 320px; max-width: 100%; margin-bottom: .5rem; }
|
|
45514
|
+
</style>
|
|
45515
|
+
</head>
|
|
45516
|
+
<body>
|
|
45517
|
+
<h1>SQLAnvil catalog</h1>
|
|
45518
|
+
<div class="meta">${escapeHtml(summaryLine)} · ${runLine} · generated ${escapeHtml(model.generatedAt)}</div>
|
|
45519
|
+
|
|
45520
|
+
<input id="q" type="search" placeholder="Filter models…" oninput="filterModels(this.value)">
|
|
45521
|
+
<table id="models">
|
|
45522
|
+
<thead><tr><th>Model</th><th>Type</th><th>Tags</th><th>Last run</th><th>Depends on</th><th>Description</th></tr></thead>
|
|
45523
|
+
<tbody>
|
|
45524
|
+
${modelRows}
|
|
45525
|
+
</tbody>
|
|
45526
|
+
</table>
|
|
45527
|
+
|
|
45528
|
+
<h2>Columns</h2>
|
|
45529
|
+
<table>
|
|
45530
|
+
<thead><tr><th>Model</th><th>Column</th><th>Description</th></tr></thead>
|
|
45531
|
+
<tbody>
|
|
45532
|
+
${columnRows || '<tr><td colspan="3" class="meta">No documented columns.</td></tr>'}
|
|
45533
|
+
</tbody>
|
|
45534
|
+
</table>
|
|
45535
|
+
|
|
45536
|
+
<script>
|
|
45537
|
+
function filterModels(q) {
|
|
45538
|
+
q = q.toLowerCase();
|
|
45539
|
+
for (const tr of document.querySelectorAll('#models tbody tr')) {
|
|
45540
|
+
tr.style.display = tr.getAttribute('data-search').includes(q) ? '' : 'none';
|
|
45541
|
+
}
|
|
45542
|
+
}
|
|
45543
|
+
</script>
|
|
45544
|
+
</body>
|
|
45545
|
+
</html>
|
|
45546
|
+
`;
|
|
45547
|
+
}
|
|
45548
|
+
|
|
45549
|
+
function targetKey(target) {
|
|
45550
|
+
return [target.database, target.schema, target.name].filter(Boolean).join(".");
|
|
45551
|
+
}
|
|
45552
|
+
function topoOrder(nodes) {
|
|
45553
|
+
const byKey = new Map(nodes.map(n => [n.key, n]));
|
|
45554
|
+
const indegree = new Map();
|
|
45555
|
+
const dependents = new Map();
|
|
45556
|
+
for (const n of nodes) {
|
|
45557
|
+
const inGraphDeps = Array.from(new Set(n.dependencyKeys.filter(d => byKey.has(d) && d !== n.key)));
|
|
45558
|
+
indegree.set(n.key, inGraphDeps.length);
|
|
45559
|
+
for (const dep of inGraphDeps) {
|
|
45560
|
+
dependents.set(dep, (dependents.get(dep) || []).concat(n.key));
|
|
45561
|
+
}
|
|
45562
|
+
}
|
|
45563
|
+
const ready = nodes
|
|
45564
|
+
.filter(n => (indegree.get(n.key) || 0) === 0)
|
|
45565
|
+
.map(n => n.key)
|
|
45566
|
+
.sort();
|
|
45567
|
+
const ordered = [];
|
|
45568
|
+
const emitted = new Set();
|
|
45569
|
+
while (ready.length) {
|
|
45570
|
+
const key = ready.shift();
|
|
45571
|
+
if (emitted.has(key)) {
|
|
45572
|
+
continue;
|
|
45573
|
+
}
|
|
45574
|
+
emitted.add(key);
|
|
45575
|
+
ordered.push(byKey.get(key));
|
|
45576
|
+
for (const dep of (dependents.get(key) || []).slice().sort()) {
|
|
45577
|
+
indegree.set(dep, (indegree.get(dep) || 0) - 1);
|
|
45578
|
+
if ((indegree.get(dep) || 0) <= 0 && !emitted.has(dep)) {
|
|
45579
|
+
ready.push(dep);
|
|
45580
|
+
ready.sort();
|
|
45581
|
+
}
|
|
45582
|
+
}
|
|
45583
|
+
}
|
|
45584
|
+
if (ordered.length < nodes.length) {
|
|
45585
|
+
for (const n of nodes.slice().sort((a, b) => a.key.localeCompare(b.key))) {
|
|
45586
|
+
if (!emitted.has(n.key)) {
|
|
45587
|
+
emitted.add(n.key);
|
|
45588
|
+
ordered.push(n);
|
|
45589
|
+
}
|
|
45590
|
+
}
|
|
45591
|
+
}
|
|
45592
|
+
return ordered;
|
|
45593
|
+
}
|
|
45594
|
+
function dependencyBlocked(dependencyKeys, statusByKey) {
|
|
45595
|
+
return dependencyKeys.some(dep => {
|
|
45596
|
+
const status = statusByKey.get(dep);
|
|
45597
|
+
return status !== undefined && status !== "PASS";
|
|
45598
|
+
});
|
|
45599
|
+
}
|
|
45600
|
+
const VALIDATE_SHADOW_PREFIX = "sqlanvil_validate_";
|
|
45601
|
+
function validateShadowSuffix(nowMs) {
|
|
45602
|
+
return `${VALIDATE_SHADOW_PREFIX}${nowMs}`;
|
|
45603
|
+
}
|
|
45604
|
+
function parseShadowTimestamp(schemaName) {
|
|
45605
|
+
const match = schemaName.match(/sqlanvil_validate_(\d+)/);
|
|
45606
|
+
return match ? Number(match[1]) : null;
|
|
45607
|
+
}
|
|
45608
|
+
function shadowSchemasToSweep(schemaNames, nowMs, maxAgeMs) {
|
|
45609
|
+
return schemaNames.filter(name => {
|
|
45610
|
+
const ts = parseShadowTimestamp(name);
|
|
45611
|
+
return ts !== null && nowMs - ts > maxAgeMs;
|
|
45612
|
+
});
|
|
45613
|
+
}
|
|
45614
|
+
|
|
45615
|
+
const SHADOW_MAX_AGE_MS = 60 * 60 * 1000;
|
|
45616
|
+
async function sweepOrphanShadows(deps, nowMs, maxAgeMs = SHADOW_MAX_AGE_MS) {
|
|
45617
|
+
try {
|
|
45618
|
+
const schemas = await deps.listSchemas();
|
|
45619
|
+
for (const schema of shadowSchemasToSweep(schemas, nowMs, maxAgeMs)) {
|
|
45620
|
+
try {
|
|
45621
|
+
await deps.execute(deps.dropSchemaCascadeSql(schema));
|
|
45622
|
+
}
|
|
45623
|
+
catch (e) {
|
|
45624
|
+
}
|
|
45625
|
+
}
|
|
45626
|
+
}
|
|
45627
|
+
catch (e) {
|
|
45628
|
+
}
|
|
45629
|
+
}
|
|
45630
|
+
function tableType(enumType) {
|
|
45631
|
+
switch (enumType) {
|
|
45632
|
+
case sqlanvil.TableType.VIEW:
|
|
45633
|
+
return "view";
|
|
45634
|
+
case sqlanvil.TableType.INCREMENTAL:
|
|
45635
|
+
return "incremental";
|
|
45636
|
+
default:
|
|
45637
|
+
return "table";
|
|
45638
|
+
}
|
|
45639
|
+
}
|
|
45640
|
+
function depKeys(deps) {
|
|
45641
|
+
return (deps || []).map(targetKey);
|
|
45642
|
+
}
|
|
45643
|
+
async function validate(compiledGraph, deps, options = {}) {
|
|
45644
|
+
const nodes = [];
|
|
45645
|
+
for (const table of compiledGraph.tables || []) {
|
|
45646
|
+
nodes.push({
|
|
45647
|
+
key: targetKey(table.target),
|
|
45648
|
+
dependencyKeys: depKeys(table.dependencyTargets),
|
|
45649
|
+
kind: "table",
|
|
45650
|
+
type: tableType(table.enumType),
|
|
45651
|
+
target: table.target,
|
|
45652
|
+
table,
|
|
45653
|
+
action: table
|
|
45654
|
+
});
|
|
45655
|
+
}
|
|
45656
|
+
for (const assertion of compiledGraph.assertions || []) {
|
|
45657
|
+
nodes.push({
|
|
45658
|
+
key: targetKey(assertion.target),
|
|
45659
|
+
dependencyKeys: depKeys(assertion.dependencyTargets),
|
|
45660
|
+
kind: "assertion",
|
|
45661
|
+
type: "assertion",
|
|
45662
|
+
target: assertion.target,
|
|
45663
|
+
action: assertion
|
|
45664
|
+
});
|
|
45665
|
+
}
|
|
45666
|
+
for (const operation of compiledGraph.operations || []) {
|
|
45667
|
+
if (!operation.target) {
|
|
45668
|
+
continue;
|
|
45669
|
+
}
|
|
45670
|
+
nodes.push({
|
|
45671
|
+
key: targetKey(operation.target),
|
|
45672
|
+
dependencyKeys: depKeys(operation.dependencyTargets),
|
|
45673
|
+
kind: "operation",
|
|
45674
|
+
type: "operation",
|
|
45675
|
+
target: operation.target,
|
|
45676
|
+
action: undefined
|
|
45677
|
+
});
|
|
45678
|
+
}
|
|
45679
|
+
const ordered = topoOrder(nodes);
|
|
45680
|
+
const shadowSchemas = Array.from(new Set(nodes.filter(n => n.kind === "table").map(n => n.target.schema)));
|
|
45681
|
+
const statusByKey = new Map();
|
|
45682
|
+
const results = [];
|
|
45683
|
+
try {
|
|
45684
|
+
for (const schema of shadowSchemas) {
|
|
45685
|
+
await deps.execute(deps.createSchemaSql(schema));
|
|
45686
|
+
}
|
|
45687
|
+
for (const node of ordered) {
|
|
45688
|
+
if (node.kind === "operation") {
|
|
45689
|
+
statusByKey.set(node.key, "SKIPPED");
|
|
45690
|
+
results.push({ target: node.target, type: node.type, status: "SKIPPED", errors: [] });
|
|
45691
|
+
continue;
|
|
45692
|
+
}
|
|
45693
|
+
if (dependencyBlocked(node.dependencyKeys, statusByKey)) {
|
|
45694
|
+
statusByKey.set(node.key, "BLOCKED");
|
|
45695
|
+
results.push({ target: node.target, type: node.type, status: "BLOCKED", errors: [] });
|
|
45696
|
+
continue;
|
|
45697
|
+
}
|
|
45698
|
+
const evaluations = await deps.evaluate(node.action);
|
|
45699
|
+
const failed = evaluations.some(e => e.status === sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE);
|
|
45700
|
+
const status = failed ? "FAILURE" : "PASS";
|
|
45701
|
+
statusByKey.set(node.key, status);
|
|
45702
|
+
results.push({ target: node.target, type: node.type, status, errors: evaluations });
|
|
45703
|
+
if (status === "PASS" && node.kind === "table") {
|
|
45704
|
+
try {
|
|
45705
|
+
await deps.execute(deps.validationStubSql(node.table));
|
|
45706
|
+
}
|
|
45707
|
+
catch (e) {
|
|
45708
|
+
}
|
|
45709
|
+
}
|
|
45710
|
+
}
|
|
45711
|
+
}
|
|
45712
|
+
finally {
|
|
45713
|
+
if (!options.keepShadow) {
|
|
45714
|
+
for (const schema of shadowSchemas.slice().reverse()) {
|
|
45715
|
+
try {
|
|
45716
|
+
await deps.execute(deps.dropSchemaCascadeSql(schema));
|
|
45717
|
+
}
|
|
45718
|
+
catch (e) {
|
|
45719
|
+
}
|
|
45720
|
+
}
|
|
45721
|
+
}
|
|
45722
|
+
}
|
|
45723
|
+
return results;
|
|
45724
|
+
}
|
|
45725
|
+
|
|
45154
45726
|
function parsePostgresEvalError(_query, error) {
|
|
45155
45727
|
return sqlanvil.QueryEvaluationError.create({
|
|
45156
45728
|
message: (error === null || error === void 0 ? void 0 : error.message) ? String(error.message) : String(error)
|
|
@@ -47734,6 +48306,16 @@ const timeoutOption = option("timeout", {
|
|
|
47734
48306
|
default: null,
|
|
47735
48307
|
coerce: (rawTimeoutString) => rawTimeoutString ? parseDuration__default["default"](rawTimeoutString) : null
|
|
47736
48308
|
});
|
|
48309
|
+
const noArtifactsOption = option("no-artifacts", {
|
|
48310
|
+
describe: "Skip writing the queryable Parquet artifacts under target/ (catalog on compile; run history " +
|
|
48311
|
+
"on run).",
|
|
48312
|
+
type: "boolean"
|
|
48313
|
+
});
|
|
48314
|
+
const keepShadowOption = option("keep-shadow", {
|
|
48315
|
+
describe: "If set, `validate` leaves its temporary shadow schema(s) in place instead of dropping them " +
|
|
48316
|
+
"(debugging aid).",
|
|
48317
|
+
type: "boolean"
|
|
48318
|
+
});
|
|
47737
48319
|
const jobPrefixOption = option("job-prefix", {
|
|
47738
48320
|
describe: "Adds an additional prefix in the form of `sqlanvil-${jobPrefix}-`.",
|
|
47739
48321
|
type: "string",
|
|
@@ -47795,6 +48377,208 @@ function credentialsPathWithEnvironment(projectDir, argv) {
|
|
|
47795
48377
|
const chosen = resolveCredentials(envCredentials, argv[credentialsOption.name], CREDENTIALS_FILENAME);
|
|
47796
48378
|
return getCredentialsPath(projectDir, chosen);
|
|
47797
48379
|
}
|
|
48380
|
+
function printValidationResults(results, json) {
|
|
48381
|
+
const failures = results.filter(r => r.status === "FAILURE");
|
|
48382
|
+
const blocked = results.filter(r => r.status === "BLOCKED");
|
|
48383
|
+
const passed = results.filter(r => r.status === "PASS");
|
|
48384
|
+
const skipped = results.filter(r => r.status === "SKIPPED");
|
|
48385
|
+
if (json) {
|
|
48386
|
+
print(prettyJsonStringify(results));
|
|
48387
|
+
}
|
|
48388
|
+
else {
|
|
48389
|
+
for (const result of results) {
|
|
48390
|
+
const label = targetAsReadableString(result.target);
|
|
48391
|
+
if (result.status === "PASS") {
|
|
48392
|
+
printSuccess(` PASS ${label}`);
|
|
48393
|
+
}
|
|
48394
|
+
else if (result.status === "SKIPPED") {
|
|
48395
|
+
print(` SKIP ${label} (${result.type} — not validated)`);
|
|
48396
|
+
}
|
|
48397
|
+
else if (result.status === "BLOCKED") {
|
|
48398
|
+
printError(` BLOCK ${label} — blocked by an upstream failure`);
|
|
48399
|
+
}
|
|
48400
|
+
else {
|
|
48401
|
+
printError(` FAIL ${label}`);
|
|
48402
|
+
result.errors
|
|
48403
|
+
.filter(e => e.status === sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE)
|
|
48404
|
+
.forEach(e => {
|
|
48405
|
+
var _a;
|
|
48406
|
+
const loc = ((_a = e.error) === null || _a === void 0 ? void 0 : _a.errorLocation) ? ` (line ${e.error.errorLocation.line}, col ${e.error.errorLocation.column})`
|
|
48407
|
+
: "";
|
|
48408
|
+
printError(` ${(e.error && e.error.message) || "validation failed"}${loc}`);
|
|
48409
|
+
});
|
|
48410
|
+
}
|
|
48411
|
+
}
|
|
48412
|
+
print(`\n${passed.length} passed, ${failures.length} failed, ${blocked.length} blocked` +
|
|
48413
|
+
(skipped.length ? `, ${skipped.length} skipped` : ""));
|
|
48414
|
+
}
|
|
48415
|
+
return failures.length > 0 || blocked.length > 0 ? 1 : 0;
|
|
48416
|
+
}
|
|
48417
|
+
async function runValidate(argv) {
|
|
48418
|
+
const projectDir = argv[projectDirOption.name];
|
|
48419
|
+
if (!argv[jsonOutputOption.name]) {
|
|
48420
|
+
print("Compiling...\n");
|
|
48421
|
+
}
|
|
48422
|
+
const baseOverride = projectConfigOverrideWithEnvironment(projectDir, argv);
|
|
48423
|
+
const shadowSuffix = validateShadowSuffix(Date.now());
|
|
48424
|
+
const compiledGraph = await compile({
|
|
48425
|
+
projectDir,
|
|
48426
|
+
projectConfigOverride: Object.assign(Object.assign({}, baseOverride), { schemaSuffix: [baseOverride.schemaSuffix, shadowSuffix].filter(Boolean).join("_") }),
|
|
48427
|
+
timeoutMillis: argv[timeoutOption.name] || undefined
|
|
48428
|
+
});
|
|
48429
|
+
if (compiledGraphHasErrors(compiledGraph)) {
|
|
48430
|
+
printCompiledGraphErrors(compiledGraph.graphErrors, argv[quietCompileOption.name]);
|
|
48431
|
+
return 1;
|
|
48432
|
+
}
|
|
48433
|
+
if (!argv[jsonOutputOption.name]) {
|
|
48434
|
+
printSuccess("Compiled successfully.\n");
|
|
48435
|
+
}
|
|
48436
|
+
const warehouse = (compiledGraph.projectConfig.warehouse || "bigquery").toLowerCase();
|
|
48437
|
+
const readCredentials = read(credentialsPathWithEnvironment(projectDir, argv), warehouse);
|
|
48438
|
+
let dbadapter;
|
|
48439
|
+
if (warehouse === "supabase") {
|
|
48440
|
+
dbadapter = await SupabaseDbAdapter.create(readCredentials);
|
|
48441
|
+
}
|
|
48442
|
+
else if (warehouse === "mysql") {
|
|
48443
|
+
dbadapter = await MySqlDbAdapter.create(readCredentials);
|
|
48444
|
+
}
|
|
48445
|
+
else if (warehouse === "bigquery") {
|
|
48446
|
+
dbadapter = new BigQueryDbAdapter(readCredentials);
|
|
48447
|
+
}
|
|
48448
|
+
else {
|
|
48449
|
+
dbadapter = await PostgresDbAdapter.create(readCredentials);
|
|
48450
|
+
}
|
|
48451
|
+
const prunedGraph = prune(compiledGraph, {
|
|
48452
|
+
actions: argv[actionsOption.name],
|
|
48453
|
+
includeDependencies: argv[includeDepsOption.name],
|
|
48454
|
+
includeDependents: argv[includeDependentsOption.name],
|
|
48455
|
+
tags: argv[tagsOption.name]
|
|
48456
|
+
});
|
|
48457
|
+
const executionSql = new ExecutionSql(compiledGraph.projectConfig, dataformVersion);
|
|
48458
|
+
const shadowSchemas = Array.from(new Set((prunedGraph.tables || []).map(table => table.target.schema)));
|
|
48459
|
+
process.on("SIGINT", () => {
|
|
48460
|
+
Promise.all(shadowSchemas.map(schema => dbadapter.execute(executionSql.dropSchemaCascadeSql(schema)).catch(() => undefined))).then(() => process.exit(1));
|
|
48461
|
+
});
|
|
48462
|
+
const deps = {
|
|
48463
|
+
evaluate: action => dbadapter.evaluate(action.enumType !== undefined
|
|
48464
|
+
? sqlanvil.Table.create(action)
|
|
48465
|
+
: sqlanvil.Assertion.create(action)),
|
|
48466
|
+
execute: sql => dbadapter.execute(sql).then(() => undefined),
|
|
48467
|
+
validationStubSql: table => executionSql.validationStubSql(table),
|
|
48468
|
+
createSchemaSql: schema => executionSql.createSchemaSql(schema),
|
|
48469
|
+
dropSchemaCascadeSql: schema => executionSql.dropSchemaCascadeSql(schema),
|
|
48470
|
+
listSchemas: async () => {
|
|
48471
|
+
const result = await dbadapter.execute("select schema_name as name from information_schema.schemata");
|
|
48472
|
+
return ((result && result.rows) || []).map((row) => row.name);
|
|
48473
|
+
}
|
|
48474
|
+
};
|
|
48475
|
+
await sweepOrphanShadows(deps, Date.now());
|
|
48476
|
+
if (!argv[jsonOutputOption.name]) {
|
|
48477
|
+
print("Validating...\n");
|
|
48478
|
+
}
|
|
48479
|
+
const results = await validate(prunedGraph, deps, { keepShadow: argv[keepShadowOption.name] });
|
|
48480
|
+
return printValidationResults(results, argv[jsonOutputOption.name]);
|
|
48481
|
+
}
|
|
48482
|
+
function resolveArtifactViews(projectDir) {
|
|
48483
|
+
const catalogDir = path__namespace.join(projectDir, TARGET_DIR, "catalog");
|
|
48484
|
+
const runsDir = path__namespace.join(projectDir, TARGET_DIR, "runs");
|
|
48485
|
+
const views = [];
|
|
48486
|
+
for (const name of ["actions", "dependencies", "columns"]) {
|
|
48487
|
+
const file = path__namespace.join(catalogDir, `${name}.parquet`);
|
|
48488
|
+
if (fs__namespace$1.existsSync(file)) {
|
|
48489
|
+
views.push({ name, glob: file });
|
|
48490
|
+
}
|
|
48491
|
+
}
|
|
48492
|
+
const hasRuns = fs__namespace$1.existsSync(runsDir) && fs__namespace$1.readdirSync(runsDir).some(f => f.endsWith(".parquet"));
|
|
48493
|
+
if (hasRuns) {
|
|
48494
|
+
views.push({ name: "runs", glob: path__namespace.join(runsDir, "*.parquet") });
|
|
48495
|
+
}
|
|
48496
|
+
return { views, hasCatalog: views.some(v => v.name === "actions"), hasRuns };
|
|
48497
|
+
}
|
|
48498
|
+
function printArtifactRows(rows) {
|
|
48499
|
+
if (!rows || rows.length === 0) {
|
|
48500
|
+
print("(0 rows)");
|
|
48501
|
+
return;
|
|
48502
|
+
}
|
|
48503
|
+
const cols = Object.keys(rows[0]);
|
|
48504
|
+
const widths = cols.map(c => Math.max(c.length, ...rows.map(r => String(r[c] === null || r[c] === undefined ? "" : r[c]).length)));
|
|
48505
|
+
const fmtRow = (vals) => vals.map((v, i) => v.padEnd(widths[i])).join(" ");
|
|
48506
|
+
print(fmtRow(cols));
|
|
48507
|
+
print(fmtRow(widths.map(w => "-".repeat(w))));
|
|
48508
|
+
for (const row of rows) {
|
|
48509
|
+
print(fmtRow(cols.map(c => String(row[c] === null || row[c] === undefined ? "" : row[c]))));
|
|
48510
|
+
}
|
|
48511
|
+
print(`\n(${rows.length} row${rows.length === 1 ? "" : "s"})`);
|
|
48512
|
+
}
|
|
48513
|
+
const NO_ARTIFACTS = "No artifacts found under target/. Run `sqlanvil compile` (or `run`) first.";
|
|
48514
|
+
async function runQuery(projectDir, sql, json) {
|
|
48515
|
+
const { views, hasCatalog } = resolveArtifactViews(projectDir);
|
|
48516
|
+
if (!hasCatalog) {
|
|
48517
|
+
printError(NO_ARTIFACTS);
|
|
48518
|
+
return 1;
|
|
48519
|
+
}
|
|
48520
|
+
const rows = await queryParquet(sql, views);
|
|
48521
|
+
if (json) {
|
|
48522
|
+
print(prettyJsonStringify(rows));
|
|
48523
|
+
}
|
|
48524
|
+
else {
|
|
48525
|
+
printArtifactRows(rows);
|
|
48526
|
+
}
|
|
48527
|
+
return 0;
|
|
48528
|
+
}
|
|
48529
|
+
async function runInspect(projectDir, json) {
|
|
48530
|
+
const { views, hasCatalog, hasRuns } = resolveArtifactViews(projectDir);
|
|
48531
|
+
if (!hasCatalog) {
|
|
48532
|
+
printError(NO_ARTIFACTS);
|
|
48533
|
+
return 1;
|
|
48534
|
+
}
|
|
48535
|
+
const actionsByType = await queryParquet("select type, count(*) as n from actions group by type order by type", views);
|
|
48536
|
+
let latestRun = null;
|
|
48537
|
+
let failures = [];
|
|
48538
|
+
if (hasRuns) {
|
|
48539
|
+
const latest = await queryParquet("select run_id, run_status, " +
|
|
48540
|
+
"count(*) filter (where status = 'SUCCESSFUL') as succeeded, " +
|
|
48541
|
+
"count(*) filter (where status = 'FAILED') as failed, " +
|
|
48542
|
+
"max(end_millis) - min(start_millis) as wall_ms " +
|
|
48543
|
+
"from runs where run_id = (select max(run_id) from runs) group by run_id, run_status", views);
|
|
48544
|
+
latestRun = latest[0] || null;
|
|
48545
|
+
failures = await queryParquet("select readable_name, error_message from runs " +
|
|
48546
|
+
"where run_id = (select max(run_id) from runs) and status = 'FAILED' limit 20", views);
|
|
48547
|
+
}
|
|
48548
|
+
if (json) {
|
|
48549
|
+
print(prettyJsonStringify({ actionsByType, latestRun, failures }));
|
|
48550
|
+
return 0;
|
|
48551
|
+
}
|
|
48552
|
+
print("Actions by type:");
|
|
48553
|
+
printArtifactRows(actionsByType);
|
|
48554
|
+
if (!hasRuns || !latestRun) {
|
|
48555
|
+
print("\nNo runs recorded yet.");
|
|
48556
|
+
}
|
|
48557
|
+
else {
|
|
48558
|
+
print(`\nLatest run (${latestRun.run_status}): ${latestRun.succeeded} succeeded, ` +
|
|
48559
|
+
`${latestRun.failed} failed, ${latestRun.wall_ms}ms`);
|
|
48560
|
+
if (failures.length > 0) {
|
|
48561
|
+
print("\nFailures:");
|
|
48562
|
+
printArtifactRows(failures);
|
|
48563
|
+
}
|
|
48564
|
+
}
|
|
48565
|
+
return 0;
|
|
48566
|
+
}
|
|
48567
|
+
async function runDocs(projectDir) {
|
|
48568
|
+
const { views, hasCatalog } = resolveArtifactViews(projectDir);
|
|
48569
|
+
if (!hasCatalog) {
|
|
48570
|
+
printError(NO_ARTIFACTS);
|
|
48571
|
+
return 1;
|
|
48572
|
+
}
|
|
48573
|
+
const model = await buildDocsModel(views, new Date().toISOString());
|
|
48574
|
+
const html = renderDocsHtml(model);
|
|
48575
|
+
const outDir = path__namespace.join(projectDir, TARGET_DIR, "docs");
|
|
48576
|
+
fs__namespace$1.mkdirSync(outDir, { recursive: true });
|
|
48577
|
+
const outFile = path__namespace.join(outDir, "index.html");
|
|
48578
|
+
fs__namespace$1.writeFileSync(outFile, html);
|
|
48579
|
+
printSuccess(`Wrote catalog to ${outFile}`);
|
|
48580
|
+
return 0;
|
|
48581
|
+
}
|
|
47798
48582
|
function runCli() {
|
|
47799
48583
|
const builtYargs = createYargsCli({
|
|
47800
48584
|
commands: [
|
|
@@ -47925,6 +48709,7 @@ function runCli() {
|
|
|
47925
48709
|
compileTagsOption,
|
|
47926
48710
|
compileIncludeDepsOption,
|
|
47927
48711
|
compileIncludeDependentsOption,
|
|
48712
|
+
noArtifactsOption,
|
|
47928
48713
|
option(verboseOptionName, {
|
|
47929
48714
|
describe: "Enable verbose compilation output. Example usage: 'sqlanvil compile --verbose'",
|
|
47930
48715
|
type: "boolean",
|
|
@@ -47971,6 +48756,9 @@ function runCli() {
|
|
|
47971
48756
|
printCompiledGraphErrors(compiledGraph.graphErrors, argv[quietCompileOption.name]);
|
|
47972
48757
|
return true;
|
|
47973
48758
|
}
|
|
48759
|
+
if (!argv[noArtifactsOption.name]) {
|
|
48760
|
+
await safeWriteArtifacts(compiledGraph, projectDir, { warn: print });
|
|
48761
|
+
}
|
|
47974
48762
|
return false;
|
|
47975
48763
|
}
|
|
47976
48764
|
const graphHasErrors = await compileAndPrint();
|
|
@@ -48075,6 +48863,24 @@ function runCli() {
|
|
|
48075
48863
|
return testResults.every(testResult => testResult.successful) ? 0 : 1;
|
|
48076
48864
|
}
|
|
48077
48865
|
},
|
|
48866
|
+
{
|
|
48867
|
+
format: `validate [${projectDirMustExistOption.name}]`,
|
|
48868
|
+
description: "Validate the project's SQL against the warehouse planner (EXPLAIN/dry-run) without " +
|
|
48869
|
+
"executing. Postgres/Supabase/MySQL only.",
|
|
48870
|
+
positionalOptions: [projectDirMustExistOption],
|
|
48871
|
+
options: [
|
|
48872
|
+
actionsOption,
|
|
48873
|
+
tagsOption,
|
|
48874
|
+
includeDepsOption,
|
|
48875
|
+
includeDependentsOption,
|
|
48876
|
+
credentialsOption,
|
|
48877
|
+
jsonOutputOption,
|
|
48878
|
+
timeoutOption,
|
|
48879
|
+
keepShadowOption,
|
|
48880
|
+
...ProjectConfigOptions.allYargsOptions
|
|
48881
|
+
],
|
|
48882
|
+
processFn: async (argv) => runValidate(argv)
|
|
48883
|
+
},
|
|
48078
48884
|
{
|
|
48079
48885
|
format: `run [${projectDirMustExistOption.name}]`,
|
|
48080
48886
|
description: "Run the sqlanvil project.",
|
|
@@ -48103,6 +48909,7 @@ function runCli() {
|
|
|
48103
48909
|
timeoutOption,
|
|
48104
48910
|
tagsOption,
|
|
48105
48911
|
bigqueryJobLabelsOption,
|
|
48912
|
+
noArtifactsOption,
|
|
48106
48913
|
...ProjectConfigOptions.allYargsOptions
|
|
48107
48914
|
],
|
|
48108
48915
|
processFn: async (argv) => {
|
|
@@ -48127,6 +48934,9 @@ function runCli() {
|
|
|
48127
48934
|
printSuccess("Compiled successfully.\n");
|
|
48128
48935
|
}
|
|
48129
48936
|
const warehouse = compiledGraph.projectConfig.warehouse || "bigquery";
|
|
48937
|
+
if (argv[dryRunOptionName] && warehouse.toLowerCase() !== "bigquery") {
|
|
48938
|
+
return runValidate(argv);
|
|
48939
|
+
}
|
|
48130
48940
|
const readCredentials = read(credentialsPathWithEnvironment(argv[projectDirOption.name], argv), warehouse);
|
|
48131
48941
|
let dbadapter;
|
|
48132
48942
|
if (warehouse.toLowerCase() === "supabase") {
|
|
@@ -48214,9 +49024,47 @@ function runCli() {
|
|
|
48214
49024
|
runner.onChange(printExecutedGraph);
|
|
48215
49025
|
const runResult = await runner.result();
|
|
48216
49026
|
printExecutedGraph(runResult);
|
|
49027
|
+
if (!argv[noArtifactsOption.name]) {
|
|
49028
|
+
await safeWriteArtifacts(compiledGraph, argv[projectDirOption.name], {
|
|
49029
|
+
runResult,
|
|
49030
|
+
runId: Date.now(),
|
|
49031
|
+
warn: print
|
|
49032
|
+
});
|
|
49033
|
+
}
|
|
48217
49034
|
return runResult.status === sqlanvil.RunResult.ExecutionStatus.SUCCESSFUL ? 0 : 1;
|
|
48218
49035
|
}
|
|
48219
49036
|
},
|
|
49037
|
+
{
|
|
49038
|
+
format: `query [sql] [${projectDirOption.name}]`,
|
|
49039
|
+
description: "Run SQL over the project's queryable artifacts in target/ (views: actions, " +
|
|
49040
|
+
"dependencies, columns, runs), via the bundled DuckDB.",
|
|
49041
|
+
positionalOptions: [
|
|
49042
|
+
positionalOption("sql", { describe: 'SQL to run, e.g. "select type, count(*) from actions group by 1".' }, (argv) => {
|
|
49043
|
+
if (!argv.sql) {
|
|
49044
|
+
throw new Error('Provide a SQL query, e.g. sqlanvil query "select * from actions".');
|
|
49045
|
+
}
|
|
49046
|
+
}),
|
|
49047
|
+
projectDirOption
|
|
49048
|
+
],
|
|
49049
|
+
options: [jsonOutputOption],
|
|
49050
|
+
processFn: async (argv) => runQuery(argv[projectDirOption.name], argv.sql, argv[jsonOutputOption.name])
|
|
49051
|
+
},
|
|
49052
|
+
{
|
|
49053
|
+
format: `inspect [${projectDirOption.name}]`,
|
|
49054
|
+
description: "Summarize the project's artifacts: action counts by type, the latest run's status/" +
|
|
49055
|
+
"timing, and recent failures.",
|
|
49056
|
+
positionalOptions: [projectDirOption],
|
|
49057
|
+
options: [jsonOutputOption],
|
|
49058
|
+
processFn: async (argv) => runInspect(argv[projectDirOption.name], argv[jsonOutputOption.name])
|
|
49059
|
+
},
|
|
49060
|
+
{
|
|
49061
|
+
format: `docs [${projectDirOption.name}]`,
|
|
49062
|
+
description: "Generate a self-contained HTML catalog of the project (models, columns, dependencies, " +
|
|
49063
|
+
"last-run status) at target/docs/index.html, from the artifacts.",
|
|
49064
|
+
positionalOptions: [projectDirOption],
|
|
49065
|
+
options: [],
|
|
49066
|
+
processFn: async (argv) => runDocs(argv[projectDirOption.name])
|
|
49067
|
+
},
|
|
48220
49068
|
{
|
|
48221
49069
|
format: `format [${projectDirMustExistOption.name}]`,
|
|
48222
49070
|
description: "Format the sqlanvil project's files.",
|