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
@@ -125,6 +125,7 @@ import { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
125
125
  import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UNSAFE, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
126
126
  import { suggestKey } from './query/utils.js';
127
127
  import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
128
+ import { assertWhereDepth, MAX_WHERE_DEPTH } from './query/where-compile.js';
128
129
  // ---------------------------------------------------------------------------
129
130
  // Prisma.sql-style raw fragments (local, minimal, never imports @prisma/client)
130
131
  // ---------------------------------------------------------------------------
@@ -270,7 +271,63 @@ function relTargetModel(ctx, mm, turbineRel) {
270
271
  // Argument translation
271
272
  // ---------------------------------------------------------------------------
272
273
  const COMBINATORS = new Set(['AND', 'OR', 'NOT']);
274
+ /**
275
+ * The to-many half of the relation-filter wrapper list.
276
+ *
277
+ * DRIFT RISK, and this is the third copy of that list in the tree. The canonical
278
+ * NAMED one is `RELATION_FILTER_WRAPPERS` in `cli/pii-predicate-guard.ts`
279
+ * (`some`/`none`/`every`/`is`/`isNot`), and a second is inlined in
280
+ * `query/where-compile.ts` as `'some' in x || 'every' in x || …`. This copy is
281
+ * the worst of the three because it is SPLIT: the to-many quantifiers are here,
282
+ * and `is` / `isNot` are two inline `k === …` tests in
283
+ * {@link translateRelationFilter}, so half of it does not read as a list and a
284
+ * grep for the list will not find it. A wrapper the compiler learns and this set
285
+ * does not is a relation body that reaches core untranslated, i.e. with Prisma
286
+ * field names still in it.
287
+ *
288
+ * The fix is an export from `query/filters.ts` that all three import; until that
289
+ * exists, these comments are what hold them in step.
290
+ */
273
291
  const RELATION_QUANTIFIERS = new Set(['some', 'every', 'none']);
292
+ /**
293
+ * Refuse a translation walk that has nested past {@link MAX_WHERE_DEPTH}.
294
+ *
295
+ * Core caps every WHERE / HAVING walk (`assertWhereDepth`), but this layer runs
296
+ * BEFORE core sees anything: `translateWhere` rebuilds the whole clause into
297
+ * turbine field names first, so on a deeply nested predicate the stack overflows
298
+ * HERE and core's guard is unreachable for every compat consumer. Measured on
299
+ * Node 24 against a wire-realistic body: a 1,000-deep `NOT` chain translated
300
+ * fine and a 4,000-deep one (a 32 KB body, comfortably under `express.json()`'s
301
+ * 100 KB default) threw `RangeError: Maximum call stack size exceeded`, which is
302
+ * not a {@link TurbineError} and so walks straight past the typed-error surface
303
+ * callers catch on.
304
+ *
305
+ * FIVE families needed the cap, not one. `where` and `having` have a core
306
+ * counterpart and DELEGATE to it, so a compat caller reads the byte-identical
307
+ * message a core caller reads at the byte-identical nesting. The other three
308
+ * have no core counterpart at all and are the sharper half:
309
+ *
310
+ * - `orderBy` recurses on nested ARRAYS, and core ignores those entirely
311
+ * (`normalizeOrderBy` reads one level), so there is no second line of
312
+ * defense: `{"orderBy":[[[…]]]}` at 2,000 (a 4 KB body) was the cheapest
313
+ * `RangeError` on the whole surface.
314
+ * - `include` / `select` recurse through `translateWithOption`
315
+ * (45 KB), and
316
+ * - nested-write `data` recurses through `translateNestedWrite` (43 KB).
317
+ *
318
+ * The depth is core's, not a second convention: the same {@link MAX_WHERE_DEPTH}
319
+ * for all five, so nothing core would accept is refused here for depth alone.
320
+ */
321
+ function assertTranslateDepth(depth, clause) {
322
+ if (depth <= MAX_WHERE_DEPTH)
323
+ return;
324
+ // The two clauses core also walks raise core's own error, verbatim.
325
+ if (clause === 'where' || clause === 'having')
326
+ assertWhereDepth(depth, clause);
327
+ throw new ValidationError(`[turbine] \`${clause}\` nests more than ${MAX_WHERE_DEPTH} levels deep. That is far past anything a real ` +
328
+ `query needs, and an unbounded walk over caller-supplied nesting is a stack-overflow surface, so it is ` +
329
+ `refused. If this is a generated argument, flatten it: a single array of N entries is one level, not N.`);
330
+ }
274
331
  /** Whether a value is a plain object usable as a compound-unique selector. */
275
332
  function isPlainObject(v) {
276
333
  return (typeof v === 'object' &&
@@ -531,14 +588,25 @@ function mapRelationLoadStrategy(value) {
531
588
  * `none`) against the target model, and rewrites compound-unique selectors -
532
589
  * including custom `@@unique(name:)` names, into the core-derived selector form
533
590
  * so Turbine's `findUnique`-family expansion handles them uniformly.
591
+ *
592
+ * `depth` is incremented at exactly the two places core's `walkWhere` increments
593
+ * it, a combinator branch and a relation-filter descent (the wrapper body counts
594
+ * as its own level there, so it does here too). That is what makes the refusal
595
+ * land on the same clause at the same nesting whether a caller arrives through
596
+ * this layer or straight through core.
534
597
  */
535
- function translateWhere(ctx, mm, where) {
598
+ function translateWhere(ctx, mm, where, depth = 0) {
599
+ assertTranslateDepth(depth, 'where');
536
600
  if (!isPlainObject(where))
537
601
  return where;
538
602
  const out = {};
539
603
  for (const [key, val] of Object.entries(where)) {
540
604
  if (COMBINATORS.has(key)) {
541
- out[key] = Array.isArray(val) ? val.map((v) => translateWhere(ctx, mm, v)) : translateWhere(ctx, mm, val);
605
+ // An `AND` / `OR` array of N conditions is ONE level, not N: the elements
606
+ // are siblings, so they all translate at the same incremented depth.
607
+ out[key] = Array.isArray(val)
608
+ ? val.map((v) => translateWhere(ctx, mm, v, depth + 1))
609
+ : translateWhere(ctx, mm, val, depth + 1);
542
610
  continue;
543
611
  }
544
612
  // Compound-unique selector (custom or default Prisma name).
@@ -556,7 +624,7 @@ function translateWhere(ctx, mm, where) {
556
624
  const rel = mm.relations[key];
557
625
  if (rel) {
558
626
  const target = relTargetModel(ctx, mm, rel.name);
559
- out[rel.name] = translateRelationFilter(ctx, target, val);
627
+ out[rel.name] = translateRelationFilter(ctx, target, val, depth + 1);
560
628
  continue;
561
629
  }
562
630
  // Scalar field, key renamed, value (literal or operator object) passes
@@ -565,7 +633,7 @@ function translateWhere(ctx, mm, where) {
565
633
  }
566
634
  return out;
567
635
  }
568
- function translateRelationFilter(ctx, target, val) {
636
+ function translateRelationFilter(ctx, target, val, depth) {
569
637
  if (!isPlainObject(val))
570
638
  return val;
571
639
  const keys = Object.keys(val);
@@ -574,17 +642,32 @@ function translateRelationFilter(ctx, target, val) {
574
642
  const out = {};
575
643
  for (const [k, v] of Object.entries(val)) {
576
644
  out[k] =
577
- target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot') ? translateWhere(ctx, target, v) : v;
645
+ target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot')
646
+ ? translateWhere(ctx, target, v, depth + 1)
647
+ : v;
578
648
  }
579
649
  return out;
580
650
  }
581
- // A bare object filter on a to-one relation: translate its body.
582
- return target ? translateWhere(ctx, target, val) : val;
651
+ // A bare object filter on a to-one relation: translate its body. Core wraps
652
+ // this shape in `{ is: … }` (`normalizeRelationFilter`) and charges it the
653
+ // same level the explicit wrapper above costs, so charge it here too.
654
+ return target ? translateWhere(ctx, target, val, depth + 1) : val;
583
655
  }
584
- /** Translate a Prisma `orderBy` (object / array) into a Turbine `orderBy`. */
585
- function translateOrderBy(ctx, mm, ob) {
656
+ /**
657
+ * Translate a Prisma `orderBy` (object / array) into a Turbine `orderBy`.
658
+ *
659
+ * The ARRAY branch is the reason this walker has its own cap rather than relying
660
+ * on core's: `normalizeOrderBy` reads ONE level of array and never recurses, so
661
+ * core has no second line of defense here at all, and `[[[…]]]` at 2,000 (a 4 KB
662
+ * body) overflowed the stack. Nesting is what counts, not width: the elements of
663
+ * one array are siblings and all translate at the same incremented depth, so a
664
+ * legal `orderBy: [{ a: 'asc' }, { b: 'desc' }]` of any length is one level and
665
+ * only an array INSIDE an array pays for another.
666
+ */
667
+ function translateOrderBy(ctx, mm, ob, depth = 0) {
668
+ assertTranslateDepth(depth, 'orderBy');
586
669
  if (Array.isArray(ob))
587
- return ob.map((o) => translateOrderBy(ctx, mm, o));
670
+ return ob.map((o) => translateOrderBy(ctx, mm, o, depth + 1));
588
671
  if (!isPlainObject(ob))
589
672
  return ob;
590
673
  const out = {};
@@ -596,7 +679,8 @@ function translateOrderBy(ctx, mm, ob) {
596
679
  const rel = mm.relations[key];
597
680
  if (rel) {
598
681
  const target = relTargetModel(ctx, mm, rel.name);
599
- out[rel.name] = isPlainObject(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val) : val;
682
+ out[rel.name] =
683
+ isPlainObject(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
600
684
  continue;
601
685
  }
602
686
  out[renameField(mm, key)] = val;
@@ -645,8 +729,14 @@ function translateOmit(ctx, mm, omit) {
645
729
  * `select` narrows scalars and may also pull relations + `_count`. `include`
646
730
  * and `select` are mutually exclusive, and so are `select` and `omit` (a
647
731
  * narrowed projection minus fields is ambiguous; Prisma refuses the pair too).
732
+ *
733
+ * Mutually recursive with {@link translateWithOption}, one level per relation
734
+ * hop, and it was unbounded: a 1,000-hop `include` chain (a 45 KB body) threw
735
+ * `RangeError` here, before core's own depth-10 relation cap
736
+ * (`CircularRelationError`) could ever see the `with` clause it was building.
648
737
  */
649
- function translateProjection(ctx, mm, args) {
738
+ function translateProjection(ctx, mm, args, depth = 0) {
739
+ assertTranslateDepth(depth, 'include');
650
740
  const include = args.include;
651
741
  const select = args.select;
652
742
  const omitArg = args.omit;
@@ -673,7 +763,7 @@ function translateProjection(ctx, mm, args) {
673
763
  if (!rel) {
674
764
  throw new ValidationError(`[turbine] prisma-compat: unknown relation "${key}" in include on model "${modelName(ctx, mm)}".`);
675
765
  }
676
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val);
766
+ withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
677
767
  hasWith = true;
678
768
  }
679
769
  return { omit, with: hasWith ? withClause : undefined };
@@ -691,7 +781,7 @@ function translateProjection(ctx, mm, args) {
691
781
  }
692
782
  const rel = mm.relations[key];
693
783
  if (rel) {
694
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val);
784
+ withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
695
785
  hasWith = true;
696
786
  continue;
697
787
  }
@@ -825,8 +915,15 @@ function applyWriteProjection(proj, row) {
825
915
  }
826
916
  return row;
827
917
  }
828
- /** Translate a Prisma relation include payload into a Turbine `WithOptions`. */
829
- function translateWithOption(ctx, mm, turbineRel, val) {
918
+ /**
919
+ * Translate a Prisma relation include payload into a Turbine `WithOptions`.
920
+ *
921
+ * `depth` is the relation hop count carried down from {@link translateProjection}
922
+ * and is charged to the nested `where` / `orderBy` too, so a payload that pads
923
+ * one arg with relation hops and then nests the other cannot spend two separate
924
+ * budgets to reach the same stack depth.
925
+ */
926
+ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
830
927
  if (val === true)
831
928
  return true;
832
929
  if (!isPlainObject(val))
@@ -834,16 +931,16 @@ function translateWithOption(ctx, mm, turbineRel, val) {
834
931
  const target = relTargetModel(ctx, mm, turbineRel);
835
932
  const opt = {};
836
933
  if (val.where !== undefined)
837
- opt.where = target ? translateWhere(ctx, target, val.where) : val.where;
934
+ opt.where = target ? translateWhere(ctx, target, val.where, depth) : val.where;
838
935
  if (val.orderBy !== undefined)
839
- opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy) : val.orderBy;
936
+ opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy, depth) : val.orderBy;
840
937
  if (val.take !== undefined)
841
938
  opt.limit = mapTake(val.take);
842
939
  if (val.skip !== undefined) {
843
940
  throw new UnsupportedFeatureError('skip (offset) on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no offset, page the relation with a separate query.");
844
941
  }
845
942
  if (target && (val.select !== undefined || val.include !== undefined || val.omit !== undefined)) {
846
- const proj = translateProjection(ctx, target, val);
943
+ const proj = translateProjection(ctx, target, val, depth);
847
944
  if (proj.select)
848
945
  opt.select = proj.select;
849
946
  // Same silent drop as the top level: a nested `omit` names the sensitive
@@ -1113,7 +1210,8 @@ const NESTED_WRITE_OPS = new Set([
1113
1210
  * renamed to their Turbine relation name and their nested-write payloads are
1114
1211
  * translated against the target model (op names match Prisma's).
1115
1212
  */
1116
- function translateWriteData(ctx, mm, data) {
1213
+ function translateWriteData(ctx, mm, data, depth = 0) {
1214
+ assertTranslateDepth(depth, 'data');
1117
1215
  if (!isPlainObject(data))
1118
1216
  return data;
1119
1217
  const out = {};
@@ -1121,39 +1219,50 @@ function translateWriteData(ctx, mm, data) {
1121
1219
  const rel = mm.relations[key];
1122
1220
  if (rel && isPlainObject(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
1123
1221
  const target = relTargetModel(ctx, mm, rel.name);
1124
- out[rel.name] = translateNestedWrite(ctx, target, val);
1222
+ out[rel.name] = translateNestedWrite(ctx, target, val, depth + 1);
1125
1223
  continue;
1126
1224
  }
1127
1225
  out[renameField(mm, key)] = val;
1128
1226
  }
1129
1227
  return out;
1130
1228
  }
1131
- function translateNestedWrite(ctx, target, ops) {
1229
+ /**
1230
+ * One relation's nested-write ops. `depth` is threaded into EVERY payload
1231
+ * translator, the `where` ones included, so the budget is spent on total
1232
+ * nesting rather than per-arg: a chain that alternates relation hops with
1233
+ * combinator nesting reaches the same stack depth as either alone.
1234
+ *
1235
+ * Core's own nested-write engine caps at depth 10 (`nested-write.ts`), so this
1236
+ * cap can only ever fire on a payload core was going to refuse anyway. What it
1237
+ * changes is HOW: a typed refusal instead of a `RangeError` raised before core
1238
+ * is reached at all (measured: a 1,000-hop `create` chain, a 43 KB body).
1239
+ */
1240
+ function translateNestedWrite(ctx, target, ops, depth) {
1132
1241
  const out = {};
1133
1242
  for (const [op, payload] of Object.entries(ops)) {
1134
1243
  switch (op) {
1135
1244
  case 'create':
1136
1245
  case 'createMany':
1137
- out[op] = mapMaybeArray(payload, (p) => (target ? translateWriteData(ctx, target, p) : p));
1246
+ out[op] = mapMaybeArray(payload, (p) => (target ? translateWriteData(ctx, target, p, depth + 1) : p));
1138
1247
  break;
1139
1248
  case 'connect':
1140
1249
  case 'disconnect':
1141
1250
  case 'delete':
1142
1251
  case 'set':
1143
- out[op] = mapMaybeArray(payload, (p) => (target ? translateWhere(ctx, target, p) : p));
1252
+ out[op] = mapMaybeArray(payload, (p) => (target ? translateWhere(ctx, target, p, depth + 1) : p));
1144
1253
  break;
1145
1254
  case 'deleteMany':
1146
1255
  case 'updateMany':
1147
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p));
1256
+ out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1148
1257
  break;
1149
1258
  case 'update':
1150
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p));
1259
+ out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1151
1260
  break;
1152
1261
  case 'connectOrCreate':
1153
- out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p));
1262
+ out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p, depth + 1));
1154
1263
  break;
1155
1264
  case 'upsert':
1156
- out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p));
1265
+ out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p, depth + 1));
1157
1266
  break;
1158
1267
  default:
1159
1268
  out[op] = payload;
@@ -1165,39 +1274,39 @@ function mapMaybeArray(val, fn) {
1165
1274
  return Array.isArray(val) ? val.map(fn) : fn(val);
1166
1275
  }
1167
1276
  /** A `{ where?, data }` pair (nested update/updateMany), or a bare data object. */
1168
- function translateWhereDataPair(ctx, target, p) {
1277
+ function translateWhereDataPair(ctx, target, p, depth) {
1169
1278
  if (!isPlainObject(p))
1170
1279
  return p;
1171
1280
  if ('data' in p || 'where' in p) {
1172
1281
  const out = {};
1173
1282
  if (p.where !== undefined)
1174
- out.where = target ? translateWhere(ctx, target, p.where) : p.where;
1283
+ out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
1175
1284
  if (p.data !== undefined)
1176
- out.data = target ? translateWriteData(ctx, target, p.data) : p.data;
1285
+ out.data = target ? translateWriteData(ctx, target, p.data, depth) : p.data;
1177
1286
  return out;
1178
1287
  }
1179
- return target ? translateWriteData(ctx, target, p) : p;
1288
+ return target ? translateWriteData(ctx, target, p, depth) : p;
1180
1289
  }
1181
- function translateConnectOrCreate(ctx, target, p) {
1290
+ function translateConnectOrCreate(ctx, target, p, depth) {
1182
1291
  if (!isPlainObject(p))
1183
1292
  return p;
1184
1293
  const out = {};
1185
1294
  if (p.where !== undefined)
1186
- out.where = target ? translateWhere(ctx, target, p.where) : p.where;
1295
+ out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
1187
1296
  if (p.create !== undefined)
1188
- out.create = target ? translateWriteData(ctx, target, p.create) : p.create;
1297
+ out.create = target ? translateWriteData(ctx, target, p.create, depth) : p.create;
1189
1298
  return out;
1190
1299
  }
1191
- function translateUpsertNested(ctx, target, p) {
1300
+ function translateUpsertNested(ctx, target, p, depth) {
1192
1301
  if (!isPlainObject(p))
1193
1302
  return p;
1194
1303
  const out = {};
1195
1304
  if (p.where !== undefined)
1196
- out.where = target ? translateWhere(ctx, target, p.where) : p.where;
1305
+ out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
1197
1306
  if (p.create !== undefined)
1198
- out.create = target ? translateWriteData(ctx, target, p.create) : p.create;
1307
+ out.create = target ? translateWriteData(ctx, target, p.create, depth) : p.create;
1199
1308
  if (p.update !== undefined)
1200
- out.update = target ? translateWriteData(ctx, target, p.update) : p.update;
1309
+ out.update = target ? translateWriteData(ctx, target, p.update, depth) : p.update;
1201
1310
  return out;
1202
1311
  }
1203
1312
  // --- aggregate / groupBy translation --------------------------------------
@@ -1260,13 +1369,23 @@ function renameAggBlock(mm, block, isCount) {
1260
1369
  }
1261
1370
  return out;
1262
1371
  }
1263
- function renameHaving(mm, having) {
1372
+ /**
1373
+ * Rename a groupBy `having` into turbine field names.
1374
+ *
1375
+ * Capped for the same reason and at the same depth as {@link translateWhere}:
1376
+ * core caps its own HAVING walk (`assertWhereDepth(depth, 'having')` in
1377
+ * `query/aggregates.ts`), but this rename runs first and was unbounded, so a
1378
+ * 8,000-deep `NOT` chain (a 64 KB body) overflowed here and core's guard never
1379
+ * ran.
1380
+ */
1381
+ function renameHaving(mm, having, depth = 0) {
1382
+ assertTranslateDepth(depth, 'having');
1264
1383
  if (!isPlainObject(having))
1265
1384
  return having;
1266
1385
  const out = {};
1267
1386
  for (const [key, val] of Object.entries(having)) {
1268
1387
  if (COMBINATORS.has(key)) {
1269
- out[key] = Array.isArray(val) ? val.map((v) => renameHaving(mm, v)) : renameHaving(mm, val);
1388
+ out[key] = Array.isArray(val) ? val.map((v) => renameHaving(mm, v, depth + 1)) : renameHaving(mm, val, depth + 1);
1270
1389
  continue;
1271
1390
  }
1272
1391
  if (key === '_count') {
@@ -104,7 +104,7 @@ export type HavingGroupKey = {
104
104
  * {@link HavingGroupKey}): a scalar filter is legal ONLY on a group key,
105
105
  * because a non-grouped column cannot be referenced in HAVING at all.
106
106
  */
107
- export declare function buildHavingClauses<T extends object>(qi: BuilderCtx, having: HavingClause<T>, params: unknown[], jsonAggExprs?: Map<string, string>, groupKeys?: Map<string, HavingGroupKey>): string[];
107
+ export declare function buildHavingClauses<T extends object>(qi: BuilderCtx, having: HavingClause<T>, params: unknown[], jsonAggExprs?: Map<string, string>, groupKeys?: Map<string, HavingGroupKey>, depth?: number): string[];
108
108
  /**
109
109
  * Convert a single having aggregate filter into one or more parameterized SQL
110
110
  * comparisons against the given aggregate expression. A bare value is
@@ -10,10 +10,11 @@
10
10
  */
11
11
  import { UnsupportedFeatureError, ValidationError } from '../errors.js';
12
12
  import { snakeToCamel } from '../schema.js';
13
- import { isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, } from './filters.js';
13
+ import { dedupeColumnList, isJsonPathOrderBy, isUnmatchedPlainObject, isVectorOrderBy, isWhereOperator, normalizeOrderBy, orderByEntries, } from './filters.js';
14
14
  import { assertOrderDirection, resolveSkipGlobalFilters, resolveUnsafeFlag } from './types.js';
15
- import { isTemporalInfinity, ownLookup, unknownFieldMessage } from './utils.js';
15
+ import { isTemporalInfinity, ownLookup, parseDbDate, unknownFieldMessage, warnRedundantSortTerm } from './utils.js';
16
16
  import * as whereMod from './where.js';
17
+ import { assertWhereDepth } from './where-compile.js';
17
18
  /**
18
19
  * Enforce the PII contract on the aggregate surface. A PII-tagged
19
20
  * (`defineSchema` `pii: true`) column is excluded from every default
@@ -364,6 +365,32 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
364
365
  return keys.join(', ') || '(none)';
365
366
  };
366
367
  const parts = [];
368
+ // Redundant-sort-term dropping, groupBy's spelling of it (see
369
+ // `dedupeOrderEntries` in filters.ts for why this drops rather than refuses).
370
+ // The findMany rule compares the RESOLVED COLUMN because its keys are field
371
+ // names; here the keys are RESULT keys (by-fields, JSON group-key aliases,
372
+ // aggregate blocks) that have already been resolved into the exact SELECT
373
+ // expression they re-emit, so the expression IS the identity, and comparing
374
+ // it is both cheaper and stricter: an alias and a JSON group key that extract
375
+ // the same path collapse onto one term, which is what "sorts by the same
376
+ // thing" means. Direction is excluded for the same reason as in filters.ts: a
377
+ // second term on an expression the first already ordered by cannot move a row
378
+ // whichever way it points.
379
+ //
380
+ // Done in place rather than up front because groupBy does not go through the
381
+ // SQL-template cache at all, so there is no fingerprint for this to stay in
382
+ // step with; the compile path is the only path.
383
+ const seen = new Map();
384
+ const pushOrderTerm = (expr, label, value) => {
385
+ const first = seen.get(expr);
386
+ if (first !== undefined) {
387
+ warnRedundantSortTerm(qi.table, 'groupBy orderBy', [{ key: label, first, resolved: expr }]);
388
+ return;
389
+ }
390
+ seen.set(expr, label);
391
+ const { dir, nulls } = normalizeOrderBy(value);
392
+ parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
393
+ };
367
394
  for (const [key, value] of orderByEntries(orderBy)) {
368
395
  if (value === undefined)
369
396
  continue;
@@ -378,8 +405,7 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
378
405
  // Refuse a direction that is neither asc nor desc BEFORE normalizeOrderBy,
379
406
  // whose `=== 'desc' ? DESC : ASC` would silently sort ascending.
380
407
  assertOrderDirection(value, `groupBy orderBy "_count" on table "${qi.table}"`);
381
- const { dir, nulls } = normalizeOrderBy(value);
382
- parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
408
+ pushOrderTerm(expr, '_count', value);
383
409
  continue;
384
410
  }
385
411
  // `_sum` / `_avg` / `_min` / `_max`: an object of field → direction/spec.
@@ -396,8 +422,7 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
396
422
  `that aggregate is not requested in this call. Orderable keys: ${validKeys()}.`);
397
423
  }
398
424
  assertOrderDirection(dirSpec, `groupBy orderBy "${key}.${field}" on table "${qi.table}"`);
399
- const { dir, nulls } = normalizeOrderBy(dirSpec);
400
- parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
425
+ pushOrderTerm(expr, `${key}.${field}`, dirSpec);
401
426
  }
402
427
  continue;
403
428
  }
@@ -408,8 +433,7 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
408
433
  `Orderable keys: ${validKeys()}.`);
409
434
  }
410
435
  assertOrderDirection(value, `groupBy orderBy "${key}" on table "${qi.table}"`);
411
- const { dir, nulls } = normalizeOrderBy(value);
412
- parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
436
+ pushOrderTerm(expr, key, value);
413
437
  }
414
438
  return parts.join(', ');
415
439
  }
@@ -462,7 +486,14 @@ export function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
462
486
  throw new ValidationError(`[turbine] groupBy distinctOn on table "${qi.table}" requires \`orderBy\` to pick ONE row per ` +
463
487
  "column combination deterministically (e.g. orderBy: { createdAt: 'desc' }).");
464
488
  }
465
- const distinctCols = distinctOn.columns.map((c) => qi.q(qi.toColumn(c)));
489
+ // A repeated DISTINCT ON column is a no-op, so drop it rather than emit it
490
+ // twice (see `dedupeColumnList`). The orderBy below needs no equivalent: its
491
+ // keys come from an object, and the DISTINCT ON columns lead the ORDER BY, so
492
+ // a key repeating one of them is already merged by `orderParts`' construction.
493
+ const dedupedCols = dedupeColumnList(qi.tableMeta, distinctOn.columns);
494
+ if (dedupedCols)
495
+ warnRedundantSortTerm(qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
496
+ const distinctCols = (dedupedCols?.columns ?? distinctOn.columns).map((c) => qi.q(qi.toColumn(c)));
466
497
  // DISTINCT ON expressions must lead the ORDER BY; the user's orderBy then
467
498
  // decides which row survives per combination.
468
499
  const orderParts = [...distinctCols];
@@ -521,7 +552,8 @@ const HAVING_AGGREGATE_FNS = {
521
552
  * {@link HavingGroupKey}): a scalar filter is legal ONLY on a group key,
522
553
  * because a non-grouped column cannot be referenced in HAVING at all.
523
554
  */
524
- export function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys) {
555
+ export function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys, depth = 0) {
556
+ assertWhereDepth(depth, 'having');
525
557
  const clauses = [];
526
558
  for (const [key, value] of Object.entries(having)) {
527
559
  if (value === undefined)
@@ -533,7 +565,7 @@ export function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys)
533
565
  }
534
566
  // AND / OR / NOT, mixing scalar and aggregate predicates at any depth.
535
567
  if (key === 'AND' || key === 'OR' || key === 'NOT') {
536
- clauses.push(...buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys));
568
+ clauses.push(...buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth));
537
569
  continue;
538
570
  }
539
571
  // Otherwise `key` is a field name. Split its aggregate keys from its
@@ -603,15 +635,28 @@ function splitHavingField(qi, field, value) {
603
635
  * (the caller ANDs them), mirroring {@link buildWhereClause}'s combinator
604
636
  * shapes so HAVING and WHERE read the same way.
605
637
  */
606
- function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys) {
638
+ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth = 0) {
607
639
  const conditions = Array.isArray(value) ? value : [value];
640
+ // Same variable-arity shape as the WHERE combinators: an ARRAY branch writes
641
+ // one parenthesized condition per element into the SQL text, so a
642
+ // caller-sized `having.OR` is a new statement per length.
643
+ //
644
+ // DEFENSIVE, not load-bearing, today: `buildGroupBy` and `buildAggregate`
645
+ // assemble their SQL directly and never go through `acquireSql`, so they
646
+ // carry no prepared-statement name for the mark to clear (asserted in
647
+ // prepared-statement-arity.test.ts). It is marked anyway so that routing
648
+ // them through the cache later cannot silently reopen the hole, and so the
649
+ // rule reads the same in both clause compilers. The DEPTH cap above is the
650
+ // half of this that bites here and now.
651
+ if (Array.isArray(value) && key !== 'NOT')
652
+ qi.markVariableArity();
608
653
  const parts = [];
609
654
  for (const condition of conditions) {
610
655
  if (!isUnmatchedPlainObject(condition)) {
611
656
  throw new ValidationError(`[turbine] Invalid having "${key}" on table "${qi.table}": expected ` +
612
657
  `${key === 'OR' ? 'an array of having objects' : 'a having object (or an array of them)'}.`);
613
658
  }
614
- const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys);
659
+ const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys, depth + 1);
615
660
  if (sub.length === 0)
616
661
  continue;
617
662
  parts.push(sub.length === 1 ? sub[0] : `(${sub.join(' AND ')})`);
@@ -763,13 +808,30 @@ export function buildHavingNumericClauses(qi, expr, filter, params) {
763
808
  function temporalAggValue(qi, col, value) {
764
809
  if (!qi.tableMeta.dateColumns.has(col))
765
810
  return value;
766
- if (!isTemporalInfinity(value))
767
- return value;
768
- if (qi.temporalInfinity === 'null')
769
- return null;
770
- if (typeof value === 'number')
771
- return value;
772
- return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
811
+ if (isTemporalInfinity(value)) {
812
+ if (qi.temporalInfinity === 'null')
813
+ return null;
814
+ if (typeof value === 'number')
815
+ return value;
816
+ return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
817
+ }
818
+ // A STRING on a temporal column means the driver handed the value back
819
+ // untyped, which is what every engine without pg's OID-keyed type parsers
820
+ // does. Verified on SQLite: `findMany` returned a `Date` for `at` and
821
+ // `groupBy` returned a `Date` for the same column used as a group key, while
822
+ // `aggregate({ _max: { at: true } })` returned the raw string
823
+ // '2024-01-15 12:00:00', so ONE column disagreed with itself across three
824
+ // read paths and with the `Date` the generated types promise. `_min`/`_max`
825
+ // are assembled from the RAW row here (they cannot go through `parseRow`,
826
+ // whose snake→camel mapping would collide with the `_min_` alias), so the
827
+ // coercion `parseRow` applies has to be applied here as well, on the same
828
+ // terms: offset-less text is pinned to UTC unless `utcTimestamps: false`.
829
+ //
830
+ // On PostgreSQL this branch is unreachable and the emitted values are
831
+ // unchanged: the driver's date/timestamp parsers already produce a `Date`.
832
+ if (typeof value === 'string')
833
+ return qi.utcTimestamps !== false ? parseDbDate(value) : new Date(value);
834
+ return value;
773
835
  }
774
836
  export function buildAggregate(qi, args) {
775
837
  qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
@@ -54,6 +54,7 @@
54
54
  * @module
55
55
  */
56
56
  import type pg from 'pg';
57
+ import type { PartitionLimitInput } from '../dialect.js';
57
58
  import { type RelationDef, type SchemaMetadata, type TableMetadata } from '../schema.js';
58
59
  import type { ReselectExecutor } from './builder.js';
59
60
  import type { SkipGlobalFilters, Unsafe, WithClause, WithCount } from './types.js';
@@ -123,6 +124,15 @@ export interface RelationLoadContext {
123
124
  clause: string;
124
125
  params: unknown[];
125
126
  } | null;
127
+ /**
128
+ * The active dialect's {@link Dialect.buildPartitionLimit}, when it has one.
129
+ * Absent means this engine cannot bound the follow-up per correlation key, so
130
+ * the loader falls back to fetching every child and slicing client-side. The
131
+ * same fallback runs, on every engine, for a relation whose ordering does not
132
+ * force which rows the limit keeps. See {@link partitionOrderBy} and
133
+ * {@link boundedChildQuery}.
134
+ */
135
+ buildPartitionLimit?: (input: PartitionLimitInput) => string;
126
136
  }
127
137
  /**
128
138
  * The default projection of `meta` expressed in FIELD names: which fields the