turbine-orm 0.65.0 → 0.66.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 (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
@@ -46,6 +46,7 @@ var __importStar = (this && this.__importStar) || (function () {
46
46
  })();
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
48
  exports.QueryInterface = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = void 0;
49
+ exports.resetCacheCrossCheckEnv = resetCacheCrossCheckEnv;
49
50
  exports.unlockNestedWriteTx = unlockNestedWriteTx;
50
51
  const dialect_js_1 = require("../dialect.js");
51
52
  const errors_js_1 = require("../errors.js");
@@ -87,18 +88,55 @@ const writesMod = __importStar(require("./writes.js"));
87
88
  * throws, same as dev. `0`, unset, or an unparseable value means never check.
88
89
  */
89
90
  function cacheCrossCheckMode() {
91
+ const env = crossCheckEnv ?? readCrossCheckEnv();
92
+ if (env.mode !== 'sampled')
93
+ return env.mode;
94
+ // The SAMPLING decision, and only it, is genuinely per hit.
95
+ return env.rate >= 1 || Math.random() < env.rate ? 'sampled' : 'off';
96
+ }
97
+ /**
98
+ * The env-derived half of {@link cacheCrossCheckMode}, resolved ONCE per
99
+ * process and memoized.
100
+ *
101
+ * `process.env` is not a plain object: every property read crosses a C++
102
+ * interceptor. Measured at 112.7 ns per read against 3.6 ns for a module-level
103
+ * constant, and this function ran TWO reads on every cache hit, which put it
104
+ * at the top of the SQL-build CPU profile (12.53% self time, with a further
105
+ * ~12% inside the interceptor) for a value that does not change.
106
+ *
107
+ * It is memoized rather than hoisted to a `const` because the toggle IS
108
+ * observed at runtime, contrary to what a quick look suggests: the
109
+ * cross-check and sampling suites flip `NODE_ENV` /
110
+ * `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE` around a
111
+ * synchronous block in-process, not at spawn. So the read is deferred to first
112
+ * use and {@link resetCacheCrossCheckEnv} re-arms it; that is the entire
113
+ * contract, and the only thing lost versus reading every time is that a
114
+ * process which mutates these variables mid-run must say so.
115
+ */
116
+ let crossCheckEnv;
117
+ function readCrossCheckEnv() {
118
+ let resolved;
90
119
  if (process.env.NODE_ENV !== 'production') {
91
- return process.env.TURBINE_DISABLE_CACHE_CHECK === '1' ? 'off' : 'dev';
92
- }
93
- const raw = process.env.TURBINE_CACHE_CHECK_SAMPLE;
94
- if (raw === undefined)
95
- return 'off';
96
- const rate = Number.parseFloat(raw);
97
- if (!Number.isFinite(rate) || rate <= 0)
98
- return 'off';
99
- if (rate >= 1)
100
- return 'sampled';
101
- return Math.random() < rate ? 'sampled' : 'off';
120
+ resolved = { mode: process.env.TURBINE_DISABLE_CACHE_CHECK === '1' ? 'off' : 'dev' };
121
+ }
122
+ else {
123
+ const raw = process.env.TURBINE_CACHE_CHECK_SAMPLE;
124
+ const rate = raw === undefined ? Number.NaN : Number.parseFloat(raw);
125
+ resolved = !Number.isFinite(rate) || rate <= 0 ? { mode: 'off' } : { mode: 'sampled', rate };
126
+ }
127
+ crossCheckEnv = resolved;
128
+ return resolved;
129
+ }
130
+ /**
131
+ * Discard the memoized cross-check environment so the next cache hit re-reads
132
+ * `process.env`.
133
+ *
134
+ * @internal Exposed for the tests that toggle these variables in-process (see
135
+ * {@link crossCheckEnv}). Production code sets them before the first query and
136
+ * never again.
137
+ */
138
+ function resetCacheCrossCheckEnv() {
139
+ crossCheckEnv = undefined;
102
140
  }
103
141
  /**
104
142
  * Distinct cache-mismatch fingerprints already logged by the sampled production
@@ -417,6 +455,16 @@ class QueryInterface {
417
455
  * inline, not through the top-level cache).
418
456
  */
419
457
  lastCacheHit = false;
