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/query/utils.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Standalone utility functions and classes used by the query builder.
|
|
5
5
|
*/
|
|
6
6
|
import pg from 'pg';
|
|
7
|
-
import { camelToSnake, localDateTimeKind, timeOfDayKind } from '../schema.js';
|
|
7
|
+
import { camelToSnake, localDateTimeKind, snakeToCamel, timeOfDayKind } from '../schema.js';
|
|
8
8
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Identifier quoting, prevents SQL injection via table/column names
|
|
@@ -67,6 +67,55 @@ export function resolveColumnName(meta, key) {
|
|
|
67
67
|
return snake;
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a user-supplied key to a relation's CANONICAL name and definition, or
|
|
72
|
+
* `undefined` when the key names no relation on the table.
|
|
73
|
+
*
|
|
74
|
+
* The relation-name half of the rule {@link resolveColumnName} states for
|
|
75
|
+
* columns, and deliberately the same shape: the declared name first, else
|
|
76
|
+
* `snakeToCamel(key)` accepted ONLY when that names a real relation.
|
|
77
|
+
* `snakeToCamel` is idempotent on an already-camel string, so a canonical key
|
|
78
|
+
* takes the first branch and this is a no-op for every existing caller.
|
|
79
|
+
*
|
|
80
|
+
* WHY IT EXISTS. A relation has one declared name, `ripeningChecks`, while the
|
|
81
|
+
* DDL anyone reads has only the TABLE name, `ripening_checks`. Writing back
|
|
82
|
+
* what the schema shows therefore failed with E005 in `with`, E003 in a
|
|
83
|
+
* relation filter, and E005 in `orderBy`, on names the error text was already
|
|
84
|
+
* computing correctly ("Did you mean ...?"). A system that can name the
|
|
85
|
+
* intended relation can accept it.
|
|
86
|
+
*
|
|
87
|
+
* NOT A GUESS, for the same reason the column rule is not: the transformed name
|
|
88
|
+
* is accepted only when it is a real declared relation, so an unknown key still
|
|
89
|
+
* fails and a typo is still a typo. Exact match wins first, so a schema that
|
|
90
|
+
* literally declares `ripening_checks` keeps it, even alongside a
|
|
91
|
+
* `ripeningChecks`.
|
|
92
|
+
*
|
|
93
|
+
* The RESULT KEY is the canonical name, not the caller's spelling, matching the
|
|
94
|
+
* column side (`select: { ledger_handle: true }` already returns
|
|
95
|
+
* `{ ledgerHandle }`). Resolving here rather than normalizing the args up front
|
|
96
|
+
* also means both spellings share one SQL-cache entry instead of minting two
|
|
97
|
+
* templates for one query.
|
|
98
|
+
*
|
|
99
|
+
* Prototype-safe via {@link ownLookup}, so `__proto__` cannot name a relation.
|
|
100
|
+
*/
|
|
101
|
+
export function resolveRelation(relations, key) {
|
|
102
|
+
const direct = ownLookup(relations, key);
|
|
103
|
+
if (direct !== undefined)
|
|
104
|
+
return { name: key, def: direct };
|
|
105
|
+
const camel = snakeToCamel(key);
|
|
106
|
+
if (camel === key)
|
|
107
|
+
return undefined;
|
|
108
|
+
const mapped = ownLookup(relations, camel);
|
|
109
|
+
return mapped === undefined ? undefined : { name: camel, def: mapped };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* {@link resolveRelation} when only the definition is wanted: a drop-in for the
|
|
113
|
+
* `ownLookup(meta.relations, key)` it replaces, with the same signature and the
|
|
114
|
+
* same `undefined` on a miss.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveRelationDef(relations, key) {
|
|
117
|
+
return resolveRelation(relations, key)?.def;
|
|
118
|
+
}
|
|
70
119
|
// ---------------------------------------------------------------------------
|
|
71
120
|
// Caller-controlled key ORDER, canonicalized
|
|
72
121
|
// ---------------------------------------------------------------------------
|
|
@@ -530,6 +579,211 @@ export function isTemporalInfinity(value) {
|
|
|
530
579
|
// ---------------------------------------------------------------------------
|
|
531
580
|
// Driver type parsers for the zone-less temporal OIDs
|
|
532
581
|
// ---------------------------------------------------------------------------
|
|
582
|
+
// ---------------------------------------------------------------------------
|
|
583
|
+
// The fast temporal scan
|
|
584
|
+
//
|
|
585
|
+
// Every temporal parser below is a two-stage function: a hand-written
|
|
586
|
+
// character scan that claims the ONE wire shape a busy application actually
|
|
587
|
+
// produces, and behind it the general parser, which is where every other shape
|
|
588
|
+
// (and every shape a future server adds) still goes.
|
|
589
|
+
//
|
|
590
|
+
// WHY IT EXISTS, stated as a measurement rather than an intuition. Draining
|
|
591
|
+
// 50,000 rows of the benchmark `comments` fixture spends ~23.5 ms in
|
|
592
|
+
// client-side type decoding, and `timestamptz` alone is 20.7 ms of it (88%).
|
|
593
|
+
// The remaining types are not worth touching: `text`, `numeric`, `uuid` and
|
|
594
|
+
// `bool` cost nothing at all, because pg registers no parser for them. Two
|
|
595
|
+
// hard bounds were verified in node-postgres' source before any of this was
|
|
596
|
+
// written, and they say what a decoder rewrite can and cannot reach: every
|
|
597
|
+
// cell is materialised as a JS string by `reader.string(len)` before any
|
|
598
|
+
// parser is consulted, so the `utf8Slice` half is unreachable; and the binary
|
|
599
|
+
// protocol is not an escape hatch, the parser constructor throws
|
|
600
|
+
// `Binary mode not supported yet`. The type-PARSING half is the whole budget,
|
|
601
|
+
// and this is a claim on it.
|
|
602
|
+
//
|
|
603
|
+
// THE RULE, and it is the only thing that makes a fast path acceptable here:
|
|
604
|
+
// **the scan must return exactly what the parser it replaces returned, or
|
|
605
|
+
// return `null` and not claim the value at all.** It never guesses, never
|
|
606
|
+
// "handles" a shape approximately, and never widens what it accepts to cover
|
|
607
|
+
// one more case. Every refusal costs a few charCode compares and is repaid by
|
|
608
|
+
// the general parser being correct.
|
|
609
|
+
//
|
|
610
|
+
// Two traps are baked into the refusals rather than into corrections, because
|
|
611
|
+
// a throwaway version of this decoder wrote during the ceiling measurement got
|
|
612
|
+
// 3 of 15 edge values wrong while believing it had delegated the hard ones:
|
|
613
|
+
//
|
|
614
|
+
// 1. `Date.UTC` maps years 0-99 onto 1900-1999, so `0044-03-15` decodes as
|
|
615
|
+
// 1944 and `0001-01-01` as 1901. The obvious repair, build the Date then
|
|
616
|
+
// `setUTCFullYear`, is ALSO wrong: year 0 is a leap year in the proleptic
|
|
617
|
+
// Gregorian calendar and 1900 is not, so `0000-02-29` built that way
|
|
618
|
+
// lands on March 1st. The scan refuses `year < 100` and lets the general
|
|
619
|
+
// parser's `new Date(0)` + `setUTCFullYear(y, m, d)` assembly (which sets
|
|
620
|
+
// all three fields against the right year's calendar) answer it.
|
|
621
|
+
// 2. An offset parse that runs to end-of-string silently EATS a trailing
|
|
622
|
+
// ` BC`: `4713-01-01 00:00:00+00 BC` decodes as AD 4713, off by 9,424
|
|
623
|
+
// years with no error. The scan requires end-of-string after the offset.
|
|
624
|
+
//
|
|
625
|
+
// Differential coverage: src/test/fast-temporal-decode.test.ts compares the
|
|
626
|
+
// scan against the parser it replaces value for value, and
|
|
627
|
+
// src/test/fast-temporal-decode.integration.test.ts does the same against a
|
|
628
|
+
// live server with `DateStyle` and `TimeZone` varied, because the wire shape
|
|
629
|
+
// is a server setting and a decoder tuned to one `DateStyle` is a latent
|
|
630
|
+
// corruption bug.
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
// Wire shape the scan is reading. Plain module constants rather than an enum:
|
|
633
|
+
// this repo's lint config rejects `const enum` (it does not survive
|
|
634
|
+
// `isolatedModules`), and a plain `enum` emits a runtime object, so every
|
|
635
|
+
// `Shape.Date` in the scan below would become a property load in the hottest
|
|
636
|
+
// loop in the library. These are values, never serialized, never persisted.
|
|
637
|
+
/** `YYYY-MM-DD` (OID 1082). */
|
|
638
|
+
const SHAPE_DATE = 0;
|
|
639
|
+
/** `YYYY-MM-DD[ T]HH:MM:SS[.f…]` (OID 1114), never an offset. */
|
|
640
|
+
const SHAPE_TIMESTAMP = 1;
|
|
641
|
+
/** `YYYY-MM-DD HH:MM:SS[.f…](Z|±HH[:MM[:SS]])` (OID 1184), offset REQUIRED. */
|
|
642
|
+
const SHAPE_TIMESTAMPTZ = 2;
|
|
643
|
+
const CH_HYPHEN = 45;
|
|
644
|
+
const CH_COLON = 58;
|
|
645
|
+
const CH_DOT = 46;
|
|
646
|
+
const CH_SPACE = 32;
|
|
647
|
+
const CH_T = 84;
|
|
648
|
+
const CH_Z = 90;
|
|
649
|
+
const CH_PLUS = 43;
|
|
650
|
+
/** Same code point as {@link CH_HYPHEN}; named separately because the two read
|
|
651
|
+
* as different things (a date separator, an offset sign) at their use sites. */
|
|
652
|
+
const CH_MINUS = 45;
|
|
653
|
+
/**
|
|
654
|
+
* Two ASCII digits at `i` as a number, or `-1` if either character is not a
|
|
655
|
+
* digit. `charCodeAt` past the end returns `NaN`, and `NaN - 48` is `NaN`,
|
|
656
|
+
* which fails both range tests, so this needs no separate length check.
|
|
657
|
+
*/
|
|
658
|
+
function twoDigitsAt(text, i) {
|
|
659
|
+
const hi = text.charCodeAt(i) - 48;
|
|
660
|
+
const lo = text.charCodeAt(i + 1) - 48;
|
|
661
|
+
return hi >= 0 && hi <= 9 && lo >= 0 && lo <= 9 ? hi * 10 + lo : -1;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Decode `text` if it is exactly the canonical ISO wire shape for `shape`, or
|
|
665
|
+
* return `null` to say "not mine" (see the rule in the block comment above).
|
|
666
|
+
*
|
|
667
|
+
* Deliberately NOT accepted, each because the parser it replaces would answer
|
|
668
|
+
* differently:
|
|
669
|
+
*
|
|
670
|
+
* - a year that is not exactly four digits. A 5-digit year is a real
|
|
671
|
+
* PostgreSQL output and the general parser handles it; a fast path that
|
|
672
|
+
* matched a variable-width year would have to re-find every later field.
|
|
673
|
+
* - a year below 100 (trap 1 above).
|
|
674
|
+
* - a `T` separator for `SHAPE_TIMESTAMPTZ`. `postgres-date`'s
|
|
675
|
+
* own date-time regex requires a literal SPACE, so it returns `null` for
|
|
676
|
+
* `2024-01-01T00:00:00+00`; a scan that accepted `T` there would invent a
|
|
677
|
+
* Date where the driver hands back null. `timestamp` (1114) does accept it
|
|
678
|
+
* because the general parser it replaces does.
|
|
679
|
+
* - a missing offset for `SHAPE_TIMESTAMPTZ`. `postgres-date` reads an
|
|
680
|
+
* offset-less value in the process's LOCAL zone, which is not what this
|
|
681
|
+
* scan computes.
|
|
682
|
+
* - a colon-less offset (`+0530`), an alphabetic zone (` UTC`), or anything
|
|
683
|
+
* at all after the offset, including ` BC` (trap 2 above).
|
|
684
|
+
* - a bare `.` with no fractional digits.
|
|
685
|
+
*
|
|
686
|
+
* Field-value overflow (`2024-13-45`, `25:70:99`) IS accepted, because
|
|
687
|
+
* `Date.UTC` rolls those over exactly as the general parser's `setUTCFullYear`
|
|
688
|
+
* / `setUTCHours` do. PostgreSQL never emits them; agreeing on them is free.
|
|
689
|
+
*/
|
|
690
|
+
function scanIsoTemporal(text, shape) {
|
|
691
|
+
const len = text.length;
|
|
692
|
+
if (len < 10)
|
|
693
|
+
return null;
|
|
694
|
+
if (text.charCodeAt(4) !== CH_HYPHEN || text.charCodeAt(7) !== CH_HYPHEN)
|
|
695
|
+
return null;
|
|
696
|
+
const yearHi = twoDigitsAt(text, 0);
|
|
697
|
+
const yearLo = twoDigitsAt(text, 2);
|
|
698
|
+
if (yearHi < 0 || yearLo < 0)
|
|
699
|
+
return null;
|
|
700
|
+
const year = yearHi * 100 + yearLo;
|
|
701
|
+
if (year < 100)
|
|
702
|
+
return null;
|
|
703
|
+
const month = twoDigitsAt(text, 5);
|
|
704
|
+
const day = twoDigitsAt(text, 8);
|
|
705
|
+
if (month < 0 || day < 0)
|
|
706
|
+
return null;
|
|
707
|
+
if (shape === SHAPE_DATE) {
|
|
708
|
+
return len === 10 ? new Date(Date.UTC(year, month - 1, day)) : null;
|
|
709
|
+
}
|
|
710
|
+
if (len < 19)
|
|
711
|
+
return null;
|
|
712
|
+
const sep = text.charCodeAt(10);
|
|
713
|
+
if (shape === SHAPE_TIMESTAMP ? sep !== CH_SPACE && sep !== CH_T : sep !== CH_SPACE)
|
|
714
|
+
return null;
|
|
715
|
+
if (text.charCodeAt(13) !== CH_COLON || text.charCodeAt(16) !== CH_COLON)
|
|
716
|
+
return null;
|
|
717
|
+
const hour = twoDigitsAt(text, 11);
|
|
718
|
+
const minute = twoDigitsAt(text, 14);
|
|
719
|
+
const second = twoDigitsAt(text, 17);
|
|
720
|
+
if (hour < 0 || minute < 0 || second < 0)
|
|
721
|
+
return null;
|
|
722
|
+
let i = 19;
|
|
723
|
+
let ms = 0;
|
|
724
|
+
if (text.charCodeAt(i) === CH_DOT) {
|
|
725
|
+
i++;
|
|
726
|
+
let digits = 0;
|
|
727
|
+
for (;;) {
|
|
728
|
+
const d = text.charCodeAt(i) - 48;
|
|
729
|
+
if (!(d >= 0 && d <= 9))
|
|
730
|
+
break;
|
|
731
|
+
// Only the first three digits survive: PostgreSQL emits up to six and a
|
|
732
|
+
// JS Date holds milliseconds. `postgres-date` gets the same answer by a
|
|
733
|
+
// different route (`1000 * parseFloat('.476603')`, truncated by
|
|
734
|
+
// `Date.UTC`), and the two agree on every 1-to-6-digit fraction.
|
|
735
|
+
if (digits < 3)
|
|
736
|
+
ms = ms * 10 + d;
|
|
737
|
+
digits++;
|
|
738
|
+
i++;
|
|
739
|
+
}
|
|
740
|
+
if (digits === 0)
|
|
741
|
+
return null;
|
|
742
|
+
if (digits === 1)
|
|
743
|
+
ms *= 100;
|
|
744
|
+
else if (digits === 2)
|
|
745
|
+
ms *= 10;
|
|
746
|
+
}
|
|
747
|
+
if (shape === SHAPE_TIMESTAMP) {
|
|
748
|
+
return i === len ? new Date(Date.UTC(year, month - 1, day, hour, minute, second, ms)) : null;
|
|
749
|
+
}
|
|
750
|
+
let offsetMs = 0;
|
|
751
|
+
const signCode = text.charCodeAt(i);
|
|
752
|
+
if (signCode === CH_Z) {
|
|
753
|
+
i++;
|
|
754
|
+
}
|
|
755
|
+
else if (signCode === CH_PLUS || signCode === CH_MINUS) {
|
|
756
|
+
i++;
|
|
757
|
+
const offsetHours = twoDigitsAt(text, i);
|
|
758
|
+
if (offsetHours < 0)
|
|
759
|
+
return null;
|
|
760
|
+
i += 2;
|
|
761
|
+
let offsetMinutes = 0;
|
|
762
|
+
let offsetSeconds = 0;
|
|
763
|
+
if (text.charCodeAt(i) === CH_COLON) {
|
|
764
|
+
offsetMinutes = twoDigitsAt(text, i + 1);
|
|
765
|
+
if (offsetMinutes < 0)
|
|
766
|
+
return null;
|
|
767
|
+
i += 3;
|
|
768
|
+
// A zone whose historical LMT offset was not a whole minute emits
|
|
769
|
+
// seconds too (`1850-01-01 00:00:00+00:19:32`).
|
|
770
|
+
if (text.charCodeAt(i) === CH_COLON) {
|
|
771
|
+
offsetSeconds = twoDigitsAt(text, i + 1);
|
|
772
|
+
if (offsetSeconds < 0)
|
|
773
|
+
return null;
|
|
774
|
+
i += 3;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
offsetMs =
|
|
778
|
+
(offsetHours * 3_600_000 + offsetMinutes * 60_000 + offsetSeconds * 1000) * (signCode === CH_MINUS ? -1 : 1);
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
if (i !== len)
|
|
784
|
+
return null;
|
|
785
|
+
return new Date(Date.UTC(year, month - 1, day, hour, minute, second, ms) - offsetMs);
|
|
786
|
+
}
|
|
533
787
|
/**
|
|
534
788
|
* A Postgres `date` wire value: `YYYY-MM-DD`, optionally with more than four
|
|
535
789
|
* year digits, optionally suffixed ` BC`. Anything else (`infinity`,
|
|
@@ -563,6 +817,22 @@ const PG_DATE_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})( BC)?$/;
|
|
|
563
817
|
* astronomical year (`0044 BC` → -43) the way the driver's own parser does.
|
|
564
818
|
*/
|
|
565
819
|
export function createUtcDateParser(fallback) {
|
|
820
|
+
const general = createUtcDateParserGeneral(fallback);
|
|
821
|
+
return (text) => scanIsoTemporal(text, SHAPE_DATE) ?? general(text);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* The `date` (OID 1082) parser WITHOUT the fast scan in front of it: the regex
|
|
825
|
+
* implementation described by {@link createUtcDateParser}, and the reference
|
|
826
|
+
* side of the differential test.
|
|
827
|
+
*
|
|
828
|
+
* Exported so "what the fast path must agree with" is the running code rather
|
|
829
|
+
* than a transcription of it in a test file. Two hand-synced copies of a
|
|
830
|
+
* parser is the drift class this repo has been bitten by before; there is one
|
|
831
|
+
* copy, and the fast path delegates to it.
|
|
832
|
+
*
|
|
833
|
+
* @internal
|
|
834
|
+
*/
|
|
835
|
+
export function createUtcDateParserGeneral(fallback) {
|
|
566
836
|
return (text) => {
|
|
567
837
|
const m = PG_DATE_TEXT_RE.exec(text);
|
|
568
838
|
if (!m)
|
|
@@ -602,6 +872,18 @@ const PG_TIMESTAMP_TEXT_RE = /^(\d{4,})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2
|
|
|
602
872
|
* `Date`-string parsing did too.
|
|
603
873
|
*/
|
|
604
874
|
export function createUtcTimestampParser(fallback) {
|
|
875
|
+
const general = createUtcTimestampParserGeneral(fallback);
|
|
876
|
+
return (text) => scanIsoTemporal(text, SHAPE_TIMESTAMP) ?? general(text);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The `timestamp` (OID 1114) parser WITHOUT the fast scan in front of it: the
|
|
880
|
+
* regex implementation described by {@link createUtcTimestampParser}, and the
|
|
881
|
+
* reference side of the differential test. Same reasoning as
|
|
882
|
+
* {@link createUtcDateParserGeneral}.
|
|
883
|
+
*
|
|
884
|
+
* @internal
|
|
885
|
+
*/
|
|
886
|
+
export function createUtcTimestampParserGeneral(fallback) {
|
|
605
887
|
return (text) => {
|
|
606
888
|
const m = PG_TIMESTAMP_TEXT_RE.exec(text);
|
|
607
889
|
if (!m)
|
|
@@ -614,6 +896,27 @@ export function createUtcTimestampParser(fallback) {
|
|
|
614
896
|
return date;
|
|
615
897
|
};
|
|
616
898
|
}
|
|
899
|
+
/**
|
|
900
|
+
* Build the driver parser for Postgres `timestamptz` (OID 1184): the ISO wire
|
|
901
|
+
* shape decoded by {@link scanIsoTemporal}, everything else handed straight to
|
|
902
|
+
* `fallback`.
|
|
903
|
+
*
|
|
904
|
+
* UNLIKE the `date` and `timestamp` parsers beside it, this one changes NO
|
|
905
|
+
* READING. A `timestamptz` arrives with an explicit offset, so its instant is
|
|
906
|
+
* unambiguous and both this and `postgres-date` produce the same `Date`; the
|
|
907
|
+
* only difference is how long it takes. That is also why it is not governed by
|
|
908
|
+
* a semantic decision the way `utcTimestamps` governs the zone-less types: there
|
|
909
|
+
* is no second interpretation to choose between.
|
|
910
|
+
*
|
|
911
|
+
* `fallback` must be captured with `pg.types.getTypeParser(1184, 'text')`
|
|
912
|
+
* BEFORE registration, for the same reason as the parsers above: reading it
|
|
913
|
+
* afterwards hands back this function and recurses forever. It is what keeps
|
|
914
|
+
* `infinity` / `-infinity`, ` BC`, wide and low years, and every non-ISO
|
|
915
|
+
* `DateStyle` on `postgres-date`, which already handles them.
|
|
916
|
+
*/
|
|
917
|
+
export function createFastTimestamptzParser(fallback) {
|
|
918
|
+
return (text) => scanIsoTemporal(text, SHAPE_TIMESTAMPTZ) ?? fallback(text);
|
|
919
|
+
}
|
|
617
920
|
/**
|
|
618
921
|
* The offset-less-timestamp-as-UTC reading, with no fallback: `text` must be a
|
|
619
922
|
* plain `YYYY-MM-DD HH:MM:SS[.ffffff]`. Used where the input shape is already
|
|
@@ -635,10 +938,21 @@ export function parseUtcTimestampText(text) {
|
|
|
635
938
|
* `pg.types.arrayParser` is a public member of the `pg` module (it is what the
|
|
636
939
|
* driver's own `_text` / `_date` parsers are built from), so this adds no
|
|
637
940
|
* dependency. NULL elements stay `null` and are never handed to `element`.
|
|
941
|
+
*
|
|
942
|
+
* The empty-string guard mirrors pg's own `parseDateArray`, which opens
|
|
943
|
+
* `if (!value) return null`. Turbine's copy did not, and answered `[]` where
|
|
944
|
+
* the driver answers `null` for the same input. No column produces it (a SQL
|
|
945
|
+
* NULL never reaches a parser, and an empty array is `{}`), so this is parity
|
|
946
|
+
* for its own sake rather than a bug report; it matters because these parsers
|
|
947
|
+
* are registered process-globally over pg's, and a shape where Turbine's
|
|
948
|
+
* answer differs from the driver's is a difference somebody eventually finds
|
|
949
|
+
* the hard way.
|
|
638
950
|
*/
|
|
639
951
|
export function createPgArrayParser(element) {
|
|
640
952
|
const arrayParser = pg.types.arrayParser;
|
|
641
|
-
return (text) =>
|
|
953
|
+
return (text) => text
|
|
954
|
+
? arrayParser.create(text, (entry) => (entry === null || entry === undefined ? null : element(entry))).parse()
|
|
955
|
+
: null;
|
|
642
956
|
}
|
|
643
957
|
/**
|
|
644
958
|
* A canonical wire value per OID, plus the JS value pg's OWN default text
|
|
@@ -657,6 +971,13 @@ const DEFAULT_PARSER_PROBES = {
|
|
|
657
971
|
1114: { text: '2020-01-02 03:04:05', expected: () => new Date(2020, 0, 2, 3, 4, 5) },
|
|
658
972
|
1115: { text: '{"2020-01-02 03:04:05"}', expected: () => [new Date(2020, 0, 2, 3, 4, 5)] },
|
|
659
973
|
1182: { text: '{2020-01-02}', expected: () => [new Date(2020, 0, 2)] },
|
|
974
|
+
// The `timestamptz` pair carries an explicit offset, so unlike the four
|
|
975
|
+
// above its expectation is NOT computed from local components: pg's default
|
|
976
|
+
// and Turbine's fast scan produce the same instant in every zone. These two
|
|
977
|
+
// probes are consulted by {@link registerFastTemporalParserIfDefault}, which
|
|
978
|
+
// DECLINES to install rather than warning-and-overwriting.
|
|
979
|
+
1184: { text: '2020-01-02 03:04:05+00', expected: () => new Date(Date.UTC(2020, 0, 2, 3, 4, 5)) },
|
|
980
|
+
1185: { text: '{"2020-01-02 03:04:05+00"}', expected: () => [new Date(Date.UTC(2020, 0, 2, 3, 4, 5))] },
|
|
660
981
|
};
|
|
661
982
|
/**
|
|
662
983
|
* Marks a text parser as one TURBINE installed, so a later registration in the
|
|
@@ -666,8 +987,15 @@ const DEFAULT_PARSER_PROBES = {
|
|
|
666
987
|
* uses one.
|
|
667
988
|
*/
|
|
668
989
|
const TURBINE_PARSER = Symbol.for('turbine.typeParser');
|
|
669
|
-
/**
|
|
670
|
-
|
|
990
|
+
/**
|
|
991
|
+
* The OIDs the `utcTimestamps` flag governs, and so the ones it can opt out of.
|
|
992
|
+
*
|
|
993
|
+
* 1184 / 1185 are in the set because the flag gates their registration too,
|
|
994
|
+
* but they are there for a different reason from the other four: those four
|
|
995
|
+
* change a READING (local zone to UTC), while the `timestamptz` pair changes
|
|
996
|
+
* only decode SPEED. See {@link registerUtcTemporalParsers}.
|
|
997
|
+
*/
|
|
998
|
+
const TEMPORAL_PARSER_OIDS = new Set([1114, 1082, 1115, 1182, 1184, 1185]);
|
|
671
999
|
/** Tag `parser` as Turbine's own and return it (see {@link TURBINE_PARSER}). */
|
|
672
1000
|
export function markTurbineParser(parser) {
|
|
673
1001
|
parser[TURBINE_PARSER] = true;
|
|
@@ -760,11 +1088,14 @@ export function warnParserOverwrite(oid, typeName) {
|
|
|
760
1088
|
return;
|
|
761
1089
|
if (!shouldWarnOnce(WARN_NS.parserOverwrite, String(oid)))
|
|
762
1090
|
return;
|
|
763
|
-
// The `utcTimestamps: false` opt-out only governs the
|
|
1091
|
+
// The `utcTimestamps: false` opt-out only governs the six TEMPORAL OIDs.
|
|
764
1092
|
// Offering it as the remedy for int8 (20) would name a setting that does
|
|
765
1093
|
// nothing for the OID being warned about; that registration has no opt-out.
|
|
1094
|
+
// (1184 / 1185 never reach this warning at all: they DECLINE over a
|
|
1095
|
+
// non-default parser instead of overwriting it. They are named in the
|
|
1096
|
+
// sentence because it describes what the flag leaves alone.)
|
|
766
1097
|
const remedy = TEMPORAL_PARSER_OIDS.has(oid)
|
|
767
|
-
? ' `utcTimestamps: false` leaves the
|
|
1098
|
+
? ' `utcTimestamps: false` leaves the six temporal OIDs (1114, 1082, 1115, 1182, 1184, 1185) alone entirely.'
|
|
768
1099
|
: ' There is no opt-out for this OID: Turbine registers it so bigint values come back as numbers.';
|
|
769
1100
|
console.warn(`[turbine] pg type parser for OID ${oid} (${typeName}) was already customized by something else in this ` +
|
|
770
1101
|
'process, and Turbine is replacing it. `pg.types.setTypeParser` is process-global and takes effect ' +
|
|
@@ -775,8 +1106,21 @@ export function warnParserOverwrite(oid, typeName) {
|
|
|
775
1106
|
'in production from import order alone.');
|
|
776
1107
|
}
|
|
777
1108
|
/**
|
|
778
|
-
* Register
|
|
779
|
-
*
|
|
1109
|
+
* Register Turbine's temporal text parsers on the pg module. SIX OIDs, doing
|
|
1110
|
+
* two different jobs:
|
|
1111
|
+
*
|
|
1112
|
+
* 1114 / 1082 / 1115 / 1182 the UTC READING of the zone-less types,
|
|
1113
|
+
* `timestamp`, `date` and their array forms.
|
|
1114
|
+
* This changes what a column means and is what
|
|
1115
|
+
* `utcTimestamps` is named for.
|
|
1116
|
+
* 1184 / 1185 the fast decode path for `timestamptz` and
|
|
1117
|
+
* `timestamptz[]`. This changes NOTHING about
|
|
1118
|
+
* what a column means: an offset-carrying value
|
|
1119
|
+
* has one instant and this reads the same one.
|
|
1120
|
+
* It is here for speed, `timestamptz` being ~88%
|
|
1121
|
+
* of the client-side decode cost of a wide row
|
|
1122
|
+
* drain, and it DECLINES rather than overwrites
|
|
1123
|
+
* (see the comment at the call site).
|
|
780
1124
|
*
|
|
781
1125
|
* ONE place, because `pg.types.setTypeParser` is process-global and the pairing
|
|
782
1126
|
* matters: registering a scalar without its array form, or a `date` without the
|
|
@@ -814,6 +1158,58 @@ export function registerUtcTemporalParsers() {
|
|
|
814
1158
|
// beside them returned UTC ones.
|
|
815
1159
|
setParser(1182, markTurbineParser(createPgArrayParser(parseDate)));
|
|
816
1160
|
setParser(1115, markTurbineParser(createPgArrayParser(parseTimestamp)));
|
|
1161
|
+
// `timestamptz` (1184) and `timestamptz[]` (1185). SPEED ONLY: an offset-
|
|
1162
|
+
// carrying value has exactly one instant, and this reads it as the same
|
|
1163
|
+
// instant `postgres-date` does, so nothing here changes what a column means.
|
|
1164
|
+
//
|
|
1165
|
+
// Two things about it are deliberate and neither is obvious.
|
|
1166
|
+
//
|
|
1167
|
+
// It is gated behind `utcTimestamps` along with the other four, even though
|
|
1168
|
+
// that flag is about a READING and this is not. The alternative was a second
|
|
1169
|
+
// registration site outside this function, and one process-global parser
|
|
1170
|
+
// table with two places that write to it is the exact shape the "ONE place"
|
|
1171
|
+
// rule above exists to prevent. So the flag reads as "leave pg's temporal
|
|
1172
|
+
// parser table alone", and `utcTimestamps: false` costs the optimisation as
|
|
1173
|
+
// well as the UTC reading. That is a documented cost, not an oversight.
|
|
1174
|
+
//
|
|
1175
|
+
// And it DECLINES rather than overwrites (see
|
|
1176
|
+
// {@link registerFastTemporalParserIfDefault}), which is the opposite of what
|
|
1177
|
+
// the four above do. They MUST overwrite: they exist to replace a reading,
|
|
1178
|
+
// and a process where half the temporal columns read local and half read UTC
|
|
1179
|
+
// is broken. This one exists only to be faster, so a caller who installed
|
|
1180
|
+
// their own `timestamptz` parser (to get strings, or Luxon objects, or a
|
|
1181
|
+
// Temporal instant) keeps it. Overwriting them would trade their correctness
|
|
1182
|
+
// for our speed, which is never the right trade, and Turbine has never
|
|
1183
|
+
// touched 1184 before now, so declining is also what preserves that.
|
|
1184
|
+
const parseTimestamptz = registerFastTemporalParserIfDefault(1184, createFastTimestamptzParser);
|
|
1185
|
+
if (parseTimestamptz) {
|
|
1186
|
+
registerFastTemporalParserIfDefault(1185, () => createPgArrayParser(parseTimestamptz));
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Install a SPEED-ONLY parser for `oid`, but only over pg's own default (or
|
|
1191
|
+
* over a parser Turbine itself installed earlier in this process).
|
|
1192
|
+
*
|
|
1193
|
+
* Returns the installed parser, or `undefined` when it declined, so a caller
|
|
1194
|
+
* can hold a scalar and its array form to the same decision: registering the
|
|
1195
|
+
* array half over a caller's customized scalar half would make the two
|
|
1196
|
+
* disagree, which is worse than leaving both slow.
|
|
1197
|
+
*
|
|
1198
|
+
* `build` receives the parser being replaced, which becomes the fast path's
|
|
1199
|
+
* fallback. That is only sound because the parser being replaced is known to
|
|
1200
|
+
* be pg's default (or Turbine's own wrapper around it); the check below is
|
|
1201
|
+
* what makes it so, and is not an optimisation of it.
|
|
1202
|
+
*/
|
|
1203
|
+
function registerFastTemporalParserIfDefault(oid, build) {
|
|
1204
|
+
const getParser = pg.types.getTypeParser;
|
|
1205
|
+
const setParser = pg.types.setTypeParser;
|
|
1206
|
+
const current = getParser(oid, 'text');
|
|
1207
|
+
const isTurbines = current[TURBINE_PARSER] === true;
|
|
1208
|
+
if (!isTurbines && !isDefaultTextParser(oid, current))
|
|
1209
|
+
return undefined;
|
|
1210
|
+
const parser = markTurbineParser(build(current));
|
|
1211
|
+
setParser(oid, parser);
|
|
1212
|
+
return parser;
|
|
817
1213
|
}
|
|
818
1214
|
// ---------------------------------------------------------------------------
|
|
819
1215
|
// JSON-wire value coercion (relationLoadStrategy: 'join')
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { ValidationError } from '../errors.js';
|
|
37
37
|
import { findArrayUniqueKey, findJsonUniqueKey, fingerprintArrayFilterShape, fingerprintJsonFilterShape, fingerprintOperatorShape, isArrayFilter, isJsonFilter, isTextSearchFilter, isUnmatchedPlainObject, isVectorFilter, isWhereOperator, sortedKeys, VECTOR_DISTANCE_COMPARATORS, } from './filters.js';
|
|
38
|
-
import {
|
|
38
|
+
import { resolveRelation } from './utils.js';
|
|
39
39
|
/**
|
|
40
40
|
* Maximum nesting of `OR` / `AND` / `NOT` combinators and relation-filter
|
|
41
41
|
* descents in one WHERE clause (and in one groupBy `HAVING`).
|
|
@@ -113,11 +113,16 @@ export function walkWhere(host, where) {
|
|
|
113
113
|
events.push({ kind: 'not', condition: value });
|
|
114
114
|
continue;
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
// Resolved rather than looked up, so a relation filter accepts the
|
|
117
|
+
// snake_case spelling of the relation exactly as `with` does. This is the
|
|
118
|
+
// ONE branch authority (build, fingerprint and param-collect all consume
|
|
119
|
+
// these events), so resolving here cannot drift between them; the emitted
|
|
120
|
+
// event carries the DECLARED name, which is what reaches the fingerprint.
|
|
121
|
+
const resolvedRel = resolveRelation(host.tableMeta.relations, key);
|
|
122
|
+
if (resolvedRel && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
123
|
+
const filterObj = host.normalizeRelationFilter(resolvedRel.def, value);
|
|
119
124
|
if (isRelationFilterObj(filterObj)) {
|
|
120
|
-
events.push({ kind: 'relation', key, relDef, filterObj });
|
|
125
|
+
events.push({ kind: 'relation', key: resolvedRel.name, relDef: resolvedRel.def, filterObj });
|
|
121
126
|
continue;
|
|
122
127
|
}
|
|
123
128
|
}
|
package/dist/query/where.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { getErrorMessageMode, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake, normalizeKeyColumns } from '../schema.js';
|
|
15
15
|
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_FILTER_KEYS, JSON_RANGE_OPERATORS, JSON_STRING_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
16
|
-
import { coerceTemporalValue, escapeLike, isInternalCombinator, markInternalCombinator, OPERATOR_KEYS, ownLookup, } from './utils.js';
|
|
16
|
+
import { coerceTemporalValue, escapeLike, isInternalCombinator, markInternalCombinator, OPERATOR_KEYS, ownLookup, resolveColumnName, } from './utils.js';
|
|
17
17
|
import { assertWhereDepth, classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
|
|
18
18
|
/**
|
|
19
19
|
* LIKE-escape one bound operand through the ACTIVE dialect, falling back to the
|
|
@@ -745,8 +745,8 @@ export function buildScopedWhere(qi, scope, where, params, depth = 0) {
|
|
|
745
745
|
*/
|
|
746
746
|
export function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
747
747
|
const meta = scope.meta;
|
|
748
|
-
const col =
|
|
749
|
-
if (
|
|
748
|
+
const col = resolveColumnName(meta, field);
|
|
749
|
+
if (col === undefined)
|
|
750
750
|
throw scope.unknownColumn(field);
|
|
751
751
|
const qCol = `${scope.qualifier}${qi.q(col)}`;
|
|
752
752
|
if (value === null) {
|
|
@@ -824,7 +824,11 @@ export function collectScopedScalarParams(qi, scope, field, value, params) {
|
|
|
824
824
|
if (value === null)
|
|
825
825
|
return;
|
|
826
826
|
const meta = scope.meta;
|
|
827
|
-
|
|
827
|
+
// Unvalidated on purpose: this is the cache-HIT mirror, and a key that does
|
|
828
|
+
// not resolve could never have produced the entry being served. It still
|
|
829
|
+
// goes through the one authority, so the column it binds against cannot
|
|
830
|
+
// differ from the one the build path emitted.
|
|
831
|
+
const col = resolveColumnName(meta, field) ?? camelToSnake(field);
|
|
828
832
|
if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
|
|
829
833
|
const colType = pgTypeForColumn(qi, meta, col);
|
|
830
834
|
if (isJsonColumnType(qi, colType)) {
|
|
@@ -1164,8 +1168,8 @@ export function resolveColumnRef(_qi, ref, ctx, mode) {
|
|
|
1164
1168
|
`Case-insensitive column-to-column comparison is not supported: use client.sql\`...\` ` +
|
|
1165
1169
|
`for lower(a) = lower(b).`);
|
|
1166
1170
|
}
|
|
1167
|
-
const col =
|
|
1168
|
-
if (
|
|
1171
|
+
const col = resolveColumnName(ctx.meta, ref.col);
|
|
1172
|
+
if (col === undefined) {
|
|
1169
1173
|
throw new ValidationError(`[turbine] Unknown field "${ref.col}" referenced by { col } in where on table "${ctx.table}". ` +
|
|
1170
1174
|
`Known fields: ${Object.keys(ctx.meta.columnMap).join(', ') || '(none)'}.`);
|
|
1171
1175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.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).",
|
|
@@ -122,10 +122,12 @@
|
|
|
122
122
|
"dogfood": "tsx examples/dogfood.ts",
|
|
123
123
|
"test": "tsx --test --test-concurrency=1 src/test/*.test.ts",
|
|
124
124
|
"test:unit": "DATABASE_URL= tsx --test src/test/*.test.ts",
|
|
125
|
-
"test:coverage": "c8
|
|
125
|
+
"//test:coverage": "Collection and reporting are SEPARATE, and the report is not c8's. c8 merges every process's V8 coverage into one ProcessCov before converting to istanbul, and that merge is not monotonic on a run this size: measured on this suite, query/relations.ts read 3043 covered lines at 240 process files and 1333 at 359, and a 448-test SUBSET read 91.88% where the full 5,819-test superset read 41.78%. scripts/coverage-report.mjs keeps every other c8 behaviour (same .c8rc.json, same reporters, same checkCoverages gate) and only moves the merge to the istanbul level, where it is additive and cannot go backwards. Collection is a bare NODE_V8_COVERAGE, which is exactly what the c8 wrapper set; dropping the wrapper also drops the broken merge from the critical path instead of running it and discarding the result.",
|
|
126
|
+
"test:coverage": "rm -rf coverage/tmp && mkdir -p coverage/tmp && NODE_V8_COVERAGE=coverage/tmp tsx --test --test-concurrency=1 src/test/*.test.ts && node scripts/coverage-report.mjs",
|
|
126
127
|
"//coverage:cli": "The CLI coverage gate, split into ONE collection run plus a per-file threshold check for every file in it, and an aggregate. c8 enforces a single threshold set per invocation and its --per-file applies the SAME numbers to every file, neither of which can express 'destructive.ts holds 100 while migrate.ts holds 70'. An aggregate-only floor lets the least-covered file spend the whole slack the best-covered file earned: at the measured 3623/2951 lines, migrate.ts could fall from 70.3% to 66.6% with the aggregate still green. So each file gets its OWN floor, checked by re-reporting the coverage already on disk (c8 report re-reads ./coverage/tmp, so this costs no extra test run). The aggregate check is kept as well: it catches all three sagging together inside their individual margins. Per-file gates run FIRST because their failure names the file. DATABASE_URL is neutralized on the collection run: these test files include live migration tests that create and drop tables, and this script runs from prepublishOnly.",
|
|
127
128
|
"test:coverage:cli": "npm run coverage:cli:collect && npm run coverage:cli:gate:destructive && npm run coverage:cli:gate:sql-statements && npm run coverage:cli:gate:pii-guard && npm run coverage:cli:gate:error-catalog && npm run coverage:cli:gate:mcp && npm run coverage:cli:gate:compile-query && npm run coverage:cli:gate:studio && npm run coverage:cli:gate:migrate && npm run coverage:cli:gate:aggregate",
|
|
128
|
-
"coverage:cli:collect": "
|
|
129
|
+
"//coverage:cli:collect": "--check-coverage=false is load-bearing, not tidying. c8 reads .c8rc.json for defaults, so this COLLECTION step was silently enforcing the MAIN gate's global thresholds against a src/cli-only file set. That is a gate nobody wrote and nobody wanted, and it went unnoticed only because the main floor was low enough (75) for the CLI aggregate (89.29) to clear it by accident. When the main floors were re-baselined to 93/89/78 (see //merge-bug in .c8rc.json) it started failing here, several steps before the per-file gates that are supposed to decide. Thresholds for these files belong to the coverage:cli:gate:* scripts, which pass their own explicitly.",
|
|
130
|
+
"coverage:cli:collect": "DATABASE_URL= c8 --all --check-coverage=false --reporter text --exclude 'src/test/**' --include src/cli/studio.ts --include src/cli/migrate.ts --include src/cli/destructive.ts --include src/cli/sql-statements.ts --include src/cli/pii-predicate-guard.ts --include src/cli/mcp.ts --include src/cli/compile-query.ts --include src/cli/error-catalog.ts tsx --test src/test/studio-write.test.ts src/test/studio-demo.test.ts src/test/studio.test.ts src/test/studio-security.test.ts src/test/migrate.test.ts src/test/migrate-deploy.test.ts src/test/migrate-smoke-fixes.test.ts src/test/destructive-migrations.test.ts src/test/backfill-recipe.test.ts src/test/cli.test.ts src/test/cli-diff-migration.test.ts src/test/cli-flags.test.ts src/test/cli-first-run.test.ts src/test/mcp.test.ts src/test/mcp-relations.test.ts src/test/mcp-pii.test.ts src/test/mcp-pii-round2.test.ts src/test/mcp-agent-tools.test.ts src/test/mcp-compile-query.test.ts",
|
|
129
131
|
"coverage:cli:gate": "c8 report --all --exclude 'src/test/**' --reporter text --check-coverage",
|
|
130
132
|
"coverage:cli:gate:destructive": "npm run coverage:cli:gate -- --include src/cli/destructive.ts --lines 98 --statements 98 --branches 84 --functions 98",
|
|
131
133
|
"coverage:cli:gate:sql-statements": "npm run coverage:cli:gate -- --include src/cli/sql-statements.ts --lines 100 --statements 100 --branches 98 --functions 100",
|