turbine-orm 0.67.0 → 0.70.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/dist/cjs/cli/error-catalog.d.ts +77 -0
- package/dist/cjs/cli/error-catalog.js +388 -0
- package/dist/cjs/cli/index.js +3 -2
- package/dist/cjs/cli/mcp.d.ts +19 -3
- package/dist/cjs/cli/mcp.js +709 -22
- package/dist/cjs/cli/migrate.d.ts +19 -2
- package/dist/cjs/cli/observe.d.ts +2 -2
- package/dist/cjs/cli/observe.js +20 -2
- package/dist/cjs/cli/pii-predicate-guard.d.ts +6 -2
- package/dist/cjs/cli/pii-predicate-guard.js +6 -2
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/client.d.ts +30 -75
- package/dist/cjs/client.js +31 -11
- package/dist/cjs/introspect.d.ts +113 -17
- package/dist/cjs/introspect.js +229 -33
- package/dist/cjs/pg-types.d.ts +153 -0
- package/dist/cjs/pg-types.js +38 -0
- package/dist/cjs/pipeline.d.ts +3 -3
- package/dist/cjs/query/batched-loader.d.ts +2 -2
- package/dist/cjs/query/builder.d.ts +2 -2
- package/dist/cjs/query/builder.js +10 -1
- package/dist/cjs/query/deferred.d.ts +6 -6
- package/dist/cjs/query/filters.d.ts +13 -7
- package/dist/cjs/query/filters.js +13 -14
- package/dist/cjs/query/where.d.ts +2 -2
- package/dist/cjs/schema-sql.d.ts +18 -0
- package/dist/cjs/schema-sql.js +18 -0
- package/dist/cli/error-catalog.d.ts +77 -0
- package/dist/cli/error-catalog.js +383 -0
- package/dist/cli/index.js +3 -2
- package/dist/cli/mcp.d.ts +19 -3
- package/dist/cli/mcp.js +709 -23
- package/dist/cli/migrate.d.ts +19 -2
- package/dist/cli/observe.d.ts +2 -2
- package/dist/cli/observe.js +20 -2
- package/dist/cli/pii-predicate-guard.d.ts +6 -2
- package/dist/cli/pii-predicate-guard.js +6 -2
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/client.d.ts +30 -75
- package/dist/client.js +31 -11
- package/dist/introspect.d.ts +113 -17
- package/dist/introspect.js +227 -33
- package/dist/pg-types.d.ts +153 -0
- package/dist/pg-types.js +37 -0
- package/dist/pipeline.d.ts +3 -3
- package/dist/query/batched-loader.d.ts +2 -2
- package/dist/query/builder.d.ts +2 -2
- package/dist/query/builder.js +10 -1
- package/dist/query/deferred.d.ts +6 -6
- package/dist/query/filters.d.ts +13 -7
- package/dist/query/filters.js +13 -13
- package/dist/query/where-compile.js +1 -1
- package/dist/query/where.d.ts +2 -2
- package/dist/schema-sql.d.ts +18 -0
- package/dist/schema-sql.js +18 -0
- package/package.json +19 -7
package/dist/cjs/introspect.js
CHANGED
|
@@ -19,6 +19,8 @@ exports.defaultExcludedTablesPresent = defaultExcludedTablesPresent;
|
|
|
19
19
|
exports.introspect = introspect;
|
|
20
20
|
exports.applyRelationRenames = applyRelationRenames;
|
|
21
21
|
exports.introspectPostgresCatalog = introspectPostgresCatalog;
|
|
22
|
+
exports.parseIndexKeyEntries = parseIndexKeyEntries;
|
|
23
|
+
exports.indexKeyColumn = indexKeyColumn;
|
|
22
24
|
exports.parseIndexColumns = parseIndexColumns;
|
|
23
25
|
exports.indexHasWhere = indexHasWhere;
|
|
24
26
|
exports.stripCheckWrapper = stripCheckWrapper;
|
|
@@ -681,40 +683,196 @@ async function introspectPostgresCatalog(options) {
|
|
|
681
683
|
}
|
|
682
684
|
}
|
|
683
685
|
/**
|
|
684
|
-
*
|
|
686
|
+
* The index-definition key-list SCANNER, and the only one that reads an
|
|
687
|
+
* `indexdef` character by character.
|
|
685
688
|
*
|
|
686
|
-
* `indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING
|
|
687
|
-
* (
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
* never mistaken for the column list. The older greedy `/\((.+)\)/` swallowed
|
|
691
|
-
* `) WHERE (` and spliced a raw predicate fragment into the column names, which
|
|
692
|
-
* then leaked into generated compound-unique selector names.
|
|
689
|
+
* `pg_indexes.indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING
|
|
690
|
+
* method (key, ...) [INCLUDE (col, ...)] [WITH (...)] [TABLESPACE ts]
|
|
691
|
+
* [WHERE predicate]`. This returns the raw entries of the KEY LIST only, one per
|
|
692
|
+
* top-level comma, expression entries included and verbatim.
|
|
693
693
|
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
694
|
+
* ## THREE indexdef parsers coexist in this repo. This is one of them.
|
|
695
|
+
*
|
|
696
|
+
* An earlier version of this comment said there was exactly one. There is not,
|
|
697
|
+
* and pretending otherwise is how hand-synced parsers drift here, so each of the
|
|
698
|
+
* three names the other two:
|
|
699
|
+
*
|
|
700
|
+
* 1. THIS scanner (with {@link indexKeyColumn} / {@link parseIndexColumns}).
|
|
701
|
+
* Safe for any `indexdef` pg emits, including expression keys, quoted
|
|
702
|
+
* identifiers holding commas or parens, string literals, INCLUDE lists and
|
|
703
|
+
* partial predicates. Feeds generated metadata, compound-unique selectors,
|
|
704
|
+
* the FK-index advisor and m2m detection, plus `cli/mcp.ts`.
|
|
705
|
+
* 2. {@link parsePlainUniqueIndexColumns}, below. Still the old
|
|
706
|
+
* `USING \w+ \(([^)]*)\)` regex. It answers a NARROWER question ("is this a
|
|
707
|
+
* plain, whole-table unique index over these exact columns") and returns
|
|
708
|
+
* `null` on everything it cannot read, so its regex's known weaknesses cost
|
|
709
|
+
* a missed hasOne flip rather than a wrong answer.
|
|
710
|
+
* 3. `describeIndexDefMismatch` in `schema-sql.ts`. Same old regex, same
|
|
711
|
+
* fail-toward-a-warning posture.
|
|
712
|
+
*
|
|
713
|
+
* Unifying them is a separate change with its own risk: (2) and (3) both treat
|
|
714
|
+
* "cannot read this" as a safe refusal, and swapping in a parser that reads MORE
|
|
715
|
+
* turns some of those refusals into answers. Until then, prefer this scanner for
|
|
716
|
+
* any new caller, and do not assume a fix here reaches the other two.
|
|
717
|
+
*
|
|
718
|
+
* ## Why the mcp.ts copy is gone
|
|
719
|
+
*
|
|
720
|
+
* `cli/mcp.ts` kept its own weaker copy of this, and the two drifted: on
|
|
721
|
+
* `USING btree (id) INCLUDE (email)` the copy answered `['id) INCLUDE (email']`
|
|
722
|
+
* while this one answered `['id']`. Those columns feed `deriveCatalogRelations`,
|
|
723
|
+
* which decides hasOne-vs-hasMany and auto-m2m, so a UNIQUE index with INCLUDE
|
|
724
|
+
* columns was visible to `turbine generate` and invisible to the MCP server, and
|
|
725
|
+
* the two surfaces disagreed about which relations the schema has. The
|
|
726
|
+
* duplication is also what produced the predicate leak that
|
|
727
|
+
* {@link parseIndexColumns}'s own history records. So mcp.ts consumes this
|
|
728
|
+
* function now, and the split between the two callers is expressed as the two
|
|
729
|
+
* exports below rather than as two implementations.
|
|
730
|
+
*
|
|
731
|
+
* ## Why a scanner rather than a regex
|
|
732
|
+
*
|
|
733
|
+
* The regex this replaces (`/USING\s+\w+\s*\(([^)]*)\)/`) stops at the FIRST
|
|
734
|
+
* `)`, which is the wrong paren for any expression key (`lower(email)`), and a
|
|
735
|
+
* plain `.split(',')` cuts `coalesce(a, b)` in half. The scan tracks paren depth
|
|
736
|
+
* and single-quoted literals, so a comma or a paren inside an expression or a
|
|
737
|
+
* literal is not a boundary.
|
|
738
|
+
*
|
|
739
|
+
* The anchor requires `USING <method> (`, which no predicate, INCLUDE list, WITH
|
|
740
|
+
* list or literal can spell, so the key list is found positionally rather than
|
|
741
|
+
* by hoping the first paren is the right one. When the anchor is absent (not a
|
|
742
|
+
* shape pg emits) it falls back to the first parenthesised group, matching the
|
|
743
|
+
* previous behaviour.
|
|
699
744
|
*/
|
|
700
|
-
function
|
|
701
|
-
const
|
|
702
|
-
|
|
745
|
+
function parseIndexKeyEntries(indexdef) {
|
|
746
|
+
const anchor = /USING\s+\w+\s*\(/i.exec(indexdef);
|
|
747
|
+
const open = anchor ? anchor.index + anchor[0].length : indexdef.indexOf('(') + 1;
|
|
748
|
+
if (open === 0)
|
|
703
749
|
return [];
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
750
|
+
const entries = [];
|
|
751
|
+
let depth = 0;
|
|
752
|
+
let inLiteral = false;
|
|
753
|
+
let inQuotedIdent = false;
|
|
754
|
+
let start = open;
|
|
755
|
+
for (let i = open; i < indexdef.length; i++) {
|
|
756
|
+
const char = indexdef[i];
|
|
757
|
+
// A doubled quote inside a literal (or a quoted identifier) toggles twice,
|
|
758
|
+
// which is the same as not toggling, so pg's `''` / `""` escapes need no
|
|
759
|
+
// special case. Identifier quoting is tracked as well as literal quoting: a
|
|
760
|
+
// column named `it's` renders as `"it's"`, and reading its apostrophe as the
|
|
761
|
+
// start of a literal swallows the rest of the definition.
|
|
762
|
+
if (char === "'" && !inQuotedIdent) {
|
|
763
|
+
inLiteral = !inLiteral;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
if (char === '"' && !inLiteral) {
|
|
767
|
+
inQuotedIdent = !inQuotedIdent;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (inLiteral || inQuotedIdent)
|
|
771
|
+
continue;
|
|
772
|
+
if (char === '(')
|
|
773
|
+
depth++;
|
|
774
|
+
else if (char === ')') {
|
|
775
|
+
if (depth === 0) {
|
|
776
|
+
entries.push(indexdef.slice(start, i));
|
|
777
|
+
return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
778
|
+
}
|
|
779
|
+
depth--;
|
|
780
|
+
}
|
|
781
|
+
else if (char === ',' && depth === 0) {
|
|
782
|
+
entries.push(indexdef.slice(start, i));
|
|
783
|
+
start = i + 1;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// Unterminated key list: the definition does not parse, so it names no
|
|
787
|
+
// columns. Never "all of it" - the one input this cannot read must not be the
|
|
788
|
+
// one it forwards.
|
|
789
|
+
return [];
|
|
711
790
|
}
|
|
712
|
-
/**
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
791
|
+
/**
|
|
792
|
+
* The plain COLUMN NAME an index key entry indexes, or `null` when the entry is
|
|
793
|
+
* an expression rather than a column.
|
|
794
|
+
*
|
|
795
|
+
* A key entry is `{ column | (expression) } [COLLATE c] [opclass [(params)]]
|
|
796
|
+
* [ASC|DESC] [NULLS FIRST|LAST]`, so the column is the LEADING token and
|
|
797
|
+
* everything after it is a modifier. Reading it that way is what makes
|
|
798
|
+
* `email COLLATE "C" text_pattern_ops` resolve to `email`; the previous
|
|
799
|
+
* suffix-stripping (`ASC`/`DESC` only) returned the whole entry verbatim as a
|
|
800
|
+
* "column name", which matches no real column.
|
|
801
|
+
*
|
|
802
|
+
* SCOPE OF THAT FIX, stated exactly because an earlier version of this comment
|
|
803
|
+
* overstated it: what changes is what {@link parseIndexColumns} reports, and so
|
|
804
|
+
* what its consumers see. Those are the generated `metadata.ts` index lists,
|
|
805
|
+
* the compound-unique selector derivation, the FK-index advisor's leading-column
|
|
806
|
+
* check, and m2m junction detection. Relation CARDINALITY is NOT among them: the
|
|
807
|
+
* `hasMany`/`hasOne` flip reads {@link parsePlainUniqueIndexColumns}, a separate
|
|
808
|
+
* parser this does not feed, and that one still answers `null` for an opclass'd
|
|
809
|
+
* UNIQUE index exactly as it did before.
|
|
810
|
+
*
|
|
811
|
+
* The name is de-quoted (Postgres quotes non-lowercase identifiers such as a
|
|
812
|
+
* Prisma implicit m2m junction's `"A"` / `"B"`) so it matches the unquoted names
|
|
813
|
+
* carried elsewhere in the metadata.
|
|
814
|
+
*/
|
|
815
|
+
function indexKeyColumn(entry) {
|
|
816
|
+
const trimmed = entry.trim();
|
|
817
|
+
if (trimmed.length === 0)
|
|
818
|
+
return null;
|
|
819
|
+
if (trimmed.startsWith('"')) {
|
|
820
|
+
// A quoted identifier ends at the first unpaired `"`; anything after it is a
|
|
821
|
+
// modifier. `""` inside is one escaped quote and does not end it.
|
|
822
|
+
let i = 1;
|
|
823
|
+
let name = '';
|
|
824
|
+
while (i < trimmed.length) {
|
|
825
|
+
if (trimmed[i] === '"') {
|
|
826
|
+
if (trimmed[i + 1] === '"') {
|
|
827
|
+
name += '"';
|
|
828
|
+
i += 2;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
// The SAME trailing-modifier rule the unquoted branch applies below,
|
|
832
|
+
// and for the same reason: what follows the name must be whitespace
|
|
833
|
+
// (a COLLATE clause, an opclass, ASC/DESC/NULLS) or nothing. Returning
|
|
834
|
+
// at the closing quote without asking read `"MyFunc"(email)` as a
|
|
835
|
+
// column named `MyFunc`, synthesizing a column that does not exist and
|
|
836
|
+
// handing it to generated metadata, the FK-advisor lead-column check,
|
|
837
|
+
// compound-unique selectors and m2m junction detection alike.
|
|
838
|
+
const rest = trimmed.slice(i + 1);
|
|
839
|
+
if (rest.length > 0 && !/^\s/.test(rest))
|
|
840
|
+
return null;
|
|
841
|
+
return name;
|
|
842
|
+
}
|
|
843
|
+
name += trimmed[i];
|
|
844
|
+
i++;
|
|
845
|
+
}
|
|
846
|
+
return null; // unterminated quote: not a name this can vouch for
|
|
716
847
|
}
|
|
717
|
-
|
|
848
|
+
// An expression key is anything that is not a bare leading identifier, which
|
|
849
|
+
// includes every parenthesised or operator-bearing form pg renders.
|
|
850
|
+
// Non-ASCII letters are identifier characters to Postgres and are left
|
|
851
|
+
// unquoted in `indexdef`, so the class has to admit them or a `café` column
|
|
852
|
+
// reads as an expression and disappears.
|
|
853
|
+
const leading = /^[A-Za-z_\u0080-\uFFFF][A-Za-z0-9_$\u0080-\uFFFF]*/.exec(trimmed);
|
|
854
|
+
if (!leading)
|
|
855
|
+
return null;
|
|
856
|
+
const rest = trimmed.slice(leading[0].length);
|
|
857
|
+
// The rest must be modifiers (whitespace-separated words / quoted collation /
|
|
858
|
+
// opclass parameters), never a continuation of an expression: `lower(email)`
|
|
859
|
+
// has a leading identifier too, and it is not a column.
|
|
860
|
+
if (rest.length > 0 && !/^\s/.test(rest))
|
|
861
|
+
return null;
|
|
862
|
+
return leading[0];
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Parse the indexed COLUMN names out of a `pg_indexes.indexdef` string.
|
|
866
|
+
*
|
|
867
|
+
* Expression keys are dropped, conservatively: a functional index does not name
|
|
868
|
+
* a plain column, and generated metadata has nowhere to say "there was a key
|
|
869
|
+
* here that is not a column". {@link parseIndexKeyEntries} is the variant that
|
|
870
|
+
* keeps them, for the one caller (`cli/mcp.ts`) that reports their presence.
|
|
871
|
+
*/
|
|
872
|
+
function parseIndexColumns(indexdef) {
|
|
873
|
+
return parseIndexKeyEntries(indexdef)
|
|
874
|
+
.map(indexKeyColumn)
|
|
875
|
+
.filter((column) => column !== null);
|
|
718
876
|
}
|
|
719
877
|
/**
|
|
720
878
|
* Whether an `indexdef` carries a top-level `WHERE` predicate (a PARTIAL index).
|
|
@@ -806,10 +964,29 @@ function columnSetsEqual(a, b) {
|
|
|
806
964
|
* expression, not the raw FK column set.
|
|
807
965
|
*
|
|
808
966
|
* Anchors on the `USING <method> (` clause the same way
|
|
809
|
-
*
|
|
810
|
-
* parentheses are never mistaken for the column list. Every column
|
|
811
|
-
* a bare or double-quoted identifier; anything else (a function
|
|
812
|
-
* operator expression) fails the check and yields `null`.
|
|
967
|
+
* `describeIndexDefMismatch` (schema-sql.ts) does, so a partial index's
|
|
968
|
+
* `WHERE (...)` parentheses are never mistaken for the column list. Every column
|
|
969
|
+
* token must be a bare or double-quoted identifier; anything else (a function
|
|
970
|
+
* call, an operator expression) fails the check and yields `null`.
|
|
971
|
+
*
|
|
972
|
+
* ## Parser 2 of 3, and what it is safe for
|
|
973
|
+
*
|
|
974
|
+
* This is the second of the three indexdef parsers catalogued on
|
|
975
|
+
* {@link parseIndexKeyEntries}; the third is `describeIndexDefMismatch` in
|
|
976
|
+
* schema-sql.ts. It is still the `USING \w+ \(([^)]*)\)` regex that the scanner
|
|
977
|
+
* up there was written to replace, so it inherits that regex's weaknesses: it
|
|
978
|
+
* stops at the FIRST `)`, and it splits on every comma. On an expression key
|
|
979
|
+
* (`lower(email)`), on a quoted identifier containing a comma or a paren, and on
|
|
980
|
+
* an opclass'd key (`email text_pattern_ops`) it therefore reads a token that is
|
|
981
|
+
* not a bare identifier and returns `null`.
|
|
982
|
+
*
|
|
983
|
+
* That is SAFE HERE and only here, because `null` is this function's "I cannot
|
|
984
|
+
* vouch for this index" answer and its single consumer
|
|
985
|
+
* ({@link detectUniqueForeignKeySets}) treats it as "this index does not prove
|
|
986
|
+
* uniqueness". The cost of every misread is a relation left as `hasMany` that
|
|
987
|
+
* could have been `hasOne`, never a uniqueness claim the database does not back.
|
|
988
|
+
* Do NOT reuse it anywhere a wrong-but-plausible column list would be acted on;
|
|
989
|
+
* use the scanner for that.
|
|
813
990
|
*/
|
|
814
991
|
function parsePlainUniqueIndexColumns(indexdef) {
|
|
815
992
|
// Partial index: uniqueness is scoped to the WHERE predicate.
|
|
@@ -1233,9 +1410,28 @@ function deriveCatalogRelations(inputs) {
|
|
|
1233
1410
|
// Prisma's implicit m2m junctions have no primary key (just a two-column
|
|
1234
1411
|
// UNIQUE index over the FK columns), so pass the introspected two-column
|
|
1235
1412
|
// unique indexes as the fallback junction-key source.
|
|
1413
|
+
//
|
|
1414
|
+
// `idx.columns` is the key list with EXPRESSION keys already dropped, so its
|
|
1415
|
+
// length is not the index's arity and cannot stand in for it. On
|
|
1416
|
+
// `UNIQUE (a, lower(b), c)` it reads `['a', 'c']`, which looks exactly like a
|
|
1417
|
+
// two-column junction key while the pair `(a, c)` is not unique at all, only
|
|
1418
|
+
// `(a, lower(b), c)` is. A manyToMany derived from it returns DUPLICATE ROWS.
|
|
1419
|
+
// So the arity is re-read from the raw definition and the two must agree:
|
|
1420
|
+
// an index with any expression key is not a junction key this can vouch for.
|
|
1421
|
+
// A PARTIAL unique index is refused for the same reason and is the MAINSTREAM
|
|
1422
|
+
// shape of it: `UNIQUE (post_id, tag_id) WHERE deleted_at IS NULL` on a
|
|
1423
|
+
// soft-deleted junction guarantees uniqueness only over the rows matching the
|
|
1424
|
+
// predicate, so the pair can repeat across the whole table and the derived
|
|
1425
|
+
// manyToMany returns duplicate rows. `IndexMetadata.partial` already exists and
|
|
1426
|
+
// already documents this, and the hasOne path already honours it (see
|
|
1427
|
+
// parsePlainUniqueIndexColumns, which refuses a definition carrying a WHERE).
|
|
1428
|
+
// This filter was simply the one place that did not read it, so the two
|
|
1429
|
+
// cardinality paths disagreed about whether the same index proved uniqueness.
|
|
1236
1430
|
const uniqueIndexColsByTable = new Map();
|
|
1237
1431
|
for (const [tbl, idxs] of indexesByTable) {
|
|
1238
|
-
const twoColUniques = idxs
|
|
1432
|
+
const twoColUniques = idxs
|
|
1433
|
+
.filter((idx) => idx.unique && !idx.partial && idx.columns.length === 2 && parseIndexKeyEntries(idx.definition).length === 2)
|
|
1434
|
+
.map((idx) => idx.columns);
|
|
1239
1435
|
if (twoColUniques.length > 0)
|
|
1240
1436
|
uniqueIndexColsByTable.set(tbl, twoColUniques);
|
|
1241
1437
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm, the pg-compatible driver contract
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS MODULE EXISTS, AND WHY IT IMPORTS NOTHING. The `pg` package ships
|
|
5
|
+
* no type declarations of its own, so every `@types/pg` name that reaches an
|
|
6
|
+
* EXPORTED declaration makes TypeScript emit an import of the pg module into
|
|
7
|
+
* the published `.d.ts`, which turns `@types/pg` into a hard requirement for
|
|
8
|
+
* any consumer compiling under `strict` - a runtime-free types package that
|
|
9
|
+
* nonetheless has to sit in `dependencies` for consumer builds to typecheck.
|
|
10
|
+
* That is the v0.28.1 regression: `@types/pg` was moved to `devDependencies`
|
|
11
|
+
* while the declaration surface still named `pg.Pool` / `pg.PoolClient` /
|
|
12
|
+
* `pg.QueryResult`, and consumer `tsc` broke. The order matters and it is
|
|
13
|
+
* one-way: clear the declaration surface FIRST, then move the dependency.
|
|
14
|
+
* The published declarations are checked for pg references on every release,
|
|
15
|
+
* so the surface cannot silently grow one back.
|
|
16
|
+
*
|
|
17
|
+
* So these are the pg shapes, declared natively. They are not a new parallel
|
|
18
|
+
* abstraction: `PgCompatPool` / `PgCompatPoolClient` / `PgCompatQueryResult`
|
|
19
|
+
* are the SAME interfaces the external-pool seam has used since the serverless
|
|
20
|
+
* binding shipped (`TurbineConfig.pool`, `turbineHttp`, and the `SqlitePool` /
|
|
21
|
+
* `MysqlPool` / `MssqlPool` / `PowdbPool` engine shims), lifted out of
|
|
22
|
+
* client.ts so that `query/` can name them without an import edge back to the
|
|
23
|
+
* client (see `scripts/check-import-cycles.mjs`). client.ts re-exports all of
|
|
24
|
+
* them, so every existing `from './client.js'` import path is unchanged.
|
|
25
|
+
*
|
|
26
|
+
* A genuine `pg.Pool`, `pg.PoolClient` and `pg.QueryResult` each satisfy the
|
|
27
|
+
* matching interface here structurally, which is what keeps `pg` usable at
|
|
28
|
+
* every position Turbine accepts one. The reverse is NOT true and never was:
|
|
29
|
+
* an HTTP driver's pool is not a `pg.Pool`, which is why the fields pg alone
|
|
30
|
+
* provides (`totalCount`, the `error` event) are optional.
|
|
31
|
+
*
|
|
32
|
+
* `pg` itself remains a real runtime dependency. This module is about the
|
|
33
|
+
* TYPE surface only.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* One column of a result's row description. `rows` is what Turbine reads; a
|
|
39
|
+
* driver that describes its columns supplies these too.
|
|
40
|
+
*
|
|
41
|
+
* `name` and `dataTypeID` are required because a driver that reports fields at
|
|
42
|
+
* all reports those two. The rest are pg's, optional so that a genuine
|
|
43
|
+
* `pg.FieldDef` object LITERAL is accepted (excess-property checking makes
|
|
44
|
+
* literal-level compatibility a real thing callers hit when they hand-write a
|
|
45
|
+
* fake result), and so that a driver reporting only the two is accepted as
|
|
46
|
+
* well.
|
|
47
|
+
*/
|
|
48
|
+
export interface PgCompatFieldDef {
|
|
49
|
+
name: string;
|
|
50
|
+
dataTypeID: number;
|
|
51
|
+
tableID?: number;
|
|
52
|
+
columnID?: number;
|
|
53
|
+
dataTypeSize?: number;
|
|
54
|
+
dataTypeModifier?: number;
|
|
55
|
+
format?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Minimal pg-compatible query result.
|
|
59
|
+
* `pg.Pool`, `@neondatabase/serverless` Pool, `@vercel/postgres` Pool and
|
|
60
|
+
* any driver speaking the node-postgres API all satisfy this shape.
|
|
61
|
+
*
|
|
62
|
+
* `rows` and `rowCount` are the contract: they are the only two members
|
|
63
|
+
* Turbine reads, and the two every supported driver produces. `command` /
|
|
64
|
+
* `oid` / `fields` are pg's and therefore OPTIONAL, which is what makes this
|
|
65
|
+
* interface a structural supertype of `pg.QueryResult` rather than a
|
|
66
|
+
* lookalike. That direction is the one that matters, since this type appears
|
|
67
|
+
* in PARAMETER position throughout the public surface
|
|
68
|
+
* ({@link PgCompatPool.query}'s result, `DeferredQuery.transform`'s argument):
|
|
69
|
+
* whatever pg hands back must be accepted, while the engine shims that return
|
|
70
|
+
* only `{ rows, rowCount }` must be accepted too.
|
|
71
|
+
*/
|
|
72
|
+
export interface PgCompatQueryResult<R = Record<string, unknown>> {
|
|
73
|
+
rows: R[];
|
|
74
|
+
rowCount: number | null;
|
|
75
|
+
fields?: PgCompatFieldDef[];
|
|
76
|
+
/** pg-family drivers: the completed command tag (`SELECT`, `INSERT`, ...). */
|
|
77
|
+
command?: string;
|
|
78
|
+
/** pg-family drivers: the legacy inserted-row OID, `0` for everything else. */
|
|
79
|
+
oid?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The object form of a query, `{ name, text, values }`, which node-postgres
|
|
83
|
+
* accepts in place of `(text, values)` in order to name a prepared statement.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately NOT a second overload on {@link PgCompatPool.query}. The engine
|
|
86
|
+
* pool shims implement that interface and speak only the two-argument form, so
|
|
87
|
+
* requiring the object form of every implementer would be a contract they
|
|
88
|
+
* cannot meet. Named prepared statements are a Postgres-path optimization, and
|
|
89
|
+
* the one call site that uses them casts to this shape.
|
|
90
|
+
*/
|
|
91
|
+
export interface PgCompatQueryConfig {
|
|
92
|
+
/** Prepared-statement name. Omit for an unnamed (re-parsed) statement. */
|
|
93
|
+
name?: string;
|
|
94
|
+
text: string;
|
|
95
|
+
values?: unknown[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Minimal pg-compatible client used by TurbineClient for transactions.
|
|
99
|
+
* `pg.PoolClient` satisfies this; so do Neon and Vercel's equivalents.
|
|
100
|
+
*/
|
|
101
|
+
export interface PgCompatPoolClient {
|
|
102
|
+
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
103
|
+
release(err?: Error | boolean): void;
|
|
104
|
+
/**
|
|
105
|
+
* Optional driver capability: `true` when `query()` may be called again on
|
|
106
|
+
* this connection while earlier calls are still in flight, with replies
|
|
107
|
+
* delivered to callers in FIFO submission order. Drivers that set this let
|
|
108
|
+
* the batch `$transaction([...])` overload dispatch every statement in one
|
|
109
|
+
* write burst (~1 network round trip plus server time) instead of awaiting
|
|
110
|
+
* each reply before sending the next (N round trips). Leave unset for
|
|
111
|
+
* drivers (node-postgres included) whose batch path must stay strictly
|
|
112
|
+
* sequential.
|
|
113
|
+
*/
|
|
114
|
+
readonly supportsPipelining?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Optional engine seam: scope a transaction's user callback to its own
|
|
117
|
+
* async subtree. When present, `TurbineClient.transaction` / `$transaction`
|
|
118
|
+
* invoke the callback as `wrapTransactionCallback(() => fn(tx))` instead of
|
|
119
|
+
* `fn(tx)` directly. Single-writer engines (PowDB) implement it with
|
|
120
|
+
* `AsyncLocalStorage.run()` to plant their re-entrancy marker so that it
|
|
121
|
+
* exists ONLY inside the callback's async subtree: a transaction opened
|
|
122
|
+
* from inside the callback is detected as re-entrant (typed E017), while
|
|
123
|
+
* the CALLER's context stays unmarked, so same-tick sibling transactions
|
|
124
|
+
* queue FIFO instead of being falsely flagged. Absent on pg and every other
|
|
125
|
+
* engine, in which case the callback runs unwrapped (zero behavior change).
|
|
126
|
+
*/
|
|
127
|
+
wrapTransactionCallback?<R>(fn: () => Promise<R>): Promise<R>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Minimal pg-compatible pool. Pass any driver that satisfies this interface
|
|
131
|
+
* via `TurbineConfig.pool`, lets Turbine run on Neon HTTP, Vercel Postgres,
|
|
132
|
+
* Cloudflare Hyperdrive, or any other serverless Postgres driver.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* import { Pool } from '@neondatabase/serverless';
|
|
137
|
+
* import { TurbineClient } from 'turbine-orm';
|
|
138
|
+
*
|
|
139
|
+
* const neonPool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
140
|
+
* const db = new TurbineClient({ pool: neonPool }, schema);
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export interface PgCompatPool {
|
|
144
|
+
query<R = Record<string, unknown>>(text: string, values?: unknown[]): Promise<PgCompatQueryResult<R>>;
|
|
145
|
+
connect(): Promise<PgCompatPoolClient>;
|
|
146
|
+
end(): Promise<void>;
|
|
147
|
+
/** Optional, pools that expose stats (pg.Pool does; Neon HTTP does not) */
|
|
148
|
+
readonly totalCount?: number;
|
|
149
|
+
readonly idleCount?: number;
|
|
150
|
+
readonly waitingCount?: number;
|
|
151
|
+
/** Optional, pg.Pool supports 'error' event; HTTP drivers typically do not */
|
|
152
|
+
on?(event: 'error', listener: (err: Error) => void): this;
|
|
153
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm, the pg-compatible driver contract
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS MODULE EXISTS, AND WHY IT IMPORTS NOTHING. The `pg` package ships
|
|
6
|
+
* no type declarations of its own, so every `@types/pg` name that reaches an
|
|
7
|
+
* EXPORTED declaration makes TypeScript emit an import of the pg module into
|
|
8
|
+
* the published `.d.ts`, which turns `@types/pg` into a hard requirement for
|
|
9
|
+
* any consumer compiling under `strict` - a runtime-free types package that
|
|
10
|
+
* nonetheless has to sit in `dependencies` for consumer builds to typecheck.
|
|
11
|
+
* That is the v0.28.1 regression: `@types/pg` was moved to `devDependencies`
|
|
12
|
+
* while the declaration surface still named `pg.Pool` / `pg.PoolClient` /
|
|
13
|
+
* `pg.QueryResult`, and consumer `tsc` broke. The order matters and it is
|
|
14
|
+
* one-way: clear the declaration surface FIRST, then move the dependency.
|
|
15
|
+
* The published declarations are checked for pg references on every release,
|
|
16
|
+
* so the surface cannot silently grow one back.
|
|
17
|
+
*
|
|
18
|
+
* So these are the pg shapes, declared natively. They are not a new parallel
|
|
19
|
+
* abstraction: `PgCompatPool` / `PgCompatPoolClient` / `PgCompatQueryResult`
|
|
20
|
+
* are the SAME interfaces the external-pool seam has used since the serverless
|
|
21
|
+
* binding shipped (`TurbineConfig.pool`, `turbineHttp`, and the `SqlitePool` /
|
|
22
|
+
* `MysqlPool` / `MssqlPool` / `PowdbPool` engine shims), lifted out of
|
|
23
|
+
* client.ts so that `query/` can name them without an import edge back to the
|
|
24
|
+
* client (see `scripts/check-import-cycles.mjs`). client.ts re-exports all of
|
|
25
|
+
* them, so every existing `from './client.js'` import path is unchanged.
|
|
26
|
+
*
|
|
27
|
+
* A genuine `pg.Pool`, `pg.PoolClient` and `pg.QueryResult` each satisfy the
|
|
28
|
+
* matching interface here structurally, which is what keeps `pg` usable at
|
|
29
|
+
* every position Turbine accepts one. The reverse is NOT true and never was:
|
|
30
|
+
* an HTTP driver's pool is not a `pg.Pool`, which is why the fields pg alone
|
|
31
|
+
* provides (`totalCount`, the `error` event) are optional.
|
|
32
|
+
*
|
|
33
|
+
* `pg` itself remains a real runtime dependency. This module is about the
|
|
34
|
+
* TYPE surface only.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/cjs/pipeline.d.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* Sequential fallback covers HTTP-based drivers (Neon HTTP, Vercel Postgres, Cloudflare
|
|
19
19
|
* Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
|
|
20
20
|
*/
|
|
21
|
-
import type
|
|
21
|
+
import type { PgCompatPool } from './pg-types.js';
|
|
22
22
|
import type { DeferredQuery } from './query/index.js';
|
|
23
23
|
export interface PipelineOptions {
|
|
24
24
|
/**
|
|
@@ -52,7 +52,7 @@ export interface PipelineOptions {
|
|
|
52
52
|
* ]);
|
|
53
53
|
* ```
|
|
54
54
|
*/
|
|
55
|
-
export declare function executePipeline<T extends readonly DeferredQuery<unknown>[]>(pool:
|
|
55
|
+
export declare function executePipeline<T extends readonly DeferredQuery<unknown>[]>(pool: PgCompatPool, queries: T, options?: PipelineOptions): Promise<PipelineResults<T>>;
|
|
56
56
|
/**
|
|
57
57
|
* Check whether a pool supports the real pipeline protocol.
|
|
58
58
|
* Call this to determine at runtime whether pipelines will use the fast path
|
|
@@ -60,7 +60,7 @@ export declare function executePipeline<T extends readonly DeferredQuery<unknown
|
|
|
60
60
|
*
|
|
61
61
|
* Note: This acquires and immediately releases a connection to inspect it.
|
|
62
62
|
*/
|
|
63
|
-
export declare function pipelineSupported(pool:
|
|
63
|
+
export declare function pipelineSupported(pool: PgCompatPool): Promise<boolean>;
|
|
64
64
|
/**
|
|
65
65
|
* Extract the result types from a tuple of DeferredQuery objects.
|
|
66
66
|
* If you pass [DeferredQuery<User>, DeferredQuery<number>, DeferredQuery<Post[]>],
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
*
|
|
54
54
|
* @module
|
|
55
55
|
*/
|
|
56
|
-
import type pg from 'pg';
|
|
57
56
|
import type { PartitionLimitInput } from '../dialect.js';
|
|
57
|
+
import type { PgCompatQueryResult } from '../pg-types.js';
|
|
58
58
|
import { type RelationDef, type SchemaMetadata, type TableMetadata } from '../schema.js';
|
|
59
59
|
import type { ReselectExecutor } from './builder.js';
|
|
60
60
|
import type { SkipGlobalFilters, Unsafe, WithClause, WithCount } from './types.js';
|
|
@@ -66,7 +66,7 @@ interface Deferred {
|
|
|
66
66
|
sql: string;
|
|
67
67
|
params: unknown[];
|
|
68
68
|
preparedName?: string;
|
|
69
|
-
transform: (result:
|
|
69
|
+
transform: (result: PgCompatQueryResult) => unknown;
|
|
70
70
|
}
|
|
71
71
|
/**
|
|
72
72
|
* The read surface the loader needs from a child QueryInterface: build (but do
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* Schema-driven: all column names, types, and relations come from introspected
|
|
11
11
|
* metadata, nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
|
-
import type
|
|
13
|
+
import type { PgCompatPool } from '../pg-types.js';
|
|
14
14
|
import type { SchemaMetadata } from '../schema.js';
|
|
15
15
|
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
|
|
16
16
|
/**
|
|
@@ -381,7 +381,7 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
381
381
|
* class-resident primitives they need without widening the public surface.
|
|
382
382
|
*/
|
|
383
383
|
private readonly ctx;
|
|
384
|
-
constructor(pool:
|
|
384
|
+
constructor(pool: PgCompatPool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
385
385
|
/**
|
|
386
386
|
* Dev-only, once per table: the columns whose database type is absent from
|
|
387
387
|
* BOTH the column entry and the table-level type maps, the residual case
|
|
@@ -1935,9 +1935,18 @@ class QueryInterface {
|
|
|
1935
1935
|
const action = this.currentAction;
|
|
1936
1936
|
// Build the query argument, use object form with `name` for prepared
|
|
1937
1937
|
// statements, or the plain (text, values) form otherwise.
|
|
1938
|
+
//
|
|
1939
|
+
// The object form is not part of the PgCompatPool contract (the engine
|
|
1940
|
+
// pool shims only speak `(text, values)`), so it is reached through a
|
|
1941
|
+
// cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
|
|
1942
|
+
// drivers that accept it turn on.
|
|
1938
1943
|
const usePrepared = preparedName && this.preparedStatementsEnabled;
|
|
1939
1944
|
const exec = usePrepared
|
|
1940
|
-
? this.pool.query({
|
|
1945
|
+
? this.pool.query({
|
|
1946
|
+
name: preparedName,
|
|
1947
|
+
text: sql,
|
|
1948
|
+
values: params,
|
|
1949
|
+
})
|
|
1941
1950
|
: this.pool.query(sql, params);
|
|
1942
1951
|
if (!timeout) {
|
|
1943
1952
|
try {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Split from builder.ts so the class file focuses on SQL assembly / execution.
|
|
5
5
|
*/
|
|
6
|
-
import type pg from 'pg';
|
|
7
6
|
import type { Dialect } from '../dialect.js';
|
|
7
|
+
import type { PgCompatPool, PgCompatQueryResult } from '../pg-types.js';
|
|
8
8
|
import type { SchemaMetadata } from '../schema.js';
|
|
9
9
|
import type { QueryInterface } from './builder.js';
|
|
10
10
|
import type { GlobalFilters, RelationLoadStrategy } from './types.js';
|
|
@@ -13,14 +13,14 @@ import type { GlobalFilters, RelationLoadStrategy } from './types.js';
|
|
|
13
13
|
* {@link DeferredQuery.reselect} plan so it can run the write and the follow-up
|
|
14
14
|
* SELECT through the same timeout/instrumentation path as the primary query.
|
|
15
15
|
*/
|
|
16
|
-
export type ReselectExecutor = (sql: string, params: unknown[], preparedName?: string) => Promise<
|
|
16
|
+
export type ReselectExecutor = (sql: string, params: unknown[], preparedName?: string) => Promise<PgCompatQueryResult>;
|
|
17
17
|
export interface DeferredQuery<T> {
|
|
18
18
|
/** SQL text with $1, $2 placeholders */
|
|
19
19
|
sql: string;
|
|
20
20
|
/** Bound parameter values */
|
|
21
21
|
params: unknown[];
|
|
22
|
-
/** How to transform the raw
|
|
23
|
-
transform: (result:
|
|
22
|
+
/** How to transform the raw driver result into the final value */
|
|
23
|
+
transform: (result: PgCompatQueryResult) => T;
|
|
24
24
|
/** Tag for debugging / logging */
|
|
25
25
|
tag: string;
|
|
26
26
|
/** Prepared statement name (t_<16hex>). Set when SQL cache is enabled. */
|
|
@@ -33,7 +33,7 @@ export interface DeferredQuery<T> {
|
|
|
33
33
|
* Absent for `'returning'`/`'output'` dialects (the statement returns its own
|
|
34
34
|
* rows), so the PostgreSQL path never allocates or consults it.
|
|
35
35
|
*/
|
|
36
|
-
reselect?: (exec: ReselectExecutor) => Promise<
|
|
36
|
+
reselect?: (exec: ReselectExecutor) => Promise<PgCompatQueryResult>;
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
39
|
* How the ORM hands back a Postgres temporal `infinity` / `-infinity`.
|
|
@@ -269,5 +269,5 @@ export interface QueryInterfaceOptions {
|
|
|
269
269
|
* own query language instead of SQL. The SQL dialects never set this, so their
|
|
270
270
|
* `table()` behavior is byte-identical.
|
|
271
271
|
*/
|
|
272
|
-
queryInterfaceFactory?: (pool:
|
|
272
|
+
queryInterfaceFactory?: (pool: PgCompatPool, table: string, schema: SchemaMetadata, middlewares: MiddlewareFn[], options: QueryInterfaceOptions) => QueryInterface<object>;
|
|
273
273
|
}
|
|
@@ -46,17 +46,23 @@ export declare const COLUMN_REF_OPERATORS: Set<string>;
|
|
|
46
46
|
* dependency is fixed: `cli/` and the prisma-compat shim may import from the
|
|
47
47
|
* query path, and the query path may never import from `cli/`
|
|
48
48
|
* (`scripts/check-import-cycles.mjs`).
|
|
49
|
+
*
|
|
50
|
+
* ## STATUS: a landing spot, not yet a deduplication. Read this before trusting it.
|
|
51
|
+
*
|
|
52
|
+
* NOTHING IMPORTS THIS YET. The three copies listed above are all still in
|
|
53
|
+
* place and still hand-synced; declaring the canonical home did not by itself
|
|
54
|
+
* move any of them onto it. It is kept, rather than deleted as unused, because
|
|
55
|
+
* `cli/pii-predicate-guard.ts` names this module as where its own copy wants to
|
|
56
|
+
* go, and deleting the destination is the one change that makes converging
|
|
57
|
+
* harder. Wiring the copies up is a separate change: each one sits on a
|
|
58
|
+
* different walk, so each has to be re-tested on its own.
|
|
59
|
+
*
|
|
60
|
+
* So do not read this constant as evidence that the walkers agree. The comment
|
|
61
|
+
* on each copy is still the only thing holding them in step.
|
|
49
62
|
*/
|
|
50
63
|
export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every", "is", "isNot"];
|
|
51
64
|
/** {@link RELATION_FILTER_WRAPPERS} as a membership set, for the walkers. */
|
|
52
65
|
export declare const RELATION_FILTER_WRAPPER_SET: ReadonlySet<string>;
|
|
53
|
-
/**
|
|
54
|
-
* True when a normalized relation-filter body carries at least one cardinality
|
|
55
|
-
* wrapper. THE predicate the SQL compiler branches on: a key that names a
|
|
56
|
-
* relation but whose value is not one of these falls through to the scalar
|
|
57
|
-
* path.
|
|
58
|
-
*/
|
|
59
|
-
export declare function hasRelationFilterWrapper(filterObj: Record<string, unknown>): boolean;
|
|
60
66
|
/**
|
|
61
67
|
* Check if an operator value is a column reference: a plain object whose ONLY
|
|
62
68
|
* key is `col` with a string value. Anything else (extra keys, non-string
|