turbine-orm 0.70.0 → 0.71.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 +164 -1041
- package/dist/cjs/cli/compile-query.d.ts +198 -0
- package/dist/cjs/cli/compile-query.js +529 -0
- package/dist/cjs/cli/index.d.ts +25 -1
- package/dist/cjs/cli/index.js +49 -1
- package/dist/cjs/cli/mcp.js +198 -16
- package/dist/cjs/client.d.ts +45 -10
- package/dist/cjs/client.js +21 -3
- package/dist/cjs/connection-url.d.ts +160 -0
- package/dist/cjs/connection-url.js +296 -0
- package/dist/cjs/index-stats.d.ts +4 -1
- package/dist/cjs/index-stats.js +27 -11
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/plan-flip-probe.js +17 -1
- package/dist/cjs/powql.d.ts +1 -0
- package/dist/cjs/powql.js +9 -0
- package/dist/cjs/query/builder.d.ts +133 -2
- package/dist/cjs/query/builder.js +288 -64
- package/dist/cjs/query/deferred.d.ts +12 -6
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/option-surface.js +6 -0
- package/dist/cjs/query/types.d.ts +47 -0
- package/dist/cjs/query/where.d.ts +11 -2
- package/dist/cli/compile-query.d.ts +198 -0
- package/dist/cli/compile-query.js +522 -0
- package/dist/cli/index.d.ts +25 -1
- package/dist/cli/index.js +48 -1
- package/dist/cli/mcp.js +198 -16
- package/dist/client.d.ts +45 -10
- package/dist/client.js +19 -1
- package/dist/connection-url.d.ts +160 -0
- package/dist/connection-url.js +289 -0
- package/dist/index-stats.d.ts +4 -1
- package/dist/index-stats.js +27 -11
- package/dist/index.d.ts +1 -1
- package/dist/plan-flip-probe.js +17 -1
- package/dist/powql.d.ts +1 -0
- package/dist/powql.js +9 -0
- package/dist/query/builder.d.ts +133 -2
- package/dist/query/builder.js +288 -64
- package/dist/query/deferred.d.ts +12 -6
- package/dist/query/index.d.ts +1 -1
- package/dist/query/option-surface.js +6 -0
- package/dist/query/types.d.ts +47 -0
- package/dist/query/where.d.ts +11 -2
- package/package.json +8 -6
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Connection-string inspection: pooler detection, and connection-time GUCs.
|
|
4
|
+
*
|
|
5
|
+
* A pure leaf. It imports nothing (not even `pg`), so every consumer, the CLI,
|
|
6
|
+
* the statistics collectors, and the tests, reads the same rules without
|
|
7
|
+
* dragging a driver in.
|
|
8
|
+
*
|
|
9
|
+
* ## Why this file exists
|
|
10
|
+
*
|
|
11
|
+
* A **transaction-pooling** proxy (PgBouncer, a Neon `-pooler` endpoint,
|
|
12
|
+
* Supabase's pooler) does not give a client its own server backend. It
|
|
13
|
+
* multiplexes many clients onto a few shared backends and hands a backend back
|
|
14
|
+
* to the pool at the end of each TRANSACTION, without running `DISCARD ALL`.
|
|
15
|
+
* Two consequences drive everything below:
|
|
16
|
+
*
|
|
17
|
+
* 1. A session-level `SET` issued outside an explicit transaction attaches to
|
|
18
|
+
* whichever shared backend served that statement, and stays there for the
|
|
19
|
+
* next client that gets it. A `SET statement_timeout` meant to bound one
|
|
20
|
+
* tool's own reads becomes a setting imposed on an application's queries.
|
|
21
|
+
* This is a production-incident class, not a theoretical one.
|
|
22
|
+
* 2. Session-scoped catalogs (`pg_prepared_statements` above all) describe
|
|
23
|
+
* whichever backend answered, which through a pooler is not "your" session
|
|
24
|
+
* in any useful sense.
|
|
25
|
+
*
|
|
26
|
+
* So there are two jobs here. {@link withStatementTimeoutOption} removes the
|
|
27
|
+
* need for the `SET` in (1) by moving the GUC into the connection's startup
|
|
28
|
+
* parameters, the same mechanism `TurbineClient` uses for `plan_cache_mode`.
|
|
29
|
+
* {@link detectPooler} lets a command that depends on session semantics refuse
|
|
30
|
+
* the endpoint outright rather than degrade silently.
|
|
31
|
+
*
|
|
32
|
+
* NEITHER is a substitute for the other. The connection parameter is what makes
|
|
33
|
+
* the collectors safe for any caller; the refusal is what keeps a diagnostic
|
|
34
|
+
* command from reporting confidently about a connection it cannot reason about.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.POOLER_PORTS = exports.POOLER_HOST_TOKENS = void 0;
|
|
38
|
+
exports.parseConnectionTarget = parseConnectionTarget;
|
|
39
|
+
exports.detectPooler = detectPooler;
|
|
40
|
+
exports.poolerRefusalMessage = poolerRefusalMessage;
|
|
41
|
+
exports.withStatementTimeoutOption = withStatementTimeoutOption;
|
|
42
|
+
/**
|
|
43
|
+
* Host and port from a connection string, in either shape libpq accepts.
|
|
44
|
+
*
|
|
45
|
+
* URL form (`postgres://user:pw@host:5432/db`) goes through `URL`; the
|
|
46
|
+
* key/value DSN form (`host=db.example.com port=6543 dbname=app`) falls back to
|
|
47
|
+
* a scan. An unparseable string yields `{ host: null, port: null }`, which every
|
|
48
|
+
* caller here treats as "no evidence", never as "safe".
|
|
49
|
+
*/
|
|
50
|
+
function parseConnectionTarget(connectionString) {
|
|
51
|
+
const trimmed = connectionString.trim();
|
|
52
|
+
if (trimmed === '')
|
|
53
|
+
return { host: null, port: null };
|
|
54
|
+
try {
|
|
55
|
+
const url = new URL(trimmed);
|
|
56
|
+
// A `postgresql:///db?host=/var/run/postgresql` unix socket parses with an
|
|
57
|
+
// empty hostname. Report it as absent rather than as the empty string.
|
|
58
|
+
const host = url.hostname === '' ? null : url.hostname.toLowerCase();
|
|
59
|
+
const port = url.port === '' ? null : Number.parseInt(url.port, 10);
|
|
60
|
+
return { host, port: port !== null && Number.isInteger(port) ? port : null };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Not a URL: try the key/value DSN form. Unquoted values only, which is
|
|
64
|
+
// what a host or a port is in practice.
|
|
65
|
+
const hostMatch = /(?:^|\s)host\s*=\s*([^\s'"]+)/i.exec(trimmed);
|
|
66
|
+
const portMatch = /(?:^|\s)port\s*=\s*(\d+)/i.exec(trimmed);
|
|
67
|
+
const host = hostMatch?.[1]?.toLowerCase() ?? null;
|
|
68
|
+
const port = portMatch?.[1] !== undefined ? Number.parseInt(portMatch[1], 10) : null;
|
|
69
|
+
return { host: host === '' ? null : host, port: port !== null && Number.isInteger(port) ? port : null };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Pooler detection
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* Hostname tokens that name a pooler.
|
|
77
|
+
*
|
|
78
|
+
* Matched as whole dot/dash/underscore-delimited TOKENS (with an optional
|
|
79
|
+
* trailing instance number), never as substrings of the connection string.
|
|
80
|
+
* Substring matching is what makes this class of check untrustworthy: a
|
|
81
|
+
* database named `poolers`, a role named `pooler_admin`, or a host called
|
|
82
|
+
* `spooler.internal` all contain the letters and none of them is a pooler, and
|
|
83
|
+
* a detector that cries wolf gets disabled by the first person it blocks.
|
|
84
|
+
*/
|
|
85
|
+
exports.POOLER_HOST_TOKENS = Object.freeze(['pooler', 'pgbouncer']);
|
|
86
|
+
/**
|
|
87
|
+
* Ports that name a pooler on their own.
|
|
88
|
+
*
|
|
89
|
+
* 6543 is the transaction-pooling port of the hosted poolers people point a CLI
|
|
90
|
+
* at. 6432 is PgBouncer's own documented default `listen_port`, so anyone who
|
|
91
|
+
* ran PgBouncer without changing it is here.
|
|
92
|
+
*
|
|
93
|
+
* The counter-argument to 6432 is that it refuses someone running plain
|
|
94
|
+
* Postgres on a non-standard port. That is true and it is the right trade,
|
|
95
|
+
* because the two errors do not cost the same: a false positive costs one
|
|
96
|
+
* `--allow-pooler` flag on a command the user is running interactively and
|
|
97
|
+
* reading the output of, while a false negative is silently the exact hazard
|
|
98
|
+
* this gate exists to prevent. When a detector's errors are asymmetric, tune it
|
|
99
|
+
* toward the cheap one.
|
|
100
|
+
*/
|
|
101
|
+
exports.POOLER_PORTS = Object.freeze([6543, 6432]);
|
|
102
|
+
/** The "nothing detected" result, so callers never build one by hand. */
|
|
103
|
+
function noPooler(target) {
|
|
104
|
+
return { pooled: false, signal: null, host: target.host, port: target.port, matchedToken: null, directHost: null };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether a connection string points at a transaction-pooling proxy.
|
|
108
|
+
*
|
|
109
|
+
* Deliberately conservative: it answers from the endpoint's SHAPE (hostname
|
|
110
|
+
* tokens, port), because there is no way to ask a pooler what it is without
|
|
111
|
+
* connecting through it, and the whole point is to decide before connecting.
|
|
112
|
+
* A false negative leaves the caller where it already was; a false positive
|
|
113
|
+
* blocks a legitimate database, so precision wins and the caller is expected to
|
|
114
|
+
* offer an override.
|
|
115
|
+
*/
|
|
116
|
+
function detectPooler(connectionString) {
|
|
117
|
+
const target = parseConnectionTarget(connectionString);
|
|
118
|
+
if (target.host !== null) {
|
|
119
|
+
for (const label of target.host.split('.')) {
|
|
120
|
+
for (const token of label.split(/[-_]/)) {
|
|
121
|
+
// `pooler`, `pgbouncer`, and numbered instances of either (`pooler2`).
|
|
122
|
+
const base = /^([a-z]+)\d*$/.exec(token)?.[1] ?? token;
|
|
123
|
+
if (exports.POOLER_HOST_TOKENS.includes(base)) {
|
|
124
|
+
return {
|
|
125
|
+
pooled: true,
|
|
126
|
+
signal: 'host',
|
|
127
|
+
host: target.host,
|
|
128
|
+
port: target.port,
|
|
129
|
+
matchedToken: token,
|
|
130
|
+
directHost: deriveDirectHost(target.host, token),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (target.port !== null && exports.POOLER_PORTS.includes(target.port)) {
|
|
137
|
+
return {
|
|
138
|
+
pooled: true,
|
|
139
|
+
signal: 'port',
|
|
140
|
+
host: target.host,
|
|
141
|
+
port: target.port,
|
|
142
|
+
matchedToken: null,
|
|
143
|
+
directHost: null,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return noPooler(target);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* `host` with an in-label `-<token>` pooler suffix removed, or `null`.
|
|
150
|
+
*
|
|
151
|
+
* Only the suffix form is derivable, see {@link PoolerDetection.directHost}.
|
|
152
|
+
*/
|
|
153
|
+
function deriveDirectHost(host, token) {
|
|
154
|
+
const labels = host.split('.');
|
|
155
|
+
let changed = false;
|
|
156
|
+
const rewritten = labels.map((label) => {
|
|
157
|
+
if (changed)
|
|
158
|
+
return label;
|
|
159
|
+
// The token must be a trailing segment of a MULTI-segment label, so
|
|
160
|
+
// `pooler.supabase.com` (a whole label) is not rewritten.
|
|
161
|
+
const suffix = `-${token}`;
|
|
162
|
+
if (label.length > suffix.length && label.endsWith(suffix)) {
|
|
163
|
+
changed = true;
|
|
164
|
+
return label.slice(0, -suffix.length);
|
|
165
|
+
}
|
|
166
|
+
return label;
|
|
167
|
+
});
|
|
168
|
+
return changed ? rewritten.join('.') : null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* The lines a command prints when it refuses a pooler endpoint.
|
|
172
|
+
*
|
|
173
|
+
* Plain text, no colour and no `console` call, so it is assertable in a unit
|
|
174
|
+
* test and reusable by any command that grows the same gate.
|
|
175
|
+
*/
|
|
176
|
+
function poolerRefusalMessage(detection, options) {
|
|
177
|
+
const what = detection.signal === 'port'
|
|
178
|
+
? `port ${detection.port} is a transaction-pooling port`
|
|
179
|
+
: `the hostname contains "${detection.matchedToken}"`;
|
|
180
|
+
const where = detection.host ?? '(host not stated)';
|
|
181
|
+
const lines = [
|
|
182
|
+
`${options.command} refuses to run through a connection pooler.`,
|
|
183
|
+
'',
|
|
184
|
+
` Endpoint: ${where}${detection.port === null ? '' : `:${detection.port}`}`,
|
|
185
|
+
` Detected: ${what}`,
|
|
186
|
+
'',
|
|
187
|
+
'A transaction pooler (PgBouncer, a Neon "-pooler" endpoint, Supabase\'s pooler) does not',
|
|
188
|
+
'give a client its own server backend. It multiplexes many clients onto a few shared',
|
|
189
|
+
'backends and reuses one as soon as a transaction ends, so:',
|
|
190
|
+
'',
|
|
191
|
+
' 1. Session state is not private. Anything this command sets on the session can be left',
|
|
192
|
+
' behind for another client, and whatever an earlier client left behind can be in',
|
|
193
|
+
' force for this one. That is how a "read-only guardrail" becomes an outage.',
|
|
194
|
+
' 2. Session-scoped views describe the wrong session. pg_prepared_statements, which this',
|
|
195
|
+
' report tells you to read to confirm a cached plan, belongs to whichever backend',
|
|
196
|
+
' answered, not to your application.',
|
|
197
|
+
' 3. The read bounds may not apply. This command asks for its statement_timeout as a',
|
|
198
|
+
' connection parameter, and a pooler is free to drop or reject one.',
|
|
199
|
+
'',
|
|
200
|
+
'Use the DIRECT (non-pooled) endpoint for the same database instead.',
|
|
201
|
+
];
|
|
202
|
+
if (detection.directHost !== null) {
|
|
203
|
+
lines.push('', ` ${detection.host}`, ` -> ${detection.directHost}`);
|
|
204
|
+
lines.push('', 'That is the same connection string with the pooler marker removed from the host.');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
lines.push('', 'On a managed provider that is the "direct" or "session" connection string in your', 'dashboard, not the pooled one. Self-hosted, it is the Postgres host itself rather', 'than the proxy in front of it.');
|
|
208
|
+
}
|
|
209
|
+
lines.push('', `If you are certain this endpoint is not a transaction pooler, re-run with ${options.allowFlag}.`);
|
|
210
|
+
return lines;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* `config` with `statement_timeout` moved into the connection's **startup
|
|
214
|
+
* parameters** (`options=-c statement_timeout=<ms>`) instead of a `SET`.
|
|
215
|
+
*
|
|
216
|
+
* PostgreSQL applies the `options` startup parameter as the backend starts the
|
|
217
|
+
* session, so the bound is in force for the connection's very first statement
|
|
218
|
+
* and for its whole life, with no extra round trip and nothing to reset. The
|
|
219
|
+
* alternative, `SET statement_timeout = <ms>` on a fresh connection, is what
|
|
220
|
+
* this exists to remove: outside an explicit transaction it is exactly the
|
|
221
|
+
* session-state write that a transaction pooler leaves on a shared backend.
|
|
222
|
+
*
|
|
223
|
+
* `TurbineClient` uses the same mechanism for `plan_cache_mode`; see the note
|
|
224
|
+
* there for why a `pool.on('connect')` `SET` is not the alternative it looks
|
|
225
|
+
* like (it races the caller's first query through pg's deprecated same-client
|
|
226
|
+
* queueing).
|
|
227
|
+
*
|
|
228
|
+
* Nothing already set is discarded, in either place pg reads `options` from.
|
|
229
|
+
* pg's `ConnectionParameters` lets a value parsed out of the connection string
|
|
230
|
+
* OVERRIDE the explicit `options` field, so when the URL already carries
|
|
231
|
+
* `?options=...` the GUC is appended THERE; the explicit field itself falls back
|
|
232
|
+
* to `process.env.PGOPTIONS` only while unset, so setting it blind would drop a
|
|
233
|
+
* deployment's `PGOPTIONS`. Both are read first and appended to.
|
|
234
|
+
*
|
|
235
|
+
* THE INJECTION BOUNDARY: a GUC value cannot be a bind parameter, so the
|
|
236
|
+
* emitted text necessarily contains a literal. `statementTimeoutMs` is
|
|
237
|
+
* therefore narrowed to a non-negative safe INTEGER and rendered from the
|
|
238
|
+
* narrowed number; anything else (a float, a negative, `NaN`, a string that
|
|
239
|
+
* coerced) returns the config untouched rather than reaching the wire, because
|
|
240
|
+
* a value that can carry a space can carry a second `-c`.
|
|
241
|
+
*/
|
|
242
|
+
function withStatementTimeoutOption(config, statementTimeoutMs) {
|
|
243
|
+
if (!Number.isSafeInteger(statementTimeoutMs) || statementTimeoutMs < 0)
|
|
244
|
+
return config;
|
|
245
|
+
// No unit suffix: statement_timeout's default unit IS milliseconds, which is
|
|
246
|
+
// what `SET statement_timeout = <n>` meant before this replaced it.
|
|
247
|
+
const setting = `-c statement_timeout=${statementTimeoutMs}`;
|
|
248
|
+
const merged = mergeConnectionStringOptions(config.connectionString, setting);
|
|
249
|
+
if (merged !== null)
|
|
250
|
+
return { ...config, connectionString: merged };
|
|
251
|
+
const existing = config.options || (typeof process !== 'undefined' ? process.env?.PGOPTIONS : undefined);
|
|
252
|
+
return { ...config, options: existing ? `${existing} ${setting}` : setting };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* `connectionString` with `setting` appended to an existing `options` query
|
|
256
|
+
* parameter, or `null` when it carries none (in which case the caller uses the
|
|
257
|
+
* `options` config field, which the connection string does not override).
|
|
258
|
+
*
|
|
259
|
+
* Only the query string is rewritten, never the userinfo or host, so a
|
|
260
|
+
* percent-encoded password cannot be mangled by a round trip through `URL`. The
|
|
261
|
+
* split is on the first `?`, which is also where pg's own parser puts the
|
|
262
|
+
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
263
|
+
* password is not parseable by pg either, so there is no shape this handles
|
|
264
|
+
* differently from the driver.
|
|
265
|
+
*
|
|
266
|
+
* DELIBERATELY DUPLICATED with `TurbineClient.mergeConnectionStringOptions`,
|
|
267
|
+
* which needs the identical merge for `plan_cache_mode`. Collapsing the two
|
|
268
|
+
* onto this copy is the obviously correct refactor and it was tried; it is
|
|
269
|
+
* reverted, and the reason is worth writing down because the next person will
|
|
270
|
+
* try it too.
|
|
271
|
+
*
|
|
272
|
+
* Exporting it and importing it from `client.ts` adds an import edge from a
|
|
273
|
+
* module that ~100 test processes load. In each of those processes this file's
|
|
274
|
+
* top level runs and its functions do not, so c8 merges ~100 top-level-only
|
|
275
|
+
* entries against the one full entry from `pooler-guard.test.ts`, and the file
|
|
276
|
+
* reports 40% in the merged report while measuring 100% in isolation. Real
|
|
277
|
+
* coverage is unchanged either way, but the aggregate gate moved 75.48% ->
|
|
278
|
+
* 75.03% against a 75% floor, i.e. the refactor spent almost all the headroom
|
|
279
|
+
* on a reporting artifact.
|
|
280
|
+
*
|
|
281
|
+
* So: two copies, both small, both pure, neither reachable from the other. If
|
|
282
|
+
* you unify them, re-measure `npm run test:coverage` as a whole and not just
|
|
283
|
+
* this file, and raise the floor's headroom first. Keep this module
|
|
284
|
+
* import-free regardless.
|
|
285
|
+
*/
|
|
286
|
+
function mergeConnectionStringOptions(connectionString, setting) {
|
|
287
|
+
const q = connectionString.indexOf('?');
|
|
288
|
+
if (q === -1)
|
|
289
|
+
return null;
|
|
290
|
+
const params = new URLSearchParams(connectionString.slice(q + 1));
|
|
291
|
+
const existing = params.get('options');
|
|
292
|
+
if (existing === null)
|
|
293
|
+
return null;
|
|
294
|
+
params.set('options', `${existing} ${setting}`);
|
|
295
|
+
return connectionString.slice(0, q + 1) + params.toString();
|
|
296
|
+
}
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
*
|
|
13
13
|
* DESIGN: the file splits into two halves.
|
|
14
14
|
* - The COLLECTOR half (`collectStatsSnapshot`) reads pg catalogs. It uses a
|
|
15
|
-
* single one-connection pool,
|
|
15
|
+
* single one-connection pool, bounds every read with a CONNECTION-TIME
|
|
16
|
+
* statement_timeout rather than a session-level `SET` (see
|
|
17
|
+
* `withStatementTimeoutOption` in connection-url.ts for why that
|
|
18
|
+
* distinction is a safety property and not a style choice), and treats every
|
|
16
19
|
* catalog read as INDIVIDUALLY OPTIONAL: a read that fails (privileges,
|
|
17
20
|
* CockroachDB/YugabyteDB catalog gaps, missing view) degrades that one
|
|
18
21
|
* signal and records a notice rather than aborting the whole snapshot.
|
package/dist/cjs/index-stats.js
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
*
|
|
14
14
|
* DESIGN: the file splits into two halves.
|
|
15
15
|
* - The COLLECTOR half (`collectStatsSnapshot`) reads pg catalogs. It uses a
|
|
16
|
-
* single one-connection pool,
|
|
16
|
+
* single one-connection pool, bounds every read with a CONNECTION-TIME
|
|
17
|
+
* statement_timeout rather than a session-level `SET` (see
|
|
18
|
+
* `withStatementTimeoutOption` in connection-url.ts for why that
|
|
19
|
+
* distinction is a safety property and not a style choice), and treats every
|
|
17
20
|
* catalog read as INDIVIDUALLY OPTIONAL: a read that fails (privileges,
|
|
18
21
|
* CockroachDB/YugabyteDB catalog gaps, missing view) degrades that one
|
|
19
22
|
* signal and records a notice rather than aborting the whole snapshot.
|
|
@@ -71,6 +74,7 @@ exports.auditDoctorIndexes = auditDoctorIndexes;
|
|
|
71
74
|
exports.isSnapshotUsable = isSnapshotUsable;
|
|
72
75
|
exports.collectStatsSnapshot = collectStatsSnapshot;
|
|
73
76
|
exports.collectTableHeat = collectTableHeat;
|
|
77
|
+
const connection_url_js_1 = require("./connection-url.js");
|
|
74
78
|
const index_advisor_js_1 = require("./index-advisor.js");
|
|
75
79
|
// ---------------------------------------------------------------------------
|
|
76
80
|
// Thresholds (exported + printed so a user can see exactly why a tier was chosen)
|
|
@@ -563,7 +567,20 @@ async function collectStatsSnapshot(options) {
|
|
|
563
567
|
const notices = [];
|
|
564
568
|
const snapshot = emptyStatsSnapshot(notices);
|
|
565
569
|
const { Pool } = (await Promise.resolve().then(() => __importStar(require('pg')))).default;
|
|
566
|
-
|
|
570
|
+
// statement_timeout travels as a CONNECTION PARAMETER, never as a `SET`. A
|
|
571
|
+
// bare `SET` on a fresh connection is a session-state write, and through a
|
|
572
|
+
// transaction-pooling proxy it attaches to a shared server backend that is
|
|
573
|
+
// then handed to somebody else's queries. The startup parameter is applied by
|
|
574
|
+
// the backend as it starts the session, so it bounds this collector's very
|
|
575
|
+
// first read, costs no round trip, and cannot outlive the connection. Nothing
|
|
576
|
+
// in the caller's `?options=` or `PGOPTIONS` is discarded; see
|
|
577
|
+
// withStatementTimeoutOption. (`turbine doctor` also refuses a pooler
|
|
578
|
+
// endpoint outright, but this collector is callable on its own, so it does
|
|
579
|
+
// not lean on that.)
|
|
580
|
+
const pool = new Pool({
|
|
581
|
+
...(0, connection_url_js_1.withStatementTimeoutOption)({ connectionString: options.connectionString }, timeout),
|
|
582
|
+
max: 1,
|
|
583
|
+
});
|
|
567
584
|
const run = async (label, text, values) => {
|
|
568
585
|
try {
|
|
569
586
|
const res = await pool.query(text, values);
|
|
@@ -575,8 +592,6 @@ async function collectStatsSnapshot(options) {
|
|
|
575
592
|
}
|
|
576
593
|
};
|
|
577
594
|
try {
|
|
578
|
-
// statement_timeout is best-effort; if it fails the reads still run.
|
|
579
|
-
await run('statement_timeout', `SET statement_timeout = ${Number(timeout)}`);
|
|
580
595
|
// --- stats_reset / age --------------------------------------------------
|
|
581
596
|
const resetRows = await run('pg_stat_database.stats_reset', `SELECT stats_reset FROM pg_stat_database WHERE datname = current_database()`);
|
|
582
597
|
if (resetRows && resetRows.length > 0) {
|
|
@@ -765,14 +780,15 @@ async function collectTableHeat(options) {
|
|
|
765
780
|
return result;
|
|
766
781
|
}
|
|
767
782
|
const { Pool } = (await Promise.resolve().then(() => __importStar(require('pg')))).default;
|
|
768
|
-
|
|
783
|
+
// As in collectStatsSnapshot: the bound is a connection parameter, not a
|
|
784
|
+
// `SET`. This collector points at whatever `--metrics-url` names, which is
|
|
785
|
+
// frequently a shared observability database, so leaving session state on a
|
|
786
|
+
// pooled backend there is if anything the worse version of the same bug.
|
|
787
|
+
const pool = new Pool({
|
|
788
|
+
...(0, connection_url_js_1.withStatementTimeoutOption)({ connectionString: options.connectionString }, timeout),
|
|
789
|
+
max: 1,
|
|
790
|
+
});
|
|
769
791
|
try {
|
|
770
|
-
try {
|
|
771
|
-
await pool.query(`SET statement_timeout = ${Number(timeout)}`);
|
|
772
|
-
}
|
|
773
|
-
catch {
|
|
774
|
-
/* best-effort */
|
|
775
|
-
}
|
|
776
792
|
const exists = await pool
|
|
777
793
|
.query(`SELECT to_regclass('_turbine_metrics')::text AS reg`)
|
|
778
794
|
.then((r) => r.rows[0]?.reg ?? null)
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type Neste
|
|
|
44
44
|
export { HttpJsonSink, type HttpJsonSinkOptions, type MetricsFlushBatch, type MetricsFlushRow, type ObserveConfig, type ObserveHandle, type ObserveSink, PgMetricsSink, type PgMetricsSinkOptions, } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
46
|
export { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
47
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
47
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type GroupByResult, type HavingClause, type JsonEncoding, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderByObject, type OrderDirection, type PrivilegeOption, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TemporalInfinityReading, type TextSearchFilter, type TypedWithClause, UNSAFE, type Unsafe, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithOrderByObject, type WithResult, } from './query/index.js';
|
|
48
48
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
49
49
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, PrismaCompatMap, PrismaModelMap, PrismaRelationMap, PrismaSchemaSource, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
50
50
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, withDbFieldNames, } from './schema.js';
|
|
@@ -264,11 +264,27 @@ async function probePlanFlips(options) {
|
|
|
264
264
|
const client = new Client({ connectionString: options.connectionString });
|
|
265
265
|
try {
|
|
266
266
|
await client.connect();
|
|
267
|
-
await client.query(`SET statement_timeout = ${Number(options.statementTimeoutMs ?? 5000)}`);
|
|
268
267
|
// READ ONLY is belt-and-braces: EXPLAIN without ANALYZE cannot write, and the
|
|
269
268
|
// transaction is rolled back regardless. It costs nothing and makes the
|
|
270
269
|
// read-only intent checkable from a server-side log.
|
|
271
270
|
await client.query('BEGIN READ ONLY');
|
|
271
|
+
// statement_timeout is set INSIDE the transaction and transaction-locally.
|
|
272
|
+
// It used to be a session-level `SET` on the fresh connection, one statement
|
|
273
|
+
// earlier, and a dedicated `Client` does not make that safe: through a
|
|
274
|
+
// transaction-pooling proxy a bare `SET` outside a transaction attaches to a
|
|
275
|
+
// SHARED server backend, which the pooler then hands to another client
|
|
276
|
+
// without a DISCARD. Every statement this pass runs is inside this
|
|
277
|
+
// transaction, so a transaction-local bound covers exactly the same ground
|
|
278
|
+
// and provably cannot outlive the ROLLBACK below.
|
|
279
|
+
//
|
|
280
|
+
// `set_config(name, value, is_local => true)` rather than `SET LOCAL`:
|
|
281
|
+
// Postgres rejects a bind parameter in `SET LOCAL` (`SET LOCAL x = $1` is a
|
|
282
|
+
// syntax error), so this is the parameterizable form of the same thing. The
|
|
283
|
+
// value carries no unit because statement_timeout's default unit is
|
|
284
|
+
// milliseconds, which is what the previous statement meant.
|
|
285
|
+
await client.query(`SELECT set_config('statement_timeout', $1, true)`, [
|
|
286
|
+
String(Number(options.statementTimeoutMs ?? 5000)),
|
|
287
|
+
]);
|
|
272
288
|
result.available = true;
|
|
273
289
|
for (let i = 0; i < targets.length; i++) {
|
|
274
290
|
const finding = targets[i];
|
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -821,6 +821,7 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
821
821
|
/** Resolve a groupBy order direction, refusing `nulls: 'first'` (E017); `nulls: 'last'` is a no-op. */
|
|
822
822
|
private groupOrderDir;
|
|
823
823
|
findManyStream(): AsyncGenerator<T>;
|
|
824
|
+
findManyStreamBatches(): AsyncGenerator<T[]>;
|
|
824
825
|
/** Reselect a single row by its single-column primary key value. */
|
|
825
826
|
private reselectByPk;
|
|
826
827
|
/**
|
package/dist/cjs/powql.js
CHANGED
|
@@ -3178,6 +3178,15 @@ class PowqlInterface {
|
|
|
3178
3178
|
async *findManyStream() {
|
|
3179
3179
|
throw new errors_js_1.UnsupportedFeatureError('cursor streaming (findManyStream)', 'PowDB', 'PowDB has no server-side cursor; page with findMany({ limit, offset }) instead');
|
|
3180
3180
|
}
|
|
3181
|
+
// The batch-yielding sibling needs its OWN stub. `PowqlInterface` is a
|
|
3182
|
+
// parallel implementation of QueryInterface's surface rather than a subclass,
|
|
3183
|
+
// so a method that exists on one and not the other is not a type error here:
|
|
3184
|
+
// it is `undefined` at runtime, and the caller gets `TypeError: not a
|
|
3185
|
+
// function` instead of the typed E017 that tells them what to do instead.
|
|
3186
|
+
// biome-ignore lint/correctness/useYield: intentionally throws before yielding, PowDB has no server cursor.
|
|
3187
|
+
async *findManyStreamBatches() {
|
|
3188
|
+
throw new errors_js_1.UnsupportedFeatureError('cursor streaming (findManyStreamBatches)', 'PowDB', 'PowDB has no server-side cursor; page with findMany({ limit, offset }) instead');
|
|
3189
|
+
}
|
|
3181
3190
|
// -------------------------------------------------------------------------
|
|
3182
3191
|
// Reselect helper (upsert only, PowDB's upsert has no `returning`)
|
|
3183
3192
|
// -------------------------------------------------------------------------
|
|
@@ -266,8 +266,31 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
266
266
|
* is derived. See {@link autoToOneThreshold}.
|
|
267
267
|
*/
|
|
268
268
|
private readonly autoRoundTripMs;
|
|
269
|
-
/**
|
|
269
|
+
/**
|
|
270
|
+
* The CLIENT-level nested-relation JSON encoding: what a query that names no
|
|
271
|
+
* `jsonEncoding` of its own gets. `'positional'` on PostgreSQL, `'object'`
|
|
272
|
+
* everywhere else (see {@link defaultJsonEncoding}).
|
|
273
|
+
*
|
|
274
|
+
* Read through {@link QueryInterface.currentJsonEncoding}, never directly, so
|
|
275
|
+
* a per-query override cannot be missed by one reader.
|
|
276
|
+
*/
|
|
270
277
|
private readonly jsonEncoding;
|
|
278
|
+
/**
|
|
279
|
+
* The encoding the query BEING BUILT resolved to, i.e. its own
|
|
280
|
+
* `jsonEncoding` or {@link QueryInterface.jsonEncoding}.
|
|
281
|
+
*
|
|
282
|
+
* Reassigned per `build*` call and exposed on the {@link BuilderCtx} as a live
|
|
283
|
+
* getter, exactly like {@link QueryInterface.currentSkip} and for the same
|
|
284
|
+
* reason: relations.ts reads the encoding from four places deep inside the
|
|
285
|
+
* SELECT walk, and threading it through every one of them as a parameter
|
|
286
|
+
* would be four chances to forget it.
|
|
287
|
+
*
|
|
288
|
+
* Safe because a build is SYNCHRONOUS from the assignment to the last read:
|
|
289
|
+
* `buildFindMany` / `buildFindUnique` return a fully-formed DeferredQuery
|
|
290
|
+
* whose parser closure already captured the shapes, so nothing reads this
|
|
291
|
+
* field after the build returns and no two builds can interleave on it.
|
|
292
|
+
*/
|
|
293
|
+
private currentJsonEncoding;
|
|
271
294
|
/**
|
|
272
295
|
* `parseRow` decode plans, keyed by table plus the exact column list. Bounded
|
|
273
296
|
* like the SQL template cache and for the same reason: the shapes come from
|
|
@@ -497,6 +520,24 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
497
520
|
* the per-query arg wins, then the client-level default (off).
|
|
498
521
|
*/
|
|
499
522
|
private resolveStableOrder;
|
|
523
|
+
/**
|
|
524
|
+
* Resolve one query's relation JSON encoding and PIN it for the build, so
|
|
525
|
+
* every reader inside relations.ts sees the same answer.
|
|
526
|
+
*
|
|
527
|
+
* Called at the TOP of each entry point that can emit or decode relation JSON
|
|
528
|
+
* (`buildFindMany`, `buildFindUnique`, `makeStreamRowParser`), before the
|
|
529
|
+
* cache key is assembled and before the flatten plan is consulted, because
|
|
530
|
+
* both of those depend on the answer.
|
|
531
|
+
*
|
|
532
|
+
* An unrecognized value THROWS (E003) rather than falling back. A per-query
|
|
533
|
+
* option that is silently ignored when misspelled is the exact failure this
|
|
534
|
+
* package has been bitten by before (see query/option-surface.ts), and here it
|
|
535
|
+
* would be invisible: the wrong encoding still returns correct rows, just
|
|
536
|
+
* without the saving the caller asked for, or with the flatten plan they were
|
|
537
|
+
* trying to re-enable still refused. Thrown before the SQL cache is consulted
|
|
538
|
+
* so a warm template can never serve a call the cold path would refuse.
|
|
539
|
+
*/
|
|
540
|
+
private resolveJsonEncoding;
|
|
500
541
|
/**
|
|
501
542
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
502
543
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -999,10 +1040,87 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
999
1040
|
*/
|
|
1000
1041
|
private measureWithDepth;
|
|
1001
1042
|
buildFindMany<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args?: FindManyArgs<T, R, W, S, O>): DeferredQuery<QueryResult<T, R, W, S, O>[]>;
|
|
1043
|
+
/**
|
|
1044
|
+
* Build the one row parser a whole drain uses.
|
|
1045
|
+
*
|
|
1046
|
+
* Shared by {@link findManyStream} and {@link findManyStreamBatches} so the
|
|
1047
|
+
* two can never disagree about a row's SHAPE while agreeing about its
|
|
1048
|
+
* contents: the flatten plan, the positional/object relation decode and the
|
|
1049
|
+
* PII projection are all decided here, once, before a statement is issued.
|
|
1050
|
+
*
|
|
1051
|
+
* The plan is a pure function of the schema, the `with` shape and
|
|
1052
|
+
* `includePii`, never of `limit`, so the batch-size override the speculative
|
|
1053
|
+
* fetch applies cannot change it and the parser always matches the emitted
|
|
1054
|
+
* SQL. Reading the raw `includePii` sentinel with `=== true` here would have
|
|
1055
|
+
* quietly planned a no-PII parser over a with-PII statement, which is why it
|
|
1056
|
+
* goes through `resolveUnsafeFlag` on every path, `with` clause or not.
|
|
1057
|
+
*/
|
|
1058
|
+
private makeStreamRowParser;
|
|
1059
|
+
/**
|
|
1060
|
+
* Everything a stream does to the DATABASE, and nothing it does to a row.
|
|
1061
|
+
*
|
|
1062
|
+
* {@link findManyStream} and {@link findManyStreamBatches} are the same drain
|
|
1063
|
+
* handed out at two granularities, so the statement they issue, when the
|
|
1064
|
+
* cursor opens, and how the connection is released all live here once. Only
|
|
1065
|
+
* the yielding differs, which is the entire point of having both.
|
|
1066
|
+
*
|
|
1067
|
+
* Parsing deliberately stays in the callers: they parse at different
|
|
1068
|
+
* granularities, and the per-row method must keep parsing LAZILY, one row at
|
|
1069
|
+
* a time, exactly as it always has. Hoisting the parse in here would make a
|
|
1070
|
+
* consumer that breaks after the first row pay for the rest of its batch.
|
|
1071
|
+
*
|
|
1072
|
+
* `action` is the tag query events carry. It is a parameter rather than a
|
|
1073
|
+
* constant so each public method reports its own name instead of the name of
|
|
1074
|
+
* whichever one happens to be implemented over the other.
|
|
1075
|
+
*
|
|
1076
|
+
* An EMPTY batch is never yielded: the dialect's cursor loop breaks on a
|
|
1077
|
+
* zero-row FETCH, and the speculative path below yields nothing at all when
|
|
1078
|
+
* the result set is empty. Callers may therefore treat a yielded batch as
|
|
1079
|
+
* non-empty.
|
|
1080
|
+
*/
|
|
1081
|
+
private streamRaw;
|
|
1082
|
+
/**
|
|
1083
|
+
* Stream rows from a findMany query using PostgreSQL cursors, one BATCH of
|
|
1084
|
+
* rows at a time.
|
|
1085
|
+
*
|
|
1086
|
+
* The same drain {@link findManyStream} performs, the same statements, the
|
|
1087
|
+
* same cursor, the same rows in the same order, handed out as arrays instead
|
|
1088
|
+
* of one row at a time. That is worth having because the per-row form costs
|
|
1089
|
+
* one promise resolution and one microtask turn PER ROW: measured over 50,000
|
|
1090
|
+
* rows on a local PostgreSQL, per-row yielding costs ~7 ms (~140 ns/row) that
|
|
1091
|
+
* batch yielding does not, which is roughly half of the streaming overhead
|
|
1092
|
+
* over a hand-written cursor loop. Nothing else about the two paths differs,
|
|
1093
|
+
* so the entire saving is the yielding.
|
|
1094
|
+
*
|
|
1095
|
+
* Prefer this whenever the consumer can work on an array; keep
|
|
1096
|
+
* {@link findManyStream} when a row at a time is what the code actually wants,
|
|
1097
|
+
* since flattening a batch by hand costs exactly what it saves.
|
|
1098
|
+
*
|
|
1099
|
+
* A yielded batch is never empty, and its length is NOT a contract: it is at
|
|
1100
|
+
* most `batchSize`, the final batch is usually shorter, and a result set that
|
|
1101
|
+
* fits within one batch arrives as a single array from the speculative fetch
|
|
1102
|
+
* with no cursor involved. Do not use batch boundaries to infer anything
|
|
1103
|
+
* about the data.
|
|
1104
|
+
*
|
|
1105
|
+
* Every other streaming behaviour is shared and documented on
|
|
1106
|
+
* {@link findManyStream}: the speculative fast path, the cursor path and its
|
|
1107
|
+
* cleanup, the snapshot semantics, and early `break`.
|
|
1108
|
+
*
|
|
1109
|
+
* @example
|
|
1110
|
+
* ```ts
|
|
1111
|
+
* for await (const batch of db.users.findManyStreamBatches({ where: { orgId: 1 }, batchSize: 500 })) {
|
|
1112
|
+
* await sink.writeAll(batch);
|
|
1113
|
+
* }
|
|
1114
|
+
* ```
|
|
1115
|
+
*/
|
|
1116
|
+
findManyStreamBatches<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args?: FindManyStreamArgs<T, R, W, S, O>): AsyncGenerator<QueryResult<T, R, W, S, O>[], void, undefined>;
|
|
1002
1117
|
/**
|
|
1003
1118
|
* Stream rows from a findMany query using PostgreSQL cursors.
|
|
1004
1119
|
* Returns an AsyncIterable that yields individual rows, fetching in batches internally.
|
|
1005
1120
|
*
|
|
1121
|
+
* See {@link findManyStreamBatches} for the same drain yielded a batch at a
|
|
1122
|
+
* time, which is measurably cheaper when the consumer can take an array.
|
|
1123
|
+
*
|
|
1006
1124
|
* **Speculative fast-path:** Before opening a cursor, issues a single
|
|
1007
1125
|
* `SELECT ... LIMIT batchSize+1`. If the result fits within `batchSize`,
|
|
1008
1126
|
* all rows are yielded immediately with zero cursor overhead (no BEGIN /
|
|
@@ -1118,7 +1236,11 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1118
1236
|
* strategy, silently and byte-identically):
|
|
1119
1237
|
* - the resolved strategy is not `'flatten'`;
|
|
1120
1238
|
* - `jsonEncoding: 'positional'` (a flattened relation emits no JSON at all,
|
|
1121
|
-
* so the two
|
|
1239
|
+
* so the two are not composed in this version). NOTE that this is the
|
|
1240
|
+
* PostgreSQL DEFAULT, so on Postgres `'flatten'` engages only when the
|
|
1241
|
+
* caller ALSO passes `jsonEncoding: 'object'`. The fallback is silent and
|
|
1242
|
+
* byte-identical, and `warnFlattenBlocked` names the encoding and the
|
|
1243
|
+
* escape hatch in dev;
|
|
1122
1244
|
* - the dialect owns relation-subquery generation
|
|
1123
1245
|
* (`dialect.buildRelationSubquery`, i.e. SQL Server's `FOR JSON PATH`);
|
|
1124
1246
|
* - `distinct` (the `DISTINCT ON` rewrite re-orders in an outer wrapper, and
|
|
@@ -1255,6 +1377,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1255
1377
|
* Build the decode plan for one exact column list, and remember it as this
|
|
1256
1378
|
* table's most recent shape so the next row of the same result set hits the
|
|
1257
1379
|
* fast path in {@link parseRow}.
|
|
1380
|
+
*
|
|
1381
|
+
* The shape key's delimiter is NUL because it is the one byte a Postgres
|
|
1382
|
+
* identifier cannot contain, even quoted, so no table or column name can
|
|
1383
|
+
* forge a collision. It MUST be written as the six-character escape, never
|
|
1384
|
+
* as a literal NUL byte: a raw NUL makes byte-oriented tools classify this
|
|
1385
|
+
* file as binary, and `grep` then reports ZERO matches for a term that is
|
|
1386
|
+
* present rather than saying it declined to look. This is the largest file
|
|
1387
|
+
* in the repo, and that silent empty result has already sent more than one
|
|
1388
|
+
* search down the wrong path.
|
|
1258
1389
|
*/
|
|
1259
1390
|
private buildRowDecodePlan;
|
|
1260
1391
|
/**
|