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
@@ -162,6 +162,7 @@ const prisma_schema_fingerprint_js_1 = require("./prisma-schema-fingerprint.js")
162
162
  const index_js_1 = require("./query/index.js");
163
163
  const utils_js_1 = require("./query/utils.js");
164
164
  const warn_registry_js_1 = require("./query/warn-registry.js");
165
+ const where_compile_js_1 = require("./query/where-compile.js");
165
166
  // ---------------------------------------------------------------------------
166
167
  // Prisma.sql-style raw fragments (local, minimal, never imports @prisma/client)
167
168
  // ---------------------------------------------------------------------------
@@ -307,7 +308,63 @@ function relTargetModel(ctx, mm, turbineRel) {
307
308
  // Argument translation
308
309
  // ---------------------------------------------------------------------------
309
310
  const COMBINATORS = new Set(['AND', 'OR', 'NOT']);
311
+ /**
312
+ * The to-many half of the relation-filter wrapper list.
313
+ *
314
+ * DRIFT RISK, and this is the third copy of that list in the tree. The canonical
315
+ * NAMED one is `RELATION_FILTER_WRAPPERS` in `cli/pii-predicate-guard.ts`
316
+ * (`some`/`none`/`every`/`is`/`isNot`), and a second is inlined in
317
+ * `query/where-compile.ts` as `'some' in x || 'every' in x || …`. This copy is
318
+ * the worst of the three because it is SPLIT: the to-many quantifiers are here,
319
+ * and `is` / `isNot` are two inline `k === …` tests in
320
+ * {@link translateRelationFilter}, so half of it does not read as a list and a
321
+ * grep for the list will not find it. A wrapper the compiler learns and this set
322
+ * does not is a relation body that reaches core untranslated, i.e. with Prisma
323
+ * field names still in it.
324
+ *
325
+ * The fix is an export from `query/filters.ts` that all three import; until that
326
+ * exists, these comments are what hold them in step.
327
+ */
310
328
  const RELATION_QUANTIFIERS = new Set(['some', 'every', 'none']);
329
+ /**
330
+ * Refuse a translation walk that has nested past {@link MAX_WHERE_DEPTH}.
331
+ *
332
+ * Core caps every WHERE / HAVING walk (`assertWhereDepth`), but this layer runs
333
+ * BEFORE core sees anything: `translateWhere` rebuilds the whole clause into
334
+ * turbine field names first, so on a deeply nested predicate the stack overflows
335
+ * HERE and core's guard is unreachable for every compat consumer. Measured on
336
+ * Node 24 against a wire-realistic body: a 1,000-deep `NOT` chain translated
337
+ * fine and a 4,000-deep one (a 32 KB body, comfortably under `express.json()`'s
338
+ * 100 KB default) threw `RangeError: Maximum call stack size exceeded`, which is
339
+ * not a {@link TurbineError} and so walks straight past the typed-error surface
340
+ * callers catch on.
341
+ *
342
+ * FIVE families needed the cap, not one. `where` and `having` have a core
343
+ * counterpart and DELEGATE to it, so a compat caller reads the byte-identical
344
+ * message a core caller reads at the byte-identical nesting. The other three
345
+ * have no core counterpart at all and are the sharper half:
346
+ *
347
+ * - `orderBy` recurses on nested ARRAYS, and core ignores those entirely
348
+ * (`normalizeOrderBy` reads one level), so there is no second line of
349
+ * defense: `{"orderBy":[[[…]]]}` at 2,000 (a 4 KB body) was the cheapest
350
+ * `RangeError` on the whole surface.
351
+ * - `include` / `select` recurse through `translateWithOption`
352
+ * (45 KB), and
353
+ * - nested-write `data` recurses through `translateNestedWrite` (43 KB).
354
+ *
355
+ * The depth is core's, not a second convention: the same {@link MAX_WHERE_DEPTH}
356
+ * for all five, so nothing core would accept is refused here for depth alone.
357
+ */
358
+ function assertTranslateDepth(depth, clause) {
359
+ if (depth <= where_compile_js_1.MAX_WHERE_DEPTH)
360
+ return;
361
+ // The two clauses core also walks raise core's own error, verbatim.
362
+ if (clause === 'where' || clause === 'having')
363
+ (0, where_compile_js_1.assertWhereDepth)(depth, clause);
364
+ throw new errors_js_1.ValidationError(`[turbine] \`${clause}\` nests more than ${where_compile_js_1.MAX_WHERE_DEPTH} levels deep. That is far past anything a real ` +
365
+ `query needs, and an unbounded walk over caller-supplied nesting is a stack-overflow surface, so it is ` +
366
+ `refused. If this is a generated argument, flatten it: a single array of N entries is one level, not N.`);
367
+ }
311
368
  /** Whether a value is a plain object usable as a compound-unique selector. */
312
369
  function isPlainObject(v) {
313
370
  return (typeof v === 'object' &&
@@ -568,14 +625,25 @@ function mapRelationLoadStrategy(value) {
568
625
  * `none`) against the target model, and rewrites compound-unique selectors -
569
626
  * including custom `@@unique(name:)` names, into the core-derived selector form
570
627
  * so Turbine's `findUnique`-family expansion handles them uniformly.
628
+ *
629
+ * `depth` is incremented at exactly the two places core's `walkWhere` increments
630
+ * it, a combinator branch and a relation-filter descent (the wrapper body counts
631
+ * as its own level there, so it does here too). That is what makes the refusal
632
+ * land on the same clause at the same nesting whether a caller arrives through
633
+ * this layer or straight through core.
571
634
  */
572
- function translateWhere(ctx, mm, where) {
635
+ function translateWhere(ctx, mm, where, depth = 0) {
636
+ assertTranslateDepth(depth, 'where');
573
637
  if (!isPlainObject(where))
574
638
  return where;
575
639
  const out = {};
576
640
  for (const [key, val] of Object.entries(where)) {
577
641
  if (COMBINATORS.has(key)) {
578
- out[key] = Array.isArray(val) ? val.map((v) => translateWhere(ctx, mm, v)) : translateWhere(ctx, mm, val);
642
+ // An `AND` / `OR` array of N conditions is ONE level, not N: the elements
643
+ // are siblings, so they all translate at the same incremented depth.
644
+ out[key] = Array.isArray(val)
645
+ ? val.map((v) => translateWhere(ctx, mm, v, depth + 1))
646
+ : translateWhere(ctx, mm, val, depth + 1);
579
647
  continue;
580
648
  }
581
649
  // Compound-unique selector (custom or default Prisma name).
@@ -593,7 +661,7 @@ function translateWhere(ctx, mm, where) {
593
661
  const rel = mm.relations[key];
594
662
  if (rel) {
595
663
  const target = relTargetModel(ctx, mm, rel.name);
596
- out[rel.name] = translateRelationFilter(ctx, target, val);
664
+ out[rel.name] = translateRelationFilter(ctx, target, val, depth + 1);
597
665
  continue;
598
666
  }
599
667
  // Scalar field, key renamed, value (literal or operator object) passes
@@ -602,7 +670,7 @@ function translateWhere(ctx, mm, where) {
602
670
  }
603
671
  return out;
604
672
  }
605
- function translateRelationFilter(ctx, target, val) {
673
+ function translateRelationFilter(ctx, target, val, depth) {
606
674
  if (!isPlainObject(val))
607
675
  return val;
608
676
  const keys = Object.keys(val);
@@ -611,17 +679,32 @@ function translateRelationFilter(ctx, target, val) {
611
679
  const out = {};
612
680
  for (const [k, v] of Object.entries(val)) {
613
681
  out[k] =
614
- target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot') ? translateWhere(ctx, target, v) : v;
682
+ target && (RELATION_QUANTIFIERS.has(k) || k === 'is' || k === 'isNot')
683
+ ? translateWhere(ctx, target, v, depth + 1)
684
+ : v;
615
685
  }
616
686
  return out;
617
687
  }
618
- // A bare object filter on a to-one relation: translate its body.
619
- return target ? translateWhere(ctx, target, val) : val;
688
+ // A bare object filter on a to-one relation: translate its body. Core wraps
689
+ // this shape in `{ is: … }` (`normalizeRelationFilter`) and charges it the
690
+ // same level the explicit wrapper above costs, so charge it here too.
691
+ return target ? translateWhere(ctx, target, val, depth + 1) : val;
620
692
  }
621
- /** Translate a Prisma `orderBy` (object / array) into a Turbine `orderBy`. */
622
- function translateOrderBy(ctx, mm, ob) {
693
+ /**
694
+ * Translate a Prisma `orderBy` (object / array) into a Turbine `orderBy`.
695
+ *
696
+ * The ARRAY branch is the reason this walker has its own cap rather than relying
697
+ * on core's: `normalizeOrderBy` reads ONE level of array and never recurses, so
698
+ * core has no second line of defense here at all, and `[[[…]]]` at 2,000 (a 4 KB
699
+ * body) overflowed the stack. Nesting is what counts, not width: the elements of
700
+ * one array are siblings and all translate at the same incremented depth, so a
701
+ * legal `orderBy: [{ a: 'asc' }, { b: 'desc' }]` of any length is one level and
702
+ * only an array INSIDE an array pays for another.
703
+ */
704
+ function translateOrderBy(ctx, mm, ob, depth = 0) {
705
+ assertTranslateDepth(depth, 'orderBy');
623
706
  if (Array.isArray(ob))
624
- return ob.map((o) => translateOrderBy(ctx, mm, o));
707
+ return ob.map((o) => translateOrderBy(ctx, mm, o, depth + 1));
625
708
  if (!isPlainObject(ob))
626
709
  return ob;
627
710
  const out = {};
@@ -633,7 +716,8 @@ function translateOrderBy(ctx, mm, ob) {
633
716
  const rel = mm.relations[key];
634
717
  if (rel) {
635
718
  const target = relTargetModel(ctx, mm, rel.name);
636
- out[rel.name] = isPlainObject(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val) : val;
719
+ out[rel.name] =
720
+ isPlainObject(val) && !('_count' in val) && target ? translateOrderBy(ctx, target, val, depth + 1) : val;
637
721
  continue;
638
722
  }
639
723
  out[renameField(mm, key)] = val;
@@ -682,8 +766,14 @@ function translateOmit(ctx, mm, omit) {
682
766
  * `select` narrows scalars and may also pull relations + `_count`. `include`
683
767
  * and `select` are mutually exclusive, and so are `select` and `omit` (a
684
768
  * narrowed projection minus fields is ambiguous; Prisma refuses the pair too).
769
+ *
770
+ * Mutually recursive with {@link translateWithOption}, one level per relation
771
+ * hop, and it was unbounded: a 1,000-hop `include` chain (a 45 KB body) threw
772
+ * `RangeError` here, before core's own depth-10 relation cap
773
+ * (`CircularRelationError`) could ever see the `with` clause it was building.
685
774
  */
686
- function translateProjection(ctx, mm, args) {
775
+ function translateProjection(ctx, mm, args, depth = 0) {
776
+ assertTranslateDepth(depth, 'include');
687
777
  const include = args.include;
688
778
  const select = args.select;
689
779
  const omitArg = args.omit;
@@ -710,7 +800,7 @@ function translateProjection(ctx, mm, args) {
710
800
  if (!rel) {
711
801
  throw new errors_js_1.ValidationError(`[turbine] prisma-compat: unknown relation "${key}" in include on model "${modelName(ctx, mm)}".`);
712
802
  }
713
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val);
803
+ withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
714
804
  hasWith = true;
715
805
  }
716
806
  return { omit, with: hasWith ? withClause : undefined };
@@ -728,7 +818,7 @@ function translateProjection(ctx, mm, args) {
728
818
  }
729
819
  const rel = mm.relations[key];
730
820
  if (rel) {
731
- withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val);
821
+ withClause[rel.name] = translateWithOption(ctx, mm, rel.name, val, depth + 1);
732
822
  hasWith = true;
733
823
  continue;
734
824
  }
@@ -862,8 +952,15 @@ function applyWriteProjection(proj, row) {
862
952
  }
863
953
  return row;
864
954
  }
865
- /** Translate a Prisma relation include payload into a Turbine `WithOptions`. */
866
- function translateWithOption(ctx, mm, turbineRel, val) {
955
+ /**
956
+ * Translate a Prisma relation include payload into a Turbine `WithOptions`.
957
+ *
958
+ * `depth` is the relation hop count carried down from {@link translateProjection}
959
+ * and is charged to the nested `where` / `orderBy` too, so a payload that pads
960
+ * one arg with relation hops and then nests the other cannot spend two separate
961
+ * budgets to reach the same stack depth.
962
+ */
963
+ function translateWithOption(ctx, mm, turbineRel, val, depth = 0) {
867
964
  if (val === true)
868
965
  return true;
869
966
  if (!isPlainObject(val))
@@ -871,16 +968,16 @@ function translateWithOption(ctx, mm, turbineRel, val) {
871
968
  const target = relTargetModel(ctx, mm, turbineRel);
872
969
  const opt = {};
873
970
  if (val.where !== undefined)
874
- opt.where = target ? translateWhere(ctx, target, val.where) : val.where;
971
+ opt.where = target ? translateWhere(ctx, target, val.where, depth) : val.where;
875
972
  if (val.orderBy !== undefined)
876
- opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy) : val.orderBy;
973
+ opt.orderBy = target ? translateOrderBy(ctx, target, val.orderBy, depth) : val.orderBy;
877
974
  if (val.take !== undefined)
878
975
  opt.limit = mapTake(val.take);
879
976
  if (val.skip !== undefined) {
880
977
  throw new errors_js_1.UnsupportedFeatureError('skip (offset) on a nested relation include', 'prisma-compat', "Turbine's `with` clause has no offset, page the relation with a separate query.");
881
978
  }
882
979
  if (target && (val.select !== undefined || val.include !== undefined || val.omit !== undefined)) {
883
- const proj = translateProjection(ctx, target, val);
980
+ const proj = translateProjection(ctx, target, val, depth);
884
981
  if (proj.select)
885
982
  opt.select = proj.select;
886
983
  // Same silent drop as the top level: a nested `omit` names the sensitive
@@ -1150,7 +1247,8 @@ const NESTED_WRITE_OPS = new Set([
1150
1247
  * renamed to their Turbine relation name and their nested-write payloads are
1151
1248
  * translated against the target model (op names match Prisma's).
1152
1249
  */
1153
- function translateWriteData(ctx, mm, data) {
1250
+ function translateWriteData(ctx, mm, data, depth = 0) {
1251
+ assertTranslateDepth(depth, 'data');
1154
1252
  if (!isPlainObject(data))
1155
1253
  return data;
1156
1254
  const out = {};
@@ -1158,39 +1256,50 @@ function translateWriteData(ctx, mm, data) {
1158
1256
  const rel = mm.relations[key];
1159
1257
  if (rel && isPlainObject(val) && Object.keys(val).some((k) => NESTED_WRITE_OPS.has(k))) {
1160
1258
  const target = relTargetModel(ctx, mm, rel.name);
1161
- out[rel.name] = translateNestedWrite(ctx, target, val);
1259
+ out[rel.name] = translateNestedWrite(ctx, target, val, depth + 1);
1162
1260
  continue;
1163
1261
  }
1164
1262
  out[renameField(mm, key)] = val;
1165
1263
  }
1166
1264
  return out;
1167
1265
  }
1168
- function translateNestedWrite(ctx, target, ops) {
1266
+ /**
1267
+ * One relation's nested-write ops. `depth` is threaded into EVERY payload
1268
+ * translator, the `where` ones included, so the budget is spent on total
1269
+ * nesting rather than per-arg: a chain that alternates relation hops with
1270
+ * combinator nesting reaches the same stack depth as either alone.
1271
+ *
1272
+ * Core's own nested-write engine caps at depth 10 (`nested-write.ts`), so this
1273
+ * cap can only ever fire on a payload core was going to refuse anyway. What it
1274
+ * changes is HOW: a typed refusal instead of a `RangeError` raised before core
1275
+ * is reached at all (measured: a 1,000-hop `create` chain, a 43 KB body).
1276
+ */
1277
+ function translateNestedWrite(ctx, target, ops, depth) {
1169
1278
  const out = {};
1170
1279
  for (const [op, payload] of Object.entries(ops)) {
1171
1280
  switch (op) {
1172
1281
  case 'create':
1173
1282
  case 'createMany':
1174
- out[op] = mapMaybeArray(payload, (p) => (target ? translateWriteData(ctx, target, p) : p));
1283
+ out[op] = mapMaybeArray(payload, (p) => (target ? translateWriteData(ctx, target, p, depth + 1) : p));
1175
1284
  break;
1176
1285
  case 'connect':
1177
1286
  case 'disconnect':
1178
1287
  case 'delete':
1179
1288
  case 'set':
1180
- out[op] = mapMaybeArray(payload, (p) => (target ? translateWhere(ctx, target, p) : p));
1289
+ out[op] = mapMaybeArray(payload, (p) => (target ? translateWhere(ctx, target, p, depth + 1) : p));
1181
1290
  break;
1182
1291
  case 'deleteMany':
1183
1292
  case 'updateMany':
1184
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p));
1293
+ out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1185
1294
  break;
1186
1295
  case 'update':
1187
- out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p));
1296
+ out[op] = mapMaybeArray(payload, (p) => translateWhereDataPair(ctx, target, p, depth + 1));
1188
1297
  break;
1189
1298
  case 'connectOrCreate':
1190
- out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p));
1299
+ out[op] = mapMaybeArray(payload, (p) => translateConnectOrCreate(ctx, target, p, depth + 1));
1191
1300
  break;
1192
1301
  case 'upsert':
1193
- out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p));
1302
+ out[op] = mapMaybeArray(payload, (p) => translateUpsertNested(ctx, target, p, depth + 1));
1194
1303
  break;
1195
1304
  default:
1196
1305
  out[op] = payload;
@@ -1202,39 +1311,39 @@ function mapMaybeArray(val, fn) {
1202
1311
  return Array.isArray(val) ? val.map(fn) : fn(val);
1203
1312
  }
1204
1313
  /** A `{ where?, data }` pair (nested update/updateMany), or a bare data object. */
1205
- function translateWhereDataPair(ctx, target, p) {
1314
+ function translateWhereDataPair(ctx, target, p, depth) {
1206
1315
  if (!isPlainObject(p))
1207
1316
  return p;
1208
1317
  if ('data' in p || 'where' in p) {
1209
1318
  const out = {};
1210
1319
  if (p.where !== undefined)
1211
- out.where = target ? translateWhere(ctx, target, p.where) : p.where;
1320
+ out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
1212
1321
  if (p.data !== undefined)
1213
- out.data = target ? translateWriteData(ctx, target, p.data) : p.data;
1322
+ out.data = target ? translateWriteData(ctx, target, p.data, depth) : p.data;
1214
1323
  return out;
1215
1324
  }
1216
- return target ? translateWriteData(ctx, target, p) : p;
1325
+ return target ? translateWriteData(ctx, target, p, depth) : p;
1217
1326
  }
1218
- function translateConnectOrCreate(ctx, target, p) {
1327
+ function translateConnectOrCreate(ctx, target, p, depth) {
1219
1328
  if (!isPlainObject(p))
1220
1329
  return p;
1221
1330
  const out = {};
1222
1331
  if (p.where !== undefined)
1223
- out.where = target ? translateWhere(ctx, target, p.where) : p.where;
1332
+ out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
1224
1333
  if (p.create !== undefined)
1225
- out.create = target ? translateWriteData(ctx, target, p.create) : p.create;
1334
+ out.create = target ? translateWriteData(ctx, target, p.create, depth) : p.create;
1226
1335
  return out;
1227
1336
  }
1228
- function translateUpsertNested(ctx, target, p) {
1337
+ function translateUpsertNested(ctx, target, p, depth) {
1229
1338
  if (!isPlainObject(p))
1230
1339
  return p;
1231
1340
  const out = {};
1232
1341
  if (p.where !== undefined)
1233
- out.where = target ? translateWhere(ctx, target, p.where) : p.where;
1342
+ out.where = target ? translateWhere(ctx, target, p.where, depth) : p.where;
1234
1343
  if (p.create !== undefined)
1235
- out.create = target ? translateWriteData(ctx, target, p.create) : p.create;
1344
+ out.create = target ? translateWriteData(ctx, target, p.create, depth) : p.create;
1236
1345
  if (p.update !== undefined)
1237
- out.update = target ? translateWriteData(ctx, target, p.update) : p.update;
1346
+ out.update = target ? translateWriteData(ctx, target, p.update, depth) : p.update;
1238
1347
  return out;
1239
1348
  }
1240
1349
  // --- aggregate / groupBy translation --------------------------------------
@@ -1297,13 +1406,23 @@ function renameAggBlock(mm, block, isCount) {
1297
1406
  }
1298
1407
  return out;
1299
1408
  }
1300
- function renameHaving(mm, having) {
1409
+ /**
1410
+ * Rename a groupBy `having` into turbine field names.
1411
+ *
1412
+ * Capped for the same reason and at the same depth as {@link translateWhere}:
1413
+ * core caps its own HAVING walk (`assertWhereDepth(depth, 'having')` in
1414
+ * `query/aggregates.ts`), but this rename runs first and was unbounded, so a
1415
+ * 8,000-deep `NOT` chain (a 64 KB body) overflowed here and core's guard never
1416
+ * ran.
1417
+ */
1418
+ function renameHaving(mm, having, depth = 0) {
1419
+ assertTranslateDepth(depth, 'having');
1301
1420
  if (!isPlainObject(having))
1302
1421
  return having;
1303
1422
  const out = {};
1304
1423
  for (const [key, val] of Object.entries(having)) {
1305
1424
  if (COMBINATORS.has(key)) {
1306
- out[key] = Array.isArray(val) ? val.map((v) => renameHaving(mm, v)) : renameHaving(mm, val);
1425
+ out[key] = Array.isArray(val) ? val.map((v) => renameHaving(mm, v, depth + 1)) : renameHaving(mm, val, depth + 1);
1307
1426
  continue;
1308
1427
  }
1309
1428
  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
@@ -57,6 +57,7 @@ const filters_js_1 = require("./filters.js");
57
57
  const types_js_1 = require("./types.js");
58
58
  const utils_js_1 = require("./utils.js");
59
59
  const whereMod = __importStar(require("./where.js"));
60
+ const where_compile_js_1 = require("./where-compile.js");
60
61
  /**
61
62
  * Enforce the PII contract on the aggregate surface. A PII-tagged
62
63
  * (`defineSchema` `pii: true`) column is excluded from every default
@@ -407,6 +408,32 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
407
408
  return keys.join(', ') || '(none)';
408
409
  };
409
410
  const parts = [];
411
+ // Redundant-sort-term dropping, groupBy's spelling of it (see
412
+ // `dedupeOrderEntries` in filters.ts for why this drops rather than refuses).
413
+ // The findMany rule compares the RESOLVED COLUMN because its keys are field
414
+ // names; here the keys are RESULT keys (by-fields, JSON group-key aliases,
415
+ // aggregate blocks) that have already been resolved into the exact SELECT
416
+ // expression they re-emit, so the expression IS the identity, and comparing
417
+ // it is both cheaper and stricter: an alias and a JSON group key that extract
418
+ // the same path collapse onto one term, which is what "sorts by the same
419
+ // thing" means. Direction is excluded for the same reason as in filters.ts: a
420
+ // second term on an expression the first already ordered by cannot move a row
421
+ // whichever way it points.
422
+ //
423
+ // Done in place rather than up front because groupBy does not go through the
424
+ // SQL-template cache at all, so there is no fingerprint for this to stay in
425
+ // step with; the compile path is the only path.
426
+ const seen = new Map();
427
+ const pushOrderTerm = (expr, label, value) => {
428
+ const first = seen.get(expr);
429
+ if (first !== undefined) {
430
+ (0, utils_js_1.warnRedundantSortTerm)(qi.table, 'groupBy orderBy', [{ key: label, first, resolved: expr }]);
431
+ return;
432
+ }
433
+ seen.set(expr, label);
434
+ const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
435
+ parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
436
+ };
410
437
  for (const [key, value] of (0, filters_js_1.orderByEntries)(orderBy)) {
411
438
  if (value === undefined)
412
439
  continue;
@@ -421,8 +448,7 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
421
448
  // Refuse a direction that is neither asc nor desc BEFORE normalizeOrderBy,
422
449
  // whose `=== 'desc' ? DESC : ASC` would silently sort ascending.
423
450
  (0, types_js_1.assertOrderDirection)(value, `groupBy orderBy "_count" on table "${qi.table}"`);
424
- const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
425
- parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
451
+ pushOrderTerm(expr, '_count', value);
426
452
  continue;
427
453
  }
428
454
  // `_sum` / `_avg` / `_min` / `_max`: an object of field → direction/spec.
@@ -439,8 +465,7 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
439
465
  `that aggregate is not requested in this call. Orderable keys: ${validKeys()}.`);
440
466
  }
441
467
  (0, types_js_1.assertOrderDirection)(dirSpec, `groupBy orderBy "${key}.${field}" on table "${qi.table}"`);
442
- const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirSpec);
443
- parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
468
+ pushOrderTerm(expr, `${key}.${field}`, dirSpec);
444
469
  }
445
470
  continue;
446
471
  }
@@ -451,8 +476,7 @@ function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
451
476
  `Orderable keys: ${validKeys()}.`);
452
477
  }
453
478
  (0, types_js_1.assertOrderDirection)(value, `groupBy orderBy "${key}" on table "${qi.table}"`);
454
- const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(value);
455
- parts.push(`${expr} ${dir}${qi.nullsSuffix(nulls)}`);
479
+ pushOrderTerm(expr, key, value);
456
480
  }
457
481
  return parts.join(', ');
458
482
  }
@@ -505,7 +529,14 @@ function buildDistinctOnSource(qi, distinctOn, whereSql, params) {
505
529
  throw new errors_js_1.ValidationError(`[turbine] groupBy distinctOn on table "${qi.table}" requires \`orderBy\` to pick ONE row per ` +
506
530
  "column combination deterministically (e.g. orderBy: { createdAt: 'desc' }).");
507
531
  }
508
- const distinctCols = distinctOn.columns.map((c) => qi.q(qi.toColumn(c)));
532
+ // A repeated DISTINCT ON column is a no-op, so drop it rather than emit it
533
+ // twice (see `dedupeColumnList`). The orderBy below needs no equivalent: its
534
+ // keys come from an object, and the DISTINCT ON columns lead the ORDER BY, so
535
+ // a key repeating one of them is already merged by `orderParts`' construction.
536
+ const dedupedCols = (0, filters_js_1.dedupeColumnList)(qi.tableMeta, distinctOn.columns);
537
+ if (dedupedCols)
538
+ (0, utils_js_1.warnRedundantSortTerm)(qi.table, 'groupBy distinctOn.columns', dedupedCols.dropped);
539
+ const distinctCols = (dedupedCols?.columns ?? distinctOn.columns).map((c) => qi.q(qi.toColumn(c)));
509
540
  // DISTINCT ON expressions must lead the ORDER BY; the user's orderBy then
510
541
  // decides which row survives per combination.
511
542
  const orderParts = [...distinctCols];
@@ -564,7 +595,8 @@ const HAVING_AGGREGATE_FNS = {
564
595
  * {@link HavingGroupKey}): a scalar filter is legal ONLY on a group key,
565
596
  * because a non-grouped column cannot be referenced in HAVING at all.
566
597
  */
567
- function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys) {
598
+ function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys, depth = 0) {
599
+ (0, where_compile_js_1.assertWhereDepth)(depth, 'having');
568
600
  const clauses = [];
569
601
  for (const [key, value] of Object.entries(having)) {
570
602
  if (value === undefined)
@@ -576,7 +608,7 @@ function buildHavingClauses(qi, having, params, jsonAggExprs, groupKeys) {
576
608
  }
577
609
  // AND / OR / NOT, mixing scalar and aggregate predicates at any depth.
578
610
  if (key === 'AND' || key === 'OR' || key === 'NOT') {
579
- clauses.push(...buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys));
611
+ clauses.push(...buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth));
580
612
  continue;
581
613
  }
582
614
  // Otherwise `key` is a field name. Split its aggregate keys from its
@@ -646,15 +678,28 @@ function splitHavingField(qi, field, value) {
646
678
  * (the caller ANDs them), mirroring {@link buildWhereClause}'s combinator
647
679
  * shapes so HAVING and WHERE read the same way.
648
680
  */
649
- function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys) {
681
+ function buildHavingCombinator(qi, key, value, params, jsonAggExprs, groupKeys, depth = 0) {
650
682
  const conditions = Array.isArray(value) ? value : [value];
683
+ // Same variable-arity shape as the WHERE combinators: an ARRAY branch writes
684
+ // one parenthesized condition per element into the SQL text, so a
685
+ // caller-sized `having.OR` is a new statement per length.
686
+ //
687
+ // DEFENSIVE, not load-bearing, today: `buildGroupBy` and `buildAggregate`
688
+ // assemble their SQL directly and never go through `acquireSql`, so they
689
+ // carry no prepared-statement name for the mark to clear (asserted in
690
+ // prepared-statement-arity.test.ts). It is marked anyway so that routing
691
+ // them through the cache later cannot silently reopen the hole, and so the
692
+ // rule reads the same in both clause compilers. The DEPTH cap above is the
693
+ // half of this that bites here and now.
694
+ if (Array.isArray(value) && key !== 'NOT')
695
+ qi.markVariableArity();
651
696
  const parts = [];
652
697
  for (const condition of conditions) {
653
698
  if (!(0, filters_js_1.isUnmatchedPlainObject)(condition)) {
654
699
  throw new errors_js_1.ValidationError(`[turbine] Invalid having "${key}" on table "${qi.table}": expected ` +
655
700
  `${key === 'OR' ? 'an array of having objects' : 'a having object (or an array of them)'}.`);
656
701
  }
657
- const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys);
702
+ const sub = buildHavingClauses(qi, condition, params, jsonAggExprs, groupKeys, depth + 1);
658
703
  if (sub.length === 0)
659
704
  continue;
660
705
  parts.push(sub.length === 1 ? sub[0] : `(${sub.join(' AND ')})`);
@@ -806,13 +851,30 @@ function buildHavingNumericClauses(qi, expr, filter, params) {
806
851
  function temporalAggValue(qi, col, value) {
807
852
  if (!qi.tableMeta.dateColumns.has(col))
808
853
  return value;
809
- if (!(0, utils_js_1.isTemporalInfinity)(value))
810
- return value;
811
- if (qi.temporalInfinity === 'null')
812
- return null;
813
- if (typeof value === 'number')
814
- return value;
815
- return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
854
+ if ((0, utils_js_1.isTemporalInfinity)(value)) {
855
+ if (qi.temporalInfinity === 'null')
856
+ return null;
857
+ if (typeof value === 'number')
858
+ return value;
859
+ return value === '-infinity' ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
860
+ }
861
+ // A STRING on a temporal column means the driver handed the value back
862
+ // untyped, which is what every engine without pg's OID-keyed type parsers
863
+ // does. Verified on SQLite: `findMany` returned a `Date` for `at` and
864
+ // `groupBy` returned a `Date` for the same column used as a group key, while
865
+ // `aggregate({ _max: { at: true } })` returned the raw string
866
+ // '2024-01-15 12:00:00', so ONE column disagreed with itself across three
867
+ // read paths and with the `Date` the generated types promise. `_min`/`_max`
868
+ // are assembled from the RAW row here (they cannot go through `parseRow`,
869
+ // whose snake→camel mapping would collide with the `_min_` alias), so the
870
+ // coercion `parseRow` applies has to be applied here as well, on the same
871
+ // terms: offset-less text is pinned to UTC unless `utcTimestamps: false`.
872
+ //
873
+ // On PostgreSQL this branch is unreachable and the emitted values are
874
+ // unchanged: the driver's date/timestamp parsers already produce a `Date`.
875
+ if (typeof value === 'string')
876
+ return qi.utcTimestamps !== false ? (0, utils_js_1.parseDbDate)(value) : new Date(value);
877
+ return value;
816
878
  }
817
879
  function buildAggregate(qi, args) {
818
880
  qi.currentSkip = (0, types_js_1.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