@sqlanvil/cli 1.17.1 → 1.18.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 (3) hide show
  1. package/bundle.js +1548 -1384
  2. package/package.json +1 -1
  3. package/worker_bundle.js +46 -0
package/bundle.js CHANGED
@@ -27044,6 +27044,7 @@ const sqlanvil = $root.sqlanvil = (() => {
27044
27044
  * @property {string|null} [billingProject] Extract billingProject
27045
27045
  * @property {Object.<string,string>|null} [columnTypes] Extract columnTypes
27046
27046
  * @property {string|null} [fileName] Extract fileName
27047
+ * @property {string|null} [database] Extract database
27047
27048
  */
27048
27049
 
27049
27050
  /**
@@ -27184,6 +27185,14 @@ const sqlanvil = $root.sqlanvil = (() => {
27184
27185
  */
27185
27186
  Extract.prototype.fileName = "";
27186
27187
 
27188
+ /**
27189
+ * Extract database.
27190
+ * @member {string} database
27191
+ * @memberof sqlanvil.Extract
27192
+ * @instance
27193
+ */
27194
+ Extract.prototype.database = "";
27195
+
27187
27196
  /**
27188
27197
  * Creates a new Extract instance using the specified properties.
27189
27198
  * @function create
@@ -27245,6 +27254,8 @@ const sqlanvil = $root.sqlanvil = (() => {
27245
27254
  writer.uint32(/* id 14, wireType 2 =*/114).fork().uint32(/* id 1, wireType 2 =*/10).string(keys[i]).uint32(/* id 2, wireType 2 =*/18).string(message.columnTypes[keys[i]]).ldelim();
27246
27255
  if (message.fileName != null && Object.hasOwnProperty.call(message, "fileName"))
27247
27256
  writer.uint32(/* id 15, wireType 2 =*/122).string(message.fileName);
27257
+ if (message.database != null && Object.hasOwnProperty.call(message, "database"))
27258
+ writer.uint32(/* id 16, wireType 2 =*/130).string(message.database);
27248
27259
  return writer;
27249
27260
  };
27250
27261
 
