@relaybus/postgres 0.4.0 → 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.
@@ -250,8 +252,8 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
250
252
  * Creates the schema, tables and indexes if they do not exist. Safe to call
251
253
  * from many processes at once: a transaction-scoped advisory lock serializes
252
254
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
253
- * When the lane table already carries the marker of the current statements
254
- * ({@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
255
257
  * that start next to running dispatchers cannot deadlock with their claims.
256
258
  * Skip it (`setupStore: false`) when a migration tool applies
257
259
  * `createPostgresSchemaSql()` instead.
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.
@@ -250,8 +252,8 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
250
252
  * Creates the schema, tables and indexes if they do not exist. Safe to call
251
253
  * from many processes at once: a transaction-scoped advisory lock serializes
252
254
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
253
- * When the lane table already carries the marker of the current statements
254
- * ({@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
255
257
  * that start next to running dispatchers cannot deadlock with their claims.
256
258
  * Skip it (`setupStore: false`) when a migration tool applies
257
259
  * `createPostgresSchemaSql()` instead.
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
  `;
@@ -1444,6 +1480,67 @@ var PostgresMaintenance = class {
1444
1480
  });
1445
1481
  }
1446
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
+ }
1447
1544
  var WAKE_UPS = ["notify", "poll"];
1448
1545
  var DEFAULT_PURGE_BATCH_SIZE = 1e3;
1449
1546
  function resolveStoreSettings(options) {
@@ -1518,8 +1615,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1518
1615
  reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
1519
1616
  + row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
1520
1617
  END AS sequence
1521
- FROM jsonb_to_recordset($1::jsonb) AS incoming (
1522
- 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,
1523
1620
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1524
1621
  LEFT JOIN reserved ON reserved.key = incoming.key
1525
1622
  )`;
@@ -1530,7 +1627,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1530
1627
  INSERT INTO ${this.#names.events}
1531
1628
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1532
1629
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1533
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1630
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1534
1631
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1535
1632
  COALESCE(incoming.handlers, '{}')
1536
1633
  FROM incoming
@@ -1583,11 +1680,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1583
1680
  return `INSERT INTO ${this.#names.events}
1584
1681
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1585
1682
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1586
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1683
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1587
1684
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1588
1685
  COALESCE(incoming.handlers, '{}')
1589
- FROM jsonb_to_recordset($1::jsonb) AS incoming (
1590
- 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,
1591
1688
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1592
1689
  ORDER BY incoming.id
1593
1690
  ON CONFLICT (id) DO NOTHING
@@ -2837,8 +2934,10 @@ var SetupStatements = class {
2837
2934
  return `relaybus:${this.#names.schema}`;
2838
2935
  }
2839
2936
  /**
2840
- * Whether the lane table carries `marker` as its comment, that is whether the schema statements
2841
- * 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).
2842
2941
  */
2843
2942
  get schemaCurrent() {
2844
2943
  return `SELECT EXISTS (
@@ -2847,7 +2946,14 @@ var SetupStatements = class {
2847
2946
  JOIN pg_catalog.pg_description AS d
2848
2947
  ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
2849
2948
  WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
2850
- ) 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`;
2851
2957
  }
2852
2958
  /**
2853
2959
  * Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
@@ -2885,6 +2991,7 @@ var PostgresEventStore = class {
2885
2991
  #claims;
2886
2992
  #completion;
2887
2993
  #maintenance;
2994
+ #setup;
2888
2995
  #owned = new OwnedConnections();
2889
2996
  constructor(options) {
2890
2997
  const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
@@ -2898,7 +3005,7 @@ var PostgresEventStore = class {
2898
3005
  statements,
2899
3006
  schemaStatements,
2900
3007
  setupLockKey: statements.setup.setupLockKey,
2901
- schemaMarker: postgresSchemaMarker(schemaStatements.slice(0, -1)),
3008
+ schemaMarker: version040SchemaMarker(options),
2902
3009
  purgeBatchSize,
2903
3010
  completionHistory: names.completionHistory,
2904
3011
  logger,
@@ -2922,42 +3029,20 @@ var PostgresEventStore = class {
2922
3029
  this.#claims = new PostgresClaims(state);
2923
3030
  this.#completion = new PostgresCompletion(state);
2924
3031
  this.#maintenance = new PostgresMaintenance(state);
3032
+ this.#setup = new PostgresSetup(state, options);
2925
3033
  }
2926
3034
  /**
2927
3035
  * Creates the schema, tables and indexes if they do not exist. Safe to call
2928
3036
  * from many processes at once: a transaction-scoped advisory lock serializes
2929
3037
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
2930
- * When the lane table already carries the marker of the current statements
2931
- * ({@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
2932
3040
  * that start next to running dispatchers cannot deadlock with their claims.
2933
3041
  * Skip it (`setupStore: false`) when a migration tool applies
2934
3042
  * `createPostgresSchemaSql()` instead.
2935
3043
  */
2936
3044
  async setup() {
2937
- if (await this.#schemaIsCurrent()) {
2938
- return;
2939
- }
2940
- await this.#state.executor.transaction(async (transaction) => {
2941
- await this.#state.executor.query(
2942
- this.#state.statements.setup.setupLock,
2943
- [this.#state.setupLockKey],
2944
- transaction
2945
- );
2946
- if (await this.#schemaIsCurrent(transaction)) {
2947
- return;
2948
- }
2949
- for (const statement of this.#state.schemaStatements) {
2950
- await this.#state.executor.query(statement, [], transaction);
2951
- }
2952
- });
2953
- }
2954
- async #schemaIsCurrent(transaction) {
2955
- const rows = await this.#state.executor.query(
2956
- this.#state.statements.setup.schemaCurrent,
2957
- [this.#state.schemaMarker],
2958
- transaction
2959
- );
2960
- return rows[0]["current"] === true;
3045
+ await this.#setup.run();
2961
3046
  }
2962
3047
  /**
2963
3048
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)