@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/README.md CHANGED
@@ -91,6 +91,16 @@ released. The other adapters keep only `statement_timeout`, because a server-end
91
91
  (the optional executor method `transactionWithBudget`), so the budget costs no extra round trip; executors without it
92
92
  send them as the first statement of the transaction.
93
93
 
94
+ **Isolation levels.** `bus.transaction(work, { isolationLevel })` opens the transaction at `'read committed'`,
95
+ `'repeatable read'` or `'serializable'`, and at the last two reruns `work` after a serialization failure
96
+ (SQLSTATE `40001`; see the `@relaybus/nestjs` README). `PgExecutor` sends `BEGIN ISOLATION LEVEL ...` in the same round
97
+ trip as the budget settings, so a level costs no statement. The TypeORM adapter uses TypeORM's own `transaction('SERIALIZABLE', ...)` (so the level is set before TypeORM subscribers run). Any other executor gets `SET TRANSACTION ISOLATION LEVEL ...`
98
+ as the first statement of the transaction and the budget statement second (PostgreSQL refuses the level once a query ran,
99
+ and the budget's `set_config` is a query). The store recognizes `40001` through `pg` (also what Kysely and Knex pass
100
+ through), TypeORM's `driverError`, the `cause` of a Drizzle error and Prisma's `P2034`, `meta.code` and driver-adapter
101
+ errors. A custom executor implements the optional `transactionWithIsolation` to send the level with its `BEGIN`, and the
102
+ optional `isSerializationFailure` for an error shape the store does not know.
103
+
94
104
  Where the budget lives depends on who owns the pool:
95
105
 
96
106
  - **A pool the store created** (`connectionString`/`poolConfig`): each connection keeps the budget as a session setting.
@@ -102,7 +112,8 @@ Where the budget lives depends on who owns the pool:
102
112
  - **A pool or executor you pass in**: the budget is transaction-scoped (`SET LOCAL` semantics), set with `BEGIN` by
103
113
  `PgExecutor` and by its own statement for the other adapters. Session settings of your pool are never changed.
104
114
 
105
- **Prisma** is integration-tested with a generated Prisma Client 7.10 (and 6.19 in CI) on the `@prisma/adapter-pg`
115
+ **Prisma** 5, 6 and 7 are supported (optional peer `@prisma/client` `^5.0.0 || ^6.0.0 || ^7.0.0`). It is
116
+ integration-tested with a generated Prisma Client 7.10 (and 6.19 and 5.22 in CI) on the `@prisma/adapter-pg`
106
117
  driver adapter: the full event store contract, publishing in `prisma.$transaction`, rollback and concurrent
107
118
  transactions. The adapter is typed structurally, so the package has no Prisma dependency. Prisma interactive
108
119
  transactions default to a 5 s timeout, so pass `createPrismaExecutor(prisma, { timeout, maxWait })` if handlers run
@@ -155,14 +166,14 @@ import { createPostgresSchemaSql, createPostgresSchemaStatements } from '@relayb
155
166
  const migration = createPostgresSchemaSql({ schema: 'app', tablePrefix: 'bus_' });
