@noormdev/sdk 1.4.2 → 1.5.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.
@@ -1519,5 +1519,5 @@ var observer = new ObserverEngine({
1519
1519
  });
1520
1520
 
1521
1521
  export { ObserverRelay, getEnvConfigName, isDebug, observer, shouldSkipConfirmations };
1522
- //# sourceMappingURL=chunk-A2OYZAVT.js.map
1523
- //# sourceMappingURL=chunk-A2OYZAVT.js.map
1522
+ //# sourceMappingURL=chunk-6LG4ENOJ.js.map
1523
+ //# sourceMappingURL=chunk-6LG4ENOJ.js.map
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'module';
2
- import { observer } from './chunk-A2OYZAVT.js';
2
+ import { observer } from './chunk-6LG4ENOJ.js';
3
3
  import { attemptSync, attempt } from './chunk-TXDGFA44.js';
4
4
  import { dist_default } from './chunk-TBRUDUBQ.js';
5
5
  import { readFile as readFile$1, readdir, stat } from 'fs/promises';
@@ -2506,7 +2506,7 @@ function createIncludeHelper(templateDir, projectRoot, options) {
2506
2506
  throw new Error(`Include path escapes project root: ${includePath}`);
2507
2507
  }
2508
2508
  if (resolved.endsWith(".tmpl")) {
2509
- const { processFile: processFile2 } = await import('./engine-6ALQ5YTN.js');
2509
+ const { processFile: processFile2 } = await import('./engine-P74UTJQO.js');
2510
2510
  const result = await processFile2(resolved, options);
2511
2511
  return result.sql;
2512
2512
  }
@@ -2588,5 +2588,5 @@ voca/es/index.js:
2588
2588
  */
2589
2589
 
2590
2590
  export { DT_EXTENSIONS, DtReader, ENCODED_TYPES, FORMAT_VERSION, encryptWithPassphrase, es_default, eta, isTemplate, processFile, processFiles, renderTemplate };
2591
- //# sourceMappingURL=chunk-CDTFXLXU.js.map
2592
- //# sourceMappingURL=chunk-CDTFXLXU.js.map
2591
+ //# sourceMappingURL=chunk-M7Q44VFJ.js.map
2592
+ //# sourceMappingURL=chunk-M7Q44VFJ.js.map
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from 'module';
2
- import { observer, shouldSkipConfirmations } from './chunk-A2OYZAVT.js';
2
+ import { observer, shouldSkipConfirmations } from './chunk-6LG4ENOJ.js';
3
3
  import { attempt, attemptSync } from './chunk-TXDGFA44.js';
4
4
  import { __commonJS, __toESM } from './chunk-D2XW2UOP.js';
5
5
  import { generateKeyPairSync, randomBytes, createCipheriv, createPrivateKey, createPublicKey, diffieHellman, hkdfSync, createDecipheriv } from 'crypto';
@@ -97661,5 +97661,5 @@ async function listVaultSecretKeysChecked(gate, db, dialect) {
97661
97661
  }
97662
97662
 
97663
97663
  export { assertPolicy, assertVaultPolicy, checkConfigPolicy, deleteVaultSecret, deleteVaultSecretChecked, detectAgentHarness, encryptVaultKey, getAllVaultSecrets, getNoormTables, getVaultKey, getVaultKeyChecked, getVaultSecret, getVaultStatus, guarded, initializeVault, initializeVaultChecked, listVaultSecretKeys, listVaultSecretKeysChecked, noormDb, resolveLegacyAccess, setVaultSecret, setVaultSecretChecked, vaultSecretExists };
97664
- //# sourceMappingURL=chunk-OMY6CKR4.js.map
97665
- //# sourceMappingURL=chunk-OMY6CKR4.js.map
97664
+ //# sourceMappingURL=chunk-WUMCIKEQ.js.map
97665
+ //# sourceMappingURL=chunk-WUMCIKEQ.js.map
@@ -1,12 +1,12 @@
1
1
  import { createRequire } from 'module';
2
- export { eta, isTemplate, processFile, processFiles, renderTemplate } from './chunk-CDTFXLXU.js';
2
+ export { eta, isTemplate, processFile, processFiles, renderTemplate } from './chunk-M7Q44VFJ.js';
3
3
  import './chunk-UOFJT5PY.js';
4
- import './chunk-A2OYZAVT.js';
4
+ import './chunk-6LG4ENOJ.js';
5
5
  import './chunk-TXDGFA44.js';
6
6
  import './chunk-TBRUDUBQ.js';
7
7
  import './chunk-YJBKNUTP.js';
8
8
  import './chunk-D2XW2UOP.js';
9
9
 
10
10
  createRequire(import.meta.url);
