pg-boss 12.25.0 → 12.26.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
@@ -32,8 +32,9 @@ export function getRollbackPlans(schema, version) {
32
32
  export class PgBoss extends EventEmitter {
33
33
  #stoppingOn;
34
34
  #stopped;
35
- #starting;
36
35
  #started;
36
+ #startingPromise = null;
37
+ #stoppingPromise = null;
37
38
  #config;
38
39
  #db;
39
40
  #boss;
@@ -83,43 +84,58 @@ export class PgBoss extends EventEmitter {
83
84
  }
84
85
  }
85
86
  async start() {
86
- if (this.#starting || this.#started) {
87
+ // A stop() already in flight must finish (clearing any resources it's tearing down) before a
88
+ // fresh start() begins, otherwise the two race over the same intervals/pool.
89
+ if (this.#stoppingPromise) {
90
+ await this.#stoppingPromise.catch(() => { });
91
+ }
92
+ // Return the SAME in-flight promise to a concurrent caller instead of a fresh `this` — a
93
+ // second caller must observe the actual outcome (including a rejection), not silently no-op
94
+ // while the first call is still mid-flight.
95
+ if (this.#startingPromise) {
96
+ return this.#startingPromise;
97
+ }
98
+ if (this.#started) {
87
99
  return this;
88
100
  }
89
- this.#starting = true;
101
+ // Cleared to false before any subsystem is started (not just on success): if #doStart throws
102
+ // partway through, subsystems already started (e.g. manager's queueCacheInterval/wipInterval)
103
+ // must still be reachable by stop() for cleanup, and stop() no-ops whenever #stopped is true.
104
+ this.#stopped = false;
105
+ this.#startingPromise = this.#doStart();
90
106
  try {
91
- if (this.#db._pgbdb && !this.#db.opened) {
92
- await this.#db.open();
93
- }
94
- await this.#warnIfDistributedMisconfigured();
95
- if (this.#config.migrate) {
96
- await this.#contractor.start();
97
- }
98
- else {
99
- await this.#contractor.check();
100
- }
101
- await this.#manager.start();
102
- if (this.#config.useListenNotify) {
103
- await this.#notifier.start();
104
- }
105
- if (this.#config.supervise) {
106
- await this.#boss.start();
107
- await this.#navigator.start();
108
- }
109
- if (this.#config.schedule) {
110
- await this.#timekeeper.start();
111
- }
112
- if (this.#config.migrate) {
113
- await this.#bam.start();
114
- }
107
+ return await this.#startingPromise;
108
+ }
109
+ finally {
110
+ this.#startingPromise = null;
111
+ }
112
+ }
113
+ async #doStart() {
114
+ if (this.#db._pgbdb && !this.#db.opened) {
115
+ await this.#db.open();
116
+ }
117
+ await this.#warnIfDistributedMisconfigured();
118
+ if (this.#config.migrate) {
119
+ await this.#contractor.start();
120
+ }
121
+ else {
122
+ await this.#contractor.check();
123
+ }
124
+ await this.#manager.start();
125
+ if (this.#config.useListenNotify) {
126
+ await this.#notifier.start();
115
127
  }
116
- catch (err) {
117
- this.#starting = false;
118
- throw err;
128
+ if (this.#config.supervise) {
129
+ await this.#boss.start();
130
+ await this.#navigator.start();
131
+ }
132
+ if (this.#config.schedule) {
133
+ await this.#timekeeper.start();
134
+ }
135
+ if (this.#config.migrate) {
136
+ await this.#bam.start();
119
137
  }
120
- this.#starting = false;
121
138
  this.#started = true;
122
- this.#stopped = false;
123
139
  return this;
124
140
  }
125
141
  // YugabyteDB needs the yugabytedb backend profile (no table partitioning + no advisory locks;
@@ -144,37 +160,61 @@ export class PgBoss extends EventEmitter {
144
160
  }
145
161
  }
146
162
  async stop(options = {}) {
147
- if (this.#stoppingOn || this.#stopped) {
163
+ // A start() already in flight must finish (or fail) before stop() evaluates state, otherwise
164
+ // stop() reads #stopped mid-start and silently no-ops while start() keeps running.
165
+ if (this.#startingPromise) {
166
+ await this.#startingPromise.catch(() => { });
167
+ }
168
+ if (this.#stoppingPromise) {
169
+ return this.#stoppingPromise;
170
+ }
171
+ if (this.#stopped) {
148
172
  return;
149
173
  }
150
174
  let { close = true, graceful = true, timeout = 30000 } = options;
151
175
  timeout = Math.max(timeout, 1000);
152
176
  this.#stoppingOn = Date.now();
153
- await this.#notifier.stop();
154
- await this.#manager.stop();
155
- await this.#timekeeper.stop();
156
- await this.#boss.stop();
157
- await this.#navigator.stop();
158
- await this.#bam.stop();
159
- const shutdown = async () => {
160
- await this.#manager.failWip();
161
- if (this.#db._pgbdb && this.#db.opened && close) {
162
- await this.#db.close();
163
- // Give event loop time to process socket closes
164
- await delay(10);
177
+ this.#stoppingPromise = this.#doStop(close, graceful, timeout);
178
+ try {
179
+ return await this.#stoppingPromise;
180
+ }
181
+ finally {
182
+ this.#stoppingPromise = null;
183
+ }
184
+ }
185
+ async #doStop(close, graceful, timeout) {
186
+ try {
187
+ await this.#notifier.stop();
188
+ await this.#manager.stop();
189
+ await this.#timekeeper.stop();
190
+ await this.#boss.stop();
191
+ await this.#navigator.stop();
192
+ await this.#bam.stop();
193
+ const shutdown = async () => {
194
+ await this.#manager.failWip();
195
+ if (this.#db._pgbdb && this.#db.opened && close) {
196
+ await this.#db.close();
197
+ // Give event loop time to process socket closes
198
+ await delay(10);
199
+ }
200
+ this.#stopped = true;
201
+ this.#started = false;
202
+ this.emit(events.stopped);
203
+ };
204
+ if (!graceful) {
205
+ await shutdown();
206
+ return;
165
207
  }
166
- this.#stopped = true;
167
- this.#stoppingOn = null;
168
- this.#started = false;
169
- this.emit(events.stopped);
170
- };
171
- if (!graceful) {
172
- return await shutdown();
208
+ while ((Date.now() - this.#stoppingOn) < timeout && this.#manager.hasPendingCleanups()) {
209
+ await delay(500);
210
+ }
211
+ await shutdown();
173
212
  }
174
- while ((Date.now() - this.#stoppingOn) < timeout && this.#manager.hasPendingCleanups()) {
175
- await delay(500);
213
+ finally {
214
+ // Reset unconditionally (success or throw) so a stop() that fails partway can be retried
215
+ // instead of every future stop()/start() call silently no-op-ing forever on the stale marker.
216
+ this.#stoppingOn = null;
176
217
  }
177
- await shutdown();
178
218
  }
179
219
  async send(...args) {
180
220
  return await this.#manager.send(...args);
@@ -325,6 +365,9 @@ export class PgBoss extends EventEmitter {
325
365
  schemaVersion() {
326
366
  return this.#contractor.schemaVersion();
327
367
  }
368
+ detectSchemaDrift() {
369
+ return this.#contractor.detectDrift();
370
+ }
328
371
  schedule(name, cron, data, options) {
329
372
  return this.#timekeeper.schedule(name, cron, data, options);
330
373
  }
@@ -1 +1 @@
1
- {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../src/manager.ts"],"names":[],"mappings":"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"}
package/dist/manager.js CHANGED
@@ -252,7 +252,7 @@ class Manager extends EventEmitter {
252
252
  // Fail the whole batch so the mistake surfaces and the jobs are retried.
253
253
  const err = new Error('perJobResults handler must resolve with an array of job results');
254
254
  await this.fail(name, jobs.map(job => job.id), err);
255
- this.#trackJobsFailed(name, jobs, err);
255
+ await this.#trackJobsFailed(name, jobs, err);
256
256
  return;
257
257
  }
258
258
  // Index the handler's dispositions by job id, keeping only valid entries that reference a job
@@ -393,7 +393,12 @@ class Manager extends EventEmitter {
393
393
  async #processJobs(name, jobs, callback, worker, heartbeatRefreshSeconds, perJobResults = false) {
394
394
  const jobIds = jobs.map(job => job.id);
395
395
  const maxExpiration = jobs.reduce((acc, i) => Math.max(acc, i.expireInSeconds), 0);
396
- const heartbeatSeconds = jobs.reduce((acc, j) => Math.max(acc, j.heartbeatSeconds || 0), 0);
396
+ // Minimum, not maximum: heartbeatSeconds is per-job, and failJobsByHeartbeat fails a job once
397
+ // its OWN heartbeat_on is stale by ITS OWN heartbeat_seconds. A refresh cadence derived from
398
+ // the batch max would let a small-heartbeat job in a mixed batch go stale and get failed out
399
+ // from under a still-running handler before the shared timer ever touches it.
400
+ const heartbeatCandidates = jobs.map(j => j.heartbeatSeconds || 0).filter(s => s > 0);
401
+ const heartbeatSeconds = heartbeatCandidates.length ? Math.min(...heartbeatCandidates) : 0;
397
402
  const ac = new AbortController();
398
403
  jobs.forEach(job => { job.signal = ac.signal; });
399
404
  // Store AbortController on worker so it can be aborted after graceful shutdown
@@ -578,18 +583,18 @@ class Manager extends EventEmitter {
578
583
  }
579
584
  else {
580
585
  const { allowed, excess, groupedJobs } = this.#trackLocalGroupStart(name, jobs);
581
- if (excess.length > 0) {
582
- const excessIds = excess.map(job => job.id);
583
- await this.restore(name, excessIds);
584
- }
585
- if (allowed.length > 0) {
586
- try {
587
- await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds, perJobResults);
586
+ try {
587
+ if (excess.length > 0) {
588
+ const excessIds = excess.map(job => job.id);
589
+ await this.restore(name, excessIds);
588
590
  }
589
- finally {
590
- this.#trackLocalGroupEnd(name, groupedJobs);
591
+ if (allowed.length > 0) {
592
+ await this.#processJobs(name, allowed, callback, worker, heartbeatRefreshSeconds, perJobResults);
591
593
  }
592
594
  }
595
+ finally {
596
+ this.#trackLocalGroupEnd(name, groupedJobs);
597
+ }
593
598
  }
594
599
  this.emitWip(name);
595
600
  };
@@ -638,7 +643,13 @@ class Manager extends EventEmitter {
638
643
  async offWork(name, options = { wait: true }) {
639
644
  assert(name, 'queue name is required');
640
645
  assert(typeof name === 'string', 'queue name must be a string');
641
- const query = (i) => options?.id ? i.id === options.id : i.name === name;
646
+ // work() returns only the first spawned worker's id (shared as `workId` across every worker
647
+ // it spawned under localConcurrency), so { id } must match on workId too — otherwise only
648
+ // worker 0 of a localConcurrency > 1 call ever stops, and the rest poll forever with no other
649
+ // way to reach them. i.id is still checked so a specific worker id from getWipData() still
650
+ // targets just that one worker. name is always required so a stray/mismatched id can't stop
651
+ // a worker on a different queue.
652
+ const query = (i) => i.name === name && (options?.id ? (i.id === options.id || i.workId === options.id) : true);
642
653
  const workers = this.getWorkers().filter(i => query(i) && !i.stopping && !i.stopped);
643
654
  if (workers.length === 0) {
644
655
  return;
@@ -886,6 +897,15 @@ class Manager extends EventEmitter {
886
897
  }
887
898
  async insert(name, jobs, options = {}) {
888
899
  assert(Array.isArray(jobs), 'jobs argument should be an array');
900
+ const seenIds = new Set();
901
+ for (const job of jobs) {
902
+ if (job.id != null) {
903
+ if (seenIds.has(job.id)) {
904
+ throw new Error(`duplicate job id in insert batch: ${job.id}`);
905
+ }
906
+ seenIds.add(job.id);
907
+ }
908
+ }
889
909
  const { table, policy, notify } = await this.getQueueCache(name);
890
910
  if (policy === plans.QUEUE_POLICIES.key_strict_fifo) {
891
911
  for (const job of jobs) {
@@ -894,20 +914,35 @@ class Manager extends EventEmitter {
894
914
  }
895
915
  }
896
916
  }
917
+ const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
918
+ // insertJobs ends in ON CONFLICT DO NOTHING, so skipped rows shift the returned rows out of
919
+ // alignment with the input jobs — a positional rows[i] <-> jobs[i] pairing attributes the wrong
920
+ // data to the wrong id. When a spy is watching, assign every job an explicit id up front (the
921
+ // insert COALESCEs id, so this is equivalent to letting the DB generate one) and index data by
922
+ // id, so returned rows can be matched back to their job regardless of any conflicts.
923
+ const dataById = spy ? new Map() : undefined;
897
924
  const insertPayload = jobs.map(j => {
898
925
  const { blocked, blocking, pendingDependencies, ...rest } = j;
926
+ if (dataById) {
927
+ // Best-effort spy bookkeeping, only reached when __test__enableSpies is set (a test-intended
928
+ // opt-in, off by default). The id we assign here is exactly what the DB would otherwise
929
+ // COALESCE in, so generating it client-side is harmless — and if randomUUID ever fell short,
930
+ // only spy attribution would degrade, never the insert itself.
931
+ rest.id ??= randomUUID();
932
+ dataById.set(rest.id, j.data ?? {});
933
+ }
899
934
  return rest;
900
935
  });
901
936
  const db = this.assertDb(options);
902
- const spy = this.config.__test__enableSpies ? this.#spies.get(name) : undefined;
903
937
  // Return IDs if spy is active for this queue (needed for job tracking)
904
938
  const returnId = !!spy || !!options.returnId;
905
939
  const sql = plans.insertJobs(this.config.schema, { table, name, returnId, notify: this.#notifyEnabled(notify) });
906
940
  const { rows } = await db.executeSql(sql, [JSON.stringify(insertPayload)]);
907
941
  if (rows.length) {
908
942
  if (spy) {
909
- for (let i = 0; i < rows.length; i++) {
910
- spy.addJob(rows[i].id, name, jobs[i].data || {}, 'created');
943
+ // dataById is populated for every job when a spy is active
944
+ for (const row of rows) {
945
+ spy.addJob(row.id, name, dataById.get(row.id), 'created');
911
946
  }
912
947
  }
913
948
  return rows.map((i) => i.id);
@@ -1035,7 +1070,13 @@ class Manager extends EventEmitter {
1035
1070
  result = await db.executeSql(query.text, query.values);
1036
1071
  }
1037
1072
  catch (err) {
1038
- // errors from fetchquery should only be unique constraint violations
1073
+ // The only fetch error we tolerate is a unique-constraint violation (SQLSTATE 23505) from a
1074
+ // policy/singleton index when a concurrent fetch won the same slot — treat that as an empty
1075
+ // fetch. Anything else (a DB outage, a malformed query) must surface: swallowing it turned
1076
+ // every failed fetch into a silent [] with no error event, indistinguishable from an empty
1077
+ // queue. Rethrowing routes it to the worker's onError (emits `error`) or to a direct caller.
1078
+ if (err?.code !== '23505')
1079
+ throw err;
1039
1080
  }
1040
1081
  const rows = result?.rows || [];
1041
1082
  // CockroachDB returns integer columns as strings; normalize them. Even a minimal fetch
@@ -1220,7 +1261,7 @@ class Manager extends EventEmitter {
1220
1261
  }
1221
1262
  else {
1222
1263
  const exp = Math.min(16, retryCount + 1);
1223
- const delay = retryDelay * (Math.pow(2, exp) / 2 + Math.pow(2, exp) / 2 * Math.random());
1264
+ const delay = Math.max(retryDelay, 1) * (Math.pow(2, exp) / 2 + Math.pow(2, exp) / 2 * Math.random());
1224
1265
  // Match the canonical failJobs() SQL: LEAST(retry_delay_max, delay) caps the backoff,
1225
1266
  // treating NULL as "no cap" and 0 as a real cap. (`?:` would wrongly treat 0 as no cap.)
1226
1267
  const cappedDelay = retryDelayMax != null ? Math.min(retryDelayMax, delay) : delay;
@@ -1253,7 +1294,7 @@ class Manager extends EventEmitter {
1253
1294
  ]);
1254
1295
  // Insert to dead letter queue if failed and has dead_letter configured
1255
1296
  if (job.dead_letter) {
1256
- await tx.executeSql(dlqSql, [job.dead_letter, job.data, jobOutput, job.name, job.id, job.created_on, job.retry_count]);
1297
+ await tx.executeSql(dlqSql, [job.dead_letter, job.data, jobOutput, job.name, job.id, job.created_on, job.retry_count, job.singleton_key, job.heartbeat_seconds]);
1257
1298
  }
1258
1299
  }
1259
1300
  count++;
@@ -1399,13 +1440,18 @@ class Manager extends EventEmitter {
1399
1440
  }
1400
1441
  async deleteQueue(name) {
1401
1442
  Attorney.assertQueueName(name);
1443
+ // Scope the catch to the cache lookup only: a queue that doesn't exist is a no-op. The DELETE
1444
+ // and cache eviction must NOT be swallowed — a transient connection error there previously
1445
+ // resolved as success while the queue (and its stale cache entry) survived.
1402
1446
  try {
1403
1447
  await this.getQueueCache(name);
1404
- const sql = plans.deleteQueue(this.config.schema, name, this.config.noAdvisoryLocks);
1405
- await this.db.executeSql(sql);
1406
- this.#evictQueueCache(name);
1407
1448
  }
1408
- catch { }
1449
+ catch {
1450
+ return;
1451
+ }
1452
+ const sql = plans.deleteQueue(this.config.schema, name, this.config.noAdvisoryLocks);
1453
+ await this.db.executeSql(sql);
1454
+ this.#evictQueueCache(name);
1409
1455
  }
1410
1456
  async deleteQueuedJobs(name) {
1411
1457
  Attorney.assertQueueName(name);
@@ -11,6 +11,7 @@ declare function rollback(schema: string, version: number, migrations?: types.Mi
11
11
  declare function next(schema: string, version: number, migrations?: types.Migration[], noAdvisoryLocks?: boolean): string;
12
12
  declare function migrateCommands(schema: string, version: number, migrations?: types.Migration[], noAdvisoryLocks?: boolean, options?: MigrateOptions): MigrationCommands;
13
13
  declare function migrate(schema: string, version: number, migrations?: types.Migration[], noAdvisoryLocks?: boolean, options?: MigrateOptions): string;
14
+ declare function getMinVersion(schema: string): number;
14
15
  declare function getAll(schema: string, noPartitioning?: boolean, noCovering?: boolean): types.Migration[];
15
- export { rollback, next, migrate, migrateCommands, getAll, };
16
+ export { rollback, next, migrate, migrateCommands, getAll, getMinVersion, };
16
17
  //# sourceMappingURL=migrationStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAKnC,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,OAAO,CAAA;IAIrB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AA4CD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AASD,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAQ5G;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAQxG;AAKD,iBAAS,eAAe,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CAgCrK;AAKD,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,UAIzI;AAopBD,iBAAS,MAAM,CAAE,MAAM,EAAE,MAAM,EAAE,cAAc,UAAQ,EAAE,UAAU,UAAQ,GAAG,KAAK,CAAC,SAAS,EAAE,CAmZ9F;AAED,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,eAAe,EACf,MAAM,GACP,CAAA"}
1
+ {"version":3,"file":"migrationStore.d.ts","sourceRoot":"","sources":["../src/migrationStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AAKnC,UAAU,cAAc;IACtB,WAAW,CAAC,EAAE,OAAO,CAAA;IAIrB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B;AAgDD,UAAU,iBAAiB;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,EAAE,CAAA;CACrB;AASD,iBAAS,QAAQ,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAQ5G;AAED,iBAAS,IAAI,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,UAQxG;AAKD,iBAAS,eAAe,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,GAAG,iBAAiB,CA+CrK;AAKD,iBAAS,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,EAAE,eAAe,CAAC,EAAE,OAAO,EAAE,OAAO,GAAE,cAAmB,UAIzI;AAyrBD,iBAAS,aAAa,CAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED,iBAAS,MAAM,CAAE,MAAM,EAAE,MAAM,EAAE,cAAc,UAAQ,EAAE,UAAU,UAAQ,GAAG,KAAK,CAAC,SAAS,EAAE,CAoa9F;AAED,OAAO,EACL,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,eAAe,EACf,MAAM,EACN,aAAa,GACd,CAAA"}
@@ -4,9 +4,13 @@ import * as types from "./types.js";
4
4
  // Mirrors the SQL job_table_format() function (src/plans.ts): rewrites a command targeting
5
5
  // the base `job` table to target a specific partition table.
6
6
  function formatJobTable(command, table) {
7
+ // Anchor both rewrites so a schema name that itself contains these substrings (e.g. `job_intake`)
8
+ // isn't mangled: `.job\b` only matches the base table reference (`schema.job`, not `schema.job_i5`
9
+ // whose `job` is followed by `_`), and `job_iN` only matches the bare index-name tokens (job_i1..9),
10
+ // never the `job_i` inside an arbitrary schema name.
7
11
  return command
8
- .replaceAll('.job', `.${table}`)
9
- .replaceAll('job_i', `${table}_i`);
12
+ .replace(/\.job\b/g, `.${table}`)
13
+ .replace(/\bjob_i(\d+)/g, `${table}_i$1`);
10
14
  }
11
15
  // Derives the direct index DDL that a job_table_run_async() command would eventually run
12
16
  // via BAM, one statement per target table, each prefixed with a provenance comment. The
@@ -53,6 +57,19 @@ function next(schema, version, migrations, noAdvisoryLocks) {
53
57
  // programmatically must run `concurrent` statements individually, outside a transaction.
54
58
  function migrateCommands(schema, version, migrations, noAdvisoryLocks, options = {}) {
55
59
  migrations = migrations || getAll(schema);
60
+ // Refuse to migrate from a real DB version older than the oldest migration can start from.
61
+ // Without this floor, `filter(i => i.previous >= version)` happily selects the whole chain for any
62
+ // version below the minimum `previous`, applying migrations over missing intermediate steps — a
63
+ // cryptic mid-transaction failure, or worse a "success" that stamps the latest version onto an
64
+ // incomplete schema. Version 0 is the sentinel for a full "from scratch" export (getMigrationPlans)
65
+ // and is intentionally exempt.
66
+ // Only floor a valid numeric version; a non-numeric/garbage version falls through to the
67
+ // "Version X not found" assert below. Version 0 is the full-export sentinel and is exempt.
68
+ if (Number.isInteger(version) && version !== 0) {
69
+ const minPrevious = Math.min(...migrations.map(i => i.previous));
70
+ assert(version >= minPrevious, `Cannot migrate pg-boss schema from version ${version}: the oldest supported starting version is ${minPrevious}. ` +
71
+ 'Upgrade to a schema at or above that version using an older pg-boss release first.');
72
+ }
56
73
  const concurrent = [];
57
74
  const result = migrations
58
75
  .filter(i => i.previous >= version)
@@ -732,6 +749,45 @@ const ensureQueueStatsPartitionsFn = {
732
749
  $$
733
750
  `
734
751
  };
752
+ // Frozen job_table_format() bodies, one per schema version, so the v37 migration is an immutable
753
+ // snapshot even if plans.jobTableFormatFunction drifts later. 37 is the anchored regexp_replace
754
+ // fix (matches only the base table reference and bare job_iN tokens); 36 is the prior naive
755
+ // replace(), kept solely so v37's rollback restores the exact previous definition.
756
+ const jobTableFormatFn = {
757
+ 36: (schema) => `
758
+ CREATE OR REPLACE FUNCTION ${schema}.job_table_format(command text, table_name text)
759
+ RETURNS text AS
760
+ $$
761
+ SELECT format(
762
+ replace(
763
+ replace(command, '.job', '.%1$I'),
764
+ 'job_i', '%1$s_i'
765
+ ),
766
+ table_name
767
+ );
768
+ $$
769
+ LANGUAGE sql IMMUTABLE;
770
+ `,
771
+ 37: (schema) => `
772
+ CREATE OR REPLACE FUNCTION ${schema}.job_table_format(command text, table_name text)
773
+ RETURNS text AS
774
+ $$
775
+ SELECT format(
776
+ regexp_replace(
777
+ regexp_replace(command, '\\.job\\y', '.%1$I', 'g'),
778
+ '\\yjob_i(\\d+)', '%1$s_i\\1', 'g'
779
+ ),
780
+ table_name
781
+ );
782
+ $$
783
+ LANGUAGE sql IMMUTABLE;
784
+ `
785
+ };
786
+ // Lowest schema version a migration can start from (the smallest `previous` in the set). Below
787
+ // this there is no chain to apply; callers use it as an honest floor / offline fallback.
788
+ function getMinVersion(schema) {
789
+ return Math.min(...getAll(schema).map(i => i.previous));
790
+ }
735
791
  function getAll(schema, noPartitioning = false, noCovering = false) {
736
792
  return [
737
793
  {
@@ -877,7 +933,7 @@ function getAll(schema, noPartitioning = false, noCovering = false) {
877
933
  `ALTER INDEX ${schema}.job_common_i3 RENAME TO job_i3`,
878
934
  `ALTER INDEX ${schema}.job_common_i2 RENAME TO job_i2`,
879
935
  `ALTER INDEX ${schema}.job_common_i1 RENAME TO job_i1`,
880
- `SELECT ${schema}.job_table_run('DROP INDEX ${schema}.job_i7')`,
936
+ `SELECT ${schema}.job_table_run('DROP INDEX IF EXISTS ${schema}.job_i7')`,
881
937
  createQueueFn[26](schema),
882
938
  `DROP FUNCTION ${schema}.job_table_run(text, text, text)`,
883
939
  `DROP FUNCTION ${schema}.job_table_run_async(text, int, text, text, text)`,
@@ -1133,7 +1189,24 @@ function getAll(schema, noPartitioning = false, noCovering = false) {
1133
1189
  ],
1134
1190
  // The default change is forward-compatible and harmless to keep, so rollback leaves it in place.
1135
1191
  uninstall: []
1192
+ },
1193
+ {
1194
+ release: '12.26.0',
1195
+ version: 37,
1196
+ previous: 36,
1197
+ // Fix job_table_format(): the naive replace() mangled schema names containing `.job` or
1198
+ // `job_i` (e.g. `job_intake`), rewriting index builds to a nonexistent schema. The anchored
1199
+ // regexp_replace version matches only the base table reference and bare job_iN index tokens.
1200
+ // Only installed where partitioning is enabled — the function is created by plans.create()
1201
+ // solely in that case (plans.ts), so a noPartitioning database has none to replace.
1202
+ install: noPartitioning
1203
+ ? []
1204
+ : [jobTableFormatFn[37](schema)],
1205
+ // Restore the prior (naive) definition on rollback so the schema matches v36 exactly.
1206
+ uninstall: noPartitioning
1207
+ ? []
1208
+ : [jobTableFormatFn[36](schema)]
1136
1209
  }
1137
1210
  ];
1138
1211
  }
1139
- export { rollback, next, migrate, migrateCommands, getAll, };
1212
+ export { rollback, next, migrate, migrateCommands, getAll, getMinVersion, };
package/dist/plans.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { JobMatchStrategy, UpdateQueueOptions } from './types.ts';
1
+ import type { JobMatchStrategy, UpdateQueueOptions, ManagedIndex, ManagedFunction } from './types.ts';
2
+ import type { ExpectedColumns, ExpectedConstraints } from './drifter.ts';
2
3
  export interface SqlQuery {
3
4
  text: string;
4
5
  values: unknown[];
@@ -9,6 +10,7 @@ export declare const PG_ERROR: {
9
10
  export declare const DEFAULT_SCHEMA = "pgboss";
10
11
  export declare const MIGRATE_RACE_MESSAGE = "division by zero";
11
12
  export declare const CREATE_RACE_MESSAGE = "already exists";
13
+ export declare const SINGLE_QUOTE_REGEX: RegExp;
12
14
  export declare const JOB_STATES: Readonly<{
13
15
  created: "created";
14
16
  retry: "retry";
@@ -25,6 +27,7 @@ export declare const QUEUE_POLICIES: Readonly<{
25
27
  exclusive: "exclusive";
26
28
  key_strict_fifo: "key_strict_fifo";
27
29
  }>;
30
+ export declare const COMMON_JOB_TABLE = "job_common";
28
31
  interface CreateOptions {
29
32
  createSchema?: boolean;
30
33
  noTablePartitioning?: boolean;
@@ -37,6 +40,7 @@ export declare function createTableWarning(schema: string): string;
37
40
  export declare function createIndexWarning(schema: string): string;
38
41
  export declare function createTableJobDependency(schema: string): string;
39
42
  export declare function createIndexJobDependencyParent(schema: string): string;
43
+ export declare function jobTableFormatFunction(schema: string): string;
40
44
  export declare function createQueue(schema: string, name: string, options: unknown, noAdvisoryLocks?: boolean): string;
41
45
  export declare function notifyChannelSql(schema: string): string;
42
46
  export declare function notifyQueue(schema: string, name: string): string;
@@ -178,10 +182,28 @@ export declare function getDependencies(schema: string): string;
178
182
  export declare function getDependents(schema: string): string;
179
183
  export declare function cleanupDependencies(schema: string, table: string, queues: string[], noAdvisoryLocks?: boolean): string;
180
184
  export declare function getBlockedKeys(schema: string, table: string): string;
181
- export declare function getNextBamCommand(schema: string): string;
185
+ export declare function getNextBamCommand(schema: string, { useLiveness }?: {
186
+ useLiveness?: boolean;
187
+ }): string;
188
+ export declare function bamHealDrop(schema: string, command: string): string | null;
189
+ export declare function bamHealProbe(schema: string, command: string): string | null;
182
190
  export declare function setBamCompleted(schema: string, id: string): string;
183
191
  export declare function setBamFailed(schema: string, id: string, error: string): string;
184
192
  export declare function getBamStatus(schema: string): string;
185
193
  export declare function getBamEntries(schema: string): string;
194
+ export declare function jobCommonExists(schema: string): string;
195
+ export declare function getManagedQueuePartitions(schema: string): string;
196
+ export declare function getIncompleteBamCommands(schema: string): string;
197
+ export declare function bamCommandIndexName(command: string): string | null;
198
+ interface QueuePartition {
199
+ table: string;
200
+ policy?: string | null;
201
+ }
202
+ export declare const EXPECTED_JOB_STATES: readonly string[];
203
+ export declare function expectedManagedTables(schema: string, partitioned: boolean, partitions?: QueuePartition[]): string[];
204
+ export declare function expectedManagedColumns(schema: string, partitioned: boolean, partitions?: QueuePartition[]): ExpectedColumns[];
205
+ export declare function expectedManagedConstraints(schema: string, partitioned: boolean): ExpectedConstraints[];
206
+ export declare function expectedManagedFunctions(schema: string, partitioned: boolean): ManagedFunction[];
207
+ export declare function expectedManagedIndexes(schema: string, partitioned: boolean, partitions?: QueuePartition[]): ManagedIndex[];
186
208
  export {};
187
209
  //# sourceMappingURL=plans.d.ts.map