turbine-orm 0.54.0 → 0.55.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 +3 -3
- package/dist/cjs/client.d.ts +85 -15
- package/dist/cjs/client.js +22 -2
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +38 -4
- package/dist/cjs/query/builder.d.ts +56 -1
- package/dist/cjs/query/builder.js +145 -23
- package/dist/cjs/query/deferred.d.ts +35 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/utils.d.ts +83 -0
- package/dist/cjs/query/utils.js +174 -4
- package/dist/cjs/query/warn-registry.d.ts +14 -0
- package/dist/cjs/query/warn-registry.js +14 -0
- package/dist/cjs/query/where.d.ts +8 -0
- package/dist/client.d.ts +85 -15
- package/dist/client.js +23 -3
- package/dist/index.d.ts +1 -1
- package/dist/query/aggregates.js +39 -5
- package/dist/query/builder.d.ts +56 -1
- package/dist/query/builder.js +146 -24
- package/dist/query/deferred.d.ts +35 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/utils.d.ts +83 -0
- package/dist/query/utils.js +170 -4
- package/dist/query/warn-registry.d.ts +14 -0
- package/dist/query/warn-registry.js +14 -0
- package/dist/query/where.d.ts +8 -0
- package/package.json +1 -1
|
@@ -35,6 +35,36 @@ export interface DeferredQuery<T> {
|
|
|
35
35
|
*/
|
|
36
36
|
reselect?: (exec: ReselectExecutor) => Promise<pg.QueryResult>;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* How the ORM hands back a Postgres temporal `infinity` / `-infinity`.
|
|
40
|
+
*
|
|
41
|
+
* JavaScript has no `Date` for either value, so every available reading is
|
|
42
|
+
* wrong in some way and the choice is which way:
|
|
43
|
+
*
|
|
44
|
+
* `'preserve'` the default. The JS numbers `Infinity` / `-Infinity`,
|
|
45
|
+
* normalized to the same value on EVERY read strategy (the
|
|
46
|
+
* join and positional paths see the JSON string `"infinity"`
|
|
47
|
+
* and are mapped to the number, which is what 0.54 got wrong).
|
|
48
|
+
* LOSSLESS: binding the number back to a temporal column
|
|
49
|
+
* stores `infinity` again, so `update({ data: { ...row } })`
|
|
50
|
+
* round-trips. THE COST: the field's declared type is `Date`,
|
|
51
|
+
* so `row.validUntil.toISOString()` throws a TypeError on
|
|
52
|
+
* those rows, and `JSON.stringify` still renders the value
|
|
53
|
+
* `null`.
|
|
54
|
+
* `'null'` opt-in. Matches what a caller serializing the row already
|
|
55
|
+
* saw (`JSON.stringify` renders every other candidate as null
|
|
56
|
+
* too) and is permitted by the declared type of a nullable
|
|
57
|
+
* column, so no method call throws. THE COST is data loss: a
|
|
58
|
+
* stored `infinity` and a stored NULL become
|
|
59
|
+
* indistinguishable, so a read-modify-write over a nullable
|
|
60
|
+
* column (`update({ data: { ...row } })`) writes SQL NULL and
|
|
61
|
+
* the infinity is gone, with no error.
|
|
62
|
+
*
|
|
63
|
+
* The default is the reading that cannot lose a value. Pick `'null'` when the
|
|
64
|
+
* declared type contract matters more than the stored value, knowing that rows
|
|
65
|
+
* read under it must not be written back.
|
|
66
|
+
*/
|
|
67
|
+
export type TemporalInfinityReading = 'null' | 'preserve';
|
|
38
68
|
/** Middleware function type, imported from client to avoid circular deps */
|
|
39
69
|
export type MiddlewareFn = (params: {
|
|
40
70
|
model: string;
|
|
@@ -142,6 +172,11 @@ export interface QueryInterfaceOptions {
|
|
|
142
172
|
* the pre-0.26 behavior (JS local-time interpretation).
|
|
143
173
|
*/
|
|
144
174
|
utcTimestamps?: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back. See
|
|
177
|
+
* {@link TemporalInfinityReading}. Default `'preserve'`.
|
|
178
|
+
*/
|
|
179
|
+
temporalInfinity?: TemporalInfinityReading;
|
|
145
180
|
/**
|
|
146
181
|
* Client-level default relation-loading strategy for `with` clauses; a
|
|
147
182
|
* per-query `relationLoadStrategy` arg overrides it. On SQL engines the default
|
|
@@ -10,5 +10,5 @@ export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, C
|
|
|
10
10
|
export { postgresDialect } from '../dialect.js';
|
|
11
11
|
export type { SqlCacheEntry } from './utils.js';
|
|
12
12
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
13
|
-
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './builder.js';
|
|
13
|
+
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './builder.js';
|
|
14
14
|
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
|
@@ -176,6 +176,38 @@ export declare function coerceTemporalValue(dbType: string | undefined, value: u
|
|
|
176
176
|
* as-is.
|
|
177
177
|
*/
|
|
178
178
|
export declare function parseDbDate(value: string): Date;
|
|
179
|
+
/**
|
|
180
|
+
* Is `value` one of the two representations a Postgres temporal `infinity` /
|
|
181
|
+
* `-infinity` reaches the ORM row parser in?
|
|
182
|
+
*
|
|
183
|
+
* TWO representations, because a temporal column is read two different ways
|
|
184
|
+
* and they disagree on the wire:
|
|
185
|
+
*
|
|
186
|
+
* driver the pg text parser for `timestamp` / `date` does not
|
|
187
|
+
* recognise the word, so it falls through to the driver's own
|
|
188
|
+
* parser, which returns the JS NUMBERS `Infinity` /
|
|
189
|
+
* `-Infinity`. This is what a top-level row, the batched and
|
|
190
|
+
* flatten strategies, a write `RETURNING` projection and a
|
|
191
|
+
* `groupBy` key all see.
|
|
192
|
+
* JSON wire `json_build_object` renders the same value as the STRING
|
|
193
|
+
* `"infinity"`, and scalar `timestamp` / `timestamptz` are
|
|
194
|
+
* deliberately absent from {@link JSON_WIRE_COERCION_OIDS},
|
|
195
|
+
* so no driver parser runs over it. This is what the `'join'`
|
|
196
|
+
* strategy and the positional encoding see.
|
|
197
|
+
*
|
|
198
|
+
* Both are normalized in one place ({@link QueryInterface}'s row parser), to
|
|
199
|
+
* whichever reading `temporalInfinity` selects (the JS number by default, or
|
|
200
|
+
* `null`), so the same stored value cannot read differently depending on which
|
|
201
|
+
* plan the query happened to take. The string form is only ever consulted for a column
|
|
202
|
+
* the schema says is temporal, so a `text` column holding the word "infinity"
|
|
203
|
+
* is untouched.
|
|
204
|
+
*
|
|
205
|
+
* Not dialect-gated. Postgres is the only engine with an infinite temporal
|
|
206
|
+
* value, but the row parser is engine-shared and the alternative reading on the
|
|
207
|
+
* other engines (a stray `'infinity'` string becoming an Invalid Date) is not
|
|
208
|
+
* one worth preserving.
|
|
209
|
+
*/
|
|
210
|
+
export declare function isTemporalInfinity(value: unknown): boolean;
|
|
179
211
|
/**
|
|
180
212
|
* Build the driver parser for Postgres `date` (OID 1082) that reads a
|
|
181
213
|
* zone-less calendar day as **UTC midnight**.
|
|
@@ -243,6 +275,52 @@ export declare function parseUtcTimestampText(text: string): Date;
|
|
|
243
275
|
* dependency. NULL elements stay `null` and are never handed to `element`.
|
|
244
276
|
*/
|
|
245
277
|
export declare function createPgArrayParser(element: (text: string) => unknown): (text: string) => unknown[];
|
|
278
|
+
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
279
|
+
export declare function markTurbineParser<F extends (text: string) => unknown>(parser: F): F;
|
|
280
|
+
/**
|
|
281
|
+
* Is the parser currently registered for `oid` still pg's own default?
|
|
282
|
+
*
|
|
283
|
+
* DETECTED BY BEHAVIOUR, NOT BY IDENTITY, and deliberately so. `pg-types` keeps
|
|
284
|
+
* its default parser table private: `getTypeParser` hands back whatever is
|
|
285
|
+
* registered NOW, and there is no exported way to ask what the default WAS, so
|
|
286
|
+
* a function-identity comparison would need a deep import of a file the package
|
|
287
|
+
* does not publish as an entry point. Instead this runs the registered parser
|
|
288
|
+
* over a canonical wire value and compares the result with what pg's default
|
|
289
|
+
* produces for it.
|
|
290
|
+
*
|
|
291
|
+
* What that buys and what it costs, stated honestly:
|
|
292
|
+
* - Every parser that behaves OBSERVABLY differently on the probe value is
|
|
293
|
+
* detected, which is the case worth warning about (someone else's reading
|
|
294
|
+
* is about to be replaced by Turbine's).
|
|
295
|
+
* - A replacement that is observably EQUIVALENT on the probe is reported as
|
|
296
|
+
* the default and draws no warning. That is a false negative, and an
|
|
297
|
+
* acceptable one: if it agrees with the default here it is not a reading
|
|
298
|
+
* anybody would notice Turbine overwriting.
|
|
299
|
+
* - A parser that THROWS on the probe is reported as non-default; pg's own
|
|
300
|
+
* never throws on a valid value of its type.
|
|
301
|
+
* - An OID with no probe entry is reported as default (never warn on a guess).
|
|
302
|
+
*
|
|
303
|
+
* The registered parser is invoked once, on a synthetic value, at client
|
|
304
|
+
* construction. A decode parser with side effects would be surprising, and pg's
|
|
305
|
+
* own have none.
|
|
306
|
+
*/
|
|
307
|
+
export declare function isDefaultTextParser(oid: number, parser: (text: string) => unknown): boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Warn ONCE per OID when Turbine is about to replace a text parser that is not
|
|
310
|
+
* pg's default, i.e. when some other module in the process has already
|
|
311
|
+
* customized it.
|
|
312
|
+
*
|
|
313
|
+
* `pg.types.setTypeParser` is process-global and retroactive: it changes how
|
|
314
|
+
* every `pg.Pool` in the process decodes that OID, including pools that were
|
|
315
|
+
* constructed and were already querying before the Turbine client existed. When
|
|
316
|
+
* the OID was still on pg's default that is the documented, intended trade (the
|
|
317
|
+
* whole point of `utcTimestamps`). When somebody else had already installed
|
|
318
|
+
* their own reading, Turbine is silently rewriting an expectation it cannot see
|
|
319
|
+
* the origin of, and the resulting bug is order-dependent: which reading wins
|
|
320
|
+
* depends on module evaluation order, which lazy route imports make unstable
|
|
321
|
+
* between requests. So say it out loud, once.
|
|
322
|
+
*/
|
|
323
|
+
export declare function warnParserOverwrite(oid: number, typeName: string): void;
|
|
246
324
|
/**
|
|
247
325
|
* Register the UTC readings of the four zone-less temporal OIDs on the pg
|
|
248
326
|
* module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
|
|
@@ -258,6 +336,11 @@ export declare function createPgArrayParser(element: (text: string) => unknown):
|
|
|
258
336
|
* Each fallback is read BEFORE its parser is installed, so an unrecognised wire
|
|
259
337
|
* value (`infinity`, and whatever a future server adds) still reaches the
|
|
260
338
|
* driver's own parser.
|
|
339
|
+
*
|
|
340
|
+
* Registration is RETROACTIVE for the whole process, pools included that were
|
|
341
|
+
* created and are already querying (there is one parser table, and it is read
|
|
342
|
+
* per row at decode time, not captured per pool). If any of the four OIDs is
|
|
343
|
+
* already on a NON-default parser, {@link warnParserOverwrite} says so once.
|
|
261
344
|
*/
|
|
262
345
|
export declare function registerUtcTemporalParsers(): void;
|
|
263
346
|
/**
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -22,10 +22,14 @@ exports.toLocalDateTimeLiteral = toLocalDateTimeLiteral;
|
|
|
22
22
|
exports.temporalBindKind = temporalBindKind;
|
|
23
23
|
exports.coerceTemporalValue = coerceTemporalValue;
|
|
24
24
|
exports.parseDbDate = parseDbDate;
|
|
25
|
+
exports.isTemporalInfinity = isTemporalInfinity;
|
|
25
26
|
exports.createUtcDateParser = createUtcDateParser;
|
|
26
27
|
exports.createUtcTimestampParser = createUtcTimestampParser;
|
|
27
28
|
exports.parseUtcTimestampText = parseUtcTimestampText;
|
|
28
29
|
exports.createPgArrayParser = createPgArrayParser;
|
|
30
|
+
exports.markTurbineParser = markTurbineParser;
|
|
31
|
+
exports.isDefaultTextParser = isDefaultTextParser;
|
|
32
|
+
exports.warnParserOverwrite = warnParserOverwrite;
|
|
29
33
|
exports.registerUtcTemporalParsers = registerUtcTemporalParsers;
|
|
30
34
|
exports.jsonWireCoercionOid = jsonWireCoercionOid;
|
|
31
35
|
exports.coerceJsonWireValue = coerceJsonWireValue;
|
|
@@ -33,6 +37,7 @@ exports.closestName = closestName;
|
|
|
33
37
|
exports.unknownFieldMessage = unknownFieldMessage;
|
|
34
38
|
const pg_1 = __importDefault(require("pg"));
|
|
35
39
|
const schema_js_1 = require("../schema.js");
|
|
40
|
+
const warn_registry_js_1 = require("./warn-registry.js");
|
|
36
41
|
// ---------------------------------------------------------------------------
|
|
37
42
|
// Identifier quoting, prevents SQL injection via table/column names
|
|
38
43
|
// ---------------------------------------------------------------------------
|
|
@@ -349,6 +354,42 @@ function parseDbDate(value) {
|
|
|
349
354
|
// normalize `YYYY-MM-DD HH:MM:SS` (driver form) to ISO before pinning UTC
|
|
350
355
|
return new Date(`${value.replace(' ', 'T')}Z`);
|
|
351
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Is `value` one of the two representations a Postgres temporal `infinity` /
|
|
359
|
+
* `-infinity` reaches the ORM row parser in?
|
|
360
|
+
*
|
|
361
|
+
* TWO representations, because a temporal column is read two different ways
|
|
362
|
+
* and they disagree on the wire:
|
|
363
|
+
*
|
|
364
|
+
* driver the pg text parser for `timestamp` / `date` does not
|
|
365
|
+
* recognise the word, so it falls through to the driver's own
|
|
366
|
+
* parser, which returns the JS NUMBERS `Infinity` /
|
|
367
|
+
* `-Infinity`. This is what a top-level row, the batched and
|
|
368
|
+
* flatten strategies, a write `RETURNING` projection and a
|
|
369
|
+
* `groupBy` key all see.
|
|
370
|
+
* JSON wire `json_build_object` renders the same value as the STRING
|
|
371
|
+
* `"infinity"`, and scalar `timestamp` / `timestamptz` are
|
|
372
|
+
* deliberately absent from {@link JSON_WIRE_COERCION_OIDS},
|
|
373
|
+
* so no driver parser runs over it. This is what the `'join'`
|
|
374
|
+
* strategy and the positional encoding see.
|
|
375
|
+
*
|
|
376
|
+
* Both are normalized in one place ({@link QueryInterface}'s row parser), to
|
|
377
|
+
* whichever reading `temporalInfinity` selects (the JS number by default, or
|
|
378
|
+
* `null`), so the same stored value cannot read differently depending on which
|
|
379
|
+
* plan the query happened to take. The string form is only ever consulted for a column
|
|
380
|
+
* the schema says is temporal, so a `text` column holding the word "infinity"
|
|
381
|
+
* is untouched.
|
|
382
|
+
*
|
|
383
|
+
* Not dialect-gated. Postgres is the only engine with an infinite temporal
|
|
384
|
+
* value, but the row parser is engine-shared and the alternative reading on the
|
|
385
|
+
* other engines (a stray `'infinity'` string becoming an Invalid Date) is not
|
|
386
|
+
* one worth preserving.
|
|
387
|
+
*/
|
|
388
|
+
function isTemporalInfinity(value) {
|
|
389
|
+
if (typeof value === 'number')
|
|
390
|
+
return value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY;
|
|
391
|
+
return value === 'infinity' || value === '-infinity';
|
|
392
|
+
}
|
|
352
393
|
// ---------------------------------------------------------------------------
|
|
353
394
|
// Driver type parsers for the zone-less temporal OIDs
|
|
354
395
|
// ---------------------------------------------------------------------------
|
|
@@ -462,6 +503,126 @@ function createPgArrayParser(element) {
|
|
|
462
503
|
const arrayParser = pg_1.default.types.arrayParser;
|
|
463
504
|
return (text) => arrayParser.create(text, (entry) => (entry === null || entry === undefined ? null : element(entry))).parse();
|
|
464
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* A canonical wire value per OID, plus the JS value pg's OWN default text
|
|
508
|
+
* parser produces from it, used to tell "still on the driver default" from
|
|
509
|
+
* "somebody else already customized this OID" (see
|
|
510
|
+
* {@link isDefaultTextParser}).
|
|
511
|
+
*
|
|
512
|
+
* The expected values are computed from LOCAL date components on purpose: pg's
|
|
513
|
+
* default for the zone-less temporal OIDs builds its `Date` in the process's
|
|
514
|
+
* zone (that is the reading `utcTimestamps` exists to replace), so the
|
|
515
|
+
* expectation has to be computed the same way in whatever zone the process runs.
|
|
516
|
+
*/
|
|
517
|
+
const DEFAULT_PARSER_PROBES = {
|
|
518
|
+
20: { text: '9007199254740993', expected: () => '9007199254740993' },
|
|
519
|
+
1082: { text: '2020-01-02', expected: () => new Date(2020, 0, 2) },
|
|
520
|
+
1114: { text: '2020-01-02 03:04:05', expected: () => new Date(2020, 0, 2, 3, 4, 5) },
|
|
521
|
+
1115: { text: '{"2020-01-02 03:04:05"}', expected: () => [new Date(2020, 0, 2, 3, 4, 5)] },
|
|
522
|
+
1182: { text: '{2020-01-02}', expected: () => [new Date(2020, 0, 2)] },
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Marks a text parser as one TURBINE installed, so a later registration in the
|
|
526
|
+
* same process (a client plus `turbine studio`, ESM plus CJS copies of this
|
|
527
|
+
* module) does not report Turbine's own parser as "somebody else's". A
|
|
528
|
+
* `Symbol.for` key, for the same cross-copy-identity reason the warn registry
|
|
529
|
+
* uses one.
|
|
530
|
+
*/
|
|
531
|
+
const TURBINE_PARSER = Symbol.for('turbine.typeParser');
|
|
532
|
+
/** The OIDs the `utcTimestamps` flag governs, and so the ones it can opt out of. */
|
|
533
|
+
const TEMPORAL_PARSER_OIDS = new Set([1114, 1082, 1115, 1182]);
|
|
534
|
+
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
535
|
+
function markTurbineParser(parser) {
|
|
536
|
+
parser[TURBINE_PARSER] = true;
|
|
537
|
+
return parser;
|
|
538
|
+
}
|
|
539
|
+
/** Comparable rendering of a parser result (Date by instant, array by element). */
|
|
540
|
+
function parserResultSignature(value) {
|
|
541
|
+
if (value === null || value === undefined)
|
|
542
|
+
return String(value);
|
|
543
|
+
if (value instanceof Date)
|
|
544
|
+
return `date:${value.getTime()}`;
|
|
545
|
+
if (Array.isArray(value))
|
|
546
|
+
return `[${value.map(parserResultSignature).join(',')}]`;
|
|
547
|
+
return `${typeof value}:${String(value)}`;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Is the parser currently registered for `oid` still pg's own default?
|
|
551
|
+
*
|
|
552
|
+
* DETECTED BY BEHAVIOUR, NOT BY IDENTITY, and deliberately so. `pg-types` keeps
|
|
553
|
+
* its default parser table private: `getTypeParser` hands back whatever is
|
|
554
|
+
* registered NOW, and there is no exported way to ask what the default WAS, so
|
|
555
|
+
* a function-identity comparison would need a deep import of a file the package
|
|
556
|
+
* does not publish as an entry point. Instead this runs the registered parser
|
|
557
|
+
* over a canonical wire value and compares the result with what pg's default
|
|
558
|
+
* produces for it.
|
|
559
|
+
*
|
|
560
|
+
* What that buys and what it costs, stated honestly:
|
|
561
|
+
* - Every parser that behaves OBSERVABLY differently on the probe value is
|
|
562
|
+
* detected, which is the case worth warning about (someone else's reading
|
|
563
|
+
* is about to be replaced by Turbine's).
|
|
564
|
+
* - A replacement that is observably EQUIVALENT on the probe is reported as
|
|
565
|
+
* the default and draws no warning. That is a false negative, and an
|
|
566
|
+
* acceptable one: if it agrees with the default here it is not a reading
|
|
567
|
+
* anybody would notice Turbine overwriting.
|
|
568
|
+
* - A parser that THROWS on the probe is reported as non-default; pg's own
|
|
569
|
+
* never throws on a valid value of its type.
|
|
570
|
+
* - An OID with no probe entry is reported as default (never warn on a guess).
|
|
571
|
+
*
|
|
572
|
+
* The registered parser is invoked once, on a synthetic value, at client
|
|
573
|
+
* construction. A decode parser with side effects would be surprising, and pg's
|
|
574
|
+
* own have none.
|
|
575
|
+
*/
|
|
576
|
+
function isDefaultTextParser(oid, parser) {
|
|
577
|
+
const probe = DEFAULT_PARSER_PROBES[oid];
|
|
578
|
+
if (!probe)
|
|
579
|
+
return true;
|
|
580
|
+
try {
|
|
581
|
+
return parserResultSignature(parser(probe.text)) === parserResultSignature(probe.expected());
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Warn ONCE per OID when Turbine is about to replace a text parser that is not
|
|
589
|
+
* pg's default, i.e. when some other module in the process has already
|
|
590
|
+
* customized it.
|
|
591
|
+
*
|
|
592
|
+
* `pg.types.setTypeParser` is process-global and retroactive: it changes how
|
|
593
|
+
* every `pg.Pool` in the process decodes that OID, including pools that were
|
|
594
|
+
* constructed and were already querying before the Turbine client existed. When
|
|
595
|
+
* the OID was still on pg's default that is the documented, intended trade (the
|
|
596
|
+
* whole point of `utcTimestamps`). When somebody else had already installed
|
|
597
|
+
* their own reading, Turbine is silently rewriting an expectation it cannot see
|
|
598
|
+
* the origin of, and the resulting bug is order-dependent: which reading wins
|
|
599
|
+
* depends on module evaluation order, which lazy route imports make unstable
|
|
600
|
+
* between requests. So say it out loud, once.
|
|
601
|
+
*/
|
|
602
|
+
function warnParserOverwrite(oid, typeName) {
|
|
603
|
+
if (process.env.NODE_ENV === 'production')
|
|
604
|
+
return;
|
|
605
|
+
const getParser = pg_1.default.types.getTypeParser;
|
|
606
|
+
const current = getParser(oid, 'text');
|
|
607
|
+
// Turbine's own earlier registration is not a third party's expectation.
|
|
608
|
+
if (current[TURBINE_PARSER])
|
|
609
|
+
return;
|
|
610
|
+
if (isDefaultTextParser(oid, current))
|
|
611
|
+
return;
|
|
612
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.parserOverwrite, String(oid)))
|
|
613
|
+
return;
|
|
614
|
+
// The `utcTimestamps: false` opt-out only governs the four TEMPORAL OIDs.
|
|
615
|
+
// Offering it as the remedy for int8 (20) would name a setting that does
|
|
616
|
+
// nothing for the OID being warned about; that registration has no opt-out.
|
|
617
|
+
const remedy = TEMPORAL_PARSER_OIDS.has(oid)
|
|
618
|
+
? ' `utcTimestamps: false` leaves the four temporal OIDs (1114, 1082, 1115, 1182) alone entirely.'
|
|
619
|
+
: ' There is no opt-out for this OID: Turbine registers it so bigint values come back as numbers.';
|
|
620
|
+
console.warn(`[turbine] pg type parser for OID ${oid} (${typeName}) was already customized by something else in this ` +
|
|
621
|
+
'process, and Turbine is replacing it. `pg.types.setTypeParser` is process-global and takes effect ' +
|
|
622
|
+
'immediately for EVERY pg.Pool in the process, including pools that already exist and are already ' +
|
|
623
|
+
'querying, so whatever set that parser will now read this column differently. If yours should win, ' +
|
|
624
|
+
`register it AFTER constructing the client.${remedy} Dev-only: silent under \`NODE_ENV=production\`.`);
|
|
625
|
+
}
|
|
465
626
|
/**
|
|
466
627
|
* Register the UTC readings of the four zone-less temporal OIDs on the pg
|
|
467
628
|
* module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
|
|
@@ -477,6 +638,11 @@ function createPgArrayParser(element) {
|
|
|
477
638
|
* Each fallback is read BEFORE its parser is installed, so an unrecognised wire
|
|
478
639
|
* value (`infinity`, and whatever a future server adds) still reaches the
|
|
479
640
|
* driver's own parser.
|
|
641
|
+
*
|
|
642
|
+
* Registration is RETROACTIVE for the whole process, pools included that were
|
|
643
|
+
* created and are already querying (there is one parser table, and it is read
|
|
644
|
+
* per row at decode time, not captured per pool). If any of the four OIDs is
|
|
645
|
+
* already on a NON-default parser, {@link warnParserOverwrite} says so once.
|
|
480
646
|
*/
|
|
481
647
|
function registerUtcTemporalParsers() {
|
|
482
648
|
// pg-types declares get/setTypeParser over its own OID enum, which lists the
|
|
@@ -484,15 +650,19 @@ function registerUtcTemporalParsers() {
|
|
|
484
650
|
// retyped over a plain number rather than the incomplete enum.
|
|
485
651
|
const getParser = pg_1.default.types.getTypeParser;
|
|
486
652
|
const setParser = pg_1.default.types.setTypeParser;
|
|
653
|
+
warnParserOverwrite(1114, 'timestamp');
|
|
654
|
+
warnParserOverwrite(1082, 'date');
|
|
655
|
+
warnParserOverwrite(1115, 'timestamp[]');
|
|
656
|
+
warnParserOverwrite(1182, 'date[]');
|
|
487
657
|
const parseDate = createUtcDateParser(getParser(1082, 'text'));
|
|
488
658
|
const parseTimestamp = createUtcTimestampParser(getParser(1114, 'text'));
|
|
489
|
-
setParser(1114, parseTimestamp);
|
|
490
|
-
setParser(1082, parseDate);
|
|
659
|
+
setParser(1114, markTurbineParser(parseTimestamp));
|
|
660
|
+
setParser(1082, markTurbineParser(parseDate));
|
|
491
661
|
// Array OIDs do not inherit their element parser, so `date[]` / `timestamp[]`
|
|
492
662
|
// would otherwise keep returning local-zone Dates while the scalar columns
|
|
493
663
|
// beside them returned UTC ones.
|
|
494
|
-
setParser(1182, createPgArrayParser(parseDate));
|
|
495
|
-
setParser(1115, createPgArrayParser(parseTimestamp));
|
|
664
|
+
setParser(1182, markTurbineParser(createPgArrayParser(parseDate)));
|
|
665
|
+
setParser(1115, markTurbineParser(createPgArrayParser(parseTimestamp)));
|
|
496
666
|
}
|
|
497
667
|
// ---------------------------------------------------------------------------
|
|
498
668
|
// JSON-wire value coercion (relationLoadStrategy: 'join')
|
|
@@ -82,6 +82,20 @@ 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
|
+
* A stored temporal `infinity` / `-infinity` was actually read, and
|
|
87
|
+
* `temporalInfinity` was left unset, so the row parser describes the reading
|
|
88
|
+
* in force (builder.ts `warnTemporalInfinity`). Keyed on `table.column`, so a
|
|
89
|
+
* table with two such columns says it once for each and a million rows say it
|
|
90
|
+
* once in total.
|
|
91
|
+
*/
|
|
92
|
+
readonly temporalInfinity: "temporalInfinity";
|
|
93
|
+
/**
|
|
94
|
+
* Turbine replaced a pg text parser that was NOT on the driver default, i.e.
|
|
95
|
+
* something else in the process had already customized that OID (utils.ts
|
|
96
|
+
* `warnParserOverwrite`). Keyed on the OID.
|
|
97
|
+
*/
|
|
98
|
+
readonly parserOverwrite: "parserOverwrite";
|
|
85
99
|
/**
|
|
86
100
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
87
101
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
|
@@ -123,6 +123,20 @@ exports.WARN_NS = {
|
|
|
123
123
|
* the offenders, not one per column. The namespace name is historical.
|
|
124
124
|
*/
|
|
125
125
|
untypedDateColumn: 'untypedDateColumn',
|
|
126
|
+
/**
|
|
127
|
+
* A stored temporal `infinity` / `-infinity` was actually read, and
|
|
128
|
+
* `temporalInfinity` was left unset, so the row parser describes the reading
|
|
129
|
+
* in force (builder.ts `warnTemporalInfinity`). Keyed on `table.column`, so a
|
|
130
|
+
* table with two such columns says it once for each and a million rows say it
|
|
131
|
+
* once in total.
|
|
132
|
+
*/
|
|
133
|
+
temporalInfinity: 'temporalInfinity',
|
|
134
|
+
/**
|
|
135
|
+
* Turbine replaced a pg text parser that was NOT on the driver default, i.e.
|
|
136
|
+
* something else in the process had already customized that OID (utils.ts
|
|
137
|
+
* `warnParserOverwrite`). Keyed on the OID.
|
|
138
|
+
*/
|
|
139
|
+
parserOverwrite: 'parserOverwrite',
|
|
126
140
|
/**
|
|
127
141
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
128
142
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
|
@@ -14,6 +14,7 @@ import type pg from 'pg';
|
|
|
14
14
|
import type { Dialect } from '../dialect.js';
|
|
15
15
|
import { ValidationError } from '../errors.js';
|
|
16
16
|
import type { RelationDef, SchemaMetadata, TableMetadata } from '../schema.js';
|
|
17
|
+
import type { TemporalInfinityReading } from './deferred.js';
|
|
17
18
|
import type { ArrayFilter, ColumnRef, GlobalFilters, JsonFilter, JsonPathOrderBy, SkipGlobalFilters, TextSearchFilter, VectorFilter, WhereClause, WhereOperator } from './types.js';
|
|
18
19
|
import { type SqlCacheEntry } from './utils.js';
|
|
19
20
|
import { type WhereHost, type WhereRecord } from './where-compile.js';
|
|
@@ -43,6 +44,13 @@ export interface BuilderCtx {
|
|
|
43
44
|
* `timestamp` columns (see `coerceWriteValue` in writes.ts).
|
|
44
45
|
*/
|
|
45
46
|
readonly utcTimestamps?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The client's `temporalInfinity` reading (`'preserve'` default, `'null'`).
|
|
49
|
+
* Read by aggregates.ts, where `_min` / `_max` are assembled from the raw row
|
|
50
|
+
* and so cannot go through `parseRow`. Optional so a hand-built ctx keeps the
|
|
51
|
+
* default.
|
|
52
|
+
*/
|
|
53
|
+
readonly temporalInfinity?: TemporalInfinityReading;
|
|
46
54
|
readonly crossSchemaTypeColumns: Set<string>;
|
|
47
55
|
/**
|
|
48
56
|
* The active query's `skipGlobalFilters` opt-out. A live getter/setter over
|
package/dist/client.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { type Dialect } from './dialect.js';
|
|
|
26
26
|
import { type ErrorMessageMode } from './errors.js';
|
|
27
27
|
import { type ObserveConfig, type ObserveHandle } from './observe.js';
|
|
28
28
|
import { type PipelineOptions, type PipelineResults } from './pipeline.js';
|
|
29
|
-
import { type DeferredQuery, type GlobalFilters, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy } from './query/index.js';
|
|
29
|
+
import { type DeferredQuery, type GlobalFilters, type QueryEventListener, QueryInterface, type QueryInterfaceOptions, type RelationLoadStrategy, type TemporalInfinityReading } from './query/index.js';
|
|
30
30
|
import { type NotificationHandler, type Subscription } from './realtime.js';
|
|
31
31
|
import type { SchemaMetadata } from './schema.js';
|
|
32
32
|
import { TypedSqlQuery } from './typed-sql.js';
|
|
@@ -192,11 +192,21 @@ export interface TurbineConfig {
|
|
|
192
192
|
* rewrite (a bound `Date` on a zone-less `date` / `timestamp` column is then
|
|
193
193
|
* serialized by the driver in the process's zone).
|
|
194
194
|
*
|
|
195
|
-
* PER PROCESS, NOT PER CLIENT. The read half is a pg type
|
|
196
|
-
* `pg.types.setTypeParser` installs one parser per OID for the
|
|
197
|
-
* process.
|
|
198
|
-
*
|
|
199
|
-
*
|
|
195
|
+
* PER PROCESS, NOT PER CLIENT, AND RETROACTIVE. The read half is a pg type
|
|
196
|
+
* parser, and `pg.types.setTypeParser` installs one parser per OID for the
|
|
197
|
+
* whole process. There is ONE parser table and it is consulted per row at
|
|
198
|
+
* decode time, so registration also changes POOLS THAT ALREADY EXIST AND ARE
|
|
199
|
+
* ALREADY QUERYING: the same `pg.Pool` running the same query returns
|
|
200
|
+
* different values before and after a Turbine client is constructed
|
|
201
|
+
* somewhere else in the process. With lazy route imports that ordering is
|
|
202
|
+
* not stable between requests, so a reporting job on its own pool can return
|
|
203
|
+
* different days depending on what has been imported yet. When the OID was
|
|
204
|
+
* still on the driver's default that is the intended trade; when something
|
|
205
|
+
* else had already customized it, Turbine says so once (dev-only, see
|
|
206
|
+
* `warnParserOverwrite`). The first client settles it for every later one, so
|
|
207
|
+
* constructing a second client with the OPPOSITE value throws a
|
|
208
|
+
* `ValidationError` rather than handing back a client whose writes and reads
|
|
209
|
+
* disagree. Give every
|
|
200
210
|
* client in the process the same value, or isolate the odd one in its own
|
|
201
211
|
* process. A client on an EXTERNAL pool never REGISTERS a parser (it inherits
|
|
202
212
|
* whatever configuration the caller's driver has, so a process containing
|
|
@@ -207,27 +217,80 @@ export interface TurbineConfig {
|
|
|
207
217
|
* UTC.
|
|
208
218
|
*/
|
|
209
219
|
utcTimestamps?: boolean;
|
|
220
|
+
/**
|
|
221
|
+
* How a Postgres temporal `infinity` / `-infinity` is handed back:
|
|
222
|
+
* `'preserve'` (the default, the JS numbers `Infinity` / `-Infinity`) or
|
|
223
|
+
* `'null'`. See {@link TemporalInfinityReading} for the full trade.
|
|
224
|
+
*
|
|
225
|
+
* There is no JS `Date` for either value, so both readings are wrong in some
|
|
226
|
+
* way and the option is which way. `'preserve'` is LOSSLESS: binding the
|
|
227
|
+
* number back stores `infinity` again, so a read-modify-write
|
|
228
|
+
* (`update({ data: { ...row } })`) round-trips. Its cost is a number on a
|
|
229
|
+
* `Date`-typed field, so `row.validUntil.toISOString()` throws a TypeError on
|
|
230
|
+
* exactly those rows, and `JSON.stringify` still renders the value `null`.
|
|
231
|
+
* `'null'` matches what `JSON.stringify` already produced and keeps the
|
|
232
|
+
* field's declared `Date | null` type honest, at the cost of DATA LOSS: a
|
|
233
|
+
* stored `infinity` and a stored NULL become indistinguishable, so that same
|
|
234
|
+
* read-modify-write writes SQL NULL and destroys the value with no error.
|
|
235
|
+
* The default is the reading that cannot lose a value.
|
|
236
|
+
*
|
|
237
|
+
* Unlike 0.54, which returned the number on some read strategies and an
|
|
238
|
+
* Invalid Date on others, BOTH readings here are identical on every
|
|
239
|
+
* strategy, every write projection, `groupBy` keys and `_min` / `_max`.
|
|
240
|
+
*
|
|
241
|
+
* Leaving the option unset selects `'preserve'` AND enables a one-time
|
|
242
|
+
* warning (per process, per field, not silenced by `NODE_ENV=production`) the
|
|
243
|
+
* first time a stored infinity is actually read, describing that reading and
|
|
244
|
+
* both escapes. Naming either reading explicitly silences it.
|
|
245
|
+
*/
|
|
246
|
+
temporalInfinity?: TemporalInfinityReading;
|
|
210
247
|
/**
|
|
211
248
|
* Pin `plan_cache_mode` on every connection this client opens, fixing how the
|
|
212
249
|
* PostgreSQL backend chooses between a custom plan (re-planned per parameter
|
|
213
250
|
* set) and a generic plan (planned once, blind to the values).
|
|
214
251
|
*
|
|
215
252
|
* Why it exists: Turbine sends NAMED prepared statements by default on a pool
|
|
216
|
-
* it owns, and PostgreSQL
|
|
217
|
-
* five executions
|
|
253
|
+
* it owns, and PostgreSQL MAY promote a named statement to a generic plan
|
|
254
|
+
* after five executions (see the ceiling note below: it promotes only when
|
|
255
|
+
* the generic plan's estimated cost is not worse than the average custom
|
|
256
|
+
* cost). A generic plan is planned for the AVERAGE parameter, so a
|
|
218
257
|
* predicate whose selectivity varies wildly per value (the canonical case is a
|
|
219
258
|
* `tenant_id` / `user_id` equality on a shared table, where one value matches
|
|
220
259
|
* a handful of rows and another matches most of them) is planned blind to the
|
|
221
260
|
* value it will actually get, and it never reverts. `'force_custom_plan'`
|
|
222
261
|
* removes that, at the cost of re-planning each execution.
|
|
223
262
|
*
|
|
224
|
-
* Scope
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* `LIMIT $n
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
263
|
+
* Scope. It applies to the statements Turbine itself promotes, which is any
|
|
264
|
+
* of them: `count()`, `findMany` and `findFirst` alike. CORRECTION TO THE
|
|
265
|
+
* 0.54.0 TEXT, which claimed `findMany` / `findFirst` were "much less
|
|
266
|
+
* exposed" because they bind `LIMIT $n`: that was false. PostgreSQL does not
|
|
267
|
+
* deny the planner a limit fraction for a bound limit, it SUBSTITUTES a
|
|
268
|
+
* default of 10% of the child node's own row estimate (clamped at one row),
|
|
269
|
+
* which is simply a different wrong number. An unknown `OFFSET` triggers the
|
|
270
|
+
* same substitution on its own even when the limit is a constant, and a
|
|
271
|
+
* paginated Turbine read binds both.
|
|
272
|
+
*
|
|
273
|
+
* Two things the sentence above this one glosses over. The sixth execution
|
|
274
|
+
* is a CEILING, not a trigger: `auto` promotes only when the generic plan's
|
|
275
|
+
* ESTIMATED cost is not worse than the average custom cost, so plenty of
|
|
276
|
+
* statements are never promoted at all (`pg_prepared_statements.generic_plans`
|
|
277
|
+
* is how you tell). And the shape that gets promoted unprompted is the one
|
|
278
|
+
* with NO limit: measured on a skewed join predicate, the unlimited statement
|
|
279
|
+
* promoted under the default `auto` and ran a nested loop at 430x the buffers
|
|
280
|
+
* of the custom plan, while the same predicate under `LIMIT $n` was never
|
|
281
|
+
* promoted across eight executions, because its substituted row count made
|
|
282
|
+
* the generic plan look MORE expensive. A limited `findMany` gives the
|
|
283
|
+
* planner two unknowns instead of one, which is not the same thing as more
|
|
284
|
+
* damage.
|
|
285
|
+
*
|
|
286
|
+
* `implicitPkOrdering` is OFF by default in core, so a default `findMany`
|
|
287
|
+
* emits no `ORDER BY` at all; switching it on adds an ordering a generic plan
|
|
288
|
+
* can walk the whole table in.
|
|
289
|
+
*
|
|
290
|
+
* Treat the option as a targeted remedy for a plan that is measurably worse
|
|
291
|
+
* after its fifth execution, not a general speed-up, and measure with
|
|
292
|
+
* `plan_cache_mode = force_generic_plan` versus `force_custom_plan` rather
|
|
293
|
+
* than reasoning about which query shapes "should" be safe.
|
|
231
294
|
*
|
|
232
295
|
* Default `undefined`: Turbine issues NOTHING and the backend keeps its own
|
|
233
296
|
* default (`auto`), byte-identical to not setting the option.
|
|
@@ -689,6 +752,13 @@ export declare class TurbineClient {
|
|
|
689
752
|
* `UnsupportedFeatureError` (E017), in the same style as the other
|
|
690
753
|
* capability refusals.
|
|
691
754
|
*/
|
|
755
|
+
/**
|
|
756
|
+
* Validate the `temporalInfinity` reading. A closed two-value enum, checked
|
|
757
|
+
* at construction so a typo (`'preserved'`, `'raw'`) fails loudly rather than
|
|
758
|
+
* silently falling back to the default reading the caller was trying to
|
|
759
|
+
* change.
|
|
760
|
+
*/
|
|
761
|
+
private static resolveTemporalInfinity;
|
|
692
762
|
private static resolvePlanCacheMode;
|
|
693
763
|
/**
|
|
694
764
|
* Pin `plan_cache_mode` on every connection an OWNED pool opens, by putting
|