@@ -27370,6 +27381,10 @@ const sqlanvil = $root.sqlanvil = (() => {
27370
27381
  message.fileName = reader.string();
27371
27382
  break;
27372
27383
  }
27384
+ case 16: {
27385
+ message.database = reader.string();
27386
+ break;
27387
+ }
27373
27388
  default:
27374
27389
  reader.skipType(tag & 7, long);
27375
27390
  break;
@@ -27481,6 +27496,9 @@ const sqlanvil = $root.sqlanvil = (() => {
27481
27496
  if (message.fileName != null && Object.hasOwnProperty.call(message, "fileName"))
27482
27497
  if (!$util.isString(message.fileName))
27483
27498
  return "fileName: string expected";
27499
+ if (message.database != null && Object.hasOwnProperty.call(message, "database"))
27500
+ if (!$util.isString(message.database))
27501
+ return "database: string expected";
27484
27502
  return null;
27485
27503
  };
27486
27504
 
@@ -27580,6 +27598,8 @@ const sqlanvil = $root.sqlanvil = (() => {
27580
27598
  }
27581
27599
  if (object.fileName != null)
27582
27600
  message.fileName = String(object.fileName);
27601
+ if (object.database != null)
27602
+ message.database = String(object.database);
27583
27603
  return message;
27584
27604
  };
27585
27605
 
@@ -27619,6 +27639,7 @@ const sqlanvil = $root.sqlanvil = (() => {
27619
27639
  object.sourceName = "";
27620
27640
  object.billingProject = "";
27621
27641
  object.fileName = "";
27642
+ object.database = "";
27622
27643
  }
27623
27644
  if (message.target != null && Object.hasOwnProperty.call(message, "target"))
27624
27645
  object.target = $root.sqlanvil.Target.toObject(message.target, options, q + 1);
@@ -27663,6 +27684,8 @@ const sqlanvil = $root.sqlanvil = (() => {
27663
27684
  }
27664
27685
  if (message.fileName != null && Object.hasOwnProperty.call(message, "fileName"))
27665
27686
  object.fileName = message.fileName;
27687
+ if (message.database != null && Object.hasOwnProperty.call(message, "database"))
27688
+ object.database = message.database;
27666
27689
  return object;
27667
27690
  };
27668
27691
 
@@ -37844,6 +37867,7 @@ const sqlanvil = $root.sqlanvil = (() => {
37844
37867
  * @property {string|null} [sourceName] ExtractSpec sourceName
37845
37868
  * @property {string|null} [billingProject] ExtractSpec billingProject
37846
37869
  * @property {Object.<string,string>|null} [columnTypes] ExtractSpec columnTypes
37870
+ * @property {string|null} [database] ExtractSpec database
37847
37871
  */
37848
37872
 
37849
37873
  /**
@@ -37918,6 +37942,14 @@ const sqlanvil = $root.sqlanvil = (() => {
37918
37942
  */
37919
37943
  ExtractSpec.prototype.columnTypes = $util.emptyObject;
37920
37944
 
37945
+ /**
37946
+ * ExtractSpec database.
37947
+ * @member {string} database
37948
+ * @memberof sqlanvil.ExtractSpec
37949
+ * @instance
37950
+ */
37951
+ ExtractSpec.prototype.database = "";
37952
+
37921
37953
  /**
37922
37954
  * Creates a new ExtractSpec instance using the specified properties.
37923
37955
  * @function create
@@ -37961,6 +37993,8 @@ const sqlanvil = $root.sqlanvil = (() => {
37961
37993
  if (message.columnTypes != null && Object.hasOwnProperty.call(message, "columnTypes"))
37962
37994
  for (let keys = Object.keys(message.columnTypes), i = 0; i < keys.length; ++i)
37963
37995
  writer.uint32(/* id 7, wireType 2 =*/58).fork().uint32(/* id 1, wireType 2 =*/10).string(keys[i]).uint32(/* id 2, wireType 2 =*/18).string(message.columnTypes[keys[i]]).ldelim();
37996
+ if (message.database != null && Object.hasOwnProperty.call(message, "database"))
37997
+ writer.uint32(/* id 8, wireType 2 =*/66).string(message.database);
37964
37998
  return writer;
37965
37999
  };
37966
38000
 
@@ -38050,6 +38084,10 @@ const sqlanvil = $root.sqlanvil = (() => {
38050
38084
  message.columnTypes[key] = value;
38051
38085
  break;
38052
38086
  }
38087
+ case 8: {
38088
+ message.database = reader.string();
38089
+ break;
38090
+ }
38053
38091
  default:
38054
38092
  reader.skipType(tag & 7, long);
38055
38093
  break;
@@ -38115,6 +38153,9 @@ const sqlanvil = $root.sqlanvil = (() => {
38115
38153
  if (!$util.isString(message.columnTypes[key[i]]))
38116
38154
  return "columnTypes: string{k:string} expected";
38117
38155
  }
38156
+ if (message.database != null && Object.hasOwnProperty.call(message, "database"))
38157
+ if (!$util.isString(message.database))
38158
+ return "database: string expected";
38118
38159
  return null;
38119
38160
  };
38120
38161
 
@@ -38158,6 +38199,8 @@ const sqlanvil = $root.sqlanvil = (() => {
38158
38199
  message.columnTypes[keys[i]] = String(object.columnTypes[keys[i]]);
38159
38200
  }
38160
38201
  }
38202
+ if (object.database != null)
38203
+ message.database = String(object.database);
38161
38204
  return message;
38162
38205
  };
38163
38206
 
@@ -38187,6 +38230,7 @@ const sqlanvil = $root.sqlanvil = (() => {
38187
38230
  object.dataset = "";
38188
38231
  object.sourceName = "";
38189
38232
  object.billingProject = "";
38233
+ object.database = "";
38190
38234
  }
38191
38235
  if (message.connectionName != null && Object.hasOwnProperty.call(message, "connectionName"))
38192
38236
  object.connectionName = message.connectionName;
@@ -38209,6 +38253,8 @@ const sqlanvil = $root.sqlanvil = (() => {
38209
38253
  object.columnTypes[keys2[j]] = message.columnTypes[keys2[j]];
38210
38254
  }
38211
38255
  }
38256
+ if (message.database != null && Object.hasOwnProperty.call(message, "database"))
38257
+ object.database = message.database;
38212
38258
  return object;
38213
38259
  };
38214
38260
 
@@ -48751,7 +48797,7 @@ function collectEvaluationQueries(queryOrAction, concatenate, queryModifier = (q
48751
48797
  .filter(validationQuery => !!validationQuery.query);
48752
48798
  }
48753
48799
 
48754
- const version = "1.17.1";
48800
+ const version = "1.18.0";
48755
48801
  const dataformVersion = "3.0.61";
48756
48802
 
48757
48803
  async function build(compiledGraph, runConfig, dbadapter) {
@@ -48817,6 +48863,7 @@ class Builder {
48817
48863
  platform: ext.platform,
48818
48864
  project: ext.project,
48819
48865
  dataset: ext.dataset,
48866
+ database: ext.database,
48820
48867
  sourceName: ext.sourceName,
48821
48868
  billingProject: ext.billingProject,
48822
48869
  columnTypes: ext.columnTypes
@@ -49269,8 +49316,36 @@ function mapBigQueryType(bqType) {
49269
49316
  function mapPostgresType(pgType) {
49270
49317
  return pgType.trim().toLowerCase();
49271
49318
  }
49319
+ const MYSQL_TYPE_MAP = {
49320
+ tinyint: "smallint",
49321
+ mediumint: "integer",
49322
+ int: "integer",
49323
+ double: "double precision",
49324
+ float: "real",
49325
+ decimal: "numeric",
49326
+ bit: "smallint",
49327
+ year: "smallint",
49328
+ datetime: "timestamp",
49329
+ timestamp: "timestamptz",
49330
+ char: "text",
49331
+ varchar: "text",
49332
+ tinytext: "text",
49333
+ mediumtext: "text",
49334
+ longtext: "text",
49335
+ enum: "text",
49336
+ set: "text",
49337
+ binary: "bytea",
49338
+ varbinary: "bytea",
49339
+ tinyblob: "bytea",
49340
+ blob: "bytea",
49341
+ mediumblob: "bytea",
49342
+ longblob: "bytea",
49343
+ json: "jsonb",
49344
+ geometry: "text"
49345
+ };
49272
49346
  function mapMysqlType(mysqlType) {
49273
- return mysqlType.trim().toLowerCase();
49347
+ const key = mysqlType.trim().toLowerCase();
49348
+ return MYSQL_TYPE_MAP[key] || key;
49274
49349
  }
49275
49350
  function keyToken(name) {
49276
49351
  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) ? name : `"${name}"`;
@@ -50358,10 +50433,55 @@ class PostgresDbAdapter {
50358
50433
  }
50359
50434
  }
50360
50435
 
50361
- const DEFAULT_ROW_CAP = 1000000;
50362
- const DEFAULT_BYTE_CAP = 512 * 1024 * 1024;
50363
50436
  const MAX_PARAMS_PER_INSERT = 60000;
50364
50437
  const quoteIdent = (id) => `"${String(id).replace(/"/g, '""')}"`;
50438
+ async function loadRowsIntoPostgres(args) {
50439
+ var _a;
50440
+ const { target, columnTypes, rows } = args;
50441
+ const cols = Object.keys(columnTypes);
50442
+ const coerce = args.coerce || ((v) => (v === undefined ? null : v));
50443
+ const pg = await PostgresDbAdapter.create(args.pg, {
50444
+ disableSslForTestsOnly: args.disableSslForTestsOnly
50445
+ });
50446
+ try {
50447
+ const qualified = `${quoteIdent(target.schema)}.${quoteIdent(target.name)}`;
50448
+ const colDefs = cols.map(c => `${quoteIdent(c)} ${columnTypes[c]}`).join(", ");
50449
+ await pg.execute(`create schema if not exists ${quoteIdent(target.schema)}`);
50450
+ const { rows: existing } = await pg.execute(`select c.relkind from pg_class c join pg_namespace n on n.oid = c.relnamespace ` +
50451
+ `where n.nspname = $1 and c.relname = $2`, { params: [target.schema, target.name] });
50452
+ const relkind = (_a = existing[0]) === null || _a === void 0 ? void 0 : _a.relkind;
50453
+ if (relkind === "f") {
50454
+ await pg.execute(`drop foreign table ${qualified} cascade`);
50455
+ }
50456
+ else if (relkind === "v") {
50457
+ await pg.execute(`drop view ${qualified} cascade`);
50458
+ }
50459
+ else if (relkind) {
50460
+ await pg.execute(`drop table ${qualified} cascade`);
50461
+ }
50462
+ await pg.execute(`create table ${qualified} (${colDefs})`);
50463
+ const colIdents = cols.map(quoteIdent).join(", ");
50464
+ const batchRows = Math.max(1, Math.min(1000, Math.floor(MAX_PARAMS_PER_INSERT / cols.length)));
50465
+ for (let i = 0; i < rows.length; i += batchRows) {
50466
+ const batch = rows.slice(i, i + batchRows);
50467
+ const params = [];
50468
+ const tuples = batch.map((row, r) => {
50469
+ const placeholders = cols.map((c, ci) => `$${r * cols.length + ci + 1}`);
50470
+ cols.forEach(c => params.push(coerce(row[c])));
50471
+ return `(${placeholders.join(", ")})`;
50472
+ });
50473
+ await pg.execute(`insert into ${qualified} (${colIdents}) values ${tuples.join(", ")}`, {
50474
+ params
50475
+ });
50476
+ }
50477
+ }
50478
+ finally {
50479
+ await pg.close();
50480
+ }
50481
+ }
50482
+
50483
+ const DEFAULT_ROW_CAP$1 = 1000000;
50484
+ const DEFAULT_BYTE_CAP$1 = 512 * 1024 * 1024;
50365
50485
  function coerce(v) {
50366
50486
  if (v === null || v === undefined) {
50367
50487
  return null;
@@ -50375,7 +50495,7 @@ async function runBigQueryExtract(args) {
50375
50495
  var _a, _b;
50376
50496
  const { spec, target, connectionCredentials } = args;
50377
50497
  if ((spec.platform || "bigquery") !== "bigquery") {
50378
- throw new Error(`runner-extract supports only bigquery sources (got "${spec.platform}").`);
50498
+ throw new Error(`bigquery runner-extract got a "${spec.platform}" source.`);
50379
50499
  }
50380
50500
  const cols = Object.keys(spec.columnTypes || {});
50381
50501
  if (cols.length === 0) {
@@ -50391,8 +50511,8 @@ async function runBigQueryExtract(args) {
50391
50511
  accessToken: conn.accessToken,
50392
50512
  credentials: conn.credentials
50393
50513
  }));
50394
- const rowCap = (_a = args.rowCap) !== null && _a !== void 0 ? _a : DEFAULT_ROW_CAP;
50395
- const byteCap = (_b = args.byteCap) !== null && _b !== void 0 ? _b : DEFAULT_BYTE_CAP;
50514
+ const rowCap = (_a = args.rowCap) !== null && _a !== void 0 ? _a : DEFAULT_ROW_CAP$1;
50515
+ const byteCap = (_b = args.byteCap) !== null && _b !== void 0 ? _b : DEFAULT_BYTE_CAP$1;
50396
50516
  const colList = cols.map(c => "`" + c + "`").join(", ");
50397
50517
  const fqn = "`" + `${spec.project}.${spec.dataset}.${spec.sourceName}` + "`";
50398
50518
  const { rows } = await bq.execute(`select ${colList} from ${fqn}`, {
@@ -50402,471 +50522,868 @@ async function runBigQueryExtract(args) {
50402
50522
  if (rows.length >= rowCap) {
50403
50523
  console.warn(`runner-extract: ${target.schema}.${target.name} truncated at ${rowCap} rows (source is larger).`);
50404
50524
  }
50405
- const pg = await PostgresDbAdapter.create(args.pg, {
50525
+ await loadRowsIntoPostgres({
50526
+ pg: args.pg,
50527
+ target,
50528
+ columnTypes: spec.columnTypes,
50529
+ rows,
50530
+ coerce,
50406
50531
  disableSslForTestsOnly: args.disableSslForTestsOnly
50407
50532
  });
50408
- try {
50409
- const qualified = `${quoteIdent(target.schema)}.${quoteIdent(target.name)}`;
50410
- const colDefs = cols.map(c => `${quoteIdent(c)} ${spec.columnTypes[c]}`).join(", ");
50411
- await pg.execute(`create schema if not exists ${quoteIdent(target.schema)}`);
50412
- await pg.execute(`drop foreign table if exists ${qualified} cascade`);
50413
- await pg.execute(`drop table if exists ${qualified} cascade`);
50414
- await pg.execute(`create table ${qualified} (${colDefs})`);
50415
- const colIdents = cols.map(quoteIdent).join(", ");
50416
- const batchRows = Math.max(1, Math.min(1000, Math.floor(MAX_PARAMS_PER_INSERT / cols.length)));
50417
- for (let i = 0; i < rows.length; i += batchRows) {
50418
- const batch = rows.slice(i, i + batchRows);
50419
- const params = [];
50420
- const tuples = batch.map((row, r) => {
50421
- const placeholders = cols.map((c, ci) => `$${r * cols.length + ci + 1}`);
50422
- cols.forEach(c => params.push(coerce(row[c])));
50423
- return `(${placeholders.join(", ")})`;
50424
- });
50425
- await pg.execute(`insert into ${qualified} (${colIdents}) values ${tuples.join(", ")}`, {
50426
- params
50427
- });
50428
- }
50429
- return { rowCount: rows.length };
50430
- }
50431
- finally {
50432
- await pg.close();
50433
- }
50533
+ return { rowCount: rows.length };
50434
50534
  }
50435
50535
 
50436
- function loadDuckdb() {
50437
- try {
50438
- return nativeRequire("@duckdb/node-api");
50439
- }
50440
- catch (e) {
50441
- throw new Error(`This feature requires the optional "@duckdb/node-api" dependency, which failed to load: ` +
50442
- `${e.message}`);
50536
+ class MySqlPoolExecutor {
50537
+ constructor(config, options) {
50538
+ this.pool = mysql__namespace.createPool(Object.assign(Object.assign({}, config), { connectionLimit: (options === null || options === void 0 ? void 0 : options.concurrencyLimit) || 10, waitForConnections: true, multipleStatements: false }));
50443
50539
  }
50444
- }
50445
- async function runAsync(conn, sql) {
50446
- await conn.run(sql);
50447
- }
50448
- async function allAsync(conn, sql) {
50449
- const reader = await conn.runAndReadAll(sql);
50450
- return reader.getRowObjects();
50451
- }
50452
- async function withDuckdb(fn, dbPath = ":memory:") {
50453
- const { DuckDBInstance } = loadDuckdb();
50454
- const instance = await DuckDBInstance.create(dbPath);
50455
- const conn = await instance.connect();
50456
- const done = () => {
50457
- var _a, _b;
50540
+ async verifyConnection() {
50541
+ const conn = await this.pool.getConnection();
50458
50542
  try {
50459
- (_a = conn.closeSync) === null || _a === void 0 ? void 0 : _a.call(conn);
50543
+ await conn.query("select 1");
50460
50544
  }
50461
- catch (e) {
50545
+ finally {
50546
+ conn.release();
50462
50547
  }
50548
+ }
50549
+ async withClientLock(callback) {
50550
+ const conn = await this.pool.getConnection();
50551
+ let released = false;
50552
+ const releaseOnce = () => {
50553
+ if (released) {
50554
+ return;
50555
+ }
50556
+ released = true;
50557
+ conn.release();
50558
+ };
50463
50559
  try {
50464
- (_b = instance.closeSync) === null || _b === void 0 ? void 0 : _b.call(instance);
50465
- }
50466
- catch (e) {
50560
+ return await callback({
50561
+ execute: async (statement, options = { rowLimit: 1000 }) => {
50562
+ const [rows] = await conn.query(statement, options.params || []);
50563
+ const arr = Array.isArray(rows) ? rows : [];
50564
+ return options.rowLimit && arr.length > options.rowLimit
50565
+ ? arr.slice(0, options.rowLimit)
50566
+ : arr;
50567
+ }
50568
+ });
50467
50569
  }
50468
- };
50469
- try {
50470
- if (!process.env.HOME) {
50471
- await runAsync(conn, `SET home_directory='${os__namespace.tmpdir()}'`);
50570
+ finally {
50571
+ releaseOnce();
50472
50572
  }
50473
- const result = await fn(conn);
50474
- done();
50475
- return result;
50476
50573
  }
50477
- catch (e) {
50478
- done();
50479
- throw e;
50574
+ async close() {
50575
+ await this.pool.end();
50480
50576
  }
50481
50577
  }
50482
-
50483
- const PG_ATTACH_ALIAS = "pg";
50484
- const SECRET_NAME = "sa_export";
50485
- function buildAttachSql(pg, opts = {}) {
50486
- const readOnly = opts.readOnly !== false;
50487
- const dsn = [
50488
- pg.host && `host=${pg.host}`,
50489
- pg.port && `port=${pg.port}`,
50490
- pg.database && `dbname=${pg.database}`,
50491
- pg.user && `user=${pg.user}`,
50492
- pg.password && `password=${pg.password}`
50493
- ]
50494
- .filter(Boolean)
50495
- .join(" ");
50496
- const mode = readOnly ? ", READ_ONLY" : "";
50497
- return `ATTACH '${dsn}' AS ${PG_ATTACH_ALIAS} (TYPE postgres${mode})`;
50498
- }
50499
- function buildSecretSql(scheme, creds) {
50500
- if (scheme === "s3") {
50501
- const fields = [
50502
- "TYPE s3",
50503
- creds.accessKeyId && `KEY_ID '${creds.accessKeyId}'`,
50504
- creds.secretAccessKey && `SECRET '${creds.secretAccessKey}'`,
50505
- creds.region && `REGION '${creds.region}'`,
50506
- creds.endpoint && `ENDPOINT '${creds.endpoint}'`,
50507
- creds.endpoint && `URL_STYLE 'path'`,
50508
- creds.endpoint && `USE_SSL true`
50509
- ]
50510
- .filter(Boolean)
50511
- .join(", ");
50512
- return `CREATE OR REPLACE SECRET ${SECRET_NAME} (${fields})`;
50513
- }
50514
- if (scheme === "gcs") {
50515
- const fields = [
50516
- "TYPE gcs",
50517
- creds.keyId && `KEY_ID '${creds.keyId}'`,
50518
- creds.secret && `SECRET '${creds.secret}'`
50519
- ]
50520
- .filter(Boolean)
50521
- .join(", ");
50522
- return `CREATE OR REPLACE SECRET ${SECRET_NAME} (${fields})`;
50578
+ function convertFieldType(type) {
50579
+ switch (String(type).toUpperCase()) {
50580
+ case "FLOAT":
50581
+ case "DOUBLE":
50582
+ case "REAL":
50583
+ return sqlanvil.Field.Primitive.FLOAT;
50584
+ case "TINYINT":
50585
+ case "SMALLINT":
50586
+ case "MEDIUMINT":
50587
+ case "INT":
50588
+ case "INTEGER":
50589
+ case "BIGINT":
50590
+ case "YEAR":
50591
+ case "BIT":
50592
+ return sqlanvil.Field.Primitive.INTEGER;
50593
+ case "DECIMAL":
50594
+ case "DEC":
50595
+ case "NUMERIC":
50596
+ case "FIXED":
50597
+ return sqlanvil.Field.Primitive.NUMERIC;
50598
+ case "BOOL":
50599
+ case "BOOLEAN":
50600
+ return sqlanvil.Field.Primitive.BOOLEAN;
50601
+ case "CHAR":
50602
+ case "VARCHAR":
50603
+ case "TINYTEXT":
50604
+ case "TEXT":
50605
+ case "MEDIUMTEXT":
50606
+ case "LONGTEXT":
50607
+ case "ENUM":
50608
+ case "SET":
50609
+ case "JSON":
50610
+ case "TIME":
50611
+ return sqlanvil.Field.Primitive.STRING;
50612
+ case "DATE":
50613
+ return sqlanvil.Field.Primitive.DATE;
50614
+ case "DATETIME":
50615
+ case "TIMESTAMP":
50616
+ return sqlanvil.Field.Primitive.TIMESTAMP;
50617
+ default:
50618
+ return sqlanvil.Field.Primitive.UNKNOWN;
50523
50619
  }
50524
- return null;
50525
50620
  }
50526
- function toCopyTarget(uri) {
50527
- if (uri.startsWith("local://")) {
50528
- return uri.slice("local://".length);
50529
- }
50530
- return uri;
50621
+ function escapeMysqlString(s) {
50622
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
50531
50623
  }
50532
- function schemeOf(uri) {
50533
- if (uri.startsWith("s3://")) {
50534
- return "s3";
50624
+ function reconstructColumnDef(col) {
50625
+ const extra = (col.extra || "").toLowerCase();
50626
+ const notNull = col.isNullable === "NO";
50627
+ if (extra.includes("generated") && col.generationExpression) {
50628
+ const storage = extra.includes("stored") ? "STORED" : "VIRTUAL";
50629
+ return `${col.columnType} GENERATED ALWAYS AS (${col.generationExpression}) ${storage}${notNull ? " NOT NULL" : ""}`;
50535
50630
  }
50536
- if (uri.startsWith("gs://") || uri.startsWith("gcs://")) {
50537
- return "gcs";
50631
+ let def = col.columnType;
50632
+ if (col.collationName) {
50633
+ def += ` COLLATE ${col.collationName}`;
50538
50634
  }
50539
- return "local";
50540
- }
50541
- function buildCopySql(selectSql, uri, format, options = {}) {
50542
- const target = toCopyTarget(uri);
50543
- const fmt = (format || "").toLowerCase();
50544
- const extraOptions = Object.entries(options || {}).map(([key, value]) => `${key.toUpperCase()} ${value}`);
50545
- const optionList = [`FORMAT ${fmt}`, ...extraOptions].join(", ");
50546
- return `COPY (SELECT * FROM postgres_query('${PG_ATTACH_ALIAS}', $sa$${selectSql}$sa$)) TO '${target}' (${optionList})`;
50547
- }
50548
- async function runDuckdbExport(args) {
50549
- const { spec, selectSql, pg, storage, actionName } = args;
50550
- const uri = resolveExportUri(spec, actionName, { wildcard: false });
50551
- const scheme = schemeOf(uri);
50552
- if (scheme !== "local" && !(storage === null || storage === void 0 ? void 0 : storage[scheme])) {
50553
- throw new Error(`No "${scheme}" storage credentials found in .df-credentials.json (storage.${scheme}) for ` +
50554
- `export to ${uri}.`);
50635
+ def += notNull ? " NOT NULL" : " NULL";
50636
+ def += defaultClause(col, extra);
50637
+ if (extra.includes("on update current_timestamp")) {
50638
+ def += " ON UPDATE CURRENT_TIMESTAMP";
50555
50639
  }
50556
- return withDuckdb(async (conn) => {
50557
- await runAsync(conn, "INSTALL postgres; LOAD postgres; INSTALL httpfs; LOAD httpfs;");
50558
- await runAsync(conn, buildAttachSql(pg));
50559
- if (scheme !== "local") {
50560
- const secret = buildSecretSql(scheme, storage[scheme]);
50561
- if (secret) {
50562
- await runAsync(conn, secret);
50563
- }
50564
- }
50565
- await runAsync(conn, buildCopySql(selectSql, uri, spec.format, spec.options || {}));
50566
- return { destination: toCopyTarget(uri) };
50567
- });
50568
- }
50569
-
50570
- function readerForFormat(format) {
50571
- switch ((format || "").toLowerCase()) {
50572
- case "csv":
50573
- return "read_csv_auto";
50574
- case "json":
50575
- return "read_json_auto";
50576
- case "parquet":
50577
- default:
50578
- return "read_parquet";
50640
+ if (extra.includes("auto_increment")) {
50641
+ def += " AUTO_INCREMENT";
50579
50642
  }
50643
+ return def;
50580
50644
  }
50581
- function importTargetSql(target) {
50582
- return target.schema
50583
- ? `${PG_ATTACH_ALIAS}."${target.schema}"."${target.name}"`
50584
- : `${PG_ATTACH_ALIAS}."${target.name}"`;
50585
- }
50586
- function buildImportSql(target, location, format, overwrite) {
50587
- const reader = readerForFormat(format);
50588
- const source = toCopyTarget(location);
50589
- const dest = importTargetSql(target);
50590
- const select = `SELECT * FROM ${reader}('${source}')`;
50591
- if (overwrite) {
50592
- return [`DROP TABLE IF EXISTS ${dest}`, `CREATE TABLE ${dest} AS ${select}`];
50645
+ function defaultClause(col, extra) {
50646
+ if (extra.includes("default_generated")) {
50647
+ const d = col.columnDefault || "";
50648
+ return /^current_timestamp/i.test(d) ? ` DEFAULT ${d}` : ` DEFAULT (${d})`;
50593
50649
  }
50594
- return [`INSERT INTO ${dest} ${select}`];
50595
- }
50596
- async function runDuckdbImport(args) {
50597
- const { spec, target, pg, storage } = args;
50598
- const location = spec.location || "";
50599
- const scheme = schemeOf(location);
50600
- if (scheme !== "local" && !(storage === null || storage === void 0 ? void 0 : storage[scheme])) {
50601
- throw new Error(`No "${scheme}" storage credentials found in .df-credentials.json (storage.${scheme}) for ` +
50602
- `import from ${location}.`);
50650
+ if (col.columnDefault === null || col.columnDefault === undefined) {
50651
+ return "";
50603
50652
  }
50604
- return withDuckdb(async (conn) => {
50605
- await runAsync(conn, "INSTALL postgres; LOAD postgres; INSTALL httpfs; LOAD httpfs;");
50606
- await runAsync(conn, buildAttachSql(pg, { readOnly: false }));
50607
- if (scheme !== "local") {
50608
- const secret = buildSecretSql(scheme, storage[scheme]);
50609
- if (secret) {
50610
- await runAsync(conn, secret);
50611
- }
50612
- }
50613
- for (const statement of buildImportSql(target, location, spec.format, spec.overwrite)) {
50614
- await runAsync(conn, statement);
50615
- }
50616
- return { source: toCopyTarget(location) };
50617
- });
50653
+ const d = col.columnDefault;
50654
+ if (d.toUpperCase() === "NULL" || /^-?\d+(\.\d+)?$/.test(d)) {
50655
+ return ` DEFAULT ${d}`;
50656
+ }
50657
+ return ` DEFAULT '${escapeMysqlString(d)}'`;
50618
50658
  }
50619
50659
 
50620
- class Flags {
50621
- static boolean(name, defaultValue = false) {
50622
- return new SingleValueFlag(name, defaultValue, (stringValue) => {
50623
- if (stringValue === "" || stringValue === "true") {
50624
- return true;
50625
- }
50626
- if (stringValue === "false") {
50627
- return false;
50628
- }
50629
- throw Flags.invalidFlagValueError(name);
50630
- });
50631
- }
50632
- static string(name, defaultValue = "") {
50633
- return new SingleValueFlag(name, defaultValue, (stringValue) => stringValue);
50634
- }
50635
- static number(name, defaultValue = 0) {
50636
- return new SingleValueFlag(name, defaultValue, (stringValue) => parseFloat(stringValue));
50637
- }
50638
- static long(name, defaultValue = Long__default["default"].ZERO) {
50639
- return new SingleValueFlag(name, defaultValue, Long__default["default"].fromString);
50660
+ const INTERNAL_SCHEMAS = new Set([
50661
+ "information_schema",
50662
+ "mysql",
50663
+ "performance_schema",
50664
+ "sys"
50665
+ ]);
50666
+ class MySqlDbAdapter {
50667
+ constructor(queryExecutor) {
50668
+ this.queryExecutor = queryExecutor;
50640
50669
  }
50641
- static stringSet(name, defaultValue = new Set()) {
50642
- return new SetFlag(name, defaultValue, (singleValue) => singleValue);
50670
+ static async create(credentials, options) {
50671
+ const sslMode = (credentials.sslMode || "").toLowerCase();
50672
+ const ssl = !(options === null || options === void 0 ? void 0 : options.disableSslForTestsOnly) && sslMode && sslMode !== "disable"
50673
+ ?
50674
+ { rejectUnauthorized: false }
50675
+ : undefined;
50676
+ const queryExecutor = new MySqlPoolExecutor({
50677
+ host: credentials.host,
50678
+ port: credentials.port || 3306,
50679
+ user: credentials.user,
50680
+ password: credentials.password,
50681
+ database: credentials.database || undefined,
50682
+ ssl
50683
+ }, options);
50684
+ try {
50685
+ await queryExecutor.verifyConnection();
50686
+ }
50687
+ catch (e) {
50688
+ await queryExecutor.close().catch(() => undefined);
50689
+ throw new ErrorWithCause(`Could not connect to MySQL at ${credentials.host}:${credentials.port || 3306} ` +
50690
+ `as "${credentials.user}": ${e.message}`, e);
50691
+ }
50692
+ return new MySqlDbAdapter(queryExecutor);
50643
50693
  }
50644
- static getRawFlagValue(flagName) {
50645
- return Flags.parsedArgv[flagName];
50694
+ async execute(statement, options = { rowLimit: 1000, byteLimit: 1024 * 1024 }) {
50695
+ return await this.withClientLock(client => client.execute(statement, options));
50646
50696
  }
50647
- static setRawFlagValueForTesting(flagName, value) {
50648
- Flags.parsedArgv[flagName] = value;
50697
+ async executeRaw(statement, options = { rowLimit: 1000 }) {
50698
+ const result = await this.execute(statement, options);
50699
+ return Object.assign(Object.assign({}, result), { schema: [] });
50649
50700
  }
50650
- static invalidFlagValueError(flagName) {
50651
- return new Error(`Invalid flag value: ${Flags.getRawFlagValue(flagName)} [${flagName}]`);
50701
+ async withClientLock(callback) {
50702
+ return await this.queryExecutor.withClientLock(client => callback({
50703
+ execute: async (stmt, opts = { rowLimit: 1000, byteLimit: 1024 * 1024 }) => {
50704
+ try {
50705
+ const rows = await client.execute(stmt, { params: opts.params, rowLimit: opts.rowLimit });
50706
+ return { rows, metadata: {} };
50707
+ }
50708
+ catch (e) {
50709
+ if (opts.includeQueryInError) {
50710
+ throw new Error(`Error encountered while running "${stmt}": ${e.message}`);
50711
+ }
50712
+ throw new ErrorWithCause(`Error executing mysql query: ${e.message}`, e);
50713
+ }
50714
+ },
50715
+ executeRaw: async (stmt, opts = { rowLimit: 1000 }) => {
50716
+ const positional = opts.params ? Object.values(opts.params) : undefined;
50717
+ const rows = await client.execute(stmt, { params: positional, rowLimit: opts.rowLimit });
50718
+ return { rows, schema: [], metadata: {} };
50719
+ }
50720
+ }));
50652
50721
  }
50653
- }
50654
- Flags.parsedArgv = parseArgv(process.argv);
50655
- function parseArgv(argv) {
50656
- const parsedArgv = {};
50657
- const splitArgv = [];
50658
- for (let arg of argv) {
50659
- if (arg.startsWith("--no-")) {
50660
- arg = `--${arg.slice(5)}=false`;
50661
- }
50662
- if (arg.startsWith("--") && arg.includes("=")) {
50663
- splitArgv.push(arg.slice(0, arg.indexOf("=")));
50664
- splitArgv.push(arg.slice(arg.indexOf("=") + 1));
50665
- }
50666
- else {
50667
- splitArgv.push(arg);
50722
+ async evaluate(queryOrAction) {
50723
+ const validationQueries = collectEvaluationQueries(queryOrAction, false, (query) => !!query ? `explain ${query}` : "").map((validationQuery, index) => ({ index, validationQuery }));
50724
+ const validationQueriesWithoutWrappers = collectEvaluationQueries(queryOrAction, false);
50725
+ const queryEvaluations = new Array();
50726
+ for (const { index, validationQuery } of validationQueries) {
50727
+ let evaluationResponse = {
50728
+ status: sqlanvil.QueryEvaluation.QueryEvaluationStatus.SUCCESS
50729
+ };
50730
+ try {
50731
+ await this.execute(validationQuery.query);
50732
+ }
50733
+ catch (e) {
50734
+ evaluationResponse = {
50735
+ status: sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE,
50736
+ error: sqlanvil.QueryEvaluationError.create({
50737
+ message: (e === null || e === void 0 ? void 0 : e.message) ? String(e.message) : String(e)
50738
+ })
50739
+ };
50740
+ }
50741
+ queryEvaluations.push(sqlanvil.QueryEvaluation.create(Object.assign(Object.assign({}, evaluationResponse), { incremental: validationQuery.incremental, query: validationQueriesWithoutWrappers[index].query })));
50668
50742
  }
50743
+ return queryEvaluations;
50669
50744
  }
50670
- let currentFlagName = "";
50671
- for (const splitArg of splitArgv) {
50672
- if (splitArg.startsWith("--")) {
50673
- currentFlagName = splitArg.slice(2);
50674
- parsedArgv[currentFlagName] = "";
50675
- }
50676
- else if (currentFlagName) {
50677
- parsedArgv[currentFlagName] = splitArg;
50678
- currentFlagName = "";
50745
+ async tables(_database, schema) {
50746
+ const params = [];
50747
+ let schemaClause = "";
50748
+ if (schema) {
50749
+ schemaClause = "and table_schema = ?";
50750
+ params.push(schema);
50679
50751
  }
50752
+ const queryResult = await this.execute(`select table_name, table_schema
50753
+ from information_schema.tables
50754
+ where table_schema not in ('information_schema', 'mysql', 'performance_schema', 'sys')
50755
+ ${schemaClause}`, { params, rowLimit: 10000, includeQueryInError: true });
50756
+ const targets = queryResult.rows.map(row => ({
50757
+ schema: row.table_schema,
50758
+ name: row.table_name
50759
+ }));
50760
+ return await Promise.all(targets.map(target => this.table(target)));
50680
50761
  }
50681
- return parsedArgv;
50682
- }
50683
- class AbstractFlag {
50684
- constructor(name, defaultValue) {
50685
- this.name = name;
50686
- this.defaultValue = defaultValue;
50687
- this.parsed = {};
50762
+ async search(searchText, options = { limit: 1000 }) {
50763
+ const results = await this.execute(`select tables.table_schema as table_schema, tables.table_name as table_name
50764
+ from information_schema.tables as tables
50765
+ left join information_schema.columns columns
50766
+ on tables.table_schema = columns.table_schema
50767
+ and tables.table_name = columns.table_name
50768
+ where tables.table_schema like ?
50769
+ or tables.table_name like ?
50770
+ or columns.column_name like ?
50771
+ group by 1, 2`, {
50772
+ params: [`%${searchText}%`, `%${searchText}%`, `%${searchText}%`],
50773
+ rowLimit: options.limit
50774
+ });
50775
+ return await Promise.all(results.rows.map(row => this.table({
50776
+ schema: row.table_schema,
50777
+ name: row.table_name
50778
+ })));
50688
50779
  }
50689
- get() {
50690
- if (Flags.getRawFlagValue(this.name) === undefined) {
50691
- return this.defaultValue;
50780
+ async table(target) {
50781
+ var _a, _b, _c;
50782
+ const params = [target.schema, target.name];
50783
+ const [tableResults, columnResults] = await Promise.all([
50784
+ this.execute(`select table_type, table_comment from information_schema.tables
50785
+ where table_schema = ? and table_name = ?`, { params, includeQueryInError: true }),
50786
+ this.execute(`select column_name, data_type, ordinal_position, column_comment
50787
+ from information_schema.columns
50788
+ where table_schema = ? and table_name = ?
50789
+ order by ordinal_position`, { params, includeQueryInError: true })
50790
+ ]);
50791
+ if (tableResults.rows.length === 0) {
50792
+ return null;
50692
50793
  }
50693
- const stringValue = Flags.getRawFlagValue(this.name);
50694
- if (!(stringValue in this.parsed)) {
50695
- this.parsed[stringValue] = this.parse(stringValue);
50794
+ const tableType = String((_a = tableResults.rows[0].table_type) !== null && _a !== void 0 ? _a : tableResults.rows[0].TABLE_TYPE).toUpperCase();
50795
+ const tableComment = String((_c = (_b = tableResults.rows[0].table_comment) !== null && _b !== void 0 ? _b : tableResults.rows[0].TABLE_COMMENT) !== null && _c !== void 0 ? _c : "");
50796
+ return sqlanvil.TableMetadata.create({
50797
+ target,
50798
+ type: tableType === "VIEW" ? sqlanvil.TableMetadata.Type.VIEW : sqlanvil.TableMetadata.Type.TABLE,
50799
+ description: tableComment || undefined,
50800
+ fields: columnResults.rows.map(row => {
50801
+ var _a, _b, _c, _d;
50802
+ const comment = String((_b = (_a = row.column_comment) !== null && _a !== void 0 ? _a : row.COLUMN_COMMENT) !== null && _b !== void 0 ? _b : "");
50803
+ return sqlanvil.Field.create({
50804
+ name: ((_c = row.column_name) !== null && _c !== void 0 ? _c : row.COLUMN_NAME),
50805
+ primitive: convertFieldType(((_d = row.data_type) !== null && _d !== void 0 ? _d : row.DATA_TYPE)),
50806
+ description: comment || undefined
50807
+ });
50808
+ })
50809
+ });
50810
+ }
50811
+ async deleteTable(target) {
50812
+ const metadata = await this.table(target);
50813
+ if (!metadata) {
50814
+ return;
50696
50815
  }
50697
- return this.parsed[stringValue];
50816
+ const kind = metadata.type === sqlanvil.TableMetadata.Type.VIEW ? "view" : "table";
50817
+ await this.execute(`drop ${kind} if exists \`${target.schema}\`.\`${target.name}\``, {
50818
+ includeQueryInError: true
50819
+ });
50698
50820
  }
50699
- }
50700
- class SingleValueFlag extends AbstractFlag {
50701
- constructor(name, defaultValue, parser) {
50702
- super(name, defaultValue);
50703
- this.parser = parser;
50821
+ async schemas(_database) {
50822
+ const result = await this.execute(`select schema_name from information_schema.schemata`, {
50823
+ includeQueryInError: true
50824
+ });
50825
+ return result.rows
50826
+ .map(row => { var _a; return ((_a = row.schema_name) !== null && _a !== void 0 ? _a : row.SCHEMA_NAME); })
50827
+ .filter(name => !INTERNAL_SCHEMAS.has(name));
50704
50828
  }
50705
- parse(stringValue) {
50706
- return this.parser(stringValue);
50829
+ async createSchema(_database, schema) {
50830
+ await this.execute(`create database if not exists \`${schema}\``, {
50831
+ includeQueryInError: true
50832
+ });
50707
50833
  }
50708
- }
50709
- class SetFlag extends AbstractFlag {
50710
- constructor(name, defaultValue, parser) {
50711
- super(name, defaultValue);
50712
- this.parser = parser;
50834
+ async setMetadata(action) {
50835
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
50836
+ const { target, actionDescriptor } = action;
50837
+ const actualMetadata = await this.table(target);
50838
+ if (!actualMetadata) {
50839
+ return;
50840
+ }
50841
+ if (actualMetadata.type === sqlanvil.TableMetadata.Type.VIEW) {
50842
+ return;
50843
+ }
50844
+ const resolved = `\`${target.schema}\`.\`${target.name}\``;
50845
+ if (actionDescriptor === null || actionDescriptor === void 0 ? void 0 : actionDescriptor.description) {
50846
+ await this.execute(`alter table ${resolved} comment = '${escapeMysqlString(actionDescriptor.description)}'`, { includeQueryInError: true });
50847
+ }
50848
+ const columnComments = ((actionDescriptor === null || actionDescriptor === void 0 ? void 0 : actionDescriptor.columns) || []).filter(column => {
50849
+ var _a;
50850
+ return ((_a = column.path) === null || _a === void 0 ? void 0 : _a.length) === 1 &&
50851
+ column.description != null &&
50852
+ actualMetadata.fields.some(f => f.name === column.path[0]);
50853
+ });
50854
+ if (columnComments.length === 0) {
50855
+ return;
50856
+ }
50857
+ const defResult = await this.execute(`select column_name, column_type, is_nullable, column_default, extra,
50858
+ collation_name, generation_expression
50859
+ from information_schema.columns
50860
+ where table_schema = ? and table_name = ?`, { params: [target.schema, target.name], includeQueryInError: true });
50861
+ const defByName = new Map();
50862
+ defResult.rows.forEach(row => { var _a; return defByName.set(String((_a = row.column_name) !== null && _a !== void 0 ? _a : row.COLUMN_NAME), row); });
50863
+ for (const column of columnComments) {
50864
+ const name = column.path[0];
50865
+ const row = defByName.get(name);
50866
+ if (!row) {
50867
+ continue;
50868
+ }
50869
+ const def = reconstructColumnDef({
50870
+ columnType: String((_a = row.column_type) !== null && _a !== void 0 ? _a : row.COLUMN_TYPE),
50871
+ isNullable: String((_b = row.is_nullable) !== null && _b !== void 0 ? _b : row.IS_NULLABLE),
50872
+ columnDefault: ((_d = (_c = row.column_default) !== null && _c !== void 0 ? _c : row.COLUMN_DEFAULT) !== null && _d !== void 0 ? _d : null) === null
50873
+ ? null
50874
+ : String((_e = row.column_default) !== null && _e !== void 0 ? _e : row.COLUMN_DEFAULT),
50875
+ extra: String((_g = (_f = row.extra) !== null && _f !== void 0 ? _f : row.EXTRA) !== null && _g !== void 0 ? _g : ""),
50876
+ collationName: ((_j = (_h = row.collation_name) !== null && _h !== void 0 ? _h : row.COLLATION_NAME) !== null && _j !== void 0 ? _j : null) === null
50877
+ ? null
50878
+ : String((_k = row.collation_name) !== null && _k !== void 0 ? _k : row.COLLATION_NAME),
50879
+ generationExpression: ((_m = (_l = row.generation_expression) !== null && _l !== void 0 ? _l : row.GENERATION_EXPRESSION) !== null && _m !== void 0 ? _m : null) === null
50880
+ ? null
50881
+ : String((_o = row.generation_expression) !== null && _o !== void 0 ? _o : row.GENERATION_EXPRESSION)
50882
+ });
50883
+ await this.execute(`alter table ${resolved} modify column \`${name}\` ${def} comment '${escapeMysqlString(column.description)}'`, { includeQueryInError: true });
50884
+ }
50713
50885
  }
50714
- parse(stringValue) {
50715
- return new Set(stringValue
50716
- .split(",")
50717
- .filter(singleValue => singleValue.length > 0)
50718
- .map(this.parser));
50886
+ async close() {
50887
+ await this.queryExecutor.close();
50719
50888
  }
50720
50889
  }
50721
50890
 
50722
- const CANCEL_EVENT = "jobCancel";
50723
- const flags = {
50724
- runnerNotificationPeriodMillis: Flags.number("runner-notification-period-millis", 5000)
50725
- };
50726
- const isSuccessfulAction = (actionResult) => actionResult.status === sqlanvil.ActionResult.ExecutionStatus.SUCCESSFUL ||
50727
- actionResult.status === sqlanvil.ActionResult.ExecutionStatus.DISABLED;
50728
- function run(dbadapter, graph, executionOptions, partiallyExecutedRunResult = {}, runnerNotificationPeriodMillis = flags.runnerNotificationPeriodMillis.get()) {
50729
- return new Runner(dbadapter, graph, executionOptions, partiallyExecutedRunResult, runnerNotificationPeriodMillis).execute();
50730
- }
50731
- class Runner {
50732
- constructor(dbadapter, graph, executionOptions = {}, partiallyExecutedRunResult = {}, runnerNotificationPeriodMillis = flags.runnerNotificationPeriodMillis.get()) {
50733
- var _a;
50734
- this.dbadapter = dbadapter;
50735
- this.graph = graph;
50736
- this.executionOptions = executionOptions;
50737
- this.runnerNotificationPeriodMillis = runnerNotificationPeriodMillis;
50738
- this.changeListeners = [];
50739
- this.lastNotificationTimestampMillis = 0;
50740
- this.stopped = false;
50741
- this.cancelled = false;
50742
- this.timedOut = false;
50743
- this.allActionTargets = new Set(graph.actions.map(action => targetStringifier.stringify(action.target)));
50744
- this.runResult = Object.assign({ actions: [] }, partiallyExecutedRunResult);
50745
- this.warehouseStateByTarget = new Map();
50746
- (_a = graph.warehouseState.tables) === null || _a === void 0 ? void 0 : _a.forEach(tableMetadata => this.warehouseStateByTarget.set(targetStringifier.stringify(tableMetadata.target), tableMetadata));
50747
- this.executedActionTargets = new Set(this.runResult.actions
50748
- .filter(action => action.status !== sqlanvil.ActionResult.ExecutionStatus.RUNNING)
50749
- .map(action => targetStringifier.stringify(action.target)));
50750
- this.successfullyExecutedActionTargets = new Set(this.runResult.actions
50751
- .filter(isSuccessfulAction)
50752
- .map(action => targetStringifier.stringify(action.target)));
50753
- this.pendingActions = graph.actions.filter(action => !this.executedActionTargets.has(targetStringifier.stringify(action.target)));
50754
- this.eEmitter = new EventEmitter__default["default"]();
50755
- this.eEmitter.setMaxListeners(0);
50891
+ const DEFAULT_ROW_CAP = 1000000;
50892
+ const DEFAULT_BYTE_CAP = 512 * 1024 * 1024;
50893
+ const backquote = (id) => "`" + String(id).replace(/`/g, "``") + "`";
50894
+ async function runMysqlExtract(args) {
50895
+ var _a, _b;
50896
+ const { spec, target, connectionCredentials } = args;
50897
+ if (spec.platform !== "mysql") {
50898
+ throw new Error(`mysql runner-extract got a "${spec.platform}" source.`);
50756
50899
  }
50757
- onChange(listener) {
50758
- this.changeListeners.push(listener);
50759
- return this;
50900
+ const cols = Object.keys(spec.columnTypes || {});
50901
+ if (cols.length === 0) {
50902
+ throw new Error(`Extract "${target.schema}.${target.name}" has no columnTypes; run \`sqlanvil introspect\`.`);
50760
50903
  }
50761
- execute() {
50762
- var _a, _b, _c;
50763
- if (!!this.executionTask) {
50764
- throw new Error("Executor already started.");
50765
- }
50766
- this.executionTask = this.executeGraph();
50767
- if (!!this.graph.runConfig && !!this.graph.runConfig.timeoutMillis) {
50768
- const now = Date.now();
50769
- const runStartMillis = ((_c = (_b = (_a = this.runResult.timing) === null || _a === void 0 ? void 0 : _a.startTimeMillis) === null || _b === void 0 ? void 0 : _b.toNumber) === null || _c === void 0 ? void 0 : _c.call(_b)) || now;
50770
- const elapsedTimeMillis = now - runStartMillis;
50771
- const timeoutMillis = this.graph.runConfig.timeoutMillis - elapsedTimeMillis;
50772
- this.timeout = setTimeout(() => {
50773
- this.timedOut = true;
50774
- this.cancel();
50775
- }, timeoutMillis);
50776
- }
50777
- return this;
50904
+ if (!args.pg) {
50905
+ throw new Error("runner-extract needs the write-warehouse connection (warehouseConnection).");
50778
50906
  }
50779
- stop() {
50780
- this.stopped = true;
50907
+ const conn = (connectionCredentials === null || connectionCredentials === void 0 ? void 0 : connectionCredentials[spec.connectionName]) || {};
50908
+ const database = spec.database || conn.database;
50909
+ if (!database) {
50910
+ throw new Error(`Extract "${target.schema}.${target.name}": no source database — set \`database:\` on ` +
50911
+ `connection "${spec.connectionName}" (workflow_settings.yaml) or \`schema:\` on the declaration.`);
50781
50912
  }
50782
- cancel() {
50783
- this.cancelled = true;
50784
- this.eEmitter.emit(CANCEL_EVENT, undefined, undefined);
50913
+ if (!conn.host && !conn.user) {
50914
+ throw new Error(`No credentials for connection "${spec.connectionName}" — add it to .df-credentials.json's ` +
50915
+ `"connections" map ({ host, port, user, password }).`);
50785
50916
  }
50786
- async result() {
50917
+ const rowCap = (_a = args.rowCap) !== null && _a !== void 0 ? _a : DEFAULT_ROW_CAP;
50918
+ const byteCap = (_b = args.byteCap) !== null && _b !== void 0 ? _b : DEFAULT_BYTE_CAP;
50919
+ const my = await MySqlDbAdapter.create(sqlanvil.MysqlConnection.create({
50920
+ host: conn.host,
50921
+ port: conn.port || 3306,
50922
+ user: conn.user,
50923
+ password: conn.password,
50924
+ database,
50925
+ sslMode: conn.sslMode
50926
+ }), { disableSslForTestsOnly: args.disableSslForTestsOnly });
50927
+ let rows;
50928
+ try {
50929
+ const colList = cols.map(backquote).join(", ");
50930
+ const source = `${backquote(database)}.${backquote(spec.sourceName)}`;
50931
+ ({ rows } = await my.execute(`select ${colList} from ${source} limit ${rowCap + 1}`, {
50932
+ rowLimit: rowCap + 1,
50933
+ byteLimit: byteCap
50934
+ }));
50935
+ }
50936
+ finally {
50937
+ await my.close().catch(() => undefined);
50938
+ }
50939
+ if (rows.length > rowCap) {
50940
+ rows = rows.slice(0, rowCap);
50941
+ console.warn(`runner-extract: ${target.schema}.${target.name} truncated at ${rowCap} rows (source is larger).`);
50942
+ }
50943
+ await loadRowsIntoPostgres({
50944
+ pg: args.pg,
50945
+ target,
50946
+ columnTypes: spec.columnTypes,
50947
+ rows,
50948
+ disableSslForTestsOnly: args.disableSslForTestsOnly
50949
+ });
50950
+ return { rowCount: rows.length };
50951
+ }
50952
+
50953
+ function loadDuckdb() {
50954
+ try {
50955
+ return nativeRequire("@duckdb/node-api");
50956
+ }
50957
+ catch (e) {
50958
+ throw new Error(`This feature requires the optional "@duckdb/node-api" dependency, which failed to load: ` +
50959
+ `${e.message}`);
50960
+ }
50961
+ }
50962
+ async function runAsync(conn, sql) {
50963
+ await conn.run(sql);
50964
+ }
50965
+ async function allAsync(conn, sql) {
50966
+ const reader = await conn.runAndReadAll(sql);
50967
+ return reader.getRowObjects();
50968
+ }
50969
+ async function withDuckdb(fn, dbPath = ":memory:") {
50970
+ const { DuckDBInstance } = loadDuckdb();
50971
+ const instance = await DuckDBInstance.create(dbPath);
50972
+ const conn = await instance.connect();
50973
+ const done = () => {
50974
+ var _a, _b;
50787
50975
  try {
50788
- return await this.executionTask;
50976
+ (_a = conn.closeSync) === null || _a === void 0 ? void 0 : _a.call(conn);
50789
50977
  }
50790
- finally {
50791
- if (!!this.timeout) {
50792
- clearTimeout(this.timeout);
50793
- }
50978
+ catch (e) {
50794
50979
  }
50795
- }
50796
- notifyListeners() {
50797
- if (Date.now() - this.runnerNotificationPeriodMillis < this.lastNotificationTimestampMillis) {
50798
- return;
50980
+ try {
50981
+ (_b = instance.closeSync) === null || _b === void 0 ? void 0 : _b.call(instance);
50799
50982
  }
50800
- const runResultClone = deepClone(sqlanvil.RunResult, this.runResult);
50801
- this.lastNotificationTimestampMillis = Date.now();
50802
- this.changeListeners.forEach(listener => listener(runResultClone));
50803
- }
50804
- async executeGraph() {
50805
- const timer = Timer.start(this.runResult.timing);
50806
- this.runResult.status = sqlanvil.RunResult.ExecutionStatus.RUNNING;
50807
- this.runResult.timing = timer.current();
50808
- this.notifyListeners();
50809
- if (this.runResult.actions.length === 0) {
50810
- await this.prepareAllSchemas();
50983
+ catch (e) {
50811
50984
  }
50812
- await this.executeAllActionsReadyForExecution();
50813
- if (this.stopped) {
50814
- return this.runResult;
50985
+ };
50986
+ try {
50987
+ if (!process.env.HOME) {
50988
+ await runAsync(conn, `SET home_directory='${os__namespace.tmpdir()}'`);
50815
50989
  }
50816
- this.runResult.timing = timer.end();
50817
- this.runResult.status = sqlanvil.RunResult.ExecutionStatus.SUCCESSFUL;
50818
- if (this.timedOut) {
50819
- this.runResult.status = sqlanvil.RunResult.ExecutionStatus.TIMED_OUT;
50990
+ const result = await fn(conn);
50991
+ done();
50992
+ return result;
50993
+ }
50994
+ catch (e) {
50995
+ done();
50996
+ throw e;
50997
+ }
50998
+ }
50999
+
51000
+ const PG_ATTACH_ALIAS = "pg";
51001
+ const SECRET_NAME = "sa_export";
51002
+ function buildAttachSql(pg, opts = {}) {
51003
+ const readOnly = opts.readOnly !== false;
51004
+ const dsn = [
51005
+ pg.host && `host=${pg.host}`,
51006
+ pg.port && `port=${pg.port}`,
51007
+ pg.database && `dbname=${pg.database}`,
51008
+ pg.user && `user=${pg.user}`,
51009
+ pg.password && `password=${pg.password}`
51010
+ ]
51011
+ .filter(Boolean)
51012
+ .join(" ");
51013
+ const mode = readOnly ? ", READ_ONLY" : "";
51014
+ return `ATTACH '${dsn}' AS ${PG_ATTACH_ALIAS} (TYPE postgres${mode})`;
51015
+ }
51016
+ function buildSecretSql(scheme, creds) {
51017
+ if (scheme === "s3") {
51018
+ const fields = [
51019
+ "TYPE s3",
51020
+ creds.accessKeyId && `KEY_ID '${creds.accessKeyId}'`,
51021
+ creds.secretAccessKey && `SECRET '${creds.secretAccessKey}'`,
51022
+ creds.region && `REGION '${creds.region}'`,
51023
+ creds.endpoint && `ENDPOINT '${creds.endpoint}'`,
51024
+ creds.endpoint && `URL_STYLE 'path'`,
51025
+ creds.endpoint && `USE_SSL true`
51026
+ ]
51027
+ .filter(Boolean)
51028
+ .join(", ");
51029
+ return `CREATE OR REPLACE SECRET ${SECRET_NAME} (${fields})`;
51030
+ }
51031
+ if (scheme === "gcs") {
51032
+ const fields = [
51033
+ "TYPE gcs",
51034
+ creds.keyId && `KEY_ID '${creds.keyId}'`,
51035
+ creds.secret && `SECRET '${creds.secret}'`
51036
+ ]
51037
+ .filter(Boolean)
51038
+ .join(", ");
51039
+ return `CREATE OR REPLACE SECRET ${SECRET_NAME} (${fields})`;
51040
+ }
51041
+ return null;
51042
+ }
51043
+ function toCopyTarget(uri) {
51044
+ if (uri.startsWith("local://")) {
51045
+ return uri.slice("local://".length);
51046
+ }
51047
+ return uri;
51048
+ }
51049
+ function schemeOf(uri) {
51050
+ if (uri.startsWith("s3://")) {
51051
+ return "s3";
51052
+ }
51053
+ if (uri.startsWith("gs://") || uri.startsWith("gcs://")) {
51054
+ return "gcs";
51055
+ }
51056
+ return "local";
51057
+ }
51058
+ function buildCopySql(selectSql, uri, format, options = {}) {
51059
+ const target = toCopyTarget(uri);
51060
+ const fmt = (format || "").toLowerCase();
51061
+ const extraOptions = Object.entries(options || {}).map(([key, value]) => `${key.toUpperCase()} ${value}`);
51062
+ const optionList = [`FORMAT ${fmt}`, ...extraOptions].join(", ");
51063
+ return `COPY (SELECT * FROM postgres_query('${PG_ATTACH_ALIAS}', $sa$${selectSql}$sa$)) TO '${target}' (${optionList})`;
51064
+ }
51065
+ async function runDuckdbExport(args) {
51066
+ const { spec, selectSql, pg, storage, actionName } = args;
51067
+ const uri = resolveExportUri(spec, actionName, { wildcard: false });
51068
+ const scheme = schemeOf(uri);
51069
+ if (scheme !== "local" && !(storage === null || storage === void 0 ? void 0 : storage[scheme])) {
51070
+ throw new Error(`No "${scheme}" storage credentials found in .df-credentials.json (storage.${scheme}) for ` +
51071
+ `export to ${uri}.`);
51072
+ }
51073
+ return withDuckdb(async (conn) => {
51074
+ await runAsync(conn, "INSTALL postgres; LOAD postgres; INSTALL httpfs; LOAD httpfs;");
51075
+ await runAsync(conn, buildAttachSql(pg));
51076
+ if (scheme !== "local") {
51077
+ const secret = buildSecretSql(scheme, storage[scheme]);
51078
+ if (secret) {
51079
+ await runAsync(conn, secret);
51080
+ }
50820
51081
  }
50821
- else if (this.cancelled) {
50822
- this.runResult.status = sqlanvil.RunResult.ExecutionStatus.CANCELLED;
51082
+ await runAsync(conn, buildCopySql(selectSql, uri, spec.format, spec.options || {}));
51083
+ return { destination: toCopyTarget(uri) };
51084
+ });
51085
+ }
51086
+
51087
+ function readerForFormat(format) {
51088
+ switch ((format || "").toLowerCase()) {
51089
+ case "csv":
51090
+ return "read_csv_auto";
51091
+ case "json":
51092
+ return "read_json_auto";
51093
+ case "parquet":
51094
+ default:
51095
+ return "read_parquet";
51096
+ }
51097
+ }
51098
+ function importTargetSql(target) {
51099
+ return target.schema
51100
+ ? `${PG_ATTACH_ALIAS}."${target.schema}"."${target.name}"`
51101
+ : `${PG_ATTACH_ALIAS}."${target.name}"`;
51102
+ }
51103
+ function buildImportSql(target, location, format, overwrite) {
51104
+ const reader = readerForFormat(format);
51105
+ const source = toCopyTarget(location);
51106
+ const dest = importTargetSql(target);
51107
+ const select = `SELECT * FROM ${reader}('${source}')`;
51108
+ if (overwrite) {
51109
+ return [`DROP TABLE IF EXISTS ${dest}`, `CREATE TABLE ${dest} AS ${select}`];
51110
+ }
51111
+ return [`INSERT INTO ${dest} ${select}`];
51112
+ }
51113
+ async function runDuckdbImport(args) {
51114
+ const { spec, target, pg, storage } = args;
51115
+ const location = spec.location || "";
51116
+ const scheme = schemeOf(location);
51117
+ if (scheme !== "local" && !(storage === null || storage === void 0 ? void 0 : storage[scheme])) {
51118
+ throw new Error(`No "${scheme}" storage credentials found in .df-credentials.json (storage.${scheme}) for ` +
51119
+ `import from ${location}.`);
51120
+ }
51121
+ return withDuckdb(async (conn) => {
51122
+ await runAsync(conn, "INSTALL postgres; LOAD postgres; INSTALL httpfs; LOAD httpfs;");
51123
+ await runAsync(conn, buildAttachSql(pg, { readOnly: false }));
51124
+ if (scheme !== "local") {
51125
+ const secret = buildSecretSql(scheme, storage[scheme]);
51126
+ if (secret) {
51127
+ await runAsync(conn, secret);
51128
+ }
50823
51129
  }
50824
- else if (this.runResult.actions.some(action => action.status === sqlanvil.ActionResult.ExecutionStatus.FAILED)) {
50825
- this.runResult.status = sqlanvil.RunResult.ExecutionStatus.FAILED;
51130
+ for (const statement of buildImportSql(target, location, spec.format, spec.overwrite)) {
51131
+ await runAsync(conn, statement);
50826
51132
  }
50827
- return this.runResult;
50828
- }
50829
- async prepareAllSchemas() {
50830
- const databaseSchemas = new Map();
50831
- this.graph.actions
50832
- .filter(action => !!action.target && !!action.target.schema)
50833
- .forEach(({ target }) => {
50834
- const trueDatabase = target.database || this.graph.projectConfig.defaultDatabase;
50835
- if (!databaseSchemas.has(trueDatabase)) {
50836
- databaseSchemas.set(trueDatabase, new Set());
51133
+ return { source: toCopyTarget(location) };
51134
+ });
51135
+ }
51136
+
51137
+ class Flags {
51138
+ static boolean(name, defaultValue = false) {
51139
+ return new SingleValueFlag(name, defaultValue, (stringValue) => {
51140
+ if (stringValue === "" || stringValue === "true") {
51141
+ return true;
50837
51142
  }
50838
- databaseSchemas.get(trueDatabase).add(target.schema);
51143
+ if (stringValue === "false") {
51144
+ return false;
51145
+ }
51146
+ throw Flags.invalidFlagValueError(name);
50839
51147
  });
50840
- await Promise.all(Array.from(databaseSchemas.entries()).map(async ([database, schemas]) => {
50841
- const existingSchemas = new Set(await this.dbadapter.schemas(database));
50842
- await Promise.all(Array.from(schemas)
50843
- .filter(schema => !existingSchemas.has(schema))
50844
- .map(schema => this.dbadapter.createSchema(database, schema)));
50845
- }));
50846
51148
  }
50847
- async executeAllActionsReadyForExecution() {
50848
- if (this.stopped) {
50849
- return;
51149
+ static string(name, defaultValue = "") {
51150
+ return new SingleValueFlag(name, defaultValue, (stringValue) => stringValue);
51151
+ }
51152
+ static number(name, defaultValue = 0) {
51153
+ return new SingleValueFlag(name, defaultValue, (stringValue) => parseFloat(stringValue));
51154
+ }
51155
+ static long(name, defaultValue = Long__default["default"].ZERO) {
51156
+ return new SingleValueFlag(name, defaultValue, Long__default["default"].fromString);
51157
+ }
51158
+ static stringSet(name, defaultValue = new Set()) {
51159
+ return new SetFlag(name, defaultValue, (singleValue) => singleValue);
51160
+ }
51161
+ static getRawFlagValue(flagName) {
51162
+ return Flags.parsedArgv[flagName];
51163
+ }
51164
+ static setRawFlagValueForTesting(flagName, value) {
51165
+ Flags.parsedArgv[flagName] = value;
51166
+ }
51167
+ static invalidFlagValueError(flagName) {
51168
+ return new Error(`Invalid flag value: ${Flags.getRawFlagValue(flagName)} [${flagName}]`);
51169
+ }
51170
+ }
51171
+ Flags.parsedArgv = parseArgv(process.argv);
51172
+ function parseArgv(argv) {
51173
+ const parsedArgv = {};
51174
+ const splitArgv = [];
51175
+ for (let arg of argv) {
51176
+ if (arg.startsWith("--no-")) {
51177
+ arg = `--${arg.slice(5)}=false`;
50850
51178
  }
50851
- if (this.cancelled) {
50852
- const allPendingActions = this.pendingActions;
50853
- this.pendingActions = [];
50854
- allPendingActions.forEach(pendingAction => this.runResult.actions.push({
50855
- target: pendingAction.target,
50856
- status: sqlanvil.ActionResult.ExecutionStatus.SKIPPED,
50857
- tasks: pendingAction.tasks.map(() => ({
50858
- status: sqlanvil.TaskResult.ExecutionStatus.SKIPPED
50859
- }))
50860
- }));
50861
- this.notifyListeners();
50862
- return;
51179
+ if (arg.startsWith("--") && arg.includes("=")) {
51180
+ splitArgv.push(arg.slice(0, arg.indexOf("=")));
51181
+ splitArgv.push(arg.slice(arg.indexOf("=") + 1));
50863
51182
  }
50864
- const executableActions = [];
50865
- const skippableActions = [];
50866
- const stillPendingActions = [];
50867
- for (const pendingAction of this.pendingActions) {
50868
- if (pendingAction.dependencyTargets.every(dependency => !this.allActionTargets.has(targetStringifier.stringify(dependency)) ||
50869
- this.successfullyExecutedActionTargets.has(targetStringifier.stringify(dependency)))) {
51183
+ else {
51184
+ splitArgv.push(arg);
51185
+ }
51186
+ }
51187
+ let currentFlagName = "";
51188
+ for (const splitArg of splitArgv) {
51189
+ if (splitArg.startsWith("--")) {
51190
+ currentFlagName = splitArg.slice(2);
51191
+ parsedArgv[currentFlagName] = "";
51192
+ }
51193
+ else if (currentFlagName) {
51194
+ parsedArgv[currentFlagName] = splitArg;
51195
+ currentFlagName = "";
51196
+ }
51197
+ }
51198
+ return parsedArgv;
51199
+ }
51200
+ class AbstractFlag {
51201
+ constructor(name, defaultValue) {
51202
+ this.name = name;
51203
+ this.defaultValue = defaultValue;
51204
+ this.parsed = {};
51205
+ }
51206
+ get() {
51207
+ if (Flags.getRawFlagValue(this.name) === undefined) {
51208
+ return this.defaultValue;
51209
+ }
51210
+ const stringValue = Flags.getRawFlagValue(this.name);
51211
+ if (!(stringValue in this.parsed)) {
51212
+ this.parsed[stringValue] = this.parse(stringValue);
51213
+ }
51214
+ return this.parsed[stringValue];
51215
+ }
51216
+ }
51217
+ class SingleValueFlag extends AbstractFlag {
51218
+ constructor(name, defaultValue, parser) {
51219
+ super(name, defaultValue);
51220
+ this.parser = parser;
51221
+ }
51222
+ parse(stringValue) {
51223
+ return this.parser(stringValue);
51224
+ }
51225
+ }
51226
+ class SetFlag extends AbstractFlag {
51227
+ constructor(name, defaultValue, parser) {
51228
+ super(name, defaultValue);
51229
+ this.parser = parser;
51230
+ }
51231
+ parse(stringValue) {
51232
+ return new Set(stringValue
51233
+ .split(",")
51234
+ .filter(singleValue => singleValue.length > 0)
51235
+ .map(this.parser));
51236
+ }
51237
+ }
51238
+
51239
+ const CANCEL_EVENT = "jobCancel";
51240
+ const flags = {
51241
+ runnerNotificationPeriodMillis: Flags.number("runner-notification-period-millis", 5000)
51242
+ };
51243
+ const isSuccessfulAction = (actionResult) => actionResult.status === sqlanvil.ActionResult.ExecutionStatus.SUCCESSFUL ||
51244
+ actionResult.status === sqlanvil.ActionResult.ExecutionStatus.DISABLED;
51245
+ function run(dbadapter, graph, executionOptions, partiallyExecutedRunResult = {}, runnerNotificationPeriodMillis = flags.runnerNotificationPeriodMillis.get()) {
51246
+ return new Runner(dbadapter, graph, executionOptions, partiallyExecutedRunResult, runnerNotificationPeriodMillis).execute();
51247
+ }
51248
+ class Runner {
51249
+ constructor(dbadapter, graph, executionOptions = {}, partiallyExecutedRunResult = {}, runnerNotificationPeriodMillis = flags.runnerNotificationPeriodMillis.get()) {
51250
+ var _a;
51251
+ this.dbadapter = dbadapter;
51252
+ this.graph = graph;
51253
+ this.executionOptions = executionOptions;
51254
+ this.runnerNotificationPeriodMillis = runnerNotificationPeriodMillis;
51255
+ this.changeListeners = [];
51256
+ this.lastNotificationTimestampMillis = 0;
51257
+ this.stopped = false;
51258
+ this.cancelled = false;
51259
+ this.timedOut = false;
51260
+ this.allActionTargets = new Set(graph.actions.map(action => targetStringifier.stringify(action.target)));
51261
+ this.runResult = Object.assign({ actions: [] }, partiallyExecutedRunResult);
51262
+ this.warehouseStateByTarget = new Map();
51263
+ (_a = graph.warehouseState.tables) === null || _a === void 0 ? void 0 : _a.forEach(tableMetadata => this.warehouseStateByTarget.set(targetStringifier.stringify(tableMetadata.target), tableMetadata));
51264
+ this.executedActionTargets = new Set(this.runResult.actions
51265
+ .filter(action => action.status !== sqlanvil.ActionResult.ExecutionStatus.RUNNING)
51266
+ .map(action => targetStringifier.stringify(action.target)));
51267
+ this.successfullyExecutedActionTargets = new Set(this.runResult.actions
51268
+ .filter(isSuccessfulAction)
51269
+ .map(action => targetStringifier.stringify(action.target)));
51270
+ this.pendingActions = graph.actions.filter(action => !this.executedActionTargets.has(targetStringifier.stringify(action.target)));
51271
+ this.eEmitter = new EventEmitter__default["default"]();
51272
+ this.eEmitter.setMaxListeners(0);
51273
+ }
51274
+ onChange(listener) {
51275
+ this.changeListeners.push(listener);
51276
+ return this;
51277
+ }
51278
+ execute() {
51279
+ var _a, _b, _c;
51280
+ if (!!this.executionTask) {
51281
+ throw new Error("Executor already started.");
51282
+ }
51283
+ this.executionTask = this.executeGraph();
51284
+ if (!!this.graph.runConfig && !!this.graph.runConfig.timeoutMillis) {
51285
+ const now = Date.now();
51286
+ const runStartMillis = ((_c = (_b = (_a = this.runResult.timing) === null || _a === void 0 ? void 0 : _a.startTimeMillis) === null || _b === void 0 ? void 0 : _b.toNumber) === null || _c === void 0 ? void 0 : _c.call(_b)) || now;
51287
+ const elapsedTimeMillis = now - runStartMillis;
51288
+ const timeoutMillis = this.graph.runConfig.timeoutMillis - elapsedTimeMillis;
51289
+ this.timeout = setTimeout(() => {
51290
+ this.timedOut = true;
51291
+ this.cancel();
51292
+ }, timeoutMillis);
51293
+ }
51294
+ return this;
51295
+ }
51296
+ stop() {
51297
+ this.stopped = true;
51298
+ }
51299
+ cancel() {
51300
+ this.cancelled = true;
51301
+ this.eEmitter.emit(CANCEL_EVENT, undefined, undefined);
51302
+ }
51303
+ async result() {
51304
+ try {
51305
+ return await this.executionTask;
51306
+ }
51307
+ finally {
51308
+ if (!!this.timeout) {
51309
+ clearTimeout(this.timeout);
51310
+ }
51311
+ }
51312
+ }
51313
+ notifyListeners() {
51314
+ if (Date.now() - this.runnerNotificationPeriodMillis < this.lastNotificationTimestampMillis) {
51315
+ return;
51316
+ }
51317
+ const runResultClone = deepClone(sqlanvil.RunResult, this.runResult);
51318
+ this.lastNotificationTimestampMillis = Date.now();
51319
+ this.changeListeners.forEach(listener => listener(runResultClone));
51320
+ }
51321
+ async executeGraph() {
51322
+ const timer = Timer.start(this.runResult.timing);
51323
+ this.runResult.status = sqlanvil.RunResult.ExecutionStatus.RUNNING;
51324
+ this.runResult.timing = timer.current();
51325
+ this.notifyListeners();
51326
+ if (this.runResult.actions.length === 0) {
51327
+ await this.prepareAllSchemas();
51328
+ }
51329
+ await this.executeAllActionsReadyForExecution();
51330
+ if (this.stopped) {
51331
+ return this.runResult;
51332
+ }
51333
+ this.runResult.timing = timer.end();
51334
+ this.runResult.status = sqlanvil.RunResult.ExecutionStatus.SUCCESSFUL;
51335
+ if (this.timedOut) {
51336
+ this.runResult.status = sqlanvil.RunResult.ExecutionStatus.TIMED_OUT;
51337
+ }
51338
+ else if (this.cancelled) {
51339
+ this.runResult.status = sqlanvil.RunResult.ExecutionStatus.CANCELLED;
51340
+ }
51341
+ else if (this.runResult.actions.some(action => action.status === sqlanvil.ActionResult.ExecutionStatus.FAILED)) {
51342
+ this.runResult.status = sqlanvil.RunResult.ExecutionStatus.FAILED;
51343
+ }
51344
+ return this.runResult;
51345
+ }
51346
+ async prepareAllSchemas() {
51347
+ const databaseSchemas = new Map();
51348
+ this.graph.actions
51349
+ .filter(action => !!action.target && !!action.target.schema)
51350
+ .forEach(({ target }) => {
51351
+ const trueDatabase = target.database || this.graph.projectConfig.defaultDatabase;
51352
+ if (!databaseSchemas.has(trueDatabase)) {
51353
+ databaseSchemas.set(trueDatabase, new Set());
51354
+ }
51355
+ databaseSchemas.get(trueDatabase).add(target.schema);
51356
+ });
51357
+ await Promise.all(Array.from(databaseSchemas.entries()).map(async ([database, schemas]) => {
51358
+ const existingSchemas = new Set(await this.dbadapter.schemas(database));
51359
+ await Promise.all(Array.from(schemas)
51360
+ .filter(schema => !existingSchemas.has(schema))
51361
+ .map(schema => this.dbadapter.createSchema(database, schema)));
51362
+ }));
51363
+ }
51364
+ async executeAllActionsReadyForExecution() {
51365
+ if (this.stopped) {
51366
+ return;
51367
+ }
51368
+ if (this.cancelled) {
51369
+ const allPendingActions = this.pendingActions;
51370
+ this.pendingActions = [];
51371
+ allPendingActions.forEach(pendingAction => this.runResult.actions.push({
51372
+ target: pendingAction.target,
51373
+ status: sqlanvil.ActionResult.ExecutionStatus.SKIPPED,
51374
+ tasks: pendingAction.tasks.map(() => ({
51375
+ status: sqlanvil.TaskResult.ExecutionStatus.SKIPPED
51376
+ }))
51377
+ }));
51378
+ this.notifyListeners();
51379
+ return;
51380
+ }
51381
+ const executableActions = [];
51382
+ const skippableActions = [];
51383
+ const stillPendingActions = [];
51384
+ for (const pendingAction of this.pendingActions) {
51385
+ if (pendingAction.dependencyTargets.every(dependency => !this.allActionTargets.has(targetStringifier.stringify(dependency)) ||
51386
+ this.successfullyExecutedActionTargets.has(targetStringifier.stringify(dependency)))) {
50870
51387
  executableActions.push(pendingAction);
50871
51388
  }
50872
51389
  else if (pendingAction.dependencyTargets.every(dependency => !this.allActionTargets.has(targetStringifier.stringify(dependency)) ||
@@ -50987,7 +51504,7 @@ class Runner {
50987
51504
  return actionResult;
50988
51505
  }
50989
51506
  async executeTask(client, task, parentAction, options, action) {
50990
- var _a, _b, _c, _d, _e;
51507
+ var _a, _b, _c, _d, _e, _f;
50991
51508
  const timer = Timer.start();
50992
51509
  const taskResult = {
50993
51510
  status: sqlanvil.TaskResult.ExecutionStatus.RUNNING,
@@ -51038,7 +51555,9 @@ class Runner {
51038
51555
  }
51039
51556
  else if (task.type === "extract") {
51040
51557
  try {
51041
- const extractor = this.executionOptions.bigQueryExtract || runBigQueryExtract;
51558
+ const extractor = ((_c = action === null || action === void 0 ? void 0 : action.extract) === null || _c === void 0 ? void 0 : _c.platform) === "mysql"
51559
+ ? this.executionOptions.mysqlExtract || runMysqlExtract
51560
+ : this.executionOptions.bigQueryExtract || runBigQueryExtract;
51042
51561
  await extractor({
51043
51562
  spec: action === null || action === void 0 ? void 0 : action.extract,
51044
51563
  target: action === null || action === void 0 ? void 0 : action.target,
@@ -51066,7 +51585,7 @@ class Runner {
51066
51585
  }), task.type === "operation" ? 1 : options.bigquery.actionRetryLimit + 1 || 1);
51067
51586
  taskResult.metadata = metadata;
51068
51587
  if (task.type === "assertion") {
51069
- const rowCount = (_c = rows[0]) === null || _c === void 0 ? void 0 : _c[Object.keys(rows[0])[0]];
51588
+ const rowCount = (_d = rows[0]) === null || _d === void 0 ? void 0 : _d[Object.keys(rows[0])[0]];
51070
51589
  if (rowCount > 0) {
51071
51590
  throw new Error(`Assertion failed: query returned ${rowCount} row(s).`);
51072
51591
  }
@@ -51078,1066 +51597,711 @@ class Runner {
51078
51597
  ? sqlanvil.TaskResult.ExecutionStatus.CANCELLED
51079
51598
  : sqlanvil.TaskResult.ExecutionStatus.FAILED;
51080
51599
  taskResult.errorMessage = `${this.graph.projectConfig.warehouse} error: ${e.message}`;
51081
- if ((_e = (_d = e.metadata) === null || _d === void 0 ? void 0 : _d.bigquery) === null || _e === void 0 ? void 0 : _e.jobId) {
51600
+ if ((_f = (_e = e.metadata) === null || _e === void 0 ? void 0 : _e.bigquery) === null || _f === void 0 ? void 0 : _f.jobId) {
51082
51601
  taskResult.metadata = {
51083
51602
  bigquery: {
51084
51603
  jobId: e.metadata.bigquery.jobId
51085
51604
  }
51086
- };
51087
- }
51088
- }
51089
- }
51090
- taskResult.timing = timer.end();
51091
- this.notifyListeners();
51092
- return taskResult.status;
51093
- }
51094
- }
51095
- class Timer {
51096
- constructor(startTimeMillis) {
51097
- this.startTimeMillis = startTimeMillis;
51098
- }
51099
- static start(existingTiming) {
51100
- return new Timer((existingTiming === null || existingTiming === void 0 ? void 0 : existingTiming.startTimeMillis.toNumber()) || new Date().valueOf());
51101
- }
51102
- current() {
51103
- return {
51104
- startTimeMillis: Long__default["default"].fromNumber(this.startTimeMillis)
51105
- };
51106
- }
51107
- end() {
51108
- return {
51109
- startTimeMillis: Long__default["default"].fromNumber(this.startTimeMillis),
51110
- endTimeMillis: Long__default["default"].fromNumber(new Date().valueOf())
51111
- };
51112
- }
51113
- }
51114
-
51115
- async function test(dbadapter, tests) {
51116
- return await Promise.all(tests.map(testCase => runTest(dbadapter, testCase)));
51117
- }
51118
- async function runTest(dbadapter, testCase) {
51119
- const noLimit = {};
51120
- let actualResults;
51121
- let expectedResults;
51122
- try {
51123
- [actualResults, expectedResults] = await Promise.all([
51124
- dbadapter.execute(testCase.testQuery, noLimit),
51125
- dbadapter.execute(testCase.expectedOutputQuery, noLimit)
51126
- ]);
51127
- }
51128
- catch (e) {
51129
- return {
51130
- name: testCase.name,
51131
- successful: false,
51132
- messages: [`Error thrown: ${e.message}.`]
51133
- };
51134
- }
51135
- if (actualResults.rows.length !== expectedResults.rows.length) {
51136
- return {
51137
- name: testCase.name,
51138
- successful: false,
51139
- messages: [
51140
- `Expected ${expectedResults.rows.length} rows, but saw ${actualResults.rows.length} rows.`
51141
- ]
51142
- };
51143
- }
51144
- if (actualResults.rows.length === 0) {
51145
- return {
51146
- name: testCase.name,
51147
- successful: true
51148
- };
51149
- }
51150
- const actualColumns = Object.keys(actualResults.rows[0]);
51151
- const expectedColumns = Object.keys(expectedResults.rows[0]);
51152
- if (actualColumns.length !== expectedColumns.length) {
51153
- return {
51154
- name: testCase.name,
51155
- successful: false,
51156
- messages: [`Expected columns "${expectedColumns}", but saw "${actualColumns}".`]
51157
- };
51158
- }
51159
- for (const expectedColumn of expectedColumns) {
51160
- if (!actualColumns.some(actualColumn => normalizeColumnName(actualColumn) === normalizeColumnName(expectedColumn))) {
51161
- return {
51162
- name: testCase.name,
51163
- successful: false,
51164
- messages: [`Expected columns "${expectedColumns}", but saw "${actualColumns}".`]
51165
- };
51166
- }
51167
- }
51168
- const rowMessages = [];
51169
- for (let i = 0; i < actualResults.rows.length; i++) {
51170
- const actualResultRow = normalizeRow(actualResults.rows[i]);
51171
- const expectedResultRow = normalizeRow(expectedResults.rows[i]);
51172
- for (const column of actualColumns) {
51173
- const normalizedColumn = normalizeColumnName(column);
51174
- const expectedValue = expectedResultRow[normalizedColumn];
51175
- const actualValue = actualResultRow[normalizedColumn];
51176
- if (expectedValue === null && actualValue !== null) {
51177
- rowMessages.push(`For row ${i} and column "${column}": expected null, but saw "${actualValue}".`);
51178
- break;
51179
- }
51180
- if (expectedValue !== null && actualValue === null) {
51181
- rowMessages.push(`For row ${i} and column "${column}": expected "${expectedValue}", but saw null.`);
51182
- break;
51183
- }
51184
- if (typeof expectedValue !== typeof actualValue) {
51185
- rowMessages.push(`For row ${i} and column "${column}": expected type "${typeof expectedValue}", but saw type "${typeof actualValue}".`);
51186
- break;
51187
- }
51188
- const comparableExpectedValue = typeof expectedValue === "object" ? JSON.stringify(expectedValue) : expectedValue;
51189
- const comparableActualValue = typeof actualValue === "object" ? JSON.stringify(actualValue) : actualValue;
51190
- if (comparableExpectedValue !== comparableActualValue) {
51191
- rowMessages.push(`For row ${i} and column "${column}": expected "${comparableExpectedValue}", but saw "${comparableActualValue}".`);
51192
- }
51193
- }
51194
- }
51195
- if (rowMessages.length > 0) {
51196
- return {
51197
- name: testCase.name,
51198
- successful: false,
51199
- messages: rowMessages
51200
- };
51201
- }
51202
- return {
51203
- name: testCase.name,
51204
- successful: true
51205
- };
51206
- }
51207
- function normalizeColumnName(name) {
51208
- return name.toUpperCase();
51209
- }
51210
- function normalizeRow(row) {
51211
- const newRow = {};
51212
- Object.keys(row).forEach(colName => {
51213
- newRow[normalizeColumnName(colName)] = row[colName];
51214
- });
51215
- return newRow;
51216
- }
51217
-
51218
- function resolveEnvironment(projectDir, envName) {
51219
- const settings = readConfigFromWorkflowSettings(projectDir);
51220
- const environments = (settings === null || settings === void 0 ? void 0 : settings.environments) || {};
51221
- const env = environments[envName];
51222
- if (!env) {
51223
- const available = Object.keys(environments);
51224
- throw new Error(`Environment "${envName}" not found. ` +
51225
- (available.length
51226
- ? `Available environments: ${available.join(", ")}.`
51227
- : `No environments defined in workflow_settings.yaml.`));
51228
- }
51229
- const configOverride = {};
51230
- if (env.schemaSuffix) {
51231
- configOverride.schemaSuffix = env.schemaSuffix;
51232
- }
51233
- if (env.vars && Object.keys(env.vars).length > 0) {
51234
- configOverride.vars = env.vars;
51235
- }
51236
- if (env.defaultDatabase) {
51237
- configOverride.defaultDatabase = env.defaultDatabase;
51238
- }
51239
- if (env.defaultLocation) {
51240
- configOverride.defaultLocation = env.defaultLocation;
51241
- }
51242
- return { configOverride, credentials: env.credentials || undefined };
51243
- }
51244
- function mergeProjectConfigOverride(envOverride, cliOverride) {
51245
- const merged = Object.assign(Object.assign({}, envOverride), cliOverride);
51246
- if (envOverride.vars || cliOverride.vars) {
51247
- merged.vars = Object.assign(Object.assign({}, envOverride.vars), cliOverride.vars);
51248
- }
51249
- return merged;
51250
- }
51251
- function resolveCredentials(envCredentials, cliCredentials, defaultFilename) {
51252
- if (cliCredentials && cliCredentials !== defaultFilename) {
51253
- return cliCredentials;
51254
- }
51255
- return envCredentials || defaultFilename;
51256
- }
51257
-
51258
- function key(target) {
51259
- return targetStringifier.stringify(target);
51260
- }
51261
- function enumName(enumObject, value) {
51262
- if (value === undefined || value === null) {
51263
- return "UNKNOWN";
51264
- }
51265
- const match = Object.keys(enumObject).find(k => enumObject[k] === value);
51266
- return match || String(value);
51267
- }
51268
- function tableType$1(enumType) {
51269
- switch (enumType) {
51270
- case sqlanvil.TableType.VIEW:
51271
- return "view";
51272
- case sqlanvil.TableType.INCREMENTAL:
51273
- return "incremental";
51274
- default:
51275
- return "table";
51276
- }
51277
- }
51278
- function toMillis(value) {
51279
- if (value === undefined || value === null) {
51280
- return 0;
51281
- }
51282
- if (typeof value === "number") {
51283
- return value;
51284
- }
51285
- if (typeof value.toNumber === "function") {
51286
- return value.toNumber();
51287
- }
51288
- return Number(value) || 0;
51289
- }
51290
- function actionRow(action, type) {
51291
- const descriptor = action.actionDescriptor || {};
51292
- return {
51293
- target_key: key(action.target),
51294
- database: action.target.database || "",
51295
- schema: action.target.schema || "",
51296
- name: action.target.name || "",
51297
- readable_name: targetAsReadableString(action.target),
51298
- type,
51299
- tags: JSON.stringify(action.tags || []),
51300
- disabled: !!action.disabled,
51301
- file_name: action.fileName || "",
51302
- description: descriptor.description || ""
51303
- };
51304
- }
51305
- function pushDeps(action, out) {
51306
- for (const dep of action.dependencyTargets || []) {
51307
- out.push({
51308
- from_target_key: key(action.target),
51309
- to_target_key: key(dep),
51310
- from_readable: targetAsReadableString(action.target),
51311
- to_readable: targetAsReadableString(dep)
51312
- });
51313
- }
51314
- }
51315
- function pushColumns(action, out) {
51316
- const columns = (action.actionDescriptor && action.actionDescriptor.columns) || [];
51317
- for (const column of columns) {
51318
- out.push({
51319
- target_key: key(action.target),
51320
- readable_name: targetAsReadableString(action.target),
51321
- column_name: (column.path || []).join("."),
51322
- description: column.description || ""
51323
- });
51324
- }
51325
- }
51326
- function catalogRows(compiledGraph) {
51327
- const actions = [];
51328
- const dependencies = [];
51329
- const columns = [];
51330
- const add = (action, type) => {
51331
- actions.push(actionRow(action, type));
51332
- pushDeps(action, dependencies);
51333
- pushColumns(action, columns);
51334
- };
51335
- for (const table of compiledGraph.tables || []) {
51336
- add(table, tableType$1(table.enumType));
51337
- }
51338
- for (const operation of compiledGraph.operations || []) {
51339
- add(operation, "operation");
51340
- }
51341
- for (const assertion of compiledGraph.assertions || []) {
51342
- add(assertion, "assertion");
51343
- }
51344
- for (const exp of compiledGraph.exports || []) {
51345
- add(exp, "export");
51346
- }
51347
- for (const imp of compiledGraph.imports || []) {
51348
- add(imp, "import");
51349
- }
51350
- for (const declaration of compiledGraph.declarations || []) {
51351
- add(declaration, "declaration");
51352
- }
51353
- return { actions, dependencies, columns };
51354
- }
51355
- function runRows(runResult, runId) {
51356
- const runStatus = enumName(sqlanvil.RunResult.ExecutionStatus, runResult.status);
51357
- return (runResult.actions || []).map(action => {
51358
- const start = toMillis(action.timing && action.timing.startTimeMillis);
51359
- const end = toMillis(action.timing && action.timing.endTimeMillis);
51360
- const failedTask = (action.tasks || []).find(t => !!t.errorMessage);
51361
- return {
51362
- run_id: runId,
51363
- run_status: runStatus,
51364
- target_key: key(action.target),
51365
- readable_name: targetAsReadableString(action.target),
51366
- status: enumName(sqlanvil.ActionResult.ExecutionStatus, action.status),
51367
- start_millis: start,
51368
- end_millis: end,
51369
- duration_millis: start && end ? end - start : 0,
51370
- error_message: (failedTask && failedTask.errorMessage) || ""
51371
- };
51372
- });
51373
- }
51374
-
51375
- let tmpCounter = 0;
51376
- async function writeParquet(rows, outPath, columns) {
51377
- await fs__namespace.ensureDir(path__namespace.dirname(outPath));
51378
- const tmp = path__namespace.join(os__namespace.tmpdir(), `sa_artifact_${process.pid}_${Date.now()}_${tmpCounter++}.json`);
51379
- const payload = rows.length > 0 ? rows : [columns.reduce((o, c) => ((o[c] = null), o), {})];
51380
- const whereFalse = rows.length > 0 ? "" : " WHERE false";
51381
- await fs__namespace.writeFile(tmp, JSON.stringify(payload));
51382
- try {
51383
- await withDuckdb(async (conn) => {
51384
- await runAsync(conn, `COPY (SELECT * FROM read_json_auto('${tmp}')${whereFalse}) TO '${outPath}' (FORMAT parquet)`);
51385
- });
51386
- }
51387
- finally {
51388
- await fs__namespace.remove(tmp).catch(() => undefined);
51389
- }
51390
- }
51391
- async function queryParquet(sql, views) {
51392
- return withDuckdb(async (conn) => {
51393
- for (const view of views) {
51394
- await runAsync(conn, `create view ${view.name} as select * from read_parquet('${view.glob}')`);
51605
+ };
51606
+ }
51607
+ }
51395
51608
  }
51396
- return allAsync(conn, sql);
51397
- });
51398
- }
51399
-
51400
- const TARGET_DIR = "target";
51401
- const ACTION_COLUMNS = [
51402
- "target_key",
51403
- "database",
51404
- "schema",
51405
- "name",
51406
- "readable_name",
51407
- "type",
51408
- "tags",
51409
- "disabled",
51410
- "file_name",
51411
- "description"
51412
- ];
51413
- const DEPENDENCY_COLUMNS = ["from_target_key", "to_target_key", "from_readable", "to_readable"];
51414
- const COLUMN_COLUMNS = ["target_key", "readable_name", "column_name", "description"];
51415
- const RUN_COLUMNS = [
51416
- "run_id",
51417
- "run_status",
51418
- "target_key",
51419
- "readable_name",
51420
- "status",
51421
- "start_millis",
51422
- "end_millis",
51423
- "duration_millis",
51424
- "error_message"
51425
- ];
51426
- async function writeArtifacts(compiledGraph, projectDir, options = {}) {
51427
- const targetDir = path__namespace.join(projectDir, TARGET_DIR);
51428
- const catalogDir = path__namespace.join(targetDir, "catalog");
51429
- const { actions, dependencies, columns } = catalogRows(compiledGraph);
51430
- await writeParquet(actions, path__namespace.join(catalogDir, "actions.parquet"), ACTION_COLUMNS);
51431
- await writeParquet(dependencies, path__namespace.join(catalogDir, "dependencies.parquet"), DEPENDENCY_COLUMNS);
51432
- await writeParquet(columns, path__namespace.join(catalogDir, "columns.parquet"), COLUMN_COLUMNS);
51433
- if (options.runResult) {
51434
- const runId = options.runId !== undefined ? options.runId : Date.now();
51435
- await writeParquet(runRows(options.runResult, runId), path__namespace.join(targetDir, "runs", `run_${runId}.parquet`), RUN_COLUMNS);
51609
+ taskResult.timing = timer.end();
51610
+ this.notifyListeners();
51611
+ return taskResult.status;
51436
51612
  }
51437
- return { targetDir };
51438
51613
  }
51439
- async function safeWriteArtifacts(compiledGraph, projectDir, options = {}) {
51440
- try {
51441
- await writeArtifacts(compiledGraph, projectDir, options);
51614
+ class Timer {
51615
+ constructor(startTimeMillis) {
51616
+ this.startTimeMillis = startTimeMillis;
51442
51617
  }
51443
- catch (e) {
51444
- if (options.warn) {
51445
- options.warn(`Artifacts skipped: ${e.message}`);
51446
- }
51618
+ static start(existingTiming) {
51619
+ return new Timer((existingTiming === null || existingTiming === void 0 ? void 0 : existingTiming.startTimeMillis.toNumber()) || new Date().valueOf());
51620
+ }
51621
+ current() {
51622
+ return {
51623
+ startTimeMillis: Long__default["default"].fromNumber(this.startTimeMillis)
51624
+ };
51625
+ }
51626
+ end() {
51627
+ return {
51628
+ startTimeMillis: Long__default["default"].fromNumber(this.startTimeMillis),
51629
+ endTimeMillis: Long__default["default"].fromNumber(new Date().valueOf())
51630
+ };
51447
51631
  }
51448
51632
  }
51449
51633
 
51450
- function escapeHtml(value) {
51451
- return String(value === null || value === undefined ? "" : value)
51452
- .replace(/&/g, "&amp;")
51453
- .replace(/</g, "&lt;")
51454
- .replace(/>/g, "&gt;")
51455
- .replace(/"/g, "&quot;");
51634
+ async function test(dbadapter, tests) {
51635
+ return await Promise.all(tests.map(testCase => runTest(dbadapter, testCase)));
51456
51636
  }
51457
- async function buildDocsModel(views, generatedAt) {
51458
- const hasRuns = views.some(v => v.name === "runs");
51459
- const actions = await queryParquet("select readable_name, type, tags, description from actions order by type, readable_name", views);
51460
- const dependencies = await queryParquet("select from_readable, to_readable from dependencies", views);
51461
- const columns = await queryParquet("select readable_name, column_name, description from columns order by readable_name, column_name", views);
51462
- let latestRun;
51463
- const statusByModel = new Map();
51464
- if (hasRuns) {
51465
- const head = await queryParquet("select max(run_id) as run_id from runs", views);
51466
- const runId = head[0] && head[0].run_id !== null ? Number(head[0].run_id) : undefined;
51467
- if (runId !== undefined) {
51468
- const overall = await queryParquet(`select run_status from runs where run_id = ${runId} limit 1`, views);
51469
- latestRun = { runId, status: overall[0] ? overall[0].run_status : "UNKNOWN" };
51470
- const statuses = await queryParquet(`select readable_name, status from runs where run_id = ${runId}`, views);
51471
- for (const row of statuses) {
51472
- statusByModel.set(row.readable_name, row.status);
51473
- }
51474
- }
51637
+ async function runTest(dbadapter, testCase) {
51638
+ const noLimit = {};
51639
+ let actualResults;
51640
+ let expectedResults;
51641
+ try {
51642
+ [actualResults, expectedResults] = await Promise.all([
51643
+ dbadapter.execute(testCase.testQuery, noLimit),
51644
+ dbadapter.execute(testCase.expectedOutputQuery, noLimit)
51645
+ ]);
51475
51646
  }
51476
- const dependsOn = new Map();
51477
- for (const dep of dependencies) {
51478
- dependsOn.set(dep.from_readable, (dependsOn.get(dep.from_readable) || []).concat(dep.to_readable));
51647
+ catch (e) {
51648
+ return {
51649
+ name: testCase.name,
51650
+ successful: false,
51651
+ messages: [`Error thrown: ${e.message}.`]
51652
+ };
51479
51653
  }
51480
- const byTypeMap = new Map();
51481
- const models = actions.map(a => {
51482
- byTypeMap.set(a.type, (byTypeMap.get(a.type) || 0) + 1);
51483
- let tags = [];
51484
- try {
51485
- tags = JSON.parse(a.tags || "[]");
51486
- }
51487
- catch (e) {
51488
- tags = [];
51489
- }
51654
+ if (actualResults.rows.length !== expectedResults.rows.length) {
51490
51655
  return {
51491
- readable: a.readable_name,
51492
- type: a.type,
51493
- tags,
51494
- description: a.description || "",
51495
- status: statusByModel.get(a.readable_name),
51496
- dependsOn: dependsOn.get(a.readable_name) || []
51656
+ name: testCase.name,
51657
+ successful: false,
51658
+ messages: [
51659
+ `Expected ${expectedResults.rows.length} rows, but saw ${actualResults.rows.length} rows.`
51660
+ ]
51497
51661
  };
51498
- });
51499
- return {
51500
- generatedAt,
51501
- summary: {
51502
- total: models.length,
51503
- byType: Array.from(byTypeMap.entries())
51504
- .map(([type, n]) => ({ type, n }))
51505
- .sort((x, y) => x.type.localeCompare(y.type))
51506
- },
51507
- latestRun,
51508
- models,
51509
- columns: columns.map(c => ({
51510
- readable: c.readable_name,
51511
- column: c.column_name,
51512
- description: c.description || ""
51513
- }))
51514
- };
51515
- }
51516
- function renderDocsHtml(model) {
51517
- const summaryLine = `${model.summary.total} models — ` +
51518
- model.summary.byType.map(t => `${t.n} ${t.type}`).join(", ");
51519
- const runLine = model.latestRun
51520
- ? `Last run: <strong>${escapeHtml(model.latestRun.status)}</strong> (run ${model.latestRun.runId})`
51521
- : "No runs recorded yet.";
51522
- const statusBadge = (status) => {
51523
- if (!status) {
51524
- return "";
51525
- }
51526
- const cls = status === "SUCCESSFUL" ? "ok" : status === "FAILED" ? "fail" : "muted";
51527
- return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
51528
- };
51529
- const modelRows = model.models
51530
- .map(m => `<tr data-search="${escapeHtml((m.readable + " " + m.type + " " + m.tags.join(" ")).toLowerCase())}">
51531
- <td><code>${escapeHtml(m.readable)}</code></td>
51532
- <td>${escapeHtml(m.type)}</td>
51533
- <td>${m.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join(" ")}</td>
51534
- <td>${statusBadge(m.status)}</td>
51535
- <td>${m.dependsOn.map(d => `<code>${escapeHtml(d)}</code>`).join("<br>")}</td>
51536
- <td>${escapeHtml(m.description)}</td>
51537
- </tr>`)
51538
- .join("\n");
51539
- const columnRows = model.columns
51540
- .map(c => `<tr><td><code>${escapeHtml(c.readable)}</code></td><td><code>${escapeHtml(c.column)}</code></td><td>${escapeHtml(c.description)}</td></tr>`)
51541
- .join("\n");
51542
- return `<!doctype html>
51543
- <html lang="en">
51544
- <head>
51545
- <meta charset="utf-8">
51546
- <meta name="viewport" content="width=device-width, initial-scale=1">
51547
- <title>SQLAnvil catalog</title>
51548
- <style>
51549
- :root { color-scheme: light dark; }
51550
- body { font: 14px/1.5 -apple-system, system-ui, sans-serif; margin: 2rem; max-width: 1100px; }
51551
- h1 { margin: 0 0 .25rem; }
51552
- .meta { color: #888; margin-bottom: 1.5rem; }
51553
- table { border-collapse: collapse; width: 100%; margin: 1rem 0 2rem; }
51554
- th, td { text-align: left; padding: .4rem .6rem; border-bottom: 1px solid #8884; vertical-align: top; }
51555
- th { font-weight: 600; }
51556
- code { font-size: 12px; }
51557
- .tag { background: #8883; border-radius: 4px; padding: 0 .35rem; font-size: 12px; }
51558
- .badge { border-radius: 4px; padding: 0 .4rem; font-size: 12px; font-weight: 600; }
51559
- .badge.ok { background: #1a7f37; color: #fff; }
51560
- .badge.fail { background: #b62324; color: #fff; }
51561
- .badge.muted { background: #8884; }
51562
- #q { padding: .4rem .6rem; width: 320px; max-width: 100%; margin-bottom: .5rem; }
51563
- </style>
51564
- </head>
51565
- <body>
51566
- <h1>SQLAnvil catalog</h1>
51567
- <div class="meta">${escapeHtml(summaryLine)} &middot; ${runLine} &middot; generated ${escapeHtml(model.generatedAt)}</div>
51568
-
51569
- <input id="q" type="search" placeholder="Filter models…" oninput="filterModels(this.value)">
51570
- <table id="models">
51571
- <thead><tr><th>Model</th><th>Type</th><th>Tags</th><th>Last run</th><th>Depends on</th><th>Description</th></tr></thead>
51572
- <tbody>
51573
- ${modelRows}
51574
- </tbody>
51575
- </table>
51576
-
51577
- <h2>Columns</h2>
51578
- <table>
51579
- <thead><tr><th>Model</th><th>Column</th><th>Description</th></tr></thead>
51580
- <tbody>
51581
- ${columnRows || '<tr><td colspan="3" class="meta">No documented columns.</td></tr>'}
51582
- </tbody>
51583
- </table>
51584
-
51585
- <script>
51586
- function filterModels(q) {
51587
- q = q.toLowerCase();
51588
- for (const tr of document.querySelectorAll('#models tbody tr')) {
51589
- tr.style.display = tr.getAttribute('data-search').includes(q) ? '' : 'none';
51590
51662
  }
51591
- }
51592
- </script>
51593
- </body>
51594
- </html>
51595
- `;
51596
- }
51597
-
51598
- function targetKey(target) {
51599
- return [target.database, target.schema, target.name].filter(Boolean).join(".");
51600
- }
51601
- function topoOrder(nodes) {
51602
- const byKey = new Map(nodes.map(n => [n.key, n]));
51603
- const indegree = new Map();
51604
- const dependents = new Map();
51605
- for (const n of nodes) {
51606
- const inGraphDeps = Array.from(new Set(n.dependencyKeys.filter(d => byKey.has(d) && d !== n.key)));
51607
- indegree.set(n.key, inGraphDeps.length);
51608
- for (const dep of inGraphDeps) {
51609
- dependents.set(dep, (dependents.get(dep) || []).concat(n.key));
51610
- }
51663
+ if (actualResults.rows.length === 0) {
51664
+ return {
51665
+ name: testCase.name,
51666
+ successful: true
51667
+ };
51668
+ }
51669
+ const actualColumns = Object.keys(actualResults.rows[0]);
51670
+ const expectedColumns = Object.keys(expectedResults.rows[0]);
51671
+ if (actualColumns.length !== expectedColumns.length) {
51672
+ return {
51673
+ name: testCase.name,
51674
+ successful: false,
51675
+ messages: [`Expected columns "${expectedColumns}", but saw "${actualColumns}".`]
51676
+ };
51611
51677
  }
51612
- const ready = nodes
51613
- .filter(n => (indegree.get(n.key) || 0) === 0)
51614
- .map(n => n.key)
51615
- .sort();
51616
- const ordered = [];
51617
- const emitted = new Set();
51618
- while (ready.length) {
51619
- const key = ready.shift();
51620
- if (emitted.has(key)) {
51621
- continue;
51622
- }
51623
- emitted.add(key);
51624
- ordered.push(byKey.get(key));
51625
- for (const dep of (dependents.get(key) || []).slice().sort()) {
51626
- indegree.set(dep, (indegree.get(dep) || 0) - 1);
51627
- if ((indegree.get(dep) || 0) <= 0 && !emitted.has(dep)) {
51628
- ready.push(dep);
51629
- ready.sort();
51630
- }
51678
+ for (const expectedColumn of expectedColumns) {
51679
+ if (!actualColumns.some(actualColumn => normalizeColumnName(actualColumn) === normalizeColumnName(expectedColumn))) {
51680
+ return {
51681
+ name: testCase.name,
51682
+ successful: false,
51683
+ messages: [`Expected columns "${expectedColumns}", but saw "${actualColumns}".`]
51684
+ };
51631
51685
  }
51632
51686
  }
51633
- if (ordered.length < nodes.length) {
51634
- for (const n of nodes.slice().sort((a, b) => a.key.localeCompare(b.key))) {
51635
- if (!emitted.has(n.key)) {
51636
- emitted.add(n.key);
51637
- ordered.push(n);
51687
+ const rowMessages = [];
51688
+ for (let i = 0; i < actualResults.rows.length; i++) {
51689
+ const actualResultRow = normalizeRow(actualResults.rows[i]);
51690
+ const expectedResultRow = normalizeRow(expectedResults.rows[i]);
51691
+ for (const column of actualColumns) {
51692
+ const normalizedColumn = normalizeColumnName(column);
51693
+ const expectedValue = expectedResultRow[normalizedColumn];
51694
+ const actualValue = actualResultRow[normalizedColumn];
51695
+ if (expectedValue === null && actualValue !== null) {
51696
+ rowMessages.push(`For row ${i} and column "${column}": expected null, but saw "${actualValue}".`);
51697
+ break;
51698
+ }
51699
+ if (expectedValue !== null && actualValue === null) {
51700
+ rowMessages.push(`For row ${i} and column "${column}": expected "${expectedValue}", but saw null.`);
51701
+ break;
51702
+ }
51703
+ if (typeof expectedValue !== typeof actualValue) {
51704
+ rowMessages.push(`For row ${i} and column "${column}": expected type "${typeof expectedValue}", but saw type "${typeof actualValue}".`);
51705
+ break;
51706
+ }
51707
+ const comparableExpectedValue = typeof expectedValue === "object" ? JSON.stringify(expectedValue) : expectedValue;
51708
+ const comparableActualValue = typeof actualValue === "object" ? JSON.stringify(actualValue) : actualValue;
51709
+ if (comparableExpectedValue !== comparableActualValue) {
51710
+ rowMessages.push(`For row ${i} and column "${column}": expected "${comparableExpectedValue}", but saw "${comparableActualValue}".`);
51638
51711
  }
51639
51712
  }
51640
51713
  }
51641
- return ordered;
51642
- }
51643
- function dependencyBlocked(dependencyKeys, statusByKey) {
51644
- return dependencyKeys.some(dep => {
51645
- const status = statusByKey.get(dep);
51646
- return status !== undefined && status !== "PASS";
51647
- });
51648
- }
51649
- const VALIDATE_SHADOW_PREFIX = "sqlanvil_validate_";
51650
- function validateShadowSuffix(nowMs) {
51651
- return `${VALIDATE_SHADOW_PREFIX}${nowMs}`;
51714
+ if (rowMessages.length > 0) {
51715
+ return {
51716
+ name: testCase.name,
51717
+ successful: false,
51718
+ messages: rowMessages
51719
+ };
51720
+ }
51721
+ return {
51722
+ name: testCase.name,
51723
+ successful: true
51724
+ };
51652
51725
  }
51653
- function parseShadowTimestamp(schemaName) {
51654
- const match = schemaName.match(/sqlanvil_validate_(\d+)/);
51655
- return match ? Number(match[1]) : null;
51726
+ function normalizeColumnName(name) {
51727
+ return name.toUpperCase();
51656
51728
  }
51657
- function shadowSchemasToSweep(schemaNames, nowMs, maxAgeMs) {
51658
- return schemaNames.filter(name => {
51659
- const ts = parseShadowTimestamp(name);
51660
- return ts !== null && nowMs - ts > maxAgeMs;
51729
+ function normalizeRow(row) {
51730
+ const newRow = {};
51731
+ Object.keys(row).forEach(colName => {
51732
+ newRow[normalizeColumnName(colName)] = row[colName];
51661
51733
  });
51734
+ return newRow;
51662
51735
  }
51663
51736
 
51664
- const SHADOW_MAX_AGE_MS = 60 * 60 * 1000;
51665
- async function sweepOrphanShadows(deps, nowMs, maxAgeMs = SHADOW_MAX_AGE_MS) {
51666
- try {
51667
- const schemas = await deps.listSchemas();
51668
- for (const schema of shadowSchemasToSweep(schemas, nowMs, maxAgeMs)) {
51669
- try {
51670
- await deps.execute(deps.dropSchemaCascadeSql(schema));
51671
- }
51672
- catch (e) {
51673
- }
51674
- }
51737
+ function resolveEnvironment(projectDir, envName) {
51738
+ const settings = readConfigFromWorkflowSettings(projectDir);
51739
+ const environments = (settings === null || settings === void 0 ? void 0 : settings.environments) || {};
51740
+ const env = environments[envName];
51741
+ if (!env) {
51742
+ const available = Object.keys(environments);
51743
+ throw new Error(`Environment "${envName}" not found. ` +
51744
+ (available.length
51745
+ ? `Available environments: ${available.join(", ")}.`
51746
+ : `No environments defined in workflow_settings.yaml.`));
51675
51747
  }
51676
- catch (e) {
51748
+ const configOverride = {};
51749
+ if (env.schemaSuffix) {
51750
+ configOverride.schemaSuffix = env.schemaSuffix;
51677
51751
  }
51678
- }
51679
- function tableType(enumType) {
51680
- switch (enumType) {
51681
- case sqlanvil.TableType.VIEW:
51682
- return "view";
51683
- case sqlanvil.TableType.INCREMENTAL:
51684
- return "incremental";
51685
- default:
51686
- return "table";
51752
+ if (env.vars && Object.keys(env.vars).length > 0) {
51753
+ configOverride.vars = env.vars;
51687
51754
  }
51688
- }
51689
- function depKeys(deps) {
51690
- return (deps || []).map(targetKey);
51691
- }
51692
- async function validate(compiledGraph, deps, options = {}) {
51693
- const nodes = [];
51694
- for (const table of compiledGraph.tables || []) {
51695
- nodes.push({
51696
- key: targetKey(table.target),
51697
- dependencyKeys: depKeys(table.dependencyTargets),
51698
- kind: "table",
51699
- type: tableType(table.enumType),
51700
- target: table.target,
51701
- table,
51702
- action: table
51703
- });
51755
+ if (env.defaultDatabase) {
51756
+ configOverride.defaultDatabase = env.defaultDatabase;
51704
51757
  }
51705
- for (const assertion of compiledGraph.assertions || []) {
51706
- nodes.push({
51707
- key: targetKey(assertion.target),
51708
- dependencyKeys: depKeys(assertion.dependencyTargets),
51709
- kind: "assertion",
51710
- type: "assertion",
51711
- target: assertion.target,
51712
- action: assertion
51713
- });
51758
+ if (env.defaultLocation) {
51759
+ configOverride.defaultLocation = env.defaultLocation;
51714
51760
  }
51715
- for (const operation of compiledGraph.operations || []) {
51716
- if (!operation.target) {
51717
- continue;
51718
- }
51719
- nodes.push({
51720
- key: targetKey(operation.target),
51721
- dependencyKeys: depKeys(operation.dependencyTargets),
51722
- kind: "operation",
51723
- type: "operation",
51724
- target: operation.target,
51725
- action: undefined
51726
- });
51761
+ return { configOverride, credentials: env.credentials || undefined };
51762
+ }
51763
+ function mergeProjectConfigOverride(envOverride, cliOverride) {
51764
+ const merged = Object.assign(Object.assign({}, envOverride), cliOverride);
51765
+ if (envOverride.vars || cliOverride.vars) {
51766
+ merged.vars = Object.assign(Object.assign({}, envOverride.vars), cliOverride.vars);
51727
51767
  }
51728
- for (const imp of compiledGraph.imports || []) {
51729
- if (!imp.target) {
51730
- continue;
51731
- }
51732
- nodes.push({
51733
- key: targetKey(imp.target),
51734
- dependencyKeys: depKeys(imp.dependencyTargets),
51735
- kind: "import",
51736
- type: "import",
51737
- target: imp.target,
51738
- action: undefined
51739
- });
51768
+ return merged;
51769
+ }
51770
+ function resolveCredentials(envCredentials, cliCredentials, defaultFilename) {
51771
+ if (cliCredentials && cliCredentials !== defaultFilename) {
51772
+ return cliCredentials;
51740
51773
  }
51741
- const ordered = topoOrder(nodes);
51742
- const shadowSchemas = Array.from(new Set(nodes.filter(n => n.kind === "table").map(n => n.target.schema)));
51743
- const statusByKey = new Map();
51744
- const results = [];
51745
- try {
51746
- for (const schema of shadowSchemas) {
51747
- await deps.execute(deps.createSchemaSql(schema));
51748
- }
51749
- for (const node of ordered) {
51750
- if (node.kind === "operation" || node.kind === "import") {
51751
- statusByKey.set(node.key, "SKIPPED");
51752
- results.push({ target: node.target, type: node.type, status: "SKIPPED", errors: [] });
51753
- continue;
51754
- }
51755
- if (dependencyBlocked(node.dependencyKeys, statusByKey)) {
51756
- statusByKey.set(node.key, "BLOCKED");
51757
- results.push({ target: node.target, type: node.type, status: "BLOCKED", errors: [] });
51758
- continue;
51759
- }
51760
- const evaluations = await deps.evaluate(node.action);
51761
- const failed = evaluations.some(e => e.status === sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE);
51762
- const status = failed ? "FAILURE" : "PASS";
51763
- statusByKey.set(node.key, status);
51764
- results.push({ target: node.target, type: node.type, status, errors: evaluations });
51765
- if (status === "PASS" && node.kind === "table") {
51766
- try {
51767
- await deps.execute(deps.validationStubSql(node.table));
51768
- }
51769
- catch (e) {
51770
- }
51771
- }
51772
- }
51774
+ return envCredentials || defaultFilename;
51775
+ }
51776
+
51777
+ function key(target) {
51778
+ return targetStringifier.stringify(target);
51779
+ }
51780
+ function enumName(enumObject, value) {
51781
+ if (value === undefined || value === null) {
51782
+ return "UNKNOWN";
51773
51783
  }
51774
- finally {
51775
- if (!options.keepShadow) {
51776
- for (const schema of shadowSchemas.slice().reverse()) {
51777
- try {
51778
- await deps.execute(deps.dropSchemaCascadeSql(schema));
51779
- }
51780
- catch (e) {
51781
- }
51782
- }
51783
- }
51784
+ const match = Object.keys(enumObject).find(k => enumObject[k] === value);
51785
+ return match || String(value);
51786
+ }
51787
+ function tableType$1(enumType) {
51788
+ switch (enumType) {
51789
+ case sqlanvil.TableType.VIEW:
51790
+ return "view";
51791
+ case sqlanvil.TableType.INCREMENTAL:
51792
+ return "incremental";
51793
+ default:
51794
+ return "table";
51784
51795
  }
51785
- return results;
51786
51796
  }
51787
-
51788
- class MySqlPoolExecutor {
51789
- constructor(config, options) {
51790
- this.pool = mysql__namespace.createPool(Object.assign(Object.assign({}, config), { connectionLimit: (options === null || options === void 0 ? void 0 : options.concurrencyLimit) || 10, waitForConnections: true, multipleStatements: false }));
51791
- }
51792
- async verifyConnection() {
51793
- const conn = await this.pool.getConnection();
51794
- try {
51795
- await conn.query("select 1");
51796
- }
51797
- finally {
51798
- conn.release();
51799
- }
51797
+ function toMillis(value) {
51798
+ if (value === undefined || value === null) {
51799
+ return 0;
51800
51800
  }
51801
- async withClientLock(callback) {
51802
- const conn = await this.pool.getConnection();
51803
- let released = false;
51804
- const releaseOnce = () => {
51805
- if (released) {
51806
- return;
51807
- }
51808
- released = true;
51809
- conn.release();
51810
- };
51811
- try {
51812
- return await callback({
51813
- execute: async (statement, options = { rowLimit: 1000 }) => {
51814
- const [rows] = await conn.query(statement, options.params || []);
51815
- const arr = Array.isArray(rows) ? rows : [];
51816
- return options.rowLimit && arr.length > options.rowLimit
51817
- ? arr.slice(0, options.rowLimit)
51818
- : arr;
51819
- }
51820
- });
51821
- }
51822
- finally {
51823
- releaseOnce();
51824
- }
51801
+ if (typeof value === "number") {
51802
+ return value;
51825
51803
  }
51826
- async close() {
51827
- await this.pool.end();
51804
+ if (typeof value.toNumber === "function") {
51805
+ return value.toNumber();
51828
51806
  }
51807
+ return Number(value) || 0;
51829
51808
  }
51830
- function convertFieldType(type) {
51831
- switch (String(type).toUpperCase()) {
51832
- case "FLOAT":
51833
- case "DOUBLE":
51834
- case "REAL":
51835
- return sqlanvil.Field.Primitive.FLOAT;
51836
- case "TINYINT":
51837
- case "SMALLINT":
51838
- case "MEDIUMINT":
51839
- case "INT":
51840
- case "INTEGER":
51841
- case "BIGINT":
51842
- case "YEAR":
51843
- case "BIT":
51844
- return sqlanvil.Field.Primitive.INTEGER;
51845
- case "DECIMAL":
51846
- case "DEC":
51847
- case "NUMERIC":
51848
- case "FIXED":
51849
- return sqlanvil.Field.Primitive.NUMERIC;
51850
- case "BOOL":
51851
- case "BOOLEAN":
51852
- return sqlanvil.Field.Primitive.BOOLEAN;
51853
- case "CHAR":
51854
- case "VARCHAR":
51855
- case "TINYTEXT":
51856
- case "TEXT":
51857
- case "MEDIUMTEXT":
51858
- case "LONGTEXT":
51859
- case "ENUM":
51860
- case "SET":
51861
- case "JSON":
51862
- case "TIME":
51863
- return sqlanvil.Field.Primitive.STRING;
51864
- case "DATE":
51865
- return sqlanvil.Field.Primitive.DATE;
51866
- case "DATETIME":
51867
- case "TIMESTAMP":
51868
- return sqlanvil.Field.Primitive.TIMESTAMP;
51869
- default:
51870
- return sqlanvil.Field.Primitive.UNKNOWN;
51809
+ function actionRow(action, type) {
51810
+ const descriptor = action.actionDescriptor || {};
51811
+ return {
51812
+ target_key: key(action.target),
51813
+ database: action.target.database || "",
51814
+ schema: action.target.schema || "",
51815
+ name: action.target.name || "",
51816
+ readable_name: targetAsReadableString(action.target),
51817
+ type,
51818
+ tags: JSON.stringify(action.tags || []),
51819
+ disabled: !!action.disabled,
51820
+ file_name: action.fileName || "",
51821
+ description: descriptor.description || ""
51822
+ };
51823
+ }
51824
+ function pushDeps(action, out) {
51825
+ for (const dep of action.dependencyTargets || []) {
51826
+ out.push({
51827
+ from_target_key: key(action.target),
51828
+ to_target_key: key(dep),
51829
+ from_readable: targetAsReadableString(action.target),
51830
+ to_readable: targetAsReadableString(dep)
51831
+ });
51871
51832
  }
51872
51833
  }
51873
- function escapeMysqlString(s) {
51874
- return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
51834
+ function pushColumns(action, out) {
51835
+ const columns = (action.actionDescriptor && action.actionDescriptor.columns) || [];
51836
+ for (const column of columns) {
51837
+ out.push({
51838
+ target_key: key(action.target),
51839
+ readable_name: targetAsReadableString(action.target),
51840
+ column_name: (column.path || []).join("."),
51841
+ description: column.description || ""
51842
+ });
51843
+ }
51875
51844
  }
51876
- function reconstructColumnDef(col) {
51877
- const extra = (col.extra || "").toLowerCase();
51878
- const notNull = col.isNullable === "NO";
51879
- if (extra.includes("generated") && col.generationExpression) {
51880
- const storage = extra.includes("stored") ? "STORED" : "VIRTUAL";
51881
- return `${col.columnType} GENERATED ALWAYS AS (${col.generationExpression}) ${storage}${notNull ? " NOT NULL" : ""}`;
51845
+ function catalogRows(compiledGraph) {
51846
+ const actions = [];
51847
+ const dependencies = [];
51848
+ const columns = [];
51849
+ const add = (action, type) => {
51850
+ actions.push(actionRow(action, type));
51851
+ pushDeps(action, dependencies);
51852
+ pushColumns(action, columns);
51853
+ };
51854
+ for (const table of compiledGraph.tables || []) {
51855
+ add(table, tableType$1(table.enumType));
51882
51856
  }
51883
- let def = col.columnType;
51884
- if (col.collationName) {
51885
- def += ` COLLATE ${col.collationName}`;
51857
+ for (const operation of compiledGraph.operations || []) {
51858
+ add(operation, "operation");
51886
51859
  }
51887
- def += notNull ? " NOT NULL" : " NULL";
51888
- def += defaultClause(col, extra);
51889
- if (extra.includes("on update current_timestamp")) {
51890
- def += " ON UPDATE CURRENT_TIMESTAMP";
51860
+ for (const assertion of compiledGraph.assertions || []) {
51861
+ add(assertion, "assertion");
51891
51862
  }
51892
- if (extra.includes("auto_increment")) {
51893
- def += " AUTO_INCREMENT";
51863
+ for (const exp of compiledGraph.exports || []) {
51864
+ add(exp, "export");
51894
51865
  }
51895
- return def;
51866
+ for (const imp of compiledGraph.imports || []) {
51867
+ add(imp, "import");
51868
+ }
51869
+ for (const declaration of compiledGraph.declarations || []) {
51870
+ add(declaration, "declaration");
51871
+ }
51872
+ return { actions, dependencies, columns };
51896
51873
  }
51897
- function defaultClause(col, extra) {
51898
- if (extra.includes("default_generated")) {
51899
- const d = col.columnDefault || "";
51900
- return /^current_timestamp/i.test(d) ? ` DEFAULT ${d}` : ` DEFAULT (${d})`;
51874
+ function runRows(runResult, runId) {
51875
+ const runStatus = enumName(sqlanvil.RunResult.ExecutionStatus, runResult.status);
51876
+ return (runResult.actions || []).map(action => {
51877
+ const start = toMillis(action.timing && action.timing.startTimeMillis);
51878
+ const end = toMillis(action.timing && action.timing.endTimeMillis);
51879
+ const failedTask = (action.tasks || []).find(t => !!t.errorMessage);
51880
+ return {
51881
+ run_id: runId,
51882
+ run_status: runStatus,
51883
+ target_key: key(action.target),
51884
+ readable_name: targetAsReadableString(action.target),
51885
+ status: enumName(sqlanvil.ActionResult.ExecutionStatus, action.status),
51886
+ start_millis: start,
51887
+ end_millis: end,
51888
+ duration_millis: start && end ? end - start : 0,
51889
+ error_message: (failedTask && failedTask.errorMessage) || ""
51890
+ };
51891
+ });
51892
+ }
51893
+
51894
+ let tmpCounter = 0;
51895
+ async function writeParquet(rows, outPath, columns) {
51896
+ await fs__namespace.ensureDir(path__namespace.dirname(outPath));
51897
+ const tmp = path__namespace.join(os__namespace.tmpdir(), `sa_artifact_${process.pid}_${Date.now()}_${tmpCounter++}.json`);
51898
+ const payload = rows.length > 0 ? rows : [columns.reduce((o, c) => ((o[c] = null), o), {})];
51899
+ const whereFalse = rows.length > 0 ? "" : " WHERE false";
51900
+ await fs__namespace.writeFile(tmp, JSON.stringify(payload));
51901
+ try {
51902
+ await withDuckdb(async (conn) => {
51903
+ await runAsync(conn, `COPY (SELECT * FROM read_json_auto('${tmp}')${whereFalse}) TO '${outPath}' (FORMAT parquet)`);
51904
+ });
51901
51905
  }
51902
- if (col.columnDefault === null || col.columnDefault === undefined) {
51903
- return "";
51906
+ finally {
51907
+ await fs__namespace.remove(tmp).catch(() => undefined);
51908
+ }
51909
+ }
51910
+ async function queryParquet(sql, views) {
51911
+ return withDuckdb(async (conn) => {
51912
+ for (const view of views) {
51913
+ await runAsync(conn, `create view ${view.name} as select * from read_parquet('${view.glob}')`);
51914
+ }
51915
+ return allAsync(conn, sql);
51916
+ });
51917
+ }
51918
+
51919
+ const TARGET_DIR = "target";
51920
+ const ACTION_COLUMNS = [
51921
+ "target_key",
51922
+ "database",
51923
+ "schema",
51924
+ "name",
51925
+ "readable_name",
51926
+ "type",
51927
+ "tags",
51928
+ "disabled",
51929
+ "file_name",
51930
+ "description"
51931
+ ];
51932
+ const DEPENDENCY_COLUMNS = ["from_target_key", "to_target_key", "from_readable", "to_readable"];
51933
+ const COLUMN_COLUMNS = ["target_key", "readable_name", "column_name", "description"];
51934
+ const RUN_COLUMNS = [
51935
+ "run_id",
51936
+ "run_status",
51937
+ "target_key",
51938
+ "readable_name",
51939
+ "status",
51940
+ "start_millis",
51941
+ "end_millis",
51942
+ "duration_millis",
51943
+ "error_message"
51944
+ ];
51945
+ async function writeArtifacts(compiledGraph, projectDir, options = {}) {
51946
+ const targetDir = path__namespace.join(projectDir, TARGET_DIR);
51947
+ const catalogDir = path__namespace.join(targetDir, "catalog");
51948
+ const { actions, dependencies, columns } = catalogRows(compiledGraph);
51949
+ await writeParquet(actions, path__namespace.join(catalogDir, "actions.parquet"), ACTION_COLUMNS);
51950
+ await writeParquet(dependencies, path__namespace.join(catalogDir, "dependencies.parquet"), DEPENDENCY_COLUMNS);
51951
+ await writeParquet(columns, path__namespace.join(catalogDir, "columns.parquet"), COLUMN_COLUMNS);
51952
+ if (options.runResult) {
51953
+ const runId = options.runId !== undefined ? options.runId : Date.now();
51954
+ await writeParquet(runRows(options.runResult, runId), path__namespace.join(targetDir, "runs", `run_${runId}.parquet`), RUN_COLUMNS);
51955
+ }
51956
+ return { targetDir };
51957
+ }
51958
+ async function safeWriteArtifacts(compiledGraph, projectDir, options = {}) {
51959
+ try {
51960
+ await writeArtifacts(compiledGraph, projectDir, options);
51904
51961
  }
51905
- const d = col.columnDefault;
51906
- if (d.toUpperCase() === "NULL" || /^-?\d+(\.\d+)?$/.test(d)) {
51907
- return ` DEFAULT ${d}`;
51962
+ catch (e) {
51963
+ if (options.warn) {
51964
+ options.warn(`Artifacts skipped: ${e.message}`);
51965
+ }
51908
51966
  }
51909
- return ` DEFAULT '${escapeMysqlString(d)}'`;
51910
51967
  }
51911
51968
 
51912
- const INTERNAL_SCHEMAS = new Set([
51913
- "information_schema",
51914
- "mysql",
51915
- "performance_schema",
51916
- "sys"
51917
- ]);
51918
- class MySqlDbAdapter {
51919
- constructor(queryExecutor) {
51920
- this.queryExecutor = queryExecutor;
51969
+ function escapeHtml(value) {
51970
+ return String(value === null || value === undefined ? "" : value)
51971
+ .replace(/&/g, "&amp;")
51972
+ .replace(/</g, "&lt;")
51973
+ .replace(/>/g, "&gt;")
51974
+ .replace(/"/g, "&quot;");
51975
+ }
51976
+ async function buildDocsModel(views, generatedAt) {
51977
+ const hasRuns = views.some(v => v.name === "runs");
51978
+ const actions = await queryParquet("select readable_name, type, tags, description from actions order by type, readable_name", views);
51979
+ const dependencies = await queryParquet("select from_readable, to_readable from dependencies", views);
51980
+ const columns = await queryParquet("select readable_name, column_name, description from columns order by readable_name, column_name", views);
51981
+ let latestRun;
51982
+ const statusByModel = new Map();
51983
+ if (hasRuns) {
51984
+ const head = await queryParquet("select max(run_id) as run_id from runs", views);
51985
+ const runId = head[0] && head[0].run_id !== null ? Number(head[0].run_id) : undefined;
51986
+ if (runId !== undefined) {
51987
+ const overall = await queryParquet(`select run_status from runs where run_id = ${runId} limit 1`, views);
51988
+ latestRun = { runId, status: overall[0] ? overall[0].run_status : "UNKNOWN" };
51989
+ const statuses = await queryParquet(`select readable_name, status from runs where run_id = ${runId}`, views);
51990
+ for (const row of statuses) {
51991
+ statusByModel.set(row.readable_name, row.status);
51992
+ }
51993
+ }
51921
51994
  }
51922
- static async create(credentials, options) {
51923
- const sslMode = (credentials.sslMode || "").toLowerCase();
51924
- const ssl = !(options === null || options === void 0 ? void 0 : options.disableSslForTestsOnly) && sslMode && sslMode !== "disable"
51925
- ?
51926
- { rejectUnauthorized: false }
51927
- : undefined;
51928
- const queryExecutor = new MySqlPoolExecutor({
51929
- host: credentials.host,
51930
- port: credentials.port || 3306,
51931
- user: credentials.user,
51932
- password: credentials.password,
51933
- database: credentials.database || undefined,
51934
- ssl
51935
- }, options);
51995
+ const dependsOn = new Map();
51996
+ for (const dep of dependencies) {
51997
+ dependsOn.set(dep.from_readable, (dependsOn.get(dep.from_readable) || []).concat(dep.to_readable));
51998
+ }
51999
+ const byTypeMap = new Map();
52000
+ const models = actions.map(a => {
52001
+ byTypeMap.set(a.type, (byTypeMap.get(a.type) || 0) + 1);
52002
+ let tags = [];
51936
52003
  try {
51937
- await queryExecutor.verifyConnection();
52004
+ tags = JSON.parse(a.tags || "[]");
51938
52005
  }
51939
52006
  catch (e) {
51940
- await queryExecutor.close().catch(() => undefined);
51941
- throw new ErrorWithCause(`Could not connect to MySQL at ${credentials.host}:${credentials.port || 3306} ` +
51942
- `as "${credentials.user}": ${e.message}`, e);
52007
+ tags = [];
51943
52008
  }
51944
- return new MySqlDbAdapter(queryExecutor);
52009
+ return {
52010
+ readable: a.readable_name,
52011
+ type: a.type,
52012
+ tags,
52013
+ description: a.description || "",
52014
+ status: statusByModel.get(a.readable_name),
52015
+ dependsOn: dependsOn.get(a.readable_name) || []
52016
+ };
52017
+ });
52018
+ return {
52019
+ generatedAt,
52020
+ summary: {
52021
+ total: models.length,
52022
+ byType: Array.from(byTypeMap.entries())
52023
+ .map(([type, n]) => ({ type, n }))
52024
+ .sort((x, y) => x.type.localeCompare(y.type))
52025
+ },
52026
+ latestRun,
52027
+ models,
52028
+ columns: columns.map(c => ({
52029
+ readable: c.readable_name,
52030
+ column: c.column_name,
52031
+ description: c.description || ""
52032
+ }))
52033
+ };
52034
+ }
52035
+ function renderDocsHtml(model) {
52036
+ const summaryLine = `${model.summary.total} models — ` +
52037
+ model.summary.byType.map(t => `${t.n} ${t.type}`).join(", ");
52038
+ const runLine = model.latestRun
52039
+ ? `Last run: <strong>${escapeHtml(model.latestRun.status)}</strong> (run ${model.latestRun.runId})`
52040
+ : "No runs recorded yet.";
52041
+ const statusBadge = (status) => {
52042
+ if (!status) {
52043
+ return "";
52044
+ }
52045
+ const cls = status === "SUCCESSFUL" ? "ok" : status === "FAILED" ? "fail" : "muted";
52046
+ return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
52047
+ };
52048
+ const modelRows = model.models
52049
+ .map(m => `<tr data-search="${escapeHtml((m.readable + " " + m.type + " " + m.tags.join(" ")).toLowerCase())}">
52050
+ <td><code>${escapeHtml(m.readable)}</code></td>
52051
+ <td>${escapeHtml(m.type)}</td>
52052
+ <td>${m.tags.map(t => `<span class="tag">${escapeHtml(t)}</span>`).join(" ")}</td>
52053
+ <td>${statusBadge(m.status)}</td>
52054
+ <td>${m.dependsOn.map(d => `<code>${escapeHtml(d)}</code>`).join("<br>")}</td>
52055
+ <td>${escapeHtml(m.description)}</td>
52056
+ </tr>`)
52057
+ .join("\n");
52058
+ const columnRows = model.columns
52059
+ .map(c => `<tr><td><code>${escapeHtml(c.readable)}</code></td><td><code>${escapeHtml(c.column)}</code></td><td>${escapeHtml(c.description)}</td></tr>`)
52060
+ .join("\n");
52061
+ return `<!doctype html>
52062
+ <html lang="en">
52063
+ <head>
52064
+ <meta charset="utf-8">
52065
+ <meta name="viewport" content="width=device-width, initial-scale=1">
52066
+ <title>SQLAnvil catalog</title>
52067
+ <style>
52068
+ :root { color-scheme: light dark; }
52069
+ body { font: 14px/1.5 -apple-system, system-ui, sans-serif; margin: 2rem; max-width: 1100px; }
52070
+ h1 { margin: 0 0 .25rem; }
52071
+ .meta { color: #888; margin-bottom: 1.5rem; }
52072
+ table { border-collapse: collapse; width: 100%; margin: 1rem 0 2rem; }
52073
+ th, td { text-align: left; padding: .4rem .6rem; border-bottom: 1px solid #8884; vertical-align: top; }
52074
+ th { font-weight: 600; }
52075
+ code { font-size: 12px; }
52076
+ .tag { background: #8883; border-radius: 4px; padding: 0 .35rem; font-size: 12px; }
52077
+ .badge { border-radius: 4px; padding: 0 .4rem; font-size: 12px; font-weight: 600; }
52078
+ .badge.ok { background: #1a7f37; color: #fff; }
52079
+ .badge.fail { background: #b62324; color: #fff; }
52080
+ .badge.muted { background: #8884; }
52081
+ #q { padding: .4rem .6rem; width: 320px; max-width: 100%; margin-bottom: .5rem; }
52082
+ </style>
52083
+ </head>
52084
+ <body>
52085
+ <h1>SQLAnvil catalog</h1>
52086
+ <div class="meta">${escapeHtml(summaryLine)} &middot; ${runLine} &middot; generated ${escapeHtml(model.generatedAt)}</div>
52087
+
52088
+ <input id="q" type="search" placeholder="Filter models…" oninput="filterModels(this.value)">
52089
+ <table id="models">
52090
+ <thead><tr><th>Model</th><th>Type</th><th>Tags</th><th>Last run</th><th>Depends on</th><th>Description</th></tr></thead>
52091
+ <tbody>
52092
+ ${modelRows}
52093
+ </tbody>
52094
+ </table>
52095
+
52096
+ <h2>Columns</h2>
52097
+ <table>
52098
+ <thead><tr><th>Model</th><th>Column</th><th>Description</th></tr></thead>
52099
+ <tbody>
52100
+ ${columnRows || '<tr><td colspan="3" class="meta">No documented columns.</td></tr>'}
52101
+ </tbody>
52102
+ </table>
52103
+
52104
+ <script>
52105
+ function filterModels(q) {
52106
+ q = q.toLowerCase();
52107
+ for (const tr of document.querySelectorAll('#models tbody tr')) {
52108
+ tr.style.display = tr.getAttribute('data-search').includes(q) ? '' : 'none';
51945
52109
  }
51946
- async execute(statement, options = { rowLimit: 1000, byteLimit: 1024 * 1024 }) {
51947
- return await this.withClientLock(client => client.execute(statement, options));
52110
+ }
52111
+ </script>
52112
+ </body>
52113
+ </html>
52114
+ `;
52115
+ }
52116
+
52117
+ function targetKey(target) {
52118
+ return [target.database, target.schema, target.name].filter(Boolean).join(".");
52119
+ }
52120
+ function topoOrder(nodes) {
52121
+ const byKey = new Map(nodes.map(n => [n.key, n]));
52122
+ const indegree = new Map();
52123
+ const dependents = new Map();
52124
+ for (const n of nodes) {
52125
+ const inGraphDeps = Array.from(new Set(n.dependencyKeys.filter(d => byKey.has(d) && d !== n.key)));
52126
+ indegree.set(n.key, inGraphDeps.length);
52127
+ for (const dep of inGraphDeps) {
52128
+ dependents.set(dep, (dependents.get(dep) || []).concat(n.key));
52129
+ }
51948
52130
  }
51949
- async executeRaw(statement, options = { rowLimit: 1000 }) {
51950
- const result = await this.execute(statement, options);
51951
- return Object.assign(Object.assign({}, result), { schema: [] });
52131
+ const ready = nodes
52132
+ .filter(n => (indegree.get(n.key) || 0) === 0)
52133
+ .map(n => n.key)
52134
+ .sort();
52135
+ const ordered = [];
52136
+ const emitted = new Set();
52137
+ while (ready.length) {
52138
+ const key = ready.shift();
52139
+ if (emitted.has(key)) {
52140
+ continue;
52141
+ }
52142
+ emitted.add(key);
52143
+ ordered.push(byKey.get(key));
52144
+ for (const dep of (dependents.get(key) || []).slice().sort()) {
52145
+ indegree.set(dep, (indegree.get(dep) || 0) - 1);
52146
+ if ((indegree.get(dep) || 0) <= 0 && !emitted.has(dep)) {
52147
+ ready.push(dep);
52148
+ ready.sort();
52149
+ }
52150
+ }
51952
52151
  }
51953
- async withClientLock(callback) {
51954
- return await this.queryExecutor.withClientLock(client => callback({
51955
- execute: async (stmt, opts = { rowLimit: 1000, byteLimit: 1024 * 1024 }) => {
51956
- try {
51957
- const rows = await client.execute(stmt, { params: opts.params, rowLimit: opts.rowLimit });
51958
- return { rows, metadata: {} };
51959
- }
51960
- catch (e) {
51961
- if (opts.includeQueryInError) {
51962
- throw new Error(`Error encountered while running "${stmt}": ${e.message}`);
51963
- }
51964
- throw new ErrorWithCause(`Error executing mysql query: ${e.message}`, e);
51965
- }
51966
- },
51967
- executeRaw: async (stmt, opts = { rowLimit: 1000 }) => {
51968
- const positional = opts.params ? Object.values(opts.params) : undefined;
51969
- const rows = await client.execute(stmt, { params: positional, rowLimit: opts.rowLimit });
51970
- return { rows, schema: [], metadata: {} };
52152
+ if (ordered.length < nodes.length) {
52153
+ for (const n of nodes.slice().sort((a, b) => a.key.localeCompare(b.key))) {
52154
+ if (!emitted.has(n.key)) {
52155
+ emitted.add(n.key);
52156
+ ordered.push(n);
51971
52157
  }
51972
- }));
52158
+ }
51973
52159
  }
51974
- async evaluate(queryOrAction) {
51975
- const validationQueries = collectEvaluationQueries(queryOrAction, false, (query) => !!query ? `explain ${query}` : "").map((validationQuery, index) => ({ index, validationQuery }));
51976
- const validationQueriesWithoutWrappers = collectEvaluationQueries(queryOrAction, false);
51977
- const queryEvaluations = new Array();
51978
- for (const { index, validationQuery } of validationQueries) {
51979
- let evaluationResponse = {
51980
- status: sqlanvil.QueryEvaluation.QueryEvaluationStatus.SUCCESS
51981
- };
52160
+ return ordered;
52161
+ }
52162
+ function dependencyBlocked(dependencyKeys, statusByKey) {
52163
+ return dependencyKeys.some(dep => {
52164
+ const status = statusByKey.get(dep);
52165
+ return status !== undefined && status !== "PASS";
52166
+ });
52167
+ }
52168
+ const VALIDATE_SHADOW_PREFIX = "sqlanvil_validate_";
52169
+ function validateShadowSuffix(nowMs) {
52170
+ return `${VALIDATE_SHADOW_PREFIX}${nowMs}`;
52171
+ }
52172
+ function parseShadowTimestamp(schemaName) {
52173
+ const match = schemaName.match(/sqlanvil_validate_(\d+)/);
52174
+ return match ? Number(match[1]) : null;
52175
+ }
52176
+ function shadowSchemasToSweep(schemaNames, nowMs, maxAgeMs) {
52177
+ return schemaNames.filter(name => {
52178
+ const ts = parseShadowTimestamp(name);
52179
+ return ts !== null && nowMs - ts > maxAgeMs;
52180
+ });
52181
+ }
52182
+
52183
+ const SHADOW_MAX_AGE_MS = 60 * 60 * 1000;
52184
+ async function sweepOrphanShadows(deps, nowMs, maxAgeMs = SHADOW_MAX_AGE_MS) {
52185
+ try {
52186
+ const schemas = await deps.listSchemas();
52187
+ for (const schema of shadowSchemasToSweep(schemas, nowMs, maxAgeMs)) {
51982
52188
  try {
51983
- await this.execute(validationQuery.query);
52189
+ await deps.execute(deps.dropSchemaCascadeSql(schema));
51984
52190
  }
51985
52191
  catch (e) {
51986
- evaluationResponse = {
51987
- status: sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE,
51988
- error: sqlanvil.QueryEvaluationError.create({
51989
- message: (e === null || e === void 0 ? void 0 : e.message) ? String(e.message) : String(e)
51990
- })
51991
- };
51992
52192
  }
51993
- queryEvaluations.push(sqlanvil.QueryEvaluation.create(Object.assign(Object.assign({}, evaluationResponse), { incremental: validationQuery.incremental, query: validationQueriesWithoutWrappers[index].query })));
51994
- }
51995
- return queryEvaluations;
51996
- }
51997
- async tables(_database, schema) {
51998
- const params = [];
51999
- let schemaClause = "";
52000
- if (schema) {
52001
- schemaClause = "and table_schema = ?";
52002
- params.push(schema);
52003
52193
  }
52004
- const queryResult = await this.execute(`select table_name, table_schema
52005
- from information_schema.tables
52006
- where table_schema not in ('information_schema', 'mysql', 'performance_schema', 'sys')
52007
- ${schemaClause}`, { params, rowLimit: 10000, includeQueryInError: true });
52008
- const targets = queryResult.rows.map(row => ({
52009
- schema: row.table_schema,
52010
- name: row.table_name
52011
- }));
52012
- return await Promise.all(targets.map(target => this.table(target)));
52013
52194
  }
52014
- async search(searchText, options = { limit: 1000 }) {
52015
- const results = await this.execute(`select tables.table_schema as table_schema, tables.table_name as table_name
52016
- from information_schema.tables as tables
52017
- left join information_schema.columns columns
52018
- on tables.table_schema = columns.table_schema
52019
- and tables.table_name = columns.table_name
52020
- where tables.table_schema like ?
52021
- or tables.table_name like ?
52022
- or columns.column_name like ?
52023
- group by 1, 2`, {
52024
- params: [`%${searchText}%`, `%${searchText}%`, `%${searchText}%`],
52025
- rowLimit: options.limit
52026
- });
52027
- return await Promise.all(results.rows.map(row => this.table({
52028
- schema: row.table_schema,
52029
- name: row.table_name
52030
- })));
52195
+ catch (e) {
52031
52196
  }
52032
- async table(target) {
52033
- var _a, _b, _c;
52034
- const params = [target.schema, target.name];
52035
- const [tableResults, columnResults] = await Promise.all([
52036
- this.execute(`select table_type, table_comment from information_schema.tables
52037
- where table_schema = ? and table_name = ?`, { params, includeQueryInError: true }),
52038
- this.execute(`select column_name, data_type, ordinal_position, column_comment
52039
- from information_schema.columns
52040
- where table_schema = ? and table_name = ?
52041
- order by ordinal_position`, { params, includeQueryInError: true })
52042
- ]);
52043
- if (tableResults.rows.length === 0) {
52044
- return null;
52045
- }
52046
- const tableType = String((_a = tableResults.rows[0].table_type) !== null && _a !== void 0 ? _a : tableResults.rows[0].TABLE_TYPE).toUpperCase();
52047
- const tableComment = String((_c = (_b = tableResults.rows[0].table_comment) !== null && _b !== void 0 ? _b : tableResults.rows[0].TABLE_COMMENT) !== null && _c !== void 0 ? _c : "");
52048
- return sqlanvil.TableMetadata.create({
52049
- target,
52050
- type: tableType === "VIEW" ? sqlanvil.TableMetadata.Type.VIEW : sqlanvil.TableMetadata.Type.TABLE,
52051
- description: tableComment || undefined,
52052
- fields: columnResults.rows.map(row => {
52053
- var _a, _b, _c, _d;
52054
- const comment = String((_b = (_a = row.column_comment) !== null && _a !== void 0 ? _a : row.COLUMN_COMMENT) !== null && _b !== void 0 ? _b : "");
52055
- return sqlanvil.Field.create({
52056
- name: ((_c = row.column_name) !== null && _c !== void 0 ? _c : row.COLUMN_NAME),
52057
- primitive: convertFieldType(((_d = row.data_type) !== null && _d !== void 0 ? _d : row.DATA_TYPE)),
52058
- description: comment || undefined
52059
- });
52060
- })
52061
- });
52197
+ }
52198
+ function tableType(enumType) {
52199
+ switch (enumType) {
52200
+ case sqlanvil.TableType.VIEW:
52201
+ return "view";
52202
+ case sqlanvil.TableType.INCREMENTAL:
52203
+ return "incremental";
52204
+ default:
52205
+ return "table";
52062
52206
  }
52063
- async deleteTable(target) {
52064
- const metadata = await this.table(target);
52065
- if (!metadata) {
52066
- return;
52067
- }
52068
- const kind = metadata.type === sqlanvil.TableMetadata.Type.VIEW ? "view" : "table";
52069
- await this.execute(`drop ${kind} if exists \`${target.schema}\`.\`${target.name}\``, {
52070
- includeQueryInError: true
52207
+ }
52208
+ function depKeys(deps) {
52209
+ return (deps || []).map(targetKey);
52210
+ }
52211
+ async function validate(compiledGraph, deps, options = {}) {
52212
+ const nodes = [];
52213
+ for (const table of compiledGraph.tables || []) {
52214
+ nodes.push({
52215
+ key: targetKey(table.target),
52216
+ dependencyKeys: depKeys(table.dependencyTargets),
52217
+ kind: "table",
52218
+ type: tableType(table.enumType),
52219
+ target: table.target,
52220
+ table,
52221
+ action: table
52071
52222
  });
52072
52223
  }
52073
- async schemas(_database) {
52074
- const result = await this.execute(`select schema_name from information_schema.schemata`, {
52075
- includeQueryInError: true
52224
+ for (const assertion of compiledGraph.assertions || []) {
52225
+ nodes.push({
52226
+ key: targetKey(assertion.target),
52227
+ dependencyKeys: depKeys(assertion.dependencyTargets),
52228
+ kind: "assertion",
52229
+ type: "assertion",
52230
+ target: assertion.target,
52231
+ action: assertion
52076
52232
  });
52077
- return result.rows
52078
- .map(row => { var _a; return ((_a = row.schema_name) !== null && _a !== void 0 ? _a : row.SCHEMA_NAME); })
52079
- .filter(name => !INTERNAL_SCHEMAS.has(name));
52080
52233
  }
52081
- async createSchema(_database, schema) {
52082
- await this.execute(`create database if not exists \`${schema}\``, {
52083
- includeQueryInError: true
52234
+ for (const operation of compiledGraph.operations || []) {
52235
+ if (!operation.target) {
52236
+ continue;
52237
+ }
52238
+ nodes.push({
52239
+ key: targetKey(operation.target),
52240
+ dependencyKeys: depKeys(operation.dependencyTargets),
52241
+ kind: "operation",
52242
+ type: "operation",
52243
+ target: operation.target,
52244
+ action: undefined
52084
52245
  });
52085
52246
  }
52086
- async setMetadata(action) {
52087
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
52088
- const { target, actionDescriptor } = action;
52089
- const actualMetadata = await this.table(target);
52090
- if (!actualMetadata) {
52091
- return;
52092
- }
52093
- if (actualMetadata.type === sqlanvil.TableMetadata.Type.VIEW) {
52094
- return;
52095
- }
52096
- const resolved = `\`${target.schema}\`.\`${target.name}\``;
52097
- if (actionDescriptor === null || actionDescriptor === void 0 ? void 0 : actionDescriptor.description) {
52098
- await this.execute(`alter table ${resolved} comment = '${escapeMysqlString(actionDescriptor.description)}'`, { includeQueryInError: true });
52247
+ for (const imp of compiledGraph.imports || []) {
52248
+ if (!imp.target) {
52249
+ continue;
52099
52250
  }
52100
- const columnComments = ((actionDescriptor === null || actionDescriptor === void 0 ? void 0 : actionDescriptor.columns) || []).filter(column => {
52101
- var _a;
52102
- return ((_a = column.path) === null || _a === void 0 ? void 0 : _a.length) === 1 &&
52103
- column.description != null &&
52104
- actualMetadata.fields.some(f => f.name === column.path[0]);
52251
+ nodes.push({
52252
+ key: targetKey(imp.target),
52253
+ dependencyKeys: depKeys(imp.dependencyTargets),
52254
+ kind: "import",
52255
+ type: "import",
52256
+ target: imp.target,
52257
+ action: undefined
52105
52258
  });
52106
- if (columnComments.length === 0) {
52107
- return;
52259
+ }
52260
+ const ordered = topoOrder(nodes);
52261
+ const shadowSchemas = Array.from(new Set(nodes.filter(n => n.kind === "table").map(n => n.target.schema)));
52262
+ const statusByKey = new Map();
52263
+ const results = [];
52264
+ try {
52265
+ for (const schema of shadowSchemas) {
52266
+ await deps.execute(deps.createSchemaSql(schema));
52108
52267
  }
52109
- const defResult = await this.execute(`select column_name, column_type, is_nullable, column_default, extra,
52110
- collation_name, generation_expression
52111
- from information_schema.columns
52112
- where table_schema = ? and table_name = ?`, { params: [target.schema, target.name], includeQueryInError: true });
52113
- const defByName = new Map();
52114
- defResult.rows.forEach(row => { var _a; return defByName.set(String((_a = row.column_name) !== null && _a !== void 0 ? _a : row.COLUMN_NAME), row); });
52115
- for (const column of columnComments) {
52116
- const name = column.path[0];
52117
- const row = defByName.get(name);
52118
- if (!row) {
52268
+ for (const node of ordered) {
52269
+ if (node.kind === "operation" || node.kind === "import") {
52270
+ statusByKey.set(node.key, "SKIPPED");
52271
+ results.push({ target: node.target, type: node.type, status: "SKIPPED", errors: [] });
52119
52272
  continue;
52120
52273
  }
52121
- const def = reconstructColumnDef({
52122
- columnType: String((_a = row.column_type) !== null && _a !== void 0 ? _a : row.COLUMN_TYPE),
52123
- isNullable: String((_b = row.is_nullable) !== null && _b !== void 0 ? _b : row.IS_NULLABLE),
52124
- columnDefault: ((_d = (_c = row.column_default) !== null && _c !== void 0 ? _c : row.COLUMN_DEFAULT) !== null && _d !== void 0 ? _d : null) === null
52125
- ? null
52126
- : String((_e = row.column_default) !== null && _e !== void 0 ? _e : row.COLUMN_DEFAULT),
52127
- extra: String((_g = (_f = row.extra) !== null && _f !== void 0 ? _f : row.EXTRA) !== null && _g !== void 0 ? _g : ""),
52128
- collationName: ((_j = (_h = row.collation_name) !== null && _h !== void 0 ? _h : row.COLLATION_NAME) !== null && _j !== void 0 ? _j : null) === null
52129
- ? null
52130
- : String((_k = row.collation_name) !== null && _k !== void 0 ? _k : row.COLLATION_NAME),
52131
- generationExpression: ((_m = (_l = row.generation_expression) !== null && _l !== void 0 ? _l : row.GENERATION_EXPRESSION) !== null && _m !== void 0 ? _m : null) === null
52132
- ? null
52133
- : String((_o = row.generation_expression) !== null && _o !== void 0 ? _o : row.GENERATION_EXPRESSION)
52134
- });
52135
- await this.execute(`alter table ${resolved} modify column \`${name}\` ${def} comment '${escapeMysqlString(column.description)}'`, { includeQueryInError: true });
52274
+ if (dependencyBlocked(node.dependencyKeys, statusByKey)) {
52275
+ statusByKey.set(node.key, "BLOCKED");
52276
+ results.push({ target: node.target, type: node.type, status: "BLOCKED", errors: [] });
52277
+ continue;
52278
+ }
52279
+ const evaluations = await deps.evaluate(node.action);
52280
+ const failed = evaluations.some(e => e.status === sqlanvil.QueryEvaluation.QueryEvaluationStatus.FAILURE);
52281
+ const status = failed ? "FAILURE" : "PASS";
52282
+ statusByKey.set(node.key, status);
52283
+ results.push({ target: node.target, type: node.type, status, errors: evaluations });
52284
+ if (status === "PASS" && node.kind === "table") {
52285
+ try {
52286
+ await deps.execute(deps.validationStubSql(node.table));
52287
+ }
52288
+ catch (e) {
52289
+ }
52290
+ }
52136
52291
  }
52137
52292
  }
52138
- async close() {
52139
- await this.queryExecutor.close();
52293
+ finally {
52294
+ if (!options.keepShadow) {
52295
+ for (const schema of shadowSchemas.slice().reverse()) {
52296
+ try {
52297
+ await deps.execute(deps.dropSchemaCascadeSql(schema));
52298
+ }
52299
+ catch (e) {
52300
+ }
52301
+ }
52302
+ }
52140
52303
  }
52304
+ return results;
52141
52305
  }
52142
52306
 
52143
52307
  class SupabaseDbAdapter extends PostgresDbAdapter {