11
- //# sourceMappingURL=engine-6ALQ5YTN.js.map
12
- //# sourceMappingURL=engine-6ALQ5YTN.js.map
11
+ //# sourceMappingURL=engine-P74UTJQO.js.map
12
+ //# sourceMappingURL=engine-P74UTJQO.js.map
package/dist/index.d.ts CHANGED
@@ -2541,6 +2541,42 @@ export declare class ChangesNamespace {
2541
2541
  */
2542
2542
  getFileHistory(operationId: number): Promise<FileHistoryRecord[]>;
2543
2543
  }
2544
+ /**
2545
+ * A session holding a lock the watched statement is waiting on.
2546
+ */
2547
+ export interface BlockingSession {
2548
+ pid: number;
2549
+ /** Start of the blocker's statement text, or its command/status when no text is visible. */
2550
+ query: string | null;
2551
+ /** How long the blocker has been in its current transaction or request. */
2552
+ ageMs: number | null;
2553
+ }
2554
+ /**
2555
+ * Progress an operation reports about itself, from the dialect's progress views.
2556
+ */
2557
+ export interface OperationProgress {
2558
+ /** What is running, e.g. `CREATE INDEX`, `VACUUM`, `COPY FROM`. */
2559
+ operation: string;
2560
+ /** Phase within the operation, e.g. `building index: loading tuples`. */
2561
+ phase: string | null;
2562
+ relation: string | null;
2563
+ done: number | null;
2564
+ total: number | null;
2565
+ /** 0-100, when the dialect reports enough to compute it. */
2566
+ percent: number | null;
2567
+ }
2568
+ /**
2569
+ * What the server says about a running statement at one point in time.
2570
+ */
2571
+ export interface StatementStatus {
2572
+ /** Session state, e.g. `active`, `suspended`, a mysql thread state. */
2573
+ state: string | null;
2574
+ /** What the session is waiting on, e.g. `Lock: transactionid`. Null when it is working. */
2575
+ waitEvent: string | null;
2576
+ blockedBy: BlockingSession[];
2577
+ workers: number;
2578
+ progress: OperationProgress | null;
2579
+ }
2544
2580
  export declare class RunNamespace {
2545
2581
  #private;
2546
2582
  constructor(state: ContextState);
@@ -4484,6 +4520,17 @@ export interface NoormEvents extends SettingsEvents, UpdateEvents, VaultEvents,
4484
4520
  filepath: string;
4485
4521
  reason: "unchanged" | "already-run";
4486
4522
  };
