@noormdev/sdk 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
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-
|
|
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
5
|
import { observer, getEnvConfigName, ObserverRelay, isDebug } from './chunk-6LG4ENOJ.js';
|
|
6
6
|
export { observer as noormObserver } from './chunk-6LG4ENOJ.js';
|
|
7
7
|
import { explainConnectionError, DatabaseConnectionError } from './chunk-OXTXFKWJ.js';
|
|
@@ -13,7 +13,7 @@ import { __commonJS, __toESM } from './chunk-D2XW2UOP.js';
|
|
|
13
13
|
import { existsSync, accessSync, constants, readFileSync, mkdirSync, chmodSync, createWriteStream, writeFileSync, openSync, writeSync, closeSync, unlinkSync, statSync, copyFileSync, fsyncSync, renameSync } from 'fs';
|
|
14
14
|
import path, { join, dirname, resolve, relative, sep } from 'path';
|
|
15
15
|
import { homedir, availableParallelism, userInfo } from 'os';
|
|
16
|
-
import { stat, mkdir, writeFile, unlink, rename, rm,
|
|
16
|
+
import { stat, mkdir, writeFile, unlink, rename, rm, readFile, readdir, access } from 'fs/promises';
|
|
17
17
|
import { createHash, createPrivateKey, createPublicKey, createDecipheriv, randomBytes, createCipheriv, randomUUID, hkdfSync } from 'crypto';
|
|
18
18
|
import { execSync } from 'child_process';
|
|
19
19
|
import { sql } from 'kysely';
|
|
@@ -1731,9 +1731,12 @@ var DEFAULT_BUILD_CONFIG = {
|
|
|
1731
1731
|
include: [],
|
|
1732
1732
|
exclude: []
|
|
1733
1733
|
};
|
|
1734
|
+
var DEFAULT_DB_DIR = "db";
|
|
1735
|
+
var DEFAULT_SQL_PATH = `./${DEFAULT_DB_DIR}/sql`;
|
|
1736
|
+
var DEFAULT_CHANGES_PATH = `./${DEFAULT_DB_DIR}/changes`;
|
|
1734
1737
|
var DEFAULT_PATH_CONFIG = {
|
|
1735
|
-
sql:
|
|
1736
|
-
changes:
|
|
1738
|
+
sql: DEFAULT_SQL_PATH,
|
|
1739
|
+
changes: DEFAULT_CHANGES_PATH
|
|
1737
1740
|
};
|
|
1738
1741
|
var DEFAULT_STRICT_CONFIG = {
|
|
1739
1742
|
enabled: false,
|
|
@@ -2108,6 +2111,10 @@ var SettingsManager = class {
|
|
|
2108
2111
|
/**
|
|
2109
2112
|
* Initialize a new settings file with defaults.
|
|
2110
2113
|
*
|
|
2114
|
+
* An overwrite keeps the `paths` already in the file: they say where the
|
|
2115
|
+
* project's files are, not a preference, and resetting them would point a
|
|
2116
|
+
* project laid out elsewhere at directories that do not exist.
|
|
2117
|
+
*
|
|
2111
2118
|
* @param force - Overwrite existing file if true
|
|
2112
2119
|
* @throws Error if file exists and force is false
|
|
2113
2120
|
*/
|
|
@@ -2116,7 +2123,10 @@ var SettingsManager = class {
|
|
|
2116
2123
|
if (fileExists2 && !force) {
|
|
2117
2124
|
throw new Error("Settings file already exists. Use force=true to overwrite.");
|
|
2118
2125
|
}
|
|
2126
|
+
const [previous] = fileExists2 ? await attempt(() => this.load()) : [void 0];
|
|
2127
|
+
const keptPaths = previous ? this.#document?.paths : void 0;
|
|
2119
2128
|
this.#document = createDefaultSettings();
|
|
2129
|
+
if (keptPaths) this.#document.paths = clone(keptPaths);
|
|
2120
2130
|
this.#refreshResolved();
|
|
2121
2131
|
this.#loaded = true;
|
|
2122
2132
|
await this.save();
|
|
@@ -4254,7 +4264,7 @@ function buildMssqlTvfWithTvp(rawName, params) {
|
|
|
4254
4264
|
|
|
4255
4265
|
// src/core/change/types.ts
|
|
4256
4266
|
function isPendingChange(item) {
|
|
4257
|
-
return !item.orphaned && (item.status === "pending" || item.status === "reverted" || item.status === "stale");
|
|
4267
|
+
return !item.orphaned && (item.status === "pending" || item.status === "reverted" || item.status === "stale" || item.status === "failed" && item.isNew);
|
|
4258
4268
|
}
|
|
4259
4269
|
var ChangeValidationError = class extends Error {
|
|
4260
4270
|
constructor(changeName, issue) {
|
|
@@ -4340,10 +4350,31 @@ async function parseChange(folderPath, sqlDir) {
|
|
|
4340
4350
|
hasChangelog
|
|
4341
4351
|
};
|
|
4342
4352
|
}
|
|
4353
|
+
function failureListItem(failure, status) {
|
|
4354
|
+
const { date, description } = parseName(failure.name);
|
|
4355
|
+
return {
|
|
4356
|
+
name: failure.name,
|
|
4357
|
+
path: failure.path,
|
|
4358
|
+
date,
|
|
4359
|
+
description,
|
|
4360
|
+
status: status?.status ?? "failed",
|
|
4361
|
+
appliedAt: status?.appliedAt ?? null,
|
|
4362
|
+
appliedBy: status?.appliedBy ?? null,
|
|
4363
|
+
revertedAt: status?.revertedAt ?? null,
|
|
4364
|
+
errorMessage: status ? status.errorMessage : failure.message,
|
|
4365
|
+
appliedHistoryId: status?.appliedHistoryId ?? null,
|
|
4366
|
+
isNew: !status,
|
|
4367
|
+
orphaned: false
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4343
4370
|
async function discoverChanges(changesDir, sqlDir) {
|
|
4371
|
+
const { changes } = await scanChanges(changesDir, sqlDir);
|
|
4372
|
+
return changes;
|
|
4373
|
+
}
|
|
4374
|
+
async function scanChanges(changesDir, sqlDir) {
|
|
4344
4375
|
const [exists] = await attempt(() => stat(changesDir));
|
|
4345
4376
|
if (!exists) {
|
|
4346
|
-
return [];
|
|
4377
|
+
return { changes: [], failures: [] };
|
|
4347
4378
|
}
|
|
4348
4379
|
const [entries, readErr] = await attempt(() => readdir(changesDir, { withFileTypes: true }));
|
|
4349
4380
|
if (readErr) {
|
|
@@ -4352,12 +4383,14 @@ async function discoverChanges(changesDir, sqlDir) {
|
|
|
4352
4383
|
error: readErr,
|
|
4353
4384
|
context: { changesDir, operation: "discover" }
|
|
4354
4385
|
});
|
|
4355
|
-
return [];
|
|
4386
|
+
return { changes: [], failures: [] };
|
|
4356
4387
|
}
|
|
4357
4388
|
const folders = entries.filter((e) => e.isDirectory());
|
|
4358
4389
|
const changes = [];
|
|
4390
|
+
const failures = [];
|
|
4359
4391
|
for (const folder of folders) {
|
|
4360
4392
|
const folderPath = path.join(changesDir, folder.name);
|
|
4393
|
+
if (!await looksLikeChange(folderPath)) continue;
|
|
4361
4394
|
const [change, parseErr] = await attempt(() => parseChange(folderPath, sqlDir));
|
|
4362
4395
|
if (parseErr) {
|
|
4363
4396
|
observer.emit("error", {
|
|
@@ -4365,11 +4398,15 @@ async function discoverChanges(changesDir, sqlDir) {
|
|
|
4365
4398
|
error: parseErr,
|
|
4366
4399
|
context: { folder: folder.name, operation: "parse" }
|
|
4367
4400
|
});
|
|
4401
|
+
failures.push({ name: folder.name, path: folderPath, message: parseErr.message });
|
|
4368
4402
|
continue;
|
|
4369
4403
|
}
|
|
4370
4404
|
changes.push(change);
|
|
4371
4405
|
}
|
|
4372
|
-
return
|
|
4406
|
+
return {
|
|
4407
|
+
changes: changes.sort((a, b) => a.name.localeCompare(b.name)),
|
|
4408
|
+
failures
|
|
4409
|
+
};
|
|
4373
4410
|
}
|
|
4374
4411
|
async function resolveManifest(manifestPath, sqlDir) {
|
|
4375
4412
|
const [content, readErr] = await attempt(() => readFile(manifestPath, "utf-8"));
|
|
@@ -4428,6 +4465,13 @@ function parseName(name) {
|
|
|
4428
4465
|
}
|
|
4429
4466
|
return { date: null, description: name };
|
|
4430
4467
|
}
|
|
4468
|
+
async function looksLikeChange(folderPath) {
|
|
4469
|
+
for (const subfolder of ["change", "revert"]) {
|
|
4470
|
+
const [entry] = await attempt(() => stat(path.join(folderPath, subfolder)));
|
|
4471
|
+
if (entry?.isDirectory()) return true;
|
|
4472
|
+
}
|
|
4473
|
+
return false;
|
|
4474
|
+
}
|
|
4431
4475
|
async function scanFolder(folderPath, sqlDir) {
|
|
4432
4476
|
const [entries, err] = await attempt(() => readdir(folderPath, { withFileTypes: true }));
|
|
4433
4477
|
if (err) {
|
|
@@ -4786,6 +4830,33 @@ function hydrateDate(value, dialect) {
|
|
|
4786
4830
|
}
|
|
4787
4831
|
return /* @__PURE__ */ new Date(`${value.replace(" ", "T")}Z`);
|
|
4788
4832
|
}
|
|
4833
|
+
function decideNeedsRunFile(record, checksum, force) {
|
|
4834
|
+
if (force) {
|
|
4835
|
+
return { needsRun: true, reason: "force" };
|
|
4836
|
+
}
|
|
4837
|
+
if (record instanceof Error || !record) {
|
|
4838
|
+
return { needsRun: true, reason: "new" };
|
|
4839
|
+
}
|
|
4840
|
+
if (record.exec_status === "failed") {
|
|
4841
|
+
return {
|
|
4842
|
+
needsRun: true,
|
|
4843
|
+
reason: "failed",
|
|
4844
|
+
previousChecksum: record.checksum
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
if (record.checksum !== checksum) {
|
|
4848
|
+
return {
|
|
4849
|
+
needsRun: true,
|
|
4850
|
+
reason: "changed",
|
|
4851
|
+
previousChecksum: record.checksum
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
return {
|
|
4855
|
+
needsRun: false,
|
|
4856
|
+
skipReason: "already applied",
|
|
4857
|
+
previousChecksum: record.checksum
|
|
4858
|
+
};
|
|
4859
|
+
}
|
|
4789
4860
|
var ChangeHistory = class {
|
|
4790
4861
|
#db;
|
|
4791
4862
|
#ndb;
|
|
@@ -4965,31 +5036,19 @@ var ChangeHistory = class {
|
|
|
4965
5036
|
};
|
|
4966
5037
|
}
|
|
4967
5038
|
/**
|
|
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.
|
|
5039
|
+
* Fetch the newest qualifying execution row per filepath for one
|
|
5040
|
+
* change and direction.
|
|
5041
|
+
*
|
|
5042
|
+
* Two SELECTs for any number of files: the direction boundary, then the
|
|
5043
|
+
* same query form as `Tracker.latestExecutions`.
|
|
5044
|
+
*
|
|
5045
|
+
* Rows are scoped to this change's name+direction+config, so file A's
|
|
5046
|
+
* success under one change never satisfies file A's check under another.
|
|
5047
|
+
*
|
|
5048
|
+
* `pending` and `skipped` rows never count. Neither records an outcome:
|
|
5049
|
+
* `pending` is a file its batch never reached, and `skipped` is written by
|
|
5050
|
+
* `skipRemainingFiles` for the same reason, or by older versions for a
|
|
5051
|
+
* file that matched a success still further back in history.
|
|
4993
5052
|
*
|
|
4994
5053
|
* A prior success only licenses a skip while it is still *standing*.
|
|
4995
5054
|
* Two things retire it, and neither is visible on the execution row
|
|
@@ -5010,72 +5069,56 @@ var ChangeHistory = class {
|
|
|
5010
5069
|
*
|
|
5011
5070
|
* @param name - Change name
|
|
5012
5071
|
* @param direction - 'change' or 'revert'
|
|
5013
|
-
* @
|
|
5014
|
-
*
|
|
5015
|
-
* @
|
|
5016
|
-
*
|
|
5072
|
+
* @returns Map of relative filepath to its newest qualifying row, or the lookup error
|
|
5073
|
+
*
|
|
5074
|
+
* @example
|
|
5075
|
+
* ```typescript
|
|
5076
|
+
* const [latest, err] = await history.latestFileExecutions('2024-01-15-add-users', 'change')
|
|
5077
|
+
* const result = decideNeedsRunFile(err ?? latest.get(filepath), checksum, false)
|
|
5078
|
+
* ```
|
|
5017
5079
|
*/
|
|
5018
|
-
async
|
|
5019
|
-
|
|
5020
|
-
return { needsRun: true, reason: "force" };
|
|
5021
|
-
}
|
|
5080
|
+
async latestFileExecutions(name, direction) {
|
|
5081
|
+
const { executions, change } = this.#tables;
|
|
5022
5082
|
const opposite = direction === "change" ? "revert" : "change";
|
|
5023
5083
|
const [boundary, boundaryErr] = await attempt(
|
|
5024
|
-
() => this.#ndb.selectFrom(
|
|
5084
|
+
() => 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
5085
|
);
|
|
5026
5086
|
if (boundaryErr) {
|
|
5027
5087
|
observer.emit("error", {
|
|
5028
5088
|
source: "change",
|
|
5029
5089
|
error: boundaryErr,
|
|
5030
|
-
context: { name,
|
|
5090
|
+
context: { name, operation: "latest-file-executions-boundary" }
|
|
5031
5091
|
});
|
|
5032
|
-
return
|
|
5092
|
+
return [null, boundaryErr];
|
|
5093
|
+
}
|
|
5094
|
+
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"]);
|
|
5095
|
+
if (boundary) {
|
|
5096
|
+
qualifying = qualifying.where(`${change}.id`, ">", boundary.id);
|
|
5033
5097
|
}
|
|
5034
|
-
const
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
`${this.#tables.change}.id`,
|
|
5038
|
-
`${this.#tables.executions}.change_id`
|
|
5098
|
+
const newestIds = qualifying.select((eb) => eb.fn.max(`${executions}.id`).as("id")).groupBy(`${executions}.filepath`).as("newest");
|
|
5099
|
+
const [rows, err] = await attempt(
|
|
5100
|
+
() => (
|
|
5039
5101
|
// 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
|
-
});
|
|
5102
|
+
this.#ndb.selectFrom(executions).innerJoin(newestIds, "newest.id", `${executions}.id`).select((eb) => [
|
|
5103
|
+
eb.ref(`${executions}.filepath`).as("filepath"),
|
|
5104
|
+
eb.ref(`${executions}.checksum`).as("checksum"),
|
|
5105
|
+
eb.ref(`${executions}.status`).as("exec_status")
|
|
5106
|
+
]).execute()
|
|
5107
|
+
)
|
|
5108
|
+
);
|
|
5049
5109
|
if (err) {
|
|
5050
5110
|
observer.emit("error", {
|
|
5051
5111
|
source: "change",
|
|
5052
5112
|
error: err,
|
|
5053
|
-
context: { name,
|
|
5113
|
+
context: { name, operation: "latest-file-executions" }
|
|
5054
5114
|
});
|
|
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
|
-
};
|
|
5115
|
+
return [null, err];
|
|
5066
5116
|
}
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
reason: "changed",
|
|
5071
|
-
previousChecksum: record.checksum
|
|
5072
|
-
};
|
|
5117
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5118
|
+
for (const { filepath, ...record } of rows) {
|
|
5119
|
+
latest.set(filepath, record);
|
|
5073
5120
|
}
|
|
5074
|
-
return
|
|
5075
|
-
needsRun: false,
|
|
5076
|
-
skipReason: "already applied",
|
|
5077
|
-
previousChecksum: record.checksum
|
|
5078
|
-
};
|
|
5121
|
+
return [latest, null];
|
|
5079
5122
|
}
|
|
5080
5123
|
// ─────────────────────────────────────────────────────────
|
|
5081
5124
|
// Create Records
|
|
@@ -5113,9 +5156,10 @@ var ChangeHistory = class {
|
|
|
5113
5156
|
return id;
|
|
5114
5157
|
}
|
|
5115
5158
|
/**
|
|
5116
|
-
* Create pending file records for
|
|
5159
|
+
* Create pending file records for the files about to run.
|
|
5117
5160
|
*
|
|
5118
|
-
*
|
|
5161
|
+
* The pending rows let `skipRemainingFiles` mark the files a
|
|
5162
|
+
* failure left unreached.
|
|
5119
5163
|
*
|
|
5120
5164
|
* @returns Error message if creation failed, null on success
|
|
5121
5165
|
*/
|
|
@@ -5481,6 +5525,44 @@ var ChangeHistory = class {
|
|
|
5481
5525
|
};
|
|
5482
5526
|
|
|
5483
5527
|
// src/core/runner/tracker.ts
|
|
5528
|
+
function decideNeedsRun(record, checksum, force) {
|
|
5529
|
+
if (record instanceof Error) {
|
|
5530
|
+
return { needsRun: true, reason: "error" };
|
|
5531
|
+
}
|
|
5532
|
+
if (!record) {
|
|
5533
|
+
return { needsRun: true, reason: "new" };
|
|
5534
|
+
}
|
|
5535
|
+
if (record.exec_status === "failed") {
|
|
5536
|
+
return {
|
|
5537
|
+
needsRun: true,
|
|
5538
|
+
reason: "failed",
|
|
5539
|
+
previousChecksum: record.checksum
|
|
5540
|
+
};
|
|
5541
|
+
}
|
|
5542
|
+
const isUnchangedSkip = record.exec_status === "skipped" && record.skip_reason === "unchanged";
|
|
5543
|
+
if (record.exec_status === "pending" || record.exec_status === "skipped" && !isUnchangedSkip) {
|
|
5544
|
+
return { needsRun: true, reason: "new" };
|
|
5545
|
+
}
|
|
5546
|
+
if (record.change_status === "stale") {
|
|
5547
|
+
return {
|
|
5548
|
+
needsRun: true,
|
|
5549
|
+
reason: "stale",
|
|
5550
|
+
previousChecksum: record.checksum
|
|
5551
|
+
};
|
|
5552
|
+
}
|
|
5553
|
+
if (record.checksum !== checksum) {
|
|
5554
|
+
return {
|
|
5555
|
+
needsRun: true,
|
|
5556
|
+
reason: "changed",
|
|
5557
|
+
previousChecksum: record.checksum
|
|
5558
|
+
};
|
|
5559
|
+
}
|
|
5560
|
+
return {
|
|
5561
|
+
needsRun: false,
|
|
5562
|
+
skipReason: "unchanged",
|
|
5563
|
+
previousChecksum: record.checksum
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5484
5566
|
var Tracker = class {
|
|
5485
5567
|
#db;
|
|
5486
5568
|
#ndb;
|
|
@@ -5495,89 +5577,45 @@ var Tracker = class {
|
|
|
5495
5577
|
this.#dialect = dialect;
|
|
5496
5578
|
}
|
|
5497
5579
|
/**
|
|
5498
|
-
*
|
|
5580
|
+
* Fetch the newest execution row per filepath for this config.
|
|
5499
5581
|
*
|
|
5500
|
-
*
|
|
5501
|
-
*
|
|
5502
|
-
* -
|
|
5503
|
-
* - Previous execution failed
|
|
5504
|
-
* - Parent change is stale (schema was torn down)
|
|
5505
|
-
* - Checksum differs (file changed)
|
|
5582
|
+
* One SELECT for any number of files, so a batch can decide every file
|
|
5583
|
+
* with `decideNeedsRun` before writing any tracking rows. Query form:
|
|
5584
|
+
* `docs/design/prefetch-run-gate.md`.
|
|
5506
5585
|
*
|
|
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()
|
|
5586
|
+
* @returns Map of filepath to its newest record, or the lookup error
|
|
5587
|
+
*
|
|
5588
|
+
* @example
|
|
5589
|
+
* ```typescript
|
|
5590
|
+
* const [latest, err] = await tracker.latestExecutions()
|
|
5591
|
+
* const result = decideNeedsRun(err ?? latest.get(filepath), checksum, false)
|
|
5592
|
+
* ```
|
|
5593
|
+
*/
|
|
5594
|
+
async latestExecutions() {
|
|
5595
|
+
const { executions, change } = this.#tables;
|
|
5596
|
+
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");
|
|
5597
|
+
const [rows, err] = await attempt(
|
|
5598
|
+
() => this.#ndb.selectFrom(executions).innerJoin(change, `${change}.id`, `${executions}.change_id`).innerJoin(newestIds, "newest.id", `${executions}.id`).select((eb) => [
|
|
5599
|
+
eb.ref(`${executions}.filepath`).as("filepath"),
|
|
5600
|
+
eb.ref(`${executions}.checksum`).as("checksum"),
|
|
5601
|
+
eb.ref(`${executions}.status`).as("exec_status"),
|
|
5602
|
+
eb.ref(`${executions}.skip_reason`).as("skip_reason"),
|
|
5603
|
+
eb.ref(`${change}.status`).as("change_status")
|
|
5604
|
+
]).execute()
|
|
5539
5605
|
);
|
|
5540
5606
|
if (err) {
|
|
5541
5607
|
observer.emit("error", {
|
|
5542
5608
|
source: "runner",
|
|
5543
5609
|
error: err,
|
|
5544
|
-
context: {
|
|
5610
|
+
context: { operation: "latest-executions" }
|
|
5545
5611
|
});
|
|
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" };
|
|
5561
|
-
}
|
|
5562
|
-
if (record.change_status === "stale") {
|
|
5563
|
-
return {
|
|
5564
|
-
needsRun: true,
|
|
5565
|
-
reason: "stale",
|
|
5566
|
-
previousChecksum: record.checksum
|
|
5567
|
-
};
|
|
5612
|
+
return [null, err];
|
|
5568
5613
|
}
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
reason: "changed",
|
|
5573
|
-
previousChecksum: record.checksum
|
|
5574
|
-
};
|
|
5614
|
+
const latest = /* @__PURE__ */ new Map();
|
|
5615
|
+
for (const { filepath, ...record } of rows) {
|
|
5616
|
+
latest.set(filepath, record);
|
|
5575
5617
|
}
|
|
5576
|
-
return
|
|
5577
|
-
needsRun: false,
|
|
5578
|
-
skipReason: "unchanged",
|
|
5579
|
-
previousChecksum: record.checksum
|
|
5580
|
-
};
|
|
5618
|
+
return [latest, null];
|
|
5581
5619
|
}
|
|
5582
5620
|
/**
|
|
5583
5621
|
* Create a new operation record.
|
|
@@ -5613,35 +5651,6 @@ var Tracker = class {
|
|
|
5613
5651
|
}
|
|
5614
5652
|
return id;
|
|
5615
5653
|
}
|
|
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
5654
|
/**
|
|
5646
5655
|
* Finalize an operation.
|
|
5647
5656
|
*
|
|
@@ -5689,10 +5698,10 @@ var Tracker = class {
|
|
|
5689
5698
|
// Batch File Operations (Shared by Runner and Changes)
|
|
5690
5699
|
// ─────────────────────────────────────────────────────────
|
|
5691
5700
|
/**
|
|
5692
|
-
* Create pending file records for
|
|
5701
|
+
* Create pending file records for the files about to run.
|
|
5693
5702
|
*
|
|
5694
|
-
*
|
|
5695
|
-
*
|
|
5703
|
+
* The pending rows let `skipRemainingFiles` mark the files a
|
|
5704
|
+
* failure left unreached.
|
|
5696
5705
|
*
|
|
5697
5706
|
* @param operationId - Parent operation ID
|
|
5698
5707
|
* @param files - Files to create records for
|
|
@@ -5732,23 +5741,15 @@ var Tracker = class {
|
|
|
5732
5741
|
* @param durationMs - Execution time
|
|
5733
5742
|
* @param errorMessage - Error message if failed
|
|
5734
5743
|
* @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
5744
|
* @returns Error message if update failed, null on success
|
|
5743
5745
|
*/
|
|
5744
|
-
async updateFileExecution(operationId, filepath, status, durationMs, errorMessage, skipReason
|
|
5746
|
+
async updateFileExecution(operationId, filepath, status, durationMs, errorMessage, skipReason) {
|
|
5745
5747
|
const [result, err] = await attempt(
|
|
5746
5748
|
() => this.#ndb.updateTable(this.#tables.executions).set({
|
|
5747
5749
|
status,
|
|
5748
5750
|
duration_ms: Math.round(durationMs),
|
|
5749
5751
|
error_message: errorMessage ?? "",
|
|
5750
|
-
skip_reason: skipReason ?? ""
|
|
5751
|
-
...checksum === void 0 ? {} : { checksum }
|
|
5752
|
+
skip_reason: skipReason ?? ""
|
|
5752
5753
|
}).where("change_id", "=", operationId).where("filepath", "=", filepath).executeTakeFirst()
|
|
5753
5754
|
);
|
|
5754
5755
|
if (err) {
|
|
@@ -5842,7 +5843,6 @@ var Tracker = class {
|
|
|
5842
5843
|
/**
|
|
5843
5844
|
* Check if a change needs to run by name.
|
|
5844
5845
|
*
|
|
5845
|
-
* Similar to needsRun but checks by change name instead of filepath.
|
|
5846
5846
|
* Used for change sets where we track by change name, not individual files.
|
|
5847
5847
|
*
|
|
5848
5848
|
* @param name - Change name
|
|
@@ -6363,105 +6363,264 @@ var StatementWatcher = class {
|
|
|
6363
6363
|
}
|
|
6364
6364
|
};
|
|
6365
6365
|
|
|
6366
|
-
// src/core/
|
|
6367
|
-
var
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
waitTimeout: 30 * 1e3,
|
|
6373
|
-
// 30 seconds
|
|
6374
|
-
pollInterval: 1e3
|
|
6375
|
-
// 1 second
|
|
6376
|
-
};
|
|
6377
|
-
|
|
6378
|
-
// src/core/lock/errors.ts
|
|
6379
|
-
var LockAcquireError = class extends Error {
|
|
6380
|
-
constructor(configName, holder, heldSince, expiresAt, reason) {
|
|
6381
|
-
const since = heldSince.toISOString();
|
|
6382
|
-
const expires = expiresAt.toISOString();
|
|
6383
|
-
const reasonSuffix = reason ? ` (${reason})` : "";
|
|
6384
|
-
super(
|
|
6385
|
-
`Lock for '${configName}' held by ${holder} since ${since}, expires ${expires}${reasonSuffix}`
|
|
6386
|
-
);
|
|
6387
|
-
this.configName = configName;
|
|
6388
|
-
this.holder = holder;
|
|
6389
|
-
this.heldSince = heldSince;
|
|
6390
|
-
this.expiresAt = expiresAt;
|
|
6391
|
-
this.reason = reason;
|
|
6392
|
-
}
|
|
6393
|
-
name = "LockAcquireError";
|
|
6394
|
-
};
|
|
6395
|
-
var LockExpiredError = class extends Error {
|
|
6396
|
-
constructor(configName, identity, expiredAt) {
|
|
6397
|
-
super(`Lock for '${configName}' expired at ${expiredAt.toISOString()}`);
|
|
6398
|
-
this.configName = configName;
|
|
6399
|
-
this.identity = identity;
|
|
6400
|
-
this.expiredAt = expiredAt;
|
|
6401
|
-
}
|
|
6402
|
-
name = "LockExpiredError";
|
|
6403
|
-
};
|
|
6404
|
-
var LockNotFoundError = class extends Error {
|
|
6405
|
-
constructor(configName, identity) {
|
|
6406
|
-
super(`No lock found for '${configName}' held by ${identity}`);
|
|
6407
|
-
this.configName = configName;
|
|
6408
|
-
this.identity = identity;
|
|
6409
|
-
}
|
|
6410
|
-
name = "LockNotFoundError";
|
|
6411
|
-
};
|
|
6412
|
-
var LockOwnershipError = class extends Error {
|
|
6413
|
-
constructor(configName, requestedBy, actualHolder) {
|
|
6414
|
-
super(
|
|
6415
|
-
`Cannot release lock for '${configName}': held by ${actualHolder}, not ${requestedBy}`
|
|
6416
|
-
);
|
|
6417
|
-
this.configName = configName;
|
|
6418
|
-
this.requestedBy = requestedBy;
|
|
6419
|
-
this.actualHolder = actualHolder;
|
|
6420
|
-
}
|
|
6421
|
-
name = "LockOwnershipError";
|
|
6366
|
+
// src/core/runner/sqlite-statements.ts
|
|
6367
|
+
var QUOTE_PAIRS = {
|
|
6368
|
+
"'": "'",
|
|
6369
|
+
'"': '"',
|
|
6370
|
+
"`": "`",
|
|
6371
|
+
"[": "]"
|
|
6422
6372
|
};
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
if (dialect === "mssql") {
|
|
6430
|
-
return date;
|
|
6431
|
-
}
|
|
6432
|
-
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
6433
|
-
}
|
|
6434
|
-
function parseDateFromDialect(value, dialect) {
|
|
6435
|
-
if (dialect === "sqlite" || dialect === "mssql") {
|
|
6436
|
-
return new Date(value);
|
|
6437
|
-
}
|
|
6438
|
-
if (value instanceof Date) {
|
|
6439
|
-
return new Date(Date.UTC(
|
|
6440
|
-
value.getFullYear(),
|
|
6441
|
-
value.getMonth(),
|
|
6442
|
-
value.getDate(),
|
|
6443
|
-
value.getHours(),
|
|
6444
|
-
value.getMinutes(),
|
|
6445
|
-
value.getSeconds(),
|
|
6446
|
-
value.getMilliseconds()
|
|
6447
|
-
));
|
|
6448
|
-
}
|
|
6449
|
-
const normalized = String(value).replace(" ", "T");
|
|
6450
|
-
return new Date(normalized.endsWith("Z") ? normalized : `${normalized}Z`);
|
|
6373
|
+
function keywordAt(content, index, keyword) {
|
|
6374
|
+
const end = index + keyword.length;
|
|
6375
|
+
if (content.slice(index, end).toUpperCase() !== keyword) return false;
|
|
6376
|
+
const before = index > 0 ? content[index - 1] : " ";
|
|
6377
|
+
const after = end < content.length ? content[end] : " ";
|
|
6378
|
+
return !/[A-Za-z0-9_$]/.test(before) && !/[A-Za-z0-9_$]/.test(after);
|
|
6451
6379
|
}
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6380
|
+
function splitSqliteStatements(content) {
|
|
6381
|
+
const statements = [];
|
|
6382
|
+
let current = "";
|
|
6383
|
+
let closingQuote = null;
|
|
6384
|
+
let inLineComment = false;
|
|
6385
|
+
let inBlockComment = false;
|
|
6386
|
+
let inTrigger = false;
|
|
6387
|
+
let blockDepth = 0;
|
|
6388
|
+
let i = 0;
|
|
6389
|
+
const flush = () => {
|
|
6390
|
+
const trimmed = current.trim();
|
|
6391
|
+
if (trimmed.length > 0 && !isCommentOnly(trimmed)) {
|
|
6392
|
+
statements.push(trimmed);
|
|
6393
|
+
}
|
|
6394
|
+
current = "";
|
|
6395
|
+
};
|
|
6396
|
+
while (i < content.length) {
|
|
6397
|
+
const char = content[i];
|
|
6398
|
+
if (inLineComment) {
|
|
6399
|
+
current += char;
|
|
6400
|
+
if (char === "\n") inLineComment = false;
|
|
6401
|
+
i++;
|
|
6402
|
+
continue;
|
|
6403
|
+
}
|
|
6404
|
+
if (inBlockComment) {
|
|
6405
|
+
current += char;
|
|
6406
|
+
if (char === "*" && content[i + 1] === "/") {
|
|
6407
|
+
current += "/";
|
|
6408
|
+
inBlockComment = false;
|
|
6409
|
+
i += 2;
|
|
6410
|
+
continue;
|
|
6411
|
+
}
|
|
6412
|
+
i++;
|
|
6413
|
+
continue;
|
|
6414
|
+
}
|
|
6415
|
+
if (closingQuote !== null) {
|
|
6416
|
+
current += char;
|
|
6417
|
+
if (char === closingQuote) {
|
|
6418
|
+
if (closingQuote !== "]" && content[i + 1] === closingQuote) {
|
|
6419
|
+
current += closingQuote;
|
|
6420
|
+
i += 2;
|
|
6421
|
+
continue;
|
|
6422
|
+
}
|
|
6423
|
+
closingQuote = null;
|
|
6424
|
+
}
|
|
6425
|
+
i++;
|
|
6426
|
+
continue;
|
|
6427
|
+
}
|
|
6428
|
+
if (char === "-" && content[i + 1] === "-") {
|
|
6429
|
+
inLineComment = true;
|
|
6430
|
+
current += "--";
|
|
6431
|
+
i += 2;
|
|
6432
|
+
continue;
|
|
6433
|
+
}
|
|
6434
|
+
if (char === "/" && content[i + 1] === "*") {
|
|
6435
|
+
inBlockComment = true;
|
|
6436
|
+
current += "/*";
|
|
6437
|
+
i += 2;
|
|
6438
|
+
continue;
|
|
6439
|
+
}
|
|
6440
|
+
if (QUOTE_PAIRS[char]) {
|
|
6441
|
+
closingQuote = QUOTE_PAIRS[char];
|
|
6442
|
+
current += char;
|
|
6443
|
+
i++;
|
|
6444
|
+
continue;
|
|
6445
|
+
}
|
|
6446
|
+
if (!inTrigger && keywordAt(content, i, "TRIGGER")) {
|
|
6447
|
+
inTrigger = true;
|
|
6448
|
+
current += content.slice(i, i + 7);
|
|
6449
|
+
i += 7;
|
|
6450
|
+
continue;
|
|
6451
|
+
}
|
|
6452
|
+
if (inTrigger && (keywordAt(content, i, "BEGIN") || keywordAt(content, i, "CASE"))) {
|
|
6453
|
+
const length = keywordAt(content, i, "BEGIN") ? 5 : 4;
|
|
6454
|
+
blockDepth++;
|
|
6455
|
+
current += content.slice(i, i + length);
|
|
6456
|
+
i += length;
|
|
6457
|
+
continue;
|
|
6458
|
+
}
|
|
6459
|
+
if (inTrigger && blockDepth > 0 && keywordAt(content, i, "END")) {
|
|
6460
|
+
blockDepth--;
|
|
6461
|
+
if (blockDepth === 0) inTrigger = false;
|
|
6462
|
+
current += content.slice(i, i + 3);
|
|
6463
|
+
i += 3;
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
if (char === ";" && blockDepth === 0) {
|
|
6467
|
+
current += char;
|
|
6468
|
+
inTrigger = false;
|
|
6469
|
+
flush();
|
|
6470
|
+
i++;
|
|
6471
|
+
continue;
|
|
6472
|
+
}
|
|
6473
|
+
current += char;
|
|
6474
|
+
i++;
|
|
6475
|
+
}
|
|
6476
|
+
flush();
|
|
6477
|
+
return statements;
|
|
6478
|
+
}
|
|
6479
|
+
function isCommentOnly(fragment) {
|
|
6480
|
+
const withoutBlockComments = fragment.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
6481
|
+
return withoutBlockComments.split("\n").every((line) => {
|
|
6482
|
+
const trimmed = line.trim();
|
|
6483
|
+
return trimmed.length === 0 || trimmed.startsWith("--");
|
|
6484
|
+
});
|
|
6485
|
+
}
|
|
6486
|
+
|
|
6487
|
+
// src/core/runner/mssql-batches.ts
|
|
6488
|
+
function splitMssqlBatches(content) {
|
|
6489
|
+
const batches = content.split(/^\s*GO\s*$/mi);
|
|
6490
|
+
return batches.map((b) => b.trim()).filter((b) => b.length > 0 && !isCommentOnly2(b));
|
|
6491
|
+
}
|
|
6492
|
+
function isCommentOnly2(batch) {
|
|
6493
|
+
for (const line of batch.split("\n")) {
|
|
6494
|
+
const trimmed = line.trim();
|
|
6495
|
+
if (trimmed.length > 0 && !trimmed.startsWith("--")) {
|
|
6496
|
+
return false;
|
|
6497
|
+
}
|
|
6498
|
+
}
|
|
6499
|
+
return true;
|
|
6500
|
+
}
|
|
6501
|
+
async function executeSqlBody(context, sqlContent) {
|
|
6502
|
+
if (context.dialect === "mssql") {
|
|
6503
|
+
return executeFragments(context, splitMssqlBatches(sqlContent), "batch");
|
|
6504
|
+
}
|
|
6505
|
+
if (context.dialect === "sqlite") {
|
|
6506
|
+
return executeFragments(context, splitSqliteStatements(sqlContent), "statement");
|
|
6507
|
+
}
|
|
6508
|
+
const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
|
|
6509
|
+
if (execErr) {
|
|
6510
|
+
return getSqlErrorMessage(execErr);
|
|
6511
|
+
}
|
|
6512
|
+
return null;
|
|
6513
|
+
}
|
|
6514
|
+
async function executeFragments(context, fragments, label) {
|
|
6515
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
6516
|
+
const [, execErr] = await attempt(() => sql.raw(fragments[i]).execute(context.db));
|
|
6517
|
+
if (execErr) {
|
|
6518
|
+
const message = getSqlErrorMessage(execErr);
|
|
6519
|
+
return fragments.length === 1 ? message : `[${label} ${i + 1} of ${fragments.length}] ${message}`;
|
|
6520
|
+
}
|
|
6521
|
+
}
|
|
6522
|
+
return null;
|
|
6523
|
+
}
|
|
6524
|
+
|
|
6525
|
+
// src/core/lock/types.ts
|
|
6526
|
+
var DEFAULT_LOCK_OPTIONS = {
|
|
6527
|
+
dialect: "postgres",
|
|
6528
|
+
timeout: 5 * 60 * 1e3,
|
|
6529
|
+
// 5 minutes
|
|
6530
|
+
wait: false,
|
|
6531
|
+
waitTimeout: 30 * 1e3,
|
|
6532
|
+
// 30 seconds
|
|
6533
|
+
pollInterval: 1e3
|
|
6534
|
+
// 1 second
|
|
6535
|
+
};
|
|
6536
|
+
|
|
6537
|
+
// src/core/lock/errors.ts
|
|
6538
|
+
var LockAcquireError = class extends Error {
|
|
6539
|
+
constructor(configName, holder, heldSince, expiresAt, reason) {
|
|
6540
|
+
const since = heldSince.toISOString();
|
|
6541
|
+
const expires = expiresAt.toISOString();
|
|
6542
|
+
const reasonSuffix = reason ? ` (${reason})` : "";
|
|
6543
|
+
super(
|
|
6544
|
+
`Lock for '${configName}' held by ${holder} since ${since}, expires ${expires}${reasonSuffix}`
|
|
6545
|
+
);
|
|
6546
|
+
this.configName = configName;
|
|
6547
|
+
this.holder = holder;
|
|
6548
|
+
this.heldSince = heldSince;
|
|
6549
|
+
this.expiresAt = expiresAt;
|
|
6550
|
+
this.reason = reason;
|
|
6551
|
+
}
|
|
6552
|
+
name = "LockAcquireError";
|
|
6553
|
+
};
|
|
6554
|
+
var LockExpiredError = class extends Error {
|
|
6555
|
+
constructor(configName, identity, expiredAt) {
|
|
6556
|
+
super(`Lock for '${configName}' expired at ${expiredAt.toISOString()}`);
|
|
6557
|
+
this.configName = configName;
|
|
6558
|
+
this.identity = identity;
|
|
6559
|
+
this.expiredAt = expiredAt;
|
|
6560
|
+
}
|
|
6561
|
+
name = "LockExpiredError";
|
|
6562
|
+
};
|
|
6563
|
+
var LockNotFoundError = class extends Error {
|
|
6564
|
+
constructor(configName, identity) {
|
|
6565
|
+
super(`No lock found for '${configName}' held by ${identity}`);
|
|
6566
|
+
this.configName = configName;
|
|
6567
|
+
this.identity = identity;
|
|
6568
|
+
}
|
|
6569
|
+
name = "LockNotFoundError";
|
|
6570
|
+
};
|
|
6571
|
+
var LockOwnershipError = class extends Error {
|
|
6572
|
+
constructor(configName, requestedBy, actualHolder) {
|
|
6573
|
+
super(
|
|
6574
|
+
`Cannot release lock for '${configName}': held by ${actualHolder}, not ${requestedBy}`
|
|
6575
|
+
);
|
|
6576
|
+
this.configName = configName;
|
|
6577
|
+
this.requestedBy = requestedBy;
|
|
6578
|
+
this.actualHolder = actualHolder;
|
|
6579
|
+
}
|
|
6580
|
+
name = "LockOwnershipError";
|
|
6581
|
+
};
|
|
6582
|
+
|
|
6583
|
+
// src/core/lock/manager.ts
|
|
6584
|
+
function formatDateForDialect(date, dialect) {
|
|
6585
|
+
if (dialect === "sqlite") {
|
|
6586
|
+
return date.toISOString();
|
|
6587
|
+
}
|
|
6588
|
+
if (dialect === "mssql") {
|
|
6589
|
+
return date;
|
|
6590
|
+
}
|
|
6591
|
+
return date.toISOString().replace("T", " ").replace("Z", "");
|
|
6592
|
+
}
|
|
6593
|
+
function parseDateFromDialect(value, dialect) {
|
|
6594
|
+
if (dialect === "sqlite" || dialect === "mssql") {
|
|
6595
|
+
return new Date(value);
|
|
6596
|
+
}
|
|
6597
|
+
if (value instanceof Date) {
|
|
6598
|
+
return new Date(Date.UTC(
|
|
6599
|
+
value.getFullYear(),
|
|
6600
|
+
value.getMonth(),
|
|
6601
|
+
value.getDate(),
|
|
6602
|
+
value.getHours(),
|
|
6603
|
+
value.getMinutes(),
|
|
6604
|
+
value.getSeconds(),
|
|
6605
|
+
value.getMilliseconds()
|
|
6606
|
+
));
|
|
6607
|
+
}
|
|
6608
|
+
const normalized = String(value).replace(" ", "T");
|
|
6609
|
+
return new Date(normalized.endsWith("Z") ? normalized : `${normalized}Z`);
|
|
6610
|
+
}
|
|
6611
|
+
var LockManager = class {
|
|
6612
|
+
/**
|
|
6613
|
+
* Acquire a lock for a config.
|
|
6614
|
+
*
|
|
6615
|
+
* If the lock is held by another identity and wait=false, throws LockAcquireError.
|
|
6616
|
+
* If wait=true, polls until lock is available or waitTimeout is exceeded.
|
|
6617
|
+
*
|
|
6618
|
+
* @param db - Kysely database instance
|
|
6619
|
+
* @param configName - Config/database scope for the lock
|
|
6620
|
+
* @param identity - Identity string of the requester
|
|
6621
|
+
* @param options - Lock options
|
|
6622
|
+
* @returns Lock state on success
|
|
6623
|
+
* @throws LockAcquireError if lock cannot be acquired
|
|
6465
6624
|
*/
|
|
6466
6625
|
async acquire(db, configName, identity, options = {}) {
|
|
6467
6626
|
const opts = { ...DEFAULT_LOCK_OPTIONS, ...options };
|
|
@@ -7007,6 +7166,12 @@ async function executeFiles(context, change, files, direction, checksum, force,
|
|
|
7007
7166
|
return result;
|
|
7008
7167
|
}
|
|
7009
7168
|
async function runFileBatch(context, change, expandedFiles, direction, checksum, force, history, executor, startTime, watcher) {
|
|
7169
|
+
const fileChecksums = /* @__PURE__ */ new Map();
|
|
7170
|
+
for (const file of expandedFiles) {
|
|
7171
|
+
const [cs] = await attempt(() => computeChecksum(file.path));
|
|
7172
|
+
fileChecksums.set(file.path, cs ?? "");
|
|
7173
|
+
}
|
|
7174
|
+
const skipReasons = await findSkippedFiles(context, change.name, direction, expandedFiles, fileChecksums, force, history);
|
|
7010
7175
|
const [operationId, createErr] = await attempt(
|
|
7011
7176
|
() => history.createOperation({
|
|
7012
7177
|
name: change.name,
|
|
@@ -7022,14 +7187,9 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
7022
7187
|
});
|
|
7023
7188
|
return createFailedResult(change.name, direction, createErr.message, startTime);
|
|
7024
7189
|
}
|
|
7025
|
-
const fileChecksums = /* @__PURE__ */ new Map();
|
|
7026
|
-
for (const file of expandedFiles) {
|
|
7027
|
-
const [cs] = await attempt(() => computeChecksum(file.path));
|
|
7028
|
-
fileChecksums.set(file.path, cs ?? "");
|
|
7029
|
-
}
|
|
7030
7190
|
const createRecordsErr = await history.createFileRecords(
|
|
7031
7191
|
operationId,
|
|
7032
|
-
expandedFiles.map((f) => ({
|
|
7192
|
+
expandedFiles.filter((_, i) => !skipReasons[i]).map((f) => ({
|
|
7033
7193
|
filepath: path.relative(context.projectRoot, f.path),
|
|
7034
7194
|
fileType: f.type,
|
|
7035
7195
|
checksum: fileChecksums.get(f.path) ?? ""
|
|
@@ -7069,36 +7229,15 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
7069
7229
|
const fileStart = performance.now();
|
|
7070
7230
|
const relPath = path.relative(context.projectRoot, file.path);
|
|
7071
7231
|
const fileChecksum = fileChecksums.get(file.path) ?? "";
|
|
7072
|
-
const
|
|
7073
|
-
|
|
7074
|
-
direction,
|
|
7075
|
-
relPath,
|
|
7076
|
-
fileChecksum,
|
|
7077
|
-
force
|
|
7078
|
-
);
|
|
7079
|
-
if (!needsRunFileResult.needsRun) {
|
|
7232
|
+
const skipReason = skipReasons[i];
|
|
7233
|
+
if (skipReason) {
|
|
7080
7234
|
results.push({
|
|
7081
7235
|
filepath: file.path,
|
|
7082
7236
|
checksum: fileChecksum,
|
|
7083
7237
|
status: "skipped",
|
|
7084
|
-
skipReason
|
|
7238
|
+
skipReason,
|
|
7085
7239
|
durationMs: 0
|
|
7086
7240
|
});
|
|
7087
|
-
const skipUpdateErr = await history.updateFileExecution(
|
|
7088
|
-
operationId,
|
|
7089
|
-
relPath,
|
|
7090
|
-
"skipped",
|
|
7091
|
-
0,
|
|
7092
|
-
void 0,
|
|
7093
|
-
needsRunFileResult.skipReason
|
|
7094
|
-
);
|
|
7095
|
-
if (skipUpdateErr) {
|
|
7096
|
-
observer.emit("error", {
|
|
7097
|
-
source: "change",
|
|
7098
|
-
error: new Error(skipUpdateErr),
|
|
7099
|
-
context: { filepath: relPath, operation: "update-skipped-record" }
|
|
7100
|
-
});
|
|
7101
|
-
}
|
|
7102
7241
|
continue;
|
|
7103
7242
|
}
|
|
7104
7243
|
const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile(context, file.path));
|
|
@@ -7130,12 +7269,12 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
7130
7269
|
}
|
|
7131
7270
|
break;
|
|
7132
7271
|
}
|
|
7133
|
-
const [, execErr] = await attempt(
|
|
7134
|
-
() => watcher.run(file.path, executor, (conn) =>
|
|
7272
|
+
const [execMessage, execErr] = await attempt(
|
|
7273
|
+
() => watcher.run(file.path, executor, (conn) => executeSqlBody({ db: conn, dialect: context.dialect }, sqlContent))
|
|
7135
7274
|
);
|
|
7136
7275
|
const durationMs = performance.now() - fileStart;
|
|
7137
|
-
|
|
7138
|
-
|
|
7276
|
+
const errorMessage2 = execErr ? getSqlErrorMessage(execErr) : execMessage;
|
|
7277
|
+
if (errorMessage2) {
|
|
7139
7278
|
failed = true;
|
|
7140
7279
|
failedFile = file.path;
|
|
7141
7280
|
failureError = errorMessage2;
|
|
@@ -7233,6 +7372,22 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
|
|
|
7233
7372
|
operationId
|
|
7234
7373
|
};
|
|
7235
7374
|
}
|
|
7375
|
+
async function findSkippedFiles(context, changeName, direction, files, fileChecksums, force, history) {
|
|
7376
|
+
const skipReasons = [];
|
|
7377
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7378
|
+
const [latest, latestErr] = force ? [null, null] : await history.latestFileExecutions(changeName, direction);
|
|
7379
|
+
for (const file of files) {
|
|
7380
|
+
const relPath = path.relative(context.projectRoot, file.path);
|
|
7381
|
+
if (seen.has(relPath)) {
|
|
7382
|
+
skipReasons.push("already applied");
|
|
7383
|
+
continue;
|
|
7384
|
+
}
|
|
7385
|
+
seen.add(relPath);
|
|
7386
|
+
const decision = decideNeedsRunFile(latestErr ?? latest?.get(relPath), fileChecksums.get(file.path) ?? "", force);
|
|
7387
|
+
skipReasons.push(decision.needsRun ? void 0 : decision.skipReason);
|
|
7388
|
+
}
|
|
7389
|
+
return skipReasons;
|
|
7390
|
+
}
|
|
7236
7391
|
async function executePreview(context, change, files, direction, output, startTime) {
|
|
7237
7392
|
const expandedFiles = await expandFiles(files, context.sqlDir);
|
|
7238
7393
|
const results = [];
|
|
@@ -7457,17 +7612,31 @@ var ChangeManager = class {
|
|
|
7457
7612
|
* List all changes with status.
|
|
7458
7613
|
*
|
|
7459
7614
|
* Merges changes from disk with status from database.
|
|
7460
|
-
* Includes orphaned changes (in DB but not on disk)
|
|
7615
|
+
* Includes orphaned changes (in DB but not on disk), and folders on disk
|
|
7616
|
+
* that cannot be parsed: `failed` with the parse error when they never
|
|
7617
|
+
* ran, otherwise their recorded status.
|
|
7461
7618
|
*
|
|
7462
7619
|
* @returns Array of changes with merged status
|
|
7463
7620
|
*/
|
|
7464
7621
|
async list() {
|
|
7465
|
-
const
|
|
7622
|
+
const { items } = await this.#scan();
|
|
7623
|
+
return items;
|
|
7624
|
+
}
|
|
7625
|
+
/**
|
|
7626
|
+
* Merge disk and database state, and report the folders that cannot be read.
|
|
7627
|
+
*
|
|
7628
|
+
* An unreadable folder that a batch would run (never run, or `reverted` or
|
|
7629
|
+
* `stale`) is pending work: the batch fails at its position rather than
|
|
7630
|
+
* skipping it and running later changes out of order. `unreadable` holds the
|
|
7631
|
+
* applied ones; they keep their status and only warrant a warning.
|
|
7632
|
+
*/
|
|
7633
|
+
async #scan() {
|
|
7634
|
+
const { changes: diskChanges, failures } = await scanChanges(
|
|
7466
7635
|
this.#context.changesDir,
|
|
7467
7636
|
this.#context.sqlDir
|
|
7468
7637
|
);
|
|
7469
7638
|
const statuses = await this.#history.getAllStatuses();
|
|
7470
|
-
const diskNames = new Set(diskChanges.map((cs) => cs.name));
|
|
7639
|
+
const diskNames = new Set([...diskChanges, ...failures].map((cs) => cs.name));
|
|
7471
7640
|
const orphanedNames = await this.#history.getOrphaned(diskNames);
|
|
7472
7641
|
const result = [];
|
|
7473
7642
|
for (const cs of diskChanges) {
|
|
@@ -7485,6 +7654,15 @@ var ChangeManager = class {
|
|
|
7485
7654
|
orphaned: false
|
|
7486
7655
|
});
|
|
7487
7656
|
}
|
|
7657
|
+
const unreadable = [];
|
|
7658
|
+
for (const failure of failures) {
|
|
7659
|
+
const status = statuses.get(failure.name);
|
|
7660
|
+
const item = failureListItem(failure, status);
|
|
7661
|
+
if (!isPendingChange(item)) {
|
|
7662
|
+
unreadable.push(failure);
|
|
7663
|
+
}
|
|
7664
|
+
result.push(item);
|
|
7665
|
+
}
|
|
7488
7666
|
for (const name of orphanedNames) {
|
|
7489
7667
|
const status = statuses.get(name);
|
|
7490
7668
|
result.push({
|
|
@@ -7506,7 +7684,10 @@ var ChangeManager = class {
|
|
|
7506
7684
|
hasChangelog: void 0
|
|
7507
7685
|
});
|
|
7508
7686
|
}
|
|
7509
|
-
return
|
|
7687
|
+
return {
|
|
7688
|
+
items: result.sort((a, b) => a.name.localeCompare(b.name)),
|
|
7689
|
+
unreadable
|
|
7690
|
+
};
|
|
7510
7691
|
}
|
|
7511
7692
|
// ─────────────────────────────────────────────────────────
|
|
7512
7693
|
// Run Single Change
|
|
@@ -7561,9 +7742,13 @@ var ChangeManager = class {
|
|
|
7561
7742
|
async next(count = 1, options = {}) {
|
|
7562
7743
|
const opts = { ...DEFAULT_BATCH, ...options };
|
|
7563
7744
|
const start = performance.now();
|
|
7564
|
-
const
|
|
7565
|
-
const
|
|
7566
|
-
|
|
7745
|
+
const { items, unreadable } = await this.#scan();
|
|
7746
|
+
const messages = [
|
|
7747
|
+
...await this.#collectWarnings() ?? [],
|
|
7748
|
+
...unreadable.map((f) => `Change '${f.name}' is applied but its files can no longer be read: ${f.message}`)
|
|
7749
|
+
];
|
|
7750
|
+
const warnings = messages.length > 0 ? messages : void 0;
|
|
7751
|
+
const pending = items.filter(isPendingChange).slice(0, count);
|
|
7567
7752
|
if (pending.length === 0) {
|
|
7568
7753
|
return {
|
|
7569
7754
|
status: "success",
|
|
@@ -7631,9 +7816,7 @@ var ChangeManager = class {
|
|
|
7631
7816
|
* @returns Batch result
|
|
7632
7817
|
*/
|
|
7633
7818
|
async ff(options = {}) {
|
|
7634
|
-
|
|
7635
|
-
const pendingCount = list.filter(isPendingChange).length;
|
|
7636
|
-
return this.next(pendingCount, options);
|
|
7819
|
+
return this.next(Number.POSITIVE_INFINITY, options);
|
|
7637
7820
|
}
|
|
7638
7821
|
/**
|
|
7639
7822
|
* Rewind: revert changes in reverse order.
|
|
@@ -7802,6 +7985,8 @@ var ChangeManager = class {
|
|
|
7802
7985
|
() => parseChange(changePath, this.#context.sqlDir)
|
|
7803
7986
|
);
|
|
7804
7987
|
if (err) {
|
|
7988
|
+
const [onDisk] = await attempt(() => stat(changePath));
|
|
7989
|
+
if (onDisk) throw err;
|
|
7805
7990
|
const status = await this.#history.getStatus(name);
|
|
7806
7991
|
if (status) {
|
|
7807
7992
|
throw new ChangeOrphanedError(name);
|
|
@@ -7903,7 +8088,7 @@ async function resolveVaultKey(db, dialect) {
|
|
|
7903
8088
|
async function buildSecretsContext(stateManager, configName, db, vaultKey, dialect) {
|
|
7904
8089
|
const secrets = {};
|
|
7905
8090
|
if (db && vaultKey) {
|
|
7906
|
-
const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-
|
|
8091
|
+
const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-NQA4H7PK.js');
|
|
7907
8092
|
const vaultSecrets = await getAllVaultSecrets2(db, vaultKey, dialect ?? "postgres");
|
|
7908
8093
|
for (const [key, secret] of Object.entries(vaultSecrets)) {
|
|
7909
8094
|
secrets[key] = secret.value;
|
|
@@ -8427,13 +8612,13 @@ var ChangesNamespace = class {
|
|
|
8427
8612
|
get #changesDir() {
|
|
8428
8613
|
return path.join(
|
|
8429
8614
|
this.#state.projectRoot,
|
|
8430
|
-
this.#state.settings.paths?.changes ??
|
|
8615
|
+
this.#state.settings.paths?.changes ?? DEFAULT_CHANGES_PATH
|
|
8431
8616
|
);
|
|
8432
8617
|
}
|
|
8433
8618
|
get #sqlDir() {
|
|
8434
8619
|
return path.join(
|
|
8435
8620
|
this.#state.projectRoot,
|
|
8436
|
-
this.#state.settings.paths?.sql ??
|
|
8621
|
+
this.#state.settings.paths?.sql ?? DEFAULT_SQL_PATH
|
|
8437
8622
|
);
|
|
8438
8623
|
}
|
|
8439
8624
|
get #kysely() {
|
|
@@ -8466,167 +8651,6 @@ var ChangesNamespace = class {
|
|
|
8466
8651
|
return this.#manager;
|
|
8467
8652
|
}
|
|
8468
8653
|
};
|
|
8469
|
-
|
|
8470
|
-
// src/core/runner/sqlite-statements.ts
|
|
8471
|
-
var QUOTE_PAIRS = {
|
|
8472
|
-
"'": "'",
|
|
8473
|
-
'"': '"',
|
|
8474
|
-
"`": "`",
|
|
8475
|
-
"[": "]"
|
|
8476
|
-
};
|
|
8477
|
-
function keywordAt(content, index, keyword) {
|
|
8478
|
-
const end = index + keyword.length;
|
|
8479
|
-
if (content.slice(index, end).toUpperCase() !== keyword) return false;
|
|
8480
|
-
const before = index > 0 ? content[index - 1] : " ";
|
|
8481
|
-
const after = end < content.length ? content[end] : " ";
|
|
8482
|
-
return !/[A-Za-z0-9_$]/.test(before) && !/[A-Za-z0-9_$]/.test(after);
|
|
8483
|
-
}
|
|
8484
|
-
function splitSqliteStatements(content) {
|
|
8485
|
-
const statements = [];
|
|
8486
|
-
let current = "";
|
|
8487
|
-
let closingQuote = null;
|
|
8488
|
-
let inLineComment = false;
|
|
8489
|
-
let inBlockComment = false;
|
|
8490
|
-
let inTrigger = false;
|
|
8491
|
-
let blockDepth = 0;
|
|
8492
|
-
let i = 0;
|
|
8493
|
-
const flush = () => {
|
|
8494
|
-
const trimmed = current.trim();
|
|
8495
|
-
if (trimmed.length > 0 && !isCommentOnly(trimmed)) {
|
|
8496
|
-
statements.push(trimmed);
|
|
8497
|
-
}
|
|
8498
|
-
current = "";
|
|
8499
|
-
};
|
|
8500
|
-
while (i < content.length) {
|
|
8501
|
-
const char = content[i];
|
|
8502
|
-
if (inLineComment) {
|
|
8503
|
-
current += char;
|
|
8504
|
-
if (char === "\n") inLineComment = false;
|
|
8505
|
-
i++;
|
|
8506
|
-
continue;
|
|
8507
|
-
}
|
|
8508
|
-
if (inBlockComment) {
|
|
8509
|
-
current += char;
|
|
8510
|
-
if (char === "*" && content[i + 1] === "/") {
|
|
8511
|
-
current += "/";
|
|
8512
|
-
inBlockComment = false;
|
|
8513
|
-
i += 2;
|
|
8514
|
-
continue;
|
|
8515
|
-
}
|
|
8516
|
-
i++;
|
|
8517
|
-
continue;
|
|
8518
|
-
}
|
|
8519
|
-
if (closingQuote !== null) {
|
|
8520
|
-
current += char;
|
|
8521
|
-
if (char === closingQuote) {
|
|
8522
|
-
if (closingQuote !== "]" && content[i + 1] === closingQuote) {
|
|
8523
|
-
current += closingQuote;
|
|
8524
|
-
i += 2;
|
|
8525
|
-
continue;
|
|
8526
|
-
}
|
|
8527
|
-
closingQuote = null;
|
|
8528
|
-
}
|
|
8529
|
-
i++;
|
|
8530
|
-
continue;
|
|
8531
|
-
}
|
|
8532
|
-
if (char === "-" && content[i + 1] === "-") {
|
|
8533
|
-
inLineComment = true;
|
|
8534
|
-
current += "--";
|
|
8535
|
-
i += 2;
|
|
8536
|
-
continue;
|
|
8537
|
-
}
|
|
8538
|
-
if (char === "/" && content[i + 1] === "*") {
|
|
8539
|
-
inBlockComment = true;
|
|
8540
|
-
current += "/*";
|
|
8541
|
-
i += 2;
|
|
8542
|
-
continue;
|
|
8543
|
-
}
|
|
8544
|
-
if (QUOTE_PAIRS[char]) {
|
|
8545
|
-
closingQuote = QUOTE_PAIRS[char];
|
|
8546
|
-
current += char;
|
|
8547
|
-
i++;
|
|
8548
|
-
continue;
|
|
8549
|
-
}
|
|
8550
|
-
if (!inTrigger && keywordAt(content, i, "TRIGGER")) {
|
|
8551
|
-
inTrigger = true;
|
|
8552
|
-
current += content.slice(i, i + 7);
|
|
8553
|
-
i += 7;
|
|
8554
|
-
continue;
|
|
8555
|
-
}
|
|
8556
|
-
if (inTrigger && (keywordAt(content, i, "BEGIN") || keywordAt(content, i, "CASE"))) {
|
|
8557
|
-
const length = keywordAt(content, i, "BEGIN") ? 5 : 4;
|
|
8558
|
-
blockDepth++;
|
|
8559
|
-
current += content.slice(i, i + length);
|
|
8560
|
-
i += length;
|
|
8561
|
-
continue;
|
|
8562
|
-
}
|
|
8563
|
-
if (inTrigger && blockDepth > 0 && keywordAt(content, i, "END")) {
|
|
8564
|
-
blockDepth--;
|
|
8565
|
-
if (blockDepth === 0) inTrigger = false;
|
|
8566
|
-
current += content.slice(i, i + 3);
|
|
8567
|
-
i += 3;
|
|
8568
|
-
continue;
|
|
8569
|
-
}
|
|
8570
|
-
if (char === ";" && blockDepth === 0) {
|
|
8571
|
-
current += char;
|
|
8572
|
-
inTrigger = false;
|
|
8573
|
-
flush();
|
|
8574
|
-
i++;
|
|
8575
|
-
continue;
|
|
8576
|
-
}
|
|
8577
|
-
current += char;
|
|
8578
|
-
i++;
|
|
8579
|
-
}
|
|
8580
|
-
flush();
|
|
8581
|
-
return statements;
|
|
8582
|
-
}
|
|
8583
|
-
function isCommentOnly(fragment) {
|
|
8584
|
-
const withoutBlockComments = fragment.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
8585
|
-
return withoutBlockComments.split("\n").every((line) => {
|
|
8586
|
-
const trimmed = line.trim();
|
|
8587
|
-
return trimmed.length === 0 || trimmed.startsWith("--");
|
|
8588
|
-
});
|
|
8589
|
-
}
|
|
8590
|
-
|
|
8591
|
-
// src/core/runner/mssql-batches.ts
|
|
8592
|
-
function splitMssqlBatches(content) {
|
|
8593
|
-
const batches = content.split(/^\s*GO\s*$/mi);
|
|
8594
|
-
return batches.map((b) => b.trim()).filter((b) => b.length > 0 && !isCommentOnly2(b));
|
|
8595
|
-
}
|
|
8596
|
-
function isCommentOnly2(batch) {
|
|
8597
|
-
for (const line of batch.split("\n")) {
|
|
8598
|
-
const trimmed = line.trim();
|
|
8599
|
-
if (trimmed.length > 0 && !trimmed.startsWith("--")) {
|
|
8600
|
-
return false;
|
|
8601
|
-
}
|
|
8602
|
-
}
|
|
8603
|
-
return true;
|
|
8604
|
-
}
|
|
8605
|
-
async function executeSqlBody(context, sqlContent) {
|
|
8606
|
-
if (context.dialect === "mssql") {
|
|
8607
|
-
return executeFragments(context, splitMssqlBatches(sqlContent), "batch");
|
|
8608
|
-
}
|
|
8609
|
-
if (context.dialect === "sqlite") {
|
|
8610
|
-
return executeFragments(context, splitSqliteStatements(sqlContent), "statement");
|
|
8611
|
-
}
|
|
8612
|
-
const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
|
|
8613
|
-
if (execErr) {
|
|
8614
|
-
return getSqlErrorMessage(execErr);
|
|
8615
|
-
}
|
|
8616
|
-
return null;
|
|
8617
|
-
}
|
|
8618
|
-
async function executeFragments(context, fragments, label) {
|
|
8619
|
-
for (let i = 0; i < fragments.length; i++) {
|
|
8620
|
-
const [, execErr] = await attempt(() => sql.raw(fragments[i]).execute(context.db));
|
|
8621
|
-
if (execErr) {
|
|
8622
|
-
const message = getSqlErrorMessage(execErr);
|
|
8623
|
-
return fragments.length === 1 ? message : `[${label} ${i + 1} of ${fragments.length}] ${message}`;
|
|
8624
|
-
}
|
|
8625
|
-
}
|
|
8626
|
-
return null;
|
|
8627
|
-
}
|
|
8628
|
-
|
|
8629
|
-
// src/core/runner/runner.ts
|
|
8630
8654
|
var SQL_EXTENSIONS2 = [".sql", ".sql.tmpl"];
|
|
8631
8655
|
var FILE_HEADER_TEMPLATE = `-- ============================================================
|
|
8632
8656
|
-- File: %FILE%
|
|
@@ -8675,48 +8699,20 @@ async function runBuild(context, sqlPath, options = {}, preFilteredFiles) {
|
|
|
8675
8699
|
}
|
|
8676
8700
|
async function runFile(context, filepath, options = {}) {
|
|
8677
8701
|
assertRunPolicy(context, "run:file");
|
|
8678
|
-
const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...options };
|
|
8679
8702
|
observer.emit("run:file", {
|
|
8680
8703
|
filepath,
|
|
8681
8704
|
configName: context.configName
|
|
8682
8705
|
});
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
}
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
configName: context.configName,
|
|
8694
|
-
executedBy: formatIdentity(context.identity)
|
|
8695
|
-
})
|
|
8696
|
-
);
|
|
8697
|
-
if (createErr) {
|
|
8698
|
-
observer.emit("error", {
|
|
8699
|
-
source: "runner",
|
|
8700
|
-
error: createErr,
|
|
8701
|
-
context: { filepath, operation: "create-operation" }
|
|
8702
|
-
});
|
|
8703
|
-
return {
|
|
8704
|
-
filepath,
|
|
8705
|
-
checksum: "",
|
|
8706
|
-
status: "failed",
|
|
8707
|
-
error: createErr.message
|
|
8708
|
-
};
|
|
8709
|
-
}
|
|
8710
|
-
const watcher = createWatcher(context);
|
|
8711
|
-
const result = await executeSingleFile(context, filepath, opts, tracker, operationId, watcher).finally(() => watcher.close());
|
|
8712
|
-
await tracker.finalizeOperation(
|
|
8713
|
-
operationId,
|
|
8714
|
-
result.status === "failed" || result.error ? "failed" : "success",
|
|
8715
|
-
Math.round(result.durationMs ?? 0),
|
|
8716
|
-
result.checksum,
|
|
8717
|
-
result.error
|
|
8718
|
-
);
|
|
8719
|
-
return result;
|
|
8706
|
+
const batch = await executeFiles2(context, [{ path: filepath, type: "sql" }], options, {
|
|
8707
|
+
changeType: "run",
|
|
8708
|
+
operationName: `run:${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
8709
|
+
});
|
|
8710
|
+
return batch.files[0] ?? {
|
|
8711
|
+
filepath,
|
|
8712
|
+
checksum: "",
|
|
8713
|
+
status: batch.error === RUN_CANCELLED ? "skipped" : "failed",
|
|
8714
|
+
error: batch.error
|
|
8715
|
+
};
|
|
8720
8716
|
}
|
|
8721
8717
|
async function runDir(context, dirpath, options = {}) {
|
|
8722
8718
|
assertRunPolicy(context, "run:dir");
|
|
@@ -8859,6 +8855,19 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8859
8855
|
};
|
|
8860
8856
|
}
|
|
8861
8857
|
const tracker = execOptions.tracker ?? new Tracker(context.db, context.configName, context.dialect ?? "postgres");
|
|
8858
|
+
const isChange = execOptions.changeType === "change";
|
|
8859
|
+
const prepared = await prepareFiles(context, files);
|
|
8860
|
+
const gated = isChange ? prepared : await applyRunGate(tracker, prepared, opts.force);
|
|
8861
|
+
if (!isChange && gated.every((entry) => entry.skipReason !== void 0)) {
|
|
8862
|
+
return {
|
|
8863
|
+
status: "success",
|
|
8864
|
+
files: gated.map(skipFile),
|
|
8865
|
+
filesRun: 0,
|
|
8866
|
+
filesSkipped: gated.length,
|
|
8867
|
+
filesFailed: 0,
|
|
8868
|
+
durationMs: performance.now() - start
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8862
8871
|
const [operationId, createErr] = await attempt(
|
|
8863
8872
|
() => tracker.createOperation({
|
|
8864
8873
|
name: execOptions.operationName,
|
|
@@ -8879,20 +8888,11 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8879
8888
|
performance.now() - start
|
|
8880
8889
|
);
|
|
8881
8890
|
}
|
|
8882
|
-
const
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
checksum = err ? "" : computed;
|
|
8888
|
-
}
|
|
8889
|
-
fileRecords.push({
|
|
8890
|
-
filepath: path.relative(context.projectRoot, file.path),
|
|
8891
|
-
fileType: file.type,
|
|
8892
|
-
checksum
|
|
8893
|
-
});
|
|
8894
|
-
}
|
|
8895
|
-
const createRecordsErr = await tracker.createFileRecords(operationId, fileRecords);
|
|
8891
|
+
const runSet = gated.filter((entry) => !entry.skipReason);
|
|
8892
|
+
const createRecordsErr = await tracker.createFileRecords(
|
|
8893
|
+
operationId,
|
|
8894
|
+
runSet.map((entry) => ({ filepath: entry.relFilepath, fileType: entry.file.type, checksum: entry.checksum }))
|
|
8895
|
+
);
|
|
8896
8896
|
if (createRecordsErr) {
|
|
8897
8897
|
observer.emit("error", {
|
|
8898
8898
|
source: "runner:create-file-records",
|
|
@@ -8909,26 +8909,21 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8909
8909
|
let failed = false;
|
|
8910
8910
|
let cancelled = false;
|
|
8911
8911
|
const watcher = createWatcher(context);
|
|
8912
|
+
const lastRunIndex = gated.map((entry) => !entry.skipReason).lastIndexOf(true);
|
|
8912
8913
|
try {
|
|
8913
|
-
for (let i = 0; i <
|
|
8914
|
-
const
|
|
8915
|
-
|
|
8914
|
+
for (let i = 0; i < gated.length; i++) {
|
|
8915
|
+
const entry = gated[i];
|
|
8916
|
+
if (entry.skipReason) {
|
|
8917
|
+
results.push(skipFile(entry));
|
|
8918
|
+
continue;
|
|
8919
|
+
}
|
|
8916
8920
|
if (context.signal?.aborted) {
|
|
8917
8921
|
failed = true;
|
|
8918
8922
|
cancelled = true;
|
|
8919
8923
|
await skipRemaining(tracker, operationId, RUN_CANCELLED);
|
|
8920
8924
|
break;
|
|
8921
8925
|
}
|
|
8922
|
-
const result = await executeSingleFileWithUpdate(
|
|
8923
|
-
context,
|
|
8924
|
-
file.path,
|
|
8925
|
-
fileRecord.checksum,
|
|
8926
|
-
opts,
|
|
8927
|
-
tracker,
|
|
8928
|
-
operationId,
|
|
8929
|
-
execOptions.changeType,
|
|
8930
|
-
watcher
|
|
8931
|
-
);
|
|
8926
|
+
const result = await executeSingleFileWithUpdate(context, entry, tracker, operationId, watcher);
|
|
8932
8927
|
if (!result) {
|
|
8933
8928
|
failed = true;
|
|
8934
8929
|
cancelled = true;
|
|
@@ -8936,13 +8931,13 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8936
8931
|
break;
|
|
8937
8932
|
}
|
|
8938
8933
|
results.push(result);
|
|
8939
|
-
cancelled = (context.signal?.aborted ?? false) && (i <
|
|
8934
|
+
cancelled = (context.signal?.aborted ?? false) && (i < lastRunIndex || result.status === "failed");
|
|
8940
8935
|
if (cancelled || result.status === "failed" && opts.abortOnError) {
|
|
8941
8936
|
failed = true;
|
|
8942
8937
|
await skipRemaining(
|
|
8943
8938
|
tracker,
|
|
8944
8939
|
operationId,
|
|
8945
|
-
cancelled ? RUN_CANCELLED : `Skipped: failure in ${path.basename(file.path)}`
|
|
8940
|
+
cancelled ? RUN_CANCELLED : `Skipped: failure in ${path.basename(entry.file.path)}`
|
|
8946
8941
|
);
|
|
8947
8942
|
break;
|
|
8948
8943
|
}
|
|
@@ -8963,7 +8958,7 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8963
8958
|
if (filesFailed > 0 || failed) {
|
|
8964
8959
|
status = filesRun > 0 ? "partial" : "failed";
|
|
8965
8960
|
}
|
|
8966
|
-
const combinedChecksum = execOptions.checksum ?? computeCombinedChecksum(
|
|
8961
|
+
const combinedChecksum = execOptions.checksum ?? computeCombinedChecksum(gated.map((entry) => entry.checksum));
|
|
8967
8962
|
const finalStatus = status === "success" ? "success" : "failed";
|
|
8968
8963
|
const finalizeErr = await tracker.finalizeOperation(
|
|
8969
8964
|
operationId,
|
|
@@ -8990,6 +8985,43 @@ async function executeFiles2(context, files, runOptions, execOptions) {
|
|
|
8990
8985
|
error: cancelled ? RUN_CANCELLED : void 0
|
|
8991
8986
|
};
|
|
8992
8987
|
}
|
|
8988
|
+
async function prepareFiles(context, files) {
|
|
8989
|
+
const prepared = [];
|
|
8990
|
+
for (const file of files) {
|
|
8991
|
+
const relFilepath = path.relative(context.projectRoot, file.path);
|
|
8992
|
+
const [loaded, loadError] = await attempt(async () => {
|
|
8993
|
+
const sql27 = await loadAndRenderFile2(context, file.path);
|
|
8994
|
+
return { sql: sql27, checksum: computeChecksumFromContent(sql27) };
|
|
8995
|
+
});
|
|
8996
|
+
if (loadError) {
|
|
8997
|
+
prepared.push({ file, relFilepath, checksum: "", sql: "", loadError });
|
|
8998
|
+
continue;
|
|
8999
|
+
}
|
|
9000
|
+
prepared.push({ file, relFilepath, ...loaded });
|
|
9001
|
+
}
|
|
9002
|
+
return prepared;
|
|
9003
|
+
}
|
|
9004
|
+
async function applyRunGate(tracker, prepared, force) {
|
|
9005
|
+
if (force) return prepared;
|
|
9006
|
+
const [latest, latestErr] = await tracker.latestExecutions();
|
|
9007
|
+
return prepared.map((entry) => {
|
|
9008
|
+
if (entry.loadError) return entry;
|
|
9009
|
+
const decision = decideNeedsRun(latestErr ?? latest.get(entry.relFilepath), entry.checksum);
|
|
9010
|
+
return decision.skipReason ? { ...entry, skipReason: decision.skipReason } : entry;
|
|
9011
|
+
});
|
|
9012
|
+
}
|
|
9013
|
+
function skipFile(entry) {
|
|
9014
|
+
observer.emit("file:skip", {
|
|
9015
|
+
filepath: entry.file.path,
|
|
9016
|
+
reason: entry.skipReason
|
|
9017
|
+
});
|
|
9018
|
+
return {
|
|
9019
|
+
filepath: entry.file.path,
|
|
9020
|
+
checksum: entry.checksum,
|
|
9021
|
+
status: "skipped",
|
|
9022
|
+
skipReason: entry.skipReason
|
|
9023
|
+
};
|
|
9024
|
+
}
|
|
8993
9025
|
async function executeFilesInternal(context, files, options, changeType, sqlPath) {
|
|
8994
9026
|
const fileInputs = files.map((f) => ({
|
|
8995
9027
|
path: f,
|
|
@@ -9003,17 +9035,16 @@ async function executeFilesInternal(context, files, options, changeType, sqlPath
|
|
|
9003
9035
|
};
|
|
9004
9036
|
return executeFiles2(context, fileInputs, options, execOptions);
|
|
9005
9037
|
}
|
|
9006
|
-
async function executeSingleFileWithUpdate(context,
|
|
9038
|
+
async function executeSingleFileWithUpdate(context, entry, tracker, operationId, watcher) {
|
|
9007
9039
|
const start = performance.now();
|
|
9008
|
-
const
|
|
9009
|
-
|
|
9010
|
-
if (loadErr) {
|
|
9040
|
+
const { file: { path: filepath }, relFilepath, checksum } = entry;
|
|
9041
|
+
if (entry.loadError) {
|
|
9011
9042
|
const durationMs2 = performance.now() - start;
|
|
9012
9043
|
const result2 = {
|
|
9013
9044
|
filepath,
|
|
9014
|
-
checksum
|
|
9045
|
+
checksum,
|
|
9015
9046
|
status: "failed",
|
|
9016
|
-
error:
|
|
9047
|
+
error: entry.loadError.message,
|
|
9017
9048
|
durationMs: durationMs2
|
|
9018
9049
|
};
|
|
9019
9050
|
await tracker.updateFileExecution(
|
|
@@ -9021,56 +9052,29 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
9021
9052
|
relFilepath,
|
|
9022
9053
|
"failed",
|
|
9023
9054
|
Math.round(durationMs2),
|
|
9024
|
-
|
|
9055
|
+
entry.loadError.message
|
|
9025
9056
|
);
|
|
9026
9057
|
observer.emit("file:after", {
|
|
9027
9058
|
filepath,
|
|
9028
9059
|
status: "failed",
|
|
9029
9060
|
durationMs: durationMs2,
|
|
9030
|
-
error:
|
|
9061
|
+
error: entry.loadError.message
|
|
9031
9062
|
});
|
|
9032
9063
|
return result2;
|
|
9033
9064
|
}
|
|
9034
|
-
const [renderedChecksum, checksumErr] = attemptSync(() => computeChecksumFromContent(sqlContent));
|
|
9035
|
-
const finalChecksum = checksumErr ? checksum : renderedChecksum;
|
|
9036
9065
|
observer.emit("file:before", {
|
|
9037
9066
|
filepath,
|
|
9038
|
-
checksum
|
|
9067
|
+
checksum,
|
|
9039
9068
|
configName: context.configName
|
|
9040
9069
|
});
|
|
9041
|
-
|
|
9042
|
-
const needsRunResult = await tracker.needsRun(relFilepath, finalChecksum, options.force, operationId);
|
|
9043
|
-
if (!needsRunResult.needsRun) {
|
|
9044
|
-
const result2 = {
|
|
9045
|
-
filepath,
|
|
9046
|
-
checksum: finalChecksum,
|
|
9047
|
-
status: "skipped",
|
|
9048
|
-
skipReason: needsRunResult.skipReason
|
|
9049
|
-
};
|
|
9050
|
-
await tracker.updateFileExecution(
|
|
9051
|
-
operationId,
|
|
9052
|
-
relFilepath,
|
|
9053
|
-
"skipped",
|
|
9054
|
-
0,
|
|
9055
|
-
void 0,
|
|
9056
|
-
needsRunResult.skipReason,
|
|
9057
|
-
finalChecksum
|
|
9058
|
-
);
|
|
9059
|
-
observer.emit("file:skip", {
|
|
9060
|
-
filepath,
|
|
9061
|
-
reason: needsRunResult.skipReason
|
|
9062
|
-
});
|
|
9063
|
-
return result2;
|
|
9064
|
-
}
|
|
9065
|
-
}
|
|
9066
|
-
const execErrMsg = await runWatched(context, watcher, filepath, sqlContent);
|
|
9070
|
+
const execErrMsg = await runWatched(context, watcher, filepath, entry.sql);
|
|
9067
9071
|
if (execErrMsg === NOT_STARTED) return null;
|
|
9068
9072
|
const durationMs = performance.now() - start;
|
|
9069
9073
|
if (execErrMsg) {
|
|
9070
9074
|
const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
|
|
9071
9075
|
const result2 = {
|
|
9072
9076
|
filepath,
|
|
9073
|
-
checksum
|
|
9077
|
+
checksum,
|
|
9074
9078
|
status: "failed",
|
|
9075
9079
|
error,
|
|
9076
9080
|
durationMs
|
|
@@ -9080,9 +9084,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
9080
9084
|
relFilepath,
|
|
9081
9085
|
"failed",
|
|
9082
9086
|
Math.round(durationMs),
|
|
9083
|
-
error
|
|
9084
|
-
void 0,
|
|
9085
|
-
finalChecksum
|
|
9087
|
+
error
|
|
9086
9088
|
);
|
|
9087
9089
|
observer.emit("file:after", {
|
|
9088
9090
|
filepath,
|
|
@@ -9094,7 +9096,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
9094
9096
|
}
|
|
9095
9097
|
const result = {
|
|
9096
9098
|
filepath,
|
|
9097
|
-
checksum
|
|
9099
|
+
checksum,
|
|
9098
9100
|
status: "success",
|
|
9099
9101
|
durationMs
|
|
9100
9102
|
};
|
|
@@ -9102,10 +9104,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
9102
9104
|
operationId,
|
|
9103
9105
|
relFilepath,
|
|
9104
9106
|
"success",
|
|
9105
|
-
Math.round(durationMs)
|
|
9106
|
-
void 0,
|
|
9107
|
-
void 0,
|
|
9108
|
-
finalChecksum
|
|
9107
|
+
Math.round(durationMs)
|
|
9109
9108
|
);
|
|
9110
9109
|
observer.emit("file:after", {
|
|
9111
9110
|
filepath,
|
|
@@ -9114,143 +9113,6 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
|
|
|
9114
9113
|
});
|
|
9115
9114
|
return result;
|
|
9116
9115
|
}
|
|
9117
|
-
async function executeSingleFile(context, filepath, options, tracker, operationId, watcher) {
|
|
9118
|
-
const start = performance.now();
|
|
9119
|
-
const relFilepath = path.relative(context.projectRoot, filepath);
|
|
9120
|
-
const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
|
|
9121
|
-
if (loadErr) {
|
|
9122
|
-
const durationMs2 = performance.now() - start;
|
|
9123
|
-
const result2 = {
|
|
9124
|
-
filepath,
|
|
9125
|
-
checksum: "",
|
|
9126
|
-
status: "failed",
|
|
9127
|
-
error: loadErr.message,
|
|
9128
|
-
durationMs: durationMs2
|
|
9129
|
-
};
|
|
9130
|
-
await tracker.recordExecution({
|
|
9131
|
-
changeId: operationId,
|
|
9132
|
-
filepath: relFilepath,
|
|
9133
|
-
checksum: "",
|
|
9134
|
-
status: "failed",
|
|
9135
|
-
errorMessage: loadErr.message,
|
|
9136
|
-
durationMs: Math.round(durationMs2)
|
|
9137
|
-
});
|
|
9138
|
-
observer.emit("file:after", {
|
|
9139
|
-
filepath,
|
|
9140
|
-
status: "failed",
|
|
9141
|
-
durationMs: durationMs2,
|
|
9142
|
-
error: loadErr.message
|
|
9143
|
-
});
|
|
9144
|
-
return result2;
|
|
9145
|
-
}
|
|
9146
|
-
const [checksum, checksumErr] = attemptSync(() => computeChecksumFromContent(sqlContent));
|
|
9147
|
-
if (checksumErr) {
|
|
9148
|
-
const result2 = {
|
|
9149
|
-
filepath,
|
|
9150
|
-
checksum: "",
|
|
9151
|
-
status: "failed",
|
|
9152
|
-
error: checksumErr.message,
|
|
9153
|
-
durationMs: performance.now() - start
|
|
9154
|
-
};
|
|
9155
|
-
await tracker.recordExecution({
|
|
9156
|
-
changeId: operationId,
|
|
9157
|
-
filepath: relFilepath,
|
|
9158
|
-
checksum: "",
|
|
9159
|
-
status: "failed",
|
|
9160
|
-
errorMessage: checksumErr.message,
|
|
9161
|
-
durationMs: Math.round(result2.durationMs ?? 0)
|
|
9162
|
-
});
|
|
9163
|
-
observer.emit("file:after", {
|
|
9164
|
-
filepath,
|
|
9165
|
-
status: "failed",
|
|
9166
|
-
durationMs: result2.durationMs ?? 0,
|
|
9167
|
-
error: checksumErr.message
|
|
9168
|
-
});
|
|
9169
|
-
return result2;
|
|
9170
|
-
}
|
|
9171
|
-
observer.emit("file:before", {
|
|
9172
|
-
filepath,
|
|
9173
|
-
checksum,
|
|
9174
|
-
configName: context.configName
|
|
9175
|
-
});
|
|
9176
|
-
const needsRunResult = await tracker.needsRun(relFilepath, checksum, options.force);
|
|
9177
|
-
if (!needsRunResult.needsRun) {
|
|
9178
|
-
const result2 = {
|
|
9179
|
-
filepath,
|
|
9180
|
-
checksum,
|
|
9181
|
-
status: "skipped",
|
|
9182
|
-
skipReason: needsRunResult.skipReason
|
|
9183
|
-
};
|
|
9184
|
-
await tracker.recordExecution({
|
|
9185
|
-
changeId: operationId,
|
|
9186
|
-
filepath: relFilepath,
|
|
9187
|
-
checksum,
|
|
9188
|
-
status: "skipped",
|
|
9189
|
-
skipReason: needsRunResult.skipReason
|
|
9190
|
-
});
|
|
9191
|
-
observer.emit("file:skip", {
|
|
9192
|
-
filepath,
|
|
9193
|
-
reason: needsRunResult.skipReason
|
|
9194
|
-
});
|
|
9195
|
-
return result2;
|
|
9196
|
-
}
|
|
9197
|
-
const execErrMsg = await runWatched(context, watcher, filepath, sqlContent);
|
|
9198
|
-
const durationMs = performance.now() - start;
|
|
9199
|
-
if (execErrMsg === NOT_STARTED) {
|
|
9200
|
-
await tracker.recordExecution({
|
|
9201
|
-
changeId: operationId,
|
|
9202
|
-
filepath: relFilepath,
|
|
9203
|
-
checksum,
|
|
9204
|
-
status: "skipped",
|
|
9205
|
-
skipReason: RUN_CANCELLED
|
|
9206
|
-
});
|
|
9207
|
-
return { filepath, checksum, status: "skipped", error: RUN_CANCELLED };
|
|
9208
|
-
}
|
|
9209
|
-
if (execErrMsg) {
|
|
9210
|
-
const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
|
|
9211
|
-
const result2 = {
|
|
9212
|
-
filepath,
|
|
9213
|
-
checksum,
|
|
9214
|
-
status: "failed",
|
|
9215
|
-
error,
|
|
9216
|
-
durationMs
|
|
9217
|
-
};
|
|
9218
|
-
await tracker.recordExecution({
|
|
9219
|
-
changeId: operationId,
|
|
9220
|
-
filepath: relFilepath,
|
|
9221
|
-
checksum,
|
|
9222
|
-
status: "failed",
|
|
9223
|
-
errorMessage: error,
|
|
9224
|
-
durationMs: Math.round(durationMs)
|
|
9225
|
-
});
|
|
9226
|
-
observer.emit("file:after", {
|
|
9227
|
-
filepath,
|
|
9228
|
-
status: "failed",
|
|
9229
|
-
durationMs,
|
|
9230
|
-
error
|
|
9231
|
-
});
|
|
9232
|
-
return result2;
|
|
9233
|
-
}
|
|
9234
|
-
const result = {
|
|
9235
|
-
filepath,
|
|
9236
|
-
checksum,
|
|
9237
|
-
status: "success",
|
|
9238
|
-
durationMs
|
|
9239
|
-
};
|
|
9240
|
-
await tracker.recordExecution({
|
|
9241
|
-
changeId: operationId,
|
|
9242
|
-
filepath: relFilepath,
|
|
9243
|
-
checksum,
|
|
9244
|
-
status: "success",
|
|
9245
|
-
durationMs: Math.round(durationMs)
|
|
9246
|
-
});
|
|
9247
|
-
observer.emit("file:after", {
|
|
9248
|
-
filepath,
|
|
9249
|
-
status: "success",
|
|
9250
|
-
durationMs
|
|
9251
|
-
});
|
|
9252
|
-
return result;
|
|
9253
|
-
}
|
|
9254
9116
|
async function loadAndRenderFile2(context, filepath) {
|
|
9255
9117
|
if (isTemplate(filepath)) {
|
|
9256
9118
|
const result = await processFile(filepath, {
|
|
@@ -9415,12 +9277,12 @@ var RunNamespace = class {
|
|
|
9415
9277
|
*
|
|
9416
9278
|
* @example
|
|
9417
9279
|
* ```typescript
|
|
9418
|
-
* const files = await ctx.noorm.run.discover('sql/')
|
|
9280
|
+
* const files = await ctx.noorm.run.discover('db/sql/')
|
|
9419
9281
|
* ```
|
|
9420
9282
|
*/
|
|
9421
9283
|
async discover(dirpath) {
|
|
9422
9284
|
const absolutePath = this.#resolvePath(
|
|
9423
|
-
dirpath ?? this.#state.settings.paths?.sql ??
|
|
9285
|
+
dirpath ?? this.#state.settings.paths?.sql ?? DEFAULT_SQL_PATH
|
|
9424
9286
|
);
|
|
9425
9287
|
return discoverFiles(absolutePath);
|
|
9426
9288
|
}
|
|
@@ -9432,7 +9294,7 @@ var RunNamespace = class {
|
|
|
9432
9294
|
*
|
|
9433
9295
|
* @example
|
|
9434
9296
|
* ```typescript
|
|
9435
|
-
* const results = await ctx.noorm.run.preview(['sql/001.sql', 'sql/002.sql'])
|
|
9297
|
+
* const results = await ctx.noorm.run.preview(['db/sql/001.sql', 'db/sql/002.sql'])
|
|
9436
9298
|
* ```
|
|
9437
9299
|
*/
|
|
9438
9300
|
async preview(filepaths, output) {
|
|
@@ -9509,7 +9371,7 @@ var RunNamespace = class {
|
|
|
9509
9371
|
const context = await this.#createRunContext();
|
|
9510
9372
|
const sqlPath = path.join(
|
|
9511
9373
|
this.#state.projectRoot,
|
|
9512
|
-
this.#state.settings.paths?.sql ??
|
|
9374
|
+
this.#state.settings.paths?.sql ?? DEFAULT_SQL_PATH
|
|
9513
9375
|
);
|
|
9514
9376
|
const configForMatch = {
|
|
9515
9377
|
name: this.#state.config.name,
|