156
167
  ```
157
168
 
158
- | Table | Purpose |
159
- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
- | `events` | One row per event: `id` (**text**, so any caller id works), `type`, `source`, `key`, `sequence`, `payload jsonb`, `headers jsonb`, correlation and causation ids, `occurred_at`, `created_at`, and `handlers`, the handlers that ever got a delivery (it outlives the deliveries). |
161
- | `deliveries` | One row per (event, handler) that is not finished: `lane`, `status` (`pending` or `dead`), `attempts`, `available_at`, `last_error jsonb`, key and sequence copied from the event, timestamps. `unique (event_id, handler)`, foreign key to events with `ON DELETE CASCADE`. |
162
- | `lanes` | 256 rows, one per lane: `epoch` (the fencing token), `lease_owner`, `lease_expires_at` and `in_flight` (ids of the deliveries the owner claimed). `fillfactor = 50` and no secondary index, so its updates are heap-only (HOT). |
163
- | `event_completions` | One row per event that had a delivery complete, with the time of its last completion. Retention of completed work (`retention.completed`) is measured from it, however old the event is; events that never had a delivery complete are measured from their creation. |
164
- | `key_sequences` | One counter row per ordering key. |
165
- | `completed_deliveries` | Only with `completionHistory`: one insert-only row per completed delivery. |
169
+ | Table | Purpose |
170
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
171
+ | `events` | One row per event: `id` (**text**, so any caller id works), `type`, `source`, `key`, `sequence`, `payload json`, `headers json`, correlation and causation ids, `occurred_at`, `created_at`, and `handlers`, the handlers that ever got a delivery (it outlives the deliveries). |
172
+ | `deliveries` | One row per (event, handler) that is not finished: `lane`, `status` (`pending` or `dead`), `attempts`, `available_at`, `last_error jsonb`, key and sequence copied from the event, timestamps. `unique (event_id, handler)`, foreign key to events with `ON DELETE CASCADE`. |
173
+ | `lanes` | 256 rows, one per lane: `epoch` (the fencing token), `lease_owner`, `lease_expires_at` and `in_flight` (ids of the deliveries the owner claimed). `fillfactor = 50` and no secondary index, so its updates are heap-only (HOT). |
174
+ | `event_completions` | One row per event that had a delivery complete, with the time of its last completion. Retention of completed work (`retention.completed`) is measured from it, however old the event is; events that never had a delivery complete are measured from their creation. |
175
+ | `key_sequences` | One counter row per ordering key. |
176
+ | `completed_deliveries` | Only with `completionHistory`: one insert-only row per completed delivery. |
166
177
 
167
178
  A delivery row is inserted once and deleted once when it completes. Claiming, renewing a lease and completing never
168
179
  update it: they update the lane row (claim, renewal) or delete the delivery (completion), so the indexed columns of a
@@ -257,10 +268,12 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
257
268
  - **Key counters.** `key_sequences` keeps one row per key ever published and rows are never deleted; the table grows
258
269
  with the number of distinct keys (a bigint counter each). Pick keys with bounded cardinality, or prune counters of keys
259
270
  you know are retired once nothing for them is pending.
260
- - **Stored JSON.** Payloads and headers are stored as `jsonb`: object keys come back **reordered** (shorter keys first,
261
- then bytewise) and duplicate keys collapse, values are otherwise identical. A string containing `\u0000` (NUL) or a
262
- lone UTF-16 surrogate is rejected by PostgreSQL and fails the publish with a database error; strip or encode such
263
- text before publishing.
271
+ - **Stored JSON.** Payloads and headers are stored as `json`, the text the producer serialized: an event read back
272
+ serializes byte for byte as it was published, key order included. (Before 0.4.1 they were `jsonb`, which re-orders
273
+ object keys; `setup()` converts the columns, and rows stored before the upgrade keep the order `jsonb` gave them. See
274
+ [Upgrading](https://berkcansavur.github.io/relaybus/upgrading).) A string containing `\u0000` (NUL) or a lone UTF-16
275
+ surrogate is rejected by PostgreSQL and fails the publish with a database error; strip or encode such text before
276
+ publishing.
264
277
  - **Sequence gaps.** Republishing an existing event id consumes a sequence number. Gaps are normal; only order matters.
265
278
  - **Vacuum.** Completing a delivery deletes its row, which leaves a dead tuple until autovacuum runs. The tables are
266
279
  created with `autovacuum_vacuum_scale_factor = 0.02`; for sustained throughput above a few thousand deliveries per
@@ -315,8 +328,8 @@ tool, run it as one transaction with the workers stopped. Running 0.1.0 and the
315
328
  `createPostgresSchemaSql()`.
316
329
  - `permission denied for schema`: the role needs `CREATE` on the schema for `setup()`; with `setupStore: false` it needs
317
330
  only DML rights.
318
- - `invalid input syntax for type json` / `\u0000 cannot be converted to text`: payloads with NUL characters are not valid
319
- `jsonb`.
331
+ - `invalid input syntax for type json` / `unsupported Unicode escape sequence`: the payload holds a NUL character or a
332
+ lone UTF-16 surrogate, which PostgreSQL rejects.
320
333
  - Foreign key error on append: the only known cause is republishing an event id in the same instant its last delivery
321
334
  is purged. The publishing transaction rolls back and can simply be retried.
322
335
  - A key looks stuck: a pending or processing head blocks it. Inspect `listDeliveries({ status: 'processing' })` and
@@ -0,0 +1,26 @@
1
+ import { budgetSettingsSql, budgetValue } from './chunk-UDBN4RRV.js';
2
+
3
+ // src/adapter-executor.ts
4
+ function adapterExecutor(client) {
5
+ return {
6
+ transaction: client.transaction,
7
+ ...client.transactionAt === void 0 ? {} : {
8
+ // The level is set by the library at BEGIN, so the budget statement can follow as the first query.
9
+ transactionWithIsolation: (work, { isolationLevel, timeoutMs }) => client.transactionAt(isolationLevel, async (queryable) => {
10
+ const sql = budgetSettingsSql({
11
+ value: "$1",
12
+ sessionTimeouts: false,
13
+ scope: "transaction"
14
+ });
15
+ await client.run(queryable, { sql, parameters: [budgetValue(timeoutMs)] });
16
+ return work(queryable);
17
+ })
18
+ },
19
+ // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
20
+ query: (sql, parameters, transaction) => client.run(transaction ?? client.root, { sql, parameters })
21
+ };
22
+ }
23
+
24
+ export { adapterExecutor };
25
+ //# sourceMappingURL=chunk-5WWPAN5Q.js.map
26
+ //# sourceMappingURL=chunk-5WWPAN5Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapter-executor.ts"],"names":[],"mappings":";;;AAgCO,SAAS,gBACd,MAAA,EAC8B;AAC9B,EAAA,OAAO;AAAA,IACL,aAAa,MAAA,CAAO,WAAA;AAAA,IACpB,GAAI,MAAA,CAAO,aAAA,KAAkB,MAAA,GACzB,EAAC,GACD;AAAA;AAAA,MAEE,wBAAA,EAA0B,CAAC,IAAA,EAAM,EAAE,cAAA,EAAgB,SAAA,EAAU,KAC3D,MAAA,CAAO,aAAA,CAAe,cAAA,EAAgB,OAAO,SAAA,KAAc;AACzD,QAAA,MAAM,MAAM,iBAAA,CAAkB;AAAA,UAC5B,KAAA,EAAO,IAAA;AAAA,UACP,eAAA,EAAiB,KAAA;AAAA,UACjB,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,MAAM,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,EAAE,GAAA,EAAK,UAAA,EAAY,CAAC,WAAA,CAAY,SAAS,CAAC,CAAA,EAAG,CAAA;AACzE,QAAA,OAAO,KAAK,SAAS,CAAA;AAAA,MACvB,CAAC;AAAA,KACL;AAAA;AAAA,IAEJ,KAAA,EAAO,CAAC,GAAA,EAAK,UAAA,EAAY,WAAA,KACvB,MAAA,CAAO,GAAA,CAAI,WAAA,IAAe,MAAA,CAAO,IAAA,EAAM,EAAE,GAAA,EAAK,YAAY;AAAA,GAC9D;AACF","file":"chunk-5WWPAN5Q.js","sourcesContent":["import type { TransactionIsolationLevel } from '@relaybus/core';\nimport type { PostgresExecutor, PostgresParameter, PostgresRow } from './executor.js';\nimport { budgetSettingsSql, budgetValue } from './transaction-timeouts.js';\n\n/** One statement the store sends, with its positional parameters. */\nexport type AdapterStatement = {\n readonly sql: string;\n readonly parameters: readonly PostgresParameter[];\n};\n\n/** What an adapter supplies: where statements run outside a transaction, and how it runs one. */\nexport type AdapterClient<TQueryable> = {\n /** The instance itself: statements outside a transaction run on it. */\n readonly root: TQueryable;\n readonly transaction: PostgresExecutor<TQueryable>['transaction'];\n /**\n * Optional. Opens a transaction at an isolation level through the library's own API, for a library\n * that runs code of its own (hooks, subscribers) right after it begins a transaction: that code\n * would otherwise run a query before the store's `SET TRANSACTION ISOLATION LEVEL`.\n */\n readonly transactionAt?: <T>(\n isolationLevel: TransactionIsolationLevel,\n work: (queryable: TQueryable) => Promise<T>,\n ) => Promise<T>;\n /** Runs one statement on the caller's transaction, else on the root. */\n readonly run: (queryable: TQueryable, statement: AdapterStatement) => Promise<PostgresRow[]>;\n};\n\n/**\n * The executor every library adapter shares: a statement runs on the transaction it is given,\n * else on the root instance. An adapter supplies only how its library runs one statement.\n */\nexport function adapterExecutor<TQueryable>(\n client: AdapterClient<TQueryable>,\n): PostgresExecutor<TQueryable> {\n return {\n transaction: client.transaction,\n ...(client.transactionAt === undefined\n ? {}\n : {\n // The level is set by the library at BEGIN, so the budget statement can follow as the first query.\n transactionWithIsolation: (work, { isolationLevel, timeoutMs }) =>\n client.transactionAt!(isolationLevel, async (queryable) => {\n const sql = budgetSettingsSql({\n value: '$1',\n sessionTimeouts: false,\n scope: 'transaction',\n });\n await client.run(queryable, { sql, parameters: [budgetValue(timeoutMs)] });\n return work(queryable);\n }),\n }),\n // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature\n query: (sql, parameters, transaction) =>\n client.run(transaction ?? client.root, { sql, parameters }),\n };\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { budgetSettingsSql, budgetValue } from './chunk-UDBN4RRV.js';
2
+ import { ConfigurationError } from '@relaybus/core';
3
+
4
+ var ISOLATION_KEYWORDS = {
5
+ "read committed": "READ COMMITTED",
6
+ "repeatable read": "REPEATABLE READ",
7
+ serializable: "SERIALIZABLE"
8
+ };
9
+ function isolationKeywords(isolationLevel) {
10
+ const keywords = Object.hasOwn(ISOLATION_KEYWORDS, isolationLevel) ? ISOLATION_KEYWORDS[isolationLevel] : void 0;
11
+ if (keywords === void 0) {
12
+ throw new ConfigurationError(`Unknown isolation level ${JSON.stringify(isolationLevel)}`);
13
+ }
14
+ return keywords;
15
+ }
16
+ function inIsolatedTransaction(state, {
17
+ work,
18
+ timeoutMs,
19
+ isolationLevel
20
+ }) {
21
+ const { executor } = state;
22
+ if (executor.transactionWithIsolation !== void 0) {
23
+ return executor.transactionWithIsolation(work, { isolationLevel, timeoutMs });
24
+ }
25
+ const keywords = isolationKeywords(isolationLevel);
26
+ const budget = budgetSettingsSql({
27
+ value: "$1",
28
+ sessionTimeouts: executor.sessionTimeouts === true,
29
+ scope: "transaction"
30
+ });
31
+ return executor.transaction(async (transaction) => {
32
+ await executor.query(`SET TRANSACTION ISOLATION LEVEL ${keywords}`, [], transaction);
33
+ await state.query({ sql: budget, parameters: [budgetValue(timeoutMs)], transaction });
34
+ return work(transaction);
35
+ });
36
+ }
37
+
38
+ export { inIsolatedTransaction, isolationKeywords };
39
+ //# sourceMappingURL=chunk-FY65AUUF.js.map
40
+ //# sourceMappingURL=chunk-FY65AUUF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transaction-isolation.ts"],"names":[],"mappings":";;;AAKA,IAAM,kBAAA,GAA0E;AAAA,EAC9E,gBAAA,EAAkB,gBAAA;AAAA,EAClB,iBAAA,EAAmB,iBAAA;AAAA,EACnB,YAAA,EAAc;AAChB,CAAA;AAGO,SAAS,kBAAkB,cAAA,EAAmD;AACnF,EAAA,MAAM,QAAA,GAAW,OAAO,MAAA,CAAO,kBAAA,EAAoB,cAAc,CAAA,GAC7D,kBAAA,CAAmB,cAAc,CAAA,GACjC,MAAA;AACJ,EAAA,IAAI,aAAa,MAAA,EAAW;AAC1B,IAAA,MAAM,IAAI,kBAAA,CAAmB,CAAA,wBAAA,EAA2B,KAAK,SAAA,CAAU,cAAc,CAAC,CAAA,CAAE,CAAA;AAAA,EAC1F;AACA,EAAA,OAAO,QAAA;AACT;AAQO,SAAS,sBACd,KAAA,EACA;AAAA,EACE,IAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAKY;AACZ,EAAA,MAAM,EAAE,UAAS,GAAI,KAAA;AACrB,EAAA,IAAI,QAAA,CAAS,6BAA6B,MAAA,EAAW;AACnD,IAAA,OAAO,SAAS,wBAAA,CAAyB,IAAA,EAAM,EAAE,cAAA,EAAgB,WAAW,CAAA;AAAA,EAC9E;AACA,EAAA,MAAM,QAAA,GAAW,kBAAkB,cAAc,CAAA;AACjD,EAAA,MAAM,SAAS,iBAAA,CAAkB;AAAA,IAC/B,KAAA,EAAO,IAAA;AAAA,IACP,eAAA,EAAiB,SAAS,eAAA,KAAoB,IAAA;AAAA,IAC9C,KAAA,EAAO;AAAA,GACR,CAAA;AACD,EAAA,OAAO,QAAA,CAAS,WAAA,CAAY,OAAO,WAAA,KAAgB;AACjD,IAAA,MAAM,SAAS,KAAA,CAAM,CAAA,gCAAA,EAAmC,QAAQ,CAAA,CAAA,EAAI,IAAI,WAAW,CAAA;AACnF,IAAA,MAAM,KAAA,CAAM,KAAA,CAAM,EAAE,GAAA,EAAK,MAAA,EAAQ,UAAA,EAAY,CAAC,WAAA,CAAY,SAAS,CAAC,CAAA,EAAG,WAAA,EAAa,CAAA;AACpF,IAAA,OAAO,KAAK,WAAW,CAAA;AAAA,EACzB,CAAC,CAAA;AACH","file":"chunk-FY65AUUF.js","sourcesContent":["import { ConfigurationError, type TransactionIsolationLevel } from '@relaybus/core';\nimport type { PostgresStoreState } from './postgres-store-state.js';\nimport { budgetSettingsSql, budgetValue } from './transaction-timeouts.js';\n\n/** The SQL keywords of each level. Only these literals ever reach SQL text. */\nconst ISOLATION_KEYWORDS: Readonly<Record<TransactionIsolationLevel, string>> = {\n 'read committed': 'READ COMMITTED',\n 'repeatable read': 'REPEATABLE READ',\n serializable: 'SERIALIZABLE',\n};\n\n/** The keywords of `isolationLevel` as SQL; an unknown level (a store called around the bus) is refused. */\nexport function isolationKeywords(isolationLevel: TransactionIsolationLevel): string {\n const keywords = Object.hasOwn(ISOLATION_KEYWORDS, isolationLevel)\n ? ISOLATION_KEYWORDS[isolationLevel]\n : undefined;\n if (keywords === undefined) {\n throw new ConfigurationError(`Unknown isolation level ${JSON.stringify(isolationLevel)}`);\n }\n return keywords;\n}\n\n/**\n * Runs `work` in a transaction at `isolationLevel` that the server holds to `timeoutMs`. An executor\n * with `transactionWithIsolation` (`PgExecutor`) sends both with its `BEGIN`; any other executor gets\n * `SET TRANSACTION ISOLATION LEVEL` as the first statement of the transaction and the budget as the\n * second: PostgreSQL refuses the level once a query ran, and the budget's `set_config` is a query.\n */\nexport function inIsolatedTransaction<TTransaction, T>(\n state: PostgresStoreState<TTransaction>,\n {\n work,\n timeoutMs,\n isolationLevel,\n }: {\n readonly work: (transaction: TTransaction) => Promise<T>;\n readonly timeoutMs: number;\n readonly isolationLevel: TransactionIsolationLevel;\n },\n): Promise<T> {\n const { executor } = state;\n if (executor.transactionWithIsolation !== undefined) {\n return executor.transactionWithIsolation(work, { isolationLevel, timeoutMs });\n }\n const keywords = isolationKeywords(isolationLevel);\n const budget = budgetSettingsSql({\n value: '$1',\n sessionTimeouts: executor.sessionTimeouts === true,\n scope: 'transaction',\n });\n return executor.transaction(async (transaction) => {\n await executor.query(`SET TRANSACTION ISOLATION LEVEL ${keywords}`, [], transaction);\n await state.query({ sql: budget, parameters: [budgetValue(timeoutMs)], transaction });\n return work(transaction);\n });\n}\n"]}
@@ -0,0 +1,35 @@
1
+ // src/transaction-timeouts.ts
2
+ function budgetSettingsSql({ value, sessionTimeouts, scope }) {
3
+ const local = scope === "transaction";
4
+ const statementTimeout = `SELECT set_config('statement_timeout', ${value}, ${local})`;
5
+ if (!sessionTimeouts) {
6
+ return statementTimeout;
7
+ }
8
+ 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`;
9
+ }
10
+ 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')";
11
+ function budgetValue(timeoutMs) {
12
+ return `${Math.ceil(timeoutMs)}ms`;
13
+ }
14
+ function inBudgetedTransaction(state, {
15
+ work,
16
+ timeoutMs
17
+ }) {
18
+ const { executor } = state;
19
+ if (executor.transactionWithBudget !== void 0) {
20
+ return executor.transactionWithBudget(work, timeoutMs);
21
+ }
22
+ const sql = budgetSettingsSql({
23
+ value: "$1",
24
+ sessionTimeouts: executor.sessionTimeouts === true,
25
+ scope: "transaction"
26
+ });
27
+ return executor.transaction(async (transaction) => {
28
+ await state.query({ sql, parameters: [budgetValue(timeoutMs)], transaction });
29
+ return work(transaction);
30
+ });
31
+ }
32
+
33
+ export { SESSION_DEFAULTS_SQL, budgetSettingsSql, budgetValue, inBudgetedTransaction };
34
+ //# sourceMappingURL=chunk-UDBN4RRV.js.map
35
+ //# sourceMappingURL=chunk-UDBN4RRV.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/transaction-timeouts.ts"],"names":[],"mappings":";AAiBO,SAAS,iBAAA,CAAkB,EAAE,KAAA,EAAO,eAAA,EAAiB,OAAM,EAA2B;AAC3F,EAAA,MAAM,QAAQ,KAAA,KAAU,aAAA;AACxB,EAAA,MAAM,gBAAA,GAAmB,CAAA,uCAAA,EAA0C,KAAK,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,CAAA;AAClF,EAAA,IAAI,CAAC,eAAA,EAAiB;AACpB,IAAA,OAAO,gBAAA;AAAA,EACT;AACA,EAAA,OACE,CAAA,EAAG,gBAAgB,CAAA,oDAAA,EAAuD,KAAK,KAAK,KAAK,CAAA,yGAAA,EAE/C,KAAK,CAAA,EAAA,EAAK,KAAK,CAAA,KAAA,CAAA;AAE7D;AAGO,IAAM,oBAAA,GACX;AAIK,SAAS,YAAY,SAAA,EAA2B;AACrD,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,SAAS,CAAC,CAAA,EAAA,CAAA;AAChC;AAOO,SAAS,sBACd,KAAA,EACA;AAAA,EACE,IAAA;AAAA,EACA;AACF,CAAA,EAIY;AACZ,EAAA,MAAM,EAAE,UAAS,GAAI,KAAA;AACrB,EAAA,IAAI,QAAA,CAAS,0BAA0B,MAAA,EAAW;AAChD,IAAA,OAAO,QAAA,CAAS,qBAAA,CAAsB,IAAA,EAAM,SAAS,CAAA;AAAA,EACvD;AACA,EAAA,MAAM,MAAM,iBAAA,CAAkB;AAAA,IAC5B,KAAA,EAAO,IAAA;AAAA,IACP,eAAA,EAAiB,SAAS,eAAA,KAAoB,IAAA;AAAA,IAC9C,KAAA,EAAO;AAAA,GACR,CAAA;AACD,EAAA,OAAO,QAAA,CAAS,WAAA,CAAY,OAAO,WAAA,KAAgB;AACjD,IAAA,MAAM,KAAA,CAAM,KAAA,CAAM,EAAE,GAAA,EAAK,UAAA,EAAY,CAAC,WAAA,CAAY,SAAS,CAAC,CAAA,EAAG,WAAA,EAAa,CAAA;AAC5E,IAAA,OAAO,KAAK,WAAW,CAAA;AAAA,EACzB,CAAC,CAAA;AACH","file":"chunk-UDBN4RRV.js","sourcesContent":["import type { PostgresStoreState } from './postgres-store-state.js';\n\n/** What {@link budgetSettingsSql} sets and how long the settings last. */\nexport type BudgetSettings = {\n /** The value as SQL: a parameter (`$1`) or a quoted literal (`'5000ms'`). */\n readonly value: string;\n /**\n * Also `idle_in_transaction_session_timeout` and, on PostgreSQL 17+, `transaction_timeout`: the\n * server then ends the session of an overrun, so only executors that survive that get them\n * ({@link PostgresExecutor.sessionTimeouts}).\n */\n readonly sessionTimeouts: boolean;\n /** `transaction`: `SET LOCAL` semantics, gone with the transaction. `session`: kept by the connection. */\n readonly scope: 'transaction' | 'session';\n};\n\n/** The statement that holds a transaction to a time budget: `statement_timeout` and, see {@link BudgetSettings}, more. */\nexport function budgetSettingsSql({ value, sessionTimeouts, scope }: BudgetSettings): string {\n const local = scope === 'transaction';\n const statementTimeout = `SELECT set_config('statement_timeout', ${value}, ${local})`;\n if (!sessionTimeouts) {\n return statementTimeout;\n }\n return (\n `${statementTimeout}, set_config('idle_in_transaction_session_timeout', ${value}, ${local}), ` +\n \"CASE WHEN current_setting('server_version_num')::int >= 170000 \" +\n `THEN set_config('transaction_timeout', ${value}, ${local}) END`\n );\n}\n\n/** Puts the settings {@link budgetSettingsSql} keeps at session level back to the defaults of the session. */\nexport const SESSION_DEFAULTS_SQL =\n 'SELECT set_config(name, reset_val, false) FROM pg_settings ' +\n \"WHERE name IN ('statement_timeout', 'idle_in_transaction_session_timeout', 'transaction_timeout')\";\n\n/** The budget as a setting value: whole milliseconds, so nothing but digits reaches SQL text. */\nexport function budgetValue(timeoutMs: number): string {\n return `${Math.ceil(timeoutMs)}ms`;\n}\n\n/**\n * Runs `work` in a transaction of `state`'s executor that the server holds to `timeoutMs`. An executor\n * with `transactionWithBudget` (`PgExecutor`) sets the budget in the round trip of its `BEGIN`; any\n * other executor runs it with `SET LOCAL` semantics as the first statement of the transaction.\n */\nexport function inBudgetedTransaction<TTransaction, T>(\n state: PostgresStoreState<TTransaction>,\n {\n work,\n timeoutMs,\n }: {\n readonly work: (transaction: TTransaction) => Promise<T>;\n readonly timeoutMs: number;\n },\n): Promise<T> {\n const { executor } = state;\n if (executor.transactionWithBudget !== undefined) {\n return executor.transactionWithBudget(work, timeoutMs);\n }\n const sql = budgetSettingsSql({\n value: '$1',\n sessionTimeouts: executor.sessionTimeouts === true,\n scope: 'transaction',\n });\n return executor.transaction(async (transaction) => {\n await state.query({ sql, parameters: [budgetValue(timeoutMs)], transaction });\n return work(transaction);\n });\n}\n"]}
package/dist/drizzle.cjs CHANGED
@@ -2,10 +2,34 @@
2
2
 
