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.
Files changed (56) hide show
  1. package/dist/cjs/cli/error-catalog.d.ts +77 -0
  2. package/dist/cjs/cli/error-catalog.js +388 -0
  3. package/dist/cjs/cli/index.js +3 -2
  4. package/dist/cjs/cli/mcp.d.ts +19 -3
  5. package/dist/cjs/cli/mcp.js +709 -22
  6. package/dist/cjs/cli/migrate.d.ts +19 -2
  7. package/dist/cjs/cli/observe.d.ts +2 -2
  8. package/dist/cjs/cli/observe.js +20 -2
  9. package/dist/cjs/cli/pii-predicate-guard.d.ts +6 -2
  10. package/dist/cjs/cli/pii-predicate-guard.js +6 -2
  11. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  12. package/dist/cjs/client.d.ts +30 -75
  13. package/dist/cjs/client.js +31 -11
  14. package/dist/cjs/introspect.d.ts +113 -17
  15. package/dist/cjs/introspect.js +229 -33
  16. package/dist/cjs/pg-types.d.ts +153 -0
  17. package/dist/cjs/pg-types.js +38 -0
  18. package/dist/cjs/pipeline.d.ts +3 -3
  19. package/dist/cjs/query/batched-loader.d.ts +2 -2
  20. package/dist/cjs/query/builder.d.ts +2 -2
  21. package/dist/cjs/query/builder.js +10 -1
  22. package/dist/cjs/query/deferred.d.ts +6 -6
  23. package/dist/cjs/query/filters.d.ts +13 -7
  24. package/dist/cjs/query/filters.js +13 -14
  25. package/dist/cjs/query/where.d.ts +2 -2
  26. package/dist/cjs/schema-sql.d.ts +18 -0
  27. package/dist/cjs/schema-sql.js +18 -0
  28. package/dist/cli/error-catalog.d.ts +77 -0
  29. package/dist/cli/error-catalog.js +383 -0
  30. package/dist/cli/index.js +3 -2
  31. package/dist/cli/mcp.d.ts +19 -3
  32. package/dist/cli/mcp.js +709 -23
  33. package/dist/cli/migrate.d.ts +19 -2
  34. package/dist/cli/observe.d.ts +2 -2
  35. package/dist/cli/observe.js +20 -2
  36. package/dist/cli/pii-predicate-guard.d.ts +6 -2
  37. package/dist/cli/pii-predicate-guard.js +6 -2
  38. package/dist/cli/studio-ui.generated.js +1 -1
  39. package/dist/client.d.ts +30 -75
  40. package/dist/client.js +31 -11
  41. package/dist/introspect.d.ts +113 -17
  42. package/dist/introspect.js +227 -33
  43. package/dist/pg-types.d.ts +153 -0
  44. package/dist/pg-types.js +37 -0
  45. package/dist/pipeline.d.ts +3 -3
  46. package/dist/query/batched-loader.d.ts +2 -2
  47. package/dist/query/builder.d.ts +2 -2
  48. package/dist/query/builder.js +10 -1
  49. package/dist/query/deferred.d.ts +6 -6
  50. package/dist/query/filters.d.ts +13 -7
  51. package/dist/query/filters.js +13 -13
  52. package/dist/query/where-compile.js +1 -1
  53. package/dist/query/where.d.ts +2 -2
  54. package/dist/schema-sql.d.ts +18 -0
  55. package/dist/schema-sql.js +18 -0
  56. package/package.json +19 -7
@@ -658,40 +658,196 @@ export async function introspectPostgresCatalog(options) {
658
658
  }
659
659
  }
