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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm: the `lock` read option (`SELECT ... FOR UPDATE OF "t"`).
|
|
3
|
+
*
|
|
4
|
+
* {@link resolveRowLock} is the one place a `lock` value is validated and
|
|
5
|
+
* rendered. The SQL build and the cache key both read its result, so the two
|
|
6
|
+
* cannot disagree about which lock a statement carries.
|
|
7
|
+
*
|
|
8
|
+
* The clause always names the queried table (`OF "t"`). That keeps the lock on
|
|
9
|
+
* the returned rows of that table and nothing else, on every relation
|
|
10
|
+
* strategy: correlated `with` subqueries are never locked anyway, and without
|
|
11
|
+
* `OF` PostgreSQL refuses the `flatten` and lateral-pick plans (their outer
|
|
12
|
+
* joins would put a table on the nullable side of the lock).
|
|
13
|
+
*/
|
|
14
|
+
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
15
|
+
import { tableAccessorName } from '../global-filters-config.js';
|
|
16
|
+
import { suggestKey } from './utils.js';
|
|
17
|
+
const MODES = ['update', 'noKeyUpdate', 'share', 'keyShare'];
|
|
18
|
+
const MODE_SQL = {
|
|
19
|
+
update: 'UPDATE',
|
|
20
|
+
noKeyUpdate: 'NO KEY UPDATE',
|
|
21
|
+
share: 'SHARE',
|
|
22
|
+
keyShare: 'KEY SHARE',
|
|
23
|
+
};
|
|
24
|
+
/** One letter per mode for the SQL-template cache key. */
|
|
25
|
+
const MODE_FP = { update: 'u', noKeyUpdate: 'n', share: 's', keyShare: 'k' };
|
|
26
|
+
const LOCK_KEYS = ['mode', 'noWait', 'skipLocked'];
|
|
27
|
+
/** Why an engine refuses a lock mode, keyed by dialect name. */
|
|
28
|
+
const ENGINE_HINT = {
|
|
29
|
+
sqlite: 'SQLite has no row locks: a write transaction locks the whole database. Remove `lock`; inside ' +
|
|
30
|
+
'$transaction on SQLite, writes are already serialized.',
|
|
31
|
+
mysql: "MySQL supports lock: 'update' and lock: 'share' only. Use 'update' (stronger) or 'share' (weaker).",
|
|
32
|
+
mssql: "Row locks are not available on SQL Server in this release. Use $transaction({ isolationLevel: 'RepeatableRead' }) " +
|
|
33
|
+
'or a raw query with table hints.',
|
|
34
|
+
powdb: 'PowDB has no row locks: a transaction holds the single writer gate.',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Validate `lock` for a read on `table` and render its clause.
|
|
38
|
+
*
|
|
39
|
+
* `undefined` means no lock. Anything that is not a mode string or a
|
|
40
|
+
* `{ mode, noWait?, skipLocked? }` object is `ValidationError` (E003), and so
|
|
41
|
+
* is asking for both wait policies at once. A mode the dialect does not list in
|
|
42
|
+
* {@link Dialect.rowLockModes} is `UnsupportedFeatureError` (E017).
|
|
43
|
+
*/
|
|
44
|
+
export function resolveRowLock(lock, dialect, table) {
|
|
45
|
+
if (lock === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
let mode;
|
|
48
|
+
let noWait;
|
|
49
|
+
let skipLocked;
|
|
50
|
+
if (typeof lock === 'string') {
|
|
51
|
+
mode = lock;
|
|
52
|
+
}
|
|
53
|
+
else if (typeof lock === 'object' && lock !== null && !Array.isArray(lock)) {
|
|
54
|
+
for (const key of Object.keys(lock)) {
|
|
55
|
+
if (!LOCK_KEYS.includes(key)) {
|
|
56
|
+
const hint = suggestKey(key, LOCK_KEYS);
|
|
57
|
+
throw new ValidationError(`lock on "${table}": unknown key "${key}".${hint ? ` Did you mean "${hint}"?` : ''} ` +
|
|
58
|
+
'A lock is a mode, or { mode, noWait: true } or { mode, skipLocked: true }.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
({ mode, noWait, skipLocked } = lock);
|
|
62
|
+
assertFlag(noWait, 'noWait', table);
|
|
63
|
+
assertFlag(skipLocked, 'skipLocked', table);
|
|
64
|
+
if (noWait === true && skipLocked === true) {
|
|
65
|
+
throw new ValidationError(`lock on "${table}": noWait and skipLocked cannot be combined. noWait fails at once when a row is ` +
|
|
66
|
+
'locked by another transaction; skipLocked leaves that row out of the result. Pick one.');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
throw new ValidationError(`lock on "${table}" must be a mode ('update', 'noKeyUpdate', 'share' or 'keyShare') or ` +
|
|
71
|
+
`{ mode, noWait?, skipLocked? }, received ${describe(lock)}. Leave it out for an unlocked read.`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof mode !== 'string' || !MODES.includes(mode)) {
|
|
74
|
+
const hint = typeof mode === 'string' ? suggestKey(mode, MODES) : null;
|
|
75
|
+
throw new ValidationError(`lock on "${table}": unknown mode ${describe(mode)}.${hint ? ` Did you mean '${hint}'?` : ''} ` +
|
|
76
|
+
"Use 'update', 'noKeyUpdate', 'share' or 'keyShare'.");
|
|
77
|
+
}
|
|
78
|
+
const m = mode;
|
|
79
|
+
if (!(dialect.rowLockModes ?? []).includes(m)) {
|
|
80
|
+
throw new UnsupportedFeatureError(`lock: '${m}'`, dialect.name, ENGINE_HINT[dialect.name] ?? 'This engine has no row-locking clause Turbine can emit.');
|
|
81
|
+
}
|
|
82
|
+
const wait = noWait === true ? ' NOWAIT' : skipLocked === true ? ' SKIP LOCKED' : '';
|
|
83
|
+
return {
|
|
84
|
+
sql: ` FOR ${MODE_SQL[m]} OF ${dialect.quoteIdentifier(table)}${wait}`,
|
|
85
|
+
fp: `${MODE_FP[m]}${noWait === true ? '.nw' : skipLocked === true ? '.sl' : ''}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The refusal for a locking read outside a transaction. In autocommit the lock
|
|
90
|
+
* is released when the statement ends, before the caller sees the rows, so a
|
|
91
|
+
* `skipLocked` queue claim outside a transaction claims nothing, silently.
|
|
92
|
+
*/
|
|
93
|
+
export function lockOutsideTransactionMessage(table, operation) {
|
|
94
|
+
return (`lock on "${table}".${operation}() needs a transaction: outside one the rows are unlocked again before ` +
|
|
95
|
+
`the call returns. Use db.$transaction(async (tx) => tx.${tableAccessorName(table)}.${operation}({ ..., lock })).`);
|
|
96
|
+
}
|
|
97
|
+
/** The refusal for `lock` on a streaming read. */
|
|
98
|
+
export function lockOnStreamMessage(table, operation) {
|
|
99
|
+
return (`lock is not supported on "${table}".${operation}(): a stream is not a locking read. Page with ` +
|
|
100
|
+
'findMany({ lock, limit }) inside the transaction instead.');
|
|
101
|
+
}
|
|
102
|
+
/** The refusal for `lock` together with `distinct`. */
|
|
103
|
+
export function lockWithDistinctMessage(table) {
|
|
104
|
+
return (`lock on "${table}" cannot be combined with distinct: PostgreSQL does not lock rows of a DISTINCT query. ` +
|
|
105
|
+
'Lock first (findMany with lock), then de-duplicate.');
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Refuse a locking read in a pipeline that runs each statement in autocommit
|
|
109
|
+
* (`pipeline(..., { transactional: false })`). A transactional pipeline, and
|
|
110
|
+
* `$transaction([...])`, keep the lock for the rest of the batch.
|
|
111
|
+
*/
|
|
112
|
+
export function assertNoLockInAutocommitPipeline(queries) {
|
|
113
|
+
const at = queries.findIndex((q) => q.rowLock === true);
|
|
114
|
+
if (at === -1)
|
|
115
|
+
return;
|
|
116
|
+
throw new ValidationError(`pipeline query ${at} (${queries[at]?.tag ?? 'a read'}) is a locking read, and { transactional: false } runs ` +
|
|
117
|
+
'it in autocommit, which releases the lock before the call returns. Drop transactional: false, or run the ' +
|
|
118
|
+
'batch with db.$transaction([...]).');
|
|
119
|
+
}
|
|
120
|
+
function assertFlag(value, name, table) {
|
|
121
|
+
if (value === undefined || typeof value === 'boolean')
|
|
122
|
+
return;
|
|
123
|
+
throw new ValidationError(`lock on "${table}": ${name} must be true or false, received ${describe(value)}.`);
|
|
124
|
+
}
|
|
125
|
+
function describe(value) {
|
|
126
|
+
if (typeof value === 'string')
|
|
127
|
+
return `'${value}'`;
|
|
128
|
+
if (value === null || typeof value !== 'object')
|
|
129
|
+
return String(value);
|
|
130
|
+
return Array.isArray(value) ? 'an array' : 'an object';
|
|
131
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm: `sqlComment`, sqlcommenter-style key/values on each statement
|
|
3
|
+
* Turbine builds, so `pg_stat_activity`, the server log, `auto_explain` and
|
|
4
|
+
* database APM tools can attribute a statement to a service, a model, a feature
|
|
5
|
+
* or a trace.
|
|
6
|
+
*
|
|
7
|
+
* SELECT ... LIMIT $2 /*action='findMany',application='billing',model='users'*\/
|
|
8
|
+
*
|
|
9
|
+
* FORMAT (the sqlcommenter spec): keys sorted, each value URL-encoded with
|
|
10
|
+
* `encodeURIComponent` and then `'` escaped as `\'`, each pair `key='value'`,
|
|
11
|
+
* pairs joined by commas, the whole comment appended after the statement with
|
|
12
|
+
* one space before it.
|
|
13
|
+
*
|
|
14
|
+
* WHY A VALUE CANNOT ESCAPE THE COMMENT. `encodeURIComponent` encodes `/`, so a
|
|
15
|
+
* value can hold neither `*\/` (the comment's end) nor `/*` (PostgreSQL and SQL
|
|
16
|
+
* Server nest block comments, so an opener would swallow the rest of the
|
|
17
|
+
* text). It also encodes every character a driver-side placeholder rewrite
|
|
18
|
+
* looks for (`$`, `?`, `:`, `@`) and `;`. Keys must match
|
|
19
|
+
* {@link SQL_COMMENT_KEY_PATTERN}, whose first-character rule also rules out
|
|
20
|
+
* MySQL's executable `/*!` and optimizer-hint `/*+` comments.
|
|
21
|
+
*
|
|
22
|
+
* WHERE IT IS APPENDED: at the execute seam, AFTER the SQL-template cache. The
|
|
23
|
+
* cache stores commentless text, so its keys, its hit rate and the lockstep
|
|
24
|
+
* checks are untouched by any comment, and a per-request value can never grow
|
|
25
|
+
* it. `$on('query')` events keep `sql` commentless and carry the comment in
|
|
26
|
+
* their own field.
|
|
27
|
+
*
|
|
28
|
+
* PREPARED-STATEMENT NAMES. node-postgres refuses to reuse a statement name for
|
|
29
|
+
* different text on one connection, and every name a connection prepares stays
|
|
30
|
+
* on the server until the connection closes. So:
|
|
31
|
+
*
|
|
32
|
+
* - no comment: the template's own name, as before;
|
|
33
|
+
* - static, model, action and tag only (constant per call site and label):
|
|
34
|
+
* `c_<16 hex>`, a hash of the template name and the comment, claimed from
|
|
35
|
+
* the same process-wide budget as every other name
|
|
36
|
+
* (`PREPARED_STATEMENT_BUDGET`), so the server-side cost stays bounded
|
|
37
|
+
* however many labels there are;
|
|
38
|
+
* - any dynamic value (a trace id, unique per request): unnamed, parsed on
|
|
39
|
+
* every execution and never kept. A per-request value under a name would
|
|
40
|
+
* prepare a new statement on every pooled connection for every request.
|
|
41
|
+
*
|
|
42
|
+
* Leaf apart from errors.ts, logger.ts, query-events.ts (the `$tag` scope) and
|
|
43
|
+
* utils.ts (the name budget); nothing here is Node-only.
|
|
44
|
+
*/
|
|
45
|
+
import { type TurbineLogger } from '../logger.js';
|
|
46
|
+
import type { DeferredQuery } from './deferred.js';
|
|
47
|
+
/** `TurbineConfig.sqlComment`. */
|
|
48
|
+
export interface SqlCommentOptions {
|
|
49
|
+
/** Constant key/values on every statement this client builds: `{ application: 'billing' }`. */
|
|
50
|
+
static?: Readonly<Record<string, string>> | undefined;
|
|
51
|
+
/** Add `model` (the table) and `action` (`'findMany'`, `'update'`, ...) of the operation. */
|
|
52
|
+
model?: boolean | undefined;
|
|
53
|
+
/** Add the label of the innermost `db.$tag(label, fn)` scope as `tag`. */
|
|
54
|
+
tag?: boolean | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Per-statement values, computed in the caller's async context (for example
|
|
57
|
+
* a W3C `traceparent`). A key whose value is `undefined` is left out. A
|
|
58
|
+
* statement carrying any dynamic value is sent unnamed: parsed on every
|
|
59
|
+
* execution, never kept as a prepared statement. A key or value this
|
|
60
|
+
* function returns that cannot be used is dropped with a warning
|
|
61
|
+
* (`sql-comment-dropped`), and a throw drops all of them: a tracing hook
|
|
62
|
+
* never fails a query.
|
|
63
|
+
*/
|
|
64
|
+
dynamic?: (() => Readonly<Record<string, string | undefined>> | undefined | null) | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** What a comment key must look like. Lower case, so it can never open `/*!` or `/*+`. */
|
|
67
|
+
export declare const SQL_COMMENT_KEY_PATTERN: RegExp;
|
|
68
|
+
/** Longest value accepted, in characters before encoding. */
|
|
69
|
+
export declare const MAX_SQL_COMMENT_VALUE_LENGTH = 256;
|
|
70
|
+
/** One statement's comment. */
|
|
71
|
+
export interface StatementComment {
|
|
72
|
+
/** The comment exactly as appended, without the space before it: `/*key='value',...*\/`. */
|
|
73
|
+
readonly comment: string;
|
|
74
|
+
/** It carries a dynamic value, so the statement goes out unnamed. */
|
|
75
|
+
readonly dynamic: boolean;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Builds the comment for each statement and the name it is sent under. One per
|
|
79
|
+
* client, shared by every table accessor and transaction of that client.
|
|
80
|
+
*/
|
|
81
|
+
export declare class SqlCommenter {
|
|
82
|
+
private readonly logger;
|
|
83
|
+
/** Static pairs, encoded and sorted. */
|
|
84
|
+
private readonly staticPairs;
|
|
85
|
+
/** Keys a dynamic value may not take: the static ones and the ones model / tag fill in. */
|
|
86
|
+
private readonly reservedKeys;
|
|
87
|
+
private readonly withModel;
|
|
88
|
+
private readonly withTag;
|
|
89
|
+
private readonly dynamic;
|
|
90
|
+
/** The comment when it depends on nothing per statement (no model, no tag). */
|
|
91
|
+
private readonly constant;
|
|
92
|
+
/** Rendered model / action / tag comments. Bounded: tags may come from data. */
|
|
93
|
+
private readonly rendered;
|
|
94
|
+
/** Claimed `c_` names by template name and comment. Holds only claimed names, so the budget bounds it. */
|
|
95
|
+
private readonly names;
|
|
96
|
+
/**
|
|
97
|
+
* Every statement gets a comment (a static key, or the model and action),
|
|
98
|
+
* so a template's own name is never sent and need not be claimed from the
|
|
99
|
+
* budget; the commented name is claimed instead.
|
|
100
|
+
*/
|
|
101
|
+
readonly alwaysComments: boolean;
|
|
102
|
+
constructor(staticPairs: ReadonlyArray<readonly [string, string]>, withModel: boolean, withTag: boolean, dynamic: SqlCommentOptions['dynamic'], logger: TurbineLogger);
|
|
103
|
+
/**
|
|
104
|
+
* The comment for one statement of operation `action` on `model`, or
|
|
105
|
+
* `undefined` when it carries none. Reads the `$tag` scope and calls
|
|
106
|
+
* `dynamic` in the caller's async context, so call it where the statement
|
|
107
|
+
* is sent.
|
|
108
|
+
*/
|
|
109
|
+
commentFor(model: string, action: string): StatementComment | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* The name a statement with comment `c` is sent under, given its template's
|
|
112
|
+
* name. A template sent unnamed stays unnamed; a dynamic comment is unnamed;
|
|
113
|
+
* anything else gets a stable `c_` name from the process-wide budget, or
|
|
114
|
+
* goes unnamed once that is spent.
|
|
115
|
+
*/
|
|
116
|
+
preparedName(templateName: string | undefined, c: StatementComment): string | undefined;
|
|
117
|
+
/** The usable dynamic pairs, encoded, or `undefined` when there are none. */
|
|
118
|
+
private dynamicPairs;
|
|
119
|
+
/** Why a dynamic key or value cannot be used, as [warning key, sentence], or `undefined`. */
|
|
120
|
+
private refusal;
|
|
121
|
+
/** One warning per reason and key, so a hook that misbehaves on every statement says it once. */
|
|
122
|
+
private dropped;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Validate `TurbineConfig.sqlComment` and build its commenter, or `undefined`
|
|
126
|
+
* when there is nothing to add. Throws `ValidationError` (E003) for a malformed
|
|
127
|
+
* option, and `UnsupportedFeatureError` (E017) where it cannot be honoured:
|
|
128
|
+
*
|
|
129
|
+
* - a backend that builds no SQL (PowDB, which injects its own query
|
|
130
|
+
* interface) has nothing to comment;
|
|
131
|
+
* - `dynamic` on an engine that prepares every statement text
|
|
132
|
+
* (`preparesEveryStatement`), which is MySQL: the MySQL engine sends each
|
|
133
|
+
* parameterized statement through mysql2's `execute()`, which keeps one
|
|
134
|
+
* SERVER-side prepared
|
|
135
|
+
* statement per distinct text per connection (up to 16,000 each), and the
|
|
136
|
+
* server caps the total across every client at `max_prepared_stmt_count`
|
|
137
|
+
* (16,382 by default). A value unique per request makes every text
|
|
138
|
+
* distinct, so a busy pool reaches that cap (error 1461) and every client
|
|
139
|
+
* of the server then fails to prepare.
|
|
140
|
+
*/
|
|
141
|
+
export declare function resolveSqlComment(value: unknown, logger: TurbineLogger, engine: {
|
|
142
|
+
readonly dialect: string;
|
|
143
|
+
readonly preparesEveryStatement: boolean;
|
|
144
|
+
readonly buildsSql: boolean;
|
|
145
|
+
}): SqlCommenter | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* `sql` with a comment Turbine appended removed, for code that reads statement
|
|
148
|
+
* text to decide how to run it (an engine shim telling a row-returning
|
|
149
|
+
* statement from a change count). A value can never contain `/*` (it is
|
|
150
|
+
* URL-encoded), so the last ` /*` of a statement ending in `*\/` is the
|
|
151
|
+
* comment's own opener; text Turbine did not comment is returned unchanged
|
|
152
|
+
* unless it, too, ends in a block comment, which is equally not code.
|
|
153
|
+
*/
|
|
154
|
+
export declare function withoutTrailingComment(sql: string): string;
|
|
155
|
+
/**
|
|
156
|
+
* `q` as sent with `comment`: its statement, and every statement of its
|
|
157
|
+
* reselect plan (MySQL), carry the comment. Everything else on the query is
|
|
158
|
+
* kept. Used by the batch paths (`pipeline`, `$transaction([...])`), which run
|
|
159
|
+
* deferred queries without going through a query interface.
|
|
160
|
+
*/
|
|
161
|
+
export declare function commentDeferred<T>(q: DeferredQuery<T>, comment: string | undefined): DeferredQuery<T>;
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm: `sqlComment`, sqlcommenter-style key/values on each statement
|
|
3
|
+
* Turbine builds, so `pg_stat_activity`, the server log, `auto_explain` and
|
|
4
|
+
* database APM tools can attribute a statement to a service, a model, a feature
|
|
5
|
+
* or a trace.
|
|
6
|
+
*
|
|
7
|
+
* SELECT ... LIMIT $2 /*action='findMany',application='billing',model='users'*\/
|
|
8
|
+
*
|
|
9
|
+
* FORMAT (the sqlcommenter spec): keys sorted, each value URL-encoded with
|
|
10
|
+
* `encodeURIComponent` and then `'` escaped as `\'`, each pair `key='value'`,
|
|
11
|
+
* pairs joined by commas, the whole comment appended after the statement with
|
|
12
|
+
* one space before it.
|
|
13
|
+
*
|
|
14
|
+
* WHY A VALUE CANNOT ESCAPE THE COMMENT. `encodeURIComponent` encodes `/`, so a
|
|
15
|
+
* value can hold neither `*\/` (the comment's end) nor `/*` (PostgreSQL and SQL
|
|
16
|
+
* Server nest block comments, so an opener would swallow the rest of the
|
|
17
|
+
* text). It also encodes every character a driver-side placeholder rewrite
|
|
18
|
+
* looks for (`$`, `?`, `:`, `@`) and `;`. Keys must match
|
|
19
|
+
* {@link SQL_COMMENT_KEY_PATTERN}, whose first-character rule also rules out
|
|
20
|
+
* MySQL's executable `/*!` and optimizer-hint `/*+` comments.
|
|
21
|
+
*
|
|
22
|
+
* WHERE IT IS APPENDED: at the execute seam, AFTER the SQL-template cache. The
|
|
23
|
+
* cache stores commentless text, so its keys, its hit rate and the lockstep
|
|
24
|
+
* checks are untouched by any comment, and a per-request value can never grow
|
|
25
|
+
* it. `$on('query')` events keep `sql` commentless and carry the comment in
|
|
26
|
+
* their own field.
|
|
27
|
+
*
|
|
28
|
+
* PREPARED-STATEMENT NAMES. node-postgres refuses to reuse a statement name for
|
|
29
|
+
* different text on one connection, and every name a connection prepares stays
|
|
30
|
+
* on the server until the connection closes. So:
|
|
31
|
+
*
|
|
32
|
+
* - no comment: the template's own name, as before;
|
|
33
|
+
* - static, model, action and tag only (constant per call site and label):
|
|
34
|
+
* `c_<16 hex>`, a hash of the template name and the comment, claimed from
|
|
35
|
+
* the same process-wide budget as every other name
|
|
36
|
+
* (`PREPARED_STATEMENT_BUDGET`), so the server-side cost stays bounded
|
|
37
|
+
* however many labels there are;
|
|
38
|
+
* - any dynamic value (a trace id, unique per request): unnamed, parsed on
|
|
39
|
+
* every execution and never kept. A per-request value under a name would
|
|
40
|
+
* prepare a new statement on every pooled connection for every request.
|
|
41
|
+
*
|
|
42
|
+
* Leaf apart from errors.ts, logger.ts, query-events.ts (the `$tag` scope) and
|
|
43
|
+
* utils.ts (the name budget); nothing here is Node-only.
|
|
44
|
+
*/
|
|
45
|
+
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
46
|
+
import { logTo } from '../logger.js';
|
|
47
|
+
import { currentQueryTag } from '../query-events.js';
|
|
48
|
+
import { claimPreparedName, fnv1a64Hex, LRUCache } from './utils.js';
|
|
49
|
+
import { shouldWarnOnce } from './warn-registry.js';
|
|
50
|
+
/** What a comment key must look like. Lower case, so it can never open `/*!` or `/*+`. */
|
|
51
|
+
export const SQL_COMMENT_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
52
|
+
/** Longest value accepted, in characters before encoding. */
|
|
53
|
+
export const MAX_SQL_COMMENT_VALUE_LENGTH = 256;
|
|
54
|
+
/** The keys `model: true` and `tag: true` fill in. */
|
|
55
|
+
const MODEL_KEYS = ['action', 'model'];
|
|
56
|
+
const TAG_KEY = 'tag';
|
|
57
|
+
const OPTION_KEYS = new Set(['static', 'model', 'tag', 'dynamic']);
|
|
58
|
+
/** Once-only warning namespace for dropped dynamic values. */
|
|
59
|
+
const DROPPED_WARN_NS = 'sqlCommentDropped';
|
|
60
|
+
/**
|
|
61
|
+
* `value` URL-encoded with `'` escaped, or `undefined` when it cannot be encoded
|
|
62
|
+
* (a lone surrogate). `encodeURIComponent` leaves `'` as is and never emits a
|
|
63
|
+
* backslash; the escape covers both anyway, so it stays complete on its own.
|
|
64
|
+
*/
|
|
65
|
+
function encodeValue(value) {
|
|
66
|
+
try {
|
|
67
|
+
return encodeURIComponent(value).replace(/[\\']/g, '\\$&');
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function render(pairs) {
|
|
74
|
+
let out = '/*';
|
|
75
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
76
|
+
const [key, value] = pairs[i];
|
|
77
|
+
out += `${i === 0 ? '' : ','}${key}='${value}'`;
|
|
78
|
+
}
|
|
79
|
+
return `${out}*/`;
|
|
80
|
+
}
|
|
81
|
+
function byKey(a, b) {
|
|
82
|
+
return a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
83
|
+
}
|
|
84
|
+
function isPlainObject(value) {
|
|
85
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
86
|
+
return false;
|
|
87
|
+
const proto = Object.getPrototypeOf(value);
|
|
88
|
+
return proto === Object.prototype || proto === null;
|
|
89
|
+
}
|
|
90
|
+
/** A short, printable rendering of a caller's key for a message. */
|
|
91
|
+
function describeKey(key) {
|
|
92
|
+
return JSON.stringify(key.length > 64 ? `${key.slice(0, 64)}...` : key);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Builds the comment for each statement and the name it is sent under. One per
|
|
96
|
+
* client, shared by every table accessor and transaction of that client.
|
|
97
|
+
*/
|
|
98
|
+
export class SqlCommenter {
|
|
99
|
+
logger;
|
|
100
|
+
/** Static pairs, encoded and sorted. */
|
|
101
|
+
staticPairs;
|
|
102
|
+
/** Keys a dynamic value may not take: the static ones and the ones model / tag fill in. */
|
|
103
|
+
reservedKeys;
|
|
104
|
+
withModel;
|
|
105
|
+
withTag;
|
|
106
|
+
dynamic;
|
|
107
|
+
/** The comment when it depends on nothing per statement (no model, no tag). */
|
|
108
|
+
constant;
|
|
109
|
+
/** Rendered model / action / tag comments. Bounded: tags may come from data. */
|
|
110
|
+
rendered = new LRUCache(1000);
|
|
111
|
+
/** Claimed `c_` names by template name and comment. Holds only claimed names, so the budget bounds it. */
|
|
112
|
+
names = new Map();
|
|
113
|
+
/**
|
|
114
|
+
* Every statement gets a comment (a static key, or the model and action),
|
|
115
|
+
* so a template's own name is never sent and need not be claimed from the
|
|
116
|
+
* budget; the commented name is claimed instead.
|
|
117
|
+
*/
|
|
118
|
+
alwaysComments;
|
|
119
|
+
constructor(staticPairs, withModel, withTag, dynamic, logger) {
|
|
120
|
+
this.logger = logger;
|
|
121
|
+
this.staticPairs = [...staticPairs].sort(byKey);
|
|
122
|
+
this.withModel = withModel;
|
|
123
|
+
this.withTag = withTag;
|
|
124
|
+
this.dynamic = dynamic;
|
|
125
|
+
const reserved = new Set(this.staticPairs.map(([k]) => k));
|
|
126
|
+
if (withModel)
|
|
127
|
+
for (const k of MODEL_KEYS)
|
|
128
|
+
reserved.add(k);
|
|
129
|
+
if (withTag)
|
|
130
|
+
reserved.add(TAG_KEY);
|
|
131
|
+
this.reservedKeys = reserved;
|
|
132
|
+
this.alwaysComments = this.staticPairs.length > 0 || withModel;
|
|
133
|
+
this.constant =
|
|
134
|
+
!withModel && !withTag && this.staticPairs.length > 0
|
|
135
|
+
? { comment: render(this.staticPairs), dynamic: false }
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The comment for one statement of operation `action` on `model`, or
|
|
140
|
+
* `undefined` when it carries none. Reads the `$tag` scope and calls
|
|
141
|
+
* `dynamic` in the caller's async context, so call it where the statement
|
|
142
|
+
* is sent.
|
|
143
|
+
*/
|
|
144
|
+
commentFor(model, action) {
|
|
145
|
+
let pairs;
|
|
146
|
+
let base;
|
|
147
|
+
if (this.withModel || this.withTag) {
|
|
148
|
+
const tag = this.withTag ? currentQueryTag() : undefined;
|
|
149
|
+
const key = `${model}\u0000${action}\u0000${tag ?? ''}`;
|
|
150
|
+
let hit = this.rendered.get(key);
|
|
151
|
+
if (hit === undefined) {
|
|
152
|
+
const built = [...this.staticPairs];
|
|
153
|
+
if (this.withModel) {
|
|
154
|
+
built.push(['action', encodeValue(action) ?? ''], ['model', encodeValue(model) ?? '']);
|
|
155
|
+
}
|
|
156
|
+
// A `$tag` label is 1 to 128 characters, checked when the scope opens.
|
|
157
|
+
const encodedTag = tag === undefined ? undefined : encodeValue(tag);
|
|
158
|
+
if (encodedTag !== undefined)
|
|
159
|
+
built.push([TAG_KEY, encodedTag]);
|
|
160
|
+
built.sort(byKey);
|
|
161
|
+
hit = { pairs: built, comment: built.length > 0 ? render(built) : '' };
|
|
162
|
+
this.rendered.set(key, hit);
|
|
163
|
+
}
|
|
164
|
+
pairs = hit.pairs;
|
|
165
|
+
base = hit.comment === '' ? undefined : { comment: hit.comment, dynamic: false };
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
pairs = this.staticPairs;
|
|
169
|
+
base = this.constant;
|
|
170
|
+
}
|
|
171
|
+
if (this.dynamic === undefined)
|
|
172
|
+
return base;
|
|
173
|
+
const extra = this.dynamicPairs();
|
|
174
|
+
if (extra === undefined)
|
|
175
|
+
return base;
|
|
176
|
+
return { comment: render([...pairs, ...extra].sort(byKey)), dynamic: true };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The name a statement with comment `c` is sent under, given its template's
|
|
180
|
+
* name. A template sent unnamed stays unnamed; a dynamic comment is unnamed;
|
|
181
|
+
* anything else gets a stable `c_` name from the process-wide budget, or
|
|
182
|
+
* goes unnamed once that is spent.
|
|
183
|
+
*/
|
|
184
|
+
preparedName(templateName, c) {
|
|
185
|
+
if (!templateName)
|
|
186
|
+
return templateName;
|
|
187
|
+
if (c.dynamic)
|
|
188
|
+
return '';
|
|
189
|
+
const key = `${templateName}\u0000${c.comment}`;
|
|
190
|
+
const known = this.names.get(key);
|
|
191
|
+
if (known !== undefined)
|
|
192
|
+
return known;
|
|
193
|
+
const name = claimPreparedName(`c_${fnv1a64Hex(key)}`);
|
|
194
|
+
if (name !== '')
|
|
195
|
+
this.names.set(key, name);
|
|
196
|
+
return name;
|
|
197
|
+
}
|
|
198
|
+
/** The usable dynamic pairs, encoded, or `undefined` when there are none. */
|
|
199
|
+
dynamicPairs() {
|
|
200
|
+
let values;
|
|
201
|
+
try {
|
|
202
|
+
values = this.dynamic?.();
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
this.dropped('threw', '', `sqlComment.dynamic threw (${err instanceof Error ? err.message : String(err)}), so ` +
|
|
206
|
+
'this statement carries no dynamic values. The statement itself ran as usual.');
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
if (values === undefined || values === null)
|
|
210
|
+
return undefined;
|
|
211
|
+
if (typeof values !== 'object' || Array.isArray(values)) {
|
|
212
|
+
this.dropped('shape', '', 'sqlComment.dynamic returned something other than an object of string values, so this statement carries ' +
|
|
213
|
+
'no dynamic values. Return an object such as { traceparent: "00-..." }, or undefined for none.');
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
let out;
|
|
217
|
+
for (const key of Object.keys(values)) {
|
|
218
|
+
const value = values[key];
|
|
219
|
+
if (value === undefined)
|
|
220
|
+
continue;
|
|
221
|
+
const reason = this.refusal(key, value);
|
|
222
|
+
if (reason !== undefined) {
|
|
223
|
+
this.dropped(reason[0], key, `sqlComment.dynamic key ${describeKey(key)} was left out of the comment: ${reason[1]}`);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const encoded = encodeValue(value);
|
|
227
|
+
if (encoded === undefined) {
|
|
228
|
+
this.dropped('encoding', key, `sqlComment.dynamic key ${describeKey(key)} was left out of the comment: its value is not well-formed ` +
|
|
229
|
+
'Unicode (a lone surrogate), so it cannot be URL-encoded.');
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (out === undefined)
|
|
233
|
+
out = [];
|
|
234
|
+
out.push([key, encoded]);
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
/** Why a dynamic key or value cannot be used, as [warning key, sentence], or `undefined`. */
|
|
239
|
+
refusal(key, value) {
|
|
240
|
+
if (!SQL_COMMENT_KEY_PATTERN.test(key)) {
|
|
241
|
+
return ['key', 'a key must be lower-case letters, digits and underscores, starting with a letter.'];
|
|
242
|
+
}
|
|
243
|
+
if (this.reservedKeys.has(key)) {
|
|
244
|
+
return ['reserved', 'that key is already set by sqlComment.static, model or tag, which take precedence.'];
|
|
245
|
+
}
|
|
246
|
+
if (typeof value !== 'string')
|
|
247
|
+
return ['type', `its value is ${typeof value}, not a string.`];
|
|
248
|
+
if (value.length > MAX_SQL_COMMENT_VALUE_LENGTH) {
|
|
249
|
+
return ['length', `its value is ${value.length} characters, over the ${MAX_SQL_COMMENT_VALUE_LENGTH} allowed.`];
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
/** One warning per reason and key, so a hook that misbehaves on every statement says it once. */
|
|
254
|
+
dropped(reason, key, message) {
|
|
255
|
+
if (!shouldWarnOnce(DROPPED_WARN_NS, `${reason}|${key.slice(0, 64)}`))
|
|
256
|
+
return;
|
|
257
|
+
logTo(this.logger, 'warn', message, { code: 'sql-comment-dropped', reason });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Validate `TurbineConfig.sqlComment` and build its commenter, or `undefined`
|
|
262
|
+
* when there is nothing to add. Throws `ValidationError` (E003) for a malformed
|
|
263
|
+
* option, and `UnsupportedFeatureError` (E017) where it cannot be honoured:
|
|
264
|
+
*
|
|
265
|
+
* - a backend that builds no SQL (PowDB, which injects its own query
|
|
266
|
+
* interface) has nothing to comment;
|
|
267
|
+
* - `dynamic` on an engine that prepares every statement text
|
|
268
|
+
* (`preparesEveryStatement`), which is MySQL: the MySQL engine sends each
|
|
269
|
+
* parameterized statement through mysql2's `execute()`, which keeps one
|
|
270
|
+
* SERVER-side prepared
|
|
271
|
+
* statement per distinct text per connection (up to 16,000 each), and the
|
|
272
|
+
* server caps the total across every client at `max_prepared_stmt_count`
|
|
273
|
+
* (16,382 by default). A value unique per request makes every text
|
|
274
|
+
* distinct, so a busy pool reaches that cap (error 1461) and every client
|
|
275
|
+
* of the server then fails to prepare.
|
|
276
|
+
*/
|
|
277
|
+
export function resolveSqlComment(value, logger, engine) {
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
return undefined;
|
|
280
|
+
if (!isPlainObject(value)) {
|
|
281
|
+
throw new ValidationError('Invalid sqlComment: expected an object with any of `static`, `model`, `tag` and `dynamic`, for example ' +
|
|
282
|
+
"{ static: { application: 'billing' }, model: true }.");
|
|
283
|
+
}
|
|
284
|
+
for (const key of Object.keys(value)) {
|
|
285
|
+
if (!OPTION_KEYS.has(key)) {
|
|
286
|
+
throw new ValidationError(`Invalid sqlComment: unknown key ${describeKey(key)}. The keys are \`static\`, \`model\`, \`tag\` and \`dynamic\`.`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const { static: staticValues, model, tag, dynamic } = value;
|
|
290
|
+
for (const [name, flag] of [
|
|
291
|
+
['model', model],
|
|
292
|
+
['tag', tag],
|
|
293
|
+
]) {
|
|
294
|
+
if (flag !== undefined && typeof flag !== 'boolean') {
|
|
295
|
+
throw new ValidationError(`Invalid sqlComment.${name}: expected true or false, got ${typeof flag}.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (dynamic !== undefined && typeof dynamic !== 'function') {
|
|
299
|
+
throw new ValidationError(`Invalid sqlComment.dynamic: expected a function returning an object of string values, got ${typeof dynamic}.`);
|
|
300
|
+
}
|
|
301
|
+
const pairs = [];
|
|
302
|
+
if (staticValues !== undefined) {
|
|
303
|
+
if (!isPlainObject(staticValues)) {
|
|
304
|
+
throw new ValidationError("Invalid sqlComment.static: expected an object of string values, for example { application: 'billing' }.");
|
|
305
|
+
}
|
|
306
|
+
for (const key of Object.keys(staticValues)) {
|
|
307
|
+
const v = staticValues[key];
|
|
308
|
+
const where = `sqlComment.static key ${describeKey(key)}`;
|
|
309
|
+
if (!SQL_COMMENT_KEY_PATTERN.test(key)) {
|
|
310
|
+
throw new ValidationError(`Invalid ${where}: a key must be lower-case letters, digits and underscores, starting with a letter ` +
|
|
311
|
+
'(the comment must never open a MySQL executable or hint comment, `/*!` or `/*+`).');
|
|
312
|
+
}
|
|
313
|
+
if ((model === true && MODEL_KEYS.includes(key)) || (tag === true && key === TAG_KEY)) {
|
|
314
|
+
throw new ValidationError(`Invalid ${where}: \`${key}\` is filled in by sqlComment.${key === TAG_KEY ? 'tag' : 'model'}. ` +
|
|
315
|
+
'Rename the static key or turn that option off.');
|
|
316
|
+
}
|
|
317
|
+
if (typeof v !== 'string') {
|
|
318
|
+
throw new ValidationError(`Invalid ${where}: expected a string value, got ${typeof v}.`);
|
|
319
|
+
}
|
|
320
|
+
if (v.length > MAX_SQL_COMMENT_VALUE_LENGTH) {
|
|
321
|
+
throw new ValidationError(`Invalid ${where}: the value is ${v.length} characters, over the ${MAX_SQL_COMMENT_VALUE_LENGTH} allowed.`);
|
|
322
|
+
}
|
|
323
|
+
const encoded = encodeValue(v);
|
|
324
|
+
if (encoded === undefined) {
|
|
325
|
+
throw new ValidationError(`Invalid ${where}: the value is not well-formed Unicode (a lone surrogate), so it cannot be URL-encoded.`);
|
|
326
|
+
}
|
|
327
|
+
pairs.push([key, encoded]);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (pairs.length === 0 && model !== true && tag !== true && dynamic === undefined)
|
|
331
|
+
return undefined;
|
|
332
|
+
if (!engine.buildsSql) {
|
|
333
|
+
throw new UnsupportedFeatureError('sqlComment', engine.dialect, 'This engine does not run SQL, so there is no statement to carry a comment. Remove the option for this client.');
|
|
334
|
+
}
|
|
335
|
+
if (dynamic !== undefined && engine.preparesEveryStatement) {
|
|
336
|
+
throw new UnsupportedFeatureError('sqlComment.dynamic', engine.dialect, 'The MySQL engine prepares each distinct statement text on the server (mysql2 execute(), up to 16,000 per ' +
|
|
337
|
+
'connection), and the server caps prepared statements across all clients at max_prepared_stmt_count ' +
|
|
338
|
+
'(16,382 by default). A per-statement value makes every text distinct, so a busy pool reaches that cap ' +
|
|
339
|
+
'(error 1461) and prepares start failing server-wide. Use `static`, `model` and `tag`, which are bounded.');
|
|
340
|
+
}
|
|
341
|
+
return new SqlCommenter(pairs, model === true, tag === true, dynamic, logger);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* `sql` with a comment Turbine appended removed, for code that reads statement
|
|
345
|
+
* text to decide how to run it (an engine shim telling a row-returning
|
|
346
|
+
* statement from a change count). A value can never contain `/*` (it is
|
|
347
|
+
* URL-encoded), so the last ` /*` of a statement ending in `*\/` is the
|
|
348
|
+
* comment's own opener; text Turbine did not comment is returned unchanged
|
|
349
|
+
* unless it, too, ends in a block comment, which is equally not code.
|
|
350
|
+
*/
|
|
351
|
+
export function withoutTrailingComment(sql) {
|
|
352
|
+
if (!sql.endsWith('*/'))
|
|
353
|
+
return sql;
|
|
354
|
+
const at = sql.lastIndexOf(' /*');
|
|
355
|
+
return at < 0 ? sql : sql.slice(0, at);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* `q` as sent with `comment`: its statement, and every statement of its
|
|
359
|
+
* reselect plan (MySQL), carry the comment. Everything else on the query is
|
|
360
|
+
* kept. Used by the batch paths (`pipeline`, `$transaction([...])`), which run
|
|
361
|
+
* deferred queries without going through a query interface.
|
|
362
|
+
*/
|
|
363
|
+
export function commentDeferred(q, comment) {
|
|
364
|
+
if (comment === undefined)
|
|
365
|
+
return q;
|
|
366
|
+
const reselect = q.reselect;
|
|
367
|
+
return {
|
|
368
|
+
...q,
|
|
369
|
+
sql: `${q.sql} ${comment}`,
|
|
370
|
+
...(reselect
|
|
371
|
+
? { reselect: (exec) => reselect((sql, params, name) => exec(`${sql} ${comment}`, params, name)) }
|
|
372
|
+
: {}),
|
|
373
|
+
};
|
|
374
|
+
}
|