@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.cjs CHANGED
@@ -39,6 +39,41 @@ function inBudgetedTransaction(state, {
39
39
  });
40
40
  }
41
41
 
42
+ // src/transaction-isolation.ts
43
+ var ISOLATION_KEYWORDS = {
44
+ "read committed": "READ COMMITTED",
45
+ "repeatable read": "REPEATABLE READ",
46
+ serializable: "SERIALIZABLE"
47
+ };
48
+ function isolationKeywords(isolationLevel) {
49
+ const keywords = Object.hasOwn(ISOLATION_KEYWORDS, isolationLevel) ? ISOLATION_KEYWORDS[isolationLevel] : void 0;
50
+ if (keywords === void 0) {
51
+ throw new core.ConfigurationError(`Unknown isolation level ${JSON.stringify(isolationLevel)}`);
52
+ }
53
+ return keywords;
54
+ }
55
+ function inIsolatedTransaction(state, {
56
+ work,
57
+ timeoutMs,
58
+ isolationLevel
59
+ }) {
60
+ const { executor } = state;
61
+ if (executor.transactionWithIsolation !== void 0) {
62
+ return executor.transactionWithIsolation(work, { isolationLevel, timeoutMs });
63
+ }
64
+ const keywords = isolationKeywords(isolationLevel);
65
+ const budget = budgetSettingsSql({
66
+ value: "$1",
67
+ sessionTimeouts: executor.sessionTimeouts === true,
68
+ scope: "transaction"
69
+ });
70
+ return executor.transaction(async (transaction) => {
71
+ await executor.query(`SET TRANSACTION ISOLATION LEVEL ${keywords}`, [], transaction);
72
+ await state.query({ sql: budget, parameters: [budgetValue(timeoutMs)], transaction });
73
+ return work(transaction);
74
+ });
75
+ }
76
+
42
77
  // src/pg-executor.ts
43
78
  var FOREIGN_SESSION_ERRORS = /* @__PURE__ */ new Set(["26000", "42P05"]);
44
79
  var STATEMENT_NAME_DIGITS = 24;
@@ -69,14 +104,28 @@ var PgExecutor = class {
69
104
  * connection that already keeps this budget only gets `BEGIN`.
70
105
  */
71
106
  transactionWithBudget(work, timeoutMs) {
107
+ return this.#transaction(work, this.#budgetStart(timeoutMs, "BEGIN"));
108
+ }
109
+ /**
110
+ * Like {@link PgExecutor.transactionWithBudget} with `BEGIN ISOLATION LEVEL ...`: the level is set
111
+ * with the `BEGIN` of the same simple query, before the budget's first statement.
112
+ */
113
+ transactionWithIsolation(work, { isolationLevel, timeoutMs }) {
114
+ return this.#transaction(
115
+ work,
116
+ this.#budgetStart(timeoutMs, `BEGIN ISOLATION LEVEL ${isolationKeywords(isolationLevel)}`)
117
+ );
118
+ }
119
+ /** How a transaction starts on a connection when it is held to `timeoutMs`; `begin` is the `BEGIN` statement. */
120
+ #budgetStart(timeoutMs, begin) {
72
121
  const value = budgetValue(timeoutMs);
73
122
  const settings = (scope) => budgetSettingsSql({ value: `'${value}'`, sessionTimeouts: true, scope });
74
- return this.#transaction(work, (client) => {
123
+ return (client) => {
75
124
  if (!this.#keepsSessionBudget()) {
76
- return { begin: `BEGIN; ${settings("transaction")}` };
125
+ return { begin: `${begin}; ${settings("transaction")}` };
77
126
  }
78
- return this.#sessionValues.get(client) === value ? { begin: "BEGIN" } : { begin: `BEGIN; ${settings("session")}`, sessionValue: value };
79
- });
127
+ return this.#sessionValues.get(client) === value ? { begin } : { begin: `${begin}; ${settings("session")}`, sessionValue: value };
128
+ };
80
129
  }
