@relaybus/postgres 0.4.1 → 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 +10 -0
  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 +103 -5
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +19 -3
  19. package/dist/index.d.ts +19 -3
  20. package/dist/index.js +71 -41
  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 +1 -0
  37. package/dist/prisma.d.ts +1 -0
  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 +4 -4
  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.
@@ -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 };
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) {
@@ -538,10 +587,38 @@ function withRootPool(executor, root) {
538
587
  ...executor.transactionWithBudget === void 0 ? {} : {
539
588
  transactionWithBudget: (work, timeoutMs) => executor.transactionWithBudget(work, timeoutMs)
540
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) },
541
594
  // eslint-disable-next-line relaybus/max-two-params -- PostgresExecutor port signature
542
595
  query: (sql, parameters, transaction, options) => transaction === void 0 ? root.query(sql, parameters, void 0, options) : executor.query(sql, parameters, transaction, options)
543
596
  };
544
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
+ }
545
622
  function text(row, column) {
546
623
  return row[column];
547
624
  }
@@ -3046,6 +3123,16 @@ var PostgresEventStore = class {
3046
3123
  async setup() {
3047
3124
  await this.#setup.run();
3048
3125
  }
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;
3135
+ }
3049
3136
  /**
3050
3137
  * Runs `work` in a transaction of the executor. With `options.timeoutMs` (from `bus.transaction`)
3051
3138
  * the server holds the transaction to the budget: `statement_timeout`, and for executors with
@@ -3053,6 +3140,9 @@ var PostgresEventStore = class {
3053
3140
  * `transaction_timeout`, all scoped to the transaction. An executor with `transactionWithBudget`
3054
3141
  * (`PgExecutor`) sets them with `BEGIN`, so the budget costs no round trip; on a pool the store
3055
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.
3056
3146
  */
3057
3147
  async transaction(work, options) {
3058
3148
  const own = { wake: false };
@@ -3064,12 +3154,20 @@ var PostgresEventStore = class {
3064
3154
  this.#state.ownTransactions.delete(transaction);
3065
3155
  }
3066
3156
  };
3067
- 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);
3068
3158
  if (own.wake) {
3069
3159
  this.#appender.wakeAfterCommit();
3070
3160
  }
3071
3161
  return result;
3072
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
+ }
3073
3171
  /**
3074
3172
  * Writes the entries in `transaction`, normally with one statement
3075
3173
  * (`AppendStatements.insertFreshEvents`): keyed events reserve their sequence numbers (locking