660
660
  /**
661
- * Parse the indexed column names out of a `pg_indexes.indexdef` string.
661
+ * The index-definition key-list SCANNER, and the only one that reads an
662
+ * `indexdef` character by character.
662
663
  *
663
- * `indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING method
664
- * (col, ...) [WHERE predicate]`. We anchor on the `USING` clause's parenthesised
665
- * column list (the same precedent as `describeIndexDefMismatch` in
666
- * schema-sql.ts) so a PARTIAL index's trailing `WHERE (...)` parentheses are
667
- * never mistaken for the column list. The older greedy `/\((.+)\)/` swallowed
668
- * `) WHERE (` and spliced a raw predicate fragment into the column names, which
669
- * then leaked into generated compound-unique selector names.
664
+ * `pg_indexes.indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING
665
+ * method (key, ...) [INCLUDE (col, ...)] [WITH (...)] [TABLESPACE ts]
666
+ * [WHERE predicate]`. This returns the raw entries of the KEY LIST only, one per
667
+ * top-level comma, expression entries included and verbatim.
670
668
  *
671
- * Each column is de-quoted (Postgres quotes non-lowercase identifiers such as a
672
- * Prisma implicit m2m junction's `"A"` / `"B"`), so the names match the
673
- * unquoted column names carried elsewhere in the metadata. Expression columns
674
- * (anything containing a parenthesis) are dropped conservatively: a functional
675
- * index does not name a plain column.
669
+ * ## THREE indexdef parsers coexist in this repo. This is one of them.
670
+ *
671
+ * An earlier version of this comment said there was exactly one. There is not,
672
+ * and pretending otherwise is how hand-synced parsers drift here, so each of the
673
+ * three names the other two:
674
+ *
675
+ * 1. THIS scanner (with {@link indexKeyColumn} / {@link parseIndexColumns}).
676
+ * Safe for any `indexdef` pg emits, including expression keys, quoted
677
+ * identifiers holding commas or parens, string literals, INCLUDE lists and
678
+ * partial predicates. Feeds generated metadata, compound-unique selectors,
679
+ * the FK-index advisor and m2m detection, plus `cli/mcp.ts`.
680
+ * 2. {@link parsePlainUniqueIndexColumns}, below. Still the old
681
+ * `USING \w+ \(([^)]*)\)` regex. It answers a NARROWER question ("is this a
682
+ * plain, whole-table unique index over these exact columns") and returns
683
+ * `null` on everything it cannot read, so its regex's known weaknesses cost
684
+ * a missed hasOne flip rather than a wrong answer.
685
+ * 3. `describeIndexDefMismatch` in `schema-sql.ts`. Same old regex, same
686
+ * fail-toward-a-warning posture.
687
+ *
688
+ * Unifying them is a separate change with its own risk: (2) and (3) both treat
689
+ * "cannot read this" as a safe refusal, and swapping in a parser that reads MORE
690
+ * turns some of those refusals into answers. Until then, prefer this scanner for
691
+ * any new caller, and do not assume a fix here reaches the other two.
692
+ *
693
+ * ## Why the mcp.ts copy is gone
694
+ *
695
+ * `cli/mcp.ts` kept its own weaker copy of this, and the two drifted: on
696
+ * `USING btree (id) INCLUDE (email)` the copy answered `['id) INCLUDE (email']`
697
+ * while this one answered `['id']`. Those columns feed `deriveCatalogRelations`,
698
+ * which decides hasOne-vs-hasMany and auto-m2m, so a UNIQUE index with INCLUDE
699
+ * columns was visible to `turbine generate` and invisible to the MCP server, and
700
+ * the two surfaces disagreed about which relations the schema has. The
701
+ * duplication is also what produced the predicate leak that
702
+ * {@link parseIndexColumns}'s own history records. So mcp.ts consumes this
703
+ * function now, and the split between the two callers is expressed as the two
704
+ * exports below rather than as two implementations.
705
+ *
706
+ * ## Why a scanner rather than a regex
707
+ *
708
+ * The regex this replaces (`/USING\s+\w+\s*\(([^)]*)\)/`) stops at the FIRST
709
+ * `)`, which is the wrong paren for any expression key (`lower(email)`), and a
710
+ * plain `.split(',')` cuts `coalesce(a, b)` in half. The scan tracks paren depth
711
+ * and single-quoted literals, so a comma or a paren inside an expression or a
712
+ * literal is not a boundary.
713
+ *
714
+ * The anchor requires `USING <method> (`, which no predicate, INCLUDE list, WITH
715
+ * list or literal can spell, so the key list is found positionally rather than
716
+ * by hoping the first paren is the right one. When the anchor is absent (not a
717
+ * shape pg emits) it falls back to the first parenthesised group, matching the
718
+ * previous behaviour.
676
719
  */
