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 CHANGED
@@ -429,7 +429,7 @@ const db = turbine({
429
429
  });
430
430
  ```
431
431
 
432
- > **`utcTimestamps` is a process-wide decision, not a per-client one.** The two halves settle differently. The WRITE half is per client: a `Date` bound to a zone-less `date` / `timestamp` column is rewritten to a UTC literal unless that client opted out. The READ half is a pg type parser on OID 1114, and `pg.types.setTypeParser` installs one parser per OID for the whole pg module, shared by every pool, every raw query, and any other library on the same `pg`. The first Turbine-owned client in the process settles it for all the rest. Constructing a second Turbine-owned client with the **opposite** value therefore throws `ValidationError` (`TURBINE_E003`) at construction, rather than handing back a client that writes UTC and reads local (or the reverse) and so does not round-trip its own values. The message names both values and the two ways out: give every client in the process the same `utcTimestamps`, or run the odd one out in its own process. Clients built on an external pool (`pool: ...`, `turbineHttp()`) never register a parser, settle nothing, and are exempt from the check, they inherit whatever parser configuration the caller set up.
432
+ > **`utcTimestamps` is a process-wide decision, not a per-client one.** The two halves settle differently. The WRITE half is per client: a `Date` bound to a zone-less `date` / `timestamp` column is rewritten to a UTC literal unless that client opted out. The READ half is a set of pg type parsers on OIDs 1114 (`timestamp`), 1082 (`date`) and their array forms 1115 / 1182, and `pg.types.setTypeParser` installs one parser per OID for the whole pg module, shared by every pool, every raw query, and any other library on the same `pg`. The first client in the process settles it for all the rest. Constructing a second client with the **opposite** value therefore throws `ValidationError` (`TURBINE_E003`) at construction, rather than handing back a client that writes UTC and reads local (or the reverse) and so does not round-trip its own values. The message names both values and the two ways out: give every client in the process the same `utcTimestamps`, or run the odd one out in its own process. Clients built on an external pool (`pool: ...`, `turbineHttp()`) never REGISTER a parser and settle nothing on their own, so a process holding only those keeps whatever parser configuration the caller set up. They are not exempt from the check, though: registration is process-global, so once a Turbine-owned client has installed the parsers an external-pool client reads through them too, and a disagreeing one would write local `date` literals while reading UTC.
433
433
 
434
434
  > **Upgrading with `utcTimestamps: false`.** If you already set `false`, this release changes the **stored text** of your writes: the write path now honors the flag where it previously ignored it, so zone-less columns receive local-calendar literals instead of UTC ones. Rows written before the upgrade and rows written after carry two conventions in the same column until you backfill.
435
435
 
@@ -446,11 +446,16 @@ const db = turbine({
446
446
  preparedStatements: true, // see the warning below
447
447
  sqlCache: true, // SQL template cache (default true)
448
448
  sqlCacheSize: 1000, // distinct query SHAPES retained per table (default 1000)
449
+ // Postgres only, opt-in, unset by default (Turbine then sends nothing).
450
+ // Pins how the backend picks between a custom and a generic plan.
451
+ // planCacheMode: 'force_custom_plan',
449
452
  });
450
453
  ```
451
454
 
452
455
  Where a pg-style alias exists (`max`, `idleTimeoutMillis`, `connectionTimeoutMillis`), the explicit Turbine field wins when both are set.
453
456
 
