@sqlanvil/cli 1.8.3 → 1.9.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 +359 -1
  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.9.0";
43743
43796
  const dataformVersion = "3.0.60";
43744
43797
 
43745
43798
  async function build(compiledGraph, runConfig, dbadapter) {
@@ -45151,6 +45204,183 @@ function resolveCredentials(envCredentials, cliCredentials, defaultFilename) {
45151
45204
  return envCredentials || defaultFilename;
45152
45205
  }
45153
45206
 
45207
+ function targetKey(target) {
45208
+ return [target.database, target.schema, target.name].filter(Boolean).join(".");
45209
+ }
45210
+ function topoOrder(nodes) {
45211
+ const byKey = new Map(nodes.map(n => [n.key, n]));
45212
+ const indegree = new Map();
45213
+ const dependents = new Map();
45214
+ for (const n of nodes) {
45215
+ const inGraphDeps = Array.from(new Set(n.dependencyKeys.filter(d => byKey.has(d) && d !== n.key)));
45216
+ indegree.set(n.key, inGraphDeps.length);
45217
+ for (const dep of inGraphDeps) {
45218
+ dependents.set(dep, (dependents.get(dep) || []).concat(n.key));
45219
+ }
45220
+ }
45221
+ const ready = nodes
45222
+ .filter(n => (indegree.get(n.key) || 0) === 0)
45223
+ .map(n => n.key)
45224
+ .sort();
45225
+ const ordered = [];
45226
+ const emitted = new Set();
45227
+ while (ready.length) {
45228
+ const key = ready.shift();
45229
+ if (emitted.has(key)) {
45230
+ continue;
45231
+ }
45232
+ emitted.add(key);
45233
+ ordered.push(byKey.get(key));
45234
+ for (const dep of (dependents.get(key) || []).slice().sort()) {
45235
+ indegree.set(dep, (indegree.get(dep) || 0) - 1);
45236
+ if ((indegree.get(dep) || 0) <= 0 && !emitted.has(dep)) {
45237
+ ready.push(dep);
45238
+ ready.sort();
45239
+ }
45240
+ }
45241
+ }
45242
+ if (ordered.length < nodes.length) {
45243
+ for (const n of nodes.slice().sort((a, b) => a.key.localeCompare(b.key))) {
45244
+ if (!emitted.has(n.key)) {
45245
+ emitted.add(n.key);
45246
+ ordered.push(n);
45247
+ }
45248
+ }
45249
+ }
45250
+ return ordered;
45251
+ }
45252
+ function dependencyBlocked(dependencyKeys, statusByKey) {
45253
+ return dependencyKeys.some(dep => {
45254
+ const status = statusByKey.get(dep);
45255
+ return status !== undefined && status !== "PASS";
45256
+ });
45257
+ }
45258
+ const VALIDATE_SHADOW_PREFIX = "sqlanvil_validate_";
45259
+ function validateShadowSuffix(nowMs) {
45260
+ return `${VALIDATE_SHADOW_PREFIX}${nowMs}`;
45261
+ }
45262
+ function parseShadowTimestamp(schemaName) {
45263
+ const match = schemaName.match(/sqlanvil_validate_(\d+)/);
45264
+ return match ? Number(match[1]) : null;
45265
+ }
45266
+ function shadowSchemasToSweep(schemaNames, nowMs, maxAgeMs) {
45267
+ return schemaNames.filter(name => {
45268
+ const ts = parseShadowTimestamp(name);
45269
+ return ts !== null && nowMs - ts > maxAgeMs;
45270
+ });
45271
+ }
45272
+
45273
+ const SHADOW_MAX_AGE_MS = 60 * 60 * 1000;
45274
+ async function sweepOrphanShadows(deps, nowMs, maxAgeMs = SHADOW_MAX_AGE_MS) {
45275
+ try {
45276
+ const schemas = await deps.listSchemas();
45277
+ for (const schema of shadowSchemasToSweep(schemas, nowMs, maxAgeMs)) {
45278
+ try {
45279
+ await deps.execute(deps.dropSchemaCascadeSql(schema));
45280
+ }
45281
+ catch (e) {
45282
+ }
45283
+ }
45284
+ }
45285
+ catch (e) {
45286
+ }
45287
+ }
45288
+ function tableType(enumType) {
45289
+ switch (enumType) {
45290
+ case sqlanvil.TableType.VIEW:
45291
+ return "view";
45292
+ case sqlanvil.TableType.INCREMENTAL:
45293
+ return "incremental";
45294
+ default:
45295
+ return "table";
45296
+ }
45297
+ }
45298
+ function depKeys(deps) {
45299
+ return (deps || []).map(targetKey);
45300
+ }
45301
+ async function validate(compiledGraph, deps, options = {}) {
45302
+ const nodes = [];
45303
+ for (const table of compiledGraph.tables || []) {
45304
+ nodes.push({
45305
+ key: targetKey(table.target),
45306
+ dependencyKeys: depKeys(table.dependencyTargets),
45307
+ kind: "table",
45308
+ type: tableType(table.enumType),
45309
+ target: table.target,
45310
+ table,
45311
+ action: table
45312
+ });
45313
+ }
45314
+ for (const assertion of compiledGraph.assertions || []) {
45315
+ nodes.push({
45316
+ key: targetKey(assertion.target),
45317
+ dependencyKeys: depKeys(assertion.dependencyTargets),
45318
+ kind: "assertion",
45319
+ type: "assertion",
45320
+ target: assertion.target,
45321
+ action: assertion
45322
+ });
45323
+ }
45324
+ for (const operation of compiledGraph.operations || []) {
45325
+ if (!operation.target) {
45326
+ continue;
45327
+ }
45328
+ nodes.push({
45329
+ key: targetKey(operation.target),
45330
+ dependencyKeys: depKeys(operation.dependencyTargets),
45331
+ kind: "operation",
45332
+ type: "operation",
45333
+ target: operation.target,
45334
+ action: undefined
45335
+ });
45336
+ }
45337
+ const ordered = topoOrder(nodes);
45338
+ const shadowSchemas = Array.from(new Set(nodes.filter(n => n.kind === "table").map(n => n.target.schema)));
45339
+ const statusByKey = new Map();
45340
+ const results = [];
45341
+ try {
45342
+ for (const schema of shadowSchemas) {
45343
+ await deps.execute(deps.createSchemaSql(schema));
45344
+ }
45345
+ for (const node of ordered) {
45346
+ if (node.kind === "operation") {
45347
+ statusByKey.set(node.key, "SKIPPED");
45348
+ results.push({ target: node.target, type: node.type, status: "SKIPPED", errors: [] });
45349
+ continue;
45350
+ }
45351
+ if (dependencyBlocked(node.dependencyKeys, statusByKey)) {
45352
+ statusByKey.set(node.key, "BLOCKED");
45353
+ results.push({ target: node.target, type: node.type, status: "BLOCKED", errors: [] });
45354
+ continue;
45355
+ }
45356
+ const evaluations = await deps.evaluate(node.action);
45357
+ const failed = evaluations.some(e => e.status === sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE);
45358
+ const status = failed ? "FAILURE" : "PASS";
45359
+ statusByKey.set(node.key, status);
45360
+ results.push({ target: node.target, type: node.type, status, errors: evaluations });
45361
+ if (status === "PASS" && node.kind === "table") {
45362
+ try {
45363
+ await deps.execute(deps.validationStubSql(node.table));
45364
+ }
45365
+ catch (e) {
45366
+ }
45367
+ }
45368
+ }
45369
+ }
45370
+ finally {
45371
+ if (!options.keepShadow) {
45372
+ for (const schema of shadowSchemas.slice().reverse()) {
45373
+ try {
45374
+ await deps.execute(deps.dropSchemaCascadeSql(schema));
45375
+ }
45376
+ catch (e) {
45377
+ }
45378
+ }
45379
+ }
45380
+ }
45381
+ return results;
45382
+ }
45383
+
45154
45384
  function parsePostgresEvalError(_query, error) {
45155
45385
  return sqlanvil.QueryEvaluationError.create({
45156
45386
  message: (error === null || error === void 0 ? void 0 : error.message) ? String(error.message) : String(error)
@@ -47734,6 +47964,11 @@ const timeoutOption = option("timeout", {
47734
47964
  default: null,
47735
47965
  coerce: (rawTimeoutString) => rawTimeoutString ? parseDuration__default["default"](rawTimeoutString) : null
47736
47966
  });
47967
+ const keepShadowOption = option("keep-shadow", {
47968
+ describe: "If set, `validate` leaves its temporary shadow schema(s) in place instead of dropping them " +
47969
+ "(debugging aid).",
47970
+ type: "boolean"
47971
+ });
47737
47972
  const jobPrefixOption = option("job-prefix", {
47738
47973
  describe: "Adds an additional prefix in the form of `sqlanvil-${jobPrefix}-`.",
47739
47974
  type: "string",
@@ -47795,6 +48030,108 @@ function credentialsPathWithEnvironment(projectDir, argv) {
47795
48030
  const chosen = resolveCredentials(envCredentials, argv[credentialsOption.name], CREDENTIALS_FILENAME);
47796
48031
  return getCredentialsPath(projectDir, chosen);
47797
48032
  }
48033
+ function printValidationResults(results, json) {
48034
+ const failures = results.filter(r => r.status === "FAILURE");
48035
+ const blocked = results.filter(r => r.status === "BLOCKED");
48036
+ const passed = results.filter(r => r.status === "PASS");
48037
+ const skipped = results.filter(r => r.status === "SKIPPED");
48038
+ if (json) {
48039
+ print(prettyJsonStringify(results));
48040
+ }
48041
+ else {
48042
+ for (const result of results) {
48043
+ const label = targetAsReadableString(result.target);
48044
+ if (result.status === "PASS") {
48045
+ printSuccess(` PASS ${label}`);
48046
+ }
48047
+ else if (result.status === "SKIPPED") {
48048
+ print(` SKIP ${label} (${result.type} — not validated)`);
48049
+ }
48050
+ else if (result.status === "BLOCKED") {
48051
+ printError(` BLOCK ${label} — blocked by an upstream failure`);
48052
+ }
48053
+ else {
48054
+ printError(` FAIL ${label}`);
48055
+ result.errors
48056
+ .filter(e => e.status === sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE)
48057
+ .forEach(e => {
48058
+ var _a;
48059
+ const loc = ((_a = e.error) === null || _a === void 0 ? void 0 : _a.errorLocation) ? ` (line ${e.error.errorLocation.line}, col ${e.error.errorLocation.column})`
48060
+ : "";
48061
+ printError(` ${(e.error && e.error.message) || "validation failed"}${loc}`);
48062
+ });
48063
+ }
48064
+ }
48065
+ print(`\n${passed.length} passed, ${failures.length} failed, ${blocked.length} blocked` +
48066
+ (skipped.length ? `, ${skipped.length} skipped` : ""));
48067
+ }
48068
+ return failures.length > 0 || blocked.length > 0 ? 1 : 0;
48069
+ }
48070
+ async function runValidate(argv) {
48071
+ const projectDir = argv[projectDirOption.name];
48072
+ if (!argv[jsonOutputOption.name]) {
48073
+ print("Compiling...\n");
48074
+ }
48075
+ const baseOverride = projectConfigOverrideWithEnvironment(projectDir, argv);
48076
+ const shadowSuffix = validateShadowSuffix(Date.now());
48077
+ const compiledGraph = await compile({
48078
+ projectDir,
48079
+ projectConfigOverride: Object.assign(Object.assign({}, baseOverride), { schemaSuffix: [baseOverride.schemaSuffix, shadowSuffix].filter(Boolean).join("_") }),
48080
+ timeoutMillis: argv[timeoutOption.name] || undefined
48081
+ });
48082
+ if (compiledGraphHasErrors(compiledGraph)) {
48083
+ printCompiledGraphErrors(compiledGraph.graphErrors, argv[quietCompileOption.name]);
48084
+ return 1;
48085
+ }
48086
+ if (!argv[jsonOutputOption.name]) {
48087
+ printSuccess("Compiled successfully.\n");
48088
+ }
48089
+ const warehouse = (compiledGraph.projectConfig.warehouse || "bigquery").toLowerCase();
48090
+ const readCredentials = read(credentialsPathWithEnvironment(projectDir, argv), warehouse);
48091
+ let dbadapter;
48092
+ if (warehouse === "supabase") {
48093
+ dbadapter = await SupabaseDbAdapter.create(readCredentials);
48094
+ }
48095
+ else if (warehouse === "mysql") {
48096
+ dbadapter = await MySqlDbAdapter.create(readCredentials);
48097
+ }
48098
+ else if (warehouse === "bigquery") {
48099
+ dbadapter = new BigQueryDbAdapter(readCredentials);
48100
+ }
48101
+ else {
48102
+ dbadapter = await PostgresDbAdapter.create(readCredentials);
48103
+ }
48104
+ const prunedGraph = prune(compiledGraph, {
48105
+ actions: argv[actionsOption.name],
48106
+ includeDependencies: argv[includeDepsOption.name],
48107
+ includeDependents: argv[includeDependentsOption.name],
48108
+ tags: argv[tagsOption.name]
48109
+ });
48110
+ const executionSql = new ExecutionSql(compiledGraph.projectConfig, dataformVersion);
48111
+ const shadowSchemas = Array.from(new Set((prunedGraph.tables || []).map(table => table.target.schema)));
48112
+ process.on("SIGINT", () => {
48113
+ Promise.all(shadowSchemas.map(schema => dbadapter.execute(executionSql.dropSchemaCascadeSql(schema)).catch(() => undefined))).then(() => process.exit(1));
48114
+ });
48115
+ const deps = {
48116
+ evaluate: action => dbadapter.evaluate(action.enumType !== undefined
48117
+ ? sqlanvil.Table.create(action)
48118
+ : sqlanvil.Assertion.create(action)),
48119
+ execute: sql => dbadapter.execute(sql).then(() => undefined),
48120
+ validationStubSql: table => executionSql.validationStubSql(table),
48121
+ createSchemaSql: schema => executionSql.createSchemaSql(schema),
48122
+ dropSchemaCascadeSql: schema => executionSql.dropSchemaCascadeSql(schema),
48123
+ listSchemas: async () => {
48124
+ const result = await dbadapter.execute("select schema_name as name from information_schema.schemata");
48125
+ return ((result && result.rows) || []).map((row) => row.name);
48126
+ }
48127
+ };
48128
+ await sweepOrphanShadows(deps, Date.now());
48129
+ if (!argv[jsonOutputOption.name]) {
48130
+ print("Validating...\n");
48131
+ }
48132
+ const results = await validate(prunedGraph, deps, { keepShadow: argv[keepShadowOption.name] });
48133
+ return printValidationResults(results, argv[jsonOutputOption.name]);
48134
+ }
47798
48135
  function runCli() {
47799
48136
  const builtYargs = createYargsCli({
47800
48137
  commands: [
@@ -48075,6 +48412,24 @@ function runCli() {
48075
48412
  return testResults.every(testResult => testResult.successful) ? 0 : 1;
48076
48413
  }
48077
48414
  },
48415
+ {
48416
+ format: `validate [${projectDirMustExistOption.name}]`,
48417
+ description: "Validate the project's SQL against the warehouse planner (EXPLAIN/dry-run) without " +
48418
+ "executing. Postgres/Supabase/MySQL only.",
48419
+ positionalOptions: [projectDirMustExistOption],
48420
+ options: [
48421
+ actionsOption,
48422
+ tagsOption,
48423
+ includeDepsOption,
48424
+ includeDependentsOption,
48425
+ credentialsOption,
48426
+ jsonOutputOption,
48427
+ timeoutOption,
48428
+ keepShadowOption,
48429
+ ...ProjectConfigOptions.allYargsOptions
48430
+ ],
48431
+ processFn: async (argv) => runValidate(argv)
48432
+ },
48078
48433
  {
48079
48434
  format: `run [${projectDirMustExistOption.name}]`,
48080
48435
  description: "Run the sqlanvil project.",
@@ -48127,6 +48482,9 @@ function runCli() {
48127
48482
  printSuccess("Compiled successfully.\n");
48128
48483
  }
48129
48484
  const warehouse = compiledGraph.projectConfig.warehouse || "bigquery";
48485
+ if (argv[dryRunOptionName] && warehouse.toLowerCase() !== "bigquery") {
48486
+ return runValidate(argv);
48487
+ }
48130
48488
  const readCredentials = read(credentialsPathWithEnvironment(argv[projectDirOption.name], argv), warehouse);
48131
48489
  let dbadapter;
48132
48490
  if (warehouse.toLowerCase() === "supabase") {
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.9.0",
67
67
  "name": "@sqlanvil/cli",
68
68
  "description": "sqlanvil command line interface.",
69
69
  "main": "bundle.js"