677
- export function parseIndexColumns(indexdef) {
678
- const m = indexdef.match(/USING\s+\w+\s*\(([^)]*)\)/i) ?? indexdef.match(/\(([^)]*)\)/);
679
- if (!m)
720
+ export function parseIndexKeyEntries(indexdef) {
721
+ const anchor = /USING\s+\w+\s*\(/i.exec(indexdef);
722
+ const open = anchor ? anchor.index + anchor[0].length : indexdef.indexOf('(') + 1;
723
+ if (open === 0)
680
724
  return [];
681
- return m[1]
682
- .split(',')
683
- .map((c) => unquoteIndexIdent(c
684
- .trim()
685
- .replace(/\s+(ASC|DESC)$/i, '')
686
- .trim()))
687
- .filter((c) => c.length > 0 && !c.includes('(') && !c.includes(')'));
725
+ const entries = [];
726
+ let depth = 0;
727
+ let inLiteral = false;
728
+ let inQuotedIdent = false;
729
+ let start = open;
730
+ for (let i = open; i < indexdef.length; i++) {
731
+ const char = indexdef[i];
732
+ // A doubled quote inside a literal (or a quoted identifier) toggles twice,
733
+ // which is the same as not toggling, so pg's `''` / `""` escapes need no
734
+ // special case. Identifier quoting is tracked as well as literal quoting: a
735
+ // column named `it's` renders as `"it's"`, and reading its apostrophe as the
736
+ // start of a literal swallows the rest of the definition.
737
+ if (char === "'" && !inQuotedIdent) {
738
+ inLiteral = !inLiteral;
739
+ continue;
740
+ }
741
+ if (char === '"' && !inLiteral) {
742
+ inQuotedIdent = !inQuotedIdent;
743
+ continue;
744
+ }
745
+ if (inLiteral || inQuotedIdent)
746
+ continue;
747
+ if (char === '(')
748
+ depth++;
749
+ else if (char === ')') {
750
+ if (depth === 0) {
751
+ entries.push(indexdef.slice(start, i));
752
+ return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
753
+ }
754
+ depth--;
755
+ }
756
+ else if (char === ',' && depth === 0) {
757
+ entries.push(indexdef.slice(start, i));
758
+ start = i + 1;
759
+ }
760
+ }
761
+ // Unterminated key list: the definition does not parse, so it names no
762
+ // columns. Never "all of it" - the one input this cannot read must not be the
763
+ // one it forwards.
764
+ return [];
688
765
  }
689
- /** Strip one pair of surrounding double quotes and unescape doubled `""`. */
690
- function unquoteIndexIdent(col) {
691
- if (col.length >= 2 && col.startsWith('"') && col.endsWith('"')) {
692
- return col.slice(1, -1).replace(/""/g, '"');
766
+ /**
767
+ * The plain COLUMN NAME an index key entry indexes, or `null` when the entry is
768
+ * an expression rather than a column.
769
+ *
770
+ * A key entry is `{ column | (expression) } [COLLATE c] [opclass [(params)]]
771
+ * [ASC|DESC] [NULLS FIRST|LAST]`, so the column is the LEADING token and
772
+ * everything after it is a modifier. Reading it that way is what makes
773
+ * `email COLLATE "C" text_pattern_ops` resolve to `email`; the previous
774
+ * suffix-stripping (`ASC`/`DESC` only) returned the whole entry verbatim as a
775
+ * "column name", which matches no real column.
776
+ *
777
+ * SCOPE OF THAT FIX, stated exactly because an earlier version of this comment
778
+ * overstated it: what changes is what {@link parseIndexColumns} reports, and so
779
+ * what its consumers see. Those are the generated `metadata.ts` index lists,
780
+ * the compound-unique selector derivation, the FK-index advisor's leading-column
781
+ * check, and m2m junction detection. Relation CARDINALITY is NOT among them: the
782
+ * `hasMany`/`hasOne` flip reads {@link parsePlainUniqueIndexColumns}, a separate
783
+ * parser this does not feed, and that one still answers `null` for an opclass'd
784
+ * UNIQUE index exactly as it did before.
785
+ *
786
+ * The name is de-quoted (Postgres quotes non-lowercase identifiers such as a
787
+ * Prisma implicit m2m junction's `"A"` / `"B"`) so it matches the unquoted names
788
+ * carried elsewhere in the metadata.
789
+ */
790
+ export function indexKeyColumn(entry) {
791
+ const trimmed = entry.trim();
792
+ if (trimmed.length === 0)
793
+ return null;
794
+ if (trimmed.startsWith('"')) {
795
+ // A quoted identifier ends at the first unpaired `"`; anything after it is a
796
+ // modifier. `""` inside is one escaped quote and does not end it.
797
+ let i = 1;
798
+ let name = '';
799
+ while (i < trimmed.length) {
800
+ if (trimmed[i] === '"') {
801
+ if (trimmed[i + 1] === '"') {
802
+ name += '"';
803
+ i += 2;
804
+ continue;
805
+ }
806
+ // The SAME trailing-modifier rule the unquoted branch applies below,
807
+ // and for the same reason: what follows the name must be whitespace
808
+ // (a COLLATE clause, an opclass, ASC/DESC/NULLS) or nothing. Returning
809
+ // at the closing quote without asking read `"MyFunc"(email)` as a
810
+ // column named `MyFunc`, synthesizing a column that does not exist and
811
+ // handing it to generated metadata, the FK-advisor lead-column check,
812
+ // compound-unique selectors and m2m junction detection alike.
813
+ const rest = trimmed.slice(i + 1);
814
+ if (rest.length > 0 && !/^\s/.test(rest))
815
+ return null;
816
+ return name;
817
+ }
818
+ name += trimmed[i];
819
+ i++;
820
+ }
821
+ return null; // unterminated quote: not a name this can vouch for
693
822
  }
694
- return col;
823
+ // An expression key is anything that is not a bare leading identifier, which
824
+ // includes every parenthesised or operator-bearing form pg renders.
825
+ // Non-ASCII letters are identifier characters to Postgres and are left
826
+ // unquoted in `indexdef`, so the class has to admit them or a `café` column
827
+ // reads as an expression and disappears.
828
+ const leading = /^[A-Za-z_\u0080-\uFFFF][A-Za-z0-9_$\u0080-\uFFFF]*/.exec(trimmed);
829
+ if (!leading)
830
+ return null;
831
+ const rest = trimmed.slice(leading[0].length);
832
+ // The rest must be modifiers (whitespace-separated words / quoted collation /
833
+ // opclass parameters), never a continuation of an expression: `lower(email)`
834
+ // has a leading identifier too, and it is not a column.
835
+ if (rest.length > 0 && !/^\s/.test(rest))
836
+ return null;
837
+ return leading[0];
838
+ }
839
+ /**
840
+ * Parse the indexed COLUMN names out of a `pg_indexes.indexdef` string.
841
+ *
842
+ * Expression keys are dropped, conservatively: a functional index does not name
843
+ * a plain column, and generated metadata has nowhere to say "there was a key
844
+ * here that is not a column". {@link parseIndexKeyEntries} is the variant that
845
+ * keeps them, for the one caller (`cli/mcp.ts`) that reports their presence.
846
+ */
847
+ export function parseIndexColumns(indexdef) {
848
+ return parseIndexKeyEntries(indexdef)
849
+ .map(indexKeyColumn)
850
+ .filter((column) => column !== null);
695
851
  }
696
852
  /**
697
853
  * Whether an `indexdef` carries a top-level `WHERE` predicate (a PARTIAL index).
@@ -783,10 +939,29 @@ function columnSetsEqual(a, b) {
783
939
  * expression, not the raw FK column set.
784
940
  *
785
941
  * Anchors on the `USING <method> (` clause the same way
786
- * {@link describeIndexDefMismatch} does, so a partial index's `WHERE (...)`
787
- * parentheses are never mistaken for the column list. Every column token must be
788
- * a bare or double-quoted identifier; anything else (a function call, an
789
- * operator expression) fails the check and yields `null`.
942
+ * `describeIndexDefMismatch` (schema-sql.ts) does, so a partial index's
943
+ * `WHERE (...)` parentheses are never mistaken for the column list. Every column
944
+ * token must be a bare or double-quoted identifier; anything else (a function
945
+ * call, an operator expression) fails the check and yields `null`.
946
+ *
947
+ * ## Parser 2 of 3, and what it is safe for
948
+ *
949
+ * This is the second of the three indexdef parsers catalogued on
950
+ * {@link parseIndexKeyEntries}; the third is `describeIndexDefMismatch` in
951
+ * schema-sql.ts. It is still the `USING \w+ \(([^)]*)\)` regex that the scanner
952
+ * up there was written to replace, so it inherits that regex's weaknesses: it
953
+ * stops at the FIRST `)`, and it splits on every comma. On an expression key
954
+ * (`lower(email)`), on a quoted identifier containing a comma or a paren, and on
955
+ * an opclass'd key (`email text_pattern_ops`) it therefore reads a token that is
956
+ * not a bare identifier and returns `null`.
957
+ *
958
+ * That is SAFE HERE and only here, because `null` is this function's "I cannot
959
+ * vouch for this index" answer and its single consumer
960
+ * ({@link detectUniqueForeignKeySets}) treats it as "this index does not prove
961
+ * uniqueness". The cost of every misread is a relation left as `hasMany` that
962
+ * could have been `hasOne`, never a uniqueness claim the database does not back.
963
+ * Do NOT reuse it anywhere a wrong-but-plausible column list would be acted on;
964
+ * use the scanner for that.
790
965
  */
791
966
  export function parsePlainUniqueIndexColumns(indexdef) {
792
967
  // Partial index: uniqueness is scoped to the WHERE predicate.
@@ -1210,9 +1385,28 @@ export function deriveCatalogRelations(inputs) {
1210
1385
  // Prisma's implicit m2m junctions have no primary key (just a two-column
1211
1386
  // UNIQUE index over the FK columns), so pass the introspected two-column
1212
1387
  // unique indexes as the fallback junction-key source.
1388
+ //
1389
+ // `idx.columns` is the key list with EXPRESSION keys already dropped, so its
1390
+ // length is not the index's arity and cannot stand in for it. On
1391
+ // `UNIQUE (a, lower(b), c)` it reads `['a', 'c']`, which looks exactly like a
1392
+ // two-column junction key while the pair `(a, c)` is not unique at all, only
1393
+ // `(a, lower(b), c)` is. A manyToMany derived from it returns DUPLICATE ROWS.
1394
+ // So the arity is re-read from the raw definition and the two must agree:
1395
+ // an index with any expression key is not a junction key this can vouch for.
1396
+ // A PARTIAL unique index is refused for the same reason and is the MAINSTREAM
1397
+ // shape of it: `UNIQUE (post_id, tag_id) WHERE deleted_at IS NULL` on a
1398
+ // soft-deleted junction guarantees uniqueness only over the rows matching the
1399
+ // predicate, so the pair can repeat across the whole table and the derived
1400
+ // manyToMany returns duplicate rows. `IndexMetadata.partial` already exists and
1401
+ // already documents this, and the hasOne path already honours it (see
1402
+ // parsePlainUniqueIndexColumns, which refuses a definition carrying a WHERE).
1403
+ // This filter was simply the one place that did not read it, so the two
1404
+ // cardinality paths disagreed about whether the same index proved uniqueness.
1213
1405
  const uniqueIndexColsByTable = new Map();
1214
1406
  for (const [tbl, idxs] of indexesByTable) {
1215
- const twoColUniques = idxs.filter((idx) => idx.unique && idx.columns.length === 2).map((idx) => idx.columns);
1407
+ const twoColUniques = idxs
1408
+ .filter((idx) => idx.unique && !idx.partial && idx.columns.length === 2 && parseIndexKeyEntries(idx.definition).length === 2)
1409
+ .map((idx) => idx.columns);
1216
1410
  if (twoColUniques.length > 0)
1217
1411
  uniqueIndexColsByTable.set(tbl, twoColUniques);
1218
1412
  }
@@ -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,37 @@
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
+ export {};
@@ -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 pg from 'pg';
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: pg.Pool, queries: T, options?: PipelineOptions): Promise<PipelineResults<T>>;
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: pg.Pool): Promise<boolean>;
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: pg.QueryResult) => unknown;
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 pg from 'pg';
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: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
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
@@ -1897,9 +1897,18 @@ export class QueryInterface {
1897
1897
  const action = this.currentAction;
1898
1898
  // Build the query argument, use object form with `name` for prepared
1899
1899
  // statements, or the plain (text, values) form otherwise.
1900
+ //
1901
+ // The object form is not part of the PgCompatPool contract (the engine
1902
+ // pool shims only speak `(text, values)`), so it is reached through a
1903
+ // cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
1904
+ // drivers that accept it turn on.
1900
1905
  const usePrepared = preparedName && this.preparedStatementsEnabled;
1901
1906
  const exec = usePrepared
1902
- ? this.pool.query({ name: preparedName, text: sql, values: params })
1907
+ ? this.pool.query({
1908
+ name: preparedName,
1909
+ text: sql,
1910
+ values: params,
1911
+ })
1903
1912
  : this.pool.query(sql, params);
1904
1913
  if (!timeout) {
1905
1914
  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<pg.QueryResult>;
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 pg.QueryResult into the final value */
23
- transform: (result: pg.QueryResult) => T;
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<pg.QueryResult>;
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: pg.Pool, table: string, schema: SchemaMetadata, middlewares: MiddlewareFn[], options: QueryInterfaceOptions) => QueryInterface<object>;
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