turbine-orm 0.81.0 → 0.90.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.
- package/README.md +26 -25
- package/dist/checkout.d.ts +27 -2
- package/dist/checkout.js +75 -4
- package/dist/cjs/checkout.d.ts +27 -2
- package/dist/cjs/checkout.js +76 -3
- package/dist/cjs/cli/code-first-tags.d.ts +62 -0
- package/dist/cjs/cli/code-first-tags.js +124 -0
- package/dist/cjs/cli/compile-query.js +9 -0
- package/dist/cjs/cli/config.d.ts +35 -6
- package/dist/cjs/cli/config.js +17 -1
- package/dist/cjs/cli/destructive.d.ts +7 -4
- package/dist/cjs/cli/destructive.js +224 -20
- package/dist/cjs/cli/error-catalog.js +36 -0
- package/dist/cjs/cli/error-text.d.ts +15 -0
- package/dist/cjs/cli/error-text.js +40 -0
- package/dist/cjs/cli/index.d.ts +62 -4
- package/dist/cjs/cli/index.js +568 -108
- package/dist/cjs/cli/local-server-guard.d.ts +53 -0
- package/dist/cjs/cli/local-server-guard.js +119 -0
- package/dist/cjs/cli/local-server-output.d.ts +41 -0
- package/dist/cjs/cli/local-server-output.js +103 -0
- package/dist/cjs/cli/mcp.d.ts +8 -0
- package/dist/cjs/cli/mcp.js +121 -32
- package/dist/cjs/cli/migrate.d.ts +113 -5
- package/dist/cjs/cli/migrate.js +270 -21
- package/dist/cjs/cli/observe.d.ts +6 -1
- package/dist/cjs/cli/observe.js +85 -55
- package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cjs/cli/pii-predicate-guard.js +98 -37
- package/dist/cjs/cli/prisma-resolve.js +17 -13
- package/dist/cjs/cli/prisma-schema.js +12 -4
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.d.ts +14 -2
- package/dist/cjs/cli/studio.js +244 -69
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +558 -85
- package/dist/cjs/client.js +1270 -395
- package/dist/cjs/connection-guard.d.ts +13 -0
- package/dist/cjs/connection-guard.js +28 -0
- package/dist/cjs/connection-url.d.ts +51 -16
- package/dist/cjs/connection-url.js +67 -34
- package/dist/cjs/diagnostics.d.ts +81 -0
- package/dist/cjs/diagnostics.js +147 -0
- package/dist/cjs/dialect.d.ts +449 -20
- package/dist/cjs/dialect.js +117 -76
- package/dist/cjs/engine-config.d.ts +7 -6
- package/dist/cjs/errors.d.ts +126 -6
- package/dist/cjs/errors.js +256 -13
- package/dist/cjs/generate.d.ts +57 -7
- package/dist/cjs/generate.js +635 -279
- package/dist/cjs/global-filters-config.d.ts +59 -0
- package/dist/cjs/global-filters-config.js +367 -0
- package/dist/cjs/index.d.ts +9 -4
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/introspect.d.ts +31 -6
- package/dist/cjs/introspect.js +108 -50
- package/dist/cjs/log-codes.d.ts +12 -0
- package/dist/cjs/log-codes.js +77 -0
- package/dist/cjs/logger.d.ts +87 -0
- package/dist/cjs/logger.js +98 -0
- package/dist/cjs/mssql.d.ts +8 -5
- package/dist/cjs/mssql.js +352 -65
- package/dist/cjs/mysql.d.ts +3 -5
- package/dist/cjs/mysql.js +420 -70
- package/dist/cjs/nested-write.d.ts +3 -0
- package/dist/cjs/nested-write.js +179 -114
- package/dist/cjs/pipeline-submittable.d.ts +12 -0
- package/dist/cjs/pipeline-submittable.js +118 -44
- package/dist/cjs/pipeline.js +21 -26
- package/dist/cjs/powdb-introspect.d.ts +5 -2
- package/dist/cjs/powdb-introspect.js +20 -15
- package/dist/cjs/powdb-shared.d.ts +36 -5
- package/dist/cjs/powdb-shared.js +67 -8
- package/dist/cjs/powdb.d.ts +4 -0
- package/dist/cjs/powdb.js +183 -51
- package/dist/cjs/powql.d.ts +394 -51
- package/dist/cjs/powql.js +1829 -284
- package/dist/cjs/prisma-compat.d.ts +23 -15
- package/dist/cjs/prisma-compat.js +507 -372
- package/dist/cjs/query/aggregates.d.ts +17 -1
- package/dist/cjs/query/aggregates.js +261 -29
- package/dist/cjs/query/batched-loader.d.ts +25 -6
- package/dist/cjs/query/batched-loader.js +103 -20
- package/dist/cjs/query/builder.d.ts +325 -184
- package/dist/cjs/query/builder.js +1452 -490
- package/dist/cjs/query/column-ref.d.ts +61 -0
- package/dist/cjs/query/column-ref.js +73 -0
- package/dist/cjs/query/compound-unique.d.ts +21 -0
- package/dist/cjs/query/compound-unique.js +98 -18
- package/dist/cjs/query/deferred.d.ts +53 -16
- package/dist/cjs/query/expr.d.ts +217 -0
- package/dist/cjs/query/expr.js +859 -0
- package/dist/cjs/query/filters.d.ts +155 -53
- package/dist/cjs/query/filters.js +613 -76
- package/dist/cjs/query/index.d.ts +2 -0
- package/dist/cjs/query/index.js +7 -1
- package/dist/cjs/query/json-value.d.ts +43 -0
- package/dist/cjs/query/json-value.js +59 -0
- package/dist/cjs/query/normalize-args.d.ts +87 -0
- package/dist/cjs/query/normalize-args.js +527 -0
- package/dist/cjs/query/option-surface.d.ts +16 -4
- package/dist/cjs/query/option-surface.js +57 -14
- package/dist/cjs/query/relation-names.d.ts +14 -0
- package/dist/cjs/query/relation-names.js +77 -18
- package/dist/cjs/query/relations.d.ts +30 -3
- package/dist/cjs/query/relations.js +219 -63
- package/dist/cjs/query/row-lock.d.ts +49 -0
- package/dist/cjs/query/row-lock.js +138 -0
- package/dist/cjs/query/sql-comment.d.ts +161 -0
- package/dist/cjs/query/sql-comment.js +381 -0
- package/dist/cjs/query/stream-idle.d.ts +132 -0
- package/dist/cjs/query/stream-idle.js +212 -0
- package/dist/cjs/query/table-client.d.ts +101 -0
- package/dist/cjs/query/table-client.js +74 -0
- package/dist/cjs/query/types.d.ts +677 -298
- package/dist/cjs/query/utils.d.ts +105 -6
- package/dist/cjs/query/utils.js +426 -19
- package/dist/cjs/query/warn-registry.d.ts +18 -0
- package/dist/cjs/query/warn-registry.js +18 -0
- package/dist/cjs/query/where-compile.d.ts +138 -14
- package/dist/cjs/query/where-compile.js +513 -29
- package/dist/cjs/query/where.d.ts +159 -59
- package/dist/cjs/query/where.js +716 -246
- package/dist/cjs/query/writes.d.ts +112 -50
- package/dist/cjs/query/writes.js +1498 -200
- package/dist/cjs/query-events.d.ts +4 -2
- package/dist/cjs/query-events.js +3 -1
- package/dist/cjs/realtime.d.ts +7 -3
- package/dist/cjs/realtime.js +13 -2
- package/dist/cjs/schema-builder.d.ts +55 -4
- package/dist/cjs/schema-builder.js +382 -18
- package/dist/cjs/schema-metadata.d.ts +21 -1
- package/dist/cjs/schema-metadata.js +29 -7
- package/dist/cjs/schema-sql.d.ts +75 -5
- package/dist/cjs/schema-sql.js +497 -45
- package/dist/cjs/schema.d.ts +26 -5
- package/dist/cjs/schema.js +110 -13
- package/dist/cjs/sql-placeholders.d.ts +56 -0
- package/dist/cjs/sql-placeholders.js +204 -0
- package/dist/cjs/sqlite.d.ts +75 -20
- package/dist/cjs/sqlite.js +593 -95
- package/dist/cjs/supplied-pool-types.d.ts +48 -0
- package/dist/cjs/supplied-pool-types.js +256 -0
- package/dist/cjs/transaction-control.d.ts +51 -0
- package/dist/cjs/transaction-control.js +316 -0
- package/dist/cjs/transaction-state.d.ts +342 -0
- package/dist/cjs/transaction-state.js +707 -0
- package/dist/cjs/typed-sql.d.ts +37 -3
- package/dist/cjs/typed-sql.js +102 -8
- package/dist/cli/code-first-tags.d.ts +62 -0
- package/dist/cli/code-first-tags.js +120 -0
- package/dist/cli/compile-query.js +9 -0
- package/dist/cli/config.d.ts +35 -6
- package/dist/cli/config.js +16 -1
- package/dist/cli/destructive.d.ts +7 -4
- package/dist/cli/destructive.js +224 -20
- package/dist/cli/error-catalog.js +36 -0
- package/dist/cli/error-text.d.ts +15 -0
- package/dist/cli/error-text.js +37 -0
- package/dist/cli/index.d.ts +62 -4
- package/dist/cli/index.js +569 -112
- package/dist/cli/local-server-guard.d.ts +53 -0
- package/dist/cli/local-server-guard.js +113 -0
- package/dist/cli/local-server-output.d.ts +41 -0
- package/dist/cli/local-server-output.js +98 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +121 -34
- package/dist/cli/migrate.d.ts +113 -5
- package/dist/cli/migrate.js +261 -21
- package/dist/cli/observe.d.ts +6 -1
- package/dist/cli/observe.js +84 -54
- package/dist/cli/pii-predicate-guard.d.ts +43 -23
- package/dist/cli/pii-predicate-guard.js +100 -35
- package/dist/cli/prisma-resolve.js +15 -11
- package/dist/cli/prisma-schema.js +12 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +14 -2
- package/dist/cli/studio.js +246 -71
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +558 -85
- package/dist/client.js +1275 -400
- package/dist/connection-guard.d.ts +13 -0
- package/dist/connection-guard.js +27 -0
- package/dist/connection-url.d.ts +51 -16
- package/dist/connection-url.js +63 -34
- package/dist/diagnostics.d.ts +81 -0
- package/dist/diagnostics.js +144 -0
- package/dist/dialect.d.ts +449 -20
- package/dist/dialect.js +115 -43
- package/dist/engine-config.d.ts +7 -6
- package/dist/errors.d.ts +126 -6
- package/dist/errors.js +252 -12
- package/dist/generate.d.ts +57 -7
- package/dist/generate.js +635 -281
- package/dist/global-filters-config.d.ts +59 -0
- package/dist/global-filters-config.js +361 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +4 -2
- package/dist/introspect.d.ts +31 -6
- package/dist/introspect.js +106 -50
- package/dist/log-codes.d.ts +12 -0
- package/dist/log-codes.js +74 -0
- package/dist/logger.d.ts +87 -0
- package/dist/logger.js +93 -0
- package/dist/mssql.d.ts +8 -5
- package/dist/mssql.js +354 -67
- package/dist/mysql.d.ts +3 -5
- package/dist/mysql.js +423 -73
- package/dist/nested-write.d.ts +3 -0
- package/dist/nested-write.js +177 -112
- package/dist/pipeline-submittable.d.ts +12 -0
- package/dist/pipeline-submittable.js +117 -44
- package/dist/pipeline.js +21 -26
- package/dist/powdb-introspect.d.ts +5 -2
- package/dist/powdb-introspect.js +21 -16
- package/dist/powdb-shared.d.ts +36 -5
- package/dist/powdb-shared.js +65 -8
- package/dist/powdb.d.ts +4 -0
- package/dist/powdb.js +183 -51
- package/dist/powql.d.ts +394 -51
- package/dist/powql.js +1836 -291
- package/dist/prisma-compat.d.ts +23 -15
- package/dist/prisma-compat.js +508 -373
- package/dist/query/aggregates.d.ts +17 -1
- package/dist/query/aggregates.js +263 -32
- package/dist/query/batched-loader.d.ts +25 -6
- package/dist/query/batched-loader.js +101 -21
- package/dist/query/builder.d.ts +325 -184
- package/dist/query/builder.js +1458 -495
- package/dist/query/column-ref.d.ts +61 -0
- package/dist/query/column-ref.js +66 -0
- package/dist/query/compound-unique.d.ts +21 -0
- package/dist/query/compound-unique.js +97 -19
- package/dist/query/deferred.d.ts +53 -16
- package/dist/query/expr.d.ts +217 -0
- package/dist/query/expr.js +848 -0
- package/dist/query/filters.d.ts +155 -53
- package/dist/query/filters.js +597 -72
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +4 -0
- package/dist/query/json-value.d.ts +43 -0
- package/dist/query/json-value.js +54 -0
- package/dist/query/normalize-args.d.ts +87 -0
- package/dist/query/normalize-args.js +517 -0
- package/dist/query/option-surface.d.ts +16 -4
- package/dist/query/option-surface.js +56 -13
- package/dist/query/relation-names.d.ts +14 -0
- package/dist/query/relation-names.js +77 -19
- package/dist/query/relations.d.ts +30 -3
- package/dist/query/relations.js +220 -67
- package/dist/query/row-lock.d.ts +49 -0
- package/dist/query/row-lock.js +131 -0
- package/dist/query/sql-comment.d.ts +161 -0
- package/dist/query/sql-comment.js +374 -0
- package/dist/query/stream-idle.d.ts +132 -0
- package/dist/query/stream-idle.js +203 -0
- package/dist/query/table-client.d.ts +101 -0
- package/dist/query/table-client.js +70 -0
- package/dist/query/types.d.ts +677 -298
- package/dist/query/utils.d.ts +105 -6
- package/dist/query/utils.js +414 -18
- package/dist/query/warn-registry.d.ts +18 -0
- package/dist/query/warn-registry.js +18 -0
- package/dist/query/where-compile.d.ts +138 -14
- package/dist/query/where-compile.js +508 -31
- package/dist/query/where.d.ts +159 -59
- package/dist/query/where.js +711 -248
- package/dist/query/writes.d.ts +112 -50
- package/dist/query/writes.js +1496 -202
- package/dist/query-events.d.ts +4 -2
- package/dist/query-events.js +3 -1
- package/dist/realtime.d.ts +7 -3
- package/dist/realtime.js +13 -2
- package/dist/schema-builder.d.ts +55 -4
- package/dist/schema-builder.js +381 -18
- package/dist/schema-metadata.d.ts +21 -1
- package/dist/schema-metadata.js +28 -7
- package/dist/schema-sql.d.ts +75 -5
- package/dist/schema-sql.js +494 -44
- package/dist/schema.d.ts +26 -5
- package/dist/schema.js +109 -13
- package/dist/sql-placeholders.d.ts +56 -0
- package/dist/sql-placeholders.js +200 -0
- package/dist/sqlite.d.ts +75 -20
- package/dist/sqlite.js +597 -99
- package/dist/supplied-pool-types.d.ts +48 -0
- package/dist/supplied-pool-types.js +247 -0
- package/dist/transaction-control.d.ts +51 -0
- package/dist/transaction-control.js +311 -0
- package/dist/transaction-state.d.ts +342 -0
- package/dist/transaction-state.js +696 -0
- package/dist/typed-sql.d.ts +37 -3
- package/dist/typed-sql.js +100 -8
- package/package.json +7 -4
- package/skills/turbine-orm/SKILL.md +56 -6
package/dist/cjs/client.js
CHANGED
|
@@ -32,15 +32,25 @@ const _pg_1 = __importDefault(require("pg"));
|
|
|
32
32
|
const checkout_js_1 = require("./checkout.js");
|
|
33
33
|
const connection_guard_js_1 = require("./connection-guard.js");
|
|
34
34
|
const connection_url_js_1 = require("./connection-url.js");
|
|
35
|
+
const diagnostics_js_1 = require("./diagnostics.js");
|
|
35
36
|
const dialect_js_1 = require("./dialect.js");
|
|
36
37
|
const errors_js_1 = require("./errors.js");
|
|
38
|
+
const global_filters_config_js_1 = require("./global-filters-config.js");
|
|
39
|
+
const logger_js_1 = require("./logger.js");
|
|
40
|
+
const nested_write_js_1 = require("./nested-write.js");
|
|
37
41
|
const observe_js_1 = require("./observe.js");
|
|
38
42
|
const pipeline_js_1 = require("./pipeline.js");
|
|
39
43
|
const index_js_1 = require("./query/index.js");
|
|
44
|
+
const row_lock_js_1 = require("./query/row-lock.js");
|
|
45
|
+
const sql_comment_js_1 = require("./query/sql-comment.js");
|
|
46
|
+
const stream_idle_js_1 = require("./query/stream-idle.js");
|
|
47
|
+
const table_client_js_1 = require("./query/table-client.js");
|
|
40
48
|
const utils_js_1 = require("./query/utils.js");
|
|
41
49
|
const warn_registry_js_1 = require("./query/warn-registry.js");
|
|
42
50
|
const query_events_js_1 = require("./query-events.js");
|
|
43
51
|
const realtime_js_1 = require("./realtime.js");
|
|
52
|
+
const supplied_pool_types_js_1 = require("./supplied-pool-types.js");
|
|
53
|
+
const transaction_state_js_1 = require("./transaction-state.js");
|
|
44
54
|
const typed_sql_js_1 = require("./typed-sql.js");
|
|
45
55
|
async function withRetry(fn, options) {
|
|
46
56
|
const maxAttempts = options?.maxAttempts ?? 3;
|
|
@@ -110,11 +120,17 @@ const TURBINE_CONFIG_KEYS = {
|
|
|
110
120
|
idleTimeoutMillis: true,
|
|
111
121
|
connectionTimeoutMillis: true,
|
|
112
122
|
logging: true,
|
|
123
|
+
logger: true,
|
|
124
|
+
diagnostics: true,
|
|
113
125
|
defaultLimit: true,
|
|
114
126
|
warnOnUnlimited: true,
|
|
115
127
|
utcTimestamps: true,
|
|
116
128
|
temporalInfinity: true,
|
|
117
129
|
planCacheMode: true,
|
|
130
|
+
statementTimeoutMs: true,
|
|
131
|
+
maxLifetimeSeconds: true,
|
|
132
|
+
streamIdleTimeoutMs: true,
|
|
133
|
+
sqlComment: true,
|
|
118
134
|
scopedConnect: true,
|
|
119
135
|
relationLoadStrategy: true,
|
|
120
136
|
stableRelationOrder: true,
|
|
@@ -162,8 +178,7 @@ function suggestConfigKey(key) {
|
|
|
162
178
|
return (0, utils_js_1.suggestKey)(key, CONFIG_KEY_SET);
|
|
163
179
|
}
|
|
164
180
|
/**
|
|
165
|
-
*
|
|
166
|
-
* surface.
|
|
181
|
+
* Notice for a key on the config object that is not part of the config surface.
|
|
167
182
|
*
|
|
168
183
|
* An unknown key is silently ignored (JavaScript objects have no schema), which
|
|
169
184
|
* makes a typo or a wrong guess indistinguishable from a broken feature: a
|
|
@@ -175,12 +190,10 @@ function suggestConfigKey(key) {
|
|
|
175
190
|
* turbine that passes a key this version has not heard of must keep running,
|
|
176
191
|
* and the whole check is wrapped so that a hostile / exotic config object
|
|
177
192
|
* (a Proxy whose `ownKeys` throws) cannot take down the constructor either.
|
|
178
|
-
*
|
|
179
|
-
*
|
|
193
|
+
* Always on, whatever `diagnostics` says: an ignored option is ignored in
|
|
194
|
+
* production too, and the line fires once per key per process.
|
|
180
195
|
*/
|
|
181
|
-
function warnUnknownConfigKeys(config) {
|
|
182
|
-
if (process.env.NODE_ENV === 'production')
|
|
183
|
-
return;
|
|
196
|
+
function warnUnknownConfigKeys(config, logger) {
|
|
184
197
|
try {
|
|
185
198
|
for (const key of Object.keys(config)) {
|
|
186
199
|
if (CONFIG_KEY_SET.has(key) || NON_CONFIG_KEYS.has(key))
|
|
@@ -188,8 +201,8 @@ function warnUnknownConfigKeys(config) {
|
|
|
188
201
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unknownConfigKey, key))
|
|
189
202
|
continue;
|
|
190
203
|
const suggestion = suggestConfigKey(key);
|
|
191
|
-
|
|
192
|
-
(suggestion ? ` Did you mean "${suggestion}"?` : ''));
|
|
204
|
+
(0, logger_js_1.logTo)(logger, 'warn', `Unknown option "${key}" in the config passed to TurbineClient, it is ignored.` +
|
|
205
|
+
(suggestion ? ` Did you mean "${suggestion}"?` : ''), { code: 'unknown-config-key', key, ...(suggestion ? { suggestion } : {}) });
|
|
193
206
|
}
|
|
194
207
|
}
|
|
195
208
|
catch {
|
|
@@ -336,20 +349,6 @@ const ISOLATION_LEVELS = Object.assign(Object.create(null), {
|
|
|
336
349
|
RepeatableRead: 'REPEATABLE READ',
|
|
337
350
|
Serializable: 'SERIALIZABLE',
|
|
338
351
|
});
|
|
339
|
-
/**
|
|
340
|
-
* Resolve a requested isolation level, REFUSING anything not in the map.
|
|
341
|
-
*
|
|
342
|
-
* A miss used to yield `undefined`, which `beginStatement` renders as a plain
|
|
343
|
-
* `BEGIN`. So `isolationLevel: 'serializable'` (wrong case) asked for
|
|
344
|
-
* SERIALIZABLE and silently got READ COMMITTED, which is the worst available
|
|
345
|
-
* outcome for this option: the caller believes it has a guarantee it does not
|
|
346
|
-
* have, and the workload that depended on it produces wrong data with no error.
|
|
347
|
-
*
|
|
348
|
-
* The TypeScript union on `TransactionOptions.isolationLevel` does not prevent
|
|
349
|
-
* this. It is not enforced for a JavaScript consumer of a published package, for
|
|
350
|
-
* a value read from config or an environment variable, or across any `as`.
|
|
351
|
-
* Throwing costs a caller who was already broken one clear error.
|
|
352
|
-
*/
|
|
353
352
|
/**
|
|
354
353
|
* Run a transaction-control statement (BEGIN / COMMIT / ROLLBACK) with the same
|
|
355
354
|
* error translation every other query boundary gets.
|
|
@@ -365,20 +364,101 @@ const ISOLATION_LEVELS = Object.assign(Object.create(null), {
|
|
|
365
364
|
* loops, and a loop keyed on it would never fire for the commit-time conflicts
|
|
366
365
|
* that are the main reason to run SERIALIZABLE at all.
|
|
367
366
|
*/
|
|
368
|
-
async function runTxControl(client, sql) {
|
|
367
|
+
async function runTxControl(client, sql, params) {
|
|
369
368
|
try {
|
|
370
|
-
await client.query(sql);
|
|
369
|
+
await client.query(sql, params);
|
|
371
370
|
}
|
|
372
371
|
catch (err) {
|
|
373
372
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
374
373
|
}
|
|
375
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* Refuse anything in a batch that is not a deferred query, before any of it
|
|
377
|
+
* runs.
|
|
378
|
+
*
|
|
379
|
+
* The array forms take the descriptors `build*()` returns. Prisma's array form
|
|
380
|
+
* takes promises instead, so code ported from it passes `db.users.create(...)`,
|
|
381
|
+
* whose query has ALREADY started on its own connection, outside any
|
|
382
|
+
* transaction, by the time the array exists. That cannot be undone here, so
|
|
383
|
+
* the error says so and names the builder to use instead. The promise's own
|
|
384
|
+
* rejection is observed, so this refusal is the error the caller sees rather
|
|
385
|
+
* than an unhandled rejection from a query nobody awaits.
|
|
386
|
+
*/
|
|
387
|
+
function assertDeferredQueries(queries, api, unit) {
|
|
388
|
+
queries.forEach((q, i) => {
|
|
389
|
+
if (q !== null &&
|
|
390
|
+
(typeof q === 'object' || typeof q === 'function') &&
|
|
391
|
+
typeof q.then === 'function') {
|
|
392
|
+
Promise.resolve(q).catch(() => { });
|
|
393
|
+
throw new errors_js_1.ValidationError(`${api}: item ${i} is a Promise. Its query started running on its own the moment it was created, ` +
|
|
394
|
+
`outside this ${unit}, and may already have committed. Pass the deferred form instead, built with ` +
|
|
395
|
+
'`build*()`: `db.users.buildCreate(...)` rather than `db.users.create(...)` (likewise `buildUpdate`, ' +
|
|
396
|
+
'`buildDelete`, `buildUpsert`, `buildFindMany`).' +
|
|
397
|
+
(unit === 'transaction' ? ' Or use the callback form, `$transaction(async (tx) => { ... })`.' : ''));
|
|
398
|
+
}
|
|
399
|
+
const dq = q;
|
|
400
|
+
if (!dq || typeof dq !== 'object' || typeof dq.sql !== 'string' || typeof dq.transform !== 'function') {
|
|
401
|
+
throw new errors_js_1.ValidationError(`${api}: item ${i} is not a deferred query. Build each item with a \`build*()\` method, ` +
|
|
402
|
+
'for example `db.users.buildCreate({ data })` or `db.posts.buildCount({ where })`.');
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
/** The PG* variables pg reads for a connection target when its config leaves the field unset. */
|
|
407
|
+
const PG_ENV_TARGETS = ['PGHOST', 'PGPORT', 'PGDATABASE', 'PGUSER'];
|
|
408
|
+
/** A non-empty environment variable, or undefined (also where `process` does not exist). */
|
|
409
|
+
function pgEnv(name) {
|
|
410
|
+
return (typeof process !== 'undefined' ? process.env?.[name] : undefined) || undefined;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Whether an owned pool's config names a database at all: a connection
|
|
414
|
+
* string, any host setting, DATABASE_URL (unless an explicit, empty
|
|
415
|
+
* `connectionString` suppresses it, as the pool branch does), or a PG*
|
|
416
|
+
* variable pg will read.
|
|
417
|
+
*/
|
|
418
|
+
function hasDatabaseTarget(config) {
|
|
419
|
+
if (config.connectionString)
|
|
420
|
+
return true;
|
|
421
|
+
if (config.host != null ||
|
|
422
|
+
config.port != null ||
|
|
423
|
+
config.database != null ||
|
|
424
|
+
config.user != null ||
|
|
425
|
+
config.password != null) {
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
if (config.connectionString == null && pgEnv('DATABASE_URL'))
|
|
429
|
+
return true;
|
|
430
|
+
return PG_ENV_TARGETS.some((name) => pgEnv(name) !== undefined);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The pool of a client that was given no database: every query and checkout
|
|
434
|
+
* rejects with a {@link ConnectionError} naming the fix, and `disconnect()`
|
|
435
|
+
* succeeds. Building SQL never touches the pool, so it still works.
|
|
436
|
+
*/
|
|
437
|
+
function unconfiguredPool(emptyConnectionString) {
|
|
438
|
+
const fail = () => Promise.reject(new errors_js_1.ConnectionError('No database configured: ' +
|
|
439
|
+
(emptyConnectionString
|
|
440
|
+
? "`connectionString` is an empty string, which names no database (often `process.env.DATABASE_URL ?? ''` " +
|
|
441
|
+
'evaluated before DATABASE_URL was loaded). '
|
|
442
|
+
: 'this client was given no `connectionString`, `pool` or host settings, and the DATABASE_URL environment ' +
|
|
443
|
+
'variable is not set. ') +
|
|
444
|
+
'turbine-orm does not read `.env` files: load yours before the app starts (`node --env-file=.env app.js`, ' +
|
|
445
|
+
'or `process.loadEnvFile()` on Node 20.12 and later), or pass `connectionString` explicitly.'));
|
|
446
|
+
return { query: fail, connect: fail, end: async () => { }, on: () => { } };
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Resolve a requested isolation level, REFUSING anything not in the map.
|
|
450
|
+
*
|
|
451
|
+
* A miss rendered as a plain `BEGIN` would be the worst outcome for this
|
|
452
|
+
* option: `isolationLevel: 'serializable'` (wrong case) would ask for
|
|
453
|
+
* SERIALIZABLE and silently get READ COMMITTED. The TypeScript union does not
|
|
454
|
+
* prevent it for a JavaScript caller, a value read from config, or any `as`.
|
|
455
|
+
*/
|
|
376
456
|
function resolveIsolationLevel(level) {
|
|
377
457
|
if (level === undefined)
|
|
378
458
|
return undefined;
|
|
379
459
|
const sql = (0, utils_js_1.ownLookup)(ISOLATION_LEVELS, level);
|
|
380
460
|
if (sql === undefined) {
|
|
381
|
-
throw new errors_js_1.ValidationError(`$transaction: unknown isolationLevel ${
|
|
461
|
+
throw new errors_js_1.ValidationError(`$transaction: unknown isolationLevel ${describeValue(level)}. ` +
|
|
382
462
|
`Expected one of: ${Object.keys(ISOLATION_LEVELS).join(', ')} (case-sensitive).`);
|
|
383
463
|
}
|
|
384
464
|
return sql;
|
|
@@ -391,34 +471,274 @@ function resolveIsolationLevel(level) {
|
|
|
391
471
|
*/
|
|
392
472
|
const GUC_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
393
473
|
/**
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
474
|
+
* How long a transaction being abandoned (timed out, failed, or returned with
|
|
475
|
+
* work still running) gives its in-flight statement to answer a cancel
|
|
476
|
+
* request, and its ROLLBACK to answer, before the connection is destroyed
|
|
477
|
+
* instead of reused.
|
|
478
|
+
*/
|
|
479
|
+
const TX_ABORT_GRACE_MS = 2000;
|
|
480
|
+
/** Default {@link TurbineConfig.maxLifetimeSeconds} on pools Turbine opens. */
|
|
481
|
+
const DEFAULT_MAX_LIFETIME_SECONDS = 1800;
|
|
482
|
+
/**
|
|
483
|
+
* A value as an error message should quote it. `JSON.stringify` writes `NaN` and
|
|
484
|
+
* `Infinity` as `null`, throws on a bigint, and returns `undefined` for a symbol
|
|
485
|
+
* or a function, so a message built from it can name a value the caller never
|
|
486
|
+
* passed.
|
|
487
|
+
*/
|
|
488
|
+
function describeValue(value) {
|
|
489
|
+
switch (typeof value) {
|
|
490
|
+
case 'string':
|
|
491
|
+
return JSON.stringify(value);
|
|
492
|
+
case 'bigint':
|
|
493
|
+
return `${value}n`;
|
|
494
|
+
case 'symbol':
|
|
495
|
+
return value.toString();
|
|
496
|
+
case 'function':
|
|
497
|
+
return 'a function';
|
|
498
|
+
case 'object':
|
|
499
|
+
return value === null ? 'null' : Array.isArray(value) ? 'an array' : 'an object';
|
|
500
|
+
default:
|
|
501
|
+
return String(value);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* `allowBuiltinSettings` as a lower-cased set (PostgreSQL folds setting names).
|
|
506
|
+
* Each entry must be a plain un-dotted setting name; anything else is E003.
|
|
397
507
|
*/
|
|
508
|
+
function resolveBuiltinSettingsAllowlist(value) {
|
|
509
|
+
if (value === undefined)
|
|
510
|
+
return new Set();
|
|
511
|
+
if (!Array.isArray(value)) {
|
|
512
|
+
throw new errors_js_1.ValidationError(`$transaction: allowBuiltinSettings must be an array of setting names, received ${typeof value}.`);
|
|
513
|
+
}
|
|
514
|
+
const names = new Set();
|
|
515
|
+
for (const name of value) {
|
|
516
|
+
if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
517
|
+
throw new errors_js_1.ValidationError(`$transaction: allowBuiltinSettings entry ${describeValue(name)} is not a PostgreSQL setting name ` +
|
|
518
|
+
'(a plain identifier such as "statement_timeout"). Namespaced custom settings need no entry.');
|
|
519
|
+
}
|
|
520
|
+
names.add(name.toLowerCase());
|
|
521
|
+
}
|
|
522
|
+
return names;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* SQLSTATEs with which the server refuses a `set_config`, and what each means
|
|
526
|
+
* for the caller. A function of the setting where the cause depends on it: the
|
|
527
|
+
* same SQLSTATE says "no such role" for `role` and "unreadable duration" for
|
|
528
|
+
* `statement_timeout`.
|
|
529
|
+
*/
|
|
530
|
+
const SESSION_SETTING_REFUSALS = Object.assign(Object.create(null), {
|
|
531
|
+
'22023': (setting) => setting === 'role' || setting === 'session_authorization'
|
|
532
|
+
? 'the value is not valid for this setting (the role must exist)'
|
|
533
|
+
: 'the value is not valid for this setting',
|
|
534
|
+
'42704': () => 'the server has no setting by that name',
|
|
535
|
+
'42501': () => 'the database role is not allowed to change it',
|
|
536
|
+
'55P02': () => 'it cannot be changed after the connection has started',
|
|
537
|
+
});
|
|
398
538
|
/**
|
|
399
|
-
*
|
|
539
|
+
* Apply one `sessionContext` entry. A refusal by the server names the setting
|
|
540
|
+
* that caused it rather than reading as a column or type error; the server's
|
|
541
|
+
* own message (which may contain the value) stays on `cause`.
|
|
542
|
+
*/
|
|
543
|
+
async function applySessionSetting(client, setting) {
|
|
544
|
+
try {
|
|
545
|
+
await client.query(setting.sql, setting.params);
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
const code = err?.code;
|
|
549
|
+
const describe = typeof code === 'string' ? SESSION_SETTING_REFUSALS[code] : undefined;
|
|
550
|
+
if (describe === undefined)
|
|
551
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
552
|
+
throw new errors_js_1.ValidationError(`sessionContext["${setting.name}"] was refused by the server: ${describe(setting.name.toLowerCase())} (SQLSTATE ${code}). ` +
|
|
553
|
+
'The transaction was rolled back before your callback ran.', { cause: err });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/** A non-negative safe integer config value, or `undefined` when unset. Anything else is E003. */
|
|
557
|
+
function resolveNonNegativeInteger(value, name, unit) {
|
|
558
|
+
if (value === undefined || value === null)
|
|
559
|
+
return undefined;
|
|
560
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
561
|
+
throw new errors_js_1.ValidationError(`Invalid ${name}: ${typeof value === 'string' ? JSON.stringify(value) : String(value)}. Expected a ` +
|
|
562
|
+
`non-negative whole number of ${unit}.`);
|
|
563
|
+
}
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Refuse a dialect with no `capabilities` object (E003): every code path asks
|
|
568
|
+
* it what the engine can do, so a hand-built dialect without one would fail on
|
|
569
|
+
* its first query with a TypeError instead of here.
|
|
570
|
+
*/
|
|
571
|
+
function assertDialectCapabilities(dialect) {
|
|
572
|
+
const capabilities = dialect.capabilities;
|
|
573
|
+
if (typeof capabilities === 'object' && capabilities !== null)
|
|
574
|
+
return;
|
|
575
|
+
throw new errors_js_1.ValidationError(`The dialect "${String(dialect.name)}" has no \`capabilities\` object, which says what the engine can do. ` +
|
|
576
|
+
'Build it from an engine dialect (`{ ...postgresDialect, name, capabilities: { ... } }`) and state every ' +
|
|
577
|
+
'EngineCapabilities key.');
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* The statement that opens a transaction, read-only when asked. PostgreSQL and
|
|
581
|
+
* MySQL both accept `READ ONLY` after their own BEGIN form (`BEGIN [ISOLATION
|
|
582
|
+
* LEVEL x] READ ONLY`, `[SET TRANSACTION ISOLATION LEVEL x; ]START TRANSACTION
|
|
583
|
+
* READ ONLY`); the other engines have no read-only transaction.
|
|
584
|
+
*/
|
|
585
|
+
function beginStatementFor(dialect, isolationSql, readOnly) {
|
|
586
|
+
const begin = dialect.beginStatement(isolationSql);
|
|
587
|
+
if (!readOnly)
|
|
588
|
+
return begin;
|
|
589
|
+
if (dialect.capabilities.readOnlyTransactions)
|
|
590
|
+
return `${begin} READ ONLY`;
|
|
591
|
+
throw new errors_js_1.UnsupportedFeatureError('$transaction({ readOnly: true })', dialect.name, 'This engine has no read-only transaction mode. Drop the option, or enforce read-only access with a ' +
|
|
592
|
+
'database role that has no write privileges.');
|
|
593
|
+
}
|
|
594
|
+
/** `readOnly` as a boolean; anything but a boolean or `undefined` is E003. */
|
|
595
|
+
function resolveReadOnly(value) {
|
|
596
|
+
if (value === undefined || value === false)
|
|
597
|
+
return false;
|
|
598
|
+
if (value === true)
|
|
599
|
+
return true;
|
|
600
|
+
throw new errors_js_1.ValidationError(`$transaction: readOnly must be true or false, received ${describeValue(value)}.`);
|
|
601
|
+
}
|
|
602
|
+
/** Whether `pending` settles (either way) within `ms`. */
|
|
603
|
+
async function settlesWithin(pending, ms) {
|
|
604
|
+
let timer;
|
|
605
|
+
const late = new Promise((resolve) => {
|
|
606
|
+
timer = setTimeout(() => resolve(false), ms);
|
|
607
|
+
});
|
|
608
|
+
try {
|
|
609
|
+
return await Promise.race([
|
|
610
|
+
pending.then(() => true, () => true),
|
|
611
|
+
late,
|
|
612
|
+
]);
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
clearTimeout(timer);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Stop a transaction that will not commit, on the server and now: cancel the
|
|
620
|
+
* statement it is running (the protocol's CancelRequest, which also reaches the
|
|
621
|
+
* right backend through a pooler), wait for it to answer, then ROLLBACK, which
|
|
622
|
+
* releases every lock the transaction holds.
|
|
400
623
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
624
|
+
* Closing the socket alone does not do that. PostgreSQL notices a closed
|
|
625
|
+
* client only when it next writes to it, so a backend waiting on a lock keeps
|
|
626
|
+
* the transaction, and every lock it already took, until that lock clears.
|
|
627
|
+
*
|
|
628
|
+
* Resolves `true` when the connection is rolled back and reusable, `false` when
|
|
629
|
+
* it is not (the cancel could not be sent, the statement or the ROLLBACK did not
|
|
630
|
+
* answer within {@link TX_ABORT_GRACE_MS}, or the ROLLBACK failed); the caller
|
|
631
|
+
* then destroys it. Call after `state.end()`, so nothing new is sent meanwhile.
|
|
632
|
+
*/
|
|
633
|
+
async function rollbackOnServer(client, state, dialect) {
|
|
634
|
+
if (state.running) {
|
|
635
|
+
if (!(await (0, utils_js_1.sendCancelRequest)(client, TX_ABORT_GRACE_MS)))
|
|
636
|
+
return false;
|
|
637
|
+
if (!(await state.whenIdle(TX_ABORT_GRACE_MS)))
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
let failed = false;
|
|
641
|
+
const rollback = client.query(dialect.rollbackStatement()).catch(() => {
|
|
642
|
+
failed = true;
|
|
643
|
+
});
|
|
644
|
+
return (await settlesWithin(rollback, TX_ABORT_GRACE_MS)) && !failed;
|
|
645
|
+
}
|
|
646
|
+
/** `rawUnsafe` takes statement text; a tagged-template call or a non-string is E003. */
|
|
647
|
+
function assertSqlText(text, api) {
|
|
648
|
+
if (typeof text === 'string' && text.trim() !== '')
|
|
649
|
+
return;
|
|
650
|
+
throw new errors_js_1.ValidationError(Array.isArray(text)
|
|
651
|
+
? `${api} takes the statement as a string, not as a tagged template. Use raw\`...\` or sql\`...\` for a ` +
|
|
652
|
+
'template, whose values are bound for you.'
|
|
653
|
+
: `${api} takes the statement text as a non-empty string, received ${text === null ? 'null' : typeof text}.`);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* The connection `transaction(fn)` and `tx.$connection()` hand out: the
|
|
657
|
+
* checked-out client, with every statement routed through the transaction's
|
|
658
|
+
* state, so one issued after the transaction settled is refused rather than
|
|
659
|
+
* run on a connection that is back in the pool, and transaction-control SQL
|
|
660
|
+
* (`COMMIT`, `BEGIN`, ...) is refused rather than ending the transaction behind
|
|
661
|
+
* the state that tracks it.
|
|
662
|
+
*
|
|
663
|
+
* What would take the connection away is refused too: `release` (the
|
|
664
|
+
* transaction returns it itself), `end` and `connect`, and the driver's
|
|
665
|
+
* `connection` (the wire-level object, whose writes bypass every check here) is
|
|
666
|
+
* hidden. Everything else is the driver client's own.
|
|
667
|
+
*/
|
|
668
|
+
function closableRawClient(client, state, scope = state.rootScope, api = 'transaction()') {
|
|
669
|
+
// The refusals name the API the caller used: a `tx.$connection()` caller
|
|
670
|
+
// never called `transaction()`.
|
|
671
|
+
const who = api === 'transaction()' ? 'transaction(): the callback' : '$connection(): the $transaction callback';
|
|
672
|
+
const target = client;
|
|
673
|
+
const send = target.query;
|
|
674
|
+
const query = (...args) => {
|
|
675
|
+
const first = args[0];
|
|
676
|
+
const text = typeof first === 'string' ? first : first?.text;
|
|
677
|
+
// Callback form and submittables (cursors, COPY streams) answer through
|
|
678
|
+
// their own channel, not a promise: checked, then passed straight through.
|
|
679
|
+
if (typeof args[args.length - 1] === 'function' ||
|
|
680
|
+
typeof first?.submit === 'function') {
|
|
681
|
+
state.assertOpen();
|
|
682
|
+
state.screenRaw(text);
|
|
683
|
+
return send.apply(client, args);
|
|
684
|
+
}
|
|
685
|
+
return state.runRaw(text, () => send.apply(client, args), scope);
|
|
686
|
+
};
|
|
687
|
+
const release = () => {
|
|
688
|
+
throw new errors_js_1.ValidationError(`${who} must not release its client; the transaction releases it after COMMIT or ROLLBACK.`);
|
|
689
|
+
};
|
|
690
|
+
const refuse = (verb) => {
|
|
691
|
+
return () => {
|
|
692
|
+
throw new errors_js_1.ValidationError(`${who} must not ${verb} its client; the transaction owns the connection and ` +
|
|
693
|
+
'returns it to the pool after COMMIT or ROLLBACK.');
|
|
694
|
+
};
|
|
695
|
+
};
|
|
696
|
+
const refused = new Map([
|
|
697
|
+
['end', refuse('end')],
|
|
698
|
+
['connect', refuse('connect')],
|
|
699
|
+
]);
|
|
700
|
+
const bound = new Map();
|
|
701
|
+
return new Proxy(target, {
|
|
702
|
+
has(t, prop) {
|
|
703
|
+
return prop === 'connection' ? false : Reflect.has(t, prop);
|
|
704
|
+
},
|
|
705
|
+
get(t, prop) {
|
|
706
|
+
if (prop === 'query')
|
|
707
|
+
return query;
|
|
708
|
+
if (prop === 'release')
|
|
709
|
+
return release;
|
|
710
|
+
if (prop === 'connection')
|
|
711
|
+
return undefined;
|
|
712
|
+
const refusal = refused.get(prop);
|
|
713
|
+
if (refusal !== undefined)
|
|
714
|
+
return refusal;
|
|
715
|
+
const value = Reflect.get(t, prop, t);
|
|
716
|
+
if (typeof value !== 'function')
|
|
717
|
+
return value;
|
|
718
|
+
let fn = bound.get(prop);
|
|
719
|
+
if (fn === undefined) {
|
|
720
|
+
fn = value.bind(t);
|
|
721
|
+
bound.set(prop, fn);
|
|
722
|
+
}
|
|
723
|
+
return fn;
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Operations that may be served by a read replica: the `read` entries of
|
|
729
|
+
* {@link TABLE_METHODS}. Every other method (writes, `build*`, internals) stays
|
|
730
|
+
* on the primary. A Set so the routing proxy's `get` trap is O(1).
|
|
731
|
+
*
|
|
732
|
+
* Derived, not listed, because a hand-kept list drifts SILENTLY: a read method
|
|
733
|
+
* missing here keeps working on the primary, so no test fails. A new public
|
|
734
|
+
* QueryInterface method cannot compile until it is classified in TABLE_METHODS.
|
|
735
|
+
* `src/test/read-operations-drift.test.ts` is the run-time check.
|
|
407
736
|
*
|
|
408
737
|
* Exported for that test only.
|
|
409
738
|
*/
|
|
410
|
-
exports.READ_OPERATIONS = new Set(
|
|
411
|
-
'
|
|
412
|
-
|
|
413
|
-
'findUnique',
|
|
414
|
-
'findFirstOrThrow',
|
|
415
|
-
'findUniqueOrThrow',
|
|
416
|
-
'count',
|
|
417
|
-
'aggregate',
|
|
418
|
-
'groupBy',
|
|
419
|
-
'findManyStream',
|
|
420
|
-
'findManyStreamBatches',
|
|
421
|
-
]);
|
|
739
|
+
exports.READ_OPERATIONS = new Set(Object.entries(table_client_js_1.TABLE_METHODS)
|
|
740
|
+
.filter(([, kind]) => kind === 'read')
|
|
741
|
+
.map(([name]) => name));
|
|
422
742
|
/**
|
|
423
743
|
* Internal marker on the config object that tells the `TurbineClient`
|
|
424
744
|
* constructor to build a lightweight "primary-only" view sharing an existing
|
|
@@ -433,6 +753,10 @@ const PRIMARY_VIEW = Symbol('turbine.primaryView');
|
|
|
433
753
|
* A transaction-scoped client that provides the same table accessor API as TurbineClient.
|
|
434
754
|
* All queries run on a dedicated connection within a BEGIN/COMMIT block.
|
|
435
755
|
* Supports nested transactions via SAVEPOINTs.
|
|
756
|
+
*
|
|
757
|
+
* Closed when its transaction settles (commit, rollback, timeout, error): every
|
|
758
|
+
* later call through it throws `ValidationError` (E003) instead of running on a
|
|
759
|
+
* connection that is back in the pool and may belong to another request.
|
|
436
760
|
*/
|
|
437
761
|
class TransactionClient {
|
|
438
762
|
client;
|
|
@@ -441,9 +765,19 @@ class TransactionClient {
|
|
|
441
765
|
queryOptions;
|
|
442
766
|
sourcePool;
|
|
443
767
|
tableCache = new Map();
|
|
444
|
-
savepointCounter = 0;
|
|
445
768
|
/** Active SQL dialect, owns savepoint keywords and raw-SQL placeholders. */
|
|
446
769
|
dialect;
|
|
770
|
+
/** Open/closed state, first failure and savepoint blocks, shared by everything this client hands out. */
|
|
771
|
+
state;
|
|
772
|
+
/**
|
|
773
|
+
* Where in the transaction this handle sits: the transaction itself, or the
|
|
774
|
+
* nested block whose callback received it. Statements carry it, so one issued
|
|
775
|
+
* through an outer handle from inside a block can be told from one issued
|
|
776
|
+
* through the block's own.
|
|
777
|
+
*/
|
|
778
|
+
scope;
|
|
779
|
+
/** What {@link $connection} hands out, built on first use. */
|
|
780
|
+
connectionHandle;
|
|
447
781
|
constructor(client, schema, middlewares, queryOptions,
|
|
448
782
|
/**
|
|
449
783
|
* The parent pool this transaction runs on. Only its `readonly` and
|
|
@@ -454,16 +788,26 @@ class TransactionClient {
|
|
|
454
788
|
* (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
|
|
455
789
|
* for a plain pg pool leave the proxy unchanged.
|
|
456
790
|
*/
|
|
457
|
-
sourcePool
|
|
791
|
+
sourcePool,
|
|
792
|
+
/**
|
|
793
|
+
* The state of the transaction this client belongs to. `$transaction`
|
|
794
|
+
* passes the one it opened, which also turns on per-write savepoints for
|
|
795
|
+
* multi-statement writes. Omitted for Turbine's own implicit transactions.
|
|
796
|
+
*/
|
|
797
|
+
state,
|
|
798
|
+
/** The nested block this handle belongs to; omitted for the transaction's own handle. */
|
|
799
|
+
scope) {
|
|
458
800
|
this.client = client;
|
|
459
801
|
this.schema = schema;
|
|
460
802
|
this.middlewares = middlewares;
|
|
461
803
|
this.queryOptions = queryOptions;
|
|
462
804
|
this.sourcePool = sourcePool;
|
|
463
805
|
this.dialect = queryOptions?.dialect ?? dialect_js_1.postgresDialect;
|
|
806
|
+
this.state = state ?? new transaction_state_js_1.TransactionState(client, this.dialect);
|
|
807
|
+
this.scope = scope ?? this.state.rootScope;
|
|
464
808
|
// Auto-create typed table accessors for all tables in the schema
|
|
465
809
|
for (const tableName of Object.keys(schema.tables)) {
|
|
466
|
-
const camelName =
|
|
810
|
+
const camelName = (0, global_filters_config_js_1.tableAccessorName)(tableName);
|
|
467
811
|
if (!(camelName in this)) {
|
|
468
812
|
Object.defineProperty(this, camelName, {
|
|
469
813
|
get: () => this.table(tableName),
|
|
@@ -484,43 +828,121 @@ class TransactionClient {
|
|
|
484
828
|
const txPool = this.createTxPool();
|
|
485
829
|
const txOpts = { ...this.queryOptions, _txScoped: true };
|
|
486
830
|
qi = txOpts.queryInterfaceFactory
|
|
487
|
-
? txOpts.queryInterfaceFactory(txPool, name, this.schema, this.middlewares, txOpts)
|
|
831
|
+
? (0, table_client_js_1.asQueryInterface)(txOpts.queryInterfaceFactory(txPool, name, this.schema, this.middlewares, txOpts))
|
|
488
832
|
: new index_js_1.QueryInterface(txPool, name, this.schema, this.middlewares, txOpts);
|
|
833
|
+
if (this.state.savepointWrites)
|
|
834
|
+
savepointMultiStatementWrites(qi, this.schema.tables[name], this.state);
|
|
489
835
|
this.tableCache.set(name, qi);
|
|
490
836
|
}
|
|
491
837
|
return qi;
|
|
492
838
|
}
|
|
493
839
|
/**
|
|
494
|
-
*
|
|
495
|
-
*
|
|
840
|
+
* The driver connection this transaction runs on, for code that needs one:
|
|
841
|
+
* another library's query function, a COPY stream, a driver feature Turbine
|
|
842
|
+
* does not wrap. Statements sent on it run inside this transaction.
|
|
843
|
+
*
|
|
844
|
+
* - It closes with the transaction: a statement sent after COMMIT or
|
|
845
|
+
* ROLLBACK is refused with `ValidationError` (E003) instead of running on a
|
|
846
|
+
* connection that is back in the pool.
|
|
847
|
+
* - `release()` is refused. The transaction returns the connection.
|
|
848
|
+
* - COMMIT, ROLLBACK, BEGIN, SAVEPOINT, RELEASE and SET TRANSACTION are
|
|
849
|
+
* refused with `ValidationError` (E003): the transaction ends itself, and
|
|
850
|
+
* Turbine would then commit or roll back a transaction that is no longer
|
|
851
|
+
* open. Use `tx.$transaction` for a savepoint.
|
|
852
|
+
* - `end()` and `connect()` are refused like `release()`, and the driver's
|
|
853
|
+
* `connection` object is not reachable through it.
|
|
854
|
+
* - Its queries use the driver's own parsers. On a pool you supplied, that is
|
|
855
|
+
* your parser configuration, not Turbine's int8 and UTC readings.
|
|
856
|
+
*
|
|
857
|
+
* Typed as the driver-neutral client. For the pg-only surface, cast:
|
|
858
|
+
* `tx.$connection() as unknown as PoolClient`.
|
|
859
|
+
*
|
|
860
|
+
* Throws `UnsupportedFeatureError` (E017) where no driver connection backs
|
|
861
|
+
* the transaction: PowDB, and a stateless HTTP driver.
|
|
496
862
|
*/
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
863
|
+
$connection() {
|
|
864
|
+
this.state.assertOpen();
|
|
865
|
+
if (this.connectionHandle)
|
|
866
|
+
return this.connectionHandle;
|
|
867
|
+
const raw = (0, supplied_pool_types_js_1.rawClientOf)(this.client);
|
|
868
|
+
const driver = this.dialect.capabilities.connectionDriver;
|
|
869
|
+
if (driver === 'none') {
|
|
870
|
+
throw new errors_js_1.UnsupportedFeatureError('tx.$connection()', this.dialect.name, 'A PowDB transaction has no driver connection to hand out. Run PowQL inside it with `tx.rawUnsafe(text, ...values)`.');
|
|
504
871
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
872
|
+
if (driver === 'pg-compatible' && typeof raw.on !== 'function') {
|
|
873
|
+
throw new errors_js_1.UnsupportedFeatureError('tx.$connection()', this.dialect.name, 'The pool returned a client with no event support, as a stateless HTTP driver does, so there is no ' +
|
|
874
|
+
'connection to hand out. Use `tx.raw` / `tx.rawUnsafe`, or a TCP pool.');
|
|
508
875
|
}
|
|
876
|
+
this.connectionHandle = closableRawClient(raw, this.state, this.scope, '$connection()');
|
|
877
|
+
return this.connectionHandle;
|
|
509
878
|
}
|
|
510
879
|
/**
|
|
511
|
-
* Execute a
|
|
880
|
+
* Execute a nested transaction via SAVEPOINT.
|
|
881
|
+
*
|
|
882
|
+
* If the inner function throws, only the savepoint is rolled back. Blocks
|
|
883
|
+
* started side by side (`Promise.all`) run one after another, because
|
|
884
|
+
* savepoints are a stack on the connection and a rollback in one would
|
|
885
|
+
* otherwise undo a sibling that already reported success.
|
|
886
|
+
*
|
|
887
|
+
* The callback receives a `tx` of its own, bound to the block. Use it for the
|
|
888
|
+
* work inside the block: a statement through the OUTER `tx` from inside the
|
|
889
|
+
* block would run in the block's savepoint and be undone with it, so it is
|
|
890
|
+
* refused with `ValidationError` (E003).
|
|
891
|
+
*/
|
|
892
|
+
async $transaction(fn) {
|
|
893
|
+
return this.state.withBlock((scope) => fn(this.inScope(scope)), this.scope);
|
|
894
|
+
}
|
|
895
|
+
/** A handle on the same transaction bound to nested block `scope`. */
|
|
896
|
+
inScope(scope) {
|
|
897
|
+
return new TransactionClient(this.client, this.schema, this.middlewares, this.queryOptions, this.sourcePool, this.state, scope);
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Execute a raw SQL query within this transaction, as a tagged template:
|
|
901
|
+
* every `${value}` is sent as a bound parameter.
|
|
512
902
|
*/
|
|
513
903
|
async raw(strings, ...values) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
904
|
+
const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect, 'raw');
|
|
905
|
+
return this.runRaw(sql, params, 'raw');
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Execute SQL text built at runtime within this transaction. See
|
|
909
|
+
* {@link TurbineClient.rawUnsafe}: nothing in `text` is escaped, so it must
|
|
910
|
+
* never contain untrusted input. Values go in `values`, referenced as `$1`,
|
|
911
|
+
* `$2`, ... (the engine's own placeholders on other engines).
|
|
912
|
+
*/
|
|
913
|
+
async rawUnsafe(text, ...values) {
|
|
914
|
+
assertSqlText(text, 'rawUnsafe');
|
|
915
|
+
return this.runRaw(text, values, 'rawUnsafe');
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Typed raw SQL on this transaction's connection, like
|
|
919
|
+
* {@link TurbineClient.sql}: await it for `T[]`, or call `.one()` /
|
|
920
|
+
* `.scalar()`. Refused, like every other `tx` call, once the transaction has
|
|
921
|
+
* settled.
|
|
922
|
+
*/
|
|
923
|
+
sql(strings, ...values) {
|
|
924
|
+
this.state.assertOpen();
|
|
925
|
+
const { sql, params: bound } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect);
|
|
926
|
+
const params = this.rawParams(bound, 'sql');
|
|
927
|
+
const state = this.state;
|
|
928
|
+
const client = this.client;
|
|
929
|
+
const scope = this.scope;
|
|
930
|
+
const pool = {
|
|
931
|
+
query: (text, vals) => state.runRaw(text, () => client.query(text, vals), scope),
|
|
932
|
+
};
|
|
933
|
+
return new typed_sql_js_1.TypedSqlQuery(pool, sql, params, false, undefined, this.queryOptions?._onQuery);
|
|
934
|
+
}
|
|
935
|
+
async runRaw(sql, values, action) {
|
|
936
|
+
const params = this.rawParams(values, action);
|
|
937
|
+
const result = await this.state.runRaw(sql, () => (0, query_events_js_1.runReportedStatement)(this.queryOptions?._onQuery, (0, query_events_js_1.rawIdentity)(action), sql, params, () => this.client.query(sql, params), errors_js_1.wrapPgError), this.scope);
|
|
522
938
|
return result.rows;
|
|
523
939
|
}
|
|
940
|
+
/** Bound Dates as UTC text on a PostgreSQL client reading temporal types as UTC. */
|
|
941
|
+
rawParams(params, api) {
|
|
942
|
+
return this.dialect.capabilities.utcTimestampRewrite && this.queryOptions?.utcTimestamps !== false
|
|
943
|
+
? (0, typed_sql_js_1.utcDateParams)(params, api)
|
|
944
|
+
: params;
|
|
945
|
+
}
|
|
524
946
|
/**
|
|
525
947
|
* @internal The `turbine-orm/prisma-compat` adapter's transaction seam. NOT
|
|
526
948
|
* application API: use {@link raw}, whose tagged template makes concatenating
|
|
@@ -548,7 +970,8 @@ class TransactionClient {
|
|
|
548
970
|
* passes `'$queryRaw'` / `'$executeRaw'` ...; default `'rawQuery'`).
|
|
549
971
|
*/
|
|
550
972
|
async rawQuery(text, params = [], action = 'rawQuery') {
|
|
551
|
-
const
|
|
973
|
+
const bound = this.rawParams(params, action);
|
|
974
|
+
const result = await this.state.runRaw(text, () => (0, query_events_js_1.runReportedStatement)(this.queryOptions?._onQuery, (0, query_events_js_1.rawIdentity)(action), text, bound, () => this.client.query(text, bound), errors_js_1.wrapPgError), this.scope);
|
|
552
975
|
return { rows: result.rows, rowCount: result.rowCount };
|
|
553
976
|
}
|
|
554
977
|
/**
|
|
@@ -558,28 +981,40 @@ class TransactionClient {
|
|
|
558
981
|
*
|
|
559
982
|
* pg driver errors thrown by queries are translated into typed Turbine
|
|
560
983
|
* errors via wrapPgError so transaction-scoped queries surface the same
|
|
561
|
-
* typed errors as pool-scoped queries.
|
|
984
|
+
* typed errors as pool-scoped queries. Every statement passes through the
|
|
985
|
+
* transaction state, which refuses it once the transaction has settled.
|
|
562
986
|
*/
|
|
563
987
|
createTxPool() {
|
|
564
988
|
const client = this.client;
|
|
989
|
+
const state = this.state;
|
|
990
|
+
const scope = this.scope;
|
|
991
|
+
const query = (textOrConfig, values) => state.run(async () => {
|
|
992
|
+
try {
|
|
993
|
+
if (typeof textOrConfig === 'string') {
|
|
994
|
+
return await client.query(textOrConfig, values);
|
|
995
|
+
}
|
|
996
|
+
// Object form for prepared statements: { name, text, values }
|
|
997
|
+
// A driver client accepts it, but the two-argument `query` in the
|
|
998
|
+
// PgCompatPoolClient contract does not describe it, so we cast.
|
|
999
|
+
return await client.query(textOrConfig);
|
|
1000
|
+
}
|
|
1001
|
+
catch (err) {
|
|
1002
|
+
throw (0, errors_js_1.wrapPgError)(err);
|
|
1003
|
+
}
|
|
1004
|
+
}, scope);
|
|
565
1005
|
// Return a minimal pool-compatible object that routes queries
|
|
566
1006
|
// through the transaction client
|
|
567
1007
|
const txPool = {
|
|
568
|
-
query
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
return await client.query(textOrConfig);
|
|
577
|
-
}
|
|
578
|
-
catch (err) {
|
|
579
|
-
throw (0, errors_js_1.wrapPgError)(err);
|
|
580
|
-
}
|
|
1008
|
+
query,
|
|
1009
|
+
// The transaction's connection is not this pool's to lend: a "checkout"
|
|
1010
|
+
// runs on it through the same guarded `query`, and releasing it is a
|
|
1011
|
+
// no-op, so nothing reached through the pool can hand the connection back
|
|
1012
|
+
// to the real pool while the transaction still holds it.
|
|
1013
|
+
connect: async () => {
|
|
1014
|
+
state.assertOpen();
|
|
1015
|
+
return { query, release: () => { } };
|
|
581
1016
|
},
|
|
582
|
-
|
|
1017
|
+
[transaction_state_js_1.TRANSACTION_STATE]: state,
|
|
583
1018
|
};
|
|
584
1019
|
// Carry the parent pool's PowDB-only flags through so a transaction-scoped
|
|
585
1020
|
// PowqlInterface reads the same read-only guard and capabilities it would
|
|
@@ -592,6 +1027,47 @@ class TransactionClient {
|
|
|
592
1027
|
}
|
|
593
1028
|
}
|
|
594
1029
|
exports.TransactionClient = TransactionClient;
|
|
1030
|
+
/**
|
|
1031
|
+
* Give each multi-statement write on a transaction-scoped accessor its own
|
|
1032
|
+
* savepoint: `create` / `update` carrying nested relation writes, and an
|
|
1033
|
+
* `upsert` that looks the row up first.
|
|
1034
|
+
*
|
|
1035
|
+
* Joining the caller's transaction statement by statement, a failure raised
|
|
1036
|
+
* part way through (a nested `connect` that finds nothing, a validation error
|
|
1037
|
+
* found mid-walk) would leave the statements before it in place, and a caller
|
|
1038
|
+
* that caught the error and went on would commit half of the write. In its own
|
|
1039
|
+
* savepoint the write is all or nothing, and the caller's transaction
|
|
1040
|
+
* continues either way.
|
|
1041
|
+
*
|
|
1042
|
+
* Installed as own methods on the accessor this transaction created and owns,
|
|
1043
|
+
* so a single-statement write pays one property lookup and a shape check.
|
|
1044
|
+
*/
|
|
1045
|
+
function savepointMultiStatementWrites(qi, meta, state) {
|
|
1046
|
+
if (!meta)
|
|
1047
|
+
return;
|
|
1048
|
+
const target = qi;
|
|
1049
|
+
const nestedData = (args) => {
|
|
1050
|
+
const data = args?.data;
|
|
1051
|
+
return data !== null && typeof data === 'object' && (0, nested_write_js_1.hasRelationFields)(data, meta);
|
|
1052
|
+
};
|
|
1053
|
+
const lookupUpsert = (args) => {
|
|
1054
|
+
const needsLookup = qi.upsertNeedsLookup;
|
|
1055
|
+
return typeof needsLookup === 'function' && args !== undefined && needsLookup.call(qi, args);
|
|
1056
|
+
};
|
|
1057
|
+
const wrap = (method, multiStatement) => {
|
|
1058
|
+
const original = target[method];
|
|
1059
|
+
if (typeof original !== 'function')
|
|
1060
|
+
return;
|
|
1061
|
+
Object.defineProperty(qi, method, {
|
|
1062
|
+
value: (args) => multiStatement(args) ? state.withSavepoint(() => original.call(qi, args)) : original.call(qi, args),
|
|
1063
|
+
configurable: true,
|
|
1064
|
+
writable: true,
|
|
1065
|
+
});
|
|
1066
|
+
};
|
|
1067
|
+
wrap('create', nestedData);
|
|
1068
|
+
wrap('update', nestedData);
|
|
1069
|
+
wrap('upsert', lookupUpsert);
|
|
1070
|
+
}
|
|
595
1071
|
// ---------------------------------------------------------------------------
|
|
596
1072
|
// TurbineClient
|
|
597
1073
|
// ---------------------------------------------------------------------------
|
|
@@ -609,6 +1085,13 @@ class TurbineClient {
|
|
|
609
1085
|
* `db.pool as unknown as Pool`.
|
|
610
1086
|
*/
|
|
611
1087
|
pool;
|
|
1088
|
+
/**
|
|
1089
|
+
* The pool Turbine's own statements go through: {@link pool}, or for a
|
|
1090
|
+
* caller-supplied PostgreSQL pool a view of it whose queries carry Turbine's
|
|
1091
|
+
* int8 and temporal parsers (see supplied-pool-types.ts). `pool` stays the
|
|
1092
|
+
* caller's object, and queries the caller runs on it keep their parsers.
|
|
1093
|
+
*/
|
|
1094
|
+
queryPool;
|
|
612
1095
|
/** The schema metadata this client was built from */
|
|
613
1096
|
schema;
|
|
614
1097
|
static int8ParserRegistered = false;
|
|
@@ -636,6 +1119,10 @@ class TurbineClient {
|
|
|
636
1119
|
*/
|
|
637
1120
|
static utcTimestampParsersRegistered = false;
|
|
638
1121
|
logging;
|
|
1122
|
+
/** Where this client's log lines go (the `logger` option, else console). */
|
|
1123
|
+
logger;
|
|
1124
|
+
/** The `diagnostics` option, resolved once at construction. */
|
|
1125
|
+
diagnostics;
|
|
639
1126
|
/** Active SQL dialect, owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
|
|
640
1127
|
dialect;
|
|
641
1128
|
/** Validated `plan_cache_mode` to pin on every owned connection, or undefined to issue nothing. */
|
|
@@ -644,6 +1131,10 @@ class TurbineClient {
|
|
|
644
1131
|
middlewares = [];
|
|
645
1132
|
queryListeners = new Set();
|
|
646
1133
|
queryOptions;
|
|
1134
|
+
/** The resolved `sqlComment` every accessor shares, for the batch paths that bypass them. */
|
|
1135
|
+
get sqlCommenter() {
|
|
1136
|
+
return this.queryOptions._sqlCommenter;
|
|
1137
|
+
}
|
|
647
1138
|
errorMessagesSafe;
|
|
648
1139
|
/**
|
|
649
1140
|
* THIS client's error-message mode. Not the process default: see
|
|
@@ -660,6 +1151,10 @@ class TurbineClient {
|
|
|
660
1151
|
ownsPool = true;
|
|
661
1152
|
/** Active LISTEN subscriptions, torn down on disconnect() so it never hangs */
|
|
662
1153
|
activeSubscriptions = new Set();
|
|
1154
|
+
/** `$on('poolError')` / `$on('connectionLost')` listeners. */
|
|
1155
|
+
poolListeners = { poolError: new Set(), connectionLost: new Set() };
|
|
1156
|
+
/** What each pool Turbine opened has checked out, for `disconnect({ timeoutMs })`. */
|
|
1157
|
+
checkoutWatches = [];
|
|
663
1158
|
/**
|
|
664
1159
|
* Read-replica pools in round-robin order. Empty when no replicas are
|
|
665
1160
|
* configured, in which case `table()` takes the original single-pool path.
|
|
@@ -689,19 +1184,23 @@ class TurbineClient {
|
|
|
689
1184
|
const parent = seed.parent;
|
|
690
1185
|
this.schema = schema;
|
|
691
1186
|
this.logging = parent.logging;
|
|
1187
|
+
this.logger = parent.logger;
|
|
1188
|
+
this.diagnostics = parent.diagnostics;
|
|
692
1189
|
this.dialect = parent.dialect;
|
|
693
1190
|
this.errorMessagesSafe = parent.errorMessagesSafe;
|
|
694
1191
|
this.errorMessageMode = parent.errorMessageMode;
|
|
695
1192
|
this.queryParamsVisible = parent.queryParamsVisible;
|
|
696
1193
|
this.queryOptions = parent.queryOptions;
|
|
697
1194
|
this.middlewares = parent.middlewares; // shared reference: $use on parent flows through
|
|
1195
|
+
this.poolListeners = parent.poolListeners;
|
|
698
1196
|
this.pool = parent.pool;
|
|
1197
|
+
this.queryPool = parent.queryPool;
|
|
699
1198
|
this.ownsPool = false;
|
|
700
1199
|
this.replicaPools = [];
|
|
701
1200
|
this.ownedReplicaPools = [];
|
|
702
1201
|
this.replicaTableCaches = [];
|
|
703
1202
|
for (const tableName of Object.keys(schema.tables)) {
|
|
704
|
-
const camelName =
|
|
1203
|
+
const camelName = (0, global_filters_config_js_1.tableAccessorName)(tableName);
|
|
705
1204
|
if (!(camelName in this)) {
|
|
706
1205
|
Object.defineProperty(this, camelName, {
|
|
707
1206
|
get: () => this.table(tableName),
|
|
@@ -739,15 +1238,38 @@ class TurbineClient {
|
|
|
739
1238
|
}
|
|
740
1239
|
break;
|
|
741
1240
|
}
|
|
1241
|
+
// The logger and the diagnostics mode are validated first, so every line
|
|
1242
|
+
// below, the construction warnings included, reaches the caller's logger.
|
|
1243
|
+
// NODE_ENV is read here and nowhere on a query path.
|
|
1244
|
+
const logger = (0, logger_js_1.resolveLogger)(config.logger);
|
|
1245
|
+
const diagnostics = (0, diagnostics_js_1.resolveDiagnostics)(config.diagnostics);
|
|
742
1246
|
// Name any key on the config object that is not part of the config surface
|
|
743
|
-
// (
|
|
744
|
-
warnUnknownConfigKeys(config);
|
|
1247
|
+
// (once per key, never throws). See warnUnknownConfigKeys.
|
|
1248
|
+
warnUnknownConfigKeys(config, logger);
|
|
745
1249
|
// ALL config validation runs before ANY process-global side effect below.
|
|
746
1250
|
// A constructor that throws must leave the process exactly as it found it:
|
|
747
1251
|
// settling the process-global parser mode and then rejecting the config
|
|
748
1252
|
// would poison the next, valid, TurbineClient with a phantom conflict.
|
|
749
1253
|
const dialect = config.dialect ?? dialect_js_1.postgresDialect;
|
|
1254
|
+
assertDialectCapabilities(dialect);
|
|
750
1255
|
const planCacheMode = TurbineClient.resolvePlanCacheMode(config.planCacheMode, dialect);
|
|
1256
|
+
const statementTimeoutMs = TurbineClient.resolveStatementTimeout(config.statementTimeoutMs, dialect);
|
|
1257
|
+
const maxLifetimeSeconds = resolveNonNegativeInteger(config.maxLifetimeSeconds, 'maxLifetimeSeconds', 'seconds');
|
|
1258
|
+
const streamIdleTimeoutMs = (0, stream_idle_js_1.resolveStreamIdleTimeout)(config.streamIdleTimeoutMs, 'streamIdleTimeoutMs');
|
|
1259
|
+
// A backend that injects its own query interface (PowDB) builds no SQL,
|
|
1260
|
+
// so there is nothing for a comment to ride on: refused, not ignored.
|
|
1261
|
+
const sqlComment = (0, sql_comment_js_1.resolveSqlComment)(config.sqlComment, logger, {
|
|
1262
|
+
dialect: dialect.name,
|
|
1263
|
+
preparesEveryStatement: dialect.capabilities.preparesEveryStatement,
|
|
1264
|
+
buildsSql: config.queryInterfaceFactory === undefined,
|
|
1265
|
+
});
|
|
1266
|
+
// Keyed by table name, with every key checked against the schema: a
|
|
1267
|
+
// filter under a name that matches no table would never be applied.
|
|
1268
|
+
// A non-SQL backend (PowDB) injects its own query interface and checks
|
|
1269
|
+
// its own filters; every SQL engine compiles each static filter here once,
|
|
1270
|
+
// with this client's dialect, so a value the compiler refuses fails now.
|
|
1271
|
+
const factory = config.queryInterfaceFactory;
|
|
1272
|
+
const globalFilters = (0, global_filters_config_js_1.normalizeGlobalFilters)(config.globalFilters, schema, factory === undefined ? { QueryInterface: index_js_1.QueryInterface, dialect, utcTimestamps: config.utcTimestamps } : undefined);
|
|
751
1273
|
// Refuse a connection string that cannot address a database, before it
|
|
752
1274
|
// reaches pg's permissive parser and comes back one query later as a DNS
|
|
753
1275
|
// error about a fragment of itself. Only the strings this client will
|
|
@@ -801,16 +1323,14 @@ class TurbineClient {
|
|
|
801
1323
|
// fork and rely on their own parser configuration, registration is
|
|
802
1324
|
// process-global, so flipping it because a string replica exists alongside
|
|
803
1325
|
// an external primary would silently change the external primary's parsing
|
|
804
|
-
// too.
|
|
805
|
-
//
|
|
806
|
-
// constructor-gated by the static flags, so it happens at
|
|
1326
|
+
// too. With an external primary, Turbine's own queries carry the parsers
|
|
1327
|
+
// per query instead, on the primary and on every replica (see typedPool).
|
|
1328
|
+
// Registration is constructor-gated by the static flags, so it happens at
|
|
1329
|
+
// most once.
|
|
807
1330
|
const ownsAnyPool = !config.pool;
|
|
808
1331
|
if (ownsAnyPool && !TurbineClient.int8ParserRegistered) {
|
|
809
|
-
(0, utils_js_1.warnParserOverwrite)(20, 'int8');
|
|
810
|
-
_pg_1.default.types.setTypeParser(20,
|
|
811
|
-
const n = Number(val);
|
|
812
|
-
return Number.isSafeInteger(n) ? n : val;
|
|
813
|
-
}));
|
|
1332
|
+
(0, utils_js_1.warnParserOverwrite)(20, 'int8', logger);
|
|
1333
|
+
_pg_1.default.types.setTypeParser(20, supplied_pool_types_js_1.parseInt8);
|
|
814
1334
|
TurbineClient.int8ParserRegistered = true;
|
|
815
1335
|
}
|
|
816
1336
|
// Parse the zone-less temporal types (`timestamp` OID 1114, `date` OID
|
|
@@ -835,16 +1355,22 @@ class TurbineClient {
|
|
|
835
1355
|
const wantUtcTimestamps = config.utcTimestamps !== false;
|
|
836
1356
|
TurbineClient.assertUtcTimestampsAgree(wantUtcTimestamps);
|
|
837
1357
|
if (ownsAnyPool && wantUtcTimestamps && !TurbineClient.utcTimestampParsersRegistered) {
|
|
838
|
-
(0, utils_js_1.registerUtcTemporalParsers)();
|
|
1358
|
+
(0, utils_js_1.registerUtcTemporalParsers)(logger);
|
|
839
1359
|
TurbineClient.utcTimestampParsersRegistered = true;
|
|
840
1360
|
}
|
|
841
1361
|
TurbineClient.utcTimestampParserMode = wantUtcTimestamps;
|
|
842
1362
|
this.logging = config.logging ?? false;
|
|
1363
|
+
this.logger = logger;
|
|
1364
|
+
this.diagnostics = diagnostics;
|
|
843
1365
|
this.dialect = dialect;
|
|
844
1366
|
this.planCacheMode = planCacheMode;
|
|
845
1367
|
this.schema = schema;
|
|
846
1368
|
// Respect env var kill switch
|
|
847
1369
|
const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
|
|
1370
|
+
const pooler = config.pool ? undefined : TurbineClient.poolerEndpoint(config);
|
|
1371
|
+
if (config.preparedStatements === true && pooler !== undefined) {
|
|
1372
|
+
TurbineClient.warnPreparedOnPooler(pooler, logger);
|
|
1373
|
+
}
|
|
848
1374
|
this.errorMessageMode = config.errorMessages ?? 'safe';
|
|
849
1375
|
this.errorMessagesSafe = this.errorMessageMode === 'safe';
|
|
850
1376
|
// Query-event param visibility. One derived boolean, so the two config
|
|
@@ -863,15 +1389,19 @@ class TurbineClient {
|
|
|
863
1389
|
autoToOneJoinMaxRows: config.autoToOneJoinMaxRows,
|
|
864
1390
|
autoRoundTripMs: config.autoRoundTripMs,
|
|
865
1391
|
jsonEncoding: config.jsonEncoding,
|
|
866
|
-
globalFilters
|
|
867
|
-
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
|
|
1392
|
+
globalFilters,
|
|
1393
|
+
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? (!config.pool && !pooler)),
|
|
868
1394
|
// Forwarded so a per-query `forceCustomPlan` can refuse the combination
|
|
869
1395
|
// this client's own connections would silently defeat, see the
|
|
870
1396
|
// `planCacheMode` note on QueryInterfaceOptions.
|
|
871
1397
|
planCacheMode,
|
|
872
1398
|
sqlCache: config.sqlCache ?? true,
|
|
873
1399
|
sqlCacheSize: config.sqlCacheSize,
|
|
1400
|
+
streamIdleTimeoutMs,
|
|
1401
|
+
_sqlCommenter: sqlComment,
|
|
874
1402
|
dialect: config.dialect,
|
|
1403
|
+
diagnostics,
|
|
1404
|
+
logger,
|
|
875
1405
|
// Non-SQL backends (PowDB) inject a factory that builds their own query
|
|
876
1406
|
// interface (PowqlInterface) instead of the SQL QueryInterface. SQL engines
|
|
877
1407
|
// never set this, so `table()` keeps constructing `new QueryInterface`.
|
|
@@ -891,7 +1421,7 @@ class TurbineClient {
|
|
|
891
1421
|
}
|
|
892
1422
|
catch (e) {
|
|
893
1423
|
if (this.logging)
|
|
894
|
-
|
|
1424
|
+
(0, logger_js_1.logTo)(this.logger, 'error', 'Query listener error:', { code: 'query-listener-error', error: e });
|
|
895
1425
|
}
|
|
896
1426
|
}
|
|
897
1427
|
},
|
|
@@ -914,11 +1444,24 @@ class TurbineClient {
|
|
|
914
1444
|
if (config.pool) {
|
|
915
1445
|
// External pool, use directly. Turbine doesn't manage its lifecycle.
|
|
916
1446
|
this.pool = config.pool;
|
|
1447
|
+
// Turbine registers no parsers for a pool it did not open, so its own
|
|
1448
|
+
// queries on this one carry them instead.
|
|
1449
|
+
this.queryPool = this.typedPool(config.pool);
|
|
917
1450
|
this.ownsPool = false;
|
|
918
1451
|
if (this.logging) {
|
|
919
|
-
|
|
1452
|
+
const tables = Object.keys(schema.tables).length;
|
|
1453
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `Using external pool, ${tables} tables`, { code: 'external-pool', tables });
|
|
920
1454
|
}
|
|
921
1455
|
}
|
|
1456
|
+
else if (!hasDatabaseTarget(config)) {
|
|
1457
|
+
// Nothing names a database: no pool, connection string, host settings,
|
|
1458
|
+
// DATABASE_URL or PG* variable. The first query fails with the fix,
|
|
1459
|
+
// which beats connecting to a guessed localhost. Failing here instead
|
|
1460
|
+
// would break every build step that imports the client without a database.
|
|
1461
|
+
this.pool = unconfiguredPool(config.connectionString === '');
|
|
1462
|
+
this.queryPool = this.pool;
|
|
1463
|
+
this.ownsPool = true;
|
|
1464
|
+
}
|
|
922
1465
|
else {
|
|
923
1466
|
const poolConfig = {
|
|
924
1467
|
max: config.poolSize ?? config.max ?? 10,
|
|
@@ -945,10 +1488,12 @@ class TurbineClient {
|
|
|
945
1488
|
poolConfig.connectionString = process.env.DATABASE_URL;
|
|
946
1489
|
}
|
|
947
1490
|
else {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
poolConfig.
|
|
951
|
-
poolConfig.
|
|
1491
|
+
// A default filled in here would override the PG* variable pg reads
|
|
1492
|
+
// for the same field, so a field the environment sets is left to pg.
|
|
1493
|
+
poolConfig.host = config.host ?? (pgEnv('PGHOST') ? undefined : 'localhost');
|
|
1494
|
+
poolConfig.port = config.port ?? (pgEnv('PGPORT') ? undefined : 5432);
|
|
1495
|
+
poolConfig.database = config.database ?? (pgEnv('PGDATABASE') ? undefined : 'postgres');
|
|
1496
|
+
poolConfig.user = config.user ?? (pgEnv('PGUSER') ? undefined : 'postgres');
|
|
952
1497
|
poolConfig.password = config.password;
|
|
953
1498
|
}
|
|
954
1499
|
if (config.ssl !== undefined) {
|
|
@@ -957,19 +1502,25 @@ class TurbineClient {
|
|
|
957
1502
|
// Held as a `pg.Pool` for the length of this block: `PgCompatPool.on` is
|
|
958
1503
|
// optional (HTTP drivers have no pool events), and here we know we own a
|
|
959
1504
|
// real pg pool that has one. Same shape as the replica loop below.
|
|
960
|
-
const ownPool = new _pg_1.default.Pool(TurbineClient.
|
|
961
|
-
|
|
962
|
-
console.error('[turbine] Unexpected pool error:', err.message);
|
|
963
|
-
});
|
|
964
|
-
(0, connection_guard_js_1.absorbCheckedOutErrors)(ownPool);
|
|
1505
|
+
const ownPool = new _pg_1.default.Pool(TurbineClient.ownedPoolConfig(poolConfig, planCacheMode, statementTimeoutMs, maxLifetimeSeconds));
|
|
1506
|
+
this.watchOwnedPool(ownPool, { pool: 'primary' });
|
|
965
1507
|
// After a freeze (a serverless function between invocations), wait one
|
|
966
1508
|
// loop turn before reusing a long-idle connection, so the pool has read
|
|
967
1509
|
// any close the server sent meanwhile. See connection-guard.ts.
|
|
968
1510
|
(0, connection_guard_js_1.settleLongIdleCheckouts)(ownPool);
|
|
1511
|
+
// An exhausted pool rejects a checkout with an untyped pg-pool error;
|
|
1512
|
+
// make it a ConnectionError on every path, `pool.query` included.
|
|
1513
|
+
(0, checkout_js_1.typeCheckoutTimeouts)(ownPool);
|
|
969
1514
|
this.pool = ownPool;
|
|
1515
|
+
this.queryPool = ownPool;
|
|
970
1516
|
this.ownsPool = true;
|
|
971
1517
|
if (this.logging) {
|
|
972
|
-
|
|
1518
|
+
const tables = Object.keys(schema.tables).length;
|
|
1519
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `Pool created, max ${poolConfig.max} connections, ${tables} tables`, {
|
|
1520
|
+
code: 'pool-created',
|
|
1521
|
+
max: poolConfig.max,
|
|
1522
|
+
tables,
|
|
1523
|
+
});
|
|
973
1524
|
}
|
|
974
1525
|
}
|
|
975
1526
|
// Build read-replica pools (if any). String entries become owned pg.Pools
|
|
@@ -979,23 +1530,23 @@ class TurbineClient {
|
|
|
979
1530
|
this.ownedReplicaPools = [];
|
|
980
1531
|
for (const replica of config.replicas ?? []) {
|
|
981
1532
|
if (typeof replica === 'string') {
|
|
982
|
-
const replicaPool = new _pg_1.default.Pool(TurbineClient.
|
|
1533
|
+
const replicaPool = new _pg_1.default.Pool(TurbineClient.ownedPoolConfig({
|
|
983
1534
|
connectionString: replica,
|
|
984
1535
|
max: config.poolSize ?? config.max ?? 10,
|
|
985
1536
|
idleTimeoutMillis: config.idleTimeoutMs ?? config.idleTimeoutMillis ?? 30_000,
|
|
986
1537
|
connectionTimeoutMillis: config.connectionTimeoutMs ?? config.connectionTimeoutMillis ?? 5_000,
|
|
987
1538
|
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
|
988
|
-
},
|
|
989
|
-
|
|
990
|
-
console.error('[turbine] Unexpected replica pool error:', err.message);
|
|
991
|
-
});
|
|
992
|
-
(0, connection_guard_js_1.absorbCheckedOutErrors)(replicaPool);
|
|
1539
|
+
}, planCacheMode, statementTimeoutMs, maxLifetimeSeconds));
|
|
1540
|
+
this.watchOwnedPool(replicaPool, { pool: 'replica', replica: this.replicaPools.length });
|
|
993
1541
|
(0, connection_guard_js_1.settleLongIdleCheckouts)(replicaPool);
|
|
994
|
-
|
|
1542
|
+
(0, checkout_js_1.typeCheckoutTimeouts)(replicaPool);
|
|
1543
|
+
// Next to a supplied primary no parsers were registered, so this
|
|
1544
|
+
// replica's reads carry them like the primary's do.
|
|
1545
|
+
this.replicaPools.push(this.ownsPool ? replicaPool : this.typedPool(replicaPool));
|
|
995
1546
|
this.ownedReplicaPools.push(replicaPool);
|
|
996
1547
|
}
|
|
997
1548
|
else {
|
|
998
|
-
this.replicaPools.push(replica);
|
|
1549
|
+
this.replicaPools.push(this.typedPool(replica));
|
|
999
1550
|
}
|
|
1000
1551
|
}
|
|
1001
1552
|
// `planCacheMode` reaches a pool only where Turbine opens the connections.
|
|
@@ -1006,26 +1557,34 @@ class TurbineClient {
|
|
|
1006
1557
|
// Deliberate no-op rather than a throw: the option is a performance knob,
|
|
1007
1558
|
// and an app that moves from an owned pool to a serverless driver should
|
|
1008
1559
|
// not stop booting over it. Same ownership rule as the type parsers, which
|
|
1009
|
-
// also skip external pools silently.
|
|
1010
|
-
|
|
1560
|
+
// also skip external pools silently. Always on, like every construction
|
|
1561
|
+
// warning: the option is just as ignored in production.
|
|
1562
|
+
if (this.planCacheMode !== undefined && !this.ownsPool) {
|
|
1011
1563
|
if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.planCacheModeIgnored, this.planCacheMode)) {
|
|
1012
1564
|
const replicaNote = this.ownedReplicaPools.length > 0
|
|
1013
1565
|
? ` It IS applied to the ${this.ownedReplicaPools.length} Turbine-owned read replica pool(s) on this ` +
|
|
1014
1566
|
'client, so reads and writes would run under different plan-cache policies until the primary is set too.'
|
|
1015
1567
|
: '';
|
|
1016
|
-
|
|
1568
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `planCacheMode: '${this.planCacheMode}' was not applied to the primary: this client was given an ` +
|
|
1017
1569
|
'external `pool`, whose connection lifecycle the caller owns, so Turbine never opens its connections. Set ' +
|
|
1018
1570
|
`\`plan_cache_mode\` in the driver's own connection setup (or run \`SET plan_cache_mode = ${this.planCacheMode}\` ` +
|
|
1019
|
-
`on checkout) instead.${replicaNote}
|
|
1571
|
+
`on checkout) instead.${replicaNote}`, { code: 'plan-cache-mode-ignored', option: 'planCacheMode', ownedReplicas: this.ownedReplicaPools.length });
|
|
1020
1572
|
}
|
|
1021
1573
|
}
|
|
1574
|
+
this.warnExternalPoolOptions(statementTimeoutMs, maxLifetimeSeconds);
|
|
1022
1575
|
this.replicaTableCaches = this.replicaPools.map(() => new Map());
|
|
1023
1576
|
if (this.logging && this.replicaPools.length > 0) {
|
|
1024
|
-
|
|
1577
|
+
const replicas = this.replicaPools.length;
|
|
1578
|
+
const owned = this.ownedReplicaPools.length;
|
|
1579
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `${replicas} read replica(s) configured (${owned} owned)`, {
|
|
1580
|
+
code: 'replicas-configured',
|
|
1581
|
+
replicas,
|
|
1582
|
+
owned,
|
|
1583
|
+
});
|
|
1025
1584
|
}
|
|
1026
1585
|
// Auto-create typed table accessors for all tables in the schema
|
|
1027
1586
|
for (const tableName of Object.keys(schema.tables)) {
|
|
1028
|
-
const camelName =
|
|
1587
|
+
const camelName = (0, global_filters_config_js_1.tableAccessorName)(tableName);
|
|
1029
1588
|
if (!(camelName in this)) {
|
|
1030
1589
|
Object.defineProperty(this, camelName, {
|
|
1031
1590
|
get: () => this.table(tableName),
|
|
@@ -1087,8 +1646,10 @@ class TurbineClient {
|
|
|
1087
1646
|
return matched;
|
|
1088
1647
|
}
|
|
1089
1648
|
/**
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1649
|
+
* The pg pool config for a pool Turbine opens: the `maxLifetimeSeconds`
|
|
1650
|
+
* recycling default, plus `plan_cache_mode` and `statement_timeout` pinned
|
|
1651
|
+
* on every connection through the pool's **connection parameters** rather
|
|
1652
|
+
* than a `SET`.
|
|
1092
1653
|
*
|
|
1093
1654
|
* PostgreSQL's `options` startup parameter (`-c plan_cache_mode=...`) is
|
|
1094
1655
|
* applied by the backend as it starts the session, so the setting is in force
|
|
@@ -1122,23 +1683,157 @@ class TurbineClient {
|
|
|
1122
1683
|
* should set the GUC on the server or role instead
|
|
1123
1684
|
* (`ALTER ROLE ... SET plan_cache_mode = ...`).
|
|
1124
1685
|
*/
|
|
1125
|
-
static
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1686
|
+
static ownedPoolConfig(poolConfig, planCacheMode, statementTimeoutMs, maxLifetimeSeconds) {
|
|
1687
|
+
let config = {
|
|
1688
|
+
...poolConfig,
|
|
1689
|
+
maxLifetimeSeconds: maxLifetimeSeconds ?? DEFAULT_MAX_LIFETIME_SECONDS,
|
|
1690
|
+
};
|
|
1691
|
+
// `planCacheMode` is a member of PLAN_CACHE_MODES and `statementTimeoutMs`
|
|
1692
|
+
// a narrowed integer, never caller text, which is what makes these
|
|
1693
|
+
// literals safe: a GUC value cannot be a bind parameter.
|
|
1694
|
+
if (planCacheMode !== undefined)
|
|
1695
|
+
config = (0, connection_url_js_1.withStartupSetting)(config, `-c plan_cache_mode=${planCacheMode}`);
|
|
1696
|
+
const timeout = statementTimeoutMs === undefined ? null : (0, connection_url_js_1.statementTimeoutSetting)(statementTimeoutMs);
|
|
1697
|
+
if (timeout !== null)
|
|
1698
|
+
config = (0, connection_url_js_1.withStartupSetting)(config, timeout);
|
|
1699
|
+
return config;
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Validate `statementTimeoutMs`: a non-negative integer (E003), on
|
|
1703
|
+
* PostgreSQL only (E017), since it is applied as a PostgreSQL connection
|
|
1704
|
+
* parameter.
|
|
1705
|
+
*/
|
|
1706
|
+
static resolveStatementTimeout(value, dialect) {
|
|
1707
|
+
const ms = resolveNonNegativeInteger(value, 'statementTimeoutMs', 'milliseconds');
|
|
1708
|
+
if (ms !== undefined && !dialect.capabilities.connectionParameters) {
|
|
1709
|
+
throw new errors_js_1.UnsupportedFeatureError(`The statementTimeoutMs option (statement_timeout = ${ms})`, dialect.name, '`statement_timeout` is applied as a PostgreSQL connection parameter. Remove the option on this engine, ' +
|
|
1710
|
+
'and bound slow statements with the per-query `timeout` instead.');
|
|
1711
|
+
}
|
|
1712
|
+
return ms;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* The first connection target this client opens that names a connection
|
|
1716
|
+
* pooler (see `detectPooler`), as `host:port`, or `undefined`. Covers the
|
|
1717
|
+
* connection string, the DATABASE_URL fallback, host/port settings, and
|
|
1718
|
+
* string replicas, each read the way node-postgres reads it: a host or port
|
|
1719
|
+
* the target leaves unset is `PGHOST` / `PGPORT`.
|
|
1720
|
+
*/
|
|
1721
|
+
static poolerEndpoint(config) {
|
|
1722
|
+
const explicit = config.connectionString != null ||
|
|
1723
|
+
config.host != null ||
|
|
1724
|
+
config.port != null ||
|
|
1725
|
+
config.database != null ||
|
|
1726
|
+
config.user != null ||
|
|
1727
|
+
config.password != null;
|
|
1728
|
+
const targets = [];
|
|
1729
|
+
if (config.connectionString)
|
|
1730
|
+
targets.push((0, connection_url_js_1.parseConnectionTarget)(config.connectionString));
|
|
1731
|
+
else if (!explicit && process.env.DATABASE_URL)
|
|
1732
|
+
targets.push((0, connection_url_js_1.parseConnectionTarget)(process.env.DATABASE_URL));
|
|
1733
|
+
else {
|
|
1734
|
+
targets.push({
|
|
1735
|
+
host: config.host ? config.host.toLowerCase() : null,
|
|
1736
|
+
port: typeof config.port === 'number' ? config.port : null,
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
for (const replica of config.replicas ?? []) {
|
|
1740
|
+
if (typeof replica === 'string' && replica)
|
|
1741
|
+
targets.push((0, connection_url_js_1.parseConnectionTarget)(replica));
|
|
1742
|
+
}
|
|
1743
|
+
const environment = { host: pgEnv('PGHOST'), port: pgEnv('PGPORT') };
|
|
1744
|
+
for (const target of targets) {
|
|
1745
|
+
const found = (0, connection_url_js_1.detectPoolerTarget)((0, connection_url_js_1.fillTargetFromEnvironment)(target, environment));
|
|
1746
|
+
if (found.pooled)
|
|
1747
|
+
return `${found.host ?? 'localhost'}:${found.port ?? 5432}`;
|
|
1748
|
+
}
|
|
1749
|
+
return undefined;
|
|
1750
|
+
}
|
|
1751
|
+
/** Always on: named statements behind a transaction pooler fail in production first. */
|
|
1752
|
+
static warnPreparedOnPooler(endpoint, logger) {
|
|
1753
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.preparedStatementsOnPooler, endpoint))
|
|
1754
|
+
return;
|
|
1755
|
+
(0, logger_js_1.logTo)(logger, 'warn', `preparedStatements: true on ${endpoint}, which looks like a connection pooler. In transaction ` +
|
|
1756
|
+
'mode a pooler runs each transaction on any server connection, so a statement named on one fails on ' +
|
|
1757
|
+
'the next ("prepared statement ... does not exist" or "already exists") unless the pooler tracks ' +
|
|
1758
|
+
'prepared statements: PgBouncer 1.21+ with max_prepared_statements above 0. If yours does not, leave ' +
|
|
1759
|
+
'preparedStatements unset; Turbine then sends unnamed statements to a pooler.', { code: 'prepared-statements-on-pooler', endpoint });
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* `pool` as Turbine's own statements should reach it: with Turbine's int8
|
|
1763
|
+
* and temporal parsers on every query, for a PostgreSQL pool built on
|
|
1764
|
+
* node-postgres. Any other pool is returned unchanged.
|
|
1765
|
+
*/
|
|
1766
|
+
typedPool(pool) {
|
|
1767
|
+
if (this.dialect.capabilities.connectionDriver !== 'pg-compatible')
|
|
1768
|
+
return pool;
|
|
1769
|
+
return (0, supplied_pool_types_js_1.typedQueryPool)(pool, this.queryOptions.utcTimestamps !== false) ?? pool;
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Listen to a pool this client opened: its idle-connection errors become
|
|
1773
|
+
* `poolError` events, a checked-out connection's loss a `connectionLost`
|
|
1774
|
+
* event, and its checkouts are tracked for `disconnect({ timeoutMs })`.
|
|
1775
|
+
* Also the connection guards every owned pool gets (connection-guard.ts):
|
|
1776
|
+
* no unheard `'error'`, a settle after a freeze, typed checkout timeouts.
|
|
1777
|
+
*/
|
|
1778
|
+
watchOwnedPool(pool, origin) {
|
|
1779
|
+
pool.on('error', (err) => this.emitPoolEvent('poolError', { error: err, ...origin }));
|
|
1780
|
+
(0, connection_guard_js_1.absorbCheckedOutErrors)(pool);
|
|
1781
|
+
this.checkoutWatches.push((0, connection_guard_js_1.watchCheckouts)(pool, (err) => this.emitPoolEvent('connectionLost', { error: err, ...origin })));
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* Hand a pool event to its listeners. A `poolError` with no listener is
|
|
1785
|
+
* logged at `error`, always; a listener that throws is ignored, like a query
|
|
1786
|
+
* listener.
|
|
1787
|
+
*/
|
|
1788
|
+
emitPoolEvent(kind, event) {
|
|
1789
|
+
const listeners = this.poolListeners[kind];
|
|
1790
|
+
if (listeners.size === 0) {
|
|
1791
|
+
if (kind === 'poolError') {
|
|
1792
|
+
(0, logger_js_1.logTo)(this.logger, 'error', `Unexpected ${event.pool} pool error: ${event.error.message}`, {
|
|
1793
|
+
code: 'pool-error',
|
|
1794
|
+
pool: event.pool,
|
|
1795
|
+
...(event.replica !== undefined ? { replica: event.replica } : {}),
|
|
1796
|
+
error: event.error,
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
for (const listener of listeners) {
|
|
1802
|
+
try {
|
|
1803
|
+
listener(event);
|
|
1804
|
+
}
|
|
1805
|
+
catch (e) {
|
|
1806
|
+
if (this.logging) {
|
|
1807
|
+
(0, logger_js_1.logTo)(this.logger, 'error', `${kind} listener error:`, {
|
|
1808
|
+
code: 'pool-listener-error',
|
|
1809
|
+
listener: kind,
|
|
1810
|
+
error: e,
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
/**
|
|
1817
|
+
* `statementTimeoutMs` and an explicit `maxLifetimeSeconds` reach only the
|
|
1818
|
+
* pools Turbine opens. On an external primary they do nothing, which a
|
|
1819
|
+
* warning says once, naming where the setting belongs instead. Always on: an
|
|
1820
|
+
* ignored timeout means no timeout in production.
|
|
1821
|
+
*/
|
|
1822
|
+
warnExternalPoolOptions(statementTimeoutMs, maxLifetimeSeconds) {
|
|
1823
|
+
if (this.ownsPool)
|
|
1824
|
+
return;
|
|
1825
|
+
const ownedReplicas = this.ownedReplicaPools.length;
|
|
1826
|
+
if (statementTimeoutMs !== undefined && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.connectionOptionIgnored, 'statementTimeoutMs')) {
|
|
1827
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `statementTimeoutMs: ${statementTimeoutMs} was not applied to the primary: this client was given an ` +
|
|
1828
|
+
"external `pool`, whose connections Turbine never opens. Set `statement_timeout` in the driver's own " +
|
|
1829
|
+
"connection setup, or on the role (`ALTER ROLE app SET statement_timeout = '30s'`)." +
|
|
1830
|
+
(ownedReplicas > 0 ? ' It IS applied to the Turbine-owned read replica pools.' : ''), { code: 'connection-option-ignored', option: 'statementTimeoutMs', ownedReplicas });
|
|
1831
|
+
}
|
|
1832
|
+
if (maxLifetimeSeconds !== undefined && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.connectionOptionIgnored, 'maxLifetimeSeconds')) {
|
|
1833
|
+
(0, logger_js_1.logTo)(this.logger, 'warn', `maxLifetimeSeconds: ${maxLifetimeSeconds} was not applied to the primary: this client was given ` +
|
|
1834
|
+
"an external `pool`, whose connection lifecycle the caller owns. Set it on that pool's own options." +
|
|
1835
|
+
(ownedReplicas > 0 ? ' It IS applied to the Turbine-owned read replica pools.' : ''), { code: 'connection-option-ignored', option: 'maxLifetimeSeconds', ownedReplicas });
|
|
1836
|
+
}
|
|
1142
1837
|
}
|
|
1143
1838
|
/**
|
|
1144
1839
|
* Refuse a `utcTimestamps` value that contradicts the one an earlier client
|
|
@@ -1243,14 +1938,18 @@ class TurbineClient {
|
|
|
1243
1938
|
// OUTERMOST, so leaving it out silently defeats all three above.
|
|
1244
1939
|
this.errorScopedTableCache.clear();
|
|
1245
1940
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
$
|
|
1250
|
-
this.
|
|
1941
|
+
$on(event, listener) {
|
|
1942
|
+
this.listenersFor(event, '$on').add(listener);
|
|
1943
|
+
}
|
|
1944
|
+
$off(event, listener) {
|
|
1945
|
+
this.listenersFor(event, '$off').delete(listener);
|
|
1251
1946
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1947
|
+
listenersFor(event, api) {
|
|
1948
|
+
if (event === 'query')
|
|
1949
|
+
return this.queryListeners;
|
|
1950
|
+
if (event === 'poolError' || event === 'connectionLost')
|
|
1951
|
+
return this.poolListeners[event];
|
|
1952
|
+
throw new errors_js_1.ValidationError(`${api}: unknown event ${JSON.stringify(event)}. Expected 'query', 'poolError' or 'connectionLost'.`);
|
|
1254
1953
|
}
|
|
1255
1954
|
// -------------------------------------------------------------------------
|
|
1256
1955
|
// Observability, automatic metrics collection
|
|
@@ -1340,21 +2039,23 @@ class TurbineClient {
|
|
|
1340
2039
|
*/
|
|
1341
2040
|
createErrorModeAccessor(target) {
|
|
1342
2041
|
const mode = this.errorMessageMode;
|
|
2042
|
+
// Keyed by name AND the function found under it, so a method replaced
|
|
2043
|
+
// after first use (a spy, a mock, an APM patch) is the one that runs.
|
|
1343
2044
|
const wrapped = new Map();
|
|
1344
2045
|
return new Proxy(target, {
|
|
1345
2046
|
get(t, prop, receiver) {
|
|
1346
2047
|
const value = Reflect.get(t, prop, receiver);
|
|
1347
2048
|
if (typeof value !== 'function' || typeof prop !== 'string')
|
|
1348
2049
|
return value;
|
|
1349
|
-
|
|
1350
|
-
if (
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
}
|
|
1357
|
-
return
|
|
2050
|
+
const hit = wrapped.get(prop);
|
|
2051
|
+
if (hit !== undefined && hit.fn === value)
|
|
2052
|
+
return hit.wrapper;
|
|
2053
|
+
const wrapper = (...args) => {
|
|
2054
|
+
const call = () => value.apply(t, args);
|
|
2055
|
+
return (0, errors_js_1.errorMessageModesDiverged)() ? (0, errors_js_1.runWithErrorMessageMode)(mode, call) : call();
|
|
2056
|
+
};
|
|
2057
|
+
wrapped.set(prop, { fn: value, wrapper });
|
|
2058
|
+
return wrapper;
|
|
1358
2059
|
},
|
|
1359
2060
|
});
|
|
1360
2061
|
}
|
|
@@ -1377,7 +2078,7 @@ class TurbineClient {
|
|
|
1377
2078
|
primaryTableQI(name) {
|
|
1378
2079
|
let qi = this.tableCache.get(name);
|
|
1379
2080
|
if (!qi) {
|
|
1380
|
-
qi = this.buildTableQI(this.
|
|
2081
|
+
qi = this.buildTableQI(this.queryPool, name);
|
|
1381
2082
|
this.tableCache.set(name, qi);
|
|
1382
2083
|
}
|
|
1383
2084
|
return qi;
|
|
@@ -1403,7 +2104,7 @@ class TurbineClient {
|
|
|
1403
2104
|
/** Construct a QueryInterface bound to `pool` (honoring any injected factory). */
|
|
1404
2105
|
buildTableQI(pool, name) {
|
|
1405
2106
|
return this.queryOptions?.queryInterfaceFactory
|
|
1406
|
-
? this.queryOptions.queryInterfaceFactory(pool, name, this.schema, this.middlewares, this.queryOptions)
|
|
2107
|
+
? (0, table_client_js_1.asQueryInterface)(this.queryOptions.queryInterfaceFactory(pool, name, this.schema, this.middlewares, this.queryOptions))
|
|
1407
2108
|
: new index_js_1.QueryInterface(pool, name, this.schema, this.middlewares, this.queryOptions);
|
|
1408
2109
|
}
|
|
1409
2110
|
/**
|
|
@@ -1414,20 +2115,30 @@ class TurbineClient {
|
|
|
1414
2115
|
createRoutingAccessor(name) {
|
|
1415
2116
|
const primaryQI = this.primaryTableQI(name);
|
|
1416
2117
|
const client = this;
|
|
2118
|
+
// One routing function per operation, so the accessor above can tell a
|
|
2119
|
+
// replaced method from this one by identity.
|
|
2120
|
+
const routed = new Map();
|
|
1417
2121
|
return new Proxy(primaryQI, {
|
|
1418
2122
|
get(target, prop, receiver) {
|
|
1419
|
-
|
|
2123
|
+
// A read replaced on the accessor (a spy, a mock) is an own property of
|
|
2124
|
+
// the primary instance, and runs as installed rather than routed past.
|
|
2125
|
+
if (typeof prop !== 'string' || !exports.READ_OPERATIONS.has(prop) || Object.hasOwn(target, prop)) {
|
|
2126
|
+
return Reflect.get(target, prop, receiver);
|
|
2127
|
+
}
|
|
2128
|
+
let route = routed.get(prop);
|
|
2129
|
+
if (route === undefined) {
|
|
1420
2130
|
// Pick the replica at CALL time so round-robin advances per operation.
|
|
1421
|
-
|
|
2131
|
+
route = (...args) => {
|
|
1422
2132
|
const replicaQI = client.nextReplicaTableQI(name);
|
|
1423
2133
|
const method = replicaQI[prop];
|
|
1424
2134
|
if (typeof method !== 'function') {
|
|
1425
|
-
return Reflect.get(target, prop, receiver);
|
|
2135
|
+
return Reflect.get(target, prop, receiver).apply(target, args);
|
|
1426
2136
|
}
|
|
1427
2137
|
return method.apply(replicaQI, args);
|
|
1428
2138
|
};
|
|
2139
|
+
routed.set(prop, route);
|
|
1429
2140
|
}
|
|
1430
|
-
return
|
|
2141
|
+
return route;
|
|
1431
2142
|
},
|
|
1432
2143
|
});
|
|
1433
2144
|
}
|
|
@@ -1464,27 +2175,13 @@ class TurbineClient {
|
|
|
1464
2175
|
}
|
|
1465
2176
|
return this.primaryView;
|
|
1466
2177
|
}
|
|
1467
|
-
// -------------------------------------------------------------------------
|
|
1468
|
-
// Pipeline, batch multiple queries into one round-trip
|
|
1469
|
-
// -------------------------------------------------------------------------
|
|
1470
|
-
/**
|
|
1471
|
-
* Execute multiple queries in a single database round-trip.
|
|
1472
|
-
*
|
|
1473
|
-
* Two call styles:
|
|
1474
|
-
* - `db.pipeline(q1, q2, q3)`, rest params (backward-compatible)
|
|
1475
|
-
* - `db.pipeline([q1, q2, q3], { transactional: false })`, array + options
|
|
1476
|
-
*
|
|
1477
|
-
* On pg.Pool-backed connections with TCP, this uses the real Postgres
|
|
1478
|
-
* extended-query pipeline protocol (one TCP flush, one round-trip).
|
|
1479
|
-
* On HTTP-based drivers it falls back to sequential execution.
|
|
1480
|
-
*/
|
|
1481
2178
|
async pipeline(...args) {
|
|
1482
2179
|
let queries;
|
|
1483
2180
|
let options;
|
|
1484
|
-
// Detect which overload was used
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
2181
|
+
// Detect which overload was used. A deferred query is never an array, so
|
|
2182
|
+
// an array first argument is the array form whatever it contains; its
|
|
2183
|
+
// items are checked below like the rest form's.
|
|
2184
|
+
if (args.length > 0 && Array.isArray(args[0])) {
|
|
1488
2185
|
// Array form: pipeline([q1, q2], opts?)
|
|
1489
2186
|
queries = args[0];
|
|
1490
2187
|
options = args[1];
|
|
@@ -1493,8 +2190,18 @@ class TurbineClient {
|
|
|
1493
2190
|
// Rest-param form: pipeline(q1, q2, q3)
|
|
1494
2191
|
queries = args;
|
|
1495
2192
|
}
|
|
2193
|
+
assertDeferredQueries(queries, 'pipeline', 'pipeline');
|
|
2194
|
+
// A locking read outside a transaction would release its locks before
|
|
2195
|
+
// the caller saw the rows; a transactional pipeline keeps them for the batch.
|
|
2196
|
+
if (options?.transactional === false)
|
|
2197
|
+
(0, row_lock_js_1.assertNoLockInAutocommitPipeline)(queries);
|
|
1496
2198
|
if (this.logging) {
|
|
1497
|
-
|
|
2199
|
+
const tags = queries.map((q) => q.tag);
|
|
2200
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `Pipeline: ${queries.length} queries, ${tags.join(', ')}`, {
|
|
2201
|
+
code: 'pipeline',
|
|
2202
|
+
queries: queries.length,
|
|
2203
|
+
tags,
|
|
2204
|
+
});
|
|
1498
2205
|
}
|
|
1499
2206
|
// Scoped like every other entry point. A pipeline builds NotFoundErrors (a
|
|
1500
2207
|
// `buildFindUniqueOrThrow` slot) and wraps driver errors into
|
|
@@ -1503,10 +2210,27 @@ class TurbineClient {
|
|
|
1503
2210
|
// where VALUES as soon as any verbose client existed in the process. The
|
|
1504
2211
|
// scope is established around the await, so every continuation of the batch
|
|
1505
2212
|
// resolves this client's mode.
|
|
2213
|
+
// Each statement carries the `sqlComment` its query interface would have
|
|
2214
|
+
// given it, computed here in the caller's async context.
|
|
2215
|
+
const comments = this.statementComments(queries);
|
|
2216
|
+
const sent = comments ? queries.map((q, i) => (0, sql_comment_js_1.commentDeferred)(q, comments[i])) : queries;
|
|
1506
2217
|
const sink = this.queryListeners.size > 0 ? this.queryOptions._onQuery : undefined;
|
|
1507
2218
|
if (!sink)
|
|
1508
|
-
return this.withErrorMode(() => (0, pipeline_js_1.executePipeline)(this.
|
|
1509
|
-
return this.withErrorMode(() => this.reportedPipeline(sink, queries, options));
|
|
2219
|
+
return this.withErrorMode(() => (0, pipeline_js_1.executePipeline)(this.queryPool, sent, options));
|
|
2220
|
+
return this.withErrorMode(() => this.reportedPipeline(sink, queries, options, sent, comments));
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* The `sqlComment` of each deferred query, named by its own model and action
|
|
2224
|
+
* (a pipeline can mix tables), or `undefined` when this client adds none.
|
|
2225
|
+
*/
|
|
2226
|
+
statementComments(queries) {
|
|
2227
|
+
const commenter = this.sqlCommenter;
|
|
2228
|
+
if (commenter === undefined)
|
|
2229
|
+
return undefined;
|
|
2230
|
+
return queries.map((q) => {
|
|
2231
|
+
const { model, action } = (0, query_events_js_1.deferredIdentity)(q.tag);
|
|
2232
|
+
return commenter.commentFor(model, action)?.comment;
|
|
2233
|
+
});
|
|
1510
2234
|
}
|
|
1511
2235
|
/**
|
|
1512
2236
|
* {@link pipeline} with one `$on('query')` event per statement. Only taken
|
|
@@ -1517,9 +2241,9 @@ class TurbineClient {
|
|
|
1517
2241
|
* time is split evenly across them and each event carries
|
|
1518
2242
|
* `batch: 'pipeline'`.
|
|
1519
2243
|
*/
|
|
1520
|
-
async reportedPipeline(sink, queries, options) {
|
|
2244
|
+
async reportedPipeline(sink, queries, options, sent, comments) {
|
|
1521
2245
|
const rowCounts = new Array(queries.length).fill(undefined);
|
|
1522
|
-
const observed =
|
|
2246
|
+
const observed = sent.map((q, i) => ({
|
|
1523
2247
|
...q,
|
|
1524
2248
|
transform: (raw) => {
|
|
1525
2249
|
rowCounts[i] = (0, query_events_js_1.resultRowCount)(raw);
|
|
@@ -1529,7 +2253,7 @@ class TurbineClient {
|
|
|
1529
2253
|
const start = performance.now();
|
|
1530
2254
|
let failure;
|
|
1531
2255
|
try {
|
|
1532
|
-
return await (0, pipeline_js_1.executePipeline)(this.
|
|
2256
|
+
return await (0, pipeline_js_1.executePipeline)(this.queryPool, observed, options);
|
|
1533
2257
|
}
|
|
1534
2258
|
catch (err) {
|
|
1535
2259
|
failure = err;
|
|
@@ -1555,6 +2279,7 @@ class TurbineClient {
|
|
|
1555
2279
|
...(0, query_events_js_1.deferredIdentity)(q.tag),
|
|
1556
2280
|
rows: rowCounts[i] ?? 0,
|
|
1557
2281
|
batch: 'pipeline',
|
|
2282
|
+
...(comments?.[i] === undefined ? {} : { comment: comments[i] }),
|
|
1558
2283
|
...(failed ? { error: failure instanceof Error ? failure : new Error(String(failure)) } : {}),
|
|
1559
2284
|
});
|
|
1560
2285
|
}
|
|
@@ -1567,13 +2292,17 @@ class TurbineClient {
|
|
|
1567
2292
|
* drivers (Neon HTTP, Vercel Postgres, etc.) and mock pools.
|
|
1568
2293
|
*/
|
|
1569
2294
|
async pipelineSupported() {
|
|
1570
|
-
return (0, pipeline_js_1.pipelineSupported)(this.
|
|
2295
|
+
return (0, pipeline_js_1.pipelineSupported)(this.queryPool);
|
|
1571
2296
|
}
|
|
1572
2297
|
// -------------------------------------------------------------------------
|
|
1573
2298
|
// Raw SQL, tagged template literal escape hatch
|
|
1574
2299
|
// -------------------------------------------------------------------------
|
|
1575
2300
|
/**
|
|
1576
2301
|
* Execute a raw SQL query with parameter interpolation via tagged templates.
|
|
2302
|
+
* Every `${value}` is sent as a bound parameter. Only a real tagged-template
|
|
2303
|
+
* call is accepted: an array passed in its place throws `ValidationError`
|
|
2304
|
+
* (E003), since its text would reach the SQL unescaped. For statement text
|
|
2305
|
+
* built at runtime, see {@link rawUnsafe}.
|
|
1577
2306
|
*
|
|
1578
2307
|
* @example
|
|
1579
2308
|
* ```ts
|
|
@@ -1585,17 +2314,40 @@ class TurbineClient {
|
|
|
1585
2314
|
* ```
|
|
1586
2315
|
*/
|
|
1587
2316
|
async raw(strings, ...values) {
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2317
|
+
const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect, 'raw');
|
|
2318
|
+
return this.runRaw(sql, params, 'raw');
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* Execute SQL text built at runtime: DDL read from a file, a statement in
|
|
2322
|
+
* another engine's own language (PowQL), a migration step. Values go in
|
|
2323
|
+
* `values` and are referenced from the text with the engine's placeholders
|
|
2324
|
+
* (`$1`, `$2`, ... on PostgreSQL), so they are still bound.
|
|
2325
|
+
*
|
|
2326
|
+
* NOTHING IN `text` IS ESCAPED. It is the one raw entry point where
|
|
2327
|
+
* concatenating input into the statement is possible, which is why it is
|
|
2328
|
+
* separate from {@link raw} and named for it: never build `text` from
|
|
2329
|
+
* request data. Prefer {@link raw} or {@link sql} wherever the statement is
|
|
2330
|
+
* known when the code is written.
|
|
2331
|
+
*
|
|
2332
|
+
* @example
|
|
2333
|
+
* ```ts
|
|
2334
|
+
* for (const statement of ddlStatements) await db.rawUnsafe(statement);
|
|
2335
|
+
* await db.rawUnsafe('SELECT * FROM users WHERE id = $1', userId);
|
|
2336
|
+
* ```
|
|
2337
|
+
*/
|
|
2338
|
+
async rawUnsafe(text, ...values) {
|
|
2339
|
+
assertSqlText(text, 'rawUnsafe');
|
|
2340
|
+
return this.runRaw(text, values, 'rawUnsafe');
|
|
2341
|
+
}
|
|
2342
|
+
async runRaw(sql, values, action) {
|
|
2343
|
+
const params = this.rawParams(values, action);
|
|
1595
2344
|
if (this.logging) {
|
|
1596
|
-
|
|
2345
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `Raw SQL: ${sql.trim().substring(0, 120)}...`, {
|
|
2346
|
+
code: 'raw-sql',
|
|
2347
|
+
operation: action,
|
|
2348
|
+
});
|
|
1597
2349
|
}
|
|
1598
|
-
const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions._onQuery, (0, query_events_js_1.rawIdentity)(
|
|
2350
|
+
const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions._onQuery, (0, query_events_js_1.rawIdentity)(action), sql, params, () => this.queryPool.query(sql, params), (err) => this.withErrorMode(() => (0, errors_js_1.wrapPgError)(err)));
|
|
1599
2351
|
return result.rows;
|
|
1600
2352
|
}
|
|
1601
2353
|
/**
|
|
@@ -1608,9 +2360,20 @@ class TurbineClient {
|
|
|
1608
2360
|
* the pool. Errors are translated by `wrapPgError`, like {@link raw}.
|
|
1609
2361
|
*/
|
|
1610
2362
|
async rawQuery(text, params = [], action = 'rawQuery') {
|
|
1611
|
-
const
|
|
2363
|
+
const bound = this.rawParams(params, action);
|
|
2364
|
+
const result = await (0, query_events_js_1.runReportedStatement)(this.queryOptions._onQuery, (0, query_events_js_1.rawIdentity)(action), text, bound, () => this.queryPool.query(text, bound), (err) => this.withErrorMode(() => (0, errors_js_1.wrapPgError)(err)));
|
|
1612
2365
|
return { rows: result.rows, rowCount: result.rowCount };
|
|
1613
2366
|
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Bound Dates as UTC text, on a PostgreSQL client reading zone-less temporal
|
|
2369
|
+
* types as UTC (see {@link utcDateParams}). Typed builder writes convert per
|
|
2370
|
+
* column instead and never come through here.
|
|
2371
|
+
*/
|
|
2372
|
+
rawParams(params, api) {
|
|
2373
|
+
return this.dialect.capabilities.utcTimestampRewrite && this.queryOptions.utcTimestamps !== false
|
|
2374
|
+
? (0, typed_sql_js_1.utcDateParams)(params, api)
|
|
2375
|
+
: params;
|
|
2376
|
+
}
|
|
1614
2377
|
/**
|
|
1615
2378
|
* Run `fn` with `tag` attached to every `$on('query')` event it causes, so a
|
|
1616
2379
|
* listener can attribute queries to the feature or code path that issued
|
|
@@ -1664,14 +2427,15 @@ class TurbineClient {
|
|
|
1664
2427
|
* ```
|
|
1665
2428
|
*/
|
|
1666
2429
|
sql(strings, ...values) {
|
|
1667
|
-
const { sql, params } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect);
|
|
2430
|
+
const { sql, params: bound } = (0, typed_sql_js_1.buildTypedSql)(strings, values, this.dialect);
|
|
2431
|
+
const params = this.rawParams(bound, 'sql');
|
|
1668
2432
|
// The scope has to be handed to the BUILDER, not wrapped around this call.
|
|
1669
2433
|
// `TypedSqlQuery` is lazy (it runs on `await` / `.one()` / `.scalar()`), so
|
|
1670
2434
|
// a scope opened here would close before any row was fetched. The
|
|
1671
2435
|
// `withErrorMode` docstring already claimed to cover the typed-SQL builder
|
|
1672
2436
|
// and did not, which left two raw-SQL entry points disagreeing about the
|
|
1673
2437
|
// same statement, since the adjacent `raw` tag WAS scoped.
|
|
1674
|
-
return new typed_sql_js_1.TypedSqlQuery(this.
|
|
2438
|
+
return new typed_sql_js_1.TypedSqlQuery(this.queryPool, sql, params, this.logging, (fn) => this.withErrorMode(fn), this.queryOptions._onQuery, this.logger);
|
|
1675
2439
|
}
|
|
1676
2440
|
// -------------------------------------------------------------------------
|
|
1677
2441
|
// Transaction support (raw, legacy)
|
|
@@ -1681,10 +2445,16 @@ class TurbineClient {
|
|
|
1681
2445
|
* connection. For the typed API, use `$transaction()` instead.
|
|
1682
2446
|
*
|
|
1683
2447
|
* The client is typed as {@link PgCompatPoolClient}, the driver-neutral
|
|
1684
|
-
* contract: `query(text, values)
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
1687
|
-
*
|
|
2448
|
+
* contract: `query(text, values)`, which is all any supported driver
|
|
2449
|
+
* guarantees. For the pg-only surface (cursors, COPY streams, the
|
|
2450
|
+
* `QueryConfig` object form), import `PoolClient` from the pg package and
|
|
2451
|
+
* cast: `client as unknown as PoolClient`.
|
|
2452
|
+
*
|
|
2453
|
+
* Like `$transaction`'s `tx`, the client is closed when the transaction
|
|
2454
|
+
* settles: a query sent through it afterwards throws `ValidationError`
|
|
2455
|
+
* (E003). The callback must not `release()` it, and must await every query
|
|
2456
|
+
* it starts: one still running when the callback returns rolls the
|
|
2457
|
+
* transaction back with E003.
|
|
1688
2458
|
*
|
|
1689
2459
|
* @example
|
|
1690
2460
|
* ```ts
|
|
@@ -1702,28 +2472,32 @@ class TurbineClient {
|
|
|
1702
2472
|
// driver with one shared engine handle (PowDB embedded) would roll back a
|
|
1703
2473
|
// DIFFERENT caller's open transaction.
|
|
1704
2474
|
const { client, checkout } = await (0, checkout_js_1.openCheckout)(this.pool, (c) => runTxControl(c, this.dialect.beginStatement()));
|
|
2475
|
+
const state = new transaction_state_js_1.TransactionState(client, this.dialect, { api: 'transaction' });
|
|
2476
|
+
const guarded = closableRawClient(client, state);
|
|
1705
2477
|
try {
|
|
1706
2478
|
// Engine seam: single-writer engines scope their transaction re-entrancy
|
|
1707
2479
|
// marker to the callback's async subtree (see
|
|
1708
2480
|
// PgCompatPoolClient.wrapTransactionCallback). Absent everywhere else.
|
|
1709
2481
|
const wrap = client.wrapTransactionCallback;
|
|
1710
2482
|
// `.call` erases the generic, so the callback's Promise<T> is re-asserted.
|
|
1711
|
-
const result = wrap ? (await wrap.call(client, () => fn(
|
|
1712
|
-
|
|
2483
|
+
const result = wrap ? (await wrap.call(client, () => fn(guarded))) : await fn(guarded);
|
|
2484
|
+
const unawaited = state.busy;
|
|
2485
|
+
state.end('has ended');
|
|
2486
|
+
if (unawaited)
|
|
2487
|
+
throw (0, transaction_state_js_1.unawaitedWorkError)('transaction');
|
|
2488
|
+
// A COMMIT answered with ROLLBACK (a statement failed and the callback
|
|
2489
|
+
// caught it) rejects, with the first failed statement as the cause.
|
|
2490
|
+
await (0, transaction_state_js_1.commitTransaction)(client, this.dialect.commitStatement(), state.firstFailure, 'transaction');
|
|
2491
|
+
state.end('committed');
|
|
2492
|
+
checkout.release();
|
|
1713
2493
|
return result;
|
|
1714
2494
|
}
|
|
1715
2495
|
catch (err) {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
catch {
|
|
1720
|
-
// Best-effort rollback, the connection may have died mid-query.
|
|
1721
|
-
}
|
|
2496
|
+
state.end('was rolled back');
|
|
2497
|
+
const clean = await rollbackOnServer(client, state, this.dialect);
|
|
2498
|
+
checkout.release(clean ? undefined : new Error('[turbine] transaction connection discarded: rollback not confirmed'));
|
|
1722
2499
|
throw (0, errors_js_1.explainConnectionLoss)(err, checkout.lostWith);
|
|
1723
2500
|
}
|
|
1724
|
-
finally {
|
|
1725
|
-
checkout.release();
|
|
1726
|
-
}
|
|
1727
2501
|
}
|
|
1728
2502
|
async $transaction(fnOrQueries, options) {
|
|
1729
2503
|
// Scope the WHOLE call, so the transaction-scoped QueryInterfaces built
|
|
@@ -1733,145 +2507,183 @@ class TurbineClient {
|
|
|
1733
2507
|
}
|
|
1734
2508
|
async runTransaction(fnOrQueries, options) {
|
|
1735
2509
|
// Batch overload: an array of DeferredQuery objects runs atomically inside
|
|
1736
|
-
// one BEGIN…COMMIT,
|
|
2510
|
+
// one BEGIN…COMMIT, through the same lifecycle (and options) as a callback.
|
|
1737
2511
|
if (Array.isArray(fnOrQueries)) {
|
|
1738
|
-
|
|
2512
|
+
assertDeferredQueries(fnOrQueries, '$transaction([...])', 'transaction');
|
|
2513
|
+
if (fnOrQueries.length === 0) {
|
|
2514
|
+
this.resolveTransactionOptions(options);
|
|
2515
|
+
return [];
|
|
2516
|
+
}
|
|
2517
|
+
return this.runInTransaction(options, (client, state) => this.runBatch(client, state, fnOrQueries));
|
|
2518
|
+
}
|
|
2519
|
+
if (typeof fnOrQueries !== 'function') {
|
|
2520
|
+
throw new errors_js_1.ValidationError('$transaction expects a callback, `async (tx) => { ... }`, or an array of deferred queries built with ' +
|
|
2521
|
+
`\`build*()\`, but received ${fnOrQueries === null ? 'null' : typeof fnOrQueries}.`);
|
|
1739
2522
|
}
|
|
1740
2523
|
const fn = fnOrQueries;
|
|
1741
|
-
//
|
|
1742
|
-
// the
|
|
2524
|
+
// Pass the parent pool so its read-only guard + PowDB capabilities flow
|
|
2525
|
+
// into the transaction-scoped proxy pool (see TransactionClient.createTxPool).
|
|
2526
|
+
const sourcePool = this.pool;
|
|
2527
|
+
return this.runInTransaction(options, (client, state) => fn(new TransactionClient(client, this.schema, this.middlewares, this.queryOptions, sourcePool, state)));
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Validate every transaction option and build the statements they need,
|
|
2531
|
+
* BEFORE a pool slot is taken: a bad argument is the caller's bug and should
|
|
2532
|
+
* not cost a connection to discover.
|
|
2533
|
+
*/
|
|
2534
|
+
resolveTransactionOptions(options) {
|
|
1743
2535
|
const isolationSql = resolveIsolationLevel(options?.isolationLevel);
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
// runs inside openCheckout, which sends it once more on a fresh connection
|
|
1747
|
-
// when the first one turns out to be dead, and which releases the
|
|
1748
|
-
// connection itself when BEGIN fails for good. So everything below runs in
|
|
1749
|
-
// a transaction that began.
|
|
1750
|
-
const { client, checkout } = await (0, checkout_js_1.openCheckout)(this.pool, (c) => runTxControl(c, this.dialect.beginStatement(isolationSql)));
|
|
2536
|
+
const begin = beginStatementFor(this.dialect, isolationSql, resolveReadOnly(options?.readOnly));
|
|
2537
|
+
const sessionConfig = this.resolveSessionContext(options?.sessionContext, options?.allowBuiltinSettings);
|
|
1751
2538
|
const timeout = options?.timeout;
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2539
|
+
if (timeout !== undefined && timeout !== 0 && !(typeof timeout === 'number' && timeout > 0 && timeout < 2 ** 31)) {
|
|
2540
|
+
throw new errors_js_1.ValidationError(`$transaction: timeout must be a positive number of milliseconds (or 0 for none), received ${describeValue(timeout)}.`);
|
|
2541
|
+
}
|
|
2542
|
+
return { begin, sessionConfig, timeout: timeout || undefined };
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* The one transaction lifecycle both `$transaction` forms share, so an option
|
|
2546
|
+
* one form honours is honoured by the other: BEGIN (isolation level, read
|
|
2547
|
+
* only) -> `sessionContext` GUCs -> body, raced against `timeout` -> COMMIT,
|
|
2548
|
+
* and ROLLBACK on any failure.
|
|
2549
|
+
*
|
|
2550
|
+
* The transaction state is ended as soon as the body settles, BEFORE the
|
|
2551
|
+
* COMMIT, so a `tx` that leaked out of the callback can never slip a
|
|
2552
|
+
* statement in ahead of the COMMIT or onto the connection after it. Work the
|
|
2553
|
+
* body started and did not await is caught there too: the transaction rolls
|
|
2554
|
+
* back rather than committing whatever part of it had run.
|
|
2555
|
+
*
|
|
2556
|
+
* A transaction that will not commit (timed out, failed, unawaited work) is
|
|
2557
|
+
* stopped on the server before its connection is released: the running
|
|
2558
|
+
* statement is cancelled and ROLLBACK sent, so its locks are freed now (see
|
|
2559
|
+
* {@link rollbackOnServer}). A connection that cannot be confirmed clean is
|
|
2560
|
+
* destroyed instead of reused.
|
|
2561
|
+
*/
|
|
2562
|
+
async runInTransaction(options, body) {
|
|
2563
|
+
const { begin, sessionConfig, timeout } = this.resolveTransactionOptions(options);
|
|
2564
|
+
// openCheckout sends BEGIN once more on a fresh connection when the first
|
|
2565
|
+
// one turns out to be dead, and releases the connection itself when BEGIN
|
|
2566
|
+
// fails for good. So everything below runs in a transaction that began.
|
|
2567
|
+
const { client, checkout } = await (0, checkout_js_1.openCheckout)(this.queryPool, (c) => runTxControl(c, begin));
|
|
2568
|
+
const state = new transaction_state_js_1.TransactionState(client, this.dialect, { userTransaction: true });
|
|
1769
2569
|
let timedOut = false;
|
|
1770
2570
|
try {
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
// SET LOCAL, so both name and value are BOUND params, never interpolated.
|
|
1777
|
-
if (options?.sessionContext) {
|
|
1778
|
-
if (!this.dialect.supportsRLS) {
|
|
1779
|
-
throw new errors_js_1.UnsupportedFeatureError('sessionContext (RLS session GUCs)', this.dialect.name, 'set_config-based row-level-security context requires PostgreSQL.');
|
|
1780
|
-
}
|
|
1781
|
-
for (const [name, value] of Object.entries(options.sessionContext)) {
|
|
1782
|
-
if (!GUC_NAME_REGEX.test(name)) {
|
|
1783
|
-
throw new errors_js_1.ValidationError(`Invalid session-context GUC name "${name}", must match ` +
|
|
1784
|
-
'/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/ (optionally namespaced, e.g. "app.current_tenant")');
|
|
1785
|
-
}
|
|
1786
|
-
const cfg = this.dialect.buildSetSessionConfig(name, String(value));
|
|
1787
|
-
await client.query(cfg.sql, cfg.params);
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
// Create the transaction client with typed table accessors. Pass the
|
|
1791
|
-
// parent pool so its read-only guard + PowDB capabilities flow into the
|
|
1792
|
-
// transaction-scoped proxy pool (see TransactionClient.createTxPool).
|
|
1793
|
-
const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions, this.pool);
|
|
1794
|
-
// Dynamically attach table accessors to tx
|
|
1795
|
-
for (const tableName of Object.keys(this.schema.tables)) {
|
|
1796
|
-
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
1797
|
-
if (!(camelName in tx)) {
|
|
1798
|
-
Object.defineProperty(tx, camelName, {
|
|
1799
|
-
get: () => tx.table(tableName),
|
|
1800
|
-
enumerable: true,
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
let result;
|
|
2571
|
+
// Transaction-local session context (RLS / multi-tenant GUCs), applied
|
|
2572
|
+
// after BEGIN and before the body via set_config(name, value,
|
|
2573
|
+
// is_local=true), so both name and value are BOUND params.
|
|
2574
|
+
for (const cfg of sessionConfig)
|
|
2575
|
+
await applySessionSetting(client, cfg);
|
|
1805
2576
|
// Engine seam: when the checked-out connection exposes
|
|
1806
|
-
// wrapTransactionCallback (single-writer engines such as PowDB), run the
|
|
1807
|
-
//
|
|
1808
|
-
//
|
|
2577
|
+
// wrapTransactionCallback (single-writer engines such as PowDB), run the
|
|
2578
|
+
// body through it so the engine can scope its re-entrancy marker to the
|
|
2579
|
+
// body's async subtree. All other drivers: plain body().
|
|
1809
2580
|
const wrap = client.wrapTransactionCallback;
|
|
1810
|
-
const
|
|
2581
|
+
const runBody = () => state.enter(() => (wrap ? wrap.call(client, () => body(client, state)) : body(client, state)));
|
|
2582
|
+
let result;
|
|
1811
2583
|
if (timeout) {
|
|
1812
|
-
// Race between the function and a timeout. If the timeout fires we
|
|
1813
|
-
// need to actually abort the in-flight query, otherwise the backend
|
|
1814
|
-
// keeps running until pg's own timeout, holding a pool slot the whole
|
|
1815
|
-
// time. The simplest reliable cancellation is to destroy the
|
|
1816
|
-
// connection: passing a truthy argument to client.release() tells the
|
|
1817
|
-
// pg pool to discard the client (its socket is closed, which causes
|
|
1818
|
-
// Postgres to abort the active query and roll back the transaction).
|
|
1819
|
-
// The pool will spin up a fresh connection on the next checkout.
|
|
1820
2584
|
let timer;
|
|
1821
2585
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1822
2586
|
timer = setTimeout(() => {
|
|
1823
2587
|
timedOut = true;
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2588
|
+
state.end('timed out');
|
|
2589
|
+
reject(new errors_js_1.TimeoutError(timeout, 'Transaction', {
|
|
2590
|
+
message: `Transaction timed out after ${timeout}ms and was rolled back, so none of its writes were ` +
|
|
2591
|
+
'saved. A statement it was running was cancelled on the server.',
|
|
2592
|
+
outcome: 'not-executed',
|
|
2593
|
+
}));
|
|
1829
2594
|
}, timeout);
|
|
1830
2595
|
});
|
|
1831
2596
|
try {
|
|
1832
|
-
result = await Promise.race([
|
|
2597
|
+
result = await Promise.race([runBody(), timeoutPromise]);
|
|
1833
2598
|
}
|
|
1834
2599
|
finally {
|
|
1835
2600
|
clearTimeout(timer);
|
|
1836
2601
|
}
|
|
1837
2602
|
}
|
|
1838
2603
|
else {
|
|
1839
|
-
result = await
|
|
2604
|
+
result = await runBody();
|
|
1840
2605
|
}
|
|
1841
|
-
|
|
2606
|
+
const unawaited = state.busy;
|
|
2607
|
+
state.end('has ended');
|
|
2608
|
+
if (unawaited)
|
|
2609
|
+
throw (0, transaction_state_js_1.unawaitedWorkError)('$transaction');
|
|
2610
|
+
// A statement inside was cut short by its timeout, and the callback
|
|
2611
|
+
// caught the error: whether it ran is not known, so nothing commits.
|
|
2612
|
+
if (state.abortedWith !== undefined)
|
|
2613
|
+
throw (0, transaction_state_js_1.rolledBackError)(state.abortedWith, 'transaction');
|
|
2614
|
+
await (0, transaction_state_js_1.commitTransaction)(client, this.dialect.commitStatement(), state.firstFailure);
|
|
2615
|
+
state.end('committed');
|
|
2616
|
+
checkout.release();
|
|
1842
2617
|
if (this.logging) {
|
|
1843
|
-
|
|
2618
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', 'Transaction committed', {
|
|
2619
|
+
code: 'transaction-committed',
|
|
2620
|
+
operation: '$transaction',
|
|
2621
|
+
});
|
|
1844
2622
|
}
|
|
1845
2623
|
return result;
|
|
1846
2624
|
}
|
|
1847
2625
|
catch (err) {
|
|
1848
|
-
//
|
|
1849
|
-
//
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
try {
|
|
1855
|
-
await client.query(this.dialect.rollbackStatement());
|
|
1856
|
-
}
|
|
1857
|
-
catch {
|
|
1858
|
-
// Best-effort rollback, the connection may have died mid-query.
|
|
1859
|
-
}
|
|
1860
|
-
}
|
|
2626
|
+
// A timed-out transaction keeps its 'timed out' wording for any late
|
|
2627
|
+
// statement the callback still tries to send.
|
|
2628
|
+
if (!timedOut)
|
|
2629
|
+
state.end('was rolled back');
|
|
2630
|
+
const clean = await rollbackOnServer(client, state, this.dialect);
|
|
2631
|
+
checkout.release(clean ? undefined : new Error('[turbine] transaction connection discarded: rollback not confirmed'));
|
|
1861
2632
|
if (this.logging) {
|
|
1862
|
-
|
|
2633
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', 'Transaction rolled back', {
|
|
2634
|
+
code: 'transaction-rolled-back',
|
|
2635
|
+
operation: '$transaction',
|
|
2636
|
+
});
|
|
1863
2637
|
}
|
|
1864
2638
|
throw (0, errors_js_1.explainConnectionLoss)(err, checkout.lostWith);
|
|
1865
2639
|
}
|
|
1866
|
-
|
|
1867
|
-
|
|
2640
|
+
}
|
|
2641
|
+
/**
|
|
2642
|
+
* Validate `sessionContext` and build its `set_config` statements, before
|
|
2643
|
+
* any connection is taken.
|
|
2644
|
+
*
|
|
2645
|
+
* A name must be a namespaced custom setting (`app.tenant_id`) unless it is
|
|
2646
|
+
* listed in `allowBuiltinSettings`: an un-dotted name is one of PostgreSQL's
|
|
2647
|
+
* own settings (`role`, `search_path`, `statement_timeout`), and a context
|
|
2648
|
+
* built from request data must not be able to reach one. A value that is not
|
|
2649
|
+
* a string, number or boolean is refused, so a missing tenant id cannot reach
|
|
2650
|
+
* a row-level security policy as the text `'undefined'`.
|
|
2651
|
+
*/
|
|
2652
|
+
resolveSessionContext(context, allowBuiltinSettings) {
|
|
2653
|
+
const allowed = resolveBuiltinSettingsAllowlist(allowBuiltinSettings);
|
|
2654
|
+
if (!context)
|
|
2655
|
+
return [];
|
|
2656
|
+
if (!this.dialect.supportsRLS) {
|
|
2657
|
+
throw new errors_js_1.UnsupportedFeatureError('sessionContext (RLS session GUCs)', this.dialect.name, 'set_config-based row-level-security context requires PostgreSQL.');
|
|
2658
|
+
}
|
|
2659
|
+
const statements = [];
|
|
2660
|
+
for (const [name, value] of Object.entries(context)) {
|
|
2661
|
+
if (!GUC_NAME_REGEX.test(name)) {
|
|
2662
|
+
throw new errors_js_1.ValidationError(`Invalid session-context GUC name "${name}", must match ` +
|
|
2663
|
+
'/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/ (optionally namespaced, e.g. "app.current_tenant")');
|
|
2664
|
+
}
|
|
2665
|
+
if (!name.includes('.') && !allowed.has(name.toLowerCase())) {
|
|
2666
|
+
throw new errors_js_1.ValidationError(`sessionContext["${name}"] names a PostgreSQL setting, not a custom one. A session context may set ` +
|
|
2667
|
+
'namespaced custom settings only (`app.tenant_id`, with a dot), because a built-in setting such as ' +
|
|
2668
|
+
'`role`, `search_path` or `statement_timeout` changes who the transaction runs as or what limits it ' +
|
|
2669
|
+
`has. Rename it into a namespace, or, if your code sets it on purpose, pass \`allowBuiltinSettings: ['${name}']\`.`);
|
|
2670
|
+
}
|
|
2671
|
+
const kind = typeof value;
|
|
2672
|
+
if (kind !== 'string' && kind !== 'number' && kind !== 'boolean') {
|
|
2673
|
+
throw new errors_js_1.ValidationError(`sessionContext["${name}"] is ${value === null ? 'null' : kind}. A session setting is text, and ` +
|
|
2674
|
+
`${value === undefined ? 'a missing value' : 'this value'} would reach your row-level security ` +
|
|
2675
|
+
`policies as the string "${String(value)}". Pass a string, number or boolean, and check the value ` +
|
|
2676
|
+
'your request context supplies.');
|
|
2677
|
+
}
|
|
2678
|
+
statements.push({ name, ...this.dialect.buildSetSessionConfig(name, String(value)) });
|
|
1868
2679
|
}
|
|
2680
|
+
return statements;
|
|
1869
2681
|
}
|
|
1870
2682
|
/**
|
|
1871
|
-
*
|
|
1872
|
-
*
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
2683
|
+
* Run a batch of {@link DeferredQuery} objects on a transaction's connection.
|
|
2684
|
+
* Backs the `$transaction([...])` array overload; the transaction itself
|
|
2685
|
+
* (BEGIN/COMMIT/ROLLBACK, options, release) is {@link runInTransaction}'s.
|
|
2686
|
+
* Each result is passed through its query's `transform`.
|
|
1875
2687
|
*
|
|
1876
2688
|
* Execution strategy on the single transaction connection:
|
|
1877
2689
|
* - **Sequential (default).** Await each statement's reply before sending
|
|
@@ -1892,44 +2704,48 @@ class TurbineClient {
|
|
|
1892
2704
|
* rethrowing, which keeps the connection's request/reply pairing intact and
|
|
1893
2705
|
* means ROLLBACK is only issued once no statement is still in flight.
|
|
1894
2706
|
*/
|
|
1895
|
-
async
|
|
1896
|
-
if (queries.length === 0) {
|
|
1897
|
-
return [];
|
|
1898
|
-
}
|
|
2707
|
+
async runBatch(client, state, queries) {
|
|
1899
2708
|
// One `$on('query')` event per statement, `batch: 'transaction'`, only
|
|
1900
|
-
// when someone is listening; each statement is timed on its own.
|
|
2709
|
+
// when someone is listening; each statement is timed on its own. Every
|
|
2710
|
+
// statement goes through the transaction state, so a timeout can tell
|
|
2711
|
+
// what is still on the wire and stop it.
|
|
1901
2712
|
const sink = this.queryListeners.size > 0 ? this.queryOptions._onQuery : undefined;
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
results.push(queries[i].transform(outcome.value));
|
|
1918
|
-
}
|
|
1919
|
-
return results;
|
|
1920
|
-
}
|
|
2713
|
+
// Each statement is sent with its `sqlComment`; its event reports the
|
|
2714
|
+
// commentless text and the comment beside it, as a model call's does.
|
|
2715
|
+
const comments = this.statementComments(queries);
|
|
2716
|
+
const reported = (i, run) => {
|
|
2717
|
+
const dq = queries[i];
|
|
2718
|
+
const comment = comments?.[i];
|
|
2719
|
+
return state.run(() => (0, query_events_js_1.runReportedStatement)(sink, { ...(0, query_events_js_1.deferredIdentity)(dq.tag), batch: 'transaction', ...(comment === undefined ? {} : { comment }) }, dq.sql, dq.params, () => run((0, sql_comment_js_1.commentDeferred)(dq, comment)), errors_js_1.wrapPgError));
|
|
2720
|
+
};
|
|
2721
|
+
const pipelined = client.supportsPipelining === true &&
|
|
2722
|
+
this.dialect.resultStrategy !== 'reselect';
|
|
2723
|
+
if (pipelined) {
|
|
2724
|
+
// Dispatch every statement before awaiting any reply. The driver's
|
|
2725
|
+
// FIFO guarantee makes settled[i] the reply to queries[i]. Each
|
|
2726
|
+
// statement is timed from its dispatch to its own reply.
|
|
2727
|
+
const settled = await Promise.allSettled(queries.map((_, i) => reported(i, (dq) => client.query(dq.sql, dq.params))));
|
|
1921
2728
|
const results = [];
|
|
1922
|
-
for (
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
: client.query(dq.sql, dq.params));
|
|
1929
|
-
results.push(dq.transform(raw));
|
|
2729
|
+
for (let i = 0; i < settled.length; i++) {
|
|
2730
|
+
const outcome = settled[i];
|
|
2731
|
+
if (outcome.status === 'rejected') {
|
|
2732
|
+
throw (0, errors_js_1.wrapPgError)(outcome.reason);
|
|
2733
|
+
}
|
|
2734
|
+
results.push(queries[i].transform(outcome.value));
|
|
1930
2735
|
}
|
|
1931
2736
|
return results;
|
|
1932
|
-
}
|
|
2737
|
+
}
|
|
2738
|
+
const results = [];
|
|
2739
|
+
for (let i = 0; i < queries.length; i++) {
|
|
2740
|
+
// Non-RETURNING engines (resultStrategy 'reselect', e.g. MySQL) attach a
|
|
2741
|
+
// reselect plan that runs the write plus a follow-up SELECT; running
|
|
2742
|
+
// dq.sql alone would transform a row-less write result.
|
|
2743
|
+
const raw = await reported(i, (dq) => this.dialect.resultStrategy === 'reselect' && dq.reselect
|
|
2744
|
+
? dq.reselect((sql, params) => client.query(sql, params))
|
|
2745
|
+
: client.query(dq.sql, dq.params));
|
|
2746
|
+
results.push(queries[i].transform(raw));
|
|
2747
|
+
}
|
|
2748
|
+
return results;
|
|
1933
2749
|
}
|
|
1934
2750
|
/**
|
|
1935
2751
|
* Convenience wrapper around `$transaction` for the multi-tenant / RLS case:
|
|
@@ -1978,7 +2794,8 @@ class TurbineClient {
|
|
|
1978
2794
|
* ends it instead. Postgres does not hold notifications for a disconnected
|
|
1979
2795
|
* listener, so anything sent during the gap is lost: use
|
|
1980
2796
|
* `options.onReconnect` to resynchronise. `options.onError` receives the loss
|
|
1981
|
-
* and each failed attempt (default: one
|
|
2797
|
+
* and each failed attempt (default: one log line each through `logger`, at
|
|
2798
|
+
* `warn` while it reconnects and `error` once it stops).
|
|
1982
2799
|
*
|
|
1983
2800
|
* @example
|
|
1984
2801
|
* ```ts
|
|
@@ -1999,11 +2816,11 @@ class TurbineClient {
|
|
|
1999
2816
|
(0, realtime_js_1.validateChannel)(channel);
|
|
2000
2817
|
const quoted = (0, utils_js_1.quoteIdent)(channel);
|
|
2001
2818
|
if (this.logging) {
|
|
2002
|
-
|
|
2819
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `LISTEN ${quoted}`, { code: 'listen', channel });
|
|
2003
2820
|
}
|
|
2004
2821
|
const sub = await (0, realtime_js_1.createSubscription)(this.pool, channel, quoted, handler, (closed) => {
|
|
2005
2822
|
this.activeSubscriptions.delete(closed);
|
|
2006
|
-
}, options);
|
|
2823
|
+
}, options, this.logger);
|
|
2007
2824
|
this.activeSubscriptions.add(sub);
|
|
2008
2825
|
return sub;
|
|
2009
2826
|
}
|
|
@@ -2026,7 +2843,7 @@ class TurbineClient {
|
|
|
2026
2843
|
}
|
|
2027
2844
|
(0, realtime_js_1.validateChannel)(channel);
|
|
2028
2845
|
if (this.logging) {
|
|
2029
|
-
|
|
2846
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `NOTIFY ${channel}`, { code: 'notify', channel });
|
|
2030
2847
|
}
|
|
2031
2848
|
try {
|
|
2032
2849
|
await this.pool.query('SELECT pg_notify($1, $2)', [channel, payload ?? '']);
|
|
@@ -2064,29 +2881,45 @@ class TurbineClient {
|
|
|
2064
2881
|
* Throws if the connection fails.
|
|
2065
2882
|
*/
|
|
2066
2883
|
async connect() {
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2884
|
+
await this.roundTrip();
|
|
2885
|
+
if (this.logging) {
|
|
2886
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', 'Connection verified', { code: 'connection-verified' });
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
/**
|
|
2890
|
+
* Run one cheap round trip (`SELECT 1`) on the primary and resolve with its
|
|
2891
|
+
* latency in milliseconds, for health checks and for setting
|
|
2892
|
+
* `autoRoundTripMs`. The time measures the statement on a checked-out
|
|
2893
|
+
* connection, not the wait for one: an exhausted pool rejects with
|
|
2894
|
+
* `ConnectionError` (E004) after `connectionTimeoutMs` instead. Rejects with
|
|
2895
|
+
* the typed connection error when the database cannot be reached.
|
|
2896
|
+
*/
|
|
2897
|
+
async $ping() {
|
|
2898
|
+
return this.roundTrip();
|
|
2899
|
+
}
|
|
2900
|
+
/**
|
|
2901
|
+
* `SELECT 1` on a checked-out primary connection, timed. A connection the
|
|
2902
|
+
* server closed while idle says nothing about whether the database is
|
|
2903
|
+
* reachable, so it runs on a fresh one when the first turns out to be dead
|
|
2904
|
+
* (see openCheckout).
|
|
2905
|
+
*/
|
|
2906
|
+
async roundTrip() {
|
|
2907
|
+
let latency = 0;
|
|
2070
2908
|
const { checkout } = await (0, checkout_js_1.openCheckout)(this.pool, async (client) => {
|
|
2909
|
+
const start = performance.now();
|
|
2071
2910
|
try {
|
|
2072
2911
|
await client.query('SELECT 1');
|
|
2073
2912
|
}
|
|
2074
2913
|
catch (err) {
|
|
2075
2914
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
2076
2915
|
}
|
|
2916
|
+
latency = performance.now() - start;
|
|
2077
2917
|
});
|
|
2078
2918
|
checkout.release();
|
|
2079
|
-
|
|
2080
|
-
console.log('[turbine] Connection verified');
|
|
2081
|
-
}
|
|
2919
|
+
return latency;
|
|
2082
2920
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
*
|
|
2086
|
-
* If Turbine was given an external pool via `TurbineConfig.pool`, this
|
|
2087
|
-
* method is a no-op, the caller is responsible for the pool's lifecycle.
|
|
2088
|
-
*/
|
|
2089
|
-
async disconnect() {
|
|
2921
|
+
async disconnect(options) {
|
|
2922
|
+
const timeoutMs = resolveNonNegativeInteger(options?.timeoutMs, 'disconnect timeoutMs', 'milliseconds');
|
|
2090
2923
|
// Tear down any live LISTEN subscriptions first. Each holds a dedicated
|
|
2091
2924
|
// pooled connection checked out; if we ended the pool (or returned for an
|
|
2092
2925
|
// external pool) without releasing them, pool.end() would wait forever for
|
|
@@ -2102,33 +2935,63 @@ class TurbineClient {
|
|
|
2102
2935
|
}
|
|
2103
2936
|
this.activeSubscriptions.clear();
|
|
2104
2937
|
}
|
|
2105
|
-
//
|
|
2106
|
-
// primary is owned
|
|
2107
|
-
//
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
console.error('[turbine] Error closing replica pool:', err.message);
|
|
2115
|
-
}
|
|
2938
|
+
// Owned (string-configured) replica pools are closed whether or not the
|
|
2939
|
+
// primary is owned; external replica pools are the caller's, like an
|
|
2940
|
+
// external primary.
|
|
2941
|
+
const endReplicas = Promise.all(this.ownedReplicaPools.map((replicaPool) => replicaPool.end().catch((err) => {
|
|
2942
|
+
if (this.logging) {
|
|
2943
|
+
(0, logger_js_1.logTo)(this.logger, 'error', `Error closing replica pool: ${err.message}`, {
|
|
2944
|
+
code: 'replica-close-error',
|
|
2945
|
+
error: err,
|
|
2946
|
+
});
|
|
2116
2947
|
}
|
|
2948
|
+
})));
|
|
2949
|
+
if (!this.ownsPool && this.logging) {
|
|
2950
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', 'disconnect() skipped, external primary pool is not owned by Turbine', {
|
|
2951
|
+
code: 'disconnect-skipped',
|
|
2952
|
+
});
|
|
2117
2953
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2954
|
+
const ending = Promise.all([endReplicas, this.ownsPool ? this.pool.end() : undefined]);
|
|
2955
|
+
if (timeoutMs === undefined || (await settlesWithin(ending, timeoutMs))) {
|
|
2956
|
+
await ending;
|
|
2957
|
+
if (this.logging && this.ownsPool) {
|
|
2958
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', 'Pool disconnected', { code: 'pool-disconnected', timedOut: false });
|
|
2121
2959
|
}
|
|
2122
|
-
return;
|
|
2960
|
+
return options === undefined ? undefined : { timedOut: false, forcedConnections: 0 };
|
|
2123
2961
|
}
|
|
2124
|
-
await this.
|
|
2962
|
+
const forcedConnections = await this.forceCloseCheckedOut();
|
|
2963
|
+
// Closed connections reach the pool as their holders' queries fail; give
|
|
2964
|
+
// that one grace period, then return either way rather than hang on a
|
|
2965
|
+
// holder that never releases.
|
|
2966
|
+
await settlesWithin(ending, TX_ABORT_GRACE_MS);
|
|
2125
2967
|
if (this.logging) {
|
|
2126
|
-
|
|
2968
|
+
(0, logger_js_1.logTo)(this.logger, 'debug', `Pool disconnected after ${timeoutMs}ms, ${forcedConnections} connection(s) closed in use`, { code: 'pool-disconnected', timedOut: true, timeoutMs, forcedConnections });
|
|
2127
2969
|
}
|
|
2970
|
+
return { timedOut: true, forcedConnections };
|
|
2128
2971
|
}
|
|
2129
|
-
/**
|
|
2130
|
-
|
|
2131
|
-
|
|
2972
|
+
/**
|
|
2973
|
+
* Cancel the running statement of every connection still checked out of a
|
|
2974
|
+
* pool this client opened, then close the connection. Cancelling first is
|
|
2975
|
+
* what stops the work on the server: a closed socket alone is noticed only
|
|
2976
|
+
* when the backend next writes to it. Returns how many were closed.
|
|
2977
|
+
*/
|
|
2978
|
+
async forceCloseCheckedOut() {
|
|
2979
|
+
const clients = this.checkoutWatches.flatMap((watch) => [...watch.checkedOut]);
|
|
2980
|
+
await Promise.all(clients.map(async (client) => {
|
|
2981
|
+
await (0, utils_js_1.sendCancelRequest)(client, TX_ABORT_GRACE_MS);
|
|
2982
|
+
const c = client;
|
|
2983
|
+
const closing = Promise.resolve()
|
|
2984
|
+
.then(() => c.end?.())
|
|
2985
|
+
.catch(() => { });
|
|
2986
|
+
// A backend that does not answer the terminate message within the
|
|
2987
|
+
// grace period has its socket destroyed instead.
|
|
2988
|
+
if (!(await settlesWithin(closing, TX_ABORT_GRACE_MS)))
|
|
2989
|
+
c.connection?.stream?.destroy?.();
|
|
2990
|
+
}));
|
|
2991
|
+
return clients.length;
|
|
2992
|
+
}
|
|
2993
|
+
async end(options) {
|
|
2994
|
+
return options === undefined ? this.disconnect() : this.disconnect(options);
|
|
2132
2995
|
}
|
|
2133
2996
|
/**
|
|
2134
2997
|
* Pool statistics for monitoring. Returns zeros for pools that don't
|
|
@@ -2143,3 +3006,15 @@ class TurbineClient {
|
|
|
2143
3006
|
}
|
|
2144
3007
|
}
|
|
2145
3008
|
exports.TurbineClient = TurbineClient;
|
|
3009
|
+
// What prisma-compat (and anything else that wraps a client) logs through: the
|
|
3010
|
+
// client's logger and resolved diagnostics, under a `Symbol.for` key so a CJS
|
|
3011
|
+
// and an ESM copy agree. Defined outside the class body so it stays off the
|
|
3012
|
+
// published declaration of `TurbineClient`.
|
|
3013
|
+
Object.defineProperty(TurbineClient.prototype, logger_js_1.CLIENT_LOG_CONTEXT, {
|
|
3014
|
+
get() {
|
|
3015
|
+
const self = this;
|
|
3016
|
+
return { logger: self.logger, diagnostics: self.diagnostics };
|
|
3017
|
+
},
|
|
3018
|
+
enumerable: false,
|
|
3019
|
+
configurable: false,
|
|
3020
|
+
});
|