@noormdev/sdk 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OMY6CKR4.js → chunk-24L5UNQR.js} +3 -3
- package/dist/{chunk-A2OYZAVT.js → chunk-6LG4ENOJ.js} +2 -2
- package/dist/{chunk-CDTFXLXU.js → chunk-M7Q44VFJ.js} +4 -4
- package/dist/{engine-6ALQ5YTN.js → engine-P74UTJQO.js} +4 -4
- package/dist/index.d.ts +48 -1
- package/dist/index.js +675 -484
- package/dist/{storage-V6BHWNS3.js → storage-NQA4H7PK.js} +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
|
-
import { processFile, es_default, DtReader, isTemplate, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, ENCODED_TYPES } from './chunk-
|
|
2
|
+
import { processFile, es_default, DtReader, isTemplate, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, ENCODED_TYPES } from './chunk-M7Q44VFJ.js';
|
|
3
3
|
import './chunk-UOFJT5PY.js';
|
|
4
|
-
import { resolveLegacyAccess, checkConfigPolicy, initializeVault, getVaultStatus, setVaultSecret, getVaultSecret, getAllVaultSecrets, listVaultSecretKeys, deleteVaultSecret, vaultSecretExists, getVaultKey, noormDb, getNoormTables, encryptVaultKey, assertVaultPolicy, assertPolicy, detectAgentHarness, guarded } from './chunk-
|
|
5
|
-
import { observer, getEnvConfigName, ObserverRelay, isDebug } from './chunk-
|
|
6
|
-
export { observer as noormObserver } from './chunk-
|
|
4
|
+
import { resolveLegacyAccess, checkConfigPolicy, initializeVault, getVaultStatus, setVaultSecret, getVaultSecret, getAllVaultSecrets, listVaultSecretKeys, deleteVaultSecret, vaultSecretExists, getVaultKey, noormDb, getNoormTables, encryptVaultKey, assertVaultPolicy, assertPolicy, detectAgentHarness, guarded } from './chunk-24L5UNQR.js';
|
|
5
|
+
import { observer, getEnvConfigName, ObserverRelay, isDebug } from './chunk-6LG4ENOJ.js';
|
|
6
|
+
export { observer as noormObserver } from './chunk-6LG4ENOJ.js';
|
|
7
7
|
import { explainConnectionError, DatabaseConnectionError } from './chunk-OXTXFKWJ.js';
|
|
8
8
|
import { makeNestedConfig, gigabytes, attempt, attemptSync, clone, merge, equals, retry, runWithTimeout, isTimeoutError, wait } from './chunk-TXDGFA44.js';
|
|
9
9
|
import { external_exports, PortSchema, connectTimeoutFor, DEFAULT_PORTS } from './chunk-WE2JLJMT.js';
|
|
@@ -4786,6 +4786,33 @@ function hydrateDate(value, dialect) {
|
|
|
4786
4786
|
}
|
|
4787
4787
|
return /* @__PURE__ */ new Date(`${value.replace(" ", "T")}Z`);
|
|
4788
4788
|
}
|
|
4789
|
+
function decideNeedsRunFile(record, checksum, force) {
|
|
4790
|
+
if (force) {
|
|
4791
|
+
return { needsRun: true, reason: "force" };
|
|
4792
|
+
}
|
|
4793
|
+
if (record instanceof Error || !record) {
|
|
4794
|
+
return { needsRun: true, reason: "new" };
|
|
4795
|
+
}
|
|
4796
|
+
if (record.exec_status === "failed") {
|
|
4797
|
+
return {
|
|
4798
|
+
needsRun: true,
|
|
4799
|
+
reason: "failed",
|
|
4800
|
+
previousChecksum: record.checksum
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
if (record.checksum !== checksum) {
|
|
4804
|
+
return {
|
|
4805
|
+
needsRun: true,
|
|
4806
|
+
reason: "changed",
|
|
4807
|
+
previousChecksum: record.checksum
|
|
4808
|
+
};
|
|
4809
|
+
}
|
|
4810
|
+
return {
|
|
4811
|
+
needsRun: false,
|
|
4812
|
+
skipReason: "already applied",
|
|
4813
|
+
previousChecksum: record.checksum
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4789
4816
|
var ChangeHistory = class {
|
|
4790
4817
|
#db;
|
|
4791
4818
|
#ndb;
|
|
@@ -4965,31 +4992,19 @@ var ChangeHistory = class {
|
|
|
4965
4992
|
};
|
|
4966
4993
|
}
|
|
4967
4994
|
/**
|
|
4968
|
-
*
|
|
4969
|
-
*
|
|
4970
|
-
*
|
|
4971
|
-
*
|
|
4972
|
-
*
|
|
4973
|
-
*
|
|
4974
|
-
*
|
|
4975
|
-
*
|
|
4976
|
-
*
|
|
4977
|
-
*
|
|
4978
|
-
*
|
|
4979
|
-
*
|
|
4980
|
-
*
|
|
4981
|
-
* Also excludes `skipped` rows: `status: 'skipped'` is written for two
|
|
4982
|
-
* different meanings — `skipRemainingFiles` writes it for files never
|
|
4983
|
-
* reached after an earlier failure (must re-run), while this method's
|
|
4984
|
-
* own per-file-skip path (called from `executor.ts`) writes it for a
|
|
4985
|
-
* file that matched a prior success (must stay skipped). A `skipped`
|
|
4986
|
-
* row is never itself a decision basis: a success-match skip still has
|
|
4987
|
-
* its covering `success` row further back in history (found once the
|
|
4988
|
-
* `skipped` row is excluded), and a never-reached skip has no terminal
|
|
4989
|
-
* row at all, so the lookup falls through to `{ needsRun: true, reason:
|
|
4990
|
-
* 'new' }` below and the file runs. Both resolve correctly without
|
|
4991
|
-
* consulting the ambiguous row — excluding it here is what prevents a
|
|
4992
|
-
* third attempt from re-running a file a prior success already covered.
|
|
4995
|
+
* Fetch the newest qualifying execution row per filepath for one
|
|
4996
|
+
* change and direction.
|
|
4997
|
+
*
|
|
4998
|
+
* Two SELECTs for any number of files: the direction boundary, then the
|
|
4999
|
+
* same query form as `Tracker.latestExecutions`.
|
|
5000
|
+
*
|
|
5001
|
+
* Rows are scoped to this change's name+direction+config, so file A's
|
|
5002
|
+
* success under one change never satisfies file A's check under another.
|
|
5003
|
+
*
|
|
5004
|
+
* `pending` and `skipped` rows never count. Neither records an outcome:
|
|
5005
|
+
* `pending` is a file its batch never reached, and `skipped` is written by
|
|
5006
|
+
* `skipRemainingFiles` for the same reason, or by older versions for a
|
|
5007
|
+
* file that matched a success still further back in history.
|
|
4993
5008
|
*
|
|
4994
5009
|
* A prior success only licenses a skip while it is still *standing*.
|
|
4995
5010
|
* Two things retire it, and neither is visible on the execution row
|
|
@@ -5010,72 +5025,56 @@ var ChangeHistory = class {
|
|
|
5010
5025
|
*
|
|
5011
5026
|
* @param name - Change name
|
|
5012
5027
|
* @param direction - 'change' or 'revert'
|
|
5013
|
-
* @
|
|
5014
|
-
*
|
|
5015
|
-
* @
|
|
5016
|
-
*
|
|
5028
|
+
* @returns Map of relative filepath to its newest qualifying row, or the lookup error
|
|
5029
|
+
*
|
|
5030
|
+
* @example
|
|
5031
|
+
* ```typescript
|
|
5032
|
+
* const [latest, err] = await history.latestFileExecutions('2024-01-15-add-users', 'change')
|
|
5033
|
+
* const result = decideNeedsRunFile(err ?? latest.get(filepath), checksum, false)
|
|
5034
|
+
* ```
|
|
5017
5035
|
*/
|
|
5018
|
-
async
|
|
5019
|
-
|
|
5020
|
-
return { needsRun: true, reason: "force" };
|
|
5021
|
-
}
|
|
5036
|
+
async latestFileExecutions(name, direction) {
|
|
5037
|
+
const { executions, change } = this.#tables;
|
|
5022
5038
|
const opposite = direction === "change" ? "revert" : "change";
|
|
5023
5039
|
const [boundary, boundaryErr] = await attempt(
|
|
5024
|
-
() => this.#ndb.selectFrom(
|
|
5040
|
+
() => this.#ndb.selectFrom(change).select(["id"]).where("name", "=", name).where("change_type", "=", "change").where("direction", "=", opposite).where("config_name", "=", this.#configName).orderBy("id", "desc").limit(1).executeTakeFirst()
|
|
5025
5041
|
);
|
|
5026
5042
|
if (boundaryErr) {
|
|
5027
5043
|
observer.emit("error", {
|
|
5028
5044
|
source: "change",
|
|
5029
5045
|
error: boundaryErr,
|
|
5030
|
-
context: { name,
|
|
5046
|
+
context: { name, operation: "latest-file-executions-boundary" }
|
|
5031
5047
|
});
|
|
5032
|
-
return
|
|
5048
|
+
return [null, boundaryErr];
|
|
5033
5049
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5050
|
+
let qualifying = this.#ndb.selectFrom(executions).innerJoin(change, `${change}.id`, `${executions}.change_id`).where(`${change}.name`, "=", name).where(`${change}.direction`, "=", direction).where(`${change}.config_name`, "=", this.#configName).where(`${change}.status`, "not in", ["reverted", "stale"]).where(`${executions}.status`, "not in", ["pending", "skipped"]);
|
|
5051
|
+
if (boundary) {
|
|
5052
|
+
qualifying = qualifying.where(`${change}.id`, ">", boundary.id);
|
|
5053
|
+
}
|
|
5054
|
+
const newestIds = qualifying.select((eb) => eb.fn.max(`${executions}.id`).as("id")).groupBy(`${executions}.filepath`).as("newest");
|
|
5055
|
+
const [rows, err] = await attempt(
|
|
5056
|
+
() => (
|
|
5039
5057
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
return query.orderBy(`${this.#tables.executions}.id`, "desc").limit(1).executeTakeFirst();
|
|
5048
|
-
});
|
|
5058
|
+
this.#ndb.selectFrom(executions).innerJoin(newestIds, "newest.id", `${executions}.id`).select((eb) => [
|
|
5059
|
+
eb.ref(`${executions}.filepath`).as("filepath"),
|
|
5060
|
+
eb.ref(`${executions}.checksum`).as("checksum"),
|
|
5061
|
+
eb.ref(`${executions}.status`).as("exec_status")
|
|
5062
|
+
]).execute()
|
|
5063
|
+
)
|
|
5064
|
+
);
|
|
5049
5065
|
if (err) {
|
|
5050
5066
|
observer.emit("error", {
|
|
5051
5067
|
source: "change",
|
|
5052
5068
|
error: err,
|
|
5053
|
-
context: { name,
|
|
5069
|
+
context: { name, operation: "latest-file-executions" }
|
|
5054
5070
|
});
|
|
5055
|
-
return
|
|
5056
|
-
}
|
|
5057
|
-
if (!record) {
|
|
5058
|
-
return { needsRun: true, reason: "new" };
|
|
5059
|
-
}
|
|
5060
|
-
if (record.exec_status === "failed") {
|
|
5061
|
-
return {
|
|
5062
|
-
needsRun: true,
|
|
5063
|
-
reason: "failed",
|
|
5064
|
-
previousChecksum: record.checksum
|
|
5065
|
-
};
|
|
5071
|
+
return [null, err];
|
|
5066
5072
|
}
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
reason: "changed",
|
|
5071
|
-
previousChecksum: record.checksum
|
|
5072
|
-
};
|
|
5073
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5074
|
+
for (const { filepath, ...record } of rows) {
|
|
5075
|
+
latest.set(filepath, record);
|
|
5073
5076
|
}
|
|
5074
|
-
return
|
|
5075
|
-
needsRun: false,
|
|
5076
|
-
skipReason: "already applied",
|
|
5077
|
-
previousChecksum: record.checksum
|
|
5078
|
-
};
|
|
5077
|
+
return [latest, null];
|
|
5079
5078
|
}
|
|
5080
5079
|
// ─────────────────────────────────────────────────────────
|
|
5081
5080
|
// Create Records
|
|
@@ -5113,9 +5112,10 @@ var ChangeHistory = class {
|
|
|
5113
5112
|
return id;
|
|
5114
5113
|
}
|
|
5115
5114
|
/**
|
|
5116
|
-
* Create pending file records for
|
|
5115
|
+
* Create pending file records for the files about to run.
|
|
5117
5116
|
*
|
|
5118
|
-
*
|
|
5117
|
+
* The pending rows let `skipRemainingFiles` mark the files a
|
|
5118
|
+
* failure left unreached.
|
|
5119
5119
|
*
|
|
5120
5120
|
* @returns Error message if creation failed, null on success
|
|
5121
5121
|
*/
|
|
@@ -5481,6 +5481,44 @@ var ChangeHistory = class {
|
|
|
5481
5481
|
};
|
|
5482
5482
|
|
|
5483
5483
|
// src/core/runner/tracker.ts
|
|
5484
|
+
function decideNeedsRun(record, checksum, force) {
|
|
5485
|
+
if (record instanceof Error) {
|
|
5486
|
+
return { needsRun: true, reason: "error" };
|
|
5487
|
+
}
|
|
5488
|
+
if (!record) {
|
|
5489
|
+
return { needsRun: true, reason: "new" };
|
|
5490
|
+
}
|
|
5491
|
+
if (record.exec_status === "failed") {
|
|
5492
|
+
return {
|
|
5493
|
+
needsRun: true,
|
|
5494
|
+
reason: "failed",
|
|
5495
|
+
previousChecksum: record.checksum
|
|
5496
|
+
};
|
|
5497
|
+
}
|
|
5498
|
+
const isUnchangedSkip = record.exec_status === "skipped" && record.skip_reason === "unchanged";
|
|
5499
|
+
if (record.exec_status === "pending" || record.exec_status === "skipped" && !isUnchangedSkip) {
|
|
5500
|
+
return { needsRun: true, reason: "new" };
|
|
5501
|
+
}
|
|
5502
|
+
if (record.change_status === "stale") {
|
|
5503
|
+
return {
|
|
5504
|
+
needsRun: true,
|
|
5505
|
+
reason: "stale",
|
|
5506
|
+
previousChecksum: record.checksum
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
if (record.checksum !== checksum) {
|
|
5510
|
+
return {
|
|
5511
|
+
needsRun: true,
|
|
5512
|
+
reason: "changed",
|
|
5513
|
+
previousChecksum: record.checksum
|
|
5514
|
+
};
|
|
5515
|
+
}
|
|
5516
|
+
return {
|
|
5517
|
+
needsRun: false,
|
|
5518
|
+
skipReason: "unchanged",
|
|
5519
|
+
previousChecksum: record.checksum
|
|
5520
|
+
};
|
|
5521
|
+
}
|
|
5484
5522
|
var Tracker = class {
|
|
5485
5523
|
#db;
|
|
5486
5524
|
#ndb;
|
|
@@ -5495,89 +5533,45 @@ var Tracker = class {
|
|
|
5495
5533
|
this.#dialect = dialect;
|
|
5496
5534
|
}
|
|
5497
5535
|
/**
|
|
5498
|
-
*
|
|
5536
|
+
* Fetch the newest execution row per filepath for this config.
|
|
5499
5537
|
*
|
|
5500
|
-
*
|
|
5501
|
-
*
|
|
5502
|
-
* -
|
|
5503
|
-
* - Previous execution failed
|
|
5504
|
-
* - Parent change is stale (schema was torn down)
|
|
5505
|
-
* - Checksum differs (file changed)
|
|
5538
|
+
* One SELECT for any number of files, so a batch can decide every file
|
|
5539
|
+
* with `decideNeedsRun` before writing any tracking rows. Query form:
|
|
5540
|
+
* `docs/design/prefetch-run-gate.md`.
|
|
5506
5541
|
*
|
|
5507
|
-
* @
|
|
5508
|
-
*
|
|
5509
|
-
* @
|
|
5510
|
-
*
|
|
5511
|
-
*
|
|
5512
|
-
*
|
|
5513
|
-
*
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
`${this.#tables.executions}.change_id`
|
|
5527
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5528
|
-
).select((eb) => [
|
|
5529
|
-
eb.ref(`${this.#tables.executions}.checksum`).as("checksum"),
|
|
5530
|
-
eb.ref(`${this.#tables.executions}.status`).as("exec_status"),
|
|
5531
|
-
eb.ref(`${this.#tables.executions}.skip_reason`).as("skip_reason"),
|
|
5532
|
-
eb.ref(`${this.#tables.change}.status`).as("change_status")
|
|
5533
|
-
]).where(`${this.#tables.executions}.filepath`, "=", filepath).where(`${this.#tables.change}.config_name`, "=", this.#configName);
|
|
5534
|
-
if (excludeOperationId !== void 0) {
|
|
5535
|
-
query = query.where(`${this.#tables.executions}.change_id`, "<>", excludeOperationId);
|
|
5536
|
-
}
|
|
5537
|
-
const [record, err] = await attempt(
|
|
5538
|
-
() => query.orderBy(`${this.#tables.executions}.id`, "desc").limit(1).executeTakeFirst()
|
|
5542
|
+
* @returns Map of filepath to its newest record, or the lookup error
|
|
5543
|
+
*
|
|
5544
|
+
* @example
|
|
5545
|
+
* ```typescript
|
|
5546
|
+
* const [latest, err] = await tracker.latestExecutions()
|
|
5547
|
+
* const result = decideNeedsRun(err ?? latest.get(filepath), checksum, false)
|
|
5548
|
+
* ```
|
|
5549
|
+
*/
|
|
5550
|
+
async latestExecutions() {
|
|
5551
|
+
const { executions, change } = this.#tables;
|
|
5552
|
+
const newestIds = this.#ndb.selectFrom(executions).innerJoin(change, `${change}.id`, `${executions}.change_id`).where(`${change}.config_name`, "=", this.#configName).select((eb) => eb.fn.max(`${executions}.id`).as("id")).groupBy(`${executions}.filepath`).as("newest");
|
|
5553
|
+
const [rows, err] = await attempt(
|
|
5554
|
+
() => this.#ndb.selectFrom(executions).innerJoin(change, `${change}.id`, `${executions}.change_id`).innerJoin(newestIds, "newest.id", `${executions}.id`).select((eb) => [
|
|
5555
|
+
eb.ref(`${executions}.filepath`).as("filepath"),
|
|
5556
|
+
eb.ref(`${executions}.checksum`).as("checksum"),
|
|
5557
|
+
eb.ref(`${executions}.status`).as("exec_status"),
|
|
5558
|
+
eb.ref(`${executions}.skip_reason`).as("skip_reason"),
|
|
5559
|
+
eb.ref(`${change}.status`).as("change_status")
|
|
5560
|
+
]).execute()
|
|
5539
5561
|
);
|
|
5540
5562
|
if (err) {
|
|
5541
5563
|
observer.emit("error", {
|
|
5542
5564
|
source: "runner",
|
|
5543
5565
|
error: err,
|
|
5544
|
-
context: {
|
|
5566
|
+
context: { operation: "latest-executions" }
|
|
5545
5567
|
});
|
|
5546
|
-
return
|
|
5547
|
-
}
|
|
5548
|
-
if (!record) {
|
|
5549
|
-
return { needsRun: true, reason: "new" };
|
|
5550
|
-
}
|
|
5551
|
-
if (record.exec_status === "failed") {
|
|
5552
|
-
return {
|
|
5553
|
-
needsRun: true,
|
|
5554
|
-
reason: "failed",
|
|
5555
|
-
previousChecksum: record.checksum
|
|
5556
|
-
};
|
|
5557
|
-
}
|
|
5558
|
-
const isUnchangedSkip = record.exec_status === "skipped" && record.skip_reason === "unchanged";
|
|
5559
|
-
if (record.exec_status === "pending" || record.exec_status === "skipped" && !isUnchangedSkip) {
|
|
5560
|
-
return { needsRun: true, reason: "new" };
|
|
5568
|
+
return [null, err];
|
|
5561
5569
|
}
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
reason: "stale",
|
|
5566
|
-
previousChecksum: record.checksum
|
|
5567
|
-
};
|
|
5570
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5571
|
+
for (const { filepath, ...record } of rows) {
|
|
5572
|
+
latest.set(filepath, record);
|
|
5568
5573
|
}
|
|
5569
|
-
|
|
5570
|
-
return {
|
|
5571
|
-
needsRun: true,
|
|
5572
|
-
reason: "changed",
|
|
5573
|
-
previousChecksum: record.checksum
|
|
5574
|
-
};
|
|
5575
|
-
}
|
|
5576
|
-
return {
|
|
5577
|
-
needsRun: false,
|
|
5578
|
-
skipReason: "unchanged",
|
|
5579
|
-
previousChecksum: record.checksum
|
|
5580
|
-
};
|
|
5574
|
+
return [latest, null];
|
|
5581
5575
|
}
|
|
5582
5576
|
/**
|
|
5583
5577
|
* Create a new operation record.
|
|
@@ -5613,35 +5607,6 @@ var Tracker = class {
|
|
|
5613
5607
|
}
|
|
5614
5608
|
return id;
|
|
5615
5609
|
}
|
|
5616
|
-
/**
|
|
5617
|
-
* Record a file execution.
|
|
5618
|
-
*
|
|
5619
|
-
* Creates a child record in __noorm_executions__ linked
|
|
5620
|
-
* to the parent operation.
|
|
5621
|
-
*
|
|
5622
|
-
* @param data - Execution data
|
|
5623
|
-
*/
|
|
5624
|
-
async recordExecution(data2) {
|
|
5625
|
-
const [, err] = await attempt(
|
|
5626
|
-
() => this.#ndb.insertInto(this.#tables.executions).values({
|
|
5627
|
-
change_id: data2.changeId,
|
|
5628
|
-
filepath: data2.filepath,
|
|
5629
|
-
file_type: "sql",
|
|
5630
|
-
checksum: data2.checksum,
|
|
5631
|
-
status: data2.status,
|
|
5632
|
-
skip_reason: data2.skipReason ?? "",
|
|
5633
|
-
error_message: data2.errorMessage ?? "",
|
|
5634
|
-
duration_ms: Math.round(data2.durationMs ?? 0)
|
|
5635
|
-
}).execute()
|
|
5636
|
-
);
|
|
5637
|
-
if (err) {
|
|
5638
|
-
observer.emit("error", {
|
|
5639
|
-
source: "runner",
|
|
5640
|
-
error: err,
|
|
5641
|
-
context: { filepath: data2.filepath, operation: "record-execution" }
|
|
5642
|
-
});
|
|
5643
|
-
}
|
|
5644
|
-
}
|
|
5645
5610
|
/**
|
|
5646
5611
|
* Finalize an operation.
|
|
5647
5612
|
*
|
|
@@ -5689,10 +5654,10 @@ var Tracker = class {
|
|
|
5689
5654
|
// Batch File Operations (Shared by Runner and Changes)
|
|
5690
5655
|
// ─────────────────────────────────────────────────────────
|
|
5691
5656
|
/**
|
|
5692
|
-
* Create pending file records for
|
|
5657
|
+
* Create pending file records for the files about to run.
|
|
5693
5658
|
*
|
|
5694
|
-
*
|
|
5695
|
-
*
|
|
5659
|
+
* The pending rows let `skipRemainingFiles` mark the files a
|
|
5660
|
+
* failure left unreached.
|
|
5696
5661
|
*
|
|
5697
5662
|
* @param operationId - Parent operation ID
|
|
5698
5663
|
* @param files - Files to create records for
|
|
@@ -5732,23 +5697,15 @@ var Tracker = class {
|
|
|
5732
5697
|
* @param durationMs - Execution time
|
|
5733
5698
|
* @param errorMessage - Error message if failed
|
|
5734
5699
|
* @param skipReason - Skip reason if skipped
|
|
5735
|
-
* @param checksum - Checksum of the SQL that actually reached the
|
|
5736
|
-
* database. `createFileRecords` seeds the pending row with the *raw*
|
|
5737
|
-
* file hash because rendering every template upfront would execute them
|
|
5738
|
-
* twice; for a `.sql.tmpl` that hash is not what `needsRun` compares
|
|
5739
|
-
* against, so leaving it in place made template dedup unreachable.
|
|
5740
|
-
* Omitted leaves the seeded value alone — correct only where no render
|
|
5741
|
-
* happened (e.g. the file could not be read).
|
|
5742
5700
|
* @returns Error message if update failed, null on success
|
|
5743
5701
|
*/
|
|
5744
|
-
async updateFileExecution(operationId, filepath, status, durationMs, errorMessage, skipReason
|
|
5702
|
+
async updateFileExecution(operationId, filepath, status, durationMs, errorMessage, skipReason) {
|
|
5745
5703
|
const [result, err] = await attempt(
|
|
5746
5704
|
() => this.#ndb.updateTable(this.#tables.executions).set({
|
|
5747
5705
|
status,
|
|
5748
5706
|
duration_ms: Math.round(durationMs),
|
|
5749
5707
|
error_message: errorMessage ?? "",
|
|
5750
|
-
skip_reason: skipReason ?? ""
|
|
5751
|
-
...checksum === void 0 ? {} : { checksum }
|
|
5708
|
+
skip_reason: skipReason ?? ""
|
|
5752
5709
|
}).where("change_id", "=", operationId).where("filepath", "=", filepath).executeTakeFirst()
|
|
5753
5710
|
);
|
|
5754
5711
|
if (err) {
|
|
@@ -5842,7 +5799,6 @@ var Tracker = class {
|
|
|
5842
5799
|
/**
|
|
5843
5800
|
* Check if a change needs to run by name.
|
|
5844
5801
|
*
|
|
5845
|
-
* Similar to needsRun but checks by change name instead of filepath.
|
|
5846
5802
|
* Used for change sets where we track by change name, not individual files.
|
|
5847
5803
|
*
|
|
5848
5804
|
* @param name - Change name
|
|
@@ -6019,6 +5975,349 @@ function computeCombinedChecksum(checksums) {
|
|
|
6019
5975
|
const combined = sorted.join("");
|
|
6020
5976
|
return createHash("sha256").update(combined, "utf-8").digest("hex");
|
|
6021
5977
|
}
|
|
5978
|
+
var SESSION_ID_SQL = {
|
|
5979
|
+
postgres: "select pg_backend_pid() as id",
|
|
5980
|
+
mysql: "select connection_id() as id",
|
|
5981
|
+
mssql: "select @@SPID as id"
|
|
5982
|
+
};
|
|
5983
|
+
var SERVER_CANCEL = {
|
|
5984
|
+
postgres: (db, sessionId) => sql`select pg_cancel_backend(${sessionId})`.execute(db),
|
|
5985
|
+
// KILL cannot be prepared, so the id is interpolated. It comes from
|
|
5986
|
+
// connection_id() and is checked by readSessionId before it gets here,
|
|
5987
|
+
// never from user input.
|
|
5988
|
+
mysql: (db, sessionId) => sql.raw(`kill query ${sessionId}`).execute(db)
|
|
5989
|
+
};
|
|
5990
|
+
function readSessionId(rows) {
|
|
5991
|
+
const raw = rows[0]?.id;
|
|
5992
|
+
const id = Number(raw);
|
|
5993
|
+
if (!Number.isInteger(id) || id <= 0) return void 0;
|
|
5994
|
+
return id;
|
|
5995
|
+
}
|
|
5996
|
+
function percentOf(done, total) {
|
|
5997
|
+
if (done === null || total === null || total <= 0) return null;
|
|
5998
|
+
return Math.min(100, done / total * 100);
|
|
5999
|
+
}
|
|
6000
|
+
function toNumber(value) {
|
|
6001
|
+
if (value === null || value === void 0) return null;
|
|
6002
|
+
const n = Number(value);
|
|
6003
|
+
return Number.isFinite(n) ? n : null;
|
|
6004
|
+
}
|
|
6005
|
+
function toText(value) {
|
|
6006
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
6007
|
+
return String(value);
|
|
6008
|
+
}
|
|
6009
|
+
async function rowsOrEmpty(query) {
|
|
6010
|
+
const [result, err] = await attempt(query);
|
|
6011
|
+
return err ? [] : result.rows;
|
|
6012
|
+
}
|
|
6013
|
+
function toBlockers(rows) {
|
|
6014
|
+
return rows.map((row) => ({
|
|
6015
|
+
pid: Number(row.pid),
|
|
6016
|
+
query: toText(row.query),
|
|
6017
|
+
ageMs: toNumber(row.age_ms)
|
|
6018
|
+
}));
|
|
6019
|
+
}
|
|
6020
|
+
var postgresProbe = async (side, pid) => {
|
|
6021
|
+
const [activity] = await rowsOrEmpty(() => sql`
|
|
6022
|
+
select a.state, a.wait_event_type, a.wait_event,
|
|
6023
|
+
pg_blocking_pids(a.pid) as blockers,
|
|
6024
|
+
(select count(*)::int from pg_stat_activity w
|
|
6025
|
+
where w.leader_pid = a.pid and w.pid <> a.pid) as workers
|
|
6026
|
+
from pg_stat_activity a
|
|
6027
|
+
where a.pid = ${pid}
|
|
6028
|
+
`.execute(side));
|
|
6029
|
+
const blockerPids = (activity?.blockers ?? []).map(Number);
|
|
6030
|
+
const blockers = blockerPids.length === 0 ? [] : await rowsOrEmpty(() => sql`
|
|
6031
|
+
select pid, left(query, 120) as query,
|
|
6032
|
+
(extract(epoch from now() - coalesce(xact_start, query_start)) * 1000)::float8 as age_ms
|
|
6033
|
+
from pg_stat_activity
|
|
6034
|
+
where pid = any(${blockerPids}::int[])
|
|
6035
|
+
`.execute(side));
|
|
6036
|
+
const [progress] = await rowsOrEmpty(() => sql`
|
|
6037
|
+
select command as operation, phase,
|
|
6038
|
+
nullif(relid, 0)::regclass::text as relation,
|
|
6039
|
+
(case when tuples_total > 0 then tuples_done else blocks_done end)::float8 as done,
|
|
6040
|
+
(case when tuples_total > 0 then tuples_total else blocks_total end)::float8 as total
|
|
6041
|
+
from pg_stat_progress_create_index where pid = ${pid}
|
|
6042
|
+
union all
|
|
6043
|
+
select 'VACUUM', phase, nullif(relid, 0)::regclass::text,
|
|
6044
|
+
heap_blks_scanned::float8, heap_blks_total::float8
|
|
6045
|
+
from pg_stat_progress_vacuum where pid = ${pid}
|
|
6046
|
+
union all
|
|
6047
|
+
select command, phase, nullif(relid, 0)::regclass::text,
|
|
6048
|
+
heap_blks_scanned::float8, heap_blks_total::float8
|
|
6049
|
+
from pg_stat_progress_cluster where pid = ${pid}
|
|
6050
|
+
union all
|
|
6051
|
+
select command, null, nullif(relid, 0)::regclass::text,
|
|
6052
|
+
bytes_processed::float8, nullif(bytes_total, 0)::float8
|
|
6053
|
+
from pg_stat_progress_copy where pid = ${pid}
|
|
6054
|
+
union all
|
|
6055
|
+
select 'ANALYZE', phase, nullif(relid, 0)::regclass::text,
|
|
6056
|
+
sample_blks_scanned::float8, sample_blks_total::float8
|
|
6057
|
+
from pg_stat_progress_analyze where pid = ${pid}
|
|
6058
|
+
limit 1
|
|
6059
|
+
`.execute(side));
|
|
6060
|
+
const waitEvent = activity?.wait_event ? `${activity.wait_event_type ?? "Wait"}: ${activity.wait_event}` : null;
|
|
6061
|
+
return {
|
|
6062
|
+
state: toText(activity?.state),
|
|
6063
|
+
waitEvent,
|
|
6064
|
+
blockedBy: toBlockers(blockers),
|
|
6065
|
+
workers: toNumber(activity?.workers) ?? 0,
|
|
6066
|
+
progress: progress ? {
|
|
6067
|
+
operation: progress.operation,
|
|
6068
|
+
phase: toText(progress.phase),
|
|
6069
|
+
relation: toText(progress.relation),
|
|
6070
|
+
done: toNumber(progress.done),
|
|
6071
|
+
total: toNumber(progress.total),
|
|
6072
|
+
percent: percentOf(toNumber(progress.done), toNumber(progress.total))
|
|
6073
|
+
} : null
|
|
6074
|
+
};
|
|
6075
|
+
};
|
|
6076
|
+
var mssqlProbe = async (side, spid) => {
|
|
6077
|
+
const [request] = await rowsOrEmpty(() => sql`
|
|
6078
|
+
select status, wait_type, blocking_session_id, percent_complete, command
|
|
6079
|
+
from sys.dm_exec_requests
|
|
6080
|
+
where session_id = ${spid}
|
|
6081
|
+
`.execute(side));
|
|
6082
|
+
const blockerId = toNumber(request?.blocking_session_id);
|
|
6083
|
+
const blockers = !blockerId ? [] : await rowsOrEmpty(() => sql`
|
|
6084
|
+
select s.session_id as pid,
|
|
6085
|
+
coalesce(left(t.text, 120), r.command, s.status) as query,
|
|
6086
|
+
datediff_big(millisecond, coalesce(r.start_time, s.last_request_start_time), sysdatetime()) as age_ms
|
|
6087
|
+
from sys.dm_exec_sessions s
|
|
6088
|
+
left join sys.dm_exec_requests r on r.session_id = s.session_id
|
|
6089
|
+
outer apply sys.dm_exec_sql_text(r.sql_handle) t
|
|
6090
|
+
where s.session_id = ${blockerId}
|
|
6091
|
+
`.execute(side));
|
|
6092
|
+
const percent = toNumber(request?.percent_complete);
|
|
6093
|
+
return {
|
|
6094
|
+
state: toText(request?.status),
|
|
6095
|
+
waitEvent: toText(request?.wait_type),
|
|
6096
|
+
blockedBy: toBlockers(blockers),
|
|
6097
|
+
workers: 0,
|
|
6098
|
+
progress: request?.command && percent ? { operation: request.command, phase: null, relation: null, done: null, total: null, percent } : null
|
|
6099
|
+
};
|
|
6100
|
+
};
|
|
6101
|
+
var mysqlProbe = async (side, id) => {
|
|
6102
|
+
const [process2] = await rowsOrEmpty(() => sql`
|
|
6103
|
+
select state from information_schema.processlist where id = ${id}
|
|
6104
|
+
`.execute(side));
|
|
6105
|
+
const blockerIds = await rowsOrEmpty(() => sql`
|
|
6106
|
+
select blocking_pid as pid from sys.innodb_lock_waits where waiting_pid = ${id}
|
|
6107
|
+
union
|
|
6108
|
+
select blocking_pid from sys.schema_table_lock_waits where waiting_pid = ${id}
|
|
6109
|
+
`.execute(side));
|
|
6110
|
+
const ids = blockerIds.map((row) => Number(row.pid));
|
|
6111
|
+
const blockers = ids.length === 0 ? [] : await rowsOrEmpty(() => sql`
|
|
6112
|
+
select id as pid, left(coalesce(info, command), 120) as query, time * 1000 as age_ms
|
|
6113
|
+
from information_schema.processlist
|
|
6114
|
+
where id in (${sql.join(ids)})
|
|
6115
|
+
`.execute(side));
|
|
6116
|
+
const [stage] = await rowsOrEmpty(() => sql`
|
|
6117
|
+
select s.event_name, s.work_completed, s.work_estimated
|
|
6118
|
+
from performance_schema.events_stages_current s
|
|
6119
|
+
join performance_schema.threads t on t.thread_id = s.thread_id
|
|
6120
|
+
where t.processlist_id = ${id}
|
|
6121
|
+
`.execute(side));
|
|
6122
|
+
const state = toText(process2?.state);
|
|
6123
|
+
const done = toNumber(stage?.work_completed);
|
|
6124
|
+
const total = toNumber(stage?.work_estimated);
|
|
6125
|
+
return {
|
|
6126
|
+
state,
|
|
6127
|
+
waitEvent: state?.startsWith("Waiting") ? state : null,
|
|
6128
|
+
blockedBy: toBlockers(blockers),
|
|
6129
|
+
workers: 0,
|
|
6130
|
+
progress: stage ? {
|
|
6131
|
+
operation: stage.event_name.replace(/^stage\/\w+\//, ""),
|
|
6132
|
+
phase: null,
|
|
6133
|
+
relation: null,
|
|
6134
|
+
done,
|
|
6135
|
+
total,
|
|
6136
|
+
percent: percentOf(done, total)
|
|
6137
|
+
} : null
|
|
6138
|
+
};
|
|
6139
|
+
};
|
|
6140
|
+
var STATEMENT_PROBES = {
|
|
6141
|
+
postgres: postgresProbe,
|
|
6142
|
+
mssql: mssqlProbe,
|
|
6143
|
+
mysql: mysqlProbe
|
|
6144
|
+
};
|
|
6145
|
+
|
|
6146
|
+
// src/core/runner/statement-watcher.ts
|
|
6147
|
+
var DEFAULT_WATCH_DELAY_MS = 1e4;
|
|
6148
|
+
var DEFAULT_WATCH_INTERVAL_MS = 1e4;
|
|
6149
|
+
var SIDE_CONNECTION_WAIT_MS = 5e3;
|
|
6150
|
+
var StatementWatcher = class {
|
|
6151
|
+
#pool;
|
|
6152
|
+
#dialect;
|
|
6153
|
+
#signal;
|
|
6154
|
+
#delayMs;
|
|
6155
|
+
#intervalMs;
|
|
6156
|
+
#active = null;
|
|
6157
|
+
#side = null;
|
|
6158
|
+
#sideAbandoned = false;
|
|
6159
|
+
#releaseSide = null;
|
|
6160
|
+
#sideHeld = Promise.resolve();
|
|
6161
|
+
#closed = false;
|
|
6162
|
+
#onAbort = () => {
|
|
6163
|
+
const statement = this.#active;
|
|
6164
|
+
if (statement) void this.#cancel(statement);
|
|
6165
|
+
};
|
|
6166
|
+
/**
|
|
6167
|
+
* @param pool - Root connection pool the side connection is checked out
|
|
6168
|
+
* from. Never a transaction: the side connection must be a separate session.
|
|
6169
|
+
*/
|
|
6170
|
+
constructor(pool, options = {}) {
|
|
6171
|
+
this.#pool = pool;
|
|
6172
|
+
this.#dialect = options.dialect;
|
|
6173
|
+
this.#signal = options.signal;
|
|
6174
|
+
this.#delayMs = options.delayMs ?? DEFAULT_WATCH_DELAY_MS;
|
|
6175
|
+
this.#intervalMs = options.intervalMs ?? DEFAULT_WATCH_INTERVAL_MS;
|
|
6176
|
+
this.#signal?.addEventListener("abort", this.#onAbort, { once: true });
|
|
6177
|
+
}
|
|
6178
|
+
/**
|
|
6179
|
+
* Run `fn` on one pinned connection while watching it.
|
|
6180
|
+
*
|
|
6181
|
+
* A transaction is already one connection, so it is used as is; anything
|
|
6182
|
+
* else is pinned with `connection()` so the session id read up front is
|
|
6183
|
+
* the session the SQL runs on.
|
|
6184
|
+
*
|
|
6185
|
+
* @throws OperationAbortedError when the signal aborted before `fn` started
|
|
6186
|
+
*/
|
|
6187
|
+
async run(filepath, executor, fn) {
|
|
6188
|
+
if (executor.isTransaction) {
|
|
6189
|
+
return this.#watch(filepath, executor, fn);
|
|
6190
|
+
}
|
|
6191
|
+
return executor.connection().execute((conn) => this.#watch(filepath, conn, fn));
|
|
6192
|
+
}
|
|
6193
|
+
/**
|
|
6194
|
+
* Stop watching and return the side connection to the pool.
|
|
6195
|
+
*/
|
|
6196
|
+
async close() {
|
|
6197
|
+
this.#closed = true;
|
|
6198
|
+
this.#signal?.removeEventListener("abort", this.#onAbort);
|
|
6199
|
+
this.#releaseSide?.();
|
|
6200
|
+
await this.#sideHeld;
|
|
6201
|
+
}
|
|
6202
|
+
async #watch(filepath, conn, fn) {
|
|
6203
|
+
const sessionId = await this.#readSessionId(conn);
|
|
6204
|
+
const statement = {
|
|
6205
|
+
filepath,
|
|
6206
|
+
sessionId,
|
|
6207
|
+
startedAt: performance.now(),
|
|
6208
|
+
timer: void 0,
|
|
6209
|
+
finished: false,
|
|
6210
|
+
cancelling: null
|
|
6211
|
+
};
|
|
6212
|
+
if (this.#signal?.aborted) {
|
|
6213
|
+
throw new OperationAbortedError("Run cancelled before the file started");
|
|
6214
|
+
}
|
|
6215
|
+
this.#active = statement;
|
|
6216
|
+
statement.timer = setTimeout(() => void this.#report(statement), this.#delayMs);
|
|
6217
|
+
try {
|
|
6218
|
+
return await fn(conn);
|
|
6219
|
+
} finally {
|
|
6220
|
+
clearTimeout(statement.timer);
|
|
6221
|
+
this.#active = null;
|
|
6222
|
+
statement.finished = true;
|
|
6223
|
+
await statement.cancelling;
|
|
6224
|
+
}
|
|
6225
|
+
}
|
|
6226
|
+
async #readSessionId(conn) {
|
|
6227
|
+
const query = this.#dialect ? SESSION_ID_SQL[this.#dialect] : void 0;
|
|
6228
|
+
if (!query) return null;
|
|
6229
|
+
const [result, err] = await attempt(() => sql.raw(query).execute(conn));
|
|
6230
|
+
if (err) return null;
|
|
6231
|
+
return readSessionId(result.rows) ?? null;
|
|
6232
|
+
}
|
|
6233
|
+
async #report(statement) {
|
|
6234
|
+
if (this.#active !== statement) return;
|
|
6235
|
+
const status = await this.#poll(statement);
|
|
6236
|
+
if (this.#active !== statement) return;
|
|
6237
|
+
observer.emit("file:progress", {
|
|
6238
|
+
filepath: statement.filepath,
|
|
6239
|
+
elapsedMs: performance.now() - statement.startedAt,
|
|
6240
|
+
sessionId: statement.sessionId,
|
|
6241
|
+
status
|
|
6242
|
+
});
|
|
6243
|
+
statement.timer = setTimeout(() => void this.#report(statement), this.#intervalMs);
|
|
6244
|
+
}
|
|
6245
|
+
async #poll(statement) {
|
|
6246
|
+
const probe = this.#dialect ? STATEMENT_PROBES[this.#dialect] : void 0;
|
|
6247
|
+
const sessionId = statement.sessionId;
|
|
6248
|
+
if (!probe || sessionId === null) return null;
|
|
6249
|
+
const side = await this.#sideConnection();
|
|
6250
|
+
if (!side) return null;
|
|
6251
|
+
const [status, err] = await attempt(() => probe(side, sessionId));
|
|
6252
|
+
return err ? null : status;
|
|
6253
|
+
}
|
|
6254
|
+
async #cancel(statement) {
|
|
6255
|
+
const cancel = this.#dialect ? SERVER_CANCEL[this.#dialect] : void 0;
|
|
6256
|
+
const sessionId = statement.sessionId;
|
|
6257
|
+
if (sessionId === null || !cancel) return;
|
|
6258
|
+
const side = await this.#sideConnection();
|
|
6259
|
+
if (statement.finished || this.#closed) return;
|
|
6260
|
+
if (!side) {
|
|
6261
|
+
observer.emit("error", {
|
|
6262
|
+
source: "runner:cancel",
|
|
6263
|
+
error: new Error("No free connection to send the cancel from"),
|
|
6264
|
+
context: { filepath: statement.filepath, sessionId }
|
|
6265
|
+
});
|
|
6266
|
+
return;
|
|
6267
|
+
}
|
|
6268
|
+
const sending = attempt(() => cancel(side, sessionId));
|
|
6269
|
+
statement.cancelling = sending.then(() => void 0);
|
|
6270
|
+
const [, err] = await sending;
|
|
6271
|
+
if (err) {
|
|
6272
|
+
observer.emit("error", {
|
|
6273
|
+
source: "runner:cancel",
|
|
6274
|
+
error: err,
|
|
6275
|
+
context: { filepath: statement.filepath, sessionId }
|
|
6276
|
+
});
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
async #sideConnection() {
|
|
6280
|
+
if (this.#closed || this.#sideAbandoned) return null;
|
|
6281
|
+
this.#side ??= this.#checkoutSide();
|
|
6282
|
+
const pending = this.#side;
|
|
6283
|
+
const [side] = await attempt(() => runWithTimeout(() => pending, { timeout: SIDE_CONNECTION_WAIT_MS }));
|
|
6284
|
+
if (!side && this.#side === pending) this.#sideAbandoned = true;
|
|
6285
|
+
return side ?? null;
|
|
6286
|
+
}
|
|
6287
|
+
/**
|
|
6288
|
+
* Check a connection out of the pool and hold it until `close()`.
|
|
6289
|
+
*
|
|
6290
|
+
* Kysely only lends a dedicated connection for the duration of a callback,
|
|
6291
|
+
* so the callback parks on a promise that `close()` resolves. A checkout
|
|
6292
|
+
* that arrives after the watcher gave up on it returns at once.
|
|
6293
|
+
*/
|
|
6294
|
+
#checkoutSide() {
|
|
6295
|
+
return new Promise((resolveSide) => {
|
|
6296
|
+
const released = new Promise((release) => {
|
|
6297
|
+
this.#releaseSide = release;
|
|
6298
|
+
});
|
|
6299
|
+
this.#sideHeld = attempt(() => this.#pool.connection().execute(async (side) => {
|
|
6300
|
+
if (this.#sideAbandoned || this.#closed) return;
|
|
6301
|
+
if (this.#active === null) {
|
|
6302
|
+
this.#side = null;
|
|
6303
|
+
resolveSide(null);
|
|
6304
|
+
return;
|
|
6305
|
+
}
|
|
6306
|
+
resolveSide(side);
|
|
6307
|
+
await released;
|
|
6308
|
+
})).then(([, err]) => {
|
|
6309
|
+
if (err) {
|
|
6310
|
+
resolveSide(null);
|
|
6311
|
+
observer.emit("error", {
|
|
6312
|
+
source: "runner:watch",
|
|
6313
|
+
error: err,
|
|
6314
|
+
context: { operation: "side-connection" }
|
|
6315
|
+
});
|
|
6316
|
+
}
|
|
6317
|
+
});
|
|
6318
|
+
});
|
|
6319
|
+
}
|
|
6320
|
+
};
|
|
6022
6321
|
|
|
6023
6322
|
// src/core/lock/types.ts
|
|
6024
6323
|
var DEFAULT_LOCK_OPTIONS = {
|
|
@@ -6618,6 +6917,7 @@ var ChangeRollback = class extends Error {
|
|
|
6618
6917
|
async function executeFiles(context, change, files, direction, checksum, force, history, startTime) {
|
|
6619
6918
|
const dialect = context.dialect ?? "postgres";
|
|
6620
6919
|
const expandedFiles = await expandFiles(files, context.sqlDir);
|
|
6920
|
+
const watcher = new StatementWatcher(context.db, { dialect });
|
|
6621
6921
|
if (!TRANSACTIONAL_DIALECTS.has(dialect)) {
|
|
6622
6922
|
return runFileBatch(
|
|
6623
6923
|
context,
|
|
@@ -6628,8 +6928,9 @@ async function executeFiles(context, change, files, direction, checksum, force,
|
|
|
6628
6928
|
force,
|
|
6629
6929
|
history,
|
|
6630
6930
|
context.db,
|
|
6631
|
-
startTime
|
|
6632
|
-
|
|
6931
|
+
startTime,
|
|
6932
|
+
watcher
|
|
6933
|
+
).finally(() => watcher.close());
|
|
6633
6934
|
}
|
|
6634
6935
|
const [result, err] = await attempt(
|
|
6635
6936
|
() => context.db.transaction().execute(async (trx) => {
|
|
@@ -6643,7 +6944,8 @@ async function executeFiles(context, change, files, direction, checksum, force,
|
|
|
6643
6944
|
force,
|
|
6644
6945
|
trxHistory,
|
|
6645
6946
|
trx,
|
|
6646
|
-
startTime
|
|
6947
|
+
startTime,
|
|
6948
|
+
watcher
|
|
6647
6949
|
);
|
|
6648
6950
|
if (batchResult.status !== "success") {
|
|
6649
6951
|
throw new ChangeRollback(batchResult);
|
|
@@ -6651,6 +6953,7 @@ async function executeFiles(context, change, files, direction, checksum, force,
|
|
|
6651
6953
|
return batchResult;
|
|
6652
6954
|
})
|
|
6653
6955
|
);
|
|
6956
|
+
await watcher.close();
|
|
6654
6957
|
if (err) {
|
|
6655
6958
|
if (err instanceof ChangeRollback) {
|
|
6656
6959
|
return { ...err.result, operationId: void 0 };
|
|
@@ -6659,7 +6962,13 @@ async function executeFiles(context, change, files, direction, checksum, force,
|
|
|
6659
6962
|
}
|
|
6660
6963
|
return result;
|
|
6661
6964
|
}
|
|
6662
|
-
async function runFileBatch(context, change, expandedFiles, direction, checksum, force, history, executor, startTime) {
|
|
6965
|
+
async function runFileBatch(context, change, expandedFiles, direction, checksum, force, history, executor, startTime, watcher) {
|
|
6966
|
+
const fileChecksums = /* @__PURE__ */ new Map();
|
|
6967
|
+
for (const file of expandedFiles) {
|
|
6968
|
+
const [cs] = await attempt(() => computeChecksum(file.path));
|
|
6969
|
+
fileChecksums.set(file.path, cs ?? "");
|
|
6970
|
+
}
|
|
6971
|
+
const skipReasons = await findSkippedFiles(context, change.name, direction, expandedFiles, fileChecksums, force, history);
|
|
6663
6972
|
const [operationId, createErr] = await attempt(
|
|
6664
6973
|
() => history.createOperation({
|
|
6665
6974
|
name: change.name,
|
|
@@ -6675,14 +6984,9 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
6675
6984
|
});
|
|
6676
6985
|
return createFailedResult(change.name, direction, createErr.message, startTime);
|
|
6677
6986
|
}
|
|
6678
|
-
const fileChecksums = /* @__PURE__ */ new Map();
|
|
6679
|
-
for (const file of expandedFiles) {
|
|
6680
|
-
const [cs] = await attempt(() => computeChecksum(file.path));
|
|
6681
|
-
fileChecksums.set(file.path, cs ?? "");
|
|
6682
|
-
}
|
|
6683
6987
|
const createRecordsErr = await history.createFileRecords(
|
|
6684
6988
|
operationId,
|
|
6685
|
-
expandedFiles.map((f) => ({
|
|
6989
|
+
expandedFiles.filter((_, i) => !skipReasons[i]).map((f) => ({
|
|
6686
6990
|
filepath: path.relative(context.projectRoot, f.path),
|
|
6687
6991
|
fileType: f.type,
|
|
6688
6992
|
checksum: fileChecksums.get(f.path) ?? ""
|
|
@@ -6722,36 +7026,15 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
6722
7026
|
const fileStart = performance.now();
|
|
6723
7027
|
const relPath = path.relative(context.projectRoot, file.path);
|
|
6724
7028
|
const fileChecksum = fileChecksums.get(file.path) ?? "";
|
|
6725
|
-
const
|
|
6726
|
-
|
|
6727
|
-
direction,
|
|
6728
|
-
relPath,
|
|
6729
|
-
fileChecksum,
|
|
6730
|
-
force
|
|
6731
|
-
);
|
|
6732
|
-
if (!needsRunFileResult.needsRun) {
|
|
7029
|
+
const skipReason = skipReasons[i];
|
|
7030
|
+
if (skipReason) {
|
|
6733
7031
|
results.push({
|
|
6734
7032
|
filepath: file.path,
|
|
6735
7033
|
checksum: fileChecksum,
|
|
6736
7034
|
status: "skipped",
|
|
6737
|
-
skipReason
|
|
7035
|
+
skipReason,
|
|
6738
7036
|
durationMs: 0
|
|
6739
7037
|
});
|
|
6740
|
-
const skipUpdateErr = await history.updateFileExecution(
|
|
6741
|
-
operationId,
|
|
6742
|
-
relPath,
|
|
6743
|
-
"skipped",
|
|
6744
|
-
0,
|
|
6745
|
-
void 0,
|
|
6746
|
-
needsRunFileResult.skipReason
|
|
6747
|
-
);
|
|
6748
|
-
if (skipUpdateErr) {
|
|
6749
|
-
observer.emit("error", {
|
|
6750
|
-
source: "change",
|
|
6751
|
-
error: new Error(skipUpdateErr),
|
|
6752
|
-
context: { filepath: relPath, operation: "update-skipped-record" }
|
|
6753
|
-
});
|
|
6754
|
-
}
|
|
6755
7038
|
continue;
|
|
6756
7039
|
}
|
|
6757
7040
|
const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile(context, file.path));
|
|
@@ -6783,7 +7066,9 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
6783
7066
|
}
|
|
6784
7067
|
break;
|
|
6785
7068
|
}
|
|
6786
|
-
const [, execErr] = await attempt(
|
|
7069
|
+
const [, execErr] = await attempt(
|
|
7070
|
+
() => watcher.run(file.path, executor, (conn) => sql.raw(sqlContent).execute(conn))
|
|
7071
|
+
);
|
|
6787
7072
|
const durationMs = performance.now() - fileStart;
|
|
6788
7073
|
if (execErr) {
|
|
6789
7074
|
const errorMessage2 = getSqlErrorMessage(execErr);
|
|
@@ -6884,6 +7169,22 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
6884
7169
|
operationId
|
|
6885
7170
|
};
|
|
6886
7171
|
}
|
|
7172
|
+
async function findSkippedFiles(context, changeName, direction, files, fileChecksums, force, history) {
|
|
7173
|
+
const skipReasons = [];
|
|
7174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7175
|
+
const [latest, latestErr] = force ? [null, null] : await history.latestFileExecutions(changeName, direction);
|
|
7176
|
+
for (const file of files) {
|
|
7177
|
+
const relPath = path.relative(context.projectRoot, file.path);
|
|
7178
|
+
if (seen.has(relPath)) {
|
|
7179
|
+
skipReasons.push("already applied");
|
|
7180
|
+
continue;
|
|
7181
|
+
}
|
|
7182
|
+
seen.add(relPath);
|
|
7183
|
+
const decision = decideNeedsRunFile(latestErr ?? latest?.get(relPath), fileChecksums.get(file.path) ?? "", force);
|
|
7184
|
+
skipReasons.push(decision.needsRun ? void 0 : decision.skipReason);
|
|
7185
|
+
}
|
|
7186
|
+
return skipReasons;
|
|
7187
|
+
}
|
|
6887
7188
|
async function executePreview(context, change, files, direction, output, startTime) {
|
|
6888
7189
|
const expandedFiles = await expandFiles(files, context.sqlDir);
|
|
6889
7190
|
const results = [];
|
|
@@ -7554,7 +7855,7 @@ async function resolveVaultKey(db, dialect) {
|
|
|
7554
7855
|
async function buildSecretsContext(stateManager, configName, db, vaultKey, dialect) {
|
|
7555
7856
|
const secrets = {};
|
|
7556
7857
|
if (db && vaultKey) {
|
|
7557
|
-
const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-
|
|
7858
|
+
const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-NQA4H7PK.js');
|
|
7558
7859
|
const vaultSecrets = await getAllVaultSecrets2(db, vaultKey, dialect ?? "postgres");
|
|
7559
7860
|
for (const [key, secret] of Object.entries(vaultSecrets)) {
|
|
7560
7861
|
secrets[key] = secret.value;
|
|
@@ -8326,47 +8627,20 @@ async function runBuild(context, sqlPath, options = {}, preFilteredFiles) {
|
|
|
8326
8627
|
}
|
|
8327
8628
|
async function runFile(context, filepath, options = {}) {
|
|
8328
8629
|
assertRunPolicy(context, "run:file");
|
|
8329
|
-
const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...options };
|
|
8330
8630
|
observer.emit("run:file", {
|
|
8331
8631
|
filepath,
|
|
8332
8632
|
configName: context.configName
|
|
8333
8633
|
});
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
}
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
configName: context.configName,
|
|
8345
|
-
executedBy: formatIdentity(context.identity)
|
|
8346
|
-
})
|
|
8347
|
-
);
|
|
8348
|
-
if (createErr) {
|
|
8349
|
-
observer.emit("error", {
|
|
8350
|
-
source: "runner",
|
|
8351
|
-
error: createErr,
|
|
8352
|
-
context: { filepath, operation: "create-operation" }
|
|
8353
|
-
});
|
|
8354
|
-
return {
|
|
8355
|
-
filepath,
|
|
8356
|
-
checksum: "",
|
|
8357
|
-
status: "failed",
|
|
8358
|
-
error: createErr.message
|
|
8359
|
-
};
|
|
8360
|
-
}
|
|
8361
|
-
const result = await executeSingleFile(context, filepath, opts, tracker, operationId);
|
|
8362
|
-
await tracker.finalizeOperation(
|
|
8363
|
-
operationId,
|
|
8364
|
-
result.status === "failed" ? "failed" : "success",
|
|
8365
|
-
Math.round(result.durationMs ?? 0),
|
|
8366
|
-
result.checksum,
|
|
8367
|
-
result.error
|
|
8368
|
-
);
|
|
8369
|
-
return result;
|
|
8634
|
+
const batch = await executeFiles2(context, [{ path: filepath, type: "sql" }], options, {
|
|
8635
|
+
changeType: "run",
|
|
8636
|
+
operationName: `run:${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
8637
|
+
});
|
|
8638
|
+
return batch.files[0] ?? {
|
|
8639
|
+
filepath,
|
|
8640
|
+
checksum: "",
|
|
8641
|
+
status: batch.error === RUN_CANCELLED ? "skipped" : "failed",
|
|
8642
|
+
error: batch.error
|
|
8643
|
+
};
|
|
8370
8644
|
}
|
|
8371
8645
|
async function runDir(context, dirpath, options = {}) {
|
|
8372
8646
|
assertRunPolicy(context, "run:dir");
|
|
@@ -8445,6 +8719,31 @@ var DEFAULT_RUN_OPTIONS_INTERNAL = {
|
|
|
8445
8719
|
preview: false,
|
|
8446
8720
|
output: null
|
|
8447
8721
|
};
|
|
8722
|
+
var RUN_CANCELLED = "Run cancelled";
|
|
8723
|
+
function createWatcher(context) {
|
|
8724
|
+
return new StatementWatcher(context.db, {
|
|
8725
|
+
dialect: context.dialect ?? "postgres",
|
|
8726
|
+
signal: context.signal
|
|
8727
|
+
});
|
|
8728
|
+
}
|
|
8729
|
+
var NOT_STARTED = /* @__PURE__ */ Symbol("not-started");
|
|
8730
|
+
async function runWatched(context, watcher, filepath, sqlContent) {
|
|
8731
|
+
const [execErrMsg, err] = await attempt(
|
|
8732
|
+
() => watcher.run(filepath, context.db, (conn) => executeSqlBody({ ...context, db: conn }, sqlContent))
|
|
8733
|
+
);
|
|
8734
|
+
if (err instanceof OperationAbortedError) return NOT_STARTED;
|
|
8735
|
+
return err ? getSqlErrorMessage(err) : execErrMsg;
|
|
8736
|
+
}
|
|
8737
|
+
async function skipRemaining(tracker, operationId, reason) {
|
|
8738
|
+
const skipErr = await tracker.skipRemainingFiles(operationId, reason.slice(0, 100));
|
|
8739
|
+
if (skipErr) {
|
|
8740
|
+
observer.emit("error", {
|
|
8741
|
+
source: "runner:skip-remaining",
|
|
8742
|
+
error: new Error(skipErr),
|
|
8743
|
+
context: { operationId }
|
|
8744
|
+
});
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8448
8747
|
async function executeFiles2(context, files, runOptions, execOptions) {
|
|
8449
8748
|
const start = performance.now();
|
|
8450
8749
|
const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...runOptions };
|
|
@@ -8484,6 +8783,19 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8484
8783
|
};
|
|
8485
8784
|
}
|
|
8486
8785
|
const tracker = execOptions.tracker ?? new Tracker(context.db, context.configName, context.dialect ?? "postgres");
|
|
8786
|
+
const isChange = execOptions.changeType === "change";
|
|
8787
|
+
const prepared = await prepareFiles(context, files);
|
|
8788
|
+
const gated = isChange ? prepared : await applyRunGate(tracker, prepared, opts.force);
|
|
8789
|
+
if (!isChange && gated.every((entry) => entry.skipReason !== void 0)) {
|
|
8790
|
+
return {
|
|
8791
|
+
status: "success",
|
|
8792
|
+
files: gated.map(skipFile),
|
|
8793
|
+
filesRun: 0,
|
|
8794
|
+
filesSkipped: gated.length,
|
|
8795
|
+
filesFailed: 0,
|
|
8796
|
+
durationMs: performance.now() - start
|
|
8797
|
+
};
|
|
8798
|
+
}
|
|
8487
8799
|
const [operationId, createErr] = await attempt(
|
|
8488
8800
|
() => tracker.createOperation({
|
|
8489
8801
|
name: execOptions.operationName,
|
|
@@ -8504,20 +8816,11 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8504
8816
|
performance.now() - start
|
|
8505
8817
|
);
|
|
8506
8818
|
}
|
|
8507
|
-
const
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
checksum = err ? "" : computed;
|
|
8513
|
-
}
|
|
8514
|
-
fileRecords.push({
|
|
8515
|
-
filepath: path.relative(context.projectRoot, file.path),
|
|
8516
|
-
fileType: file.type,
|
|
8517
|
-
checksum
|
|
8518
|
-
});
|
|
8519
|
-
}
|
|
8520
|
-
const createRecordsErr = await tracker.createFileRecords(operationId, fileRecords);
|
|
8819
|
+
const runSet = gated.filter((entry) => !entry.skipReason);
|
|
8820
|
+
const createRecordsErr = await tracker.createFileRecords(
|
|
8821
|
+
operationId,
|
|
8822
|
+
runSet.map((entry) => ({ filepath: entry.relFilepath, fileType: entry.file.type, checksum: entry.checksum }))
|
|
8823
|
+
);
|
|
8521
8824
|
if (createRecordsErr) {
|
|
8522
8825
|
observer.emit("error", {
|
|
8523
8826
|
source: "runner:create-file-records",
|
|
@@ -8532,34 +8835,43 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8532
8835
|
}
|
|
8533
8836
|
const results = [];
|
|
8534
8837
|
let failed = false;
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
|
|
8538
|
-
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8838
|
+
let cancelled = false;
|
|
8839
|
+
const watcher = createWatcher(context);
|
|
8840
|
+
const lastRunIndex = gated.map((entry) => !entry.skipReason).lastIndexOf(true);
|
|
8841
|
+
try {
|
|
8842
|
+
for (let i = 0; i < gated.length; i++) {
|
|
8843
|
+
const entry = gated[i];
|
|
8844
|
+
if (entry.skipReason) {
|
|
8845
|
+
results.push(skipFile(entry));
|
|
8846
|
+
continue;
|
|
8847
|
+
}
|
|
8848
|
+
if (context.signal?.aborted) {
|
|
8849
|
+
failed = true;
|
|
8850
|
+
cancelled = true;
|
|
8851
|
+
await skipRemaining(tracker, operationId, RUN_CANCELLED);
|
|
8852
|
+
break;
|
|
8853
|
+
}
|
|
8854
|
+
const result = await executeSingleFileWithUpdate(context, entry, tracker, operationId, watcher);
|
|
8855
|
+
if (!result) {
|
|
8856
|
+
failed = true;
|
|
8857
|
+
cancelled = true;
|
|
8858
|
+
await skipRemaining(tracker, operationId, RUN_CANCELLED);
|
|
8859
|
+
break;
|
|
8860
|
+
}
|
|
8861
|
+
results.push(result);
|
|
8862
|
+
cancelled = (context.signal?.aborted ?? false) && (i < lastRunIndex || result.status === "failed");
|
|
8863
|
+
if (cancelled || result.status === "failed" && opts.abortOnError) {
|
|
8864
|
+
failed = true;
|
|
8865
|
+
await skipRemaining(
|
|
8866
|
+
tracker,
|
|
8867
|
+
operationId,
|
|
8868
|
+
cancelled ? RUN_CANCELLED : `Skipped: failure in ${path.basename(entry.file.path)}`
|
|
8869
|
+
);
|
|
8870
|
+
break;
|
|
8560
8871
|
}
|
|
8561
|
-
break;
|
|
8562
8872
|
}
|
|
8873
|
+
} finally {
|
|
8874
|
+
await watcher.close();
|
|
8563
8875
|
}
|
|
8564
8876
|
if (!failed && results.length !== files.length) {
|
|
8565
8877
|
throw new Error(
|
|
@@ -8574,14 +8886,14 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8574
8886
|
if (filesFailed > 0 || failed) {
|
|
8575
8887
|
status = filesRun > 0 ? "partial" : "failed";
|
|
8576
8888
|
}
|
|
8577
|
-
const combinedChecksum = execOptions.checksum ?? computeCombinedChecksum(
|
|
8889
|
+
const combinedChecksum = execOptions.checksum ?? computeCombinedChecksum(gated.map((entry) => entry.checksum));
|
|
8578
8890
|
const finalStatus = status === "success" ? "success" : "failed";
|
|
8579
8891
|
const finalizeErr = await tracker.finalizeOperation(
|
|
8580
8892
|
operationId,
|
|
8581
8893
|
finalStatus,
|
|
8582
8894
|
Math.round(durationMs),
|
|
8583
8895
|
combinedChecksum,
|
|
8584
|
-
failed ? results.find((r) => r.status === "failed")?.error : void 0
|
|
8896
|
+
cancelled ? RUN_CANCELLED : failed ? results.find((r) => r.status === "failed")?.error : void 0
|
|
8585
8897
|
);
|
|
8586
8898
|
if (finalizeErr) {
|
|
8587
8899
|
observer.emit("error", {
|
|
@@ -8597,7 +8909,45 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8597
8909
|
filesSkipped,
|
|
8598
8910
|
filesFailed,
|
|
8599
8911
|
durationMs,
|
|
8600
|
-
changeId: operationId
|
|
8912
|
+
changeId: operationId,
|
|
8913
|
+
error: cancelled ? RUN_CANCELLED : void 0
|
|
8914
|
+
};
|
|
8915
|
+
}
|
|
8916
|
+
async function prepareFiles(context, files) {
|
|
8917
|
+
const prepared = [];
|
|
8918
|
+
for (const file of files) {
|
|
8919
|
+
const relFilepath = path.relative(context.projectRoot, file.path);
|
|
8920
|
+
const [loaded, loadError] = await attempt(async () => {
|
|
8921
|
+
const sql28 = await loadAndRenderFile2(context, file.path);
|
|
8922
|
+
return { sql: sql28, checksum: computeChecksumFromContent(sql28) };
|
|
8923
|
+
});
|
|
8924
|
+
if (loadError) {
|
|
8925
|
+
prepared.push({ file, relFilepath, checksum: "", sql: "", loadError });
|
|
8926
|
+
continue;
|
|
8927
|
+
}
|
|
8928
|
+
prepared.push({ file, relFilepath, ...loaded });
|
|
8929
|
+
}
|
|
8930
|
+
return prepared;
|
|
8931
|
+
}
|
|
8932
|
+
async function applyRunGate(tracker, prepared, force) {
|
|
8933
|
+
if (force) return prepared;
|
|
8934
|
+
const [latest, latestErr] = await tracker.latestExecutions();
|
|
8935
|
+
return prepared.map((entry) => {
|
|
8936
|
+
if (entry.loadError) return entry;
|
|
8937
|
+
const decision = decideNeedsRun(latestErr ?? latest.get(entry.relFilepath), entry.checksum);
|
|
8938
|
+
return decision.skipReason ? { ...entry, skipReason: decision.skipReason } : entry;
|
|
8939
|
+
});
|
|
8940
|
+
}
|
|
8941
|
+
function skipFile(entry) {
|
|
8942
|
+
observer.emit("file:skip", {
|
|
8943
|
+
filepath: entry.file.path,
|
|
8944
|
+
reason: entry.skipReason
|
|
8945
|
+
});
|
|
8946
|
+
return {
|
|
8947
|
+
filepath: entry.file.path,
|
|
8948
|
+
checksum: entry.checksum,
|
|
8949
|
+
status: "skipped",
|
|
8950
|
+
skipReason: entry.skipReason
|
|
8601
8951
|
};
|
|
8602
8952
|
}
|
|
8603
8953
|
async function executeFilesInternal(context, files, options, changeType, sqlPath) {
|
|
@@ -8613,17 +8963,16 @@ async function executeFilesInternal(context, files, options, changeType, sqlPath
|
|
|
8613
8963
|
};
|
|
8614
8964
|
return executeFiles2(context, fileInputs, options, execOptions);
|
|
8615
8965
|
}
|
|
8616
|
-
async function executeSingleFileWithUpdate(context,
|
|
8966
|
+
async function executeSingleFileWithUpdate(context, entry, tracker, operationId, watcher) {
|
|
8617
8967
|
const start = performance.now();
|
|
8618
|
-
const
|
|
8619
|
-
|
|
8620
|
-
if (loadErr) {
|
|
8968
|
+
const { file: { path: filepath }, relFilepath, checksum } = entry;
|
|
8969
|
+
if (entry.loadError) {
|
|
8621
8970
|
const durationMs2 = performance.now() - start;
|
|
8622
8971
|
const result2 = {
|
|
8623
8972
|
filepath,
|
|
8624
|
-
checksum
|
|
8973
|
+
checksum,
|
|
8625
8974
|
status: "failed",
|
|
8626
|
-
error:
|
|
8975
|
+
error: entry.loadError.message,
|
|
8627
8976
|
durationMs: durationMs2
|
|
8628
8977
|
};
|
|
8629
8978
|
await tracker.updateFileExecution(
|
|
@@ -8631,55 +8980,29 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
8631
8980
|
relFilepath,
|
|
8632
8981
|
"failed",
|
|
8633
8982
|
Math.round(durationMs2),
|
|
8634
|
-
|
|
8983
|
+
entry.loadError.message
|
|
8635
8984
|
);
|
|
8636
8985
|
observer.emit("file:after", {
|
|
8637
8986
|
filepath,
|
|
8638
8987
|
status: "failed",
|
|
8639
8988
|
durationMs: durationMs2,
|
|
8640
|
-
error:
|
|
8989
|
+
error: entry.loadError.message
|
|
8641
8990
|
});
|
|
8642
8991
|
return result2;
|
|
8643
8992
|
}
|
|
8644
|
-
const [renderedChecksum, checksumErr] = attemptSync(() => computeChecksumFromContent(sqlContent));
|
|
8645
|
-
const finalChecksum = checksumErr ? checksum : renderedChecksum;
|
|
8646
8993
|
observer.emit("file:before", {
|
|
8647
8994
|
filepath,
|
|
8648
|
-
checksum
|
|
8995
|
+
checksum,
|
|
8649
8996
|
configName: context.configName
|
|
8650
8997
|
});
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
if (!needsRunResult.needsRun) {
|
|
8654
|
-
const result2 = {
|
|
8655
|
-
filepath,
|
|
8656
|
-
checksum: finalChecksum,
|
|
8657
|
-
status: "skipped",
|
|
8658
|
-
skipReason: needsRunResult.skipReason
|
|
8659
|
-
};
|
|
8660
|
-
await tracker.updateFileExecution(
|
|
8661
|
-
operationId,
|
|
8662
|
-
relFilepath,
|
|
8663
|
-
"skipped",
|
|
8664
|
-
0,
|
|
8665
|
-
void 0,
|
|
8666
|
-
needsRunResult.skipReason,
|
|
8667
|
-
finalChecksum
|
|
8668
|
-
);
|
|
8669
|
-
observer.emit("file:skip", {
|
|
8670
|
-
filepath,
|
|
8671
|
-
reason: needsRunResult.skipReason
|
|
8672
|
-
});
|
|
8673
|
-
return result2;
|
|
8674
|
-
}
|
|
8675
|
-
}
|
|
8676
|
-
const execErrMsg = await executeSqlBody(context, sqlContent);
|
|
8998
|
+
const execErrMsg = await runWatched(context, watcher, filepath, entry.sql);
|
|
8999
|
+
if (execErrMsg === NOT_STARTED) return null;
|
|
8677
9000
|
const durationMs = performance.now() - start;
|
|
8678
9001
|
if (execErrMsg) {
|
|
8679
9002
|
const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
|
|
8680
9003
|
const result2 = {
|
|
8681
9004
|
filepath,
|
|
8682
|
-
checksum
|
|
9005
|
+
checksum,
|
|
8683
9006
|
status: "failed",
|
|
8684
9007
|
error,
|
|
8685
9008
|
durationMs
|
|
@@ -8689,9 +9012,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
8689
9012
|
relFilepath,
|
|
8690
9013
|
"failed",
|
|
8691
9014
|
Math.round(durationMs),
|
|
8692
|
-
error
|
|
8693
|
-
void 0,
|
|
8694
|
-
finalChecksum
|
|
9015
|
+
error
|
|
8695
9016
|
);
|
|
8696
9017
|
observer.emit("file:after", {
|
|
8697
9018
|
filepath,
|
|
@@ -8703,7 +9024,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
8703
9024
|
}
|
|
8704
9025
|
const result = {
|
|
8705
9026
|
filepath,
|
|
8706
|
-
checksum
|
|
9027
|
+
checksum,
|
|
8707
9028
|
status: "success",
|
|
8708
9029
|
durationMs
|
|
8709
9030
|
};
|
|
@@ -8711,10 +9032,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
8711
9032
|
operationId,
|
|
8712
9033
|
relFilepath,
|
|
8713
9034
|
"success",
|
|
8714
|
-
Math.round(durationMs)
|
|
8715
|
-
void 0,
|
|
8716
|
-
void 0,
|
|
8717
|
-
finalChecksum
|
|
9035
|
+
Math.round(durationMs)
|
|
8718
9036
|
);
|
|
8719
9037
|
observer.emit("file:after", {
|
|
8720
9038
|
filepath,
|
|
@@ -8723,133 +9041,6 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
8723
9041
|
});
|
|
8724
9042
|
return result;
|
|
8725
9043
|
}
|
|
8726
|
-
async function executeSingleFile(context, filepath, options, tracker, operationId) {
|
|
8727
|
-
const start = performance.now();
|
|
8728
|
-
const relFilepath = path.relative(context.projectRoot, filepath);
|
|
8729
|
-
const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
|
|
8730
|
-
if (loadErr) {
|
|
8731
|
-
const durationMs2 = performance.now() - start;
|
|
8732
|
-
const result2 = {
|
|
8733
|
-
filepath,
|
|
8734
|
-
checksum: "",
|
|
8735
|
-
status: "failed",
|
|
8736
|
-
error: loadErr.message,
|
|
8737
|
-
durationMs: durationMs2
|
|
8738
|
-
};
|
|
8739
|
-
await tracker.recordExecution({
|
|
8740
|
-
changeId: operationId,
|
|
8741
|
-
filepath: relFilepath,
|
|
8742
|
-
checksum: "",
|
|
8743
|
-
status: "failed",
|
|
8744
|
-
errorMessage: loadErr.message,
|
|
8745
|
-
durationMs: Math.round(durationMs2)
|
|
8746
|
-
});
|
|
8747
|
-
observer.emit("file:after", {
|
|
8748
|
-
filepath,
|
|
8749
|
-
status: "failed",
|
|
8750
|
-
durationMs: durationMs2,
|
|
8751
|
-
error: loadErr.message
|
|
8752
|
-
});
|
|
8753
|
-
return result2;
|
|
8754
|
-
}
|
|
8755
|
-
const [checksum, checksumErr] = attemptSync(() => computeChecksumFromContent(sqlContent));
|
|
8756
|
-
if (checksumErr) {
|
|
8757
|
-
const result2 = {
|
|
8758
|
-
filepath,
|
|
8759
|
-
checksum: "",
|
|
8760
|
-
status: "failed",
|
|
8761
|
-
error: checksumErr.message,
|
|
8762
|
-
durationMs: performance.now() - start
|
|
8763
|
-
};
|
|
8764
|
-
await tracker.recordExecution({
|
|
8765
|
-
changeId: operationId,
|
|
8766
|
-
filepath: relFilepath,
|
|
8767
|
-
checksum: "",
|
|
8768
|
-
status: "failed",
|
|
8769
|
-
errorMessage: checksumErr.message,
|
|
8770
|
-
durationMs: Math.round(result2.durationMs ?? 0)
|
|
8771
|
-
});
|
|
8772
|
-
observer.emit("file:after", {
|
|
8773
|
-
filepath,
|
|
8774
|
-
status: "failed",
|
|
8775
|
-
durationMs: result2.durationMs ?? 0,
|
|
8776
|
-
error: checksumErr.message
|
|
8777
|
-
});
|
|
8778
|
-
return result2;
|
|
8779
|
-
}
|
|
8780
|
-
observer.emit("file:before", {
|
|
8781
|
-
filepath,
|
|
8782
|
-
checksum,
|
|
8783
|
-
configName: context.configName
|
|
8784
|
-
});
|
|
8785
|
-
const needsRunResult = await tracker.needsRun(relFilepath, checksum, options.force);
|
|
8786
|
-
if (!needsRunResult.needsRun) {
|
|
8787
|
-
const result2 = {
|
|
8788
|
-
filepath,
|
|
8789
|
-
checksum,
|
|
8790
|
-
status: "skipped",
|
|
8791
|
-
skipReason: needsRunResult.skipReason
|
|
8792
|
-
};
|
|
8793
|
-
await tracker.recordExecution({
|
|
8794
|
-
changeId: operationId,
|
|
8795
|
-
filepath: relFilepath,
|
|
8796
|
-
checksum,
|
|
8797
|
-
status: "skipped",
|
|
8798
|
-
skipReason: needsRunResult.skipReason
|
|
8799
|
-
});
|
|
8800
|
-
observer.emit("file:skip", {
|
|
8801
|
-
filepath,
|
|
8802
|
-
reason: needsRunResult.skipReason
|
|
8803
|
-
});
|
|
8804
|
-
return result2;
|
|
8805
|
-
}
|
|
8806
|
-
const execErrMsg = await executeSqlBody(context, sqlContent);
|
|
8807
|
-
const durationMs = performance.now() - start;
|
|
8808
|
-
if (execErrMsg) {
|
|
8809
|
-
const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
|
|
8810
|
-
const result2 = {
|
|
8811
|
-
filepath,
|
|
8812
|
-
checksum,
|
|
8813
|
-
status: "failed",
|
|
8814
|
-
error,
|
|
8815
|
-
durationMs
|
|
8816
|
-
};
|
|
8817
|
-
await tracker.recordExecution({
|
|
8818
|
-
changeId: operationId,
|
|
8819
|
-
filepath: relFilepath,
|
|
8820
|
-
checksum,
|
|
8821
|
-
status: "failed",
|
|
8822
|
-
errorMessage: error,
|
|
8823
|
-
durationMs: Math.round(durationMs)
|
|
8824
|
-
});
|
|
8825
|
-
observer.emit("file:after", {
|
|
8826
|
-
filepath,
|
|
8827
|
-
status: "failed",
|
|
8828
|
-
durationMs,
|
|
8829
|
-
error
|
|
8830
|
-
});
|
|
8831
|
-
return result2;
|
|
8832
|
-
}
|
|
8833
|
-
const result = {
|
|
8834
|
-
filepath,
|
|
8835
|
-
checksum,
|
|
8836
|
-
status: "success",
|
|
8837
|
-
durationMs
|
|
8838
|
-
};
|
|
8839
|
-
await tracker.recordExecution({
|
|
8840
|
-
changeId: operationId,
|
|
8841
|
-
filepath: relFilepath,
|
|
8842
|
-
checksum,
|
|
8843
|
-
status: "success",
|
|
8844
|
-
durationMs: Math.round(durationMs)
|
|
8845
|
-
});
|
|
8846
|
-
observer.emit("file:after", {
|
|
8847
|
-
filepath,
|
|
8848
|
-
status: "success",
|
|
8849
|
-
durationMs
|
|
8850
|
-
});
|
|
8851
|
-
return result;
|
|
8852
|
-
}
|
|
8853
9044
|
async function loadAndRenderFile2(context, filepath) {
|
|
8854
9045
|
if (isTemplate(filepath)) {
|
|
8855
9046
|
const result = await processFile(filepath, {
|