4523
+ /**
4524
+ * A file has run past the watch delay. Repeats every watch interval until
4525
+ * it finishes. `status` is null when the dialect has no probe (sqlite) or
4526
+ * the server could not be asked.
4527
+ */
4528
+ "file:progress": {
4529
+ filepath: string;
4530
+ elapsedMs: number;
4531
+ sessionId: number | null;
4532
+ status: StatementStatus | null;
4533
+ };
4487
4534
  "file:dry-run": {
4488
4535
  filepath: string;
4489
4536
  status: "success" | "failed";
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-CDTFXLXU.js';
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-OMY6CKR4.js';
5
- import { observer, getEnvConfigName, ObserverRelay, isDebug } from './chunk-A2OYZAVT.js';
6
- export { observer as noormObserver } from './chunk-A2OYZAVT.js';
4
+ import { resolveLegacyAccess, checkConfigPolicy, initializeVault, getVaultStatus, setVaultSecret, getVaultSecret, getAllVaultSecrets, listVaultSecretKeys, deleteVaultSecret, vaultSecretExists, getVaultKey, noormDb, getNoormTables, encryptVaultKey, assertVaultPolicy, assertPolicy, detectAgentHarness, guarded } from './chunk-WUMCIKEQ.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';
@@ -6019,6 +6019,349 @@ function computeCombinedChecksum(checksums) {
6019
6019
  const combined = sorted.join("");
6020
6020
  return createHash("sha256").update(combined, "utf-8").digest("hex");
6021
6021
  }
6022
+ var SESSION_ID_SQL = {
6023
+ postgres: "select pg_backend_pid() as id",
6024
+ mysql: "select connection_id() as id",
6025
+ mssql: "select @@SPID as id"
6026
+ };
6027
+ var SERVER_CANCEL = {
6028
+ postgres: (db, sessionId) => sql`select pg_cancel_backend(${sessionId})`.execute(db),
6029
+ // KILL cannot be prepared, so the id is interpolated. It comes from
6030
+ // connection_id() and is checked by readSessionId before it gets here,
6031
+ // never from user input.
6032
+ mysql: (db, sessionId) => sql.raw(`kill query ${sessionId}`).execute(db)
6033
+ };
6034
+ function readSessionId(rows) {
6035
+ const raw = rows[0]?.id;
6036
+ const id = Number(raw);
6037
+ if (!Number.isInteger(id) || id <= 0) return void 0;
6038
+ return id;
6039
+ }
6040
+ function percentOf(done, total) {
6041
+ if (done === null || total === null || total <= 0) return null;
6042
+ return Math.min(100, done / total * 100);
6043
+ }
6044
+ function toNumber(value) {
6045
+ if (value === null || value === void 0) return null;
6046
+ const n = Number(value);
6047
+ return Number.isFinite(n) ? n : null;
6048
+ }
6049
+ function toText(value) {
6050
+ if (value === null || value === void 0 || value === "") return null;
6051
+ return String(value);
6052
+ }
6053
+ async function rowsOrEmpty(query) {
6054
+ const [result, err] = await attempt(query);
6055
+ return err ? [] : result.rows;
6056
+ }
6057
+ function toBlockers(rows) {
6058
+ return rows.map((row) => ({
6059
+ pid: Number(row.pid),
6060
+ query: toText(row.query),
6061
+ ageMs: toNumber(row.age_ms)
6062
+ }));
6063
+ }
6064
+ var postgresProbe = async (side, pid) => {
6065
+ const [activity] = await rowsOrEmpty(() => sql`
6066
+ select a.state, a.wait_event_type, a.wait_event,
6067
+ pg_blocking_pids(a.pid) as blockers,
6068
+ (select count(*)::int from pg_stat_activity w
6069
+ where w.leader_pid = a.pid and w.pid <> a.pid) as workers
6070
+ from pg_stat_activity a
6071
+ where a.pid = ${pid}
6072
+ `.execute(side));
6073
+ const blockerPids = (activity?.blockers ?? []).map(Number);
6074
+ const blockers = blockerPids.length === 0 ? [] : await rowsOrEmpty(() => sql`
6075
+ select pid, left(query, 120) as query,
6076
+ (extract(epoch from now() - coalesce(xact_start, query_start)) * 1000)::float8 as age_ms
6077
+ from pg_stat_activity
6078
+ where pid = any(${blockerPids}::int[])
6079
+ `.execute(side));
6080
+ const [progress] = await rowsOrEmpty(() => sql`
6081
+ select command as operation, phase,
6082
+ nullif(relid, 0)::regclass::text as relation,
6083
+ (case when tuples_total > 0 then tuples_done else blocks_done end)::float8 as done,
6084
+ (case when tuples_total > 0 then tuples_total else blocks_total end)::float8 as total
6085
+ from pg_stat_progress_create_index where pid = ${pid}
6086
+ union all
6087
+ select 'VACUUM', phase, nullif(relid, 0)::regclass::text,
6088
+ heap_blks_scanned::float8, heap_blks_total::float8
6089
+ from pg_stat_progress_vacuum where pid = ${pid}
6090
+ union all
6091
+ select command, phase, nullif(relid, 0)::regclass::text,
6092
+ heap_blks_scanned::float8, heap_blks_total::float8
6093
+ from pg_stat_progress_cluster where pid = ${pid}
6094
+ union all
6095
+ select command, null, nullif(relid, 0)::regclass::text,
6096
+ bytes_processed::float8, nullif(bytes_total, 0)::float8
6097
+ from pg_stat_progress_copy where pid = ${pid}
6098
+ union all
6099
+ select 'ANALYZE', phase, nullif(relid, 0)::regclass::text,
6100
+ sample_blks_scanned::float8, sample_blks_total::float8
6101
+ from pg_stat_progress_analyze where pid = ${pid}
6102
+ limit 1
6103
+ `.execute(side));
6104
+ const waitEvent = activity?.wait_event ? `${activity.wait_event_type ?? "Wait"}: ${activity.wait_event}` : null;
6105
+ return {
6106
+ state: toText(activity?.state),
6107
+ waitEvent,
6108
+ blockedBy: toBlockers(blockers),
6109
+ workers: toNumber(activity?.workers) ?? 0,
6110
+ progress: progress ? {
6111
+ operation: progress.operation,
6112
+ phase: toText(progress.phase),
6113
+ relation: toText(progress.relation),
6114
+ done: toNumber(progress.done),
6115
+ total: toNumber(progress.total),
6116
+ percent: percentOf(toNumber(progress.done), toNumber(progress.total))
6117
+ } : null
6118
+ };
6119
+ };
6120
+ var mssqlProbe = async (side, spid) => {
6121
+ const [request] = await rowsOrEmpty(() => sql`
6122
+ select status, wait_type, blocking_session_id, percent_complete, command
6123
+ from sys.dm_exec_requests
6124
+ where session_id = ${spid}
6125
+ `.execute(side));
6126
+ const blockerId = toNumber(request?.blocking_session_id);
6127
+ const blockers = !blockerId ? [] : await rowsOrEmpty(() => sql`
6128
+ select s.session_id as pid,
6129
+ coalesce(left(t.text, 120), r.command, s.status) as query,
6130
+ datediff_big(millisecond, coalesce(r.start_time, s.last_request_start_time), sysdatetime()) as age_ms
6131
+ from sys.dm_exec_sessions s
6132
+ left join sys.dm_exec_requests r on r.session_id = s.session_id
6133
+ outer apply sys.dm_exec_sql_text(r.sql_handle) t
6134
+ where s.session_id = ${blockerId}
6135
+ `.execute(side));
6136
+ const percent = toNumber(request?.percent_complete);
6137
+ return {
6138
+ state: toText(request?.status),
6139
+ waitEvent: toText(request?.wait_type),
6140
+ blockedBy: toBlockers(blockers),
6141
+ workers: 0,
6142
+ progress: request?.command && percent ? { operation: request.command, phase: null, relation: null, done: null, total: null, percent } : null
6143
+ };
6144
+ };
6145
+ var mysqlProbe = async (side, id) => {
6146
+ const [process2] = await rowsOrEmpty(() => sql`
6147
+ select state from information_schema.processlist where id = ${id}
6148
+ `.execute(side));
6149
+ const blockerIds = await rowsOrEmpty(() => sql`
6150
+ select blocking_pid as pid from sys.innodb_lock_waits where waiting_pid = ${id}
6151
+ union
6152
+ select blocking_pid from sys.schema_table_lock_waits where waiting_pid = ${id}
6153
+ `.execute(side));
6154
+ const ids = blockerIds.map((row) => Number(row.pid));
6155
+ const blockers = ids.length === 0 ? [] : await rowsOrEmpty(() => sql`
6156
+ select id as pid, left(coalesce(info, command), 120) as query, time * 1000 as age_ms
6157
+ from information_schema.processlist
6158
+ where id in (${sql.join(ids)})
6159
+ `.execute(side));
6160
+ const [stage] = await rowsOrEmpty(() => sql`
6161
+ select s.event_name, s.work_completed, s.work_estimated
6162
+ from performance_schema.events_stages_current s
6163
+ join performance_schema.threads t on t.thread_id = s.thread_id
6164
+ where t.processlist_id = ${id}
6165
+ `.execute(side));
6166
+ const state = toText(process2?.state);
6167
+ const done = toNumber(stage?.work_completed);
6168
+ const total = toNumber(stage?.work_estimated);
6169
+ return {
6170
+ state,
6171
+ waitEvent: state?.startsWith("Waiting") ? state : null,
6172
+ blockedBy: toBlockers(blockers),
6173
+ workers: 0,
6174
+ progress: stage ? {
6175
+ operation: stage.event_name.replace(/^stage\/\w+\//, ""),
6176
+ phase: null,
6177
+ relation: null,
6178
+ done,
6179
+ total,
6180
+ percent: percentOf(done, total)
6181
+ } : null
6182
+ };
6183
+ };
6184
+ var STATEMENT_PROBES = {
6185
+ postgres: postgresProbe,
6186
+ mssql: mssqlProbe,
6187
+ mysql: mysqlProbe
6188
+ };
6189
+
6190
+ // src/core/runner/statement-watcher.ts
6191
+ var DEFAULT_WATCH_DELAY_MS = 1e4;
6192
+ var DEFAULT_WATCH_INTERVAL_MS = 1e4;
6193
+ var SIDE_CONNECTION_WAIT_MS = 5e3;
6194
+ var StatementWatcher = class {
6195
+ #pool;
6196
+ #dialect;
6197
+ #signal;
6198
+ #delayMs;
6199
+ #intervalMs;
6200
+ #active = null;
6201
+ #side = null;
6202
+ #sideAbandoned = false;
6203
+ #releaseSide = null;
6204
+ #sideHeld = Promise.resolve();
6205
+ #closed = false;
6206
+ #onAbort = () => {
6207
+ const statement = this.#active;
6208
+ if (statement) void this.#cancel(statement);
6209
+ };
6210
+ /**
6211
+ * @param pool - Root connection pool the side connection is checked out
6212
+ * from. Never a transaction: the side connection must be a separate session.
6213
+ */
6214
+ constructor(pool, options = {}) {
6215
+ this.#pool = pool;
6216
+ this.#dialect = options.dialect;
6217
+ this.#signal = options.signal;
6218
+ this.#delayMs = options.delayMs ?? DEFAULT_WATCH_DELAY_MS;
6219
+ this.#intervalMs = options.intervalMs ?? DEFAULT_WATCH_INTERVAL_MS;
6220
+ this.#signal?.addEventListener("abort", this.#onAbort, { once: true });
6221
+ }
6222
+ /**
6223
+ * Run `fn` on one pinned connection while watching it.
6224
+ *
6225
+ * A transaction is already one connection, so it is used as is; anything
6226
+ * else is pinned with `connection()` so the session id read up front is
6227
+ * the session the SQL runs on.
6228
+ *
6229
+ * @throws OperationAbortedError when the signal aborted before `fn` started
6230
+ */
6231
+ async run(filepath, executor, fn) {
6232
+ if (executor.isTransaction) {
6233
+ return this.#watch(filepath, executor, fn);
6234
+ }
6235
+ return executor.connection().execute((conn) => this.#watch(filepath, conn, fn));
6236
+ }
6237
+ /**
6238
+ * Stop watching and return the side connection to the pool.
6239
+ */
6240
+ async close() {
6241
+ this.#closed = true;
6242
+ this.#signal?.removeEventListener("abort", this.#onAbort);
6243
+ this.#releaseSide?.();
6244
+ await this.#sideHeld;
6245
+ }
6246
+ async #watch(filepath, conn, fn) {
6247
+ const sessionId = await this.#readSessionId(conn);
6248
+ const statement = {
6249
+ filepath,
6250
+ sessionId,
6251
+ startedAt: performance.now(),
6252
+ timer: void 0,
6253
+ finished: false,
6254
+ cancelling: null
6255
+ };
6256
+ if (this.#signal?.aborted) {
6257
+ throw new OperationAbortedError("Run cancelled before the file started");
6258
+ }
6259
+ this.#active = statement;
6260
+ statement.timer = setTimeout(() => void this.#report(statement), this.#delayMs);
6261
+ try {
6262
+ return await fn(conn);
6263
+ } finally {
6264
+ clearTimeout(statement.timer);
6265
+ this.#active = null;
6266
+ statement.finished = true;
6267
+ await statement.cancelling;
6268
+ }
6269
+ }
6270
+ async #readSessionId(conn) {
6271
+ const query = this.#dialect ? SESSION_ID_SQL[this.#dialect] : void 0;
6272
+ if (!query) return null;
6273
+ const [result, err] = await attempt(() => sql.raw(query).execute(conn));
6274
+ if (err) return null;
6275
+ return readSessionId(result.rows) ?? null;
6276
+ }
6277
+ async #report(statement) {
6278
+ if (this.#active !== statement) return;
6279
+ const status = await this.#poll(statement);
6280
+ if (this.#active !== statement) return;
6281
+ observer.emit("file:progress", {
6282
+ filepath: statement.filepath,
6283
+ elapsedMs: performance.now() - statement.startedAt,
6284
+ sessionId: statement.sessionId,
6285
+ status
6286
+ });
6287
+ statement.timer = setTimeout(() => void this.#report(statement), this.#intervalMs);
6288
+ }
6289
+ async #poll(statement) {
6290
+ const probe = this.#dialect ? STATEMENT_PROBES[this.#dialect] : void 0;
6291
+ const sessionId = statement.sessionId;
6292
+ if (!probe || sessionId === null) return null;
6293
+ const side = await this.#sideConnection();
6294
+ if (!side) return null;
6295
+ const [status, err] = await attempt(() => probe(side, sessionId));
6296
+ return err ? null : status;
6297
+ }
6298
+ async #cancel(statement) {
6299
+ const cancel = this.#dialect ? SERVER_CANCEL[this.#dialect] : void 0;
6300
+ const sessionId = statement.sessionId;
6301
+ if (sessionId === null || !cancel) return;
6302
+ const side = await this.#sideConnection();
6303
+ if (statement.finished || this.#closed) return;
6304
+ if (!side) {
6305
+ observer.emit("error", {
6306
+ source: "runner:cancel",
6307
+ error: new Error("No free connection to send the cancel from"),
6308
+ context: { filepath: statement.filepath, sessionId }
6309
+ });
6310
+ return;
6311
+ }
6312
+ const sending = attempt(() => cancel(side, sessionId));
6313
+ statement.cancelling = sending.then(() => void 0);
6314
+ const [, err] = await sending;
6315
+ if (err) {
6316
+ observer.emit("error", {
6317
+ source: "runner:cancel",
6318
+ error: err,
6319
+ context: { filepath: statement.filepath, sessionId }
6320
+ });
6321
+ }
6322
+ }
6323
+ async #sideConnection() {
6324
+ if (this.#closed || this.#sideAbandoned) return null;
6325
+ this.#side ??= this.#checkoutSide();
6326
+ const pending = this.#side;
6327
+ const [side] = await attempt(() => runWithTimeout(() => pending, { timeout: SIDE_CONNECTION_WAIT_MS }));
6328
+ if (!side && this.#side === pending) this.#sideAbandoned = true;
6329
+ return side ?? null;
6330
+ }
6331
+ /**
6332
+ * Check a connection out of the pool and hold it until `close()`.
6333
+ *
6334
+ * Kysely only lends a dedicated connection for the duration of a callback,
6335
+ * so the callback parks on a promise that `close()` resolves. A checkout
6336
+ * that arrives after the watcher gave up on it returns at once.
6337
+ */
6338
+ #checkoutSide() {
6339
+ return new Promise((resolveSide) => {
6340
+ const released = new Promise((release) => {
6341
+ this.#releaseSide = release;
6342
+ });
6343
+ this.#sideHeld = attempt(() => this.#pool.connection().execute(async (side) => {
6344
+ if (this.#sideAbandoned || this.#closed) return;
6345
+ if (this.#active === null) {
6346
+ this.#side = null;
6347
+ resolveSide(null);
6348
+ return;
6349
+ }
6350
+ resolveSide(side);
6351
+ await released;
6352
+ })).then(([, err]) => {
6353
+ if (err) {
6354
+ resolveSide(null);
6355
+ observer.emit("error", {
6356
+ source: "runner:watch",
6357
+ error: err,
6358
+ context: { operation: "side-connection" }
6359
+ });
6360
+ }
6361
+ });
6362
+ });
6363
+ }
6364
+ };
6022
6365
 
