turbine-orm 0.71.0 → 0.72.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 +2 -2
- package/dist/cjs/client.d.ts +0 -18
- package/dist/cjs/client.js +2 -29
- package/dist/cjs/connection-url.d.ts +30 -0
- package/dist/cjs/connection-url.js +15 -17
- package/dist/cjs/powql.d.ts +38 -1
- package/dist/cjs/powql.js +106 -18
- package/dist/cjs/query/aggregates.d.ts +0 -13
- package/dist/cjs/query/aggregates.js +81 -33
- package/dist/cjs/query/batched-loader.d.ts +13 -1
- package/dist/cjs/query/batched-loader.js +46 -11
- package/dist/cjs/query/builder.d.ts +13 -0
- package/dist/cjs/query/builder.js +104 -14
- package/dist/cjs/query/compound-unique.js +29 -5
- package/dist/cjs/query/relation-names.d.ts +52 -0
- package/dist/cjs/query/relation-names.js +120 -0
- package/dist/cjs/query/relations.d.ts +11 -6
- package/dist/cjs/query/relations.js +45 -27
- package/dist/cjs/query/utils.d.ts +107 -3
- package/dist/cjs/query/utils.js +408 -7
- package/dist/cjs/query/where-compile.js +9 -4
- package/dist/cjs/query/where.js +9 -5
- package/dist/client.d.ts +0 -18
- package/dist/client.js +2 -29
- package/dist/connection-url.d.ts +30 -0
- package/dist/connection-url.js +15 -18
- package/dist/powql.d.ts +38 -1
- package/dist/powql.js +107 -19
- package/dist/query/aggregates.d.ts +0 -13
- package/dist/query/aggregates.js +82 -34
- package/dist/query/batched-loader.d.ts +13 -1
- package/dist/query/batched-loader.js +47 -12
- package/dist/query/builder.d.ts +13 -0
- package/dist/query/builder.js +105 -15
- package/dist/query/compound-unique.js +30 -6
- package/dist/query/relation-names.d.ts +52 -0
- package/dist/query/relation-names.js +117 -0
- package/dist/query/relations.d.ts +11 -6
- package/dist/query/relations.js +47 -29
- package/dist/query/utils.d.ts +107 -3
- package/dist/query/utils.js +404 -8
- package/dist/query/where-compile.js +10 -5
- package/dist/query/where.js +10 -6
- package/package.json +5 -3
package/dist/cjs/query/utils.js
CHANGED
|
@@ -12,6 +12,8 @@ exports.JSON_WIRE_COERCION_OIDS = exports.OPERATOR_KEYS = exports.LRUCache = exp
|
|
|
12
12
|
exports.quoteIdent = quoteIdent;
|
|
13
13
|
exports.ownLookup = ownLookup;
|
|
14
14
|
exports.resolveColumnName = resolveColumnName;
|
|
15
|
+
exports.resolveRelation = resolveRelation;
|
|
16
|
+
exports.resolveRelationDef = resolveRelationDef;
|
|
15
17
|
exports.canonicalColumnOrder = canonicalColumnOrder;
|
|
16
18
|
exports.canonicalWriteEntries = canonicalWriteEntries;
|
|
17
19
|
exports.markInternalCombinator = markInternalCombinator;
|
|
@@ -28,7 +30,10 @@ exports.coerceTemporalValue = coerceTemporalValue;
|
|
|
28
30
|
exports.parseDbDate = parseDbDate;
|
|
29
31
|
exports.isTemporalInfinity = isTemporalInfinity;
|
|
30
32
|
exports.createUtcDateParser = createUtcDateParser;
|
|
33
|
+
exports.createUtcDateParserGeneral = createUtcDateParserGeneral;
|
|
31
34
|
exports.createUtcTimestampParser = createUtcTimestampParser;
|
|
35
|
+
exports.createUtcTimestampParserGeneral = createUtcTimestampParserGeneral;
|
|
36
|
+
exports.createFastTimestamptzParser = createFastTimestamptzParser;
|
|
32
37
|
exports.parseUtcTimestampText = parseUtcTimestampText;
|
|
33
38
|
exports.createPgArrayParser = createPgArrayParser;
|
|
34
39
|
exports.markTurbineParser = markTurbineParser;
|
|
@@ -108,6 +113,55 @@ function resolveColumnName(meta, key) {
|
|
|
108
113
|
return snake;
|
|
109
114
|
return undefined;
|
|
110
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a user-supplied key to a relation's CANONICAL name and definition, or
|
|
118
|
+
* `undefined` when the key names no relation on the table.
|
|
119
|
+
*
|
|
120
|
+
* The relation-name half of the rule {@link resolveColumnName} states for
|
|
121
|
+
* columns, and deliberately the same shape: the declared name first, else
|
|
122
|
+
* `snakeToCamel(key)` accepted ONLY when that names a real relation.
|
|
123
|
+
* `snakeToCamel` is idempotent on an already-camel string, so a canonical key
|
|
124
|
+
* takes the first branch and this is a no-op for every existing caller.
|
|
125
|
+
*
|
|
126
|
+
* WHY IT EXISTS. A relation has one declared name, `ripeningChecks`, while the
|
|
127
|
+
* DDL anyone reads has only the TABLE name, `ripening_checks`. Writing back
|
|
128
|
+
* what the schema shows therefore failed with E005 in `with`, E003 in a
|
|
129
|
+
* relation filter, and E005 in `orderBy`, on names the error text was already
|
|
130
|
+
* computing correctly ("Did you mean ...?"). A system that can name the
|
|
131
|
+
* intended relation can accept it.
|
|
132
|
+
*
|
|
133
|
+
* NOT A GUESS, for the same reason the column rule is not: the transformed name
|
|
134
|
+
* is accepted only when it is a real declared relation, so an unknown key still
|
|
135
|
+
* fails and a typo is still a typo. Exact match wins first, so a schema that
|
|
136
|
+
* literally declares `ripening_checks` keeps it, even alongside a
|
|
137
|
+
* `ripeningChecks`.
|
|
138
|
+
*
|
|
139
|
+
* The RESULT KEY is the canonical name, not the caller's spelling, matching the
|
|
140
|
+
* column side (`select: { ledger_handle: true }` already returns
|
|
141
|
+
* `{ ledgerHandle }`). Resolving here rather than normalizing the args up front
|
|
142
|
+
* also means both spellings share one SQL-cache entry instead of minting two
|
|
143
|
+
* templates for one query.
|
|
144
|
+
*
|
|
145
|
+
* Prototype-safe via {@link ownLookup}, so `__proto__` cannot name a relation.
|
|
146
|
+
*/
|
|
147
|
+
function resolveRelation(relations, key) {
|
|
148
|
+
const direct = ownLookup(relations, key);
|
|
149
|
+
if (direct !== undefined)
|
|
150
|
+
return { name: key, def: direct };
|
|
151
|
+
const camel = (0, schema_js_1.snakeToCamel)(key);
|
|
152
|
+
if (camel === key)
|
|
153
|
+
return undefined;
|
|
154
|
+
const mapped = ownLookup(relations, camel);
|
|
155
|
+
return mapped === undefined ? undefined : { name: camel, def: mapped };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* {@link resolveRelation} when only the definition is wanted: a drop-in for the
|
|
159
|
+
* `ownLookup(meta.relations, key)` it replaces, with the same signature and the
|
|
160
|
+
* same `undefined` on a miss.
|
|
161
|
+
*/
|
|
162
|
+
function resolveRelationDef(relations, key) {
|
|
163
|
+
return resolveRelation(relations, key)?.def;
|
|
164
|
+
}
|
|
111
165
|
// ---------------------------------------------------------------------------
|
|
112
166
|
// Caller-controlled key ORDER, canonicalized
|
|
113
167
|
// ---------------------------------------------------------------------------
|
|
@@ -572,6 +626,211 @@ function isTemporalInfinity(value) {
|
|
|
572
626
|
// ---------------------------------------------------------------------------
|
|
573
627
|
// Driver type parsers for the zone-less temporal OIDs
|
|
574
628
|
// ---------------------------------------------------------------------------
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// The fast temporal scan
|
|
631
|
+
//
|
|
632
|
+
// Every temporal parser below is a two-stage function: a hand-written
|
|
633
|
+
// character scan that claims the ONE wire shape a busy application actually
|
|
634
|
+
// produces, and behind it the general parser, which is where every other shape
|
|
635
|
+
// (and every shape a future server adds) still goes.
|
|
636
|
+
//
|
|
637
|
+
// WHY IT EXISTS, stated as a measurement rather than an intuition. Draining
|
|
638
|
+
// 50,000 rows of the benchmark `comments` fixture spends ~23.5 ms in
|
|
639
|
+
// client-side type decoding, and `timestamptz` alone is 20.7 ms of it (88%).
|
|
640
|
+
// The remaining types are not worth touching: `text`, `numeric`, `uuid` and
|
|
641
|
+
// `bool` cost nothing at all, because pg registers no parser for them. Two
|
|
642
|
+
// hard bounds were verified in node-postgres' source before any of this was
|
|
643
|
+
// written, and they say what a decoder rewrite can and cannot reach: every
|
|
644
|
+
// cell is materialised as a JS string by `reader.string(len)` before any
|
|
645
|
+
// parser is consulted, so the `utf8Slice` half is unreachable; and the binary
|
|
646
|
+
// protocol is not an escape hatch, the parser constructor throws
|
|
647
|
+
// `Binary mode not supported yet`. The type-PARSING half is the whole budget,
|
|
648
|
+
// and this is a claim on it.
|
|
649
|
+
//
|
|
650
|
+
// THE RULE, and it is the only thing that makes a fast path acceptable here:
|
|
651
|
+
// **the scan must return exactly what the parser it replaces returned, or
|
|
652
|
+
// return `null` and not claim the value at all.** It never guesses, never
|
|
653
|
+
// "handles" a shape approximately, and never widens what it accepts to cover
|
|
654
|
+
// one more case. Every refusal costs a few charCode compares and is repaid by
|
|
655
|
+
// the general parser being correct.
|
|
656
|
+
//
|
|
657
|
+
// Two traps are baked into the refusals rather than into corrections, because
|
|
658
|
+
// a throwaway version of this decoder wrote during the ceiling measurement got
|
|
659
|
+
// 3 of 15 edge values wrong while believing it had delegated the hard ones:
|
|
660
|
+
//
|
|
661
|
+
// 1. `Date.UTC` maps years 0-99 onto 1900-1999, so `0044-03-15` decodes as
|
|
662
|
+
// 1944 and `0001-01-01` as 1901. The obvious repair, build the Date then
|
|
663
|
+
// `setUTCFullYear`, is ALSO wrong: year 0 is a leap year in the proleptic
|
|
664
|
+
// Gregorian calendar and 1900 is not, so `0000-02-29` built that way
|
|
665
|
+
// lands on March 1st. The scan refuses `year < 100` and lets the general
|
|
666
|
+
// parser's `new Date(0)` + `setUTCFullYear(y, m, d)` assembly (which sets
|
|
667
|
+
// all three fields against the right year's calendar) answer it.
|
|
668
|
+
// 2. An offset parse that runs to end-of-string silently EATS a trailing
|
|
669
|
+
// ` BC`: `4713-01-01 00:00:00+00 BC` decodes as AD 4713, off by 9,424
|
|
670
|
+
// years with no error. The scan requires end-of-string after the offset.
|
|
671
|
+
//
|
|
672
|
+
// Differential coverage: src/test/fast-temporal-decode.test.ts compares the
|
|
673
|
+
// scan against the parser it replaces value for value, and
|
|
674
|
+
// src/test/fast-temporal-decode.integration.test.ts does the same against a
|
|
675
|
+
// live server with `DateStyle` and `TimeZone` varied, because the wire shape
|
|
676
|
+
// is a server setting and a decoder tuned to one `DateStyle` is a latent
|
|
677
|
+
// corruption bug.
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
// Wire shape the scan is reading. Plain module constants rather than an enum:
|
|
680
|
+
// this repo's lint config rejects `const enum` (it does not survive
|
|
681
|
+
// `isolatedModules`), and a plain `enum` emits a runtime object, so every
|
|
682
|
+
// `Shape.Date` in the scan below would become a property load in the hottest
|
|
683
|
+
// loop in the library. These are values, never serialized, never persisted.
|
|
684
|
+
/** `YYYY-MM-DD` (OID 1082). */
|
|
685
|
+
const SHAPE_DATE = 0;
|
|
686
|
+
/** `YYYY-MM-DD[ T]HH:MM:SS[.f…]` (OID 1114), never an offset. */
|
|
687
|
+
const SHAPE_TIMESTAMP = 1;
|
|
688
|
+
/** `YYYY-MM-DD HH:MM:SS[.f…](Z|±HH[:MM[:SS]])` (OID 1184), offset REQUIRED. */
|
|
689
|
+
const SHAPE_TIMESTAMPTZ = 2;
|
|
690
|
+
const CH_HYPHEN = 45;
|
|
691
|
+
const CH_COLON = 58;
|
|
692
|
+
const CH_DOT = 46;
|
|
693
|
+
const CH_SPACE = 32;
|
|
694
|
+
const CH_T = 84;
|
|
695
|
+
const CH_Z = 90;
|
|
696
|
+
const CH_PLUS = 43;
|
|
697
|
+
/** Same code point as {@link CH_HYPHEN}; named separately because the two read
|
|
698
|
+
* as different things (a date separator, an offset sign) at their use sites. */
|
|
699
|
+
const CH_MINUS = 45;
|
|
700
|
+
/**
|
|
701
|
+
* Two ASCII digits at `i` as a number, or `-1` if either character is not a
|
|
702
|
+
* digit. `charCodeAt` past the end returns `NaN`, and `NaN - 48` is `NaN`,
|
|
703
|
+
* which fails both range tests, so this needs no separate length check.
|
|
704
|
+
*/
|
|
705
|
+
function twoDigitsAt(text, i) {
|
|
706
|
+
const hi = text.charCodeAt(i) - 48;
|
|
707
|
+
const lo = text.charCodeAt(i + 1) - 48;
|
|
708
|
+
return hi >= 0 && hi <= 9 && lo >= 0 && lo <= 9 ? hi * 10 + lo : -1;
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Decode `text` if it is exactly the canonical ISO wire shape for `shape`, or
|
|
712
|
+
* return `null` to say "not mine" (see the rule in the block comment above).
|
|
713
|
+
*
|
|
714
|
+
* Deliberately NOT accepted, each because the parser it replaces would answer
|
|
715
|
+
* differently:
|
|
716
|
+
*
|
|
717
|
+
* - a year that is not exactly four digits. A 5-digit year is a real
|
|
718
|
+
* PostgreSQL output and the general parser handles it; a fast path that
|
|
719
|
+
* matched a variable-width year would have to re-find every later field.
|
|
720
|
+
* - a year below 100 (trap 1 above).
|
|
721
|
+
* - a `T` separator for `SHAPE_TIMESTAMPTZ`. `postgres-date`'s
|
|
722
|
+
* own date-time regex requires a literal SPACE, so it returns `null` for
|
|
723
|
+
* `2024-01-01T00:00:00+00`; a scan that accepted `T` there would invent a
|
|
724
|
+
* Date where the driver hands back null. `timestamp` (1114) does accept it
|
|
725
|
+
* because the general parser it replaces does.
|
|
726
|
+
* - a missing offset for `SHAPE_TIMESTAMPTZ`. `postgres-date` reads an
|
|
727
|
+
* offset-less value in the process's LOCAL zone, which is not what this
|
|
728
|
+
* scan computes.
|
|
729
|
+
* - a colon-less offset (`+0530`), an alphabetic zone (` UTC`), or anything
|
|
730
|
+
* at all after the offset, including ` BC` (trap 2 above).
|
|
731
|
+
* - a bare `.` with no fractional digits.
|
|
732
|
+
*
|
|
733
|
+
* Field-value overflow (`2024-13-45`, `25:70:99`) IS accepted, because
|
|
734
|
+
* `Date.UTC` rolls those over exactly as the general parser's `setUTCFullYear`
|
|
735
|
+
* / `setUTCHours` do. PostgreSQL never emits them; agreeing on them is free.
|
|
736
|
+
*/
|
|
737
|
+
function scanIsoTemporal(text, shape) {
|
|
738
|
+
const len = text.length;
|
|
739
|
+
if (len < 10)
|
|
740
|
+
return null;
|
|
741
|
+
if (text.charCodeAt(4) !== CH_HYPHEN || text.charCodeAt(7) !== CH_HYPHEN)
|
|
742
|
+
return null;
|
|
743
|
+
const yearHi = twoDigitsAt(text, 0);
|
|
744
|
+
const yearLo = twoDigitsAt(text, 2);
|
|
745
|
+
if (yearHi < 0 || yearLo < 0)
|
|
746
|
+
return null;
|
|
747
|
+
const year = yearHi * 100 + yearLo;
|
|
748
|
+
if (year < 100)
|
|
749
|
+
return null;
|
|
750
|
+
const month = twoDigitsAt(text, 5);
|
|
751
|
+
const day = twoDigitsAt(text, 8);
|
|
752
|
+
if (month < 0 || day < 0)
|
|
753
|
+
return null;
|
|
754
|
+
if (shape === SHAPE_DATE) {
|
|
755
|
+
return len === 10 ? new Date(Date.UTC(year, month - 1, day)) : null;
|
|
756
|
+
}
|
|
757
|
+
if (len < 19)
|
|
758
|
+
return null;
|
|
759
|
+
const sep = text.charCodeAt(10);
|
|
760
|
+
if (shape === SHAPE_TIMESTAMP ? sep !== CH_SPACE && sep !== CH_T : sep !== CH_SPACE)
|
|
761
|
+
return null;
|
|
762
|
+
if (text.charCodeAt(13) !== CH_COLON || text.charCodeAt(16) !== CH_COLON)
|
|
763
|
+
return null;
|
|
764
|
+
const hour = twoDigitsAt(text, 11);
|
|
765
|
+
const minute = twoDigitsAt(text, 14);
|
|
766
|
+
const second = twoDigitsAt(text, 17);
|
|
767
|
+
if (hour < 0 || minute < 0 || second < 0)
|
|
768
|
+
return null;
|
|
769
|
+
let i = 19;
|
|
770
|
+
let ms = 0;
|
|
771
|
+
if (text.charCodeAt(i) === CH_DOT) {
|
|
772
|
+
i++;
|
|
773
|
+
let digits = 0;
|
|
774
|
+
for (;;) {
|
|
775
|
+
const d = text.charCodeAt(i) - 48;
|
|
776
|
+
if (!(d >= 0 && d <= 9))
|
|
777
|
+
break;
|
|
778
|
+
// Only the first three digits survive: PostgreSQL emits up to six and a
|
|
779
|
+
// JS Date holds milliseconds. `postgres-date` gets the same answer by a
|
|
780
|
+
// different route (`1000 * parseFloat('.476603')`, truncated by
|
|
781
|
+
// `Date.UTC`), and the two agree on every 1-to-6-digit fraction.
|
|
782
|
+
if (digits < 3)
|
|
783
|
+
ms = ms * 10 + d;
|
|
784
|
+
digits++;
|
|
785
|
+
i++;
|
|
786
|
+
}
|
|
787
|
+
if (digits === 0)
|
|
788
|
+
return null;
|
|
789
|
+
if (digits === 1)
|
|
790
|
+
ms *= 100;
|
|
791
|
+
else if (digits === 2)
|
|
792
|
+
ms *= 10;
|
|
793
|
+
}
|
|
794
|
+
if (shape === SHAPE_TIMESTAMP) {
|
|
795
|
+
return i === len ? new Date(Date.UTC(year, month - 1, day, hour, minute, second, ms)) : null;
|
|
796
|
+
}
|
|
797
|
+
let offsetMs = 0;
|
|
798
|
+
const signCode = text.charCodeAt(i);
|
|
799
|
+
if (signCode === CH_Z) {
|
|
800
|
+
i++;
|
|
801
|
+
}
|
|
802
|
+
else if (signCode === CH_PLUS || signCode === CH_MINUS) {
|
|
803
|
+
i++;
|
|
804
|
+
const offsetHours = twoDigitsAt(text, i);
|
|
805
|
+
if (offsetHours < 0)
|
|
806
|
+
return null;
|
|
807
|
+
i += 2;
|
|
808
|
+
let offsetMinutes = 0;
|
|
809
|
+
let offsetSeconds = 0;
|
|
810
|
+
if (text.charCodeAt(i) === CH_COLON) {
|
|
811
|
+
offsetMinutes = twoDigitsAt(text, i + 1);
|
|
812
|
+
if (offsetMinutes < 0)
|
|
813
|
+
return null;
|
|
814
|
+
i += 3;
|
|
815
|
+
// A zone whose historical LMT offset was not a whole minute emits
|
|
816
|
+
// seconds too (`1850-01-01 00:00:00+00:19:32`).
|
|
817
|
+
if (text.charCodeAt(i) === CH_COLON) {
|
|
818
|
+
offsetSeconds = twoDigitsAt(text, i + 1);
|
|
819
|
+
if (offsetSeconds < 0)
|
|
820
|
+
return null;
|
|
821
|
+
i += 3;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
offsetMs =
|
|
825
|
+
(offsetHours * 3_600_000 + offsetMinutes * 60_000 + offsetSeconds * 1000) * (signCode === CH_MINUS ? -1 : 1);
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
if (i !== len)
|
|
831
|
+
return null;
|
|
832
|
+
return new Date(Date.UTC(year, month - 1, day, hour, minute, second, ms) - offsetMs);
|
|
833
|
+
}
|
|
575
834
|
/**
|
|
576
835
|
* A Postgres `date` wire value: `YYYY-MM-DD`, optionally with more than four
|
|
577
836
|
* year digits, optionally suffixed ` BC`. Anything else (`infinity`,
|
|
@@ -605,6 +864,22 @@ const PG_DATE_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})( BC)?$/;
|
|
|
605
864
|
* astronomical year (`0044 BC` → -43) the way the driver's own parser does.
|
|
606
865
|
*/
|
|
607
866
|
function createUtcDateParser(fallback) {
|
|
867
|
+
const general = createUtcDateParserGeneral(fallback);
|
|
868
|
+
return (text) => scanIsoTemporal(text, SHAPE_DATE) ?? general(text);
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* The `date` (OID 1082) parser WITHOUT the fast scan in front of it: the regex
|
|
872
|
+
* implementation described by {@link createUtcDateParser}, and the reference
|
|
873
|
+
* side of the differential test.
|
|
874
|
+
*
|
|
875
|
+
* Exported so "what the fast path must agree with" is the running code rather
|
|
876
|
+
* than a transcription of it in a test file. Two hand-synced copies of a
|
|
877
|
+
* parser is the drift class this repo has been bitten by before; there is one
|
|
878
|
+
* copy, and the fast path delegates to it.
|
|
879
|
+
*
|
|
880
|
+
* @internal
|
|
881
|
+
*/
|
|
882
|
+
function createUtcDateParserGeneral(fallback) {
|
|
608
883
|
return (text) => {
|
|
609
884
|
const m = PG_DATE_TEXT_RE.exec(text);
|
|
610
885
|
if (!m)
|
|
@@ -644,6 +919,18 @@ const PG_TIMESTAMP_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2
|
|
|
644
919
|
* `Date`-string parsing did too.
|
|
645
920
|
*/
|
|
646
921
|
function createUtcTimestampParser(fallback) {
|
|
922
|
+
const general = createUtcTimestampParserGeneral(fallback);
|
|
923
|
+
return (text) => scanIsoTemporal(text, SHAPE_TIMESTAMP) ?? general(text);
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* The `timestamp` (OID 1114) parser WITHOUT the fast scan in front of it: the
|
|
927
|
+
* regex implementation described by {@link createUtcTimestampParser}, and the
|
|
928
|
+
* reference side of the differential test. Same reasoning as
|
|
929
|
+
* {@link createUtcDateParserGeneral}.
|
|
930
|
+
*
|
|
931
|
+
* @internal
|
|
932
|
+
*/
|
|
933
|
+
function createUtcTimestampParserGeneral(fallback) {
|
|
647
934
|
return (text) => {
|
|
648
935
|
const m = PG_TIMESTAMP_TEXT_RE.exec(text);
|
|
649
936
|
if (!m)
|
|
@@ -656,6 +943,27 @@ function createUtcTimestampParser(fallback) {
|
|
|
656
943
|
return date;
|
|
657
944
|
};
|
|
658
945
|
}
|
|
946
|
+
/**
|
|
947
|
+
* Build the driver parser for Postgres `timestamptz` (OID 1184): the ISO wire
|
|
948
|
+
* shape decoded by {@link scanIsoTemporal}, everything else handed straight to
|
|
949
|
+
* `fallback`.
|
|
950
|
+
*
|
|
951
|
+
* UNLIKE the `date` and `timestamp` parsers beside it, this one changes NO
|
|
952
|
+
* READING. A `timestamptz` arrives with an explicit offset, so its instant is
|
|
953
|
+
* unambiguous and both this and `postgres-date` produce the same `Date`; the
|
|
954
|
+
* only difference is how long it takes. That is also why it is not governed by
|
|
955
|
+
* a semantic decision the way `utcTimestamps` governs the zone-less types: there
|
|
956
|
+
* is no second interpretation to choose between.
|
|
957
|
+
*
|
|
958
|
+
* `fallback` must be captured with `pg.types.getTypeParser(1184, 'text')`
|
|
959
|
+
* BEFORE registration, for the same reason as the parsers above: reading it
|
|
960
|
+
* afterwards hands back this function and recurses forever. It is what keeps
|
|
961
|
+
* `infinity` / `-infinity`, ` BC`, wide and low years, and every non-ISO
|
|
962
|
+
* `DateStyle` on `postgres-date`, which already handles them.
|
|
963
|
+
*/
|
|
964
|
+
function createFastTimestamptzParser(fallback) {
|
|
965
|
+
return (text) => scanIsoTemporal(text, SHAPE_TIMESTAMPTZ) ?? fallback(text);
|
|
966
|
+
}
|
|
659
967
|
/**
|
|
660
968
|
* The offset-less-timestamp-as-UTC reading, with no fallback: `text` must be a
|
|
661
969
|
* plain `YYYY-MM-DD HH:MM:SS[.ffffff]`. Used where the input shape is already
|
|
@@ -677,10 +985,21 @@ function parseUtcTimestampText(text) {
|
|
|
677
985
|
* `pg.types.arrayParser` is a public member of the `pg` module (it is what the
|
|
678
986
|
* driver's own `_text` / `_date` parsers are built from), so this adds no
|
|
679
987
|
* dependency. NULL elements stay `null` and are never handed to `element`.
|
|
988
|
+
*
|
|
989
|
+
* The empty-string guard mirrors pg's own `parseDateArray`, which opens
|
|
990
|
+
* `if (!value) return null`. Turbine's copy did not, and answered `[]` where
|
|
991
|
+
* the driver answers `null` for the same input. No column produces it (a SQL
|
|
992
|
+
* NULL never reaches a parser, and an empty array is `{}`), so this is parity
|
|
993
|
+
* for its own sake rather than a bug report; it matters because these parsers
|
|
994
|
+
* are registered process-globally over pg's, and a shape where Turbine's
|
|
995
|
+
* answer differs from the driver's is a difference somebody eventually finds
|
|
996
|
+
* the hard way.
|
|
680
997
|
*/
|
|
681
998
|
function createPgArrayParser(element) {
|
|
682
999
|
const arrayParser = pg_1.default.types.arrayParser;
|
|
683
|
-
return (text) =>
|
|
1000
|
+
return (text) => text
|
|
1001
|
+
? arrayParser.create(text, (entry) => (entry === null || entry === undefined ? null : element(entry))).parse()
|
|
1002
|
+
: null;
|
|
684
1003
|
}
|
|
685
1004
|
/**
|
|
686
1005
|
* A canonical wire value per OID, plus the JS value pg's OWN default text
|
|
@@ -699,6 +1018,13 @@ const DEFAULT_PARSER_PROBES = {
|
|
|
699
1018
|
1114: { text: '2020-01-02 03:04:05', expected: () => new Date(2020, 0, 2, 3, 4, 5) },
|
|
700
1019
|
1115: { text: '{"2020-01-02 03:04:05"}', expected: () => [new Date(2020, 0, 2, 3, 4, 5)] },
|
|
701
1020
|
1182: { text: '{2020-01-02}', expected: () => [new Date(2020, 0, 2)] },
|
|
1021
|
+
// The `timestamptz` pair carries an explicit offset, so unlike the four
|
|
1022
|
+
// above its expectation is NOT computed from local components: pg's default
|
|
1023
|
+
// and Turbine's fast scan produce the same instant in every zone. These two
|
|
1024
|
+
// probes are consulted by {@link registerFastTemporalParserIfDefault}, which
|
|
1025
|
+
// DECLINES to install rather than warning-and-overwriting.
|
|
1026
|
+
1184: { text: '2020-01-02 03:04:05+00', expected: () => new Date(Date.UTC(2020, 0, 2, 3, 4, 5)) },
|
|
1027
|
+
1185: { text: '{"2020-01-02 03:04:05+00"}', expected: () => [new Date(Date.UTC(2020, 0, 2, 3, 4, 5))] },
|
|
702
1028
|
};
|
|
703
1029
|
/**
|
|
704
1030
|
* Marks a text parser as one TURBINE installed, so a later registration in the
|
|
@@ -708,8 +1034,15 @@ const DEFAULT_PARSER_PROBES = {
|
|
|
708
1034
|
* uses one.
|
|
709
1035
|
*/
|
|
710
1036
|
const TURBINE_PARSER = Symbol.for('turbine.typeParser');
|
|
711
|
-
/**
|
|
712
|
-
|
|
1037
|
+
/**
|
|
1038
|
+
* The OIDs the `utcTimestamps` flag governs, and so the ones it can opt out of.
|
|
1039
|
+
*
|
|
1040
|
+
* 1184 / 1185 are in the set because the flag gates their registration too,
|
|
1041
|
+
* but they are there for a different reason from the other four: those four
|
|
1042
|
+
* change a READING (local zone to UTC), while the `timestamptz` pair changes
|
|
1043
|
+
* only decode SPEED. See {@link registerUtcTemporalParsers}.
|
|
1044
|
+
*/
|
|
1045
|
+
const TEMPORAL_PARSER_OIDS = new Set([1114, 1082, 1115, 1182, 1184, 1185]);
|
|
713
1046
|
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
714
1047
|
function markTurbineParser(parser) {
|
|
715
1048
|
parser[TURBINE_PARSER] = true;
|
|
@@ -802,11 +1135,14 @@ function warnParserOverwrite(oid, typeName) {
|
|
|
802
1135
|
return;
|
|
803
1136
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.parserOverwrite, String(oid)))
|
|
804
1137
|
return;
|
|
805
|
-
// The `utcTimestamps: false` opt-out only governs the
|
|
1138
|
+
// The `utcTimestamps: false` opt-out only governs the six TEMPORAL OIDs.
|
|
806
1139
|
// Offering it as the remedy for int8 (20) would name a setting that does
|
|
807
1140
|
// nothing for the OID being warned about; that registration has no opt-out.
|
|
1141
|
+
// (1184 / 1185 never reach this warning at all: they DECLINE over a
|
|
1142
|
+
// non-default parser instead of overwriting it. They are named in the
|
|
1143
|
+
// sentence because it describes what the flag leaves alone.)
|
|
808
1144
|
const remedy = TEMPORAL_PARSER_OIDS.has(oid)
|
|
809
|
-
? ' `utcTimestamps: false` leaves the
|
|
1145
|
+
? ' `utcTimestamps: false` leaves the six temporal OIDs (1114, 1082, 1115, 1182, 1184, 1185) alone entirely.'
|
|
810
1146
|
: ' There is no opt-out for this OID: Turbine registers it so bigint values come back as numbers.';
|
|
811
1147
|
console.warn(`[turbine] pg type parser for OID ${oid} (${typeName}) was already customized by something else in this ` +
|
|
812
1148
|
'process, and Turbine is replacing it. `pg.types.setTypeParser` is process-global and takes effect ' +
|
|
@@ -817,8 +1153,21 @@ function warnParserOverwrite(oid, typeName) {
|
|
|
817
1153
|
'in production from import order alone.');
|
|
818
1154
|
}
|
|
819
1155
|
/**
|
|
820
|
-
* Register
|
|
821
|
-
*
|
|
1156
|
+
* Register Turbine's temporal text parsers on the pg module. SIX OIDs, doing
|
|
1157
|
+
* two different jobs:
|
|
1158
|
+
*
|
|
1159
|
+
* 1114 / 1082 / 1115 / 1182 the UTC READING of the zone-less types,
|
|
1160
|
+
* `timestamp`, `date` and their array forms.
|
|
1161
|
+
* This changes what a column means and is what
|
|
1162
|
+
* `utcTimestamps` is named for.
|
|
1163
|
+
* 1184 / 1185 the fast decode path for `timestamptz` and
|
|
1164
|
+
* `timestamptz[]`. This changes NOTHING about
|
|
1165
|
+
* what a column means: an offset-carrying value
|
|
1166
|
+
* has one instant and this reads the same one.
|
|
1167
|
+
* It is here for speed, `timestamptz` being ~88%
|
|
1168
|
+
* of the client-side decode cost of a wide row
|
|
1169
|
+
* drain, and it DECLINES rather than overwrites
|
|
1170
|
+
* (see the comment at the call site).
|
|
822
1171
|
*
|
|
823
1172
|
* ONE place, because `pg.types.setTypeParser` is process-global and the pairing
|
|
824
1173
|
* matters: registering a scalar without its array form, or a `date` without the
|
|
@@ -856,6 +1205,58 @@ function registerUtcTemporalParsers() {
|
|
|
856
1205
|
// beside them returned UTC ones.
|
|
857
1206
|
setParser(1182, markTurbineParser(createPgArrayParser(parseDate)));
|
|
858
1207
|
setParser(1115, markTurbineParser(createPgArrayParser(parseTimestamp)));
|
|
1208
|
+
// `timestamptz` (1184) and `timestamptz[]` (1185). SPEED ONLY: an offset-
|
|
1209
|
+
// carrying value has exactly one instant, and this reads it as the same
|
|
1210
|
+
// instant `postgres-date` does, so nothing here changes what a column means.
|
|
1211
|
+
//
|
|
1212
|
+
// Two things about it are deliberate and neither is obvious.
|
|
1213
|
+
//
|
|
1214
|
+
// It is gated behind `utcTimestamps` along with the other four, even though
|
|
1215
|
+
// that flag is about a READING and this is not. The alternative was a second
|
|
1216
|
+
// registration site outside this function, and one process-global parser
|
|
1217
|
+
// table with two places that write to it is the exact shape the "ONE place"
|
|
1218
|
+
// rule above exists to prevent. So the flag reads as "leave pg's temporal
|
|
1219
|
+
// parser table alone", and `utcTimestamps: false` costs the optimisation as
|
|
1220
|
+
// well as the UTC reading. That is a documented cost, not an oversight.
|
|
1221
|
+
//
|
|
1222
|
+
// And it DECLINES rather than overwrites (see
|
|
1223
|
+
// {@link registerFastTemporalParserIfDefault}), which is the opposite of what
|
|
1224
|
+
// the four above do. They MUST overwrite: they exist to replace a reading,
|
|
1225
|
+
// and a process where half the temporal columns read local and half read UTC
|
|
1226
|
+
// is broken. This one exists only to be faster, so a caller who installed
|
|
1227
|
+
// their own `timestamptz` parser (to get strings, or Luxon objects, or a
|
|
1228
|
+
// Temporal instant) keeps it. Overwriting them would trade their correctness
|
|
1229
|
+
// for our speed, which is never the right trade, and Turbine has never
|
|
1230
|
+
// touched 1184 before now, so declining is also what preserves that.
|
|
1231
|
+
const parseTimestamptz = registerFastTemporalParserIfDefault(1184, createFastTimestamptzParser);
|
|
1232
|
+
if (parseTimestamptz) {
|
|
1233
|
+
registerFastTemporalParserIfDefault(1185, () => createPgArrayParser(parseTimestamptz));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Install a SPEED-ONLY parser for `oid`, but only over pg's own default (or
|
|
1238
|
+
* over a parser Turbine itself installed earlier in this process).
|
|
1239
|
+
*
|
|
1240
|
+
* Returns the installed parser, or `undefined` when it declined, so a caller
|
|
1241
|
+
* can hold a scalar and its array form to the same decision: registering the
|
|
1242
|
+
* array half over a caller's customized scalar half would make the two
|
|
1243
|
+
* disagree, which is worse than leaving both slow.
|
|
1244
|
+
*
|
|
1245
|
+
* `build` receives the parser being replaced, which becomes the fast path's
|
|
1246
|
+
* fallback. That is only sound because the parser being replaced is known to
|
|
1247
|
+
* be pg's default (or Turbine's own wrapper around it); the check below is
|
|
1248
|
+
* what makes it so, and is not an optimisation of it.
|
|
1249
|
+
*/
|
|
1250
|
+
function registerFastTemporalParserIfDefault(oid, build) {
|
|
1251
|
+
const getParser = pg_1.default.types.getTypeParser;
|
|
1252
|
+
const setParser = pg_1.default.types.setTypeParser;
|
|
1253
|
+
const current = getParser(oid, 'text');
|
|
1254
|
+
const isTurbines = current[TURBINE_PARSER] === true;
|
|
1255
|
+
if (!isTurbines && !isDefaultTextParser(oid, current))
|
|
1256
|
+
return undefined;
|
|
1257
|
+
const parser = markTurbineParser(build(current));
|
|
1258
|
+
setParser(oid, parser);
|
|
1259
|
+
return parser;
|
|
859
1260
|
}
|
|
860
1261
|
// ---------------------------------------------------------------------------
|
|
861
1262
|
// JSON-wire value coercion (relationLoadStrategy: 'join')
|
|
@@ -120,11 +120,16 @@ function walkWhere(host, where) {
|
|
|
120
120
|
events.push({ kind: 'not', condition: value });
|
|
121
121
|
continue;
|
|
122
122
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
// Resolved rather than looked up, so a relation filter accepts the
|
|
124
|
+
// snake_case spelling of the relation exactly as `with` does. This is the
|
|
125
|
+
// ONE branch authority (build, fingerprint and param-collect all consume
|
|
126
|
+
// these events), so resolving here cannot drift between them; the emitted
|
|
127
|
+
// event carries the DECLARED name, which is what reaches the fingerprint.
|
|
128
|
+
const resolvedRel = (0, utils_js_1.resolveRelation)(host.tableMeta.relations, key);
|
|
129
|
+
if (resolvedRel && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
130
|
+
const filterObj = host.normalizeRelationFilter(resolvedRel.def, value);
|
|
126
131
|
if (isRelationFilterObj(filterObj)) {
|
|
127
|
-
events.push({ kind: 'relation', key, relDef, filterObj });
|
|
132
|
+
events.push({ kind: 'relation', key: resolvedRel.name, relDef: resolvedRel.def, filterObj });
|
|
128
133
|
continue;
|
|
129
134
|
}
|
|
130
135
|
}
|
package/dist/cjs/query/where.js
CHANGED
|
@@ -812,8 +812,8 @@ function buildScopedWhere(qi, scope, where, params, depth = 0) {
|
|
|
812
812
|
*/
|
|
813
813
|
function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
814
814
|
const meta = scope.meta;
|
|
815
|
-
const col = (0, utils_js_1.
|
|
816
|
-
if (
|
|
815
|
+
const col = (0, utils_js_1.resolveColumnName)(meta, field);
|
|
816
|
+
if (col === undefined)
|
|
817
817
|
throw scope.unknownColumn(field);
|
|
818
818
|
const qCol = `${scope.qualifier}${qi.q(col)}`;
|
|
819
819
|
if (value === null) {
|
|
@@ -891,7 +891,11 @@ function collectScopedScalarParams(qi, scope, field, value, params) {
|
|
|
891
891
|
if (value === null)
|
|
892
892
|
return;
|
|
893
893
|
const meta = scope.meta;
|
|
894
|
-
|
|
894
|
+
// Unvalidated on purpose: this is the cache-HIT mirror, and a key that does
|
|
895
|
+
// not resolve could never have produced the entry being served. It still
|
|
896
|
+
// goes through the one authority, so the column it binds against cannot
|
|
897
|
+
// differ from the one the build path emitted.
|
|
898
|
+
const col = (0, utils_js_1.resolveColumnName)(meta, field) ?? (0, schema_js_1.camelToSnake)(field);
|
|
895
899
|
if (typeof value === 'object' && !Array.isArray(value) && (0, filters_js_1.isJsonFilter)(value)) {
|
|
896
900
|
const colType = pgTypeForColumn(qi, meta, col);
|
|
897
901
|
if (isJsonColumnType(qi, colType)) {
|
|
@@ -1231,8 +1235,8 @@ function resolveColumnRef(_qi, ref, ctx, mode) {
|
|
|
1231
1235
|
`Case-insensitive column-to-column comparison is not supported: use client.sql\`...\` ` +
|
|
1232
1236
|
`for lower(a) = lower(b).`);
|
|
1233
1237
|
}
|
|
1234
|
-
const col = (0, utils_js_1.
|
|
1235
|
-
if (
|
|
1238
|
+
const col = (0, utils_js_1.resolveColumnName)(ctx.meta, ref.col);
|
|
1239
|
+
if (col === undefined) {
|
|
1236
1240
|
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${ref.col}" referenced by { col } in where on table "${ctx.table}". ` +
|
|
1237
1241
|
`Known fields: ${Object.keys(ctx.meta.columnMap).join(', ') || '(none)'}.`);
|
|
1238
1242
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -800,24 +800,6 @@ export declare class TurbineClient {
|
|
|
800
800
|
* (`ALTER ROLE ... SET plan_cache_mode = ...`).
|
|
801
801
|
*/
|
|
802
802
|
private static withPlanCacheMode;
|
|
803
|
-
/**
|
|
804
|
-
* `connectionString` with `setting` appended to its existing `options` query
|
|
805
|
-
* parameter, or `null` when it carries no `options` (in which case the caller
|
|
806
|
-
* should use the `options` pool field, which is not overridden).
|
|
807
|
-
*
|
|
808
|
-
* Only the query string is rewritten, never the userinfo or host, so a
|
|
809
|
-
* percent-encoded password cannot be mangled by a round trip through `URL`.
|
|
810
|
-
* The split is on the first `?`, which is also where pg's own parser puts the
|
|
811
|
-
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
812
|
-
* password is not parseable by pg either, so there is no shape this handles
|
|
813
|
-
* differently from the driver.
|
|
814
|
-
*
|
|
815
|
-
* A twin of this lives in `src/connection-url.ts`, which `turbine doctor`
|
|
816
|
-
* uses for `statement_timeout`. Unifying them is the obvious refactor and it
|
|
817
|
-
* is deliberately NOT done; the reason (a c8 merge artifact that costs almost
|
|
818
|
-
* all of the coverage gate's headroom) is written up over there.
|
|
819
|
-
*/
|
|
820
|
-
private static mergeConnectionStringOptions;
|
|
821
803
|
/**
|
|
822
804
|
* Refuse a `utcTimestamps` value that contradicts the one an earlier client
|
|
823
805
|
* in this process settled the zone-less temporal read parsers (OIDs 1114,
|
package/dist/client.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
24
|
import pg from 'pg';
|
|
25
|
+
import { mergeConnectionStringOptions } from './connection-url.js';
|
|
25
26
|
import { postgresDialect } from './dialect.js';
|
|
26
27
|
import { ConnectionError, errorMessageModesDiverged, registerClientErrorMessageMode, runWithErrorMessageMode, setErrorMessageMode, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
27
28
|
import { ObserveEngine } from './observe.js';
|
|
@@ -1138,7 +1139,7 @@ export class TurbineClient {
|
|
|
1138
1139
|
// cannot be a bind parameter.
|
|
1139
1140
|
const setting = `-c plan_cache_mode=${mode}`;
|
|
1140
1141
|
const merged = poolConfig.connectionString
|
|
1141
|
-
?
|
|
1142
|
+
? mergeConnectionStringOptions(poolConfig.connectionString, setting)
|
|
1142
1143
|
: null;
|
|
1143
1144
|
if (merged)
|
|
1144
1145
|
return { ...poolConfig, connectionString: merged };
|
|
@@ -1148,34 +1149,6 @@ export class TurbineClient {
|
|
|
1148
1149
|
const existing = poolConfig.options || (typeof process !== 'undefined' ? process.env?.PGOPTIONS : undefined);
|
|
1149
1150
|
return { ...poolConfig, options: existing ? `${existing} ${setting}` : setting };
|
|
1150
1151
|
}
|
|
1151
|
-
/**
|
|
1152
|
-
* `connectionString` with `setting` appended to its existing `options` query
|
|
1153
|
-
* parameter, or `null` when it carries no `options` (in which case the caller
|
|
1154
|
-
* should use the `options` pool field, which is not overridden).
|
|
1155
|
-
*
|
|
1156
|
-
* Only the query string is rewritten, never the userinfo or host, so a
|
|
1157
|
-
* percent-encoded password cannot be mangled by a round trip through `URL`.
|
|
1158
|
-
* The split is on the first `?`, which is also where pg's own parser puts the
|
|
1159
|
-
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
1160
|
-
* password is not parseable by pg either, so there is no shape this handles
|
|
1161
|
-
* differently from the driver.
|
|
1162
|
-
*
|
|
1163
|
-
* A twin of this lives in `src/connection-url.ts`, which `turbine doctor`
|
|
1164
|
-
* uses for `statement_timeout`. Unifying them is the obvious refactor and it
|
|
1165
|
-
* is deliberately NOT done; the reason (a c8 merge artifact that costs almost
|
|
1166
|
-
* all of the coverage gate's headroom) is written up over there.
|
|
1167
|
-
*/
|
|
1168
|
-
static mergeConnectionStringOptions(connectionString, setting) {
|
|
1169
|
-
const q = connectionString.indexOf('?');
|
|
1170
|
-
if (q === -1)
|
|
1171
|
-
return null;
|
|
1172
|
-
const params = new URLSearchParams(connectionString.slice(q + 1));
|
|
1173
|
-
const existing = params.get('options');
|
|
1174
|
-
if (existing === null)
|
|
1175
|
-
return null;
|
|
1176
|
-
params.set('options', `${existing} ${setting}`);
|
|
1177
|
-
return connectionString.slice(0, q + 1) + params.toString();
|
|
1178
|
-
}
|
|
1179
1152
|
/**
|
|
1180
1153
|
* Refuse a `utcTimestamps` value that contradicts the one an earlier client
|
|
1181
1154
|
* in this process settled the zone-less temporal read parsers (OIDs 1114,
|