81
130
  #recordSession(client, sessionValue) {
82
131
  if (sessionValue === null) {
@@ -230,6 +279,7 @@ function resolvePostgresNames(options = {}) {
230
279
  schema,
231
280
  quotedSchema,
232
281
  events: table("events"),
282
+ eventsTableName: name("events"),
233
283
  deliveries: table("deliveries"),
234
284
  keySequences: table("key_sequences"),
235
285
  lanes: table("lanes"),
@@ -256,8 +306,17 @@ function laneExpression(keyExpression, eventIdExpression) {
256
306
 
257
307
  // src/schema.ts
258
308
  var SCHEMA_MARKER_DIGITS = 32;
309
+ var PAYLOAD_COLUMNS = ` payload json NOT NULL,
310
+ headers json NOT NULL DEFAULT '{}'::json,`;
311
+ var JSONB_PAYLOAD_COLUMNS = ` payload jsonb NOT NULL,
312
+ headers jsonb NOT NULL DEFAULT '{}'::jsonb,`;
259
313
  function createPostgresSchemaStatements(options = {}) {
260
314
  const names = resolvePostgresNames(options);
315
+ const statements = schemaBodyStatements(names);
316
+ statements.push(`COMMENT ON TABLE ${names.lanes} IS '${version040Marker(names, statements)}'`);
317
+ return statements;
318
+ }
319
+ function schemaBodyStatements(names) {
261
320
  const statements = [...eventAndDeliveryStatements(names), ...laneAndCompletionStatements(names)];
262
321
  if (names.completionHistory) {
263
322
  statements.push(...completionHistoryStatements(names));
@@ -265,7 +324,6 @@ function createPostgresSchemaStatements(options = {}) {
265
324
  if (names.schema !== "public") {
266
325
  statements.unshift(`CREATE SCHEMA IF NOT EXISTS ${names.quotedSchema}`);
267
326
  }
268
- statements.push(`COMMENT ON TABLE ${names.lanes} IS '${postgresSchemaMarker(statements)}'`);
269
327
  return statements;
270
328
  }
271
329
  function eventAndDeliveryStatements(names) {
@@ -279,14 +337,14 @@ function eventAndKeySequenceStatements(names) {
279
337
  source text NOT NULL,
280
338
  key text,
281
339
  sequence bigint,
282
- payload jsonb NOT NULL,
283
- headers jsonb NOT NULL DEFAULT '{}'::jsonb,
340
+ ${PAYLOAD_COLUMNS}
284
341
  correlation_id text,
285
342
  causation_id text,
286
343
  occurred_at timestamptz NOT NULL,
287
344
  created_at timestamptz NOT NULL,
288
345
  handlers text[] NOT NULL DEFAULT '{}'
289
346
  )`,
347
+ upgradePayloadToJson(names),
290
348
  `CREATE INDEX IF NOT EXISTS ${names.indexes.eventCreated} ON ${names.events} (created_at, id)`,
291
349
  `CREATE TABLE IF NOT EXISTS ${names.keySequences} (
292
350
  key text PRIMARY KEY,
@@ -382,6 +440,33 @@ BEGIN
382
440
  END
383
441
  $relaybus_upgrade$`;
384
442
  }
443
+ function upgradePayloadToJson(names) {
444
+ return `DO $relaybus_payload_json$
445
+ BEGIN
446
+ IF EXISTS (
447
+ SELECT 1 FROM pg_attribute
448
+ WHERE attrelid = to_regclass('${names.events}') AND attname IN ('payload', 'headers')
449
+ AND atttypid = 'jsonb'::regtype AND NOT attisdropped
450
+ ) THEN
451
+ ALTER TABLE ${names.events}
452
+ ALTER COLUMN headers DROP DEFAULT,
453
+ ALTER COLUMN payload TYPE json USING payload::json,
454
+ ALTER COLUMN headers TYPE json USING headers::json,
455
+ ALTER COLUMN headers SET DEFAULT '{}'::json;
456
+ END IF;
457
+ END
458
+ $relaybus_payload_json$`;
459
+ }
460
+ function version040SchemaMarker(options = {}) {
461
+ const names = resolvePostgresNames(options);
462
+ return version040Marker(names, schemaBodyStatements(names));
463
+ }
464
+ function version040Marker(names, statements) {
465
+ const conversion = upgradePayloadToJson(names);
466
+ return postgresSchemaMarker(
467
+ statements.filter((statement) => statement !== conversion).map((statement) => statement.replace(PAYLOAD_COLUMNS, JSONB_PAYLOAD_COLUMNS))
468
+ );
469
+ }
385
470
  function createPostgresSchemaSql(options = {}) {
386
471
  return `${createPostgresSchemaStatements(options).join(";\n\n")};
387
472
  `;
@@ -502,10 +587,38 @@ function withRootPool(executor, root) {
502
587
  ...executor.transactionWithBudget === void 0 ? {} : {
503
588
  transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
504
589
  },
590
+ ...executor.transactionWithIsolation === void 0 ? {} : {
591
+ transactionWithIsolation: (work, options) => executor.transactionWithIsolation(work, options)
592
+ },
593
+ ...executor.isSerializationFailure === void 0 ? {} : { isSerializationFailure: (error) => executor.isSerializationFailure(error) },
505
594
  // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
506
595
  query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
507
596
  };
508
597
  }
598
+
599
+ // src/serialization-failure.ts
600
+ var SERIALIZATION_FAILURE = "40001";
601
+ var PRISMA_WRITE_CONFLICT = "P2034";
602
+ var CONFLICT_CODES = /* @__PURE__ */ new Set([
603
+ SERIALIZATION_FAILURE,
604
+ PRISMA_WRITE_CONFLICT
605
+ ]);
606
+ var MAXIMUM_DEPTH = 6;
607
+ var WRAPPED_ERROR_KEYS = ["cause", "driverError", "originalError"];
608
+ function isPostgresSerializationFailure(error, depth = 0) {
609
+ if (typeof error !== "object" || error === null || depth > MAXIMUM_DEPTH) {
610
+ return false;
611
+ }
612
+ const shape = error;
613
+ if (CONFLICT_CODES.has(shape.code) || shape.originalCode === SERIALIZATION_FAILURE) {
614
+ return true;
615
+ }
616
+ const meta = shape.meta;
617
+ if (isPostgresSerializationFailure(meta, depth + 1) || isPostgresSerializationFailure(meta?.driverAdapterError, depth + 1)) {
618
+ return true;
619
+ }
620
+ return WRAPPED_ERROR_KEYS.some((key) => isPostgresSerializationFailure(shape[key], depth + 1));
621
+ }
509
622
  function text(row, column) {
510
623
  return row[column];
511
624
  }
@@ -1446,6 +1559,67 @@ var PostgresMaintenance = class {
1446
1559
  });
1447
1560
  }
1448
1561
  };
1562
+
1563
+ // src/postgres-setup.ts
1564
+ var PostgresSetup = class {
1565
+ #state;
1566
+ /** The conversion of `jsonb` payload columns. */
1567
+ #payloadUpgrade;
1568
+ constructor(state, options) {
1569
+ this.#state = state;
1570
+ this.#payloadUpgrade = upgradePayloadToJson(resolvePostgresNames(options));
1571
+ }
1572
+ /** Implements {@link PostgresEventStore.setup}. */
1573
+ async run() {
1574
+ if (isCurrent(await this.#schemaState())) {
1575
+ return;
1576
+ }
1577
+ await this.#underSetupLock(async (transaction) => {
1578
+ await this.#run(this.#payloadUpgrade, transaction);
1579
+ });
1580
+ await this.#underSetupLock(async (transaction, schema) => {
1581
+ if (schema.marked) {
1582
+ await this.#run(this.#payloadUpgrade, transaction);
1583
+ return;
1584
+ }
1585
+ for (const statement of this.#state.schemaStatements) {
1586
+ await this.#run(statement, transaction);
1587
+ }
1588
+ });
1589
+ }
1590
+ /**
1591
+ * Runs `work` in a transaction under the setup lock, unless the schema became current. The
1592
+ * payload conversion goes first in a transaction of its own, so it holds no other table lock
1593
+ * while it waits for the event table (a completion locks deliveries before the event rows).
1594
+ */
1595
+ async #underSetupLock(work) {
1596
+ await this.#state.executor.transaction(async (transaction) => {
1597
+ await this.#state.executor.query(
1598
+ this.#state.statements.setup.setupLock,
1599
+ [this.#state.setupLockKey],
1600
+ transaction
1601
+ );
1602
+ const schema = await this.#schemaState(transaction);
1603
+ if (!isCurrent(schema)) {
1604
+ await work(transaction, schema);
1605
+ }
1606
+ });
1607
+ }
1608
+ async #run(statement, transaction) {
1609
+ await this.#state.executor.query(statement, [], transaction);
1610
+ }
1611
+ async #schemaState(transaction) {
1612
+ const rows = await this.#state.executor.query(
1613
+ this.#state.statements.setup.schemaCurrent,
1614
+ [this.#state.schemaMarker],
1615
+ transaction
1616
+ );
1617
+ return { marked: rows[0]["marked"] === true, payloadJson: rows[0]["payload_json"] === true };
1618
+ }
1619
+ };
1620
+ function isCurrent(schema) {
1621
+ return schema.marked && schema.payloadJson;
1622
+ }
1449
1623
  var WAKE_UPS = ["notify", "poll"];
1450
1624
  var DEFAULT_PURGE_BATCH_SIZE = 1e3;
1451
1625
  function resolveStoreSettings(options) {
@@ -1520,8 +1694,8 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1520
1694
  reserved.sequence - count(*) OVER (PARTITION BY incoming.key)
1521
1695
  + row_number() OVER (PARTITION BY incoming.key ORDER BY incoming.ordinal)
1522
1696
  END AS sequence
1523
- FROM jsonb_to_recordset($1::jsonb) AS incoming (
1524
- ordinal int, id text, type text, source text, key text, payload jsonb, headers jsonb,
1697
+ FROM json_to_recordset($1::json) AS incoming (
1698
+ ordinal int, id text, type text, source text, key text, payload json, headers json,
1525
1699
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1526
1700
  LEFT JOIN reserved ON reserved.key = incoming.key
1527
1701
  )`;
@@ -1532,7 +1706,7 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1532
1706
  INSERT INTO ${this.#names.events}
1533
1707
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1534
1708
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1535
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1709
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1536
1710
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1537
1711
  COALESCE(incoming.handlers, '{}')
1538
1712
  FROM incoming
@@ -1585,11 +1759,11 @@ WHERE (SELECT count(*) FROM woken) >= 0`;
1585
1759
  return `INSERT INTO ${this.#names.events}
1586
1760
  (id, type, source, key, sequence, payload, headers, correlation_id, causation_id, occurred_at, created_at, handlers)
1587
1761
  SELECT incoming.id, incoming.type, incoming.source, incoming.key, incoming.sequence,
1588
- COALESCE(incoming.payload, 'null'::jsonb), COALESCE(incoming.headers, '{}'::jsonb),
1762
+ COALESCE(incoming.payload, 'null'::json), COALESCE(incoming.headers, '{}'::json),
1589
1763
  incoming.correlation_id, incoming.causation_id, incoming.occurred_at, $2::timestamptz,
1590
1764
  COALESCE(incoming.handlers, '{}')
1591
- FROM jsonb_to_recordset($1::jsonb) AS incoming (
1592
- id text, type text, source text, key text, sequence bigint, payload jsonb, headers jsonb,
1765
+ FROM json_to_recordset($1::json) AS incoming (
1766
+ id text, type text, source text, key text, sequence bigint, payload json, headers json,
1593
1767
  correlation_id text, causation_id text, occurred_at timestamptz, handlers text[])
1594
1768
  ORDER BY incoming.id
1595
1769
  ON CONFLICT (id) DO NOTHING
@@ -2839,8 +3013,10 @@ var SetupStatements = class {
2839
3013
  return `relaybus:${this.#names.schema}`;
2840
3014
  }
2841
3015
  /**
2842
- * Whether the lane table carries `marker` as its comment, that is whether the schema statements
2843
- * that produce that marker already ran. Takes no table lock. Parameters: `[marker]`.
3016
+ * The state of the schema, taking no table lock. Parameters: `[marker]`. Columns: `marked`, whether
3017
+ * the lane table carries `marker` as its comment (the schema statements that produce it already
3018
+ * ran), and `payload_json`, whether the payload and headers columns of the event table are both
3019
+ * `json` (the conversion of the `jsonb` columns of schemas before 0.4.1 ran).
2844
3020
  */
2845
3021
  get schemaCurrent() {
2846
3022
  return `SELECT EXISTS (
@@ -2849,7 +3025,14 @@ var SetupStatements = class {
2849
3025
  JOIN pg_catalog.pg_description AS d
2850
3026
  ON d.objoid = c.oid AND d.classoid = 'pg_catalog.pg_class'::regclass AND d.objsubid = 0
2851
3027
  WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.lanesTableName}' AND d.description = $1::text
2852
- ) AS current`;
3028
+ ) AS marked, (
3029
+ SELECT count(*) FROM pg_catalog.pg_attribute AS a
3030
+ JOIN pg_catalog.pg_class AS c ON c.oid = a.attrelid
3031
+ JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
3032
+ WHERE n.nspname = '${this.#names.schema}' AND c.relname = '${this.#names.eventsTableName}'
3033
+ AND a.attname IN ('payload', 'headers') AND a.atttypid = 'pg_catalog.json'::pg_catalog.regtype
3034
+ AND NOT a.attisdropped
3035
+ ) = 2 AS payload_json`;
2853
3036
  }
2854
3037
  /**
2855
3038
  * Takes the setup lock. Returns a boolean column rather than the `void` of the lock function, which
@@ -2887,6 +3070,7 @@ var PostgresEventStore = class {
2887
3070
  #claims;
2888
3071
  #completion;
2889
3072
  #maintenance;
3073
+ #setup;
2890
3074
  #owned = new OwnedConnections();
2891
3075
  constructor(options) {
2892
3076
  const { purgeBatchSize, wakeUp, queryOptions } = resolveStoreSettings(options);
@@ -2900,7 +3084,7 @@ var PostgresEventStore = class {
2900
3084
  statements,
2901
3085
  schemaStatements,
2902
3086
  setupLockKey: statements.setup.setupLockKey,
2903
- schemaMarker: postgresSchemaMarker(schemaStatements.slice(0, -1)),
3087
+ schemaMarker: version040SchemaMarker(options),
2904
3088
  purgeBatchSize,
2905
3089
  completionHistory: names.completionHistory,
2906
3090
  logger,
@@ -2924,42 +3108,30 @@ var PostgresEventStore = class {
2924
3108
  this.#claims = new PostgresClaims(state);
2925
3109
  this.#completion = new PostgresCompletion(state);
2926
3110
  this.#maintenance = new PostgresMaintenance(state);
3111
+ this.#setup = new PostgresSetup(state, options);
2927
3112
  }
2928
3113
  /**
2929
3114
  * Creates the schema, tables and indexes if they do not exist. Safe to call
2930
3115
  * from many processes at once: a transaction-scoped advisory lock serializes
2931
3116
  * the DDL, so racing `CREATE TABLE IF NOT EXISTS` statements cannot collide.
2932
- * When the lane table already carries the marker of the current statements
2933
- * ({@link postgresSchemaMarker}) nothing runs and no table is locked, so instances
3117
+ * When the lane table already carries the schema marker ({@link version040SchemaMarker})
3118
+ * and the payload columns are `json`, nothing runs and no table is locked, so instances
2934
3119
  * that start next to running dispatchers cannot deadlock with their claims.
2935
3120
  * Skip it (`setupStore: false`) when a migration tool applies
2936
3121
  * `createPostgresSchemaSql()` instead.
2937
3122
  */
2938
3123
  async setup() {
2939
- if (await this.#schemaIsCurrent()) {
2940
- return;
2941
- }
2942
- await this.#state.executor.transaction(async (transaction) => {
2943
- await this.#state.executor.query(
2944
- this.#state.statements.setup.setupLock,
2945
- [this.#state.setupLockKey],
2946
- transaction
2947
- );
2948
- if (await this.#schemaIsCurrent(transaction)) {
2949
- return;
2950
- }
2951
- for (const statement of this.#state.schemaStatements) {
2952
- await this.#state.executor.query(statement, [], transaction);
2953
- }
2954
- });
3124
+ await this.#setup.run();
2955
3125
  }
2956
- async #schemaIsCurrent(transaction) {
2957
- const rows = await this.#state.executor.query(
2958
- this.#state.statements.setup.schemaCurrent,
2959
- [this.#state.schemaMarker],
2960
- transaction
2961
- );
2962
- return rows[0]["current"] === true;
3126
+ /** Every level PostgreSQL has (it runs `read uncommitted` as `read committed`, which the bus does not offer). */
3127
+ isolationLevels = core.TRANSACTION_ISOLATION_LEVELS;
3128
+ /**
3129
+ * True for a serialization failure (SQLSTATE `40001`) raised through any executor, including
3130
+ * Prisma's `P2034` and the wrappers of TypeORM and Drizzle; a custom executor can add its own
3131
+ * test with {@link PostgresExecutor.isSerializationFailure}.
3132
+ */
3133
+ isSerializationFailure(error) {
3134
+ return isPostgresSerializationFailure(error) || this.#state.executor.isSerializationFailure?.(error) === true;
2963
3135
  }
2964
3136
  /**
2965
3137
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
@@ -2968,6 +3140,9 @@ var PostgresEventStore = class {
2968
3140
  * `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
2969
3141
  * (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
2970
3142
  * created they are kept per connection and sent only when the connection does not have them yet.
3143
+ * With `options.isolationLevel` the transaction runs at that level: `PgExecutor` sends it with
3144
+ * `BEGIN ISOLATION LEVEL ...` (no extra round trip), any other executor as `SET TRANSACTION
3145
+ * ISOLATION LEVEL ...` before the budget statement.
2971
3146
  */
2972
3147
  async transaction(work, options) {
2973
3148
  const own = { wake: false };
@@ -2979,12 +3154,20 @@ var PostgresEventStore = class {
2979
3154
  this.#state.ownTransactions.delete(transaction);
2980
3155
  }
2981
3156
  };
2982
- const result = await (options === void 0 ? this.#state.executor.transaction(tracked) : inBudgetedTransaction(this.#state, { work: tracked, timeoutMs: options.timeoutMs }));
3157
+ const result = await this.#open(tracked, options);
2983
3158
  if (own.wake) {
2984
3159
  this.#appender.wakeAfterCommit();
2985
3160
  }
2986
3161
  return result;
2987
3162
  }
3163
+ /** Opens the transaction `work` runs in: plain, held to the budget, or at an isolation level too. */
3164
+ #open(work, options) {
3165
+ if (options === void 0) {
3166
+ return this.#state.executor.transaction(work);
3167
+ }
3168
+ const { timeoutMs, isolationLevel } = options;
3169
+ return isolationLevel === void 0 ? inBudgetedTransaction(this.#state, { work, timeoutMs }) : inIsolatedTransaction(this.#state, { work, timeoutMs, isolationLevel });
3170
+ }
2988
3171
  /**
2989
3172
  * Writes the entries in `transaction`, normally with one statement
2990
3173
  * (`AppendStatements.insertFreshEvents`): keyed events reserve their sequence numbers (locking