@noormdev/sdk 1.4.1 → 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.
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { createRequire } from 'module';
2
- import { processFile, es_default, DtReader, isTemplate, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, ENCODED_TYPES } from './chunk-G4B7EHL7.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-FSVO6NAJ.js';
5
- import { makeNestedConfig, gigabytes, attempt, observer, attemptSync, getEnvConfigName, clone, merge, equals, retry, runWithTimeout, isTimeoutError, ObserverRelay, isDebug, wait } from './chunk-LY2F7ZFI.js';
6
- export { observer as noormObserver } from './chunk-LY2F7ZFI.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
+ import { explainConnectionError, DatabaseConnectionError } from './chunk-OXTXFKWJ.js';
8
+ import { makeNestedConfig, gigabytes, attempt, attemptSync, clone, merge, equals, retry, runWithTimeout, isTimeoutError, wait } from './chunk-TXDGFA44.js';
7
9
  import { external_exports, PortSchema, connectTimeoutFor, DEFAULT_PORTS } from './chunk-WE2JLJMT.js';
8
10
  import { dist_default } from './chunk-TBRUDUBQ.js';
9
11
  import { require_dist } from './chunk-YJBKNUTP.js';
@@ -2926,7 +2928,7 @@ async function getDialectFactory(dialect) {
2926
2928
  case "mysql":
2927
2929
  return (await import('./mysql-32B4SXWA.js')).createMysqlConnection;
2928
2930
  case "mssql":
2929
- return (await import('./mssql-3BQB6ACD.js')).createMssqlConnection;
2931
+ return (await import('./mssql-II62FO2B.js')).createMssqlConnection;
2930
2932
  default:
2931
2933
  throw new Error(`Unsupported dialect: ${dialect}`);
2932
2934
  }
