@relaybus/postgres 0.4.1 → 1.0.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/chunk-5WWPAN5Q.js +26 -0
- package/dist/chunk-5WWPAN5Q.js.map +1 -0
- package/dist/chunk-FY65AUUF.js +40 -0
- package/dist/chunk-FY65AUUF.js.map +1 -0
- package/dist/chunk-UDBN4RRV.js +35 -0
- package/dist/chunk-UDBN4RRV.js.map +1 -0
- package/dist/drizzle.cjs +24 -0
- package/dist/drizzle.cjs.map +1 -1
- package/dist/drizzle.d.cts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.js +2 -1
- package/dist/drizzle.js.map +1 -1
- package/dist/{executor-CfJgktC6.d.cts → executor-Bt90w33Q.d.cts} +24 -1
- package/dist/{executor-CfJgktC6.d.ts → executor-Bt90w33Q.d.ts} +24 -1
- package/dist/index.cjs +103 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +71 -41
- package/dist/index.js.map +1 -1
- package/dist/knex.cjs +24 -0
- package/dist/knex.cjs.map +1 -1
- package/dist/knex.d.cts +1 -0
- package/dist/knex.d.ts +1 -0
- package/dist/knex.js +2 -1
- package/dist/knex.js.map +1 -1
- package/dist/kysely.cjs +24 -0
- package/dist/kysely.cjs.map +1 -1
- package/dist/kysely.d.cts +1 -0
- package/dist/kysely.d.ts +1 -0
- package/dist/kysely.js +2 -1
- package/dist/kysely.js.map +1 -1
- package/dist/prisma.cjs +24 -0
- package/dist/prisma.cjs.map +1 -1
- package/dist/prisma.d.cts +1 -0
- package/dist/prisma.d.ts +1 -0
- package/dist/prisma.js +2 -1
- package/dist/prisma.js.map +1 -1
- package/dist/typeorm.cjs +46 -0
- package/dist/typeorm.cjs.map +1 -1
- package/dist/typeorm.d.cts +1 -0
- package/dist/typeorm.d.ts +1 -0
- package/dist/typeorm.js +11 -1
- package/dist/typeorm.js.map +1 -1
- package/package.json +4 -4
- package/dist/chunk-NNZJR5AB.js +0 -12
- 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
|
};
|
package/dist/drizzle.cjs.map
CHANGED
|
@@ -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"]}
|
package/dist/drizzle.d.cts
CHANGED
package/dist/drizzle.d.ts
CHANGED
package/dist/drizzle.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { splitPositionalSql } from './chunk-UUFSUL2M.js';
|
|
2
|
-
import { adapterExecutor } from './chunk-
|
|
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) {
|
package/dist/drizzle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/drizzle/drizzle-executor.ts"],"names":["sql","drizzleSql"],"mappings":"
|
|
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,
|
|
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,
|
|
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
|
|
123
|
+
return (client) => {
|
|
75
124
|
if (!this.#keepsSessionBudget()) {
|
|
76
|
-
return { begin:
|
|
125
|
+
return { begin: `${begin}; ${settings("transaction")}` };
|
|
77
126
|
}
|
|
78
|
-
return this.#sessionValues.get(client) === value ? { begin
|
|
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
|
|
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
|