458
+ /**
459
+ * Set while a build closure runs when the statement's WHERE (or HAVING)
460
+ * carries a caller-written `AND`/`OR` combinator array, i.e. when its SQL text
461
+ * is a function of an arity the caller chose. Read (and reset) by
462
+ * {@link acquireSql}, which brackets the single `build()` call, so the flag
463
+ * has no lifetime outside that synchronous window and cannot leak between
464
+ * queries. See {@link BuilderCtx.markVariableArity} for the unbounded
465
+ * server-side prepared-statement growth this exists to stop.
466
+ */
467
+ variableArityShape = false;
420
468
  middlewares;
421
469
  defaultLimit;
422
470
  warnOnUnlimited;
@@ -503,6 +551,17 @@ class QueryInterface {
503
551
  * to Date as well (otherwise nested dates leak through as strings).
504
552
  */
505
553
  camelDateFieldCache = new Map();
554
+ /**
555
+ * Per-table memo of `Object.entries(meta.relations)`, consumed by the nested
556
+ * row parser (see `getRelationEntries` in relations.ts). Same rationale and
557
+ * same lifetime as {@link camelDateFieldCache}: the metadata is immutable, and
558
+ * the parser reads it once per row.
559
+ */
560
+ relationEntryCache = new Map();
561
+ /** Per-table memo of batched-loader child readers (see {@link batchedChild}). */
562
+ batchedChildCache = new Map();
563
+ /** The (option-invariant) options every batched child is built with. */
564
+ batchedChildOptions;
506
565
  /** True when this QI runs inside an active transaction (set via _txScoped option). */
507
566
  txScoped;
508
567
  /** Original options reference, forwarded to child QIs in nested writes. */
@@ -699,6 +758,9 @@ class QueryInterface {
699
758
  set currentSkip(v) {
700
759
  self.currentSkip = v;
701
760
  },
761
+ markVariableArity: () => {
762
+ this.variableArityShape = true;
763
+ },
702
764
  q: (name) => this.q(name),
703
765
  p: (index) => this.p(index),
704
766
  inParam: (values) => this.inParam(values),
@@ -716,6 +778,7 @@ class QueryInterface {
716
778
  crossCheckCache: (op, cacheKey, entry, build, collectedParams) => this.crossCheckCache(op, cacheKey, entry, build, collectedParams),
717
779
  jsonEncoding: this.jsonEncoding,
718
780
  camelDateFieldCache: this.camelDateFieldCache,
781
+ relationEntryCache: this.relationEntryCache,
719
782
  limitOneClause: () => this.limitOneClause(),
720
783
  buildPagination: (limitPh, offsetPh, hasOrderBy) => this.buildPagination(limitPh, offsetPh, hasOrderBy),
721
784
  paginationRef: (value, params, arg) => this.paginationRef(value, params, arg),
@@ -1478,15 +1541,10 @@ class QueryInterface {
1478
1541
  includePii, forceCustomPlan = false) {
1479
1542
  // The loader's own global-filter callback below needs the RESOLVED form.
1480
1543
  const resolvedSkip = (0, types_js_1.resolveSkipGlobalFilters)(skip);
1481
- const childOptions = {
1482
- ...this.options,
1483
- defaultLimit: undefined,
1484
- warnOnUnlimited: false,
1485
- };
1486
1544
  return {
1487
1545
  parentMeta: this.tableMeta,
1488
1546
  schema: this.schema,
1489
- makeChild: (table) => new QueryInterface(this.pool, table, this.schema, [], childOptions),
1547
+ makeChild: (table) => this.batchedChild(table),
1490
1548
  // The per-query `forceCustomPlan` opt-in covers the relation follow-ups
1491
1549
  // too: a batched load re-issues the SAME tenant-shaped predicate one
1492
1550
  // level down, so leaving those named would keep exactly the plan-cache
@@ -1496,6 +1554,23 @@ class QueryInterface {
1496
1554
  buildInClause: (expr, paramRef, negated) => this.inClause(expr, paramRef, negated),
1497
1555
  inClauseParam: (values) => this.inParam(values),
1498
1556
  paramPlaceholder: (index) => this.p(index),
1557
+ // Bound each relation follow-up per correlation key when the engine can
1558
+ // express it; absent leaves the loader on its client-side slice. Bound as
1559
+ // a closure rather than passing `this.dialect` so the loader keeps
1560
+ // needing nothing else from the dialect.
1561
+ //
1562
+ // The `dialect.name` test is NOT redundant with the hook's presence, and
1563
+ // this is the same trap the `distinct` gate documents: every engine
1564
+ // dialect is built by SPREADING `postgresDialect`, so an optional hook
1565
+ // added there is INHERITED by sqlite / mysql / mssql, whose "absent"
1566
+ // fallback would then never be taken. The wrapper is not portable as
1567
+ // written either (T-SQL rejects an ORDER BY inside a derived table
1568
+ // without TOP/OFFSET, and the compiled child query carries one), so the
1569
+ // pushdown stays with the dialect that owns the implementation until
1570
+ // another engine ships its own and asserts it.
1571
+ buildPartitionLimit: this.dialect.name === 'postgresql' && this.dialect.buildPartitionLimit
1572
+ ? (input) => this.dialect.buildPartitionLimit(input)
1573
+ : undefined,
1499
1574
  skipGlobalFilters: skip,
1500
1575
  // Query-level opt-in threaded onto every follow-up child `buildFindMany`,
1501
1576
  // so a batched load excludes/includes PII exactly as the join strategy.
@@ -1517,6 +1592,36 @@ class QueryInterface {
1517
1592
  },
1518
1593
  };
1519
1594
  }
1595
+ /**
1596
+ * The child {@link QueryInterface} the batched loader uses for `table`,
1597
+ * memoized per table for the lifetime of this accessor.
1598
+ *
1599
+ * It used to be constructed fresh for every relation of every query, which
1600
+ * threw away that child's SQL template cache each time: a relation follow-up
1601
+ * therefore MISSED the cache on every single request and rebuilt its SQL,
1602
+ * which is the one thing the template cache exists to avoid, and it allocated
1603
+ * a whole QueryInterface (column type maps, camel-date memo, the ctx literal)
1604
+ * per relation per query. `childOptions` was likewise a fresh spread per
1605
+ * query although it is a pure function of this instance's options.
1606
+ *
1607
+ * Safe to share across queries: a QueryInterface holds no per-query state
1608
+ * beyond the transient build fields, which live and die inside one
1609
+ * synchronous `build*` call, and every child is bound to THIS instance's pool
1610
+ * (so it still joins an active transaction) with `defaultLimit` cleared and
1611
+ * unlimited-warnings silenced, because a relation load must fetch every
1612
+ * matching child. Per-query values (`skipGlobalFilters`, `includePii`,
1613
+ * `timeout`, `forceCustomPlan`) are passed as ARGUMENTS by the loader, never
1614
+ * baked into the child, which is what makes the memo sound.
1615
+ */
1616
+ batchedChild(table) {
1617
+ let child = this.batchedChildCache.get(table);
1618
+ if (!child) {
1619
+ this.batchedChildOptions ??= { ...this.options, defaultLimit: undefined, warnOnUnlimited: false };
1620
+ child = new QueryInterface(this.pool, table, this.schema, [], this.batchedChildOptions);
1621
+ this.batchedChildCache.set(table, child);
1622
+ }
1623
+ return child;
1624
+ }
1520
1625
  /**
1521
1626
  * Run a findMany with the batched strategy: execute the base query WITHOUT
1522
1627
  * relation subqueries (all other clauses intact), then load each relation via
@@ -1601,9 +1706,7 @@ class QueryInterface {
1601
1706
  acquireSql(cacheKey, build) {
1602
1707
  if (!this.sqlCacheEnabled) {
1603
1708
  this.lastCacheHit = false;
1604
- const sql = build([]);
1605
- this.cacheMisses++;
1606
- return { sql, name: (0, utils_js_1.sqlToPreparedName)(sql) };
1709
+ return this.buildCacheEntry(build);
1607
1710
  }
1608
1711
  const cached = this.sqlTemplateCache.get(cacheKey);
1609
1712
  if (cached) {
@@ -1612,12 +1715,38 @@ class QueryInterface {
1612
1715
  return cached;
1613
1716
  }
1614
1717
  this.lastCacheHit = false;
1615
- const sql = build([]);
1616
- const entry = { sql, name: (0, utils_js_1.sqlToPreparedName)(sql) };
1718
+ const entry = this.buildCacheEntry(build);
1617
1719
  this.sqlTemplateCache.set(cacheKey, entry);
1618
- this.cacheMisses++;
1619
1720
  return entry;
1620
1721
  }
1722
+ /**
1723
+ * Run one build closure and pair its SQL with the prepared-statement name it
1724
+ * should execute under.
1725
+ *
1726
+ * The `variableArityShape` flag is reset immediately BEFORE the build and read
1727
+ * immediately AFTER it, so this method is the flag's entire lifetime: the
1728
+ * build walk (`buildWhereClause` / `buildScopedWhere` / the HAVING combinator)
1729
+ * is the only thing that can set it, nothing can observe a stale value, and a
1730
+ * throw out of `build()` leaves nothing behind to affect the next query.
1731
+ *
1732
+ * An EMPTY name is how "send this unnamed" travels: `queryWithTimeout` tests
1733
+ * the name for truthiness, so `''` takes the plain `(text, values)` form the
1734
+ * driver never registers a named statement for. Doing it HERE rather than at
1735
+ * the execute seam is what makes it survive the cache: the entry is created
1736
+ * once, on the miss, and a later HIT reuses the same (empty) name, so a
1737
+ * variable-arity shape can never acquire a name from a warmed template.
1738
+ *
1739
+ * See {@link BuilderCtx.markVariableArity} for what makes a shape
1740
+ * variable-arity and the measured reason it must not be named.
1741
+ */
1742
+ buildCacheEntry(build) {
1743
+ this.variableArityShape = false;
1744
+ const sql = build([]);
1745
+ const variableArity = this.variableArityShape;
1746
+ this.variableArityShape = false;
1747
+ this.cacheMisses++;
1748
+ return { sql, name: variableArity ? '' : (0, utils_js_1.sqlToPreparedName)(sql) };
1749
+ }
1621
1750
  /**
1622
1751
  * Dev-mode SQL-cache lockstep cross-check (see {@link cacheCrossCheckEnabled}).
1623
1752
  *
@@ -1947,7 +2076,6 @@ class QueryInterface {
1947
2076
  (0, batched_loader_js_1.stripFields)([entity], proj.strip);
1948
2077
  return entity;
1949
2078
  }
1950
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
1951
2079
  buildFindUnique(args) {
1952
2080
  this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
1953
2081
  // Prisma compound-unique selector expansion (before global-filter merge and
@@ -2286,7 +2414,6 @@ class QueryInterface {
2286
2414
  }
2287
2415
  return maxDepth;
2288
2416
  }
2289
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2290
2417
  buildFindMany(args) {
2291
2418
  this.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args?.skipGlobalFilters);
2292
2419
  // Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
@@ -2307,6 +2434,32 @@ class QueryInterface {
2307
2434
  if (args?.orderBy !== undefined && isEmptyOrderBy(args.orderBy)) {
2308
2435
  args = { ...args, orderBy: undefined };
2309
2436
  }
2437
+ // Drop `orderBy` terms that sort by an expression an earlier term already
2438
+ // sorted by, and `distinct` columns named twice. HERE, before the
2439
+ // fingerprint, for the reason every other normalization in this method is
2440
+ // here: the fingerprint, the SQL build and the param collect must all see
2441
+ // ONE list, or the cache key and the statement it caches describe different
2442
+ // queries. Rewritten into the array form, which flattens through
2443
+ // `orderByEntries` to the identical entry list the object form does, so a
2444
+ // caller who wrote no duplicate is untouched down to the cache key (the
2445
+ // helpers return null unless something was actually dropped).
2446
+ if (args?.orderBy !== undefined) {
2447
+ const deduped = (0, filters_js_1.dedupeOrderEntries)(this.tableMeta, (0, filters_js_1.orderByEntries)(args.orderBy), this.table);
2448
+ if (deduped) {
2449
+ (0, utils_js_1.warnRedundantSortTerm)(this.table, 'orderBy', deduped.dropped);
2450
+ args = {
2451
+ ...args,
2452
+ orderBy: deduped.entries.map(([k, v]) => ({ [k]: v })),
2453
+ };
2454
+ }
2455
+ }
2456
+ if (args?.distinct && args.distinct.length > 0) {
2457
+ const deduped = (0, filters_js_1.dedupeColumnList)(this.tableMeta, args.distinct);
2458
+ if (deduped) {
2459
+ (0, utils_js_1.warnRedundantSortTerm)(this.table, 'distinct', deduped.dropped);
2460
+ args = { ...args, distinct: deduped.columns };
2461
+ }
2462
+ }
2310
2463
  // Deterministic pagination (opt-in): fill a PK-asc orderBy into a paginating
2311
2464
  // query that declares none, BEFORE fingerprinting so the ordered and
2312
2465
  // unordered shapes get distinct cache entries. No-op unless
@@ -2314,6 +2467,29 @@ class QueryInterface {
2314
2467
  const implicitOrder = this.implicitPkOrderBy(args);
2315
2468
  if (implicitOrder)
2316
2469
  args = { ...args, orderBy: implicitOrder };
2470
+ // `distinct` compiles to `SELECT DISTINCT ON (...)`, which is PostgreSQL
2471
+ // syntax and nothing else's. Every other engine parsed it as far as the
2472
+ // word `ON` and answered with a RAW driver error carrying no TURBINE_ code
2473
+ // (SQLite: `near "ON": syntax error`; MySQL: ER_PARSE_ERROR), i.e. the one
2474
+ // shape the typed-error surface exists to prevent. Refused with the same
2475
+ // E017 the OTHER spelling of this feature already used, `groupBy({
2476
+ // distinctOn })` in aggregates.ts, which was gated correctly all along.
2477
+ //
2478
+ // `dialect.name` rather than a new capability flag, deliberately, and this
2479
+ // is the exception to the "capability flags, not engine names" rule the
2480
+ // rest of the dialect follows: every engine dialect is built by SPREADING
2481
+ // `postgresDialect`, so a flag defaulting to `true` would be inherited by
2482
+ // all of them (which is the bug) and one defaulting to `false` would have
2483
+ // to be re-declared on Postgres-compatible dialects that already work.
2484
+ // The established precedent for exactly this feature is the `dialect.name`
2485
+ // test in `buildDistinctOnSource`.
2486
+ //
2487
+ // Before the cache, like the two guards below, so a warmed template cannot
2488
+ // serve the query the cold path refuses.
2489
+ if (args?.distinct && args.distinct.length > 0 && this.dialect.name !== 'postgresql') {
2490
+ throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON (findMany distinct)', this.dialect.name, 'findMany({ distinct }) requires PostgreSQL: SELECT DISTINCT ON is not portable. Group in ' +
2491
+ 'application code, or use groupBy({ by }) for one row per combination.');
2492
+ }
2317
2493
  // `distinct` + relation orderBy is refused up front (E003): the distinct
2318
2494
  // path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
2319
2495
  // <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
@@ -2404,6 +2580,30 @@ class QueryInterface {
2404
2580
  if (args?.distinct && args.distinct.length > 0) {
2405
2581
  distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
2406
2582
  distinctPrefix = `DISTINCT ON (${distinctCols.join(', ')}) `;
2583
+ // A MULTI-column `distinct` gives up its server-side prepared-statement
2584
+ // name, for the same reason a caller-written `OR` array does and by the
2585
+ // same mechanism (see markVariableArity). The channel here is ORDER, not
2586
+ // arity: the column list is written into the SQL text one term at a
2587
+ // time, so the reachable statement set is the ORDERED subsets of the
2588
+ // table's columns. Measured on PostgreSQL 16, a seven-column table:
2589
+ // 5,040 `distinct` permutations left 5,040 further permanent statements
2590
+ // and took CachedPlanSource from 39.4 MB to 59.1 MB on ONE connection.
2591
+ //
2592
+ // UNNAMED rather than canonicalized, which is what `select` and the
2593
+ // write `data` list do, and the asymmetry is the point. This list is
2594
+ // re-emitted as the leading terms of the inner ORDER BY a few lines
2595
+ // below, and with no `orderBy` at all PostgreSQL's choice of
2596
+ // representative row per group is explicitly unpredictable, so
2597
+ // reordering it could hand a caller a different row. Withholding the
2598
+ // name changes no SQL text and therefore cannot. The cost is one extra
2599
+ // server-side parse per execution on an already-rare query shape.
2600
+ //
2601
+ // `length > 1` because a single column has exactly one ordering and so
2602
+ // no permutation space: the common `distinct: ['tenantId']` keeps its
2603
+ // name. Repeats are already dropped upstream (dedupeColumnList), so the
2604
+ // list here is distinct columns and nothing else.
2605
+ if (args.distinct.length > 1)
2606
+ this.ctx.markVariableArity();
2407
2607
  }
2408
2608
  // Join-sink for `relationLoadStrategy: 'flatten'`. Filled while the SELECT
2409
2609
  // list is built (so the flattened relations' ON-clause params interleave
@@ -2671,7 +2871,6 @@ class QueryInterface {
2671
2871
  return deferred.transform(result);
2672
2872
  });
2673
2873
  }
2674
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2675
2874
  buildFindFirst(args) {
2676
2875
  // Reuse findMany's SQL builder but force LIMIT 1
2677
2876
  const findManyArgs = { ...args, limit: 1 };
@@ -2696,7 +2895,6 @@ class QueryInterface {
2696
2895
  return deferred.transform(result);
2697
2896
  });
2698
2897
  }
2699
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2700
2898
  buildFindFirstOrThrow(args) {
2701
2899
  const inner = this.buildFindFirst(args);
2702
2900
  return {
@@ -2726,7 +2924,6 @@ class QueryInterface {
2726
2924
  return deferred.transform(result);
2727
2925
  });
2728
2926
  }
2729
- // biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
2730
2927
  buildFindUniqueOrThrow(args) {
2731
2928
  const inner = this.buildFindUnique(args);
2732
2929
  return {
@@ -6,6 +6,7 @@
6
6
  * and execution rather than filter-shape bookkeeping.
7
7
  */
8
8
  import type { ArrayFilter, ColumnRef, JsonFilter, JsonPathOrderBy, OrderBySpec, OrderDirection, RelationPickOrderBy, TextSearchFilter, VectorFilter, VectorOrderBy, WhereOperator } from './types.js';
9
+ import { type ColumnNameSource } from './utils.js';
9
10
  /** Check if a value is a where operator object (has at least one known operator key) */
10
11
  export declare function isWhereOperator(value: unknown): value is WhereOperator;
11
12
  /**
@@ -21,6 +22,41 @@ export declare function isUnmatchedPlainObject(value: unknown): boolean;
21
22
  * take values only.
22
23
  */
23
24
  export declare const COLUMN_REF_OPERATORS: Set<string>;
25
+ /**
26
+ * THE relation-filter wrappers: the keys whose body is a clause against the
27
+ * relation's TARGET table rather than against this one.
28
+ *
29
+ * One named list, here, because everything that walks a where clause has to
30
+ * agree about them and the copies had already spread. It lived inlined in
31
+ * `where-compile.ts` (`'some' in x || 'every' in x || …`, the SQL compiler's
32
+ * own answer), again in `normalizeRelationFilter` (where.ts) as the negated
33
+ * conjunction of the same five, again in `cli/pii-predicate-guard.ts`, and a
34
+ * fourth time in `prisma-compat.ts` SPLIT across a
35
+ * `RELATION_QUANTIFIERS` set (`some`/`every`/`none`) plus two inline
36
+ * `k === 'is' || k === 'isNot'` tests, which is the copy most likely to drift
37
+ * because half of it does not read as a list and a grep for the list does not
38
+ * find it.
39
+ *
40
+ * The failure mode is not cosmetic: a wrapper the SQL compiler treats as a
41
+ * relation filter but a WALKER does not is a wrapper whose body reaches the
42
+ * builder unwalked. That is precisely the operand-position channel this
43
+ * release closed elsewhere.
44
+ *
45
+ * `query/` is the right home rather than `cli/` because the direction of the
46
+ * dependency is fixed: `cli/` and the prisma-compat shim may import from the
47
+ * query path, and the query path may never import from `cli/`
48
+ * (`scripts/check-import-cycles.mjs`).
49
+ */
50
+ export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every", "is", "isNot"];
51
+ /** {@link RELATION_FILTER_WRAPPERS} as a membership set, for the walkers. */
52
+ 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;
24
60
  /**
25
61
  * Check if an operator value is a column reference: a plain object whose ONLY
26
62
  * key is `col` with a string value. Anything else (extra keys, non-string
@@ -210,6 +246,132 @@ export declare function isRelationPickOrderBy(value: unknown): value is Relation
210
246
  * lockstep across build, param-collect, and cache-key fingerprint.
211
247
  */
212
248
  export declare function orderByEntries(orderBy: unknown): [string, unknown][];
249
+ /**
250
+ * The longest `orderBy` whose statement keeps a NAMED prepared statement.
251
+ * Past this, the compile paths call `markVariableArity` and the statement is
252
+ * sent unnamed for the same reason a caller-written `OR` array is.
253
+ *
254
+ * WHY 3, and why a threshold at all. Refusing duplicate sort keys
255
+ * ({@link dedupeOrderEntries}) bounds an `orderBy` to the table's column
256
+ * count, which sounds like enough and is not: what is left is the PERMUTATION
257
+ * space. A 20-column table admits 20 one-key sorts, 380 two-key sorts, 6,840
258
+ * three-key sorts and 116,280 four-key sorts, and every one of those is a
259
+ * distinct SQL text and therefore a distinct un-reclaimable server-side
260
+ * prepared statement. The count is dominated by its longest term, so a cap on
261
+ * LENGTH is what actually bounds the total.
262
+ *
263
+ * 3 because that is where real sorts stop and generated ones start. The two
264
+ * shapes that show up in application code are a single sort key, and a sort key
265
+ * plus a tiebreaker for stable pagination (`[{createdAt:'desc'},{id:'asc'}]`).
266
+ * Three covers the widest genuinely hand-written form, a category or priority
267
+ * followed by recency followed by a primary-key tiebreak. A fourth key is
268
+ * effectively always a UI that lets the caller stack sort columns, which is
269
+ * exactly the shape that should not be minting named statements. The asymmetry
270
+ * of the two errors also points this way: naming a 4-key sort risks the 116,280
271
+ * above, while NOT naming one costs a single extra server-side parse per
272
+ * execution on a query that is already rare.
273
+ */
274
+ export declare const MAX_NAMED_ORDER_KEYS = 3;
275
+ /**
276
+ * Canonical identity of ONE order term's sort EXPRESSION, or `null` when this
277
+ * rule declines to decide for that shape.
278
+ *
279
+ * DIRECTION IS DELIBERATELY EXCLUDED. `ORDER BY id ASC, id DESC` sorts exactly
280
+ * like `ORDER BY id ASC`: the first term already totally orders the rows it
281
+ * covers, so no later term on the same expression can move anything, whichever
282
+ * way it points. Including the direction would have missed the very case that
283
+ * motivated this, a pair of entries spelled differently AND pointing
284
+ * differently.
285
+ *
286
+ * The column is RESOLVED rather than compared as spelled, via the same
287
+ * `resolveColumnName` every SQL builder resolves keys through, so `userId` and
288
+ * `user_id` on an introspected schema are recognised as the one column they
289
+ * both compile to. Falls back to the raw key when the key resolves to nothing
290
+ * (metadata-less test schemas): the compile path a few lines later is what
291
+ * reports an unknown field, and this function must not pre-empt that error with
292
+ * a worse one.
293
+ *
294
+ * `null` (not compared) for two shapes:
295
+ * - VECTOR KNN ordering. Two distance terms on one column with DIFFERENT
296
+ * target vectors are genuinely different expressions and the second is not a
297
+ * no-op, so deciding would mean hashing the operand, which is a 1,536-float
298
+ * array on a typical embedding column. Not worth the per-build cost for a
299
+ * shape that {@link MAX_NAMED_ORDER_KEYS} already bounds.
300
+ * - RELATION ordering (`{posts:{_count:'desc'}}`, `{author:{name:'asc'}}`,
301
+ * pick-row). Same reason in a different key: the expression is a correlated
302
+ * subquery whose identity is its whole nested shape.
303
+ * Both are bounded by the length cap; neither can be refused wrongly here.
304
+ */
305
+ export declare function orderKeyIdentity(meta: ColumnNameSource | undefined, key: string, value: unknown): string | null;
306
+ /** One term dropped as redundant: the key removed, and the key that outranked it. */
307
+ export interface DroppedTerm {
308
+ key: string;
309
+ first: string;
310
+ /** What both keys resolved to: a column name, or a groupBy SELECT expression. */
311
+ resolved: string;
312
+ }
313
+ /**
314
+ * Remove `orderBy` entries that sort by an expression an earlier entry already
315
+ * sorted by. Returns `null` when nothing is redundant, which is the common case
316
+ * and the one that must stay allocation-light and byte-identical.
317
+ *
318
+ * WHY THIS DROPS RATHER THAN REFUSES, because the reverse was specified and the
319
+ * premise turned out not to hold. A repeated sort key is a no-op in SQL, and it
320
+ * is the only way to push an `orderBy` past the table's column count (measured:
321
+ * `[{id:'asc'}] x n` produced a distinct named prepared statement for every n,
322
+ * from ONE column, with no ceiling short of the request body size). Both of
323
+ * those are true. What is NOT true is that it is always a caller bug. The
324
+ * idiomatic stable-pagination shape is
325
+ *
326
+ * orderBy: [{ [sortField]: sortDir }, { id: 'asc' }]
327
+ *
328
+ * appending a primary-key tiebreak unconditionally, which is correct defensive
329
+ * code, and which produces a duplicate exactly when the caller sorts by the
330
+ * primary key. That is a column header a user clicks, so refusing would turn a
331
+ * working table into a 500 on one column and no others. This repo's own
332
+ * differential fuzz generator writes that pattern, comment included ("a random
333
+ * key first, the PK as final tiebreaker"), and picks the sort field from a pool
334
+ * that contains `id`, so a refusal would have failed the suite on 10-35% of
335
+ * generated cases depending on table width.
336
+ *
337
+ * Dropping has no such failure mode: the removed term provably cannot move a
338
+ * row, so the result set and its order are unchanged, and the caller is told
339
+ * about the redundancy through a dev-only warn-once instead of an exception.
340
+ * The security outcome is identical, because what actually bounds the emitted
341
+ * statement set is the length cap ({@link MAX_NAMED_ORDER_KEYS}), which is
342
+ * shape-blind and applies to repeats and permutations alike. This function's
343
+ * job is narrower: keep the ordinary two-key sort under that cap so it keeps
344
+ * its named statement.
345
+ *
346
+ * A DROPPED TERM IS STILL VALIDATED, see {@link assertDroppedDirection}. The
347
+ * drop is an optimization, and an optimization must not decide whether an input
348
+ * is legal.
349
+ *
350
+ * @param table the table the sort is against, so a refused direction reads
351
+ * exactly as it does when the compile path raises it. Defaults to the
352
+ * metadata's own name, which is what a caller holding only a `TableMetadata`
353
+ * would have passed anyway.
354
+ */
355
+ export declare function dedupeOrderEntries(meta: ColumnNameSource | undefined, entries: [string, unknown][], table?: string | undefined): {
356
+ entries: [string, unknown][];
357
+ dropped: DroppedTerm[];
358
+ } | null;
359
+ /**
360
+ * The {@link dedupeOrderEntries} rule applied to a `DISTINCT ON` column list,
361
+ * the other caller-supplied list written into the SQL one term per element.
362
+ * `DISTINCT ON (a, a)` groups exactly as `DISTINCT ON (a)` does, so a repeat is
363
+ * a no-op here too, and the same generated-code shape produces it (a grouping
364
+ * column chosen by the caller, plus a fixed one appended by the code).
365
+ *
366
+ * No length cap to go with it, deliberately. Unlike `orderBy`, `DISTINCT ON`
367
+ * has a meaning that degrades as the list grows (every added column makes the
368
+ * result strictly less distinct, converging on the plain query), so a long list
369
+ * is self-limiting in a way a long sort is not.
370
+ */
371
+ export declare function dedupeColumnList(meta: ColumnNameSource | undefined, columns: readonly string[]): {
372
+ columns: string[];
373
+ dropped: DroppedTerm[];
374
+ } | null;
213
375
  /**
214
376
  * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
215
377
  * direction string or an {@link OrderBySpec}. Used by every ORDER BY compile