@@ -2997,6 +2999,9 @@ async function openConnection(config, configName, retryOptions, signal) {
2997
2999
  backoff,
2998
3000
  jitterFactor: 0.1,
2999
3001
  signal,
3002
+ // Otherwise running out of retries reads "Max retries
3003
+ // reached", and the server's own error is lost.
3004
+ throwLastError: true,
3000
3005
  shouldRetry: (err2) => {
3001
3006
  if (err2 instanceof OperationAbortedError) return false;
3002
3007
  const msg = err2.message.toLowerCase();
@@ -3014,8 +3019,10 @@ async function openConnection(config, configName, retryOptions, signal) {
3014
3019
  )
3015
3020
  );
3016
3021
  if (err) {
3017
- observer.emit("connection:error", { configName, error: err.message });
3018
- throw err;
3022
+ const explained = explainConnectionError(err, config);
3023
+ const server = explained instanceof DatabaseConnectionError ? { serverCode: explained.serverCode, serverMessage: explained.serverMessage } : {};
3024
+ observer.emit("connection:error", { configName, error: explained.message, ...server });
3025
+ throw explained;
3019
3026
  }
3020
3027
  const manager = getConnectionManager();
3021
3028
  const trackId = manager.track(conn, configName);
@@ -3055,15 +3062,21 @@ var SYSTEM_DATABASES = {
3055
3062
  // SQLite creates the file on connect
3056
3063
  mssql: "master"
3057
3064
  };
3058
- async function testConnection(config, options = {}) {
3059
- let testConfig = config;
3060
- if (options.testServerOnly && config.dialect !== "sqlite") {
3061
- const systemDb = SYSTEM_DATABASES[config.dialect];
3062
- testConfig = {
3063
- ...config,
3064
- database: systemDb ?? config.database
3065
- };
3065
+ async function probeConnection(config, signal) {
3066
+ const [conn, err] = await attempt(
3067
+ () => createConnection(config, "__test__", {}, signal)
3068
+ );
3069
+ if (err) {
3070
+ if (err instanceof OperationAbortedError) {
3071
+ return { ok: false, error: err.message, aborted: true };
3072
+ }
3073
+ return { ok: false, error: err.message };
3066
3074
  }
3075
+ await conn.destroy();
3076
+ return { ok: true };
3077
+ }
3078
+ async function testConnection(config, options = {}) {
3079
+ const systemDb = SYSTEM_DATABASES[config.dialect];
3067
3080
  if (options.testServerOnly && config.dialect === "sqlite") {
3068
3081
  const filename = config.filename ?? config.database;
3069
3082
  if (filename !== ":memory:" && !existsSync(filename)) {
@@ -3074,17 +3087,12 @@ async function testConnection(config, options = {}) {
3074
3087
  return { ok: true };
3075
3088
  }
3076
3089
  }
3077
- const [conn, err] = await attempt(
3078
- () => createConnection(testConfig, "__test__", {}, options.signal)
3079
- );
3080
- if (err) {
3081
- if (err instanceof OperationAbortedError) {
3082
- return { ok: false, error: err.message, aborted: true };
3083
- }
3084
- return { ok: false, error: err.message };
3090
+ const result = await probeConnection(config, options.signal);
3091
+ const targetMissing = !result.ok && !result.aborted && !!result.error?.includes("does not exist");
3092
+ if (options.testServerOnly && systemDb && targetMissing) {
3093
+ return probeConnection({ ...config, database: systemDb }, options.signal);
3085
3094
  }
3086
- await conn.destroy();
3087
- return { ok: true };
3095
+ return result;
3088
3096
  }
3089
3097
  function addIdColumn(builder, dialect) {
3090
3098
  if (dialect === "postgres") {
@@ -6011,6 +6019,349 @@ function computeCombinedChecksum(checksums) {
6011
6019
  const combined = sorted.join("");
6012
6020
  return createHash("sha256").update(combined, "utf-8").digest("hex");
6013
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
+ };
6014
6365
 
6015
6366
  // src/core/lock/types.ts
6016
6367
  var DEFAULT_LOCK_OPTIONS = {
@@ -6610,6 +6961,7 @@ var ChangeRollback = class extends Error {
6610
6961
  async function executeFiles(context, change, files, direction, checksum, force, history, startTime) {
6611
6962
  const dialect = context.dialect ?? "postgres";
6612
6963
  const expandedFiles = await expandFiles(files, context.sqlDir);
6964
+ const watcher = new StatementWatcher(context.db, { dialect });
6613
6965
  if (!TRANSACTIONAL_DIALECTS.has(dialect)) {
6614
6966
  return runFileBatch(
6615
6967
  context,
@@ -6620,8 +6972,9 @@ async function executeFiles(context, change, files, direction, checksum, force,
6620
6972
  force,
6621
6973
  history,
6622
6974
  context.db,
6623
- startTime
6624
- );
6975
+ startTime,
6976
+ watcher
6977
+ ).finally(() => watcher.close());
6625
6978
  }
6626
6979
  const [result, err] = await attempt(
6627
6980
  () => context.db.transaction().execute(async (trx) => {
@@ -6635,7 +6988,8 @@ async function executeFiles(context, change, files, direction, checksum, force,
6635
6988
  force,
6636
6989
  trxHistory,
6637
6990
  trx,
6638
- startTime
6991
+ startTime,
6992
+ watcher
6639
6993
  );
6640
6994
  if (batchResult.status !== "success") {
6641
6995
  throw new ChangeRollback(batchResult);
@@ -6643,6 +6997,7 @@ async function executeFiles(context, change, files, direction, checksum, force,
6643
6997
  return batchResult;
6644
6998
  })
6645
6999
  );
7000
+ await watcher.close();
6646
7001
  if (err) {
6647
7002
  if (err instanceof ChangeRollback) {
6648
7003
  return { ...err.result, operationId: void 0 };
@@ -6651,7 +7006,7 @@ async function executeFiles(context, change, files, direction, checksum, force,
6651
7006
  }
6652
7007
  return result;
6653
7008
  }
6654
- 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) {
6655
7010
  const [operationId, createErr] = await attempt(
6656
7011
  () => history.createOperation({
6657
7012
  name: change.name,
@@ -6775,7 +7130,9 @@ async function runFileBatch(context, change, expandedFiles, direction, checksum,
6775
7130
  }
6776
7131
  break;
6777
7132
  }
6778
- 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
+ );
6779
7136
  const durationMs = performance.now() - fileStart;
6780
7137
  if (execErr) {
6781
7138
  const errorMessage2 = getSqlErrorMessage(execErr);
@@ -7546,7 +7903,7 @@ async function resolveVaultKey(db, dialect) {
7546
7903
  async function buildSecretsContext(stateManager, configName, db, vaultKey, dialect) {
7547
7904
  const secrets = {};
7548
7905
  if (db && vaultKey) {
7549
- const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-V5I67ANZ.js');
7906
+ const { getAllVaultSecrets: getAllVaultSecrets2 } = await import('./storage-FBDOY3MC.js');
7550
7907
  const vaultSecrets = await getAllVaultSecrets2(db, vaultKey, dialect ?? "postgres");
7551
7908
  for (const [key, secret] of Object.entries(vaultSecrets)) {
7552
7909
  secrets[key] = secret.value;
@@ -8350,10 +8707,11 @@ async function runFile(context, filepath, options = {}) {
8350
8707
  error: createErr.message
8351
8708
  };
8352
8709
  }
8353
- 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());
8354
8712
  await tracker.finalizeOperation(
8355
8713
  operationId,
8356
- result.status === "failed" ? "failed" : "success",
8714
+ result.status === "failed" || result.error ? "failed" : "success",
8357
8715
  Math.round(result.durationMs ?? 0),
8358
8716
  result.checksum,
8359
8717
  result.error
@@ -8437,6 +8795,31 @@ var DEFAULT_RUN_OPTIONS_INTERNAL = {
8437
8795
  preview: false,
8438
8796
  output: null
8439
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
+ }
8440
8823
  async function executeFiles2(context, files, runOptions, execOptions) {
8441
8824
  const start = performance.now();
8442
8825
  const opts = { ...DEFAULT_RUN_OPTIONS_INTERNAL, ...runOptions };
@@ -8524,34 +8907,48 @@ async function executeFiles2(context, files, runOptions, execOptions) {
8524
8907
  }
8525
8908
  const results = [];
8526
8909
  let failed = false;
8527
- for (let i = 0; i < files.length; i++) {
8528
- const file = files[i];
8529
- const fileRecord = fileRecords[i];
8530
- const result = await executeSingleFileWithUpdate(
8531
- context,
8532
- file.path,
8533
- fileRecord.checksum,
8534
- opts,
8535
- tracker,
8536
- operationId,
8537
- execOptions.changeType
8538
- );
8539
- results.push(result);
8540
- if (result.status === "failed" && opts.abortOnError) {
8541
- failed = true;
8542
- 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,
8543
8928
  operationId,
8544
- `Skipped: failure in ${path.basename(file.path)}`.slice(0, 100)
8929
+ execOptions.changeType,
8930
+ watcher
8545
8931
  );
8546
- if (skipErr) {
8547
- observer.emit("error", {
8548
- source: "runner:skip-remaining",
8549
- error: new Error(skipErr),
8550
- context: { operationId }
8551
- });
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;
8552
8948
  }
8553
- break;
8554
8949
  }
8950
+ } finally {
8951
+ await watcher.close();
8555
8952
  }
8556
8953
  if (!failed && results.length !== files.length) {
8557
8954
  throw new Error(
@@ -8573,7 +8970,7 @@ async function executeFiles2(context, files, runOptions, execOptions) {
8573
8970
  finalStatus,
8574
8971
  Math.round(durationMs),
8575
8972
  combinedChecksum,
8576
- failed ? results.find((r) => r.status === "failed")?.error : void 0
8973
+ cancelled ? RUN_CANCELLED : failed ? results.find((r) => r.status === "failed")?.error : void 0
8577
8974
  );
8578
8975
  if (finalizeErr) {
8579
8976
  observer.emit("error", {
@@ -8589,7 +8986,8 @@ async function executeFiles2(context, files, runOptions, execOptions) {
8589
8986
  filesSkipped,
8590
8987
  filesFailed,
8591
8988
  durationMs,
8592
- changeId: operationId
8989
+ changeId: operationId,
8990
+ error: cancelled ? RUN_CANCELLED : void 0
8593
8991
  };
8594
8992
  }
8595
8993
  async function executeFilesInternal(context, files, options, changeType, sqlPath) {
@@ -8605,7 +9003,7 @@ async function executeFilesInternal(context, files, options, changeType, sqlPath
8605
9003
  };
8606
9004
  return executeFiles2(context, fileInputs, options, execOptions);
8607
9005
  }
8608
- async function executeSingleFileWithUpdate(context, filepath, checksum, options, tracker, operationId, changeType) {
9006
+ async function executeSingleFileWithUpdate(context, filepath, checksum, options, tracker, operationId, changeType, watcher) {
8609
9007
  const start = performance.now();
8610
9008
  const relFilepath = path.relative(context.projectRoot, filepath);
8611
9009
  const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
@@ -8665,7 +9063,8 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
8665
9063
  return result2;
8666
9064
  }
8667
9065
  }
8668
- const execErrMsg = await executeSqlBody(context, sqlContent);
9066
+ const execErrMsg = await runWatched(context, watcher, filepath, sqlContent);
9067
+ if (execErrMsg === NOT_STARTED) return null;
8669
9068
  const durationMs = performance.now() - start;
8670
9069
  if (execErrMsg) {
8671
9070
  const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
@@ -8715,7 +9114,7 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
8715
9114
  });
8716
9115
  return result;
8717
9116
  }
8718
- async function executeSingleFile(context, filepath, options, tracker, operationId) {
9117
+ async function executeSingleFile(context, filepath, options, tracker, operationId, watcher) {
8719
9118
  const start = performance.now();
8720
9119
  const relFilepath = path.relative(context.projectRoot, filepath);
8721
9120
  const [sqlContent, loadErr] = await attempt(() => loadAndRenderFile2(context, filepath));
@@ -8795,8 +9194,18 @@ async function executeSingleFile(context, filepath, options, tracker, operationI
8795
9194
  });
8796
9195
  return result2;
8797
9196
  }
8798
- const execErrMsg = await executeSqlBody(context, sqlContent);
9197
+ const execErrMsg = await runWatched(context, watcher, filepath, sqlContent);
8799
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
+ }
8800
9209
  if (execErrMsg) {
8801
9210
  const error = execErrMsg + await describePriorSuccesses(tracker, relFilepath, operationId);
8802
9211
  const result2 = {
@@ -9227,7 +9636,8 @@ var postgresExploreOperations = {
9227
9636
  SELECT
9228
9637
  p.proname,
9229
9638
  n.nspname,
9230
- p.pronargs::text as param_count
9639
+ p.pronargs::text as param_count,
9640
+ pg_get_function_identity_arguments(p.oid) as identity_args
9231
9641
  FROM pg_proc p
9232
9642
  JOIN pg_namespace n ON p.pronamespace = n.oid
9233
9643
  WHERE n.nspname ${schemaFilter(schema)}
@@ -9242,7 +9652,8 @@ var postgresExploreOperations = {
9242
9652
  return result.rows.map((row) => ({
9243
9653
  name: row.proname,
9244
9654
  schema: row.nspname,
9245
- parameterCount: parseInt(row.param_count, 10)
9655
+ parameterCount: parseInt(row.param_count, 10),
9656
+ signature: row.identity_args
9246
9657
  }));
9247
9658
  },
9248
9659
  async listFunctions(db, schema) {
@@ -9251,7 +9662,8 @@ var postgresExploreOperations = {
9251
9662
  p.proname,
9252
9663
  n.nspname,
9253
9664
  p.pronargs::text as param_count,
9254
- pg_get_function_result(p.oid) as return_type
9665
+ pg_get_function_result(p.oid) as return_type,
9666
+ pg_get_function_identity_arguments(p.oid) as identity_args
9255
9667
  FROM pg_proc p
9256
9668
  JOIN pg_namespace n ON p.pronamespace = n.oid
9257
9669
  WHERE n.nspname ${schemaFilter(schema)}
@@ -9267,7 +9679,8 @@ var postgresExploreOperations = {
9267
9679
  name: row.proname,
9268
9680
  schema: row.nspname,
9269
9681
  parameterCount: parseInt(row.param_count, 10),
9270
- returnType: row.return_type
9682
+ returnType: row.return_type,
9683
+ signature: row.identity_args
9271
9684
  }));
9272
9685
  },
9273
9686
  async listTypes(db, schema) {