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.
- package/README.md +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- package/package.json +6 -4
package/dist/prisma-compat.js
CHANGED
|
@@ -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
|
-
|
|
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')
|
|
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
|
-
|
|
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
|
-
/**
|
|
585
|
-
|
|
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] =
|
|
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
|
-
/**
|
|
829
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
package/dist/query/aggregates.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return value;
|
|
772
|
-
|
|
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
|