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.
@@ -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 (local midnight) Date (UTC midnight, off by tz)
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
  };
@@ -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 }),
@@ -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 (OID 1114 type parser, registered only when Turbine
180
- * owns the pool) and when coercing nested-relation JSON dates. This is the
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 Turbine-owned client settles it for every later one, so
190
- * constructing a second client with the OPPOSITE value throws a
191
- * `ValidationError` rather than handing back a client whose writes and reads
192
- * disagree. Give every client in the process the same value, or isolate the
193
- * odd one in its own process. Clients on an EXTERNAL pool never register a
194
- * parser and never take part in the check: they inherit whatever parser
195
- * configuration the caller's driver has.
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 Turbine-owned pool in this process
549
- * settled the OID 1114 read parser on, or `undefined` while no Turbine-owned
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. See {@link assertUtcTimestampsAgree}.
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
- * Refuse a `utcTimestamps` value that contradicts the one the process-global
596
- * OID 1114 read parser was already settled on.
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 parser for OID 1114, and `pg.types.setTypeParser`
602
- * installs ONE parser per OID for the whole process, shared by every pool,
603
- * every raw query, and any other library using the same pg module. There is
604
- * no per-pool parser hook to bind it to, and moving the coercion into
605
- * `parseRow` instead would leave every non-ORM read (raw SQL, `client.sql`,
606
- * a caller's own `pool.query`) on the driver's value while changing the
607
- * default path's output type, so the read half stays process-wide.
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
- * Only Turbine-owned pools take part. An external pool (Neon, Vercel
616
- * Postgres, Hyperdrive) inherits the caller's parser configuration and
617
- * Turbine never registers on its behalf, so it has no read half to contradict.
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
  /**