3
3
  var drizzleOrm = require('drizzle-orm');
4
4
 
5
+ // src/transaction-timeouts.ts
6
+ function budgetSettingsSql({ value, sessionTimeouts, scope }) {
7
+ const local = scope === "transaction";
8
+ const statementTimeout = `SELECT set_config('statement_timeout', ${value}, ${local})`;
9
+ {
10
+ return statementTimeout;
11
+ }
12
+ }
13
+ function budgetValue(timeoutMs) {
14
+ return `${Math.ceil(timeoutMs)}ms`;
15
+ }
16
+
5
17
  // src/adapter-executor.ts
6
18
  function adapterExecutor(client) {
7
19
  return {
8
20
  transaction: client.transaction,
21
+ ...client.transactionAt === void 0 ? {} : {
22
+ // The level is set by the library at BEGIN, so the budget statement can follow as the first query.
23
+ transactionWithIsolation: (work, { isolationLevel, timeoutMs }) => client.transactionAt(isolationLevel, async (queryable) => {
24
+ const sql = budgetSettingsSql({
25
+ value: "$1",
26
+ sessionTimeouts: false,
27
+ scope: "transaction"
28
+ });
29
+ await client.run(queryable, { sql, parameters: [budgetValue(timeoutMs)] });
30
+ return work(queryable);
31
+ })
32
+ },
9
33
  // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
10
34
  query: (sql, parameters, transaction) => client.run(transaction ?? client.root, { sql, parameters })
11
35
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/adapter-executor.ts","../src/positional-sql.ts","../src/drizzle/drizzle-executor.ts"],"names":["drizzleSql"],"mappings":";;;;;AAqBO,SAAS,gBACd,MAAA,EAC8B;AAC9B,EAAA,OAAO;AAAA,IACL,aAAa,MAAA,CAAO,WAAA;AAAA;AAAA,IAEpB,KAAA,EAAO,CAAC,GAAA,EAAK,UAAA,EAAY,WAAA,KACvB,MAAA,CAAO,GAAA,CAAI,WAAA,IAAe,MAAA,CAAO,IAAA,EAAM,EAAE,GAAA,EAAK,YAAY;AAAA,GAC9D;AACF;;;ACrBO,SAAS,mBAAmB,GAAA,EAAkC;AACnE,EAAA,MAAM,QAA6B,EAAC;AACpC,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,KAAA,MAAW,KAAA,IAAS,GAAA,CAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AAC5C,IAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,MAAM,KAAK,CAAA,EAAG,EAAE,cAAA,EAAgB,OAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,GAAG,CAAA;AACtF,IAAA,SAAA,GAAY,KAAA,CAAM,KAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,CAAE,MAAA;AAAA,EACrC;AACA,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAS,CAAC,CAAA;AAC/B,EAAA,OAAO,KAAA;AACT;;;ACCO,SAAS,sBAAsB,QAAA,EAA2D;AAC/F,EAAA,OAAO,eAAA,CAAkC;AAAA,IACvC,IAAA,EAAM,QAAA;AAAA,IACN,WAAA,EAAa,CAAC,IAAA,KAAS,QAAA,CAAS,YAAY,IAAI,CAAA;AAAA,IAChD,MAAM,GAAA,CAAI,SAAA,EAAW,EAAE,GAAA,EAAK,YAAW,EAAG;AACxC,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,GAAG,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,IAAA,KAC1C,OAAO,IAAA,KAAS,WACZA,cAAA,CAAW,GAAA,CAAI,IAAI,CAAA,GACnBA,cAAA,CAAW,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,cAAc,CAAC;AAAA,OACtD;AACA,MAAA,MAAM,SAAS,MAAM,SAAA,CAAU,QAAQA,cAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AAC9D,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAAA,GACD,CAAA;AACH","file":"drizzle.cjs","sourcesContent":["import type { PostgresExecutor, PostgresParameter, PostgresRow } from './executor.js';\n\n/** One statement the store sends, with its positional parameters. */\nexport type AdapterStatement = {\n readonly sql: string;\n readonly parameters: readonly PostgresParameter[];\n};\n\n/** What an adapter supplies: where statements run outside a transaction, and how it runs one. */\nexport type AdapterClient<TQueryable> = {\n /** The instance itself: statements outside a transaction run on it. */\n readonly root: TQueryable;\n readonly transaction: PostgresExecutor<TQueryable>['transaction'];\n /** Runs one statement on the caller's transaction, else on the root. */\n readonly run: (queryable: TQueryable, statement: AdapterStatement) => Promise<PostgresRow[]>;\n};\n\n/**\n * The executor every library adapter shares: a statement runs on the transaction it is given,\n * else on the root instance. An adapter supplies only how its library runs one statement.\n */\nexport function adapterExecutor<TQueryable>(\n client: AdapterClient<TQueryable>,\n): PostgresExecutor<TQueryable> {\n return {\n transaction: client.transaction,\n // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature\n query: (sql, parameters, transaction) =>\n client.run(transaction ?? client.root, { sql, parameters }),\n };\n}\n","/** A piece of SQL text, or a reference to the (zero-based) parameter that stands at this position. */\nexport type PositionalSqlPart = string | { readonly parameterIndex: number };\n\n/**\n * Splits SQL written with PostgreSQL `$1, $2, ...` placeholders into text and\n * parameter references, in order of appearance. A placeholder used twice\n * appears twice. Adapters for libraries with their own placeholder syntax\n * (Knex `?`, Drizzle parameter chunks) rebuild the statement from the parts.\n */\nexport function splitPositionalSql(sql: string): PositionalSqlPart[] {\n const parts: PositionalSqlPart[] = [];\n let textStart = 0;\n for (const match of sql.matchAll(/\\$(\\d+)/g)) {\n parts.push(sql.slice(textStart, match.index), { parameterIndex: Number(match[1]) - 1 });\n textStart = match.index + match[0].length;\n }\n parts.push(sql.slice(textStart));\n return parts;\n}\n","import { adapterExecutor } from '../adapter-executor.js';\nimport { sql as drizzleSql, type SQL } from 'drizzle-orm';\nimport type { PostgresExecutor } from '../executor.js';\nimport { splitPositionalSql } from '../positional-sql.js';\n\n/** What a Drizzle database and a Drizzle transaction have in common. */\nexport interface DrizzleQueryable {\n execute(query: SQL): PromiseLike<{ readonly rows: Record<string, unknown>[] }>;\n}\n\n/** The part of a Drizzle `node-postgres` database the executor uses. */\nexport interface DrizzleLike extends DrizzleQueryable {\n transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;\n}\n\n/**\n * Executor for Drizzle with the `node-postgres` driver. The transaction handle\n * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.\n */\nexport function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable> {\n return adapterExecutor<DrizzleQueryable>({\n root: database,\n transaction: (work) => database.transaction(work),\n async run(queryable, { sql, parameters }) {\n const chunks = splitPositionalSql(sql).map((part) =>\n typeof part === 'string'\n ? drizzleSql.raw(part)\n : drizzleSql.param(parameters[part.parameterIndex]),\n );\n const result = await queryable.execute(drizzleSql.join(chunks));\n return result.rows;\n },\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/transaction-timeouts.ts","../src/adapter-executor.ts","../src/positional-sql.ts","../src/drizzle/drizzle-executor.ts"],"names":["drizzleSql"],"mappings":";;;;;AAiBO,SAAS,iBAAA,CAAkB,EAAE,KAAA,EAAO,eAAA,EAAiB,OAAM,EAA2B;AAC3F,EAAA,MAAM,QAAQ,KAAA,KAAU,aAAA;AACxB,EAAA,MAAM,gBAAA,GAAmB,CAAA,uCAAA,EAA0C,KAAK,CAAA,EAAA,EAAK,KAAK,CAAA,CAAA,CAAA;AAClF,EAAsB;AACpB,IAAA,OAAO,gBAAA;AAAA,EACT;AAMF;AAQO,SAAS,YAAY,SAAA,EAA2B;AACrD,EAAA,OAAO,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,SAAS,CAAC,CAAA,EAAA,CAAA;AAChC;;;ACNO,SAAS,gBACd,MAAA,EAC8B;AAC9B,EAAA,OAAO;AAAA,IACL,aAAa,MAAA,CAAO,WAAA;AAAA,IACpB,GAAI,MAAA,CAAO,aAAA,KAAkB,MAAA,GACzB,EAAC,GACD;AAAA;AAAA,MAEE,wBAAA,EAA0B,CAAC,IAAA,EAAM,EAAE,cAAA,EAAgB,SAAA,EAAU,KAC3D,MAAA,CAAO,aAAA,CAAe,cAAA,EAAgB,OAAO,SAAA,KAAc;AACzD,QAAA,MAAM,MAAM,iBAAA,CAAkB;AAAA,UAC5B,KAAA,EAAO,IAAA;AAAA,UACP,eAAA,EAAiB,KAAA;AAAA,UACjB,KAAA,EAAO;AAAA,SACR,CAAA;AACD,QAAA,MAAM,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,EAAE,GAAA,EAAK,UAAA,EAAY,CAAC,WAAA,CAAY,SAAS,CAAC,CAAA,EAAG,CAAA;AACzE,QAAA,OAAO,KAAK,SAAS,CAAA;AAAA,MACvB,CAAC;AAAA,KACL;AAAA;AAAA,IAEJ,KAAA,EAAO,CAAC,GAAA,EAAK,UAAA,EAAY,WAAA,KACvB,MAAA,CAAO,GAAA,CAAI,WAAA,IAAe,MAAA,CAAO,IAAA,EAAM,EAAE,GAAA,EAAK,YAAY;AAAA,GAC9D;AACF;;;AC/CO,SAAS,mBAAmB,GAAA,EAAkC;AACnE,EAAA,MAAM,QAA6B,EAAC;AACpC,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,KAAA,MAAW,KAAA,IAAS,GAAA,CAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AAC5C,IAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,MAAM,KAAK,CAAA,EAAG,EAAE,cAAA,EAAgB,OAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,GAAG,CAAA;AACtF,IAAA,SAAA,GAAY,KAAA,CAAM,KAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,CAAE,MAAA;AAAA,EACrC;AACA,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAS,CAAC,CAAA;AAC/B,EAAA,OAAO,KAAA;AACT;;;ACCO,SAAS,sBAAsB,QAAA,EAA2D;AAC/F,EAAA,OAAO,eAAA,CAAkC;AAAA,IACvC,IAAA,EAAM,QAAA;AAAA,IACN,WAAA,EAAa,CAAC,IAAA,KAAS,QAAA,CAAS,YAAY,IAAI,CAAA;AAAA,IAChD,MAAM,GAAA,CAAI,SAAA,EAAW,EAAE,GAAA,EAAK,YAAW,EAAG;AACxC,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,GAAG,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,IAAA,KAC1C,OAAO,IAAA,KAAS,WACZA,cAAA,CAAW,GAAA,CAAI,IAAI,CAAA,GACnBA,cAAA,CAAW,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,cAAc,CAAC;AAAA,OACtD;AACA,MAAA,MAAM,SAAS,MAAM,SAAA,CAAU,QAAQA,cAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AAC9D,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAAA,GACD,CAAA;AACH","file":"drizzle.cjs","sourcesContent":["import type { PostgresStoreState } from './postgres-store-state.js';\n\n/** What {@link budgetSettingsSql} sets and how long the settings last. */\nexport type BudgetSettings = {\n /** The value as SQL: a parameter (`$1`) or a quoted literal (`'5000ms'`). */\n readonly value: string;\n /**\n * Also `idle_in_transaction_session_timeout` and, on PostgreSQL 17+, `transaction_timeout`: the\n * server then ends the session of an overrun, so only executors that survive that get them\n * ({@link PostgresExecutor.sessionTimeouts}).\n */\n readonly sessionTimeouts: boolean;\n /** `transaction`: `SET LOCAL` semantics, gone with the transaction. `session`: kept by the connection. */\n readonly scope: 'transaction' | 'session';\n};\n\n/** The statement that holds a transaction to a time budget: `statement_timeout` and, see {@link BudgetSettings}, more. */\nexport function budgetSettingsSql({ value, sessionTimeouts, scope }: BudgetSettings): string {\n const local = scope === 'transaction';\n const statementTimeout = `SELECT set_config('statement_timeout', ${value}, ${local})`;\n if (!sessionTimeouts) {\n return statementTimeout;\n }\n return (\n `${statementTimeout}, set_config('idle_in_transaction_session_timeout', ${value}, ${local}), ` +\n \"CASE WHEN current_setting('server_version_num')::int >= 170000 \" +\n `THEN set_config('transaction_timeout', ${value}, ${local}) END`\n );\n}\n\n/** Puts the settings {@link budgetSettingsSql} keeps at session level back to the defaults of the session. */\nexport const SESSION_DEFAULTS_SQL =\n 'SELECT set_config(name, reset_val, false) FROM pg_settings ' +\n \"WHERE name IN ('statement_timeout', 'idle_in_transaction_session_timeout', 'transaction_timeout')\";\n\n/** The budget as a setting value: whole milliseconds, so nothing but digits reaches SQL text. */\nexport function budgetValue(timeoutMs: number): string {\n return `${Math.ceil(timeoutMs)}ms`;\n}\n\n/**\n * Runs `work` in a transaction of `state`'s executor that the server holds to `timeoutMs`. An executor\n * with `transactionWithBudget` (`PgExecutor`) sets the budget in the round trip of its `BEGIN`; any\n * other executor runs it with `SET LOCAL` semantics as the first statement of the transaction.\n */\nexport function inBudgetedTransaction<TTransaction, T>(\n state: PostgresStoreState<TTransaction>,\n {\n work,\n timeoutMs,\n }: {\n readonly work: (transaction: TTransaction) => Promise<T>;\n readonly timeoutMs: number;\n },\n): Promise<T> {\n const { executor } = state;\n if (executor.transactionWithBudget !== undefined) {\n return executor.transactionWithBudget(work, timeoutMs);\n }\n const sql = budgetSettingsSql({\n value: '$1',\n sessionTimeouts: executor.sessionTimeouts === true,\n scope: 'transaction',\n });\n return executor.transaction(async (transaction) => {\n await state.query({ sql, parameters: [budgetValue(timeoutMs)], transaction });\n return work(transaction);\n });\n}\n","import type { TransactionIsolationLevel } from '@relaybus/core';\nimport type { PostgresExecutor, PostgresParameter, PostgresRow } from './executor.js';\nimport { budgetSettingsSql, budgetValue } from './transaction-timeouts.js';\n\n/** One statement the store sends, with its positional parameters. */\nexport type AdapterStatement = {\n readonly sql: string;\n readonly parameters: readonly PostgresParameter[];\n};\n\n/** What an adapter supplies: where statements run outside a transaction, and how it runs one. */\nexport type AdapterClient<TQueryable> = {\n /** The instance itself: statements outside a transaction run on it. */\n readonly root: TQueryable;\n readonly transaction: PostgresExecutor<TQueryable>['transaction'];\n /**\n * Optional. Opens a transaction at an isolation level through the library's own API, for a library\n * that runs code of its own (hooks, subscribers) right after it begins a transaction: that code\n * would otherwise run a query before the store's `SET TRANSACTION ISOLATION LEVEL`.\n */\n readonly transactionAt?: <T>(\n isolationLevel: TransactionIsolationLevel,\n work: (queryable: TQueryable) => Promise<T>,\n ) => Promise<T>;\n /** Runs one statement on the caller's transaction, else on the root. */\n readonly run: (queryable: TQueryable, statement: AdapterStatement) => Promise<PostgresRow[]>;\n};\n\n/**\n * The executor every library adapter shares: a statement runs on the transaction it is given,\n * else on the root instance. An adapter supplies only how its library runs one statement.\n */\nexport function adapterExecutor<TQueryable>(\n client: AdapterClient<TQueryable>,\n): PostgresExecutor<TQueryable> {\n return {\n transaction: client.transaction,\n ...(client.transactionAt === undefined\n ? {}\n : {\n // The level is set by the library at BEGIN, so the budget statement can follow as the first query.\n transactionWithIsolation: (work, { isolationLevel, timeoutMs }) =>\n client.transactionAt!(isolationLevel, async (queryable) => {\n const sql = budgetSettingsSql({\n value: '$1',\n sessionTimeouts: false,\n scope: 'transaction',\n });\n await client.run(queryable, { sql, parameters: [budgetValue(timeoutMs)] });\n return work(queryable);\n }),\n }),\n // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature\n query: (sql, parameters, transaction) =>\n client.run(transaction ?? client.root, { sql, parameters }),\n };\n}\n","/** A piece of SQL text, or a reference to the (zero-based) parameter that stands at this position. */\nexport type PositionalSqlPart = string | { readonly parameterIndex: number };\n\n/**\n * Splits SQL written with PostgreSQL `$1, $2, ...` placeholders into text and\n * parameter references, in order of appearance. A placeholder used twice\n * appears twice. Adapters for libraries with their own placeholder syntax\n * (Knex `?`, Drizzle parameter chunks) rebuild the statement from the parts.\n */\nexport function splitPositionalSql(sql: string): PositionalSqlPart[] {\n const parts: PositionalSqlPart[] = [];\n let textStart = 0;\n for (const match of sql.matchAll(/\\$(\\d+)/g)) {\n parts.push(sql.slice(textStart, match.index), { parameterIndex: Number(match[1]) - 1 });\n textStart = match.index + match[0].length;\n }\n parts.push(sql.slice(textStart));\n return parts;\n}\n","import { adapterExecutor } from '../adapter-executor.js';\nimport { sql as drizzleSql, type SQL } from 'drizzle-orm';\nimport type { PostgresExecutor } from '../executor.js';\nimport { splitPositionalSql } from '../positional-sql.js';\n\n/** What a Drizzle database and a Drizzle transaction have in common. */\nexport interface DrizzleQueryable {\n execute(query: SQL): PromiseLike<{ readonly rows: Record<string, unknown>[] }>;\n}\n\n/** The part of a Drizzle `node-postgres` database the executor uses. */\nexport interface DrizzleLike extends DrizzleQueryable {\n transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;\n}\n\n/**\n * Executor for Drizzle with the `node-postgres` driver. The transaction handle\n * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.\n */\nexport function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable> {\n return adapterExecutor<DrizzleQueryable>({\n root: database,\n transaction: (work) => database.transaction(work),\n async run(queryable, { sql, parameters }) {\n const chunks = splitPositionalSql(sql).map((part) =>\n typeof part === 'string'\n ? drizzleSql.raw(part)\n : drizzleSql.param(parameters[part.parameterIndex]),\n );\n const result = await queryable.execute(drizzleSql.join(chunks));\n return result.rows;\n },\n });\n}\n"]}
@@ -1,5 +1,6 @@
1
1
  import { SQL } from 'drizzle-orm';
2
2
  import { PostgresExecutor } from './index.cjs';
3
+ import '@relaybus/core';
3
4
 
4
5
  /** What a Drizzle database and a Drizzle transaction have in common. */
5
6
  interface DrizzleQueryable {
package/dist/drizzle.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { SQL } from 'drizzle-orm';
2
2
  import { PostgresExecutor } from './index.js';
3
+ import '@relaybus/core';
3
4
 
4
5
  /** What a Drizzle database and a Drizzle transaction have in common. */
5
6
  interface DrizzleQueryable {
package/dist/drizzle.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { splitPositionalSql } from './chunk-UUFSUL2M.js';
2
- import { adapterExecutor } from './chunk-NNZJR5AB.js';
2
+ import { adapterExecutor } from './chunk-5WWPAN5Q.js';
3
+ import './chunk-UDBN4RRV.js';
3
4
  import { sql } from 'drizzle-orm';
4
5
 
5
6
  function createDrizzleExecutor(database) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/drizzle/drizzle-executor.ts"],"names":["sql","drizzleSql"],"mappings":";;;;AAmBO,SAAS,sBAAsB,QAAA,EAA2D;AAC/F,EAAA,OAAO,eAAA,CAAkC;AAAA,IACvC,IAAA,EAAM,QAAA;AAAA,IACN,WAAA,EAAa,CAAC,IAAA,KAAS,QAAA,CAAS,YAAY,IAAI,CAAA;AAAA,IAChD,MAAM,GAAA,CAAI,SAAA,EAAW,OAAEA,KAAA,EAAK,YAAW,EAAG;AACxC,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmBA,KAAG,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,IAAA,KAC1C,OAAO,IAAA,KAAS,WACZC,GAAA,CAAW,GAAA,CAAI,IAAI,CAAA,GACnBA,GAAA,CAAW,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,cAAc,CAAC;AAAA,OACtD;AACA,MAAA,MAAM,SAAS,MAAM,SAAA,CAAU,QAAQA,GAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AAC9D,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAAA,GACD,CAAA;AACH","file":"drizzle.js","sourcesContent":["import { adapterExecutor } from '../adapter-executor.js';\nimport { sql as drizzleSql, type SQL } from 'drizzle-orm';\nimport type { PostgresExecutor } from '../executor.js';\nimport { splitPositionalSql } from '../positional-sql.js';\n\n/** What a Drizzle database and a Drizzle transaction have in common. */\nexport interface DrizzleQueryable {\n execute(query: SQL): PromiseLike<{ readonly rows: Record<string, unknown>[] }>;\n}\n\n/** The part of a Drizzle `node-postgres` database the executor uses. */\nexport interface DrizzleLike extends DrizzleQueryable {\n transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;\n}\n\n/**\n * Executor for Drizzle with the `node-postgres` driver. The transaction handle\n * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.\n */\nexport function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable> {\n return adapterExecutor<DrizzleQueryable>({\n root: database,\n transaction: (work) => database.transaction(work),\n async run(queryable, { sql, parameters }) {\n const chunks = splitPositionalSql(sql).map((part) =>\n typeof part === 'string'\n ? drizzleSql.raw(part)\n : drizzleSql.param(parameters[part.parameterIndex]),\n );\n const result = await queryable.execute(drizzleSql.join(chunks));\n return result.rows;\n },\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/drizzle/drizzle-executor.ts"],"names":["sql","drizzleSql"],"mappings":";;;;;AAmBO,SAAS,sBAAsB,QAAA,EAA2D;AAC/F,EAAA,OAAO,eAAA,CAAkC;AAAA,IACvC,IAAA,EAAM,QAAA;AAAA,IACN,WAAA,EAAa,CAAC,IAAA,KAAS,QAAA,CAAS,YAAY,IAAI,CAAA;AAAA,IAChD,MAAM,GAAA,CAAI,SAAA,EAAW,OAAEA,KAAA,EAAK,YAAW,EAAG;AACxC,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmBA,KAAG,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,IAAA,KAC1C,OAAO,IAAA,KAAS,WACZC,GAAA,CAAW,GAAA,CAAI,IAAI,CAAA,GACnBA,GAAA,CAAW,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,cAAc,CAAC;AAAA,OACtD;AACA,MAAA,MAAM,SAAS,MAAM,SAAA,CAAU,QAAQA,GAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AAC9D,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAAA,GACD,CAAA;AACH","file":"drizzle.js","sourcesContent":["import { adapterExecutor } from '../adapter-executor.js';\nimport { sql as drizzleSql, type SQL } from 'drizzle-orm';\nimport type { PostgresExecutor } from '../executor.js';\nimport { splitPositionalSql } from '../positional-sql.js';\n\n/** What a Drizzle database and a Drizzle transaction have in common. */\nexport interface DrizzleQueryable {\n execute(query: SQL): PromiseLike<{ readonly rows: Record<string, unknown>[] }>;\n}\n\n/** The part of a Drizzle `node-postgres` database the executor uses. */\nexport interface DrizzleLike extends DrizzleQueryable {\n transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;\n}\n\n/**\n * Executor for Drizzle with the `node-postgres` driver. The transaction handle\n * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.\n */\nexport function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable> {\n return adapterExecutor<DrizzleQueryable>({\n root: database,\n transaction: (work) => database.transaction(work),\n async run(queryable, { sql, parameters }) {\n const chunks = splitPositionalSql(sql).map((part) =>\n typeof part === 'string'\n ? drizzleSql.raw(part)\n : drizzleSql.param(parameters[part.parameterIndex]),\n );\n const result = await queryable.execute(drizzleSql.join(chunks));\n return result.rows;\n },\n });\n}\n"]}
@@ -1,3 +1,5 @@
1
+ import { TransactionIsolationLevel } from '@relaybus/core';
2
+
1
3
  /** Values the store binds: always text, numbers or null (documents travel as JSON text). */
2
4
  type PostgresParameter = string | number | null;
3
5
  type PostgresRow = Readonly<Record<string, unknown>>;
@@ -45,7 +47,28 @@ interface PostgresExecutor<TTransaction> {
45
47
  * its own, one round trip more per `bus.transaction`. `PgExecutor` implements it.
46
48
  */
47
49
  transactionWithBudget?<T>(work: (transaction: TTransaction) => Promise<T>, timeoutMs: number): Promise<T>;
50
+ /**
51
+ * Optional. Opens a transaction at `isolationLevel` that the server holds to `timeoutMs`, like
52
+ * {@link PostgresExecutor.transactionWithBudget}, with the level set in the same round trip as its
53
+ * `BEGIN` (`BEGIN ISOLATION LEVEL ...`). Without it the store sends `SET TRANSACTION ISOLATION
54
+ * LEVEL ...` as the first statement of {@link PostgresExecutor.transaction}, then the budget:
55
+ * two round trips more, on the transactions that ask for a level only. `PgExecutor` implements it.
56
+ */
57
+ transactionWithIsolation?<T>(work: (transaction: TTransaction) => Promise<T>, options: PostgresIsolatedTransactionOptions): Promise<T>;
58
+ /**
59
+ * Optional. True when `error` is a serialization failure (SQLSTATE `40001`) of this library, for
60
+ * errors the store does not recognize itself. The store already finds `40001` in `code`, in
61
+ * `cause` and `driverError` chains, and in Prisma's `P2034` / `meta.code`; implement it only for
62
+ * a library that wraps the error in another shape.
63
+ */
64
+ isSerializationFailure?(error: unknown): boolean;
48
65
  query(sql: string, parameters: readonly PostgresParameter[], transaction?: TTransaction, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
49
66
  }
67
+ /** What {@link PostgresExecutor.transactionWithIsolation} opens a transaction with. */
68
+ interface PostgresIsolatedTransactionOptions {
69
+ readonly isolationLevel: TransactionIsolationLevel;
70
+ /** The time budget, as for {@link PostgresExecutor.transactionWithBudget}. */
71
+ readonly timeoutMs: number;
72
+ }
50
73
 
51
- export type { PostgresExecutor as P, PostgresParameter as a, PostgresQueryOptions as b, PostgresRow as c };
74
+ export type { PostgresExecutor as P, PostgresIsolatedTransactionOptions as a, PostgresParameter as b, PostgresQueryOptions as c, PostgresRow as d };
@@ -1,3 +1,5 @@
1
+ import { TransactionIsolationLevel } from '@relaybus/core';
2
+
1
3
  /** Values the store binds: always text, numbers or null (documents travel as JSON text). */
2
4
  type PostgresParameter = string | number | null;
3
5
  type PostgresRow = Readonly<Record<string, unknown>>;
@@ -45,7 +47,28 @@ interface PostgresExecutor<TTransaction> {
45
47
  * its own, one round trip more per `bus.transaction`. `PgExecutor` implements it.
46
48
  */
47
49
  transactionWithBudget?<T>(work: (transaction: TTransaction) => Promise<T>, timeoutMs: number): Promise<T>;
50
+ /**
51
+ * Optional. Opens a transaction at `isolationLevel` that the server holds to `timeoutMs`, like
52
+ * {@link PostgresExecutor.transactionWithBudget}, with the level set in the same round trip as its
53
+ * `BEGIN` (`BEGIN ISOLATION LEVEL ...`). Without it the store sends `SET TRANSACTION ISOLATION
54
+ * LEVEL ...` as the first statement of {@link PostgresExecutor.transaction}, then the budget:
55
+ * two round trips more, on the transactions that ask for a level only. `PgExecutor` implements it.
56
+ */
57
+ transactionWithIsolation?<T>(work: (transaction: TTransaction) => Promise<T>, options: PostgresIsolatedTransactionOptions): Promise<T>;
58
+ /**
59
+ * Optional. True when `error` is a serialization failure (SQLSTATE `40001`) of this library, for
60
+ * errors the store does not recognize itself. The store already finds `40001` in `code`, in
61
+ * `cause` and `driverError` chains, and in Prisma's `P2034` / `meta.code`; implement it only for
62
+ * a library that wraps the error in another shape.
63
+ */
64
+ isSerializationFailure?(error: unknown): boolean;
48
65
  query(sql: string, parameters: readonly PostgresParameter[], transaction?: TTransaction, options?: PostgresQueryOptions): Promise<readonly PostgresRow[]>;
49
66
  }
67
+ /** What {@link PostgresExecutor.transactionWithIsolation} opens a transaction with. */
68
+ interface PostgresIsolatedTransactionOptions {
69
+ readonly isolationLevel: TransactionIsolationLevel;
70
+ /** The time budget, as for {@link PostgresExecutor.transactionWithBudget}. */
71
+ readonly timeoutMs: number;
72
+ }
50
73
 
51
- export type { PostgresExecutor as P, PostgresParameter as a, PostgresQueryOptions as b, PostgresRow as c };
74
+ export type { PostgresExecutor as P, PostgresIsolatedTransactionOptions as a, PostgresParameter as b, PostgresQueryOptions as c, PostgresRow as d };