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
package/dist/query/utils.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import pg from 'pg';
|
|
7
7
|
import { camelToSnake, localDateTimeKind, timeOfDayKind } from '../schema.js';
|
|
8
|
+
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Identifier quoting, prevents SQL injection via table/column names
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -320,6 +321,42 @@ export function parseDbDate(value) {
|
|
|
320
321
|
// normalize `YYYY-MM-DD HH:MM:SS` (driver form) to ISO before pinning UTC
|
|
321
322
|
return new Date(`${value.replace(' ', 'T')}Z`);
|
|
322
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Is `value` one of the two representations a Postgres temporal `infinity` /
|
|
326
|
+
* `-infinity` reaches the ORM row parser in?
|
|
327
|
+
*
|
|
328
|
+
* TWO representations, because a temporal column is read two different ways
|
|
329
|
+
* and they disagree on the wire:
|
|
330
|
+
*
|
|
331
|
+
* driver the pg text parser for `timestamp` / `date` does not
|
|
332
|
+
* recognise the word, so it falls through to the driver's own
|
|
333
|
+
* parser, which returns the JS NUMBERS `Infinity` /
|
|
334
|
+
* `-Infinity`. This is what a top-level row, the batched and
|
|
335
|
+
* flatten strategies, a write `RETURNING` projection and a
|
|
336
|
+
* `groupBy` key all see.
|
|
337
|
+
* JSON wire `json_build_object` renders the same value as the STRING
|
|
338
|
+
* `"infinity"`, and scalar `timestamp` / `timestamptz` are
|
|
339
|
+
* deliberately absent from {@link JSON_WIRE_COERCION_OIDS},
|
|
340
|
+
* so no driver parser runs over it. This is what the `'join'`
|
|
341
|
+
* strategy and the positional encoding see.
|
|
342
|
+
*
|
|
343
|
+
* Both are normalized in one place ({@link QueryInterface}'s row parser), to
|
|
344
|
+
* whichever reading `temporalInfinity` selects (the JS number by default, or
|
|
345
|
+
* `null`), so the same stored value cannot read differently depending on which
|
|
346
|
+
* plan the query happened to take. The string form is only ever consulted for a column
|
|
347
|
+
* the schema says is temporal, so a `text` column holding the word "infinity"
|
|
348
|
+
* is untouched.
|
|
349
|
+
*
|
|
350
|
+
* Not dialect-gated. Postgres is the only engine with an infinite temporal
|
|
351
|
+
* value, but the row parser is engine-shared and the alternative reading on the
|
|
352
|
+
* other engines (a stray `'infinity'` string becoming an Invalid Date) is not
|
|
353
|
+
* one worth preserving.
|
|
354
|
+
*/
|
|
355
|
+
export function isTemporalInfinity(value) {
|
|
356
|
+
if (typeof value === 'number')
|
|
357
|
+
return value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY;
|
|
358
|
+
return value === 'infinity' || value === '-infinity';
|
|
359
|
+
}
|
|
323
360
|
// ---------------------------------------------------------------------------
|
|
324
361
|
// Driver type parsers for the zone-less temporal OIDs
|
|
325
362
|
// ---------------------------------------------------------------------------
|
|
@@ -433,6 +470,126 @@ export function createPgArrayParser(element) {
|
|
|
433
470
|
const arrayParser = pg.types.arrayParser;
|
|
434
471
|
return (text) => arrayParser.create(text, (entry) => (entry === null || entry === undefined ? null : element(entry))).parse();
|
|
435
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* A canonical wire value per OID, plus the JS value pg's OWN default text
|
|
475
|
+
* parser produces from it, used to tell "still on the driver default" from
|
|
476
|
+
* "somebody else already customized this OID" (see
|
|
477
|
+
* {@link isDefaultTextParser}).
|
|
478
|
+
*
|
|
479
|
+
* The expected values are computed from LOCAL date components on purpose: pg's
|
|
480
|
+
* default for the zone-less temporal OIDs builds its `Date` in the process's
|
|
481
|
+
* zone (that is the reading `utcTimestamps` exists to replace), so the
|
|
482
|
+
* expectation has to be computed the same way in whatever zone the process runs.
|
|
483
|
+
*/
|
|
484
|
+
const DEFAULT_PARSER_PROBES = {
|
|
485
|
+
20: { text: '9007199254740993', expected: () => '9007199254740993' },
|
|
486
|
+
1082: { text: '2020-01-02', expected: () => new Date(2020, 0, 2) },
|
|
487
|
+
1114: { text: '2020-01-02 03:04:05', expected: () => new Date(2020, 0, 2, 3, 4, 5) },
|
|
488
|
+
1115: { text: '{"2020-01-02 03:04:05"}', expected: () => [new Date(2020, 0, 2, 3, 4, 5)] },
|
|
489
|
+
1182: { text: '{2020-01-02}', expected: () => [new Date(2020, 0, 2)] },
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* Marks a text parser as one TURBINE installed, so a later registration in the
|
|
493
|
+
* same process (a client plus `turbine studio`, ESM plus CJS copies of this
|
|
494
|
+
* module) does not report Turbine's own parser as "somebody else's". A
|
|
495
|
+
* `Symbol.for` key, for the same cross-copy-identity reason the warn registry
|
|
496
|
+
* uses one.
|
|
497
|
+
*/
|
|
498
|
+
const TURBINE_PARSER = Symbol.for('turbine.typeParser');
|
|
499
|
+
/** The OIDs the `utcTimestamps` flag governs, and so the ones it can opt out of. */
|
|
500
|
+
const TEMPORAL_PARSER_OIDS = new Set([1114, 1082, 1115, 1182]);
|
|
501
|
+
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
502
|
+
export function markTurbineParser(parser) {
|
|
503
|
+
parser[TURBINE_PARSER] = true;
|
|
504
|
+
return parser;
|
|
505
|
+
}
|
|
506
|
+
/** Comparable rendering of a parser result (Date by instant, array by element). */
|
|
507
|
+
function parserResultSignature(value) {
|
|
508
|
+
if (value === null || value === undefined)
|
|
509
|
+
return String(value);
|
|
510
|
+
if (value instanceof Date)
|
|
511
|
+
return `date:${value.getTime()}`;
|
|
512
|
+
if (Array.isArray(value))
|
|
513
|
+
return `[${value.map(parserResultSignature).join(',')}]`;
|
|
514
|
+
return `${typeof value}:${String(value)}`;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Is the parser currently registered for `oid` still pg's own default?
|
|
518
|
+
*
|
|
519
|
+
* DETECTED BY BEHAVIOUR, NOT BY IDENTITY, and deliberately so. `pg-types` keeps
|
|
520
|
+
* its default parser table private: `getTypeParser` hands back whatever is
|
|
521
|
+
* registered NOW, and there is no exported way to ask what the default WAS, so
|
|
522
|
+
* a function-identity comparison would need a deep import of a file the package
|
|
523
|
+
* does not publish as an entry point. Instead this runs the registered parser
|
|
524
|
+
* over a canonical wire value and compares the result with what pg's default
|
|
525
|
+
* produces for it.
|
|
526
|
+
*
|
|
527
|
+
* What that buys and what it costs, stated honestly:
|
|
528
|
+
* - Every parser that behaves OBSERVABLY differently on the probe value is
|
|
529
|
+
* detected, which is the case worth warning about (someone else's reading
|
|
530
|
+
* is about to be replaced by Turbine's).
|
|
531
|
+
* - A replacement that is observably EQUIVALENT on the probe is reported as
|
|
532
|
+
* the default and draws no warning. That is a false negative, and an
|
|
533
|
+
* acceptable one: if it agrees with the default here it is not a reading
|
|
534
|
+
* anybody would notice Turbine overwriting.
|
|
535
|
+
* - A parser that THROWS on the probe is reported as non-default; pg's own
|
|
536
|
+
* never throws on a valid value of its type.
|
|
537
|
+
* - An OID with no probe entry is reported as default (never warn on a guess).
|
|
538
|
+
*
|
|
539
|
+
* The registered parser is invoked once, on a synthetic value, at client
|
|
540
|
+
* construction. A decode parser with side effects would be surprising, and pg's
|
|
541
|
+
* own have none.
|
|
542
|
+
*/
|
|
543
|
+
export function isDefaultTextParser(oid, parser) {
|
|
544
|
+
const probe = DEFAULT_PARSER_PROBES[oid];
|
|
545
|
+
if (!probe)
|
|
546
|
+
return true;
|
|
547
|
+
try {
|
|
548
|
+
return parserResultSignature(parser(probe.text)) === parserResultSignature(probe.expected());
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Warn ONCE per OID when Turbine is about to replace a text parser that is not
|
|
556
|
+
* pg's default, i.e. when some other module in the process has already
|
|
557
|
+
* customized it.
|
|
558
|
+
*
|
|
559
|
+
* `pg.types.setTypeParser` is process-global and retroactive: it changes how
|
|
560
|
+
* every `pg.Pool` in the process decodes that OID, including pools that were
|
|
561
|
+
* constructed and were already querying before the Turbine client existed. When
|
|
562
|
+
* the OID was still on pg's default that is the documented, intended trade (the
|
|
563
|
+
* whole point of `utcTimestamps`). When somebody else had already installed
|
|
564
|
+
* their own reading, Turbine is silently rewriting an expectation it cannot see
|
|
565
|
+
* the origin of, and the resulting bug is order-dependent: which reading wins
|
|
566
|
+
* depends on module evaluation order, which lazy route imports make unstable
|
|
567
|
+
* between requests. So say it out loud, once.
|
|
568
|
+
*/
|
|
569
|
+
export function warnParserOverwrite(oid, typeName) {
|
|
570
|
+
if (process.env.NODE_ENV === 'production')
|
|
571
|
+
return;
|
|
572
|
+
const getParser = pg.types.getTypeParser;
|
|
573
|
+
const current = getParser(oid, 'text');
|
|
574
|
+
// Turbine's own earlier registration is not a third party's expectation.
|
|
575
|
+
if (current[TURBINE_PARSER])
|
|
576
|
+
return;
|
|
577
|
+
if (isDefaultTextParser(oid, current))
|
|
578
|
+
return;
|
|
579
|
+
if (!shouldWarnOnce(WARN_NS.parserOverwrite, String(oid)))
|
|
580
|
+
return;
|
|
581
|
+
// The `utcTimestamps: false` opt-out only governs the four TEMPORAL OIDs.
|
|
582
|
+
// Offering it as the remedy for int8 (20) would name a setting that does
|
|
583
|
+
// nothing for the OID being warned about; that registration has no opt-out.
|
|
584
|
+
const remedy = TEMPORAL_PARSER_OIDS.has(oid)
|
|
585
|
+
? ' `utcTimestamps: false` leaves the four temporal OIDs (1114, 1082, 1115, 1182) alone entirely.'
|
|
586
|
+
: ' There is no opt-out for this OID: Turbine registers it so bigint values come back as numbers.';
|
|
587
|
+
console.warn(`[turbine] pg type parser for OID ${oid} (${typeName}) was already customized by something else in this ` +
|
|
588
|
+
'process, and Turbine is replacing it. `pg.types.setTypeParser` is process-global and takes effect ' +
|
|
589
|
+
'immediately for EVERY pg.Pool in the process, including pools that already exist and are already ' +
|
|
590
|
+
'querying, so whatever set that parser will now read this column differently. If yours should win, ' +
|
|
591
|
+
`register it AFTER constructing the client.${remedy} Dev-only: silent under \`NODE_ENV=production\`.`);
|
|
592
|
+
}
|
|
436
593
|
/**
|
|
437
594
|
* Register the UTC readings of the four zone-less temporal OIDs on the pg
|
|
438
595
|
* module: `timestamp` (1114), `date` (1082) and their array forms (1115, 1182).
|
|
@@ -448,6 +605,11 @@ export function createPgArrayParser(element) {
|
|
|
448
605
|
* Each fallback is read BEFORE its parser is installed, so an unrecognised wire
|
|
449
606
|
* value (`infinity`, and whatever a future server adds) still reaches the
|
|
450
607
|
* driver's own parser.
|
|
608
|
+
*
|
|
609
|
+
* Registration is RETROACTIVE for the whole process, pools included that were
|
|
610
|
+
* created and are already querying (there is one parser table, and it is read
|
|
611
|
+
* per row at decode time, not captured per pool). If any of the four OIDs is
|
|
612
|
+
* already on a NON-default parser, {@link warnParserOverwrite} says so once.
|
|
451
613
|
*/
|
|
452
614
|
export function registerUtcTemporalParsers() {
|
|
453
615
|
// pg-types declares get/setTypeParser over its own OID enum, which lists the
|
|
@@ -455,15 +617,19 @@ export function registerUtcTemporalParsers() {
|
|
|
455
617
|
// retyped over a plain number rather than the incomplete enum.
|
|
456
618
|
const getParser = pg.types.getTypeParser;
|
|
457
619
|
const setParser = pg.types.setTypeParser;
|
|
620
|
+
warnParserOverwrite(1114, 'timestamp');
|
|
621
|
+
warnParserOverwrite(1082, 'date');
|
|
622
|
+
warnParserOverwrite(1115, 'timestamp[]');
|
|
623
|
+
warnParserOverwrite(1182, 'date[]');
|
|
458
624
|
const parseDate = createUtcDateParser(getParser(1082, 'text'));
|
|
459
625
|
const parseTimestamp = createUtcTimestampParser(getParser(1114, 'text'));
|
|
460
|
-
setParser(1114, parseTimestamp);
|
|
461
|
-
setParser(1082, parseDate);
|
|
626
|
+
setParser(1114, markTurbineParser(parseTimestamp));
|
|
627
|
+
setParser(1082, markTurbineParser(parseDate));
|
|
462
628
|
// Array OIDs do not inherit their element parser, so `date[]` / `timestamp[]`
|
|
463
629
|
// would otherwise keep returning local-zone Dates while the scalar columns
|
|
464
630
|
// beside them returned UTC ones.
|
|
465
|
-
setParser(1182, createPgArrayParser(parseDate));
|
|
466
|
-
setParser(1115, createPgArrayParser(parseTimestamp));
|
|
631
|
+
setParser(1182, markTurbineParser(createPgArrayParser(parseDate)));
|
|
632
|
+
setParser(1115, markTurbineParser(createPgArrayParser(parseTimestamp)));
|
|
467
633
|
}
|
|
468
634
|
// ---------------------------------------------------------------------------
|
|
469
635
|
// 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).
|
|
@@ -117,6 +117,20 @@ export const WARN_NS = {
|
|
|
117
117
|
* the offenders, not one per column. The namespace name is historical.
|
|
118
118
|
*/
|
|
119
119
|
untypedDateColumn: 'untypedDateColumn',
|
|
120
|
+
/**
|
|
121
|
+
* A stored temporal `infinity` / `-infinity` was actually read, and
|
|
122
|
+
* `temporalInfinity` was left unset, so the row parser describes the reading
|
|
123
|
+
* in force (builder.ts `warnTemporalInfinity`). Keyed on `table.column`, so a
|
|
124
|
+
* table with two such columns says it once for each and a million rows say it
|
|
125
|
+
* once in total.
|
|
126
|
+
*/
|
|
127
|
+
temporalInfinity: 'temporalInfinity',
|
|
128
|
+
/**
|
|
129
|
+
* Turbine replaced a pg text parser that was NOT on the driver default, i.e.
|
|
130
|
+
* something else in the process had already customized that OID (utils.ts
|
|
131
|
+
* `warnParserOverwrite`). Keyed on the OID.
|
|
132
|
+
*/
|
|
133
|
+
parserOverwrite: 'parserOverwrite',
|
|
120
134
|
/**
|
|
121
135
|
* `planCacheMode` was set on a client given an EXTERNAL pool, where Turbine
|
|
122
136
|
* runs no connection setup, so the option is a no-op (client.ts constructor).
|
package/dist/query/where.d.ts
CHANGED
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.55.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|