@relaybus/postgres 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -70,6 +70,8 @@ interface PostgresNames {
70
70
  readonly schema: string;
71
71
  readonly quotedSchema: string;
72
72
  readonly events: string;
73
+ /** Unquoted, unqualified name of {@link events}, for catalog lookups. */
74
+ readonly eventsTableName: string;
73
75
  readonly deliveries: string;
74
76
  readonly keySequences: string;
75
77
  /** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
@@ -110,8 +112,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
110
112
 
111
113
  /**
112
114
  * Statements that create the schema, tables and indexes, in dependency order,
113
- * and upgrade a 0.1.0 schema in place. Every statement is idempotent
114
- * (`IF NOT EXISTS`, or guarded by a check for the 0.1.0 columns). Run them in
115
+ * and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
116
+ * Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
115
117
  * one transaction and serialize concurrent runs (see
116
118
  * {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
117
119
  * into your own migration tool.
@@ -190,7 +192,18 @@ interface PostgresConnectionOptions extends PostgresEventStoreSettings {
190
192
  /** Use another database library through an adapter. The store never closes it. */
191
193
  interface PostgresExecutorOptions<TTransaction> extends PostgresEventStoreSettings {
192
194
  readonly executor: PostgresExecutor<TTransaction>;
193
- readonly pool?: never;
195
+ /**
196
+ * Optional `pg` pool on the same database, for example the one an ORM's `pg` driver adapter uses
197
+ * (`new PrismaPg(pool)`, Kysely's `PostgresDialect`, Drizzle's `node-postgres`). The store never
198
+ * closes it. With it:
199
+ * - the statements the store sends outside a transaction (the dispatcher's claims, completions and
200
+ * lease renewals, maintenance) run on it as prepared statements (`preparedStatements`), which ORM
201
+ * raw queries cannot do; statements in a transaction still go through `executor`;
202
+ * - with `wakeUp: 'notify'` the store listens for wake-ups with its connection settings, so a
203
+ * delivery published in an ORM transaction is dispatched right after that transaction commits,
204
+ * instead of after a fixed delay or the next poll.
205
+ */
206
+ readonly pool?: Pool;
194
207
  readonly connectionString?: never;
195
208
  readonly poolConfig?: never;
196
209
  }
@@ -239,8 +252,8 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
239
252
  * Creates the schema, tables and indexes if they do not exist. Safe to call
240
253
  * from many processes at once: a transaction-scoped advisory lock serializes
241
254
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
242
- * When the lane table already carries the marker of the current statements
243
- * ({@link postgresSchemaMarker}) nothing runs and no table is locked, so instances
255
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
256
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
244
257
  * that start next to running dispatchers cannot deadlock with their claims.
245
258
  * Skip it (`setupStore: false`) when a migration tool applies
246
259
  * `createPostgresSchemaSql()` instead.
@@ -330,4 +343,5 @@ declare class ReconcileFailedError extends RelayBusError {
330
343
  readonly code: string;
331
344
  }
332
345
 
333
- export { PgExecutor, type PgExecutorOptions, type PostgresConnectionOptions, PostgresEventStore, type PostgresEventStoreOptions, type PostgresEventStoreSettings, PostgresExecutor, type PostgresExecutorOptions, type PostgresNames, PostgresParameter, type PostgresPoolOptions, PostgresQueryOptions, PostgresRow, type PostgresSchemaOptions, type PostgresWakeUp, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
346
+ export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
347
+ export type { PgExecutorOptions, PostgresConnectionOptions, PostgresEventStoreOptions, PostgresEventStoreSettings, PostgresExecutorOptions, PostgresNames, PostgresPoolOptions, PostgresSchemaOptions, PostgresWakeUp };
package/dist/index.d.ts CHANGED
@@ -70,6 +70,8 @@ interface PostgresNames {
70
70
  readonly schema: string;
71
71
  readonly quotedSchema: string;
72
72
  readonly events: string;
73
+ /** Unquoted, unqualified name of {@link events}, for catalog lookups. */
74
+ readonly eventsTableName: string;
73
75
  readonly deliveries: string;
74
76
  readonly keySequences: string;
75
77
  /** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
@@ -110,8 +112,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
110
112
 
111
113
  /**
112
114
  * Statements that create the schema, tables and indexes, in dependency order,
113
- * and upgrade a 0.1.0 schema in place. Every statement is idempotent
114
- * (`IF NOT EXISTS`, or guarded by a check for the 0.1.0 columns). Run them in
115
+ * and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
116
+ * Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
115
117
  * one transaction and serialize concurrent runs (see
116
118
  * {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
117
119
  * into your own migration tool.
@@ -190,7 +192,18 @@ interface PostgresConnectionOptions extends PostgresEventStoreSettings {
190
192
  /** Use another database library through an adapter. The store never closes it. */
191
193
  interface PostgresExecutorOptions<TTransaction> extends PostgresEventStoreSettings {
192
194
  readonly executor: PostgresExecutor<TTransaction>;
193
- readonly pool?: never;
195
+ /**
196
+ * Optional `pg` pool on the same database, for example the one an ORM's `pg` driver adapter uses
197
+ * (`new PrismaPg(pool)`, Kysely's `PostgresDialect`, Drizzle's `node-postgres`). The store never
198
+ * closes it. With it:
199
+ * - the statements the store sends outside a transaction (the dispatcher's claims, completions and
200
+ * lease renewals, maintenance) run on it as prepared statements (`preparedStatements`), which ORM
201
+ * raw queries cannot do; statements in a transaction still go through `executor`;
202
+ * - with `wakeUp: 'notify'` the store listens for wake-ups with its connection settings, so a
203
+ * delivery published in an ORM transaction is dispatched right after that transaction commits,
204
+ * instead of after a fixed delay or the next poll.
205
+ */
206
+ readonly pool?: Pool;
194
207
  readonly connectionString?: never;
195
208
  readonly poolConfig?: never;
196
209
  }
@@ -239,8 +252,8 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
239
252
  * Creates the schema, tables and indexes if they do not exist. Safe to call
240
253
  * from many processes at once: a transaction-scoped advisory lock serializes
241
254
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
242
- * When the lane table already carries the marker of the current statements
243
- * ({@link postgresSchemaMarker}) nothing runs and no table is locked, so instances
255
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
256
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
244
257
  * that start next to running dispatchers cannot deadlock with their claims.
245
258
  * Skip it (`setupStore: false`) when a migration tool applies
246
259
  * `createPostgresSchemaSql()` instead.
@@ -330,4 +343,5 @@ declare class ReconcileFailedError extends RelayBusError {
330
343
  readonly code: string;
331
344
  }
332
345
 
333
- export { PgExecutor, type PgExecutorOptions, type PostgresConnectionOptions, PostgresEventStore, type PostgresEventStoreOptions, type PostgresEventStoreSettings, PostgresExecutor, type PostgresExecutorOptions, type PostgresNames, PostgresParameter, type PostgresPoolOptions, PostgresQueryOptions, PostgresRow, type PostgresSchemaOptions, type PostgresWakeUp, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
346
+ export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
347
+ export type { PgExecutorOptions, PostgresConnectionOptions, PostgresEventStoreOptions, PostgresEventStoreSettings, PostgresExecutorOptions, PostgresNames, PostgresPoolOptions, PostgresSchemaOptions, PostgresWakeUp };
package/dist/index.js CHANGED
@@ -228,6 +228,7 @@ function resolvePostgresNames(options = {}) {
228
228
  schema,
229
229
  quotedSchema,
230
230
  events: table("events"),
231
+ eventsTableName: name("events"),
231
232
  deliveries: table("deliveries"),
232
233
  keySequences: table("key_sequences"),
233
234
  lanes: table("lanes"),
@@ -254,8 +255,17 @@ function laneExpression(keyExpression, eventIdExpression) {
254
255
 
255
256
  // src/schema.ts
256
257
  var SCHEMA_MARKER_DIGITS = 32;
258
+ var PAYLOAD_COLUMNS = ` payload json NOT NULL,
259
+ headers json NOT NULL DEFAULT '{}'::json,`;
260
+ var JSONB_PAYLOAD_COLUMNS = ` payload jsonb NOT NULL,
261
+ headers jsonb NOT NULL DEFAULT '{}'::jsonb,`;
257
262
  function createPostgresSchemaStatements(options = {}) {
258
263
  const names = resolvePostgresNames(options);
264
+ const statements = schemaBodyStatements(names);
265
+ statements.push(`COMMENT ON TABLE ${names.lanes} IS '${version040Marker(names, statements)}'`);
266
+ return statements;
267
+ }
268
+ function schemaBodyStatements(names) {
259
269
  const statements = [...eventAndDeliveryStatements(names), ...laneAndCompletionStatements(names)];
260
270
  if (names.completionHistory) {
261
271
  statements.push(...completionHistoryStatements(names));
@@ -263,7 +273,6 @@ function createPostgresSchemaStatements(options = {}) {
263
273
  if (names.schema !== "public") {
264
274
  statements.unshift(`CREATE SCHEMA IF NOT EXISTS ${names.quotedSchema}`);
265
275
  }
266
- statements.push(`COMMENT ON TABLE ${names.lanes} IS '${postgresSchemaMarker(statements)}'`);
267
276
  return statements;
268
277
  }
269
278
  function eventAndDeliveryStatements(names) {
@@ -277,14 +286,14 @@ function eventAndKeySequenceStatements(names) {
277
286
  source text NOT NULL,
278
287
  key text,
279
288
  sequence bigint,
280
- payload jsonb NOT NULL,
281
- headers jsonb NOT NULL DEFAULT '{}'::jsonb,
289
+ ${PAYLOAD_COLUMNS}
282
290
  correlation_id text,
283
291
  causation_id text,
284
292
  occurred_at timestamptz NOT NULL,
285
293
  created_at timestamptz NOT NULL,
286
294
  handlers text[] NOT NULL DEFAULT '{}'
287
295
  )`,
296
+ upgradePayloadToJson(names),
288
297
  `CREATE INDEX IF NOT EXISTS ${names.indexes.eventCreated} ON ${names.events} (created_at, id)`,
289
298
  `CREATE TABLE IF NOT EXISTS ${names.keySequences} (
290
299
  key text PRIMARY KEY,
@@ -380,6 +389,33 @@ BEGIN
380
389
  END
381
390
  $relaybus_upgrade$`;
382
391
  }
392
+ function upgradePayloadToJson(names) {
393
+ return `DO $relaybus_payload_json$
394
+ BEGIN
395
+ IF EXISTS (
396
+ SELECT 1 FROM pg_attribute
397
+ WHERE attrelid = to_regclass('${names.events}') AND attname IN ('payload', 'headers')
398
+ AND atttypid = 'jsonb'::regtype AND NOT attisdropped
399
+ ) THEN
400
+ ALTER TABLE ${names.events}
401
+ ALTER COLUMN headers DROP DEFAULT,
402
+ ALTER COLUMN payload TYPE json USING payload::json,
403
+ ALTER COLUMN headers TYPE json USING headers::json,
404
+ ALTER COLUMN headers SET DEFAULT '{}'::json;
405
+ END IF;
406
+ END
407
+ $relaybus_payload_json$`;
408
+ }
409
+ function version040SchemaMarker(options = {}) {
410
+ const names = resolvePostgresNames(options);
411
+ return version040Marker(names, schemaBodyStatements(names));
412
+ }
413
+ function version040Marker(names, statements) {
414
+ const conversion = upgradePayloadToJson(names);
415
+ return postgresSchemaMarker(
416
+ statements.filter((statement) => statement !== conversion).map((statement) => statement.replace(PAYLOAD_COLUMNS, JSONB_PAYLOAD_COLUMNS))
417
+ );
418
+ }
383
419
  function createPostgresSchemaSql(options = {}) {
384
420
  return `${createPostgresSchemaStatements(options).join(";\n\n")};
385
421
  `;
@@ -491,6 +527,19 @@ var NotificationListener = class {
491
527
  await drop?.();
492
528
  }
493
529
  };
530
+
531
+ // src/root-pool-executor.ts
532
+ function withRootPool(executor, root) {
533
+ return {
534
+ ...executor.sessionTimeouts === void 0 ? {} : { sessionTimeouts: executor.sessionTimeouts },
535
+ transaction: (work) => executor.transaction(work),
536
+ ...executor.transactionWithBudget === void 0 ? {} : {
537
+ transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
538
+ },
539
+ // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
540
+ query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
541
+ };
542
+ }
494
543
  function text(row, column) {
495
544
  return row[column];
496
545
  }
@@ -1431,10 +1480,71 @@ var PostgresMaintenance = class {
1431
1480
  });
1432
1481
  }
1433
1482
  };
1483
+
1484
+ // src/postgres-setup.ts
1485
+ var PostgresSetup = class {
1486
+ #state;
1487
+ /** The conversion of `jsonb` payload columns. */
1488
+ #payloadUpgrade;
1489
+ constructor(state, options) {
1490
+ this.#state = state;
1491
+ this.#payloadUpgrade = upgradePayloadToJson(resolvePostgresNames(options));
1492
+ }
1493
+ /** Implements {@link PostgresEventStore.setup}. */
1494
+ async run() {
1495
+ if (isCurrent(await this.#schemaState())) {
1496
+ return;
1497
+ }
1498
+ await this.#underSetupLock(async (transaction) => {
1499
+ await this.#run(this.#payloadUpgrade, transaction);
1500
+ });
1501
+ await this.#underSetupLock(async (transaction, schema) => {
1502
+ if (schema.marked) {
1503
+ await this.#run(this.#payloadUpgrade, transaction);
1504
+ return;
1505
+ }
1506
+ for (const statement of this.#state.schemaStatements) {
1507
+ await this.#run(statement, transaction);
1508
+ }
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Runs `work` in a transaction under the setup lock, unless the schema became current. The
1513
+ * payload conversion goes first in a transaction of its own, so it holds no other table lock
1514
+ * while it waits for the event table (a completion locks deliveries before the event rows).
1515
+ */
1516
+ async #underSetupLock(work) {
1517
+ await this.#state.executor.transaction(async (transaction) => {
1518
+ await this.#state.executor.query(
1519
+ this.#state.statements.setup.setupLock,
1520
+ [this.#state.setupLockKey],
1521
+ transaction
1522
+ );
1523
+ const schema = await this.#schemaState(transaction);
1524
+ if (!isCurrent(schema)) {
1525
+ await work(transaction, schema);
1526
+ }
1527
+ });
1528
+ }
1529
+ async #run(statement, transaction) {
1530
+ await this.#state.executor.query(statement, [], transaction);
1531
+ }
1532
+ async #schemaState(transaction) {
1533
+ const rows = await this.#state.executor.query(
1534
+ this.#state.statements.setup.schemaCurrent,
1535
+ [this.#state.schemaMarker],
1536
+ transaction
1537
+ );
1538
+ return { marked: rows[0]["marked"] === true, payloadJson: rows[0]["payload_json"] === true };
1539
+ }
1540
+ };
1541
+ function isCurrent(schema) {
1542
+ return schema.marked && schema.payloadJson;
1543
+ }
1434
1544
  var WAKE_UPS = ["notify", "poll"];
1435
1545
  var DEFAULT_PURGE_BATCH_SIZE = 1e3;
1436
1546
  function resolveStoreSettings(options) {
1437
- const sources = [options.pool, options.connectionString, options.executor].filter(
1547
+ const sources = [options.executor ?? options.pool, options.connectionString].filter(
1438
1548
  (source) => source !== void 0
1439
1549
  );
1440
1550
  if (sources.length !== 1) {
@@ -1505,8 +1615,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1505
1615
  reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
1506
1616
  + row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
1507
1617
  END AS sequence
1508
- FROM jsonb_to_recordset($1::jsonb) AS incoming (
1509
- ordinal int, id text, type text, source text, key text, payload jsonb, headers jsonb,
1618
+ FROM json_to_recordset($1::json) AS incoming (
1619
+ ordinal int, id text, type text, source text, key text, payload json, headers json,
1510
1620
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1511
1621
  LEFT JOIN reserved ON reserved.key = incoming.key
1512
1622
  )`;
@@ -1517,7 +1627,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1517
1627
  INSERT INTO ${this.#names.events}
1518
1628
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1519
1629
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1520
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1630
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1521
1631
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1522
1632
  COALESCE(incoming.handlers, '{}')
1523
1633
  FROM incoming
@@ -1540,9 +1650,13 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1540
1650
  RETURNING available_at
1541
1651
  )`;
1542
1652
  }
1543
- /** One wake-up notification on {@link PostgresNames.notificationChannel}, outside any publishing transaction. */
1653
+ /**
1654
+ * One wake-up notification on {@link PostgresNames.notificationChannel}, outside any publishing
1655
+ * transaction. Returns a boolean column rather than the `void` of `pg_notify`, which some libraries
1656
+ * cannot read (Prisma's raw queries reject a `void` column).
1657
+ */
1544
1658
  get notify() {
1545
- return `SELECT pg_notify('${this.#names.notificationChannel}', '')`;
1659
+ return `SELECT true AS notified FROM pg_notify('${this.#names.notificationChannel}', '')`;
1546
1660
  }
1547
1661
  /**
1548
1662
  * A CTE `woken` that sends one notification on {@link PostgresNames.notificationChannel} when a row of
@@ -1566,11 +1680,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1566
1680
  return `INSERT INTO ${this.#names.events}
1567
1681
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1568
1682
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1569
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1683
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1570
1684
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1571
1685
  COALESCE(incoming.handlers, '{}')
1572
- FROM jsonb_to_recordset($1::jsonb) AS incoming (
1573
- id text, type text, source text, key text, sequence bigint, payload jsonb, headers jsonb,
1686
+ FROM json_to_recordset($1::json) AS incoming (
1687
+ id text, type text, source text, key text, sequence bigint, payload json, headers json,
1574
1688
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1575
1689
  ORDER BY incoming.id
1576
1690
  ON CONFLICT (id) DO NOTHING
@@ -2820,8 +2934,10 @@ var SetupStatements = class {
2820
2934
  return `relaybus:${this.#names.schema}`;
2821
2935
  }
2822
2936
  /**
2823
- * Whether the lane table carries `marker` as its comment, that is whether the schema statements
2824
- * that produce that marker already ran. Takes no table lock. Parameters: `[marker]`.
2937
+ * The state of the schema, taking no table lock. Parameters: `[marker]`. Columns: `marked`, whether
2938
+ * the lane table carries `marker` as its comment (the schema statements that produce it already
2939
+ * ran), and `payload_json`, whether the payload and headers columns of the event table are both
2940
+ * `json` (the conversion of the `jsonb` columns of schemas before 0.4.1 ran).
2825
2941
  */
2826
2942
  get schemaCurrent() {
2827
2943
  return `SELECT EXISTS (
@@ -2830,10 +2946,21 @@ var SetupStatements = class {
2830
2946
  JOIN pg_catalog.pg_description AS d
2831
2947
  ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
2832
2948
  WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
2833
- ) AS current`;
2949
+ ) AS marked, (
2950
+ SELECT count(*) FROM pg_catalog.pg_attribute AS a
2951
+ JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
2952
+ JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
2953
+ WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.eventsTableName}'
2954
+ AND a.attname IN ('payload', 'headers') AND a.atttypid = 'pg_catalog.json'::pg_catalog.regtype
2955
+ AND NOT a.attisdropped
2956
+ ) = 2 AS payload_json`;
2834
2957
  }
2958
+ /**
2959
+ * Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
2960
+ * some libraries cannot read (Prisma's raw queries reject a `void` column).
2961
+ */
2835
2962
  get setupLock() {
2836
- return "SELECT pg_advisory_xact_lock(hashtextextended($1::text, 0))";
2963
+ return "SELECT true AS locked FROM pg_advisory_xact_lock(hashtextextended($1::text, 0))";
2837
2964
  }
2838
2965
  };
2839
2966
 
@@ -2864,6 +2991,7 @@ var PostgresEventStore = class {
2864
2991
  #claims;
2865
2992
  #completion;
2866
2993
  #maintenance;
2994
+ #setup;
2867
2995
  #owned = new OwnedConnections();
2868
2996
  constructor(options) {
2869
2997
  const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
@@ -2877,7 +3005,7 @@ var PostgresEventStore = class {
2877
3005
  statements,
2878
3006
  schemaStatements,
2879
3007
  setupLockKey: statements.setup.setupLockKey,
2880
- schemaMarker: postgresSchemaMarker(schemaStatements.slice(0, -1)),
3008
+ schemaMarker: version040SchemaMarker(options),
2881
3009
  purgeBatchSize,
2882
3010
  completionHistory: names.completionHistory,
2883
3011
  logger,
@@ -2901,42 +3029,20 @@ var PostgresEventStore = class {
2901
3029
  this.#claims = new PostgresClaims(state);
2902
3030
  this.#completion = new PostgresCompletion(state);
2903
3031
  this.#maintenance = new PostgresMaintenance(state);
3032
+ this.#setup = new PostgresSetup(state, options);
2904
3033
  }
2905
3034
  /**
2906
3035
  * Creates the schema, tables and indexes if they do not exist. Safe to call
2907
3036
  * from many processes at once: a transaction-scoped advisory lock serializes
2908
3037
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
2909
- * When the lane table already carries the marker of the current statements
2910
- * ({@link postgresSchemaMarker}) nothing runs and no table is locked, so instances
3038
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
3039
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
2911
3040
  * that start next to running dispatchers cannot deadlock with their claims.
2912
3041
  * Skip it (`setupStore: false`) when a migration tool applies
2913
3042
  * `createPostgresSchemaSql()` instead.
2914
3043
  */
2915
3044
  async setup() {
2916
- if (await this.#schemaIsCurrent()) {
2917
- return;
2918
- }
2919
- await this.#state.executor.transaction(async (transaction) => {
2920
- await this.#state.executor.query(
2921
- this.#state.statements.setup.setupLock,
2922
- [this.#state.setupLockKey],
2923
- transaction
2924
- );
2925
- if (await this.#schemaIsCurrent(transaction)) {
2926
- return;
2927
- }
2928
- for (const statement of this.#state.schemaStatements) {
2929
- await this.#state.executor.query(statement, [], transaction);
2930
- }
2931
- });
2932
- }
2933
- async #schemaIsCurrent(transaction) {
2934
- const rows = await this.#state.executor.query(
2935
- this.#state.statements.setup.schemaCurrent,
2936
- [this.#state.schemaMarker],
2937
- transaction
2938
- );
2939
- return rows[0]["current"] === true;
3045
+ await this.#setup.run();
2940
3046
  }
2941
3047
  /**
2942
3048
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
@@ -3102,7 +3208,12 @@ var PostgresEventStore = class {
3102
3208
  };
3103
3209
  function connect(options, { logger, owned }) {
3104
3210
  if (options.executor !== void 0) {
3105
- return { executor: options.executor, pool: void 0, ownedPool: void 0 };
3211
+ const { executor: executor2, pool: pool2 } = options;
3212
+ return {
3213
+ executor: pool2 === void 0 ? executor2 : withRootPool(executor2, new PgExecutor(pool2, { logger })),
3214
+ pool: pool2,
3215
+ ownedPool: void 0
3216
+ };
3106
3217
  }
3107
3218
  const pool = options.pool ?? new Pool({
3108
3219
  ...options.poolConfig,