6023
6366
  // src/core/lock/types.ts
6024
6367
  var DEFAULT_LOCK_OPTIONS = {
@@ -6618,6 +6961,7 @@ var ChangeRollback = class extends Error {
6618
6961
  async function executeFiles(context, change, files, direction, checksum, force, history, startTime) {
6619
6962
  const dialect = context.dialect ?? "postgres";
6620
6963
  const expandedFiles = await expandFiles(files, context.sqlDir);
6964
+ const watcher = new StatementWatcher(context.db, { dialect });
6621
6965
  if (!TRANSACTIONAL_DIALECTS.has(dialect)) {
6622
6966
  return runFileBatch(
6623
6967
  context,
@@ -6628,8 +6972,9 @@ async function executeFiles(context, change, files, direction, checksum, force,
6628
6972
  force,
6629
6973
  history,
6630
6974
  context.db,
6631
- startTime
6632
- );
6975
+ startTime,
6976
+ watcher
6977
+ ).finally(() => watcher.close());
6633
6978
  }
6634
6979
  const [result, err] = await attempt(
6635
6980
  () => context.db.transaction().execute(async (trx) => {
@@ -6643,7 +6988,8 @@ async function executeFiles(context, change, files, direction, checksum, force,
6643
6988
  force,
6644
6989
  trxHistory,
6645
6990
  trx,
6646
- startTime
6991
+ startTime,
6992
+ watcher
6647
6993
  );
6648
6994
  if (batchResult.status !== "success") {
6649
6995
  throw new ChangeRollback(batchResult);
@@ -6651,6 +6997,7 @@ async function executeFiles(context, change, files, direction, checksum, force,
6651
6997
  return batchResult;
6652
6998
  })
6653
6999
  );
7000
+ await watcher.close();
6654
7001
  if (err) {
6655
7002
  if (err instanceof ChangeRollback) {
6656
7003
  return { ...err.result, operationId: void 0 };
@@ -6659,7 +7006,7 @@ async function executeFiles(context, change, files, direction, checksum, force,
6659
7006
  }
6660
7007
  return result;
6661
7008
  }
6662
- async function runFileBatch(context, change, expandedFiles, direction, checksum, force, history, executor, startTime) {
7009
+ async function runFileBatch(context, change, expandedFiles, direction, checksum, force, history, executor, startTime, watcher) {
6663
7010
  const [operationId, createErr] = await attempt(
6664
7011
  () => history.createOperation({
6665
7012
  name: change.name,
@@ -6783,7 +7130,9 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
6783
7130
  }
6784
7131
  break;
6785
7132
  }
6786
- const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(executor));
7133
+ const [, execErr] = await attempt(
7134
+ () => watcher.run(file.path, executor, (conn) => sql.raw(sqlContent).execute(conn))
7135
+ );
6787
7136
  const durationMs = performance.now() - fileStart;
6788
7137
  if (execErr) {
6789
7138
  const errorMessage2 = getSqlErrorMessage(execErr);
@@ -7554,7 +7903,7 @@ async function resolveVaultKey(db, dialect) {
7554
7903
  async function buildSecretsContext(stateManager, configName, db, vaultKey, dialect) {
7555
7904
  const secrets = {};
7556
7905
  if (db && vaultKey) {
7557
- const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-V6BHWNS3.js');
7906
+ const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-FBDOY3MC.js');
7558
7907
  const vaultSecrets = await getAllVaultSecrets2(db, vaultKey, dialect ?? "postgres");
7559
7908
  for (const [key, secret] of Object.entries(vaultSecrets)) {
7560
7909
  secrets[key] = secret.value;
@@ -8358,10 +8707,11 @@ async function runFile(context, filepath, options = {}) {
8358
8707
  error: createErr.message
8359
8708
  };
8360
8709
  }
8361
- const result = await executeSingleFile(context, filepath, opts, tracker, operationId);
8710
+ const watcher = createWatcher(context);
8711
+ const result = await executeSingleFile(context, filepath, opts, tracker, operationId, watcher).finally(() => watcher.close());
8362
8712
  await tracker.finalizeOperation(
8363
8713
  operationId,
8364
- result.status === "failed" ? "failed" : "success",
8714
+ result.status === "failed" || result.error ? "failed" : "success",
8365
8715
  Math.round(result.durationMs ?? 0),
8366
8716
  result.checksum,
8367
8717
  result.error
@@ -8445,6 +8795,31 @@ var DEFAULT_RUN_OPTIONS_INTERNAL = {
8445
8795
  preview: false,
8446
8796
  output: null
8447
8797
  };
8798
+ var RUN_CANCELLED = "Run cancelled";
8799
+ function createWatcher(context) {
8800
+ return new StatementWatcher(context.db, {
8801
+ dialect: context.dialect ?? "postgres",
8802
+ signal: context.signal
8803
+ });
8804
+ }
8805
+ var NOT_STARTED = /* @__PURE__ */ Symbol("not-started");
8806
+ async function runWatched(context, watcher, filepath, sqlContent) {
8807
+ const [execErrMsg, err] = await attempt(
8808
+ () => watcher.run(filepath, context.db, (conn) => executeSqlBody({ ...context, db: conn }, sqlContent))
8809
+ );
8810
+ if (err instanceof OperationAbortedError) return NOT_STARTED;
8811
+ return err ? getSqlErrorMessage(err) : execErrMsg;
8812
+ }
8813
+ async function skipRemaining(tracker, operationId, reason) {
8814
+ const skipErr = await tracker.skipRemainingFiles(operationId, reason.slice(0, 100));
8815
+ if (skipErr) {
8816
+ observer.emit("error", {
8817
+ source: "runner:skip-remaining",
8818
+ error: new Error(skipErr),
8819
+ context: { operationId }
8820
+ });
8821
+ }
8822
+ }
8448
8823
  async function executeFiles2(context, files, runOptions, execOptions) {
8449
8824
  const start = performance.now();
8450
8825
  const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...runOptions };
@@ -8532,34 +8907,48 @@ async function executeFiles2(context, files, runOptions, execOptions) {
8532
8907
  }
8533
8908
  const results = [];
8534
8909
  let failed = false;
8535
- for (let i = 0; i < files.length; i++) {
8536
- const file = files[i];
8537
- const fileRecord = fileRecords[i];
8538
- const result = await executeSingleFileWithUpdate(
8539
- context,
8540
- file.path,
8541
- fileRecord.checksum,
8542
- opts,
8543
- tracker,
8544
- operationId,
8545
- execOptions.changeType
8546
- );
8547
- results.push(result);
8548
- if (result.status === "failed" && opts.abortOnError) {
8549
- failed = true;
8550
- const skipErr = await tracker.skipRemainingFiles(
8910
+ let cancelled = false;
8911
+ const watcher = createWatcher(context);
8912
+ try {
8913
+ for (let i = 0; i < files.length; i++) {
8914
+ const file = files[i];
8915
+ const fileRecord = fileRecords[i];
8916
+ if (context.signal?.aborted) {
8917
+ failed = true;
8918
+ cancelled = true;
8919
+ await skipRemaining(tracker, operationId, RUN_CANCELLED);
8920
+ break;
8921
+ }
8922
+ const result = await executeSingleFileWithUpdate(
8923
+ context,
8924
+ file.path,
8925
+ fileRecord.checksum,
8926
+ opts,
8927
+ tracker,
8551
8928
  operationId,
8552
- `Skipped: failure in ${path.basename(file.path)}`.slice(0, 100)
8929
+ execOptions.changeType,
8930
+ watcher
8553
8931
  );
8554
- if (skipErr) {
8555
- observer.emit("error", {
8556
- source: "runner:skip-remaining",
8557
- error: new Error(skipErr),
8558
- context: { operationId }
8559
- });
8932
+ if (!result) {
8933
+ failed = true;
8934
+ cancelled = true;
8935
+ await skipRemaining(tracker, operationId, RUN_CANCELLED);
8936
+ break;
8937
+ }
8938
+ results.push(result);
8939
+ cancelled = (context.signal?.aborted ?? false) && (i < files.length - 1 || result.status === "failed");
8940
+ if (cancelled || result.status === "failed" && opts.abortOnError) {
8941
+ failed = true;
8942
+ await skipRemaining(
8943
+ tracker,
8944
+ operationId,
8945
+ cancelled ? RUN_CANCELLED : `Skipped: failure in ${path.basename(file.path)}`
8946
+ );
8947
+ break;
8560
8948
  }
8561
- break;
8562
8949
  }
8950
+ } finally {
8951
+ await watcher.close();
8563
8952
  }
8564
8953
  if (!failed && results.length !== files.length) {
8565
8954
  throw new Error(
@@ -8581,7 +8970,7 @@ async function executeFiles2(context, files, runOptions, execOptions) {
8581
8970
  finalStatus,
8582
8971
  Math.round(durationMs),
8583
8972
  combinedChecksum,
8584
- failed ? results.find((r) => r.status === "failed")?.error : void 0
8973
+ cancelled ? RUN_CANCELLED : failed ? results.find((r) => r.status === "failed")?.error : void 0
8585
8974
  );
8586
8975
  if (finalizeErr) {
8587
8976
  observer.emit("error", {
@@ -8597,7 +8986,8 @@ async function executeFiles2(context, files, runOptions, execOptions) {
8597
8986
  filesSkipped,
8598
8987
  filesFailed,
8599
8988
  durationMs,
8600
- changeId: operationId
8989
+ changeId: operationId,
8990
+ error: cancelled ? RUN_CANCELLED : void 0
8601
8991
  };
8602
8992
  }
8603
8993
  async function executeFilesInternal(context, files, options, changeType, sqlPath) {
@@ -8613,7 +9003,7 @@ async function executeFilesInternal(context, files, options, changeType, sqlPath
8613
9003
  };
8614
9004
  return executeFiles2(context, fileInputs, options, execOptions);
8615
9005
  }
8616
- async function executeSingleFileWithUpdate(context, filepath, checksum, options, tracker, operationId, changeType) {
9006
+ async function executeSingleFileWithUpdate(context, filepath, checksum, options, tracker, operationId, changeType, watcher) {
8617
9007
  const start = performance.now();
8618
9008
  const relFilepath = path.relative(context.projectRoot, filepath);
8619
9009
  const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
@@ -8673,7 +9063,8 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
8673
9063
  return result2;
8674
9064
  }
8675
9065
  }
8676
- const execErrMsg = await executeSqlBody(context, sqlContent);
9066
+ const execErrMsg = await runWatched(context, watcher, filepath, sqlContent);
9067
+ if (execErrMsg === NOT_STARTED) return null;
8677
9068
  const durationMs = performance.now() - start;
8678
9069
  if (execErrMsg) {
8679
9070
  const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
@@ -8723,7 +9114,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
8723
9114
  });
8724
9115
  return result;
8725
9116
  }
8726
- async function executeSingleFile(context, filepath, options, tracker, operationId) {
9117
+ async function executeSingleFile(context, filepath, options, tracker, operationId, watcher) {
8727
9118
  const start = performance.now();
8728
9119
  const relFilepath = path.relative(context.projectRoot, filepath);
8729
9120
  const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
@@ -8803,8 +9194,18 @@ async function executeSingleFile(context, filepath, options, tracker, operationI
8803
9194
  });
8804
9195
  return result2;
8805
9196
  }
8806
- const execErrMsg = await executeSqlBody(context, sqlContent);
9197
+ const execErrMsg = await runWatched(context, watcher, filepath, sqlContent);
8807
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
+ }
8808
9209
  if (execErrMsg) {
8809
9210
  const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
8810
9211
  const result2 = {
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from 'module';
2
- export { deleteVaultSecret, deleteVaultSecretChecked, getAllVaultSecrets, getVaultKey, getVaultKeyChecked, getVaultSecret, getVaultStatus, initializeVault, initializeVaultChecked, listVaultSecretKeys, listVaultSecretKeysChecked, setVaultSecret, setVaultSecretChecked, vaultSecretExists } from './chunk-OMY6CKR4.js';
3
- import './chunk-A2OYZAVT.js';
2
+ export { deleteVaultSecret, deleteVaultSecretChecked, getAllVaultSecrets, getVaultKey, getVaultKeyChecked, getVaultSecret, getVaultStatus, initializeVault, initializeVaultChecked, listVaultSecretKeys, listVaultSecretKeysChecked, setVaultSecret, setVaultSecretChecked, vaultSecretExists } from './chunk-WUMCIKEQ.js';
3
+ import './chunk-6LG4ENOJ.js';
4
4
  import './chunk-TXDGFA44.js';
5
5
  import './chunk-D2XW2UOP.js';
6
6
 
7
7
  createRequire(import.meta.url);
8
- //# sourceMappingURL=storage-V6BHWNS3.js.map
9
- //# sourceMappingURL=storage-V6BHWNS3.js.map
8
+ //# sourceMappingURL=storage-FBDOY3MC.js.map
9
+ //# sourceMappingURL=storage-FBDOY3MC.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noormdev/sdk",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Database schema & changeset manager SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",