@relaybus/postgres 0.4.0 → 0.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.
Files changed (48) hide show
  1. package/README.md +28 -15
  2. package/dist/chunk-5WWPAN5Q.js +26 -0
  3. package/dist/chunk-5WWPAN5Q.js.map +1 -0
  4. package/dist/chunk-FY65AUUF.js +40 -0
  5. package/dist/chunk-FY65AUUF.js.map +1 -0
  6. package/dist/chunk-UDBN4RRV.js +35 -0
  7. package/dist/chunk-UDBN4RRV.js.map +1 -0
  8. package/dist/drizzle.cjs +24 -0
  9. package/dist/drizzle.cjs.map +1 -1
  10. package/dist/drizzle.d.cts +1 -0
  11. package/dist/drizzle.d.ts +1 -0
  12. package/dist/drizzle.js +2 -1
  13. package/dist/drizzle.js.map +1 -1
  14. package/dist/{executor-CfJgktC6.d.cts → executor-Bt90w33Q.d.cts} +24 -1
  15. package/dist/{executor-CfJgktC6.d.ts → executor-Bt90w33Q.d.ts} +24 -1
  16. package/dist/index.cjs +226 -43
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +25 -7
  19. package/dist/index.d.ts +25 -7
  20. package/dist/index.js +194 -79
  21. package/dist/index.js.map +1 -1
  22. package/dist/knex.cjs +24 -0
  23. package/dist/knex.cjs.map +1 -1
  24. package/dist/knex.d.cts +1 -0
  25. package/dist/knex.d.ts +1 -0
  26. package/dist/knex.js +2 -1
  27. package/dist/knex.js.map +1 -1
  28. package/dist/kysely.cjs +24 -0
  29. package/dist/kysely.cjs.map +1 -1
  30. package/dist/kysely.d.cts +1 -0
  31. package/dist/kysely.d.ts +1 -0
  32. package/dist/kysely.js +2 -1
  33. package/dist/kysely.js.map +1 -1
  34. package/dist/prisma.cjs +24 -0
  35. package/dist/prisma.cjs.map +1 -1
  36. package/dist/prisma.d.cts +2 -1
  37. package/dist/prisma.d.ts +2 -1
  38. package/dist/prisma.js +2 -1
  39. package/dist/prisma.js.map +1 -1
  40. package/dist/typeorm.cjs +46 -0
  41. package/dist/typeorm.cjs.map +1 -1
  42. package/dist/typeorm.d.cts +1 -0
  43. package/dist/typeorm.d.ts +1 -0
  44. package/dist/typeorm.js +11 -1
  45. package/dist/typeorm.js.map +1 -1
  46. package/package.json +6 -5
  47. package/dist/chunk-NNZJR5AB.js +0 -12
  48. package/dist/chunk-NNZJR5AB.js.map +0 -1
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PostgresExecutor, a as PostgresParameter, b as PostgresQueryOptions, c as PostgresRow } from './executor-CfJgktC6.cjs';
2
- import { RelayBusLogger, EventStore, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
1
+ import { P as PostgresExecutor, a as PostgresIsolatedTransactionOptions, b as PostgresParameter, c as PostgresQueryOptions, d as PostgresRow } from './executor-Bt90w33Q.cjs';
2
+ import { RelayBusLogger, EventStore, TransactionIsolationLevel, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
3
3
  import { PoolClient, Pool, PoolConfig } from 'pg';
4
4
 
5
5
  interface PgExecutorOptions {
@@ -43,6 +43,11 @@ declare class PgExecutor implements PostgresExecutor<PoolClient> {
43
43
  * connection that already keeps this budget only gets `BEGIN`.
44
44
  */
45
45
  transactionWithBudget<T>(work: (transaction: PoolClient) => Promise<T>, timeoutMs: number): Promise<T>;
46
+ /**
47
+ * Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
48
+ * with the `BEGIN` of the same simple query, before the budget's first statement.
49
+ */
50
+ transactionWithIsolation<T>(work: (transaction: PoolClient) => Promise<T>, { isolationLevel, timeoutMs }: PostgresIsolatedTransactionOptions): Promise<T>;
46
51
  query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
47
52
  }
48
53
 
@@ -70,6 +75,8 @@ interface PostgresNames {
70
75
  readonly schema: string;
71
76
  readonly quotedSchema: string;
72
77
  readonly events: string;
78
+ /** Unquoted, unqualified name of {@link events}, for catalog lookups. */
79
+ readonly eventsTableName: string;
73
80
  readonly deliveries: string;
74
81
  readonly keySequences: string;
75
82
  /** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
@@ -110,8 +117,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
110
117
 
111
118
  /**
112
119
  * 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
120
+ * and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
121
+ * Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
115
122
  * one transaction and serialize concurrent runs (see
116
123
  * {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
117
124
  * into your own migration tool.
@@ -250,13 +257,21 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
250
257
  * Creates the schema, tables and indexes if they do not exist. Safe to call
251
258
  * from many processes at once: a transaction-scoped advisory lock serializes
252
259
  * 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
260
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
261
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
255
262
  * that start next to running dispatchers cannot deadlock with their claims.
256
263
  * Skip it (`setupStore: false`) when a migration tool applies
257
264
  * `createPostgresSchemaSql()` instead.
258
265
  */
259
266
  setup(): Promise<void>;
267
+ /** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
268
+ readonly isolationLevels: readonly TransactionIsolationLevel[];
269
+ /**
270
+ * True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
271
+ * Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
272
+ * test with {@link PostgresExecutor.isSerializationFailure}.
273
+ */
274
+ isSerializationFailure(error: unknown): boolean;
260
275
  /**
261
276
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
262
277
  * the server holds the transaction to the budget: `statement_timeout`, and for executors with
@@ -264,6 +279,9 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
264
279
  * `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
265
280
  * (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
266
281
  * created they are kept per connection and sent only when the connection does not have them yet.
282
+ * With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
283
+ * `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
284
+ * ISOLATION LEVEL ...` before the budget statement.
267
285
  */
268
286
  transaction<T>(work: (transaction: TTransaction) => Promise<T>, options?: EventStoreTransactionOptions): Promise<T>;
269
287
  /**
@@ -341,5 +359,5 @@ declare class ReconcileFailedError extends RelayBusError {
341
359
  readonly code: string;
342
360
  }
343
361
 
344
- export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
362
+ export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresIsolatedTransactionOptions, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
345
363
  export type { PgExecutorOptions, PostgresConnectionOptions, PostgresEventStoreOptions, PostgresEventStoreSettings, PostgresExecutorOptions, PostgresNames, PostgresPoolOptions, PostgresSchemaOptions, PostgresWakeUp };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { P as PostgresExecutor, a as PostgresParameter, b as PostgresQueryOptions, c as PostgresRow } from './executor-CfJgktC6.js';
2
- import { RelayBusLogger, EventStore, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
1
+ import { P as PostgresExecutor, a as PostgresIsolatedTransactionOptions, b as PostgresParameter, c as PostgresQueryOptions, d as PostgresRow } from './executor-Bt90w33Q.js';
2
+ import { RelayBusLogger, EventStore, TransactionIsolationLevel, EventStoreTransactionOptions, AppendEntry, AppendResult, ClaimRequest, ClaimedDelivery, LeaseReference, ErrorInfo, DeliveryQuery, DeliveryRecord, PurgeRequest, PurgeResult, EventStoreStatistics, CloseOptions, RelayBusError } from '@relaybus/core';
3
3
  import { PoolClient, Pool, PoolConfig } from 'pg';
4
4
 
5
5
  interface PgExecutorOptions {
@@ -43,6 +43,11 @@ declare class PgExecutor implements PostgresExecutor<PoolClient> {
43
43
  * connection that already keeps this budget only gets `BEGIN`.
44
44
  */
45
45
  transactionWithBudget<T>(work: (transaction: PoolClient) => Promise<T>, timeoutMs: number): Promise<T>;
46
+ /**
47
+ * Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
48
+ * with the `BEGIN` of the same simple query, before the budget's first statement.
49
+ */
50
+ transactionWithIsolation<T>(work: (transaction: PoolClient) => Promise<T>, { isolationLevel, timeoutMs }: PostgresIsolatedTransactionOptions): Promise<T>;
46
51
  query(sql: string, parameters: readonly PostgresParameter[], transaction?: PoolClient, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
47
52
  }
48
53
 
@@ -70,6 +75,8 @@ interface PostgresNames {
70
75
  readonly schema: string;
71
76
  readonly quotedSchema: string;
72
77
  readonly events: string;
78
+ /** Unquoted, unqualified name of {@link events}, for catalog lookups. */
79
+ readonly eventsTableName: string;
73
80
  readonly deliveries: string;
74
81
  readonly keySequences: string;
75
82
  /** Lane lease rows: one per lane, see {@link LANE_COUNT}. */
@@ -110,8 +117,8 @@ declare function resolvePostgresNames(options?: PostgresSchemaOptions): Postgres
110
117
 
111
118
  /**
112
119
  * 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
120
+ * and upgrade a 0.1.0 schema and the `jsonb` payload columns of schemas before 0.4.1 in place.
121
+ * Every statement is idempotent (`IF NOT EXISTS`, or guarded by a check for the old columns). Run them in
115
122
  * one transaction and serialize concurrent runs (see
116
123
  * {@link PostgresEventStore.setup}), or paste {@link createPostgresSchemaSql}
117
124
  * into your own migration tool.
@@ -250,13 +257,21 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
250
257
  * Creates the schema, tables and indexes if they do not exist. Safe to call
251
258
  * from many processes at once: a transaction-scoped advisory lock serializes
252
259
  * 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
260
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
261
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
255
262
  * that start next to running dispatchers cannot deadlock with their claims.
256
263
  * Skip it (`setupStore: false`) when a migration tool applies
257
264
  * `createPostgresSchemaSql()` instead.
258
265
  */
259
266
  setup(): Promise<void>;
267
+ /** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
268
+ readonly isolationLevels: readonly TransactionIsolationLevel[];
269
+ /**
270
+ * True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
271
+ * Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
272
+ * test with {@link PostgresExecutor.isSerializationFailure}.
273
+ */
274
+ isSerializationFailure(error: unknown): boolean;
260
275
  /**
261
276
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
262
277
  * the server holds the transaction to the budget: `statement_timeout`, and for executors with
@@ -264,6 +279,9 @@ declare class PostgresEventStore<TTransaction = PoolClient> implements EventStor
264
279
  * `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
265
280
  * (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
266
281
  * created they are kept per connection and sent only when the connection does not have them yet.
282
+ * With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
283
+ * `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
284
+ * ISOLATION LEVEL ...` before the budget statement.
267
285
  */
268
286
  transaction<T>(work: (transaction: TTransaction) => Promise<T>, options?: EventStoreTransactionOptions): Promise<T>;
269
287
  /**
@@ -341,5 +359,5 @@ declare class ReconcileFailedError extends RelayBusError {
341
359
  readonly code: string;
342
360
  }
343
361
 
344
- export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
362
+ export { PgExecutor, PostgresEventStore, PostgresExecutor, PostgresIsolatedTransactionOptions, PostgresParameter, PostgresQueryOptions, PostgresRow, ReconcileFailedError, createPostgresSchemaSql, createPostgresSchemaStatements, resolvePostgresNames };
345
363
  export type { PgExecutorOptions, PostgresConnectionOptions, PostgresEventStoreOptions, PostgresEventStoreSettings, PostgresExecutorOptions, PostgresNames, PostgresPoolOptions, PostgresSchemaOptions, PostgresWakeUp };
package/dist/index.js CHANGED
@@ -1,43 +1,10 @@
1
+ import { isolationKeywords, inIsolatedTransaction } from './chunk-FY65AUUF.js';
2
+ import { SESSION_DEFAULTS_SQL, budgetValue, inBudgetedTransaction, budgetSettingsSql } from './chunk-UDBN4RRV.js';
1
3
  import { createHash } from 'crypto';
2
- import { ConfigurationError, RelayBusError, systemClock } from '@relaybus/core';
4
+ import { ConfigurationError, RelayBusError, TRANSACTION_ISOLATION_LEVELS, systemClock } from '@relaybus/core';
3
5
  import { LaneLeaseTokenFormat, HandedOutLedger, PositiveInteger, schedule, OwnerSerializedClaims, LeaseSettlement, CompositeKey, DeliveryLeaseIdentity } from '@relaybus/core/internal';
4
6
  import { Client, Pool } from 'pg';
5
7
 
6
- // src/pg-executor.ts
7
-
8
- // src/transaction-timeouts.ts
9
- function budgetSettingsSql({ value, sessionTimeouts, scope }) {
10
- const local = scope === "transaction";
11
- const statementTimeout = `SELECT set_config('statement_timeout', ${value}, ${local})`;
12
- if (!sessionTimeouts) {
13
- return statementTimeout;
14
- }
15
- return `${statementTimeout}, set_config('idle_in_transaction_session_timeout', ${value}, ${local}), CASE WHEN current_setting('server_version_num')::int >= 170000 THEN set_config('transaction_timeout', ${value}, ${local}) END`;
16
- }
17
- var SESSION_DEFAULTS_SQL = "SELECT set_config(name, reset_val, false) FROM pg_settings WHERE name IN ('statement_timeout', 'idle_in_transaction_session_timeout', 'transaction_timeout')";
18
- function budgetValue(timeoutMs) {
19
- return `${Math.ceil(timeoutMs)}ms`;
20
- }
21
- function inBudgetedTransaction(state, {
22
- work,
23
- timeoutMs
24
- }) {
25
- const { executor } = state;
26
- if (executor.transactionWithBudget !== void 0) {
27
- return executor.transactionWithBudget(work, timeoutMs);
28
- }
29
- const sql = budgetSettingsSql({
30
- value: "$1",
31
- sessionTimeouts: executor.sessionTimeouts === true,
32
- scope: "transaction"
33
- });
34
- return executor.transaction(async (transaction) => {
35
- await state.query({ sql, parameters: [budgetValue(timeoutMs)], transaction });
36
- return work(transaction);
37
- });
38
- }
39
-
40
- // src/pg-executor.ts
41
8
  var FOREIGN_SESSION_ERRORS = /* @__PURE__ */ new Set(["26000", "42P05"]);
42
9
  var STATEMENT_NAME_DIGITS = 24;
43
10
  var PgExecutor = class {
@@ -67,14 +34,28 @@ var PgExecutor = class {
67
34
  * connection that already keeps this budget only gets `BEGIN`.
68
35
  */
69
36
  transactionWithBudget(work, timeoutMs) {
37
+ return this.#transaction(work, this.#budgetStart(timeoutMs, "BEGIN"));
38
+ }
39
+ /**
40
+ * Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
41
+ * with the `BEGIN` of the same simple query, before the budget's first statement.
42
+ */
43
+ transactionWithIsolation(work, { isolationLevel, timeoutMs }) {
44
+ return this.#transaction(
45
+ work,
46
+ this.#budgetStart(timeoutMs, `BEGIN ISOLATION LEVEL ${isolationKeywords(isolationLevel)}`)
47
+ );
48
+ }
49
+ /** How a transaction starts on a connection when it is held to `timeoutMs`; `begin` is the `BEGIN` statement. */
50
+ #budgetStart(timeoutMs, begin) {
70
51
  const value = budgetValue(timeoutMs);
71
52
  const settings = (scope) => budgetSettingsSql({ value: `'${value}'`, sessionTimeouts: true, scope });
72
- return this.#transaction(work, (client) => {
53
+ return (client) => {
73
54
  if (!this.#keepsSessionBudget()) {
74
- return { begin: `BEGIN; ${settings("transaction")}` };
55
+ return { begin: `${begin}; ${settings("transaction")}` };
75
56
  }
76
- return this.#sessionValues.get(client) === value ? { begin: "BEGIN" } : { begin: `BEGIN; ${settings("session")}`, sessionValue: value };
77
- });
57
+ return this.#sessionValues.get(client) === value ? { begin } : { begin: `${begin}; ${settings("session")}`, sessionValue: value };
58
+ };
78
59
  }
79
60
  #recordSession(client, sessionValue) {
80
61
  if (sessionValue === null) {
@@ -228,6 +209,7 @@ function resolvePostgresNames(options = {}) {
228
209
  schema,
229
210
  quotedSchema,
230
211
  events: table("events"),
212
+ eventsTableName: name("events"),
231
213
  deliveries: table("deliveries"),
232
214
  keySequences: table("key_sequences"),
233
215
  lanes: table("lanes"),
@@ -254,8 +236,17 @@ function laneExpression(keyExpression, eventIdExpression) {
254
236
 
255
237
  // src/schema.ts
256
238
  var SCHEMA_MARKER_DIGITS = 32;
239
+ var PAYLOAD_COLUMNS = ` payload json NOT NULL,
240
+ headers json NOT NULL DEFAULT '{}'::json,`;
241
+ var JSONB_PAYLOAD_COLUMNS = ` payload jsonb NOT NULL,
242
+ headers jsonb NOT NULL DEFAULT '{}'::jsonb,`;
257
243
  function createPostgresSchemaStatements(options = {}) {
258
244
  const names = resolvePostgresNames(options);
245
+ const statements = schemaBodyStatements(names);
246
+ statements.push(`COMMENT ON TABLE ${names.lanes} IS '${version040Marker(names, statements)}'`);
247
+ return statements;
248
+ }
249
+ function schemaBodyStatements(names) {
259
250
  const statements = [...eventAndDeliveryStatements(names), ...laneAndCompletionStatements(names)];
260
251
  if (names.completionHistory) {
261
252
  statements.push(...completionHistoryStatements(names));
@@ -263,7 +254,6 @@ function createPostgresSchemaStatements(options = {}) {
263
254
  if (names.schema !== "public") {
264
255
  statements.unshift(`CREATE SCHEMA IF NOT EXISTS ${names.quotedSchema}`);
265
256
  }
266
- statements.push(`COMMENT ON TABLE ${names.lanes} IS '${postgresSchemaMarker(statements)}'`);
267
257
  return statements;
268
258
  }
269
259
  function eventAndDeliveryStatements(names) {
@@ -277,14 +267,14 @@ function eventAndKeySequenceStatements(names) {
277
267
  source text NOT NULL,
278
268
  key text,
279
269
  sequence bigint,
280
- payload jsonb NOT NULL,
281
- headers jsonb NOT NULL DEFAULT '{}'::jsonb,
270
+ ${PAYLOAD_COLUMNS}
282
271
  correlation_id text,
283
272
  causation_id text,
284
273
  occurred_at timestamptz NOT NULL,
285
274
  created_at timestamptz NOT NULL,
286
275
  handlers text[] NOT NULL DEFAULT '{}'
287
276
  )`,
277
+ upgradePayloadToJson(names),
288
278
  `CREATE INDEX IF NOT EXISTS ${names.indexes.eventCreated} ON ${names.events} (created_at, id)`,
289
279
  `CREATE TABLE IF NOT EXISTS ${names.keySequences} (
290
280
  key text PRIMARY KEY,
@@ -380,6 +370,33 @@ BEGIN
380
370
  END
381
371
  $relaybus_upgrade$`;
382
372
  }
373
+ function upgradePayloadToJson(names) {
374
+ return `DO $relaybus_payload_json$
375
+ BEGIN
376
+ IF EXISTS (
377
+ SELECT 1 FROM pg_attribute
378
+ WHERE attrelid = to_regclass('${names.events}') AND attname IN ('payload', 'headers')
379
+ AND atttypid = 'jsonb'::regtype AND NOT attisdropped
380
+ ) THEN
381
+ ALTER TABLE ${names.events}
382
+ ALTER COLUMN headers DROP DEFAULT,
383
+ ALTER COLUMN payload TYPE json USING payload::json,
384
+ ALTER COLUMN headers TYPE json USING headers::json,
385
+ ALTER COLUMN headers SET DEFAULT '{}'::json;
386
+ END IF;
387
+ END
388
+ $relaybus_payload_json$`;
389
+ }
390
+ function version040SchemaMarker(options = {}) {
391
+ const names = resolvePostgresNames(options);
392
+ return version040Marker(names, schemaBodyStatements(names));
393
+ }
394
+ function version040Marker(names, statements) {
395
+ const conversion = upgradePayloadToJson(names);
396
+ return postgresSchemaMarker(
397
+ statements.filter((statement) => statement !== conversion).map((statement) => statement.replace(PAYLOAD_COLUMNS, JSONB_PAYLOAD_COLUMNS))
398
+ );
399
+ }
383
400
  function createPostgresSchemaSql(options = {}) {
384
401
  return `${createPostgresSchemaStatements(options).join(";\n\n")};
385
402
  `;
@@ -500,10 +517,38 @@ function withRootPool(executor, root) {
500
517
  ...executor.transactionWithBudget === void 0 ? {} : {
501
518
  transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
502
519
  },
520
+ ...executor.transactionWithIsolation === void 0 ? {} : {
521
+ transactionWithIsolation: (work, options) => executor.transactionWithIsolation(work, options)
522
+ },
523
+ ...executor.isSerializationFailure === void 0 ? {} : { isSerializationFailure: (error) => executor.isSerializationFailure(error) },
503
524
  // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
504
525
  query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
505
526
  };
506
527
  }
528
+
529
+ // src/serialization-failure.ts
530
+ var SERIALIZATION_FAILURE = "40001";
531
+ var PRISMA_WRITE_CONFLICT = "P2034";
532
+ var CONFLICT_CODES = /* @__PURE__ */ new Set([
533
+ SERIALIZATION_FAILURE,
534
+ PRISMA_WRITE_CONFLICT
535
+ ]);
536
+ var MAXIMUM_DEPTH = 6;
537
+ var WRAPPED_ERROR_KEYS = ["cause", "driverError", "originalError"];
538
+ function isPostgresSerializationFailure(error, depth = 0) {
539
+ if (typeof error !== "object" || error === null || depth > MAXIMUM_DEPTH) {
540
+ return false;
541
+ }
542
+ const shape = error;
543
+ if (CONFLICT_CODES.has(shape.code) || shape.originalCode === SERIALIZATION_FAILURE) {
544
+ return true;
545
+ }
546
+ const meta = shape.meta;
547
+ if (isPostgresSerializationFailure(meta, depth + 1) || isPostgresSerializationFailure(meta?.driverAdapterError, depth + 1)) {
548
+ return true;
549
+ }
550
+ return WRAPPED_ERROR_KEYS.some((key) => isPostgresSerializationFailure(shape[key], depth + 1));
551
+ }
507
552
  function text(row, column) {
508
553
  return row[column];
509
554
  }
@@ -1444,6 +1489,67 @@ var PostgresMaintenance = class {
1444
1489
  });
1445
1490
  }
1446
1491
  };
1492
+
1493
+ // src/postgres-setup.ts
1494
+ var PostgresSetup = class {
1495
+ #state;
1496
+ /** The conversion of `jsonb` payload columns. */
1497
+ #payloadUpgrade;
1498
+ constructor(state, options) {
1499
+ this.#state = state;
1500
+ this.#payloadUpgrade = upgradePayloadToJson(resolvePostgresNames(options));
1501
+ }
1502
+ /** Implements {@link PostgresEventStore.setup}. */
1503
+ async run() {
1504
+ if (isCurrent(await this.#schemaState())) {
1505
+ return;
1506
+ }
1507
+ await this.#underSetupLock(async (transaction) => {
1508
+ await this.#run(this.#payloadUpgrade, transaction);
1509
+ });
1510
+ await this.#underSetupLock(async (transaction, schema) => {
1511
+ if (schema.marked) {
1512
+ await this.#run(this.#payloadUpgrade, transaction);
1513
+ return;
1514
+ }
1515
+ for (const statement of this.#state.schemaStatements) {
1516
+ await this.#run(statement, transaction);
1517
+ }
1518
+ });
1519
+ }
1520
+ /**
1521
+ * Runs `work` in a transaction under the setup lock, unless the schema became current. The
1522
+ * payload conversion goes first in a transaction of its own, so it holds no other table lock
1523
+ * while it waits for the event table (a completion locks deliveries before the event rows).
1524
+ */
1525
+ async #underSetupLock(work) {
1526
+ await this.#state.executor.transaction(async (transaction) => {
1527
+ await this.#state.executor.query(
1528
+ this.#state.statements.setup.setupLock,
1529
+ [this.#state.setupLockKey],
1530
+ transaction
1531
+ );
1532
+ const schema = await this.#schemaState(transaction);
1533
+ if (!isCurrent(schema)) {
1534
+ await work(transaction, schema);
1535
+ }
1536
+ });
1537
+ }
1538
+ async #run(statement, transaction) {
1539
+ await this.#state.executor.query(statement, [], transaction);
1540
+ }
1541
+ async #schemaState(transaction) {
1542
+ const rows = await this.#state.executor.query(
1543
+ this.#state.statements.setup.schemaCurrent,
1544
+ [this.#state.schemaMarker],
1545
+ transaction
1546
+ );
1547
+ return { marked: rows[0]["marked"] === true, payloadJson: rows[0]["payload_json"] === true };
1548
+ }
1549
+ };
1550
+ function isCurrent(schema) {
1551
+ return schema.marked && schema.payloadJson;
1552
+ }
1447
1553
  var WAKE_UPS = ["notify", "poll"];
1448
1554
  var DEFAULT_PURGE_BATCH_SIZE = 1e3;
1449
1555
  function resolveStoreSettings(options) {
@@ -1518,8 +1624,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1518
1624
  reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
1519
1625
  + row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
1520
1626
  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,
1627
+ FROM json_to_recordset($1::json) AS incoming (
1628
+ ordinal int, id text, type text, source text, key text, payload json, headers json,
1523
1629
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1524
1630
  LEFT JOIN reserved ON reserved.key = incoming.key
1525
1631
  )`;
@@ -1530,7 +1636,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1530
1636
  INSERT INTO ${this.#names.events}
1531
1637
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1532
1638
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1533
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1639
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1534
1640
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1535
1641
  COALESCE(incoming.handlers, '{}')
1536
1642
  FROM incoming
@@ -1583,11 +1689,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1583
1689
  return `INSERT INTO ${this.#names.events}
1584
1690
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1585
1691
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1586
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1692
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1587
1693
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1588
1694
  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,
1695
+ FROM json_to_recordset($1::json) AS incoming (
1696
+ id text, type text, source text, key text, sequence bigint, payload json, headers json,
1591
1697
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1592
1698
  ORDER BY incoming.id
1593
1699
  ON CONFLICT (id) DO NOTHING
@@ -2837,8 +2943,10 @@ var SetupStatements = class {
2837
2943
  return `relaybus:${this.#names.schema}`;
2838
2944
  }
2839
2945
  /**
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]`.
2946
+ * The state of the schema, taking no table lock. Parameters: `[marker]`. Columns: `marked`, whether
2947
+ * the lane table carries `marker` as its comment (the schema statements that produce it already
2948
+ * ran), and `payload_json`, whether the payload and headers columns of the event table are both
2949
+ * `json` (the conversion of the `jsonb` columns of schemas before 0.4.1 ran).
2842
2950
  */
2843
2951
  get schemaCurrent() {
2844
2952
  return `SELECT EXISTS (
@@ -2847,7 +2955,14 @@ var SetupStatements = class {
2847
2955
  JOIN pg_catalog.pg_description AS d
2848
2956
  ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
2849
2957
  WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
2850
- ) AS current`;
2958
+ ) AS marked, (
2959
+ SELECT count(*) FROM pg_catalog.pg_attribute AS a
2960
+ JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
2961
+ JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
2962
+ WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.eventsTableName}'
2963
+ AND a.attname IN ('payload', 'headers') AND a.atttypid = 'pg_catalog.json'::pg_catalog.regtype
2964
+ AND NOT a.attisdropped
2965
+ ) = 2 AS payload_json`;
2851
2966
  }
2852
2967
  /**
2853
2968
  * Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
@@ -2885,6 +3000,7 @@ var PostgresEventStore = class {
2885
3000
  #claims;
2886
3001
  #completion;
2887
3002
  #maintenance;
3003
+ #setup;
2888
3004
  #owned = new OwnedConnections();
2889
3005
  constructor(options) {
2890
3006
  const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
@@ -2898,7 +3014,7 @@ var PostgresEventStore = class {
2898
3014
  statements,
2899
3015
  schemaStatements,
2900
3016
  setupLockKey: statements.setup.setupLockKey,
2901
- schemaMarker: postgresSchemaMarker(schemaStatements.slice(0, -1)),
3017
+ schemaMarker: version040SchemaMarker(options),
2902
3018
  purgeBatchSize,
2903
3019
  completionHistory: names.completionHistory,
2904
3020
  logger,
@@ -2922,42 +3038,30 @@ var PostgresEventStore = class {
2922
3038
  this.#claims = new PostgresClaims(state);
2923
3039
  this.#completion = new PostgresCompletion(state);
2924
3040
  this.#maintenance = new PostgresMaintenance(state);
3041
+ this.#setup = new PostgresSetup(state, options);
2925
3042
  }
2926
3043
  /**
2927
3044
  * Creates the schema, tables and indexes if they do not exist. Safe to call
2928
3045
  * from many processes at once: a transaction-scoped advisory lock serializes
2929
3046
  * 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
3047
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
3048
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
2932
3049
  * that start next to running dispatchers cannot deadlock with their claims.
2933
3050
  * Skip it (`setupStore: false`) when a migration tool applies
2934
3051
  * `createPostgresSchemaSql()` instead.
2935
3052
  */
2936
3053
  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
- });
3054
+ await this.#setup.run();
2953
3055
  }
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;
3056
+ /** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
3057
+ isolationLevels = TRANSACTION_ISOLATION_LEVELS;
3058
+ /**
3059
+ * True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
3060
+ * Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
3061
+ * test with {@link PostgresExecutor.isSerializationFailure}.
3062
+ */
3063
+ isSerializationFailure(error) {
3064
+ return isPostgresSerializationFailure(error) || this.#state.executor.isSerializationFailure?.(error) === true;
2961
3065
  }
2962
3066
  /**
2963
3067
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
@@ -2966,6 +3070,9 @@ var PostgresEventStore = class {
2966
3070
  * `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
2967
3071
  * (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
2968
3072
  * created they are kept per connection and sent only when the connection does not have them yet.
3073
+ * With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
3074
+ * `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
3075
+ * ISOLATION LEVEL ...` before the budget statement.
2969
3076
  */
2970
3077
  async transaction(work, options) {
2971
3078
  const own = { wake: false };
@@ -2977,12 +3084,20 @@ var PostgresEventStore = class {
2977
3084
  this.#state.ownTransactions.delete(transaction);
2978
3085
  }
2979
3086
  };
2980
- const result = await (options === void 0 ? this.#state.executor.transaction(tracked) : inBudgetedTransaction(this.#state, { work: tracked, timeoutMs: options.timeoutMs }));
3087
+ const result = await this.#open(tracked, options);
2981
3088
  if (own.wake) {
2982
3089
  this.#appender.wakeAfterCommit();
2983
3090
  }
2984
3091
  return result;
2985
3092
  }
3093
+ /** Opens the transaction `work` runs in: plain, held to the budget, or at an isolation level too. */
3094
+ #open(work, options) {
3095
+ if (options === void 0) {
3096
+ return this.#state.executor.transaction(work);
3097
+ }
3098
+ const { timeoutMs, isolationLevel } = options;
3099
+ return isolationLevel === void 0 ? inBudgetedTransaction(this.#state, { work, timeoutMs }) : inIsolatedTransaction(this.#state, { work, timeoutMs, isolationLevel });
3100
+ }
2986
3101
  /**
2987
3102
  * Writes the entries in `transaction`, normally with one statement
2988
3103
  * (`AppendStatements.insertFreshEvents`): keyed events reserve their sequence numbers (locking