457
+ > **`planCacheMode` (Postgres only, opt-in).** PostgreSQL promotes a **named** prepared statement to a generic plan on its sixth execution, and a generic plan is costed blind to the bound values. On a predicate whose selectivity swings per value (a `tenant_id` equality on a shared table, where one value matches a handful of rows and another matches most of them), the statement can be locked onto a plan chosen for the average value, and it never reverts. `planCacheMode: 'auto' | 'force_custom_plan' | 'force_generic_plan'` pins the backend's choice; `'force_custom_plan'` re-plans every execution and removes the cliff. It is applied as a connection parameter (`options=-c plan_cache_mode=...`) when Turbine opens a connection, so it is in force for that connection's first statement and for every checkout, `$transaction`, stream and pipeline on it, and it cannot race your first query. Leave it unset (the default) and Turbine sends nothing at all. Reach for it when you have measured a statement getting slower after its fifth execution: `count()` on a skewed predicate is the shape that promotes through Turbine, while `findMany` / `findFirst` bind `LIMIT $n` and are much less exposed. Three scope limits: it does nothing on an **external pool** (Turbine never opens those connections, so set the GUC in the driver's own setup; Turbine-owned string `replicas` on that same client DO get it); a Postgres wire-compatible engine without the setting (CockroachDB, YugabyteDB, pre-12 PostgreSQL) refuses the connection parameter itself; and a **connection pooler** may filter startup parameters (PgBouncer's `ignore_startup_parameters`), where `ALTER ROLE ... SET plan_cache_mode = ...` is the way in. Any value outside the three throws `ValidationError` at construction, and a non-Postgres engine throws `UnsupportedFeatureError` (`TURBINE_E017`).
458
+
454
459
  > **`preparedStatements` and connection poolers.** With prepared statements on, Turbine submits queries as `{ name, text, values }` so Postgres caches the parse and plan **per backend connection**. That is a real win against a database you connect to directly, and a hazard behind a transaction-pooling proxy (PgBouncer in `transaction` mode, Supabase's pooler port, some serverless poolers): the named statement is prepared on one backend and your next query may land on another, which fails with `prepared statement "..." does not exist`. Turbine defaults it to `true` only for pools it creates itself and `false` for external pools passed via `pool` / `turbineHttp()`, because serverless drivers are the common case there. If you are pointing a Turbine-owned pool at a transaction pooler, set `preparedStatements: false`. The environment variable `TURBINE_DISABLE_PREPARED=1` turns it off globally without a code change.
455
460
 
456
461
  ### Client escape hatches
@@ -1092,7 +1097,7 @@ Turbine maps Postgres types to TypeScript:
1092
1097
  | `int8` / `bigint` | `number` | Values > `Number.MAX_SAFE_INTEGER` (2^53 - 1) are returned as `string` at runtime to avoid precision loss. This affects < 0.01% of use cases (auto-increment IDs, counts, etc. are all safe). |
1093
1098
  | `numeric`, `money` | `string` | Arbitrary precision, kept as string to avoid JS float issues |
1094
1099
  | `text`, `varchar`, `uuid`, `citext` | `string` | |
1095
- | `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) is parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. Since v0.52 the flag also reaches the WRITE side on Postgres: it governs the `Date` values that `create` / `update` / `upsert` and `where` clauses bind to zone-less `date` / `timestamp` columns. Before v0.52 it reached the read path only, so a client that had set `false` was still binding UTC, and those statements (and the text they store) change on upgrade. The two halves settle at different scopes: the write half is per client, the read half is a process-global pg type parser, so two Turbine-owned clients in one process must agree or construction throws `ValidationError`. See [Relation loading and wire encoding](#relation-loading-and-wire-encoding). |
1100
+ | `timestamptz`, `timestamp`, `date` | `Date` | `timestamp` (without time zone) **and `date`** are parsed as UTC by default (Prisma/Rails/Django convention), so the same row yields the same instant in every region. Opt out with `utcTimestamps: false`. Since v0.52 the flag also reaches the WRITE side on Postgres: it governs the `Date` values that `create` / `update` / `upsert` and `where` clauses bind to zone-less `date` / `timestamp` columns. Before v0.52 it reached the read path only, so a client that had set `false` was still binding UTC, and those statements (and the text they store) change on upgrade. The two halves settle at different scopes: the write half is per client, the read half is a process-global pg type parser, so every client in one process must agree or construction throws `ValidationError`. Since v0.54 `date` reads at UTC midnight rather than the process's local midnight (its array form too), which moves the **epoch value** of a `date` by your process's UTC offset even though the calendar day it denotes is unchanged: format with `toISOString().slice(0, 10)`, not with local-component helpers like `toLocaleDateString()`. See [Relation loading and wire encoding](#relation-loading-and-wire-encoding). |
1096
1101
  | `boolean` | `boolean` | |
1097
1102
  | `json`, `jsonb` | `unknown` | |
1098
1103
  | `bytea` | `Buffer` | |
@@ -13,6 +13,7 @@ const pg_1 = __importDefault(require("pg"));
13
13
  const index_advisor_js_1 = require("../index-advisor.js");
14
14
  const introspect_js_1 = require("../introspect.js");
15
15
  const index_js_1 = require("../query/index.js");
16
+ const utils_js_1 = require("../query/utils.js");
16
17
  const schema_js_1 = require("../schema.js");
17
18
  const migrate_js_1 = require("./migrate.js");
18
19
  const pii_tags_js_1 = require("./pii-tags.js");
@@ -117,6 +118,11 @@ const TOOLS = [
117
118
  function startMcpServer(options, transport = {}) {
118
119
  const input = transport.input ?? process.stdin;
119
120
  const output = transport.output ?? process.stdout;
121
+ // Read zone-less `date` / `timestamp` values as UTC, as TurbineClient does on
122
+ // a pool it owns. This server builds its own raw pool, so without it a
123
+ // `date` sampled here is serialized at the CLI process's local midnight while
124
+ // the application reading the same row through Turbine sees UTC midnight.
125
+ (0, utils_js_1.registerUtcTemporalParsers)();
120
126
  const ctx = {
121
127
  options,
122
128
  pool: new pg_1.default.Pool({ connectionString: options.url, max: 2, idleTimeoutMillis: 10_000 }),
@@ -103,6 +103,16 @@ async function startStudio(options) {
103
103
  statementTimeout = { sql: 'SELECT 1', params: [] };
104
104
  }
105
105
  else {
106
+ // Read zone-less `date` / `timestamp` values as UTC, exactly as
107
+ // TurbineClient does on a pool it owns. Studio builds a raw pg.Pool and
108
+ // never constructs a TurbineClient, so without this the viewer renders a
109
+ // `date` cell at the CLI process's local midnight while the application
110
+ // reading the same row through Turbine sees UTC midnight: the previous
111
+ // evening east of UTC. It also matters for `--write`, where the cell the
112
+ // UI echoes back is what gets stored. The CLI process is Turbine's own, so
113
+ // there is no foreign pg consumer to disturb, and the parsers are
114
+ // registered before the first query runs.
115
+ (0, utils_js_1.registerUtcTemporalParsers)();
106
116
  // pg.Pool satisfies the PgCompatPool contract (same as the external-pool
107
117
  // seam in client.ts); the cast keeps one typed pool field for both modes.
108
118
  pool = new pg_1.default.Pool({
@@ -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
  /**