turbine-orm 0.53.0 → 0.54.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 +7 -2
- package/dist/cjs/cli/mcp.js +6 -0
- package/dist/cjs/cli/studio.js +10 -0
- package/dist/cjs/client.d.ts +182 -25
- package/dist/cjs/client.js +210 -29
- package/dist/cjs/dialect.d.ts +24 -0
- package/dist/cjs/dialect.js +1 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/mssql.js +1 -0
- package/dist/cjs/mysql.js +1 -0
- package/dist/cjs/powdb.js +1 -0
- package/dist/cjs/query/builder.js +11 -2
- package/dist/cjs/query/utils.d.ts +85 -1
- package/dist/cjs/query/utils.js +151 -1
- package/dist/cjs/query/warn-registry.d.ts +6 -0
- package/dist/cjs/query/warn-registry.js +6 -0
- package/dist/cjs/sqlite.js +1 -0
- package/dist/cli/mcp.js +6 -0
- package/dist/cli/studio.js +11 -1
- package/dist/client.d.ts +182 -25
- package/dist/client.js +211 -30
- package/dist/dialect.d.ts +24 -0
- package/dist/dialect.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/mssql.js +1 -0
- package/dist/mysql.js +1 -0
- package/dist/powdb.js +1 -0
- package/dist/query/builder.js +11 -2
- package/dist/query/utils.d.ts +85 -1
- package/dist/query/utils.js +146 -1
- package/dist/query/warn-registry.d.ts +6 -0
- package/dist/query/warn-registry.js +6 -0
- package/dist/sqlite.js +1 -0
- package/package.json +1 -1
package/dist/cjs/query/utils.js
CHANGED
|
@@ -22,6 +22,11 @@ exports.toLocalDateTimeLiteral = toLocalDateTimeLiteral;
|
|
|
22
22
|
exports.temporalBindKind = temporalBindKind;
|
|
23
23
|
exports.coerceTemporalValue = coerceTemporalValue;
|
|
24
24
|
exports.parseDbDate = parseDbDate;
|
|
25
|
+
exports.createUtcDateParser = createUtcDateParser;
|
|
26
|
+
exports.createUtcTimestampParser = createUtcTimestampParser;
|
|
27
|
+
exports.parseUtcTimestampText = parseUtcTimestampText;
|
|
28
|
+
exports.createPgArrayParser = createPgArrayParser;
|
|
29
|
+
exports.registerUtcTemporalParsers = registerUtcTemporalParsers;
|
|
25
30
|
exports.jsonWireCoercionOid = jsonWireCoercionOid;
|
|
26
31
|
exports.coerceJsonWireValue = coerceJsonWireValue;
|
|
27
32
|
exports.closestName = closestName;
|
|
@@ -345,6 +350,151 @@ function parseDbDate(value) {
|
|
|
345
350
|
return new Date(`${value.replace(' ', 'T')}Z`);
|
|
346
351
|
}
|
|
347
352
|
// ---------------------------------------------------------------------------
|
|
353
|
+
// Driver type parsers for the zone-less temporal OIDs
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
/**
|
|
356
|
+
* A Postgres `date` wire value: `YYYY-MM-DD`, optionally with more than four
|
|
357
|
+
* year digits, optionally suffixed ` BC`. Anything else (`infinity`,
|
|
358
|
+
* `-infinity`, and any shape a future server adds) is deliberately NOT matched
|
|
359
|
+
* so it falls through to the driver's own parser untouched.
|
|
360
|
+
*/
|
|
361
|
+
const PG_DATE_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})( BC)?$/;
|
|
362
|
+
/**
|
|
363
|
+
* Build the driver parser for Postgres `date` (OID 1082) that reads a
|
|
364
|
+
* zone-less calendar day as **UTC midnight**.
|
|
365
|
+
*
|
|
366
|
+
* The pg default builds the Date from the process's LOCAL zone, so the stored
|
|
367
|
+
* calendar day `2026-07-21` comes back as `2026-07-20T22:00:00Z` in
|
|
368
|
+
* `Europe/Berlin` and `2026-07-20T15:00:00Z` in `Asia/Tokyo`: the wrong
|
|
369
|
+
* calendar day everywhere east of UTC, and the wrong instant everywhere except
|
|
370
|
+
* UTC itself. It is also the exact mirror-image of the WRITE side, which
|
|
371
|
+
* already renders a bound `Date` from its UTC components
|
|
372
|
+
* ({@link toLocalDateTimeLiteral}), so today a read-modify-write cycle on a
|
|
373
|
+
* `date` column east of UTC walks the stored day one day earlier per cycle.
|
|
374
|
+
* This is the missing read half of `utcTimestamps`, matching what
|
|
375
|
+
* {@link parseDbDate} already does for the JSON path and what the OID 1114
|
|
376
|
+
* parser already does for `timestamp`.
|
|
377
|
+
*
|
|
378
|
+
* `fallback` is the parser this one REPLACES, and it must be captured with
|
|
379
|
+
* `pg.types.getTypeParser(1082, 'text')` BEFORE registration (reading it after
|
|
380
|
+
* would hand back this function and recurse forever). It keeps `infinity` /
|
|
381
|
+
* `-infinity` on the driver's `Infinity` / `-Infinity`.
|
|
382
|
+
*
|
|
383
|
+
* `setUTCFullYear` rather than the `Date` constructor, so a two-or-three-digit
|
|
384
|
+
* year is not silently mapped into the 1900s, and ` BC` maps to the
|
|
385
|
+
* astronomical year (`0044 BC` → -43) the way the driver's own parser does.
|
|
386
|
+
*/
|
|
387
|
+
function createUtcDateParser(fallback) {
|
|
388
|
+
return (text) => {
|
|
389
|
+
const m = PG_DATE_TEXT_RE.exec(text);
|
|
390
|
+
if (!m)
|
|
391
|
+
return fallback(text);
|
|
392
|
+
const year = m[4] ? -(Number(m[1]) - 1) : Number(m[1]);
|
|
393
|
+
const date = new Date(0);
|
|
394
|
+
date.setUTCFullYear(year, Number(m[2]) - 1, Number(m[3]));
|
|
395
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
396
|
+
return date;
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* A Postgres `timestamp` (without time zone) wire value:
|
|
401
|
+
* `YYYY-MM-DD HH:MM:SS`, optionally with more than four year digits, optional
|
|
402
|
+
* fractional seconds, optional ` BC`. As with {@link PG_DATE_TEXT_RE},
|
|
403
|
+
* `infinity` / `-infinity` and any shape a future server adds deliberately do
|
|
404
|
+
* NOT match, so they fall through to the driver's own parser untouched.
|
|
405
|
+
*/
|
|
406
|
+
const PG_TIMESTAMP_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?( BC)?$/;
|
|
407
|
+
/**
|
|
408
|
+
* Build the driver parser for Postgres `timestamp` (OID 1114) that reads an
|
|
409
|
+
* offset-less date-time as UTC. Also lifted to the `_timestamp` array OID
|
|
410
|
+
* (1115), so the scalar and the array can never settle on different
|
|
411
|
+
* interpretations.
|
|
412
|
+
*
|
|
413
|
+
* `fallback` is the parser this one REPLACES and must be captured with
|
|
414
|
+
* `pg.types.getTypeParser(1114, 'text')` BEFORE registration (see
|
|
415
|
+
* {@link createUtcDateParser}). It is what keeps `infinity` / `-infinity` on
|
|
416
|
+
* the driver's `Infinity` / `-Infinity`: the earlier
|
|
417
|
+
* `new Date(text.replace(' ', 'T') + 'Z')` form turned `'infinity'` into
|
|
418
|
+
* `'infinityZ'` and so into an `Invalid Date` that flowed on silently.
|
|
419
|
+
*
|
|
420
|
+
* Component assembly rather than `Date` string parsing, for the same reason as
|
|
421
|
+
* the `date` parser: a year outside four digits and a ` BC` suffix are not
|
|
422
|
+
* parseable as ISO-8601 and would otherwise also become `Invalid Date`.
|
|
423
|
+
* Fractional seconds are truncated to milliseconds, which is what
|
|
424
|
+
* `Date`-string parsing did too.
|
|
425
|
+
*/
|
|
426
|
+
function createUtcTimestampParser(fallback) {
|
|
427
|
+
return (text) => {
|
|
428
|
+
const m = PG_TIMESTAMP_TEXT_RE.exec(text);
|
|
429
|
+
if (!m)
|
|
430
|
+
return fallback(text);
|
|
431
|
+
const year = m[8] ? -(Number(m[1]) - 1) : Number(m[1]);
|
|
432
|
+
const ms = m[7] ? Number(m[7].slice(0, 3).padEnd(3, '0')) : 0;
|
|
433
|
+
const date = new Date(0);
|
|
434
|
+
date.setUTCFullYear(year, Number(m[2]) - 1, Number(m[3]));
|
|
435
|
+
date.setUTCHours(Number(m[4]), Number(m[5]), Number(m[6]), ms);
|
|
436
|
+
return date;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The offset-less-timestamp-as-UTC reading, with no fallback: `text` must be a
|
|
441
|
+
* plain `YYYY-MM-DD HH:MM:SS[.ffffff]`. Used where the input shape is already
|
|
442
|
+
* known (tests, JSON-wire coercion); the DRIVER parser is
|
|
443
|
+
* {@link createUtcTimestampParser}, which delegates everything else.
|
|
444
|
+
*/
|
|
445
|
+
function parseUtcTimestampText(text) {
|
|
446
|
+
return new Date(`${text.replace(' ', 'T')}Z`);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Lift an element parser to the matching Postgres array OID.
|
|
450
|
+
*
|
|
451
|
+
* Array OIDs do NOT inherit their element type's parser: registering a parser
|
|
452
|
+
* for `date` (1082) leaves `date[]` (1182) on the driver's default, so the same
|
|
453
|
+
* value read from a scalar column and from an array column would disagree by
|
|
454
|
+
* the process offset. Every scalar temporal parser Turbine registers is
|
|
455
|
+
* therefore registered in its array form too.
|
|
456
|
+
*
|
|
457
|
+
* `pg.types.arrayParser` is a public member of the `pg` module (it is what the
|
|
458
|
+
* driver's own `_text` / `_date` parsers are built from), so this adds no
|
|
459
|
+
* dependency. NULL elements stay `null` and are never handed to `element`.
|
|
460
|
+
*/
|
|
461
|
+
function createPgArrayParser(element) {
|
|
462
|
+
const arrayParser = pg_1.default.types.arrayParser;
|
|
463
|
+
return (text) => arrayParser.create(text, (entry) => (entry === null || entry === undefined ? null : element(entry))).parse();
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Register the UTC readings of the four zone-less temporal OIDs on the pg
|
|
467
|
+
* module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
|
|
468
|
+
*
|
|
469
|
+
* ONE place, because `pg.types.setTypeParser` is process-global and the pairing
|
|
470
|
+
* matters: registering a scalar without its array form, or a `date` without the
|
|
471
|
+
* `timestamp` beside it, produces two columns of the same row disagreeing about
|
|
472
|
+
* what the same wire text means. Both callers are processes Turbine owns the
|
|
473
|
+
* pg module in: `TurbineClient` on a pool it created (never on an external
|
|
474
|
+
* pool, whose parser configuration belongs to the caller), and `turbine studio`,
|
|
475
|
+
* which builds a raw pool of its own and must render what the application sees.
|
|
476
|
+
*
|
|
477
|
+
* Each fallback is read BEFORE its parser is installed, so an unrecognised wire
|
|
478
|
+
* value (`infinity`, and whatever a future server adds) still reaches the
|
|
479
|
+
* driver's own parser.
|
|
480
|
+
*/
|
|
481
|
+
function registerUtcTemporalParsers() {
|
|
482
|
+
// pg-types declares get/setTypeParser over its own OID enum, which lists the
|
|
483
|
+
// scalar types only. The array OIDs are just as real, so both calls are
|
|
484
|
+
// retyped over a plain number rather than the incomplete enum.
|
|
485
|
+
const getParser = pg_1.default.types.getTypeParser;
|
|
486
|
+
const setParser = pg_1.default.types.setTypeParser;
|
|
487
|
+
const parseDate = createUtcDateParser(getParser(1082, 'text'));
|
|
488
|
+
const parseTimestamp = createUtcTimestampParser(getParser(1114, 'text'));
|
|
489
|
+
setParser(1114, parseTimestamp);
|
|
490
|
+
setParser(1082, parseDate);
|
|
491
|
+
// Array OIDs do not inherit their element parser, so `date[]` / `timestamp[]`
|
|
492
|
+
// would otherwise keep returning local-zone Dates while the scalar columns
|
|
493
|
+
// beside them returned UTC ones.
|
|
494
|
+
setParser(1182, createPgArrayParser(parseDate));
|
|
495
|
+
setParser(1115, createPgArrayParser(parseTimestamp));
|
|
496
|
+
}
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
348
498
|
// JSON-wire value coercion (relationLoadStrategy: 'join')
|
|
349
499
|
// ---------------------------------------------------------------------------
|
|
350
500
|
/**
|
|
@@ -367,7 +517,7 @@ function parseDbDate(value) {
|
|
|
367
517
|
* numeric '1000.50' (string) 1000.5 (number, LOSSY)
|
|
368
518
|
* int8 '9007199254740993' 9007199254740992 (LOSSY)
|
|
369
519
|
* bytea Buffer '\xdeadbeef' (string)
|
|
370
|
-
* date Date (
|
|
520
|
+
* date Date (UTC midnight) Date (UTC midnight, by coincidence)
|
|
371
521
|
* interval { days, hours, … } '1 day 02:03:04' (string)
|
|
372
522
|
* point { x, y } '(1,2)' (string)
|
|
373
523
|
* circle { x, y, radius } '<(1,2),3>' (string)
|
|
@@ -82,4 +82,10 @@ export declare const WARN_NS: {
|
|
|
82
82
|
* the offenders, not one per column. The namespace name is historical.
|
|
83
83
|
*/
|
|
84
84
|
readonly untypedDateColumn: "untypedDateColumn";
|
|
85
|
+
/**
|
|
86
|
+
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
87
|
+
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
|
88
|
+
* Keyed on the requested mode.
|
|
89
|
+
*/
|
|
90
|
+
readonly planCacheModeIgnored: "planCacheModeIgnored";
|
|
85
91
|
};
|
|
@@ -123,4 +123,10 @@ exports.WARN_NS = {
|
|
|
123
123
|
* the offenders, not one per column. The namespace name is historical.
|
|
124
124
|
*/
|
|
125
125
|
untypedDateColumn: 'untypedDateColumn',
|
|
126
|
+
/**
|
|
127
|
+
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
128
|
+
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
|
129
|
+
* Keyed on the requested mode.
|
|
130
|
+
*/
|
|
131
|
+
planCacheModeIgnored: 'planCacheModeIgnored',
|
|
126
132
|
};
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -385,6 +385,7 @@ exports.sqliteDialect = {
|
|
|
385
385
|
supportsAdvisoryLock: false,
|
|
386
386
|
// No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
|
|
387
387
|
supportsLateralJoin: false,
|
|
388
|
+
supportsPlanCacheMode: false,
|
|
388
389
|
// SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
|
|
389
390
|
// id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
|
|
390
391
|
explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
|
package/dist/cli/mcp.js
CHANGED
|
@@ -5,6 +5,7 @@ import pg from 'pg';
|
|
|
5
5
|
import { findMissingRelationIndexes } from '../index-advisor.js';
|
|
6
6
|
import { addAutoManyToManyRelations, buildRelationsFromForeignKeys, isUnknownTsType, } from '../introspect.js';
|
|
7
7
|
import { QueryInterface, quoteIdent } from '../query/index.js';
|
|
8
|
+
import { registerUtcTemporalParsers } from '../query/utils.js';
|
|
8
9
|
import { isDateType, pgArrayType, pgTypeToTs, snakeToCamel, } from '../schema.js';
|
|
9
10
|
import { listMigrationFiles } from './migrate.js';
|
|
10
11
|
import { applyPiiTags, loadPiiTags } from './pii-tags.js';
|
|
@@ -109,6 +110,11 @@ const TOOLS = [
|
|
|
109
110
|
export function startMcpServer(options, transport = {}) {
|
|
110
111
|
const input = transport.input ?? process.stdin;
|
|
111
112
|
const output = transport.output ?? process.stdout;
|
|
113
|
+
// Read zone-less `date` / `timestamp` values as UTC, as TurbineClient does on
|
|
114
|
+
// a pool it owns. This server builds its own raw pool, so without it a
|
|
115
|
+
// `date` sampled here is serialized at the CLI process's local midnight while
|
|
116
|
+
// the application reading the same row through Turbine sees UTC midnight.
|
|
117
|
+
registerUtcTemporalParsers();
|
|
112
118
|
const ctx = {
|
|
113
119
|
options,
|
|
114
120
|
pool: new pg.Pool({ connectionString: options.url, max: 2, idleTimeoutMillis: 10_000 }),
|
package/dist/cli/studio.js
CHANGED
|
@@ -48,7 +48,7 @@ import { introspect } from '../introspect.js';
|
|
|
48
48
|
import { QueryInterface, quoteIdent } from '../query/index.js';
|
|
49
49
|
// `ownLookup` is not re-exported from the query barrel, so it is imported from
|
|
50
50
|
// its defining leaf module rather than duplicated here.
|
|
51
|
-
import { ownLookup } from '../query/utils.js';
|
|
51
|
+
import { ownLookup, registerUtcTemporalParsers } from '../query/utils.js';
|
|
52
52
|
import { applyPiiTags, loadPiiTags } from './pii-tags.js';
|
|
53
53
|
import { callerKey, checkRateLimit } from './rate-limit.js';
|
|
54
54
|
import { createDemoContext } from './studio-demo.js';
|
|
@@ -84,6 +84,16 @@ export async function startStudio(options) {
|
|
|
84
84
|
statementTimeout = { sql: 'SELECT 1', params: [] };
|
|
85
85
|
}
|
|
86
86
|
else {
|
|
87
|
+
// Read zone-less `date` / `timestamp` values as UTC, exactly as
|
|
88
|
+
// TurbineClient does on a pool it owns. Studio builds a raw pg.Pool and
|
|
89
|
+
// never constructs a TurbineClient, so without this the viewer renders a
|
|
90
|
+
// `date` cell at the CLI process's local midnight while the application
|
|
91
|
+
// reading the same row through Turbine sees UTC midnight: the previous
|
|
92
|
+
// evening east of UTC. It also matters for `--write`, where the cell the
|
|
93
|
+
// UI echoes back is what gets stored. The CLI process is Turbine's own, so
|
|
94
|
+
// there is no foreign pg consumer to disturb, and the parsers are
|
|
95
|
+
// registered before the first query runs.
|
|
96
|
+
registerUtcTemporalParsers();
|
|
87
97
|
// pg.Pool satisfies the PgCompatPool contract (same as the external-pool
|
|
88
98
|
// seam in client.ts); the cast keeps one typed pool field for both modes.
|
|
89
99
|
pool = new pg.Pool({
|
package/dist/client.d.ts
CHANGED
|
@@ -125,6 +125,11 @@ export interface TurbineDriver {
|
|
|
125
125
|
/** SQL dialect: placeholders, transaction keywords, session-config, capability flags. */
|
|
126
126
|
readonly dialect: Dialect;
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The values PostgreSQL's `plan_cache_mode` accepts. See
|
|
130
|
+
* {@link TurbineConfig.planCacheMode}.
|
|
131
|
+
*/
|
|
132
|
+
export type PlanCacheMode = 'auto' | 'force_custom_plan' | 'force_generic_plan';
|
|
128
133
|
export interface TurbineConfig {
|
|
129
134
|
/**
|
|
130
135
|
* An external pg-compatible pool. Use this to plug in serverless drivers
|
|
@@ -176,8 +181,11 @@ export interface TurbineConfig {
|
|
|
176
181
|
warnOnUnlimited?: boolean | Record<string, boolean>;
|
|
177
182
|
/**
|
|
178
183
|
* Interpret Postgres `timestamp` (without time zone) values as UTC, both
|
|
179
|
-
* at the driver level (
|
|
180
|
-
*
|
|
184
|
+
* at the driver level (type parsers for OIDs 1114 `timestamp`, 1082 `date`,
|
|
185
|
+
* and their array forms 1115 / 1182, registered only when Turbine owns the
|
|
186
|
+
* pool) and when coercing nested-relation JSON dates. A `date` column is
|
|
187
|
+
* zone-less too, so it reads back as UTC midnight rather than the process's
|
|
188
|
+
* local midnight. This is the
|
|
181
189
|
* Prisma/Rails/Django convention and makes results independent of the
|
|
182
190
|
* server's local time zone. Default: `true`. Set `false` for the legacy
|
|
183
191
|
* local-time interpretation, which also turns off the matching WRITE-side
|
|
@@ -186,15 +194,76 @@ export interface TurbineConfig {
|
|
|
186
194
|
*
|
|
187
195
|
* PER PROCESS, NOT PER CLIENT. The read half is a pg type parser, and
|
|
188
196
|
* `pg.types.setTypeParser` installs one parser per OID for the whole
|
|
189
|
-
* process. The first
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
197
|
+
* process. The first client settles it for every later one, so constructing
|
|
198
|
+
* a second client with the OPPOSITE value throws a `ValidationError` rather
|
|
199
|
+
* than handing back a client whose writes and reads disagree. Give every
|
|
200
|
+
* client in the process the same value, or isolate the odd one in its own
|
|
201
|
+
* process. A client on an EXTERNAL pool never REGISTERS a parser (it inherits
|
|
202
|
+
* whatever configuration the caller's driver has, so a process containing
|
|
203
|
+
* only external-pool clients is untouched), but it does take part in the
|
|
204
|
+
* agreement check: registration is process-global, so once a Turbine-owned
|
|
205
|
+
* client has installed the parsers an external-pool client reads through them
|
|
206
|
+
* too, and a disagreeing one would write local `date` literals while reading
|
|
207
|
+
* UTC.
|
|
196
208
|
*/
|
|
197
209
|
utcTimestamps?: boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Pin `plan_cache_mode` on every connection this client opens, fixing how the
|
|
212
|
+
* PostgreSQL backend chooses between a custom plan (re-planned per parameter
|
|
213
|
+
* set) and a generic plan (planned once, blind to the values).
|
|
214
|
+
*
|
|
215
|
+
* Why it exists: Turbine sends NAMED prepared statements by default on a pool
|
|
216
|
+
* it owns, and PostgreSQL promotes a named statement to a generic plan after
|
|
217
|
+
* five executions. A generic plan is planned for the AVERAGE parameter, so a
|
|
218
|
+
* predicate whose selectivity varies wildly per value (the canonical case is a
|
|
219
|
+
* `tenant_id` / `user_id` equality on a shared table, where one value matches
|
|
220
|
+
* a handful of rows and another matches most of them) is planned blind to the
|
|
221
|
+
* value it will actually get, and it never reverts. `'force_custom_plan'`
|
|
222
|
+
* removes that, at the cost of re-planning each execution.
|
|
223
|
+
*
|
|
224
|
+
* Scope, measured rather than assumed: it applies to the statements Turbine
|
|
225
|
+
* itself promotes. `count()` on such a predicate is promoted after five
|
|
226
|
+
* executions and is controlled by this option; `findMany` / `findFirst` bind
|
|
227
|
+
* `LIMIT $n`, and a parameterized limit denies the planner the limit fraction
|
|
228
|
+
* that makes a skewed plan look cheap, so those are much less exposed. Treat
|
|
229
|
+
* the option as a targeted remedy for a plan that is measurably worse after
|
|
230
|
+
* its fifth execution, not a general speed-up.
|
|
231
|
+
*
|
|
232
|
+
* Default `undefined`: Turbine issues NOTHING and the backend keeps its own
|
|
233
|
+
* default (`auto`), byte-identical to not setting the option.
|
|
234
|
+
*
|
|
235
|
+
* SESSION-LEVEL, NOT PER QUERY. It is applied as a connection parameter
|
|
236
|
+
* (`options=-c plan_cache_mode=...`) when the pool opens a connection, so it
|
|
237
|
+
* is in force for that connection's very first statement and persists for its
|
|
238
|
+
* whole life: every pooled checkout, `$transaction`, stream and pipeline on
|
|
239
|
+
* that connection inherits it, and it is NOT reset between checkouts. A
|
|
240
|
+
* caller's own `SET` / `SET LOCAL` still overrides it for that session or
|
|
241
|
+
* transaction, exactly as it would over any other session default.
|
|
242
|
+
*
|
|
243
|
+
* POSTGRES-ONLY. Engines whose dialect does not report
|
|
244
|
+
* `supportsPlanCacheMode` throw {@link UnsupportedFeatureError} (E017) at
|
|
245
|
+
* construction. The capability flag can only speak for the DIALECT, though,
|
|
246
|
+
* and `plan_cache_mode` is PostgreSQL 12+: a Postgres wire-compatible engine
|
|
247
|
+
* driven through the default `postgresDialect` (CockroachDB, YugabyteDB, an
|
|
248
|
+
* older server) has no such setting, and rejects the connection parameter
|
|
249
|
+
* itself with `unrecognized configuration parameter` at the first checkout
|
|
250
|
+
* rather than with E017. Leave the option unset on those.
|
|
251
|
+
*
|
|
252
|
+
* EXTERNAL POOLS ARE NOT TOUCHED. When the caller supplies `pool`, they own
|
|
253
|
+
* connection lifecycle, and Turbine has no hook that runs on their
|
|
254
|
+
* connections without also mutating a pool it does not own. Same rule as the
|
|
255
|
+
* type parsers above. The option is then a no-op on that pool, with a
|
|
256
|
+
* dev-mode warning; a serverless/HTTP driver should set the GUC in its own
|
|
257
|
+
* connection setup. Turbine-OWNED string `replicas` are still Turbine's own
|
|
258
|
+
* connections and do get it, even next to an external primary, so setting it
|
|
259
|
+
* in that shape splits the policy between reads and writes.
|
|
260
|
+
*
|
|
261
|
+
* BEHIND A CONNECTION POOLER, the GUC travels as a connection-time `options`
|
|
262
|
+
* startup parameter, which a pooler may refuse to pass through (PgBouncer's
|
|
263
|
+
* `ignore_startup_parameters`). Set it on the role or server there
|
|
264
|
+
* (`ALTER ROLE ... SET plan_cache_mode = ...`) instead.
|
|
265
|
+
*/
|
|
266
|
+
planCacheMode?: PlanCacheMode;
|
|
198
267
|
/**
|
|
199
268
|
* Refuse a nested `connect` / `connectOrCreate` that would re-parent a
|
|
200
269
|
* to-many child already owned by a different parent. Off by default,
|
|
@@ -545,21 +614,33 @@ export declare class TurbineClient {
|
|
|
545
614
|
readonly schema: SchemaMetadata;
|
|
546
615
|
private static int8ParserRegistered;
|
|
547
616
|
/**
|
|
548
|
-
* The `utcTimestamps` value the FIRST
|
|
549
|
-
*
|
|
550
|
-
* client has been constructed yet.
|
|
617
|
+
* The `utcTimestamps` value the FIRST TurbineClient in this process settled
|
|
618
|
+
* on, or `undefined` while none has been constructed yet.
|
|
551
619
|
*
|
|
552
620
|
* `pg.types.setTypeParser` is process-global by nature: there is one parser
|
|
553
621
|
* per OID for the whole pg module, so the READ side of `utcTimestamps` cannot
|
|
554
622
|
* be per client the way the WRITE side is. Recording the settled value (not
|
|
555
623
|
* just "registered yes/no") is what lets the constructor detect a second
|
|
556
624
|
* client asking for the opposite and refuse it, instead of handing back a
|
|
557
|
-
* client whose reads and writes disagree.
|
|
625
|
+
* client whose reads and writes disagree. Every client records it, including
|
|
626
|
+
* one on an external pool, which registers nothing but still READS through
|
|
627
|
+
* whatever an owned client in the same process installed. See
|
|
628
|
+
* {@link assertUtcTimestampsAgree}.
|
|
558
629
|
*/
|
|
559
630
|
private static utcTimestampParserMode;
|
|
631
|
+
/**
|
|
632
|
+
* Whether the zone-less temporal read parsers (OIDs 1114, 1082, 1115, 1182)
|
|
633
|
+
* have actually been installed. Separate from
|
|
634
|
+
* {@link utcTimestampParserMode}, which every client settles: only an OWNED
|
|
635
|
+
* pool registers, so a client on an external pool must not make a later
|
|
636
|
+
* owned client skip registration.
|
|
637
|
+
*/
|
|
638
|
+
private static utcTimestampParsersRegistered;
|
|
560
639
|
private readonly logging;
|
|
561
640
|
/** Active SQL dialect, owns transaction keywords, set_config, raw-SQL placeholders, capability flags. */
|
|
562
641
|
private readonly dialect;
|
|
642
|
+
/** Validated `plan_cache_mode` to pin on every owned connection, or undefined to issue nothing. */
|
|
643
|
+
private readonly planCacheMode;
|
|
563
644
|
private readonly tableCache;
|
|
564
645
|
private readonly middlewares;
|
|
565
646
|
private readonly queryListeners;
|
|
@@ -592,19 +673,89 @@ export declare class TurbineClient {
|
|
|
592
673
|
private primaryView?;
|
|
593
674
|
constructor(config: TurbineConfig | undefined, schema: SchemaMetadata);
|
|
594
675
|
/**
|
|
595
|
-
*
|
|
596
|
-
*
|
|
676
|
+
* Validate a caller-supplied `planCacheMode` and refuse it on an engine that
|
|
677
|
+
* has no plan cache to pin.
|
|
678
|
+
*
|
|
679
|
+
* Two refusals, both at construction rather than at first query, so a
|
|
680
|
+
* misconfigured client never opens a connection:
|
|
681
|
+
*
|
|
682
|
+
* - a value outside {@link PLAN_CACHE_MODES} throws `ValidationError`
|
|
683
|
+
* (E003). This is the security boundary as well as the usability one: a
|
|
684
|
+
* GUC value cannot be a bind parameter in either place it can be set (a
|
|
685
|
+
* `SET` statement or the connection `options` string), so the returned
|
|
686
|
+
* value is a MEMBER OF THE FROZEN LIST, never the caller's string, and
|
|
687
|
+
* there is no path by which caller text reaches the connection or the SQL.
|
|
688
|
+
* - a dialect that does not report `supportsPlanCacheMode` throws
|
|
689
|
+
* `UnsupportedFeatureError` (E017), in the same style as the other
|
|
690
|
+
* capability refusals.
|
|
691
|
+
*/
|
|
692
|
+
private static resolvePlanCacheMode;
|
|
693
|
+
/**
|
|
694
|
+
* Pin `plan_cache_mode` on every connection an OWNED pool opens, by putting
|
|
695
|
+
* it in the pool's **connection parameters** rather than issuing a `SET`.
|
|
696
|
+
*
|
|
697
|
+
* PostgreSQL's `options` startup parameter (`-c plan_cache_mode=...`) is
|
|
698
|
+
* applied by the backend as it starts the session, so the setting is in force
|
|
699
|
+
* for the connection's very first statement and for its whole life: every
|
|
700
|
+
* pooled checkout, `$transaction`, stream and pipeline on it inherits it.
|
|
701
|
+
* There is no per-checkout reset, and none is wanted, that IS the intent.
|
|
702
|
+
*
|
|
703
|
+
* Why not `pool.on('connect', c => c.query('SET ...'))`, the obvious
|
|
704
|
+
* alternative: pg hands the fresh client to the waiting caller in the same
|
|
705
|
+
* tick it emits `connect`, so the caller's first query is issued while the
|
|
706
|
+
* `SET` is still the active query. That path works today only through pg's
|
|
707
|
+
* deprecated same-client query queueing (it logs a DeprecationWarning per new
|
|
708
|
+
* connection and is slated for removal in pg 9), and it costs an extra round
|
|
709
|
+
* trip on every connection. The startup parameter costs nothing and cannot
|
|
710
|
+
* race.
|
|
711
|
+
*
|
|
712
|
+
* Nothing the caller already set is discarded, in either of the two places
|
|
713
|
+
* pg reads `options` from. pg's `ConnectionParameters` lets a value parsed
|
|
714
|
+
* out of a `connectionString` OVERRIDE the explicit `options` field, so when
|
|
715
|
+
* the URL already carries `?options=...` the GUC is appended THERE; and the
|
|
716
|
+
* explicit field itself falls back to `process.env.PGOPTIONS` only while it
|
|
717
|
+
* is unset, so setting it blind would silently drop a deployment's
|
|
718
|
+
* `PGOPTIONS` (its `search_path` or `statement_timeout`, not merely a slower
|
|
719
|
+
* plan). Both are read first and the GUC is appended to whichever applies.
|
|
720
|
+
*
|
|
721
|
+
* One deployment caveat: an `options` startup parameter is a connection-time
|
|
722
|
+
* parameter, and a connection pooler in front of Postgres may reject
|
|
723
|
+
* parameters it is not configured to pass through (PgBouncer's
|
|
724
|
+
* `ignore_startup_parameters`). A `SET` on checkout would survive that, at
|
|
725
|
+
* the cost of the race and the round trip above. Callers behind such a pooler
|
|
726
|
+
* should set the GUC on the server or role instead
|
|
727
|
+
* (`ALTER ROLE ... SET plan_cache_mode = ...`).
|
|
728
|
+
*/
|
|
729
|
+
private static withPlanCacheMode;
|
|
730
|
+
/**
|
|
731
|
+
* `connectionString` with `setting` appended to its existing `options` query
|
|
732
|
+
* parameter, or `null` when it carries no `options` (in which case the caller
|
|
733
|
+
* should use the `options` pool field, which is not overridden).
|
|
734
|
+
*
|
|
735
|
+
* Only the query string is rewritten, never the userinfo or host, so a
|
|
736
|
+
* percent-encoded password cannot be mangled by a round trip through `URL`.
|
|
737
|
+
* The split is on the first `?`, which is also where pg's own parser puts the
|
|
738
|
+
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
739
|
+
* password is not parseable by pg either, so there is no shape this handles
|
|
740
|
+
* differently from the driver.
|
|
741
|
+
*/
|
|
742
|
+
private static mergeConnectionStringOptions;
|
|
743
|
+
/**
|
|
744
|
+
* Refuse a `utcTimestamps` value that contradicts the one an earlier client
|
|
745
|
+
* in this process settled the zone-less temporal read parsers (OIDs 1114,
|
|
746
|
+
* 1082, 1115, 1182) on.
|
|
597
747
|
*
|
|
598
748
|
* The flag has two halves. The WRITE half is per client: a bound `Date` on a
|
|
599
749
|
* zone-less `date` / `timestamp` column is rewritten to a UTC literal unless
|
|
600
750
|
* the owning client opted out (`coerceWriteValue` in query/writes.ts). The
|
|
601
|
-
* READ half is the pg type
|
|
602
|
-
* installs ONE parser per OID for the whole process,
|
|
603
|
-
* every raw query, and any other library using the same
|
|
604
|
-
* no per-pool parser hook to bind it to, and moving the
|
|
605
|
-
* `parseRow` instead would leave every non-ORM read (raw SQL,
|
|
606
|
-
* a caller's own `pool.query`) on the driver's value while
|
|
607
|
-
* default path's output type, so the read half stays
|
|
751
|
+
* READ half is the pg type parsers for OIDs 1114 / 1082 / 1115 / 1182, and
|
|
752
|
+
* `pg.types.setTypeParser` installs ONE parser per OID for the whole process,
|
|
753
|
+
* shared by every pool, every raw query, and any other library using the same
|
|
754
|
+
* pg module. There is no per-pool parser hook to bind it to, and moving the
|
|
755
|
+
* coercion into `parseRow` instead would leave every non-ORM read (raw SQL,
|
|
756
|
+
* `client.sql`, a caller's own `pool.query`) on the driver's value while
|
|
757
|
+
* changing the default path's output type, so the read half stays
|
|
758
|
+
* process-wide.
|
|
608
759
|
*
|
|
609
760
|
* That makes the mixed shape unserveable rather than merely awkward: the
|
|
610
761
|
* second client would write local calendar fields and read them back as UTC
|
|
@@ -612,9 +763,15 @@ export declare class TurbineClient {
|
|
|
612
763
|
* A client that silently does not round-trip is the worst of the three
|
|
613
764
|
* outcomes, so construction fails with the two ways out.
|
|
614
765
|
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
766
|
+
* EVERY client takes part, not only the ones on a Turbine-owned pool. Only an
|
|
767
|
+
* owned pool REGISTERS the parsers, but registration is process-global, so a
|
|
768
|
+
* client on an external pool (Neon, Vercel Postgres, Hyperdrive) constructed
|
|
769
|
+
* alongside an owned one reads through them too. It is exactly the pairing
|
|
770
|
+
* that produced a silent read/write disagreement: an external-pool client
|
|
771
|
+
* with `utcTimestamps: false` writing local `date` literals while reading UTC
|
|
772
|
+
* ones, which walks the stored calendar day back a day per read-modify-write
|
|
773
|
+
* cycle. An external-pool client ALONE in a process is unaffected: it settles
|
|
774
|
+
* the value, registers nothing, and keeps the caller's parser configuration.
|
|
618
775
|
*/
|
|
619
776
|
private static assertUtcTimestampsAgree;
|
|
620
777
|
/**
|