@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.
Files changed (2) hide show
  1. package/bundle.js +892 -44
  2. 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.8.3";
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 withConnection(async (conn) => {
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, "&lt;")
45405
+ .replace(/>/g, "&gt;")
45406
+ .replace(/"/g, "&quot;");
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)} &middot; ${runLine} &middot; 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.",
package/package.json CHANGED
@@ -63,7 +63,7 @@
63
63
  "bin": {
64
64
  "sqlanvil": "bundle.js"
65
65
  },
66
- "version": "1.8.3",
66
+ "version": "1.10.0",
67
67
  "name": "@sqlanvil/cli",
68
68
  "description": "sqlanvil command line interface.",
69
69
  "main": "bundle.js"