turbine-orm 0.30.0 → 0.31.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/dist/cjs/client.js +14 -3
- package/dist/cjs/mssql.js +24 -3
- package/dist/cjs/powdb.js +197 -50
- package/dist/cjs/powql.js +7 -1
- package/dist/cjs/query/builder.js +311 -63
- package/dist/cjs/query/filters.js +49 -2
- package/dist/client.d.ts +13 -0
- package/dist/client.js +14 -3
- package/dist/index.d.ts +1 -1
- package/dist/mssql.js +24 -3
- package/dist/powdb.d.ts +33 -0
- package/dist/powdb.js +197 -50
- package/dist/powql.js +7 -1
- package/dist/query/builder.d.ts +85 -5
- package/dist/query/builder.js +312 -64
- package/dist/query/filters.d.ts +28 -1
- package/dist/query/filters.js +46 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +66 -11
- package/package.json +1 -1
|
@@ -2459,7 +2459,11 @@ class QueryInterface {
|
|
|
2459
2459
|
}
|
|
2460
2460
|
// Operator objects
|
|
2461
2461
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
2462
|
-
this.collectOperatorParams(rawColumn, value, params
|
|
2462
|
+
this.collectOperatorParams(rawColumn, value, params, {
|
|
2463
|
+
meta: this.tableMeta,
|
|
2464
|
+
table: this.table,
|
|
2465
|
+
prefix: '',
|
|
2466
|
+
});
|
|
2463
2467
|
continue;
|
|
2464
2468
|
}
|
|
2465
2469
|
// Plain equality — same strict validation as the build path, so a
|
|
@@ -2560,28 +2564,40 @@ class QueryInterface {
|
|
|
2560
2564
|
}
|
|
2561
2565
|
}
|
|
2562
2566
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
2563
|
-
this.collectOperatorParams(col, value, params);
|
|
2567
|
+
this.collectOperatorParams(col, value, params, { meta, table: targetTable, prefix: '' });
|
|
2564
2568
|
continue;
|
|
2565
2569
|
}
|
|
2566
2570
|
this.assertBindableEqualityValue(col, value, this.pgTypeForColumn(meta, col), targetTable);
|
|
2567
2571
|
params.push(value);
|
|
2568
2572
|
}
|
|
2569
2573
|
}
|
|
2570
|
-
/**
|
|
2571
|
-
|
|
2572
|
-
|
|
2574
|
+
/**
|
|
2575
|
+
* Collect params from operator clauses. Mirrors buildOperatorClauses:
|
|
2576
|
+
* {@link ColumnRef} values compile into the SQL text, so they push NOTHING -
|
|
2577
|
+
* but they re-run the same validation (unknown ref / insensitive mode) so a
|
|
2578
|
+
* warmed cache can never skip a check the build path enforces.
|
|
2579
|
+
*/
|
|
2580
|
+
collectOperatorParams(column, op, params, refCtx) {
|
|
2581
|
+
const skipRef = (v) => {
|
|
2582
|
+
if (!(0, filters_js_1.isColumnRef)(v))
|
|
2583
|
+
return false;
|
|
2584
|
+
if (refCtx)
|
|
2585
|
+
this.resolveColumnRef(v, refCtx, op.mode);
|
|
2586
|
+
return true;
|
|
2587
|
+
};
|
|
2588
|
+
if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
|
|
2573
2589
|
(0, filters_js_1.assertBindableEqualsOperand)(op.equals, `"${column}"`);
|
|
2574
2590
|
params.push(op.equals);
|
|
2575
2591
|
}
|
|
2576
|
-
if (op.gt !== undefined)
|
|
2592
|
+
if (op.gt !== undefined && !skipRef(op.gt))
|
|
2577
2593
|
params.push(op.gt);
|
|
2578
|
-
if (op.gte !== undefined)
|
|
2594
|
+
if (op.gte !== undefined && !skipRef(op.gte))
|
|
2579
2595
|
params.push(op.gte);
|
|
2580
|
-
if (op.lt !== undefined)
|
|
2596
|
+
if (op.lt !== undefined && !skipRef(op.lt))
|
|
2581
2597
|
params.push(op.lt);
|
|
2582
|
-
if (op.lte !== undefined)
|
|
2598
|
+
if (op.lte !== undefined && !skipRef(op.lte))
|
|
2583
2599
|
params.push(op.lte);
|
|
2584
|
-
if (op.not !== undefined && op.not !== null)
|
|
2600
|
+
if (op.not !== undefined && op.not !== null && !skipRef(op.not))
|
|
2585
2601
|
params.push(op.not);
|
|
2586
2602
|
if (op.in !== undefined)
|
|
2587
2603
|
params.push(this.inParam(op.in));
|
|
@@ -2637,10 +2653,10 @@ class QueryInterface {
|
|
|
2637
2653
|
// isEmpty has no params (IS NULL / IS NOT NULL)
|
|
2638
2654
|
}
|
|
2639
2655
|
/**
|
|
2640
|
-
* Collect params for an orderBy clause.
|
|
2641
|
-
*
|
|
2642
|
-
* parameterless. Mirrors buildOrderBy's push
|
|
2643
|
-
* param re-collection stays in lockstep.
|
|
2656
|
+
* Collect params for an orderBy clause. Vector KNN ordering pushes the
|
|
2657
|
+
* `$n::vector` query vector and JSON-path ordering pushes its text[] path;
|
|
2658
|
+
* plain direction ordering is parameterless. Mirrors buildOrderBy's push
|
|
2659
|
+
* order exactly so the cached-SQL param re-collection stays in lockstep.
|
|
2644
2660
|
*/
|
|
2645
2661
|
collectOrderByParams(orderBy, params) {
|
|
2646
2662
|
for (const [key, dir] of Object.entries(orderBy)) {
|
|
@@ -2652,6 +2668,13 @@ class QueryInterface {
|
|
|
2652
2668
|
this.pushVectorParam(key, rawColumn, dir.distance.to, params);
|
|
2653
2669
|
continue;
|
|
2654
2670
|
}
|
|
2671
|
+
// JSON-path ordering: mirrors buildJsonPathOrderEntry: same validation,
|
|
2672
|
+
// then the path bound as one text[] param.
|
|
2673
|
+
if ((0, filters_js_1.isJsonPathOrderBy)(dir)) {
|
|
2674
|
+
this.validateJsonPathOrderBy(this.table, this.tableMeta, key, dir);
|
|
2675
|
+
params.push(dir.path.map(String));
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2655
2678
|
// To-many relation orderBy (`{ posts: { _count } }`) uses the same count
|
|
2656
2679
|
// subquery as `_count` — mirror its global-filter params. To-one relation
|
|
2657
2680
|
// orderBy carries the target's global filter once per ordered column.
|
|
@@ -2798,9 +2821,20 @@ class QueryInterface {
|
|
|
2798
2821
|
const targetMeta = this.schema.tables[targetTable];
|
|
2799
2822
|
if (!targetMeta)
|
|
2800
2823
|
return;
|
|
2824
|
+
// A dialect that owns the whole subquery (buildRelationSubquery override,
|
|
2825
|
+
// SQL Server FOR JSON) compiles orderBy through its OWN paging clause -
|
|
2826
|
+
// plain directions only, no order params: so the native order-param
|
|
2827
|
+
// mirrors below must stay off for it (its documented contract remains
|
|
2828
|
+
// where → limit → nested).
|
|
2829
|
+
const nativeOrderPath = !this.dialect.buildRelationSubquery;
|
|
2801
2830
|
// manyToMany param order mirrors buildManyToManySubquery:
|
|
2802
|
-
// where params → limit param → nested-with params
|
|
2831
|
+
// orderBy params → where params → limit param → nested-with params
|
|
2832
|
+
// (always, both paths).
|
|
2803
2833
|
if (relDef.type === 'manyToMany') {
|
|
2834
|
+
const m2mOrderEntries = spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
2835
|
+
if (nativeOrderPath && m2mOrderEntries.length > 0) {
|
|
2836
|
+
this.collectRelationOrderParams(targetTable, targetMeta, m2mOrderEntries, params);
|
|
2837
|
+
}
|
|
2804
2838
|
if (spec.where) {
|
|
2805
2839
|
this.collectAliasWhereParams(targetTable, targetMeta, spec.where, params);
|
|
2806
2840
|
}
|
|
@@ -2819,7 +2853,8 @@ class QueryInterface {
|
|
|
2819
2853
|
return;
|
|
2820
2854
|
}
|
|
2821
2855
|
// Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
|
|
2822
|
-
const
|
|
2856
|
+
const relOrderEntries = spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
2857
|
+
const hasOrder = relOrderEntries.length > 0;
|
|
2823
2858
|
const willWrap = relDef.type === 'hasMany' && (spec.limit !== undefined || hasOrder);
|
|
2824
2859
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
2825
2860
|
if (!willWrap && spec.with) {
|
|
@@ -2830,6 +2865,12 @@ class QueryInterface {
|
|
|
2830
2865
|
this.collectRelationSubqueryParams(nestedRelDef, nestedSpec, params, 'alias', depth + 1);
|
|
2831
2866
|
}
|
|
2832
2867
|
}
|
|
2868
|
+
// orderBy params (JSON paths / relation-order global filters): mirrors
|
|
2869
|
+
// buildRelationSubquery, which builds its ORDER BY terms BEFORE compiling
|
|
2870
|
+
// spec.where (both wrapped and non-wrapped paths).
|
|
2871
|
+
if (nativeOrderPath && hasOrder) {
|
|
2872
|
+
this.collectRelationOrderParams(targetTable, targetMeta, relOrderEntries, params);
|
|
2873
|
+
}
|
|
2833
2874
|
// where params — mirrors buildAliasWhere push order
|
|
2834
2875
|
if (spec.where) {
|
|
2835
2876
|
this.collectAliasWhereParams(targetTable, targetMeta, spec.where, params);
|
|
@@ -3180,7 +3221,11 @@ class QueryInterface {
|
|
|
3180
3221
|
}
|
|
3181
3222
|
// Handle operator objects
|
|
3182
3223
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
3183
|
-
const opClauses = this.buildOperatorClauses(column, value, params
|
|
3224
|
+
const opClauses = this.buildOperatorClauses(column, value, params, {
|
|
3225
|
+
meta: this.tableMeta,
|
|
3226
|
+
table: this.table,
|
|
3227
|
+
prefix: '',
|
|
3228
|
+
});
|
|
3184
3229
|
andClauses.push(...opClauses);
|
|
3185
3230
|
continue;
|
|
3186
3231
|
}
|
|
@@ -3374,7 +3419,11 @@ class QueryInterface {
|
|
|
3374
3419
|
}
|
|
3375
3420
|
}
|
|
3376
3421
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
3377
|
-
const opClauses = this.buildOperatorClauses(qCol, value, params
|
|
3422
|
+
const opClauses = this.buildOperatorClauses(qCol, value, params, {
|
|
3423
|
+
meta,
|
|
3424
|
+
table: targetTable,
|
|
3425
|
+
prefix: `${qt}.`,
|
|
3426
|
+
});
|
|
3378
3427
|
conditions.push(...opClauses);
|
|
3379
3428
|
continue;
|
|
3380
3429
|
}
|
|
@@ -3541,7 +3590,11 @@ class QueryInterface {
|
|
|
3541
3590
|
}
|
|
3542
3591
|
}
|
|
3543
3592
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
3544
|
-
clauses.push(...this.buildOperatorClauses(qCol, value, params
|
|
3593
|
+
clauses.push(...this.buildOperatorClauses(qCol, value, params, {
|
|
3594
|
+
meta: targetMeta,
|
|
3595
|
+
table: targetTable,
|
|
3596
|
+
prefix: `${alias}.`,
|
|
3597
|
+
}));
|
|
3545
3598
|
continue;
|
|
3546
3599
|
}
|
|
3547
3600
|
this.assertBindableEqualityValue(col, value, this.pgTypeForColumn(targetMeta, col), targetTable);
|
|
@@ -3600,7 +3653,7 @@ class QueryInterface {
|
|
|
3600
3653
|
}
|
|
3601
3654
|
}
|
|
3602
3655
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
3603
|
-
this.collectOperatorParams(col, value, params);
|
|
3656
|
+
this.collectOperatorParams(col, value, params, { meta: targetMeta, table: targetTable, prefix: '' });
|
|
3604
3657
|
continue;
|
|
3605
3658
|
}
|
|
3606
3659
|
this.assertBindableEqualityValue(col, value, this.pgTypeForColumn(targetMeta, col), targetTable);
|
|
@@ -3677,16 +3730,55 @@ class QueryInterface {
|
|
|
3677
3730
|
}
|
|
3678
3731
|
return parts.join('&');
|
|
3679
3732
|
}
|
|
3733
|
+
/**
|
|
3734
|
+
* Validate a `{ col }` column reference against its table and return the
|
|
3735
|
+
* resolved snake_case column name. Shared by the SQL-build path
|
|
3736
|
+
* ({@link buildOperatorClauses}) and the cache-hit param-collect path
|
|
3737
|
+
* (`collectOperatorParams`) so both always throw identically: a warmed
|
|
3738
|
+
* cache can never skip the check.
|
|
3739
|
+
*/
|
|
3740
|
+
resolveColumnRef(ref, ctx, mode) {
|
|
3741
|
+
if (mode === 'insensitive') {
|
|
3742
|
+
throw new errors_js_1.ValidationError(`[turbine] mode: 'insensitive' cannot be combined with a column reference ({ col: "${ref.col}" }). ` +
|
|
3743
|
+
`Case-insensitive column-to-column comparison is not supported: use client.sql\`...\` ` +
|
|
3744
|
+
`for lower(a) = lower(b).`);
|
|
3745
|
+
}
|
|
3746
|
+
const col = ctx.meta.columnMap[ref.col] ?? (0, schema_js_1.camelToSnake)(ref.col);
|
|
3747
|
+
if (!ctx.meta.allColumns.includes(col)) {
|
|
3748
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${ref.col}" referenced by { col } in where on table "${ctx.table}". ` +
|
|
3749
|
+
`Known fields: ${Object.keys(ctx.meta.columnMap).join(', ') || '(none)'}.`);
|
|
3750
|
+
}
|
|
3751
|
+
return col;
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Compile a `{ col }` reference to its quoted, prefix-matched SQL identifier.
|
|
3755
|
+
* NO param is bound: the referenced column is part of the SQL text (and of
|
|
3756
|
+
* the where fingerprint, see {@link fingerprintOperatorShape}).
|
|
3757
|
+
*/
|
|
3758
|
+
columnRefSql(ref, ctx, mode) {
|
|
3759
|
+
if (!ctx) {
|
|
3760
|
+
throw new errors_js_1.ValidationError(`[turbine] Column reference { col: "${ref.col}" } is not supported in this filter context.`);
|
|
3761
|
+
}
|
|
3762
|
+
return `${ctx.prefix}${this.q(this.resolveColumnRef(ref, ctx, mode))}`;
|
|
3763
|
+
}
|
|
3680
3764
|
/**
|
|
3681
3765
|
* Build SQL clauses for a single operator object on a column.
|
|
3682
3766
|
* Each operator key becomes its own clause, all ANDed together.
|
|
3767
|
+
*
|
|
3768
|
+
* `equals`/`not`/`gt`/`gte`/`lt`/`lte` also accept a {@link ColumnRef}
|
|
3769
|
+
* (`{ col: 'otherField' }`) which compiles to a column-to-column comparison
|
|
3770
|
+
* against `refCtx`: no param bound, so `collectOperatorParams` mirrors by
|
|
3771
|
+
* pushing nothing and the referenced name lives in the fingerprint.
|
|
3683
3772
|
*/
|
|
3684
|
-
buildOperatorClauses(column, op, params) {
|
|
3773
|
+
buildOperatorClauses(column, op, params, refCtx) {
|
|
3685
3774
|
const clauses = [];
|
|
3686
3775
|
if (op.equals !== undefined) {
|
|
3687
3776
|
if (op.equals === null) {
|
|
3688
3777
|
clauses.push(`${column} IS NULL`);
|
|
3689
3778
|
}
|
|
3779
|
+
else if ((0, filters_js_1.isColumnRef)(op.equals)) {
|
|
3780
|
+
clauses.push(`${column} = ${this.columnRefSql(op.equals, refCtx, op.mode)}`);
|
|
3781
|
+
}
|
|
3690
3782
|
else {
|
|
3691
3783
|
(0, filters_js_1.assertBindableEqualsOperand)(op.equals, column);
|
|
3692
3784
|
params.push(op.equals);
|
|
@@ -3694,25 +3786,48 @@ class QueryInterface {
|
|
|
3694
3786
|
}
|
|
3695
3787
|
}
|
|
3696
3788
|
if (op.gt !== undefined) {
|
|
3697
|
-
|
|
3698
|
-
|
|
3789
|
+
if ((0, filters_js_1.isColumnRef)(op.gt)) {
|
|
3790
|
+
clauses.push(`${column} > ${this.columnRefSql(op.gt, refCtx, op.mode)}`);
|
|
3791
|
+
}
|
|
3792
|
+
else {
|
|
3793
|
+
params.push(op.gt);
|
|
3794
|
+
clauses.push(`${column} > ${this.p(params.length)}`);
|
|
3795
|
+
}
|
|
3699
3796
|
}
|
|
3700
3797
|
if (op.gte !== undefined) {
|
|
3701
|
-
|
|
3702
|
-
|
|
3798
|
+
if ((0, filters_js_1.isColumnRef)(op.gte)) {
|
|
3799
|
+
clauses.push(`${column} >= ${this.columnRefSql(op.gte, refCtx, op.mode)}`);
|
|
3800
|
+
}
|
|
3801
|
+
else {
|
|
3802
|
+
params.push(op.gte);
|
|
3803
|
+
clauses.push(`${column} >= ${this.p(params.length)}`);
|
|
3804
|
+
}
|
|
3703
3805
|
}
|
|
3704
3806
|
if (op.lt !== undefined) {
|
|
3705
|
-
|
|
3706
|
-
|
|
3807
|
+
if ((0, filters_js_1.isColumnRef)(op.lt)) {
|
|
3808
|
+
clauses.push(`${column} < ${this.columnRefSql(op.lt, refCtx, op.mode)}`);
|
|
3809
|
+
}
|
|
3810
|
+
else {
|
|
3811
|
+
params.push(op.lt);
|
|
3812
|
+
clauses.push(`${column} < ${this.p(params.length)}`);
|
|
3813
|
+
}
|
|
3707
3814
|
}
|
|
3708
3815
|
if (op.lte !== undefined) {
|
|
3709
|
-
|
|
3710
|
-
|
|
3816
|
+
if ((0, filters_js_1.isColumnRef)(op.lte)) {
|
|
3817
|
+
clauses.push(`${column} <= ${this.columnRefSql(op.lte, refCtx, op.mode)}`);
|
|
3818
|
+
}
|
|
3819
|
+
else {
|
|
3820
|
+
params.push(op.lte);
|
|
3821
|
+
clauses.push(`${column} <= ${this.p(params.length)}`);
|
|
3822
|
+
}
|
|
3711
3823
|
}
|
|
3712
3824
|
if (op.not !== undefined) {
|
|
3713
3825
|
if (op.not === null) {
|
|
3714
3826
|
clauses.push(`${column} IS NOT NULL`);
|
|
3715
3827
|
}
|
|
3828
|
+
else if ((0, filters_js_1.isColumnRef)(op.not)) {
|
|
3829
|
+
clauses.push(`${column} != ${this.columnRefSql(op.not, refCtx, op.mode)}`);
|
|
3830
|
+
}
|
|
3716
3831
|
else {
|
|
3717
3832
|
params.push(op.not);
|
|
3718
3833
|
clauses.push(`${column} != ${this.p(params.length)}`);
|
|
@@ -3763,6 +3878,11 @@ class QueryInterface {
|
|
|
3763
3878
|
if ((0, filters_js_1.isVectorOrderBy)(d)) {
|
|
3764
3879
|
return `vec(${d.distance.metric},${d.distance.direction ?? 'asc'})`;
|
|
3765
3880
|
}
|
|
3881
|
+
// JSON-path ordering: direction, cast kind, and nulls placement change the
|
|
3882
|
+
// SQL text; the path itself is a bound param and stays OUT of the key.
|
|
3883
|
+
if ((0, filters_js_1.isJsonPathOrderBy)(d)) {
|
|
3884
|
+
return `jp(${d.direction ?? 'asc'},${d.type === 'numeric' ? 'num' : 'text'},${d.nulls ?? ''})`;
|
|
3885
|
+
}
|
|
3766
3886
|
if ((0, filters_js_1.isOrderBySpec)(d))
|
|
3767
3887
|
return `spec(${d.sort},${d.nulls ?? ''})`;
|
|
3768
3888
|
if (d && typeof d === 'object') {
|
|
@@ -3808,6 +3928,11 @@ class QueryInterface {
|
|
|
3808
3928
|
const safeDir = value.distance.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
3809
3929
|
return `${this.q(rawColumn)} ${operator} ${placeholder} ${safeDir}`;
|
|
3810
3930
|
}
|
|
3931
|
+
// JSON-path ordering: { path: [...], direction?, type?, nulls? } on a
|
|
3932
|
+
// json/jsonb column of THIS table. Path is bound as one text[] param.
|
|
3933
|
+
if ((0, filters_js_1.isJsonPathOrderBy)(value)) {
|
|
3934
|
+
return this.buildJsonPathOrderEntry(this.table, this.tableMeta, key, value, '', params);
|
|
3935
|
+
}
|
|
3811
3936
|
// Relation ordering: an object value that is not a vector or OrderBySpec,
|
|
3812
3937
|
// keyed by a relation name (`{ posts: { _count: 'desc' } }` / `{ author:
|
|
3813
3938
|
// { name: 'asc' } }`).
|
|
@@ -3834,6 +3959,7 @@ class QueryInterface {
|
|
|
3834
3959
|
value !== null &&
|
|
3835
3960
|
!Array.isArray(value) &&
|
|
3836
3961
|
!(0, filters_js_1.isVectorOrderBy)(value) &&
|
|
3962
|
+
!(0, filters_js_1.isJsonPathOrderBy)(value) &&
|
|
3837
3963
|
!(0, filters_js_1.isOrderBySpec)(value));
|
|
3838
3964
|
}
|
|
3839
3965
|
/**
|
|
@@ -3850,6 +3976,63 @@ class QueryInterface {
|
|
|
3850
3976
|
}
|
|
3851
3977
|
return nulls === 'first' ? ' NULLS FIRST' : ' NULLS LAST';
|
|
3852
3978
|
}
|
|
3979
|
+
/**
|
|
3980
|
+
* Resolve an orderBy key to its snake_case column via the table's columnMap
|
|
3981
|
+
* (camelToSnake fallback), throwing the SAME unknown-field E003 the top-level
|
|
3982
|
+
* where path uses. Shared by top-level JSON-path ordering and every nested
|
|
3983
|
+
* relation orderBy path so nested orderBy accepts exactly what top-level
|
|
3984
|
+
* accepts (the 0.30.x bug: nested orderBy skipped the columnMap and rejected
|
|
3985
|
+
* camelCase-named DB columns like "sortOrder").
|
|
3986
|
+
*/
|
|
3987
|
+
resolveOrderByColumn(table, meta, key) {
|
|
3988
|
+
const col = meta.columnMap[key] ?? (0, schema_js_1.camelToSnake)(key);
|
|
3989
|
+
if (!meta.allColumns.includes(col)) {
|
|
3990
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${table}". ` +
|
|
3991
|
+
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
3992
|
+
}
|
|
3993
|
+
return col;
|
|
3994
|
+
}
|
|
3995
|
+
/**
|
|
3996
|
+
* Validate a {@link JsonPathOrderBy} entry: column must exist AND be
|
|
3997
|
+
* json/jsonb, path must be a non-empty array of keys/indexes: and return
|
|
3998
|
+
* the resolved column. Shared by the SQL-build path
|
|
3999
|
+
* ({@link buildJsonPathOrderEntry}) and the cache-hit param-collect mirrors
|
|
4000
|
+
* so both always throw identically.
|
|
4001
|
+
*/
|
|
4002
|
+
validateJsonPathOrderBy(table, meta, field, spec) {
|
|
4003
|
+
const col = this.resolveOrderByColumn(table, meta, field);
|
|
4004
|
+
if (spec.path.length === 0 ||
|
|
4005
|
+
spec.path.some((el) => typeof el !== 'string' && !(typeof el === 'number' && Number.isFinite(el)))) {
|
|
4006
|
+
throw new errors_js_1.ValidationError(`[turbine] JSON-path orderBy on "${field}" (table "${table}") requires a non-empty \`path\` array ` +
|
|
4007
|
+
`of keys/indexes (e.g. { path: ['weight'], direction: 'asc' }).`);
|
|
4008
|
+
}
|
|
4009
|
+
const colType = this.pgTypeForColumn(meta, col);
|
|
4010
|
+
if (colType !== 'json' && colType !== 'jsonb') {
|
|
4011
|
+
throw new errors_js_1.ValidationError(`[turbine] JSON-path orderBy on "${field}": column "${col}" on table "${table}" is not a JSON column ` +
|
|
4012
|
+
`(actual type: ${colType}).`);
|
|
4013
|
+
}
|
|
4014
|
+
return col;
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Compile one {@link JsonPathOrderBy} entry:
|
|
4018
|
+
* `("col" #>> $n::text[])::numeric ASC`: the numeric cast only with
|
|
4019
|
+
* `type: 'numeric'` (default is text comparison), the extraction routed
|
|
4020
|
+
* through the dialect's JSON hook exactly like the JSON where-filters, the
|
|
4021
|
+
* path bound as ONE text[] param (mirrored by the order-param collectors).
|
|
4022
|
+
* `prefix` scopes the column (`''` top-level, `t0.` inside a relation
|
|
4023
|
+
* subquery).
|
|
4024
|
+
*/
|
|
4025
|
+
buildJsonPathOrderEntry(table, meta, field, spec, prefix, params) {
|
|
4026
|
+
const col = this.validateJsonPathOrderBy(table, meta, field, spec);
|
|
4027
|
+
if (!params) {
|
|
4028
|
+
throw new errors_js_1.ValidationError(`[turbine] JSON-path ordering on "${field}" is not supported in this orderBy context.`);
|
|
4029
|
+
}
|
|
4030
|
+
params.push(spec.path.map(String));
|
|
4031
|
+
const extract = this.dialect.buildJsonPathExtract(`${prefix}${this.q(col)}`, this.p(params.length));
|
|
4032
|
+
const lhs = spec.type === 'numeric' ? this.castJsonNumeric(extract) : extract;
|
|
4033
|
+
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
4034
|
+
return `${lhs} ${dir}${this.nullsSuffix(spec.nulls)}`;
|
|
4035
|
+
}
|
|
3853
4036
|
/**
|
|
3854
4037
|
* Compile a relation ordering term. For a to-many relation the only allowed
|
|
3855
4038
|
* key is `_count`, which becomes a correlated `COUNT(*)` subquery. For a
|
|
@@ -3858,12 +4041,19 @@ class QueryInterface {
|
|
|
3858
4041
|
*
|
|
3859
4042
|
* Validation: relation must exist (E005); to-many only allows `_count`, and
|
|
3860
4043
|
* to-one only allows real target columns (E003).
|
|
4044
|
+
*
|
|
4045
|
+
* `ctx` generalizes the term beyond the root table: inside a relation
|
|
4046
|
+
* subquery's orderBy the relations live on the TARGET table's metadata and
|
|
4047
|
+
* the correlation parent is the relation's alias, not `this.table`.
|
|
3861
4048
|
*/
|
|
3862
|
-
buildRelationOrderBy(relName, value, alias, params) {
|
|
3863
|
-
const
|
|
4049
|
+
buildRelationOrderBy(relName, value, alias, params, ctx) {
|
|
4050
|
+
const ownerMeta = ctx?.meta ?? this.tableMeta;
|
|
4051
|
+
const ownerTable = ctx?.table ?? this.table;
|
|
4052
|
+
const parentRef = ctx?.parentRef ?? this.table;
|
|
4053
|
+
const relDef = ownerMeta.relations[relName];
|
|
3864
4054
|
if (!relDef) {
|
|
3865
|
-
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" in orderBy on table "${
|
|
3866
|
-
`Available: ${Object.keys(
|
|
4055
|
+
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" in orderBy on table "${ownerTable}". ` +
|
|
4056
|
+
`Available: ${Object.keys(ownerMeta.relations).join(', ')}`);
|
|
3867
4057
|
}
|
|
3868
4058
|
// To-many: only `_count` is meaningful → correlated COUNT(*) subquery.
|
|
3869
4059
|
if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
@@ -3873,14 +4063,14 @@ class QueryInterface {
|
|
|
3873
4063
|
`(got: ${keys.join(', ') || '(empty)'}).`);
|
|
3874
4064
|
}
|
|
3875
4065
|
const { dir } = (0, filters_js_1.normalizeOrderBy)(value._count);
|
|
3876
|
-
return `${this.buildRelationCountExpr(relDef,
|
|
4066
|
+
return `${this.buildRelationCountExpr(relDef, parentRef, alias, params)} ${dir}`;
|
|
3877
4067
|
}
|
|
3878
4068
|
// To-one: each entry orders by a correlated scalar subquery on a target column.
|
|
3879
4069
|
const targetMeta = this.schema.tables[relDef.to];
|
|
3880
4070
|
if (!targetMeta)
|
|
3881
4071
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${relDef.to}"`);
|
|
3882
4072
|
const qTarget = this.q(relDef.to);
|
|
3883
|
-
const qParent = this.q(
|
|
4073
|
+
const qParent = this.q(parentRef);
|
|
3884
4074
|
// belongsTo: alias.referenceKey = parent.foreignKey; hasOne: reversed.
|
|
3885
4075
|
const correlation = relDef.type === 'belongsTo'
|
|
3886
4076
|
? this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey)
|
|
@@ -3891,7 +4081,9 @@ class QueryInterface {
|
|
|
3891
4081
|
}
|
|
3892
4082
|
return entries
|
|
3893
4083
|
.map(([col, dirValue]) => {
|
|
3894
|
-
|
|
4084
|
+
// columnMap-first resolution (camelToSnake fallback): mirrors the
|
|
4085
|
+
// scalar orderBy path so camelCase-named DB columns resolve here too.
|
|
4086
|
+
const snakeCol = targetMeta.columnMap[col] ?? (0, schema_js_1.camelToSnake)(col);
|
|
3895
4087
|
if (!targetMeta.allColumns.includes(snakeCol)) {
|
|
3896
4088
|
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").`);
|
|
3897
4089
|
}
|
|
@@ -3909,6 +4101,74 @@ class QueryInterface {
|
|
|
3909
4101
|
})
|
|
3910
4102
|
.join(', ');
|
|
3911
4103
|
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Compile the ORDER BY terms of a relation `with` clause against the
|
|
4106
|
+
* relation's table alias. One unified path for every relation shape
|
|
4107
|
+
* (hasMany / manyToMany / belongsTo / hasOne) supporting exactly what the
|
|
4108
|
+
* top-level orderBy accepts at this level:
|
|
4109
|
+
*
|
|
4110
|
+
* - scalar columns via columnMap resolution (camelToSnake fallback) with
|
|
4111
|
+
* {@link OrderBySpec} nulls placement,
|
|
4112
|
+
* - {@link JsonPathOrderBy} entries (path bound as one text[] param),
|
|
4113
|
+
* - relation ordering on the TARGET's relations (`_count` for to-many, a
|
|
4114
|
+
* target column for to-one), correlated to the relation alias,
|
|
4115
|
+
* - vector KNN ordering stays top-level-only (E003, same as before).
|
|
4116
|
+
*
|
|
4117
|
+
* Param pushes (JSON paths, relation-order global filters) MUST be mirrored,
|
|
4118
|
+
* in the same order, by {@link collectRelationOrderParams}.
|
|
4119
|
+
*/
|
|
4120
|
+
buildRelationOrderClause(targetTable, targetMeta, alias, orderEntries, params) {
|
|
4121
|
+
let relOrdCounter = 0;
|
|
4122
|
+
const orders = orderEntries
|
|
4123
|
+
.map(([key, dirValue]) => {
|
|
4124
|
+
if ((0, filters_js_1.isVectorOrderBy)(dirValue)) {
|
|
4125
|
+
throw new errors_js_1.ValidationError(`[turbine] Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
|
|
4126
|
+
}
|
|
4127
|
+
if ((0, filters_js_1.isJsonPathOrderBy)(dirValue)) {
|
|
4128
|
+
return this.buildJsonPathOrderEntry(targetTable, targetMeta, key, dirValue, `${alias}.`, params);
|
|
4129
|
+
}
|
|
4130
|
+
if (this.isRelationOrderByValue(dirValue)) {
|
|
4131
|
+
return this.buildRelationOrderBy(key, dirValue, `${alias}ord${relOrdCounter++}`, params, { meta: targetMeta, table: targetTable, parentRef: alias });
|
|
4132
|
+
}
|
|
4133
|
+
const col = this.resolveOrderByColumn(targetTable, targetMeta, key);
|
|
4134
|
+
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
|
|
4135
|
+
return `${alias}.${this.q(col)} ${dir}${this.nullsSuffix(nulls)}`;
|
|
4136
|
+
})
|
|
4137
|
+
.join(', ');
|
|
4138
|
+
return ` ORDER BY ${orders}`;
|
|
4139
|
+
}
|
|
4140
|
+
/**
|
|
4141
|
+
* Param-collect mirror of {@link buildRelationOrderClause}: JSON-path
|
|
4142
|
+
* entries push their path (one text[] param each); relation-order entries
|
|
4143
|
+
* mirror {@link collectOrderByParams}' relation branch (count / to-one
|
|
4144
|
+
* global-filter params); scalar entries push nothing but re-run the same
|
|
4145
|
+
* column validation so a warmed cache can never skip it.
|
|
4146
|
+
*/
|
|
4147
|
+
collectRelationOrderParams(targetTable, targetMeta, orderEntries, params) {
|
|
4148
|
+
for (const [key, dirValue] of orderEntries) {
|
|
4149
|
+
if ((0, filters_js_1.isVectorOrderBy)(dirValue)) {
|
|
4150
|
+
throw new errors_js_1.ValidationError(`[turbine] Vector distance ordering on "${key}" is only supported in a top-level findMany orderBy.`);
|
|
4151
|
+
}
|
|
4152
|
+
if ((0, filters_js_1.isJsonPathOrderBy)(dirValue)) {
|
|
4153
|
+
this.validateJsonPathOrderBy(targetTable, targetMeta, key, dirValue);
|
|
4154
|
+
params.push(dirValue.path.map(String));
|
|
4155
|
+
continue;
|
|
4156
|
+
}
|
|
4157
|
+
if (this.isRelationOrderByValue(dirValue)) {
|
|
4158
|
+
const relDef = targetMeta.relations[key];
|
|
4159
|
+
if (relDef && (relDef.type === 'hasMany' || relDef.type === 'manyToMany')) {
|
|
4160
|
+
this.collectRelationCountParams(relDef, params);
|
|
4161
|
+
}
|
|
4162
|
+
else if (relDef) {
|
|
4163
|
+
for (const _col of Object.keys(dirValue)) {
|
|
4164
|
+
this.collectTargetGlobalFilterAlias(relDef.to, params);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
continue;
|
|
4168
|
+
}
|
|
4169
|
+
this.resolveOrderByColumn(targetTable, targetMeta, key);
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
3912
4172
|
/**
|
|
3913
4173
|
* Build a correlated `(SELECT COUNT(*) …)` scalar subquery for a to-many
|
|
3914
4174
|
* relation, correlated to `parentRef`. hasMany counts child rows via the FK;
|
|
@@ -4628,20 +4888,13 @@ class QueryInterface {
|
|
|
4628
4888
|
// Quote parent ref — can be a table name or auto-generated alias
|
|
4629
4889
|
const qParent = this.q(parentRef);
|
|
4630
4890
|
const qTarget = this.q(targetTable);
|
|
4631
|
-
// Build ORDER BY for json_agg
|
|
4891
|
+
// Build ORDER BY for json_agg: unified with the top-level orderBy surface
|
|
4892
|
+
// (columnMap resolution, OrderBySpec nulls, JSON-path, relation ordering).
|
|
4893
|
+
// Param pushes here land BEFORE the spec.where params, mirrored by
|
|
4894
|
+
// collectRelationSubqueryParams.
|
|
4632
4895
|
let orderClause = '';
|
|
4633
4896
|
if (relOrderEntries.length > 0) {
|
|
4634
|
-
|
|
4635
|
-
.map(([k, dirValue]) => {
|
|
4636
|
-
const col = (0, schema_js_1.camelToSnake)(k);
|
|
4637
|
-
if (!targetMeta.allColumns.includes(col)) {
|
|
4638
|
-
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
|
|
4639
|
-
}
|
|
4640
|
-
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
|
|
4641
|
-
return `${alias}.${this.q(col)} ${dir}${this.nullsSuffix(nulls)}`;
|
|
4642
|
-
})
|
|
4643
|
-
.join(', ');
|
|
4644
|
-
orderClause = ` ORDER BY ${orders}`;
|
|
4897
|
+
orderClause = this.buildRelationOrderClause(targetTable, targetMeta, alias, relOrderEntries, params);
|
|
4645
4898
|
}
|
|
4646
4899
|
// Build WHERE — correlate to parent via parentRef (alias or table name).
|
|
4647
4900
|
// For hasMany/hasOne: TARGET has the FK (RelationDef.foreignKey is always
|
|
@@ -4715,8 +4968,10 @@ class QueryInterface {
|
|
|
4715
4968
|
const inlineOrder = this.dialect.aggSupportsInlineOrderBy ? orderClause.trim() || undefined : undefined;
|
|
4716
4969
|
return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, inlineOrder)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
4717
4970
|
}
|
|
4718
|
-
// belongsTo / hasOne
|
|
4719
|
-
|
|
4971
|
+
// belongsTo / hasOne: return single object. An orderBy picks WHICH row
|
|
4972
|
+
// the LIMIT 1 keeps (deterministic hasOne over a non-unique FK): matching
|
|
4973
|
+
// the batched strategy, which orders its flat follow-up and takes bucket[0].
|
|
4974
|
+
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause} LIMIT 1`;
|
|
4720
4975
|
}
|
|
4721
4976
|
/**
|
|
4722
4977
|
* Build the json_agg subquery for a `manyToMany` relation, JOINing the target
|
|
@@ -4774,22 +5029,15 @@ class QueryInterface {
|
|
|
4774
5029
|
let whereClause = sourceKeys
|
|
4775
5030
|
.map((jcol, i) => `${jalias}.${this.q(jcol)} = ${qParent}.${this.q(refKeys[i])}`)
|
|
4776
5031
|
.join(' AND ');
|
|
4777
|
-
// ORDER BY on the target rows
|
|
4778
|
-
//
|
|
5032
|
+
// ORDER BY on the target rows: unified with the top-level orderBy surface
|
|
5033
|
+
// (columnMap resolution, OrderBySpec nulls, JSON-path, relation ordering).
|
|
5034
|
+
// `orderBy: {}` (no defined entries) is treated as absent: it must not
|
|
5035
|
+
// render a dangling `ORDER BY `. Param pushes here land BEFORE the
|
|
5036
|
+
// spec.where params, mirrored by collectRelationSubqueryParams' m2m branch.
|
|
4779
5037
|
const relOrderEntries = spec !== true && spec.orderBy ? Object.entries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
4780
5038
|
let orderClause = '';
|
|
4781
5039
|
if (relOrderEntries.length > 0) {
|
|
4782
|
-
|
|
4783
|
-
.map(([k, dirValue]) => {
|
|
4784
|
-
const col = (0, schema_js_1.camelToSnake)(k);
|
|
4785
|
-
if (!targetMeta.allColumns.includes(col)) {
|
|
4786
|
-
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
|
|
4787
|
-
}
|
|
4788
|
-
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
|
|
4789
|
-
return `${talias}.${this.q(col)} ${dir}${this.nullsSuffix(nulls)}`;
|
|
4790
|
-
})
|
|
4791
|
-
.join(', ');
|
|
4792
|
-
orderClause = ` ORDER BY ${orders}`;
|
|
5040
|
+
orderClause = this.buildRelationOrderClause(targetTable, targetMeta, talias, relOrderEntries, params);
|
|
4793
5041
|
}
|
|
4794
5042
|
// Additional WHERE filters on the target — full scalar where surface,
|
|
4795
5043
|
// properly parameterized against the target alias.
|