turbine-orm 0.73.1 → 0.74.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/powql.d.ts +38 -0
- package/dist/cjs/powql.js +109 -13
- package/dist/cjs/query/builder.js +8 -50
- package/dist/cjs/query/filters.d.ts +8 -0
- package/dist/cjs/query/filters.js +16 -0
- package/dist/cjs/query/relation-names.d.ts +18 -0
- package/dist/cjs/query/relation-names.js +60 -0
- package/dist/cjs/query/where.d.ts +14 -0
- package/dist/cjs/query/where.js +21 -5
- package/dist/powql.d.ts +38 -0
- package/dist/powql.js +113 -17
- package/dist/query/builder.js +8 -50
- package/dist/query/filters.d.ts +8 -0
- package/dist/query/filters.js +15 -0
- package/dist/query/relation-names.d.ts +18 -0
- package/dist/query/relation-names.js +60 -1
- package/dist/query/where.d.ts +14 -0
- package/dist/query/where.js +20 -5
- package/package.json +1 -1
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -196,7 +196,25 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
196
196
|
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
197
197
|
* not `skip` pages differently from one that reads both.
|
|
198
198
|
*/
|
|
199
|
+
/**
|
|
200
|
+
* Fill in the deterministic per-relation ordering `stableRelationOrder` asks
|
|
201
|
+
* for, through the shared transform the SQL engines use.
|
|
202
|
+
*
|
|
203
|
+
* PowDB accepted this option and did nothing with it before 0.74.0, so
|
|
204
|
+
* relation rows came back in whatever order the engine produced, from the one
|
|
205
|
+
* option whose entire purpose is that they do not. Same resolution as
|
|
206
|
+
* QueryInterface: a per-query flag wins, else the client-level default.
|
|
207
|
+
*/
|
|
208
|
+
private withStableRelationOrder;
|
|
199
209
|
private normalizeArgs;
|
|
210
|
+
/**
|
|
211
|
+
* `jsonEncoding` selects between PostgreSQL's `json_build_object` and
|
|
212
|
+
* `json_build_array` row encodings for relation subqueries. PowQL emits no
|
|
213
|
+
* JSON row encoding at all, so there is nothing for the option to select and
|
|
214
|
+
* it was simply dropped: the caller asked for a wire format and got silence.
|
|
215
|
+
* Refused for the same reason `forceCustomPlan` is.
|
|
216
|
+
*/
|
|
217
|
+
private assertNoJsonEncoding;
|
|
200
218
|
private assertNoForceCustomPlan;
|
|
201
219
|
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
202
220
|
private assertIdentifiesOneRow;
|
|
@@ -883,5 +901,25 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
883
901
|
* `{ OR: [{ f: undefined }] }`, compiles to the empty string and is refused,
|
|
884
902
|
* because emitting a filter-less write would hit every row.
|
|
885
903
|
*/
|
|
904
|
+
/**
|
|
905
|
+
* AND this table's configured global filter (soft-delete / multi-tenancy)
|
|
906
|
+
* onto an ALREADY-COMPILED user filter, pushing its params after the user's.
|
|
907
|
+
*
|
|
908
|
+
* Before 0.74.0 `PowqlInterface` did not read `globalFilters` at all, so a
|
|
909
|
+
* client configured for multi-tenancy applied its tenant predicate on every
|
|
910
|
+
* SQL engine and NONE on PowDB, silently, and `skipGlobalFilters` was
|
|
911
|
+
* accepted and inert. The rule itself is shared
|
|
912
|
+
* ({@link resolveGlobalFilterFrom}) rather than restated here.
|
|
913
|
+
*
|
|
914
|
+
* It takes a COMPILED user filter, and that ordering is the contract, not an
|
|
915
|
+
* implementation detail: the empty-`where` guard must see the USER filter
|
|
916
|
+
* alone, or a configured global filter would silently satisfy it and turn a
|
|
917
|
+
* refused mass mutation into an accepted one. Every write call site therefore
|
|
918
|
+
* calls {@link assertCompiledWhere} first and this second.
|
|
919
|
+
*
|
|
920
|
+
* `alias` is forwarded so the nested/native-join paths qualify the filter's
|
|
921
|
+
* column references the same way they qualify the user's.
|
|
922
|
+
*/
|
|
923
|
+
private applyGlobalFilter;
|
|
886
924
|
private assertCompiledWhere;
|
|
887
925
|
}
|
package/dist/cjs/powql.js
CHANGED
|
@@ -430,8 +430,38 @@ class PowqlInterface {
|
|
|
430
430
|
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
431
431
|
* not `skip` pages differently from one that reads both.
|
|
432
432
|
*/
|
|
433
|
+
/**
|
|
434
|
+
* Fill in the deterministic per-relation ordering `stableRelationOrder` asks
|
|
435
|
+
* for, through the shared transform the SQL engines use.
|
|
436
|
+
*
|
|
437
|
+
* PowDB accepted this option and did nothing with it before 0.74.0, so
|
|
438
|
+
* relation rows came back in whatever order the engine produced, from the one
|
|
439
|
+
* option whose entire purpose is that they do not. Same resolution as
|
|
440
|
+
* QueryInterface: a per-query flag wins, else the client-level default.
|
|
441
|
+
*/
|
|
442
|
+
withStableRelationOrder(args) {
|
|
443
|
+
if (!args?.with)
|
|
444
|
+
return args;
|
|
445
|
+
const on = args.stableRelationOrder ?? this.options.stableRelationOrder === true;
|
|
446
|
+
if (!on)
|
|
447
|
+
return args;
|
|
448
|
+
const ordered = (0, relation_names_js_1.applyStableRelationOrderTo)(this.schema, args.with, this.table);
|
|
449
|
+
return ordered === args.with ? args : { ...args, with: ordered };
|
|
450
|
+
}
|
|
433
451
|
normalizeArgs(args) {
|
|
434
|
-
return (0, utils_js_1.normalizePagination)(this.withDeclaredRelationNames(args));
|
|
452
|
+
return (0, utils_js_1.normalizePagination)(this.withStableRelationOrder(this.withDeclaredRelationNames(args)));
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* `jsonEncoding` selects between PostgreSQL's `json_build_object` and
|
|
456
|
+
* `json_build_array` row encodings for relation subqueries. PowQL emits no
|
|
457
|
+
* JSON row encoding at all, so there is nothing for the option to select and
|
|
458
|
+
* it was simply dropped: the caller asked for a wire format and got silence.
|
|
459
|
+
* Refused for the same reason `forceCustomPlan` is.
|
|
460
|
+
*/
|
|
461
|
+
assertNoJsonEncoding(args) {
|
|
462
|
+
if (args?.jsonEncoding === undefined)
|
|
463
|
+
return;
|
|
464
|
+
throw new errors_js_1.UnsupportedFeatureError('jsonEncoding', 'PowDB', 'PowQL emits no JSON row encoding for relations, so there is nothing to select; omit the option');
|
|
435
465
|
}
|
|
436
466
|
assertNoForceCustomPlan(args) {
|
|
437
467
|
if (args?.forceCustomPlan !== true)
|
|
@@ -1386,6 +1416,7 @@ class PowqlInterface {
|
|
|
1386
1416
|
// -------------------------------------------------------------------------
|
|
1387
1417
|
async findMany(args = {}) {
|
|
1388
1418
|
this.assertNoForceCustomPlan(args);
|
|
1419
|
+
this.assertNoJsonEncoding(args);
|
|
1389
1420
|
args = this.normalizeArgs(args);
|
|
1390
1421
|
return this.withMiddleware('findMany', args, async () => {
|
|
1391
1422
|
// `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
|
|
@@ -1467,7 +1498,8 @@ class PowqlInterface {
|
|
|
1467
1498
|
}
|
|
1468
1499
|
const nest = nestedPlans.length > 0 || linkPlans.length > 0;
|
|
1469
1500
|
const alias = nest ? 't0' : undefined;
|
|
1470
|
-
|
|
1501
|
+
let where = this.buildWhere(resolvedWhere, params, alias);
|
|
1502
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters, alias);
|
|
1471
1503
|
// PowQL's `distinct` is row-wide, so these names never reach the emitted
|
|
1472
1504
|
// statement. They are still caller-supplied names, and a name resolves or
|
|
1473
1505
|
// throws: reading the array for its LENGTH alone let `distinct: ['nope']`
|
|
@@ -1544,6 +1576,7 @@ class PowqlInterface {
|
|
|
1544
1576
|
}
|
|
1545
1577
|
async findUnique(args) {
|
|
1546
1578
|
this.assertNoForceCustomPlan(args);
|
|
1579
|
+
this.assertNoJsonEncoding(args);
|
|
1547
1580
|
args = this.normalizeArgs(args);
|
|
1548
1581
|
// Prisma compound-unique selector → column conjunction (engine parity with
|
|
1549
1582
|
// the SQL findUnique family; pure metadata, so this is a one-line adoption).
|
|
@@ -2638,14 +2671,41 @@ class PowqlInterface {
|
|
|
2638
2671
|
}
|
|
2639
2672
|
const params = [];
|
|
2640
2673
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2674
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2675
|
+
// `false` here refused an empty where even WITH the opt-in, while every
|
|
2676
|
+
// SQL engine accepted it: verified by probe on buildUpdate/buildDelete.
|
|
2677
|
+
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'update');
|
|
2678
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2679
|
+
let setClause = this.buildUpdateAssignments(args.data, params);
|
|
2680
|
+
// Optimistic locking, matching the SQL engines exactly: bump the version
|
|
2681
|
+
// column in the SET, add `version = expected` to the filter, and treat
|
|
2682
|
+
// "matched no row" as a lost race rather than a missing row.
|
|
2683
|
+
//
|
|
2684
|
+
// PowqlInterface ignored `optimisticLock` outright before 0.74.0, which is
|
|
2685
|
+
// worse than not supporting it: the update applied, the caller's version
|
|
2686
|
+
// check never happened, and a concurrent writer's change was overwritten
|
|
2687
|
+
// by a caller who believed they held the lock.
|
|
2688
|
+
const lock = args.optimisticLock;
|
|
2689
|
+
if (lock) {
|
|
2690
|
+
const versionCol = (0, powdb_js_1.quotePowqlIdent)(this.column(lock.field).name);
|
|
2691
|
+
setClause += `, ${versionCol} := ${this.ref(lock.field)} + 1`;
|
|
2692
|
+
params.push(lock.expected);
|
|
2693
|
+
const check = `${this.ref(lock.field)} = $${params.length}`;
|
|
2694
|
+
where = where ? `(${where}) and (${check})` : check;
|
|
2695
|
+
}
|
|
2644
2696
|
// `returning` hands back the post-update row(s); take the first (single-row contract).
|
|
2645
2697
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout, 'update');
|
|
2646
2698
|
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
2647
|
-
if (!row)
|
|
2699
|
+
if (!row) {
|
|
2700
|
+
if (lock) {
|
|
2701
|
+
throw new errors_js_1.OptimisticLockError({
|
|
2702
|
+
table: this.table,
|
|
2703
|
+
versionField: lock.field,
|
|
2704
|
+
expectedVersion: lock.expected,
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2648
2707
|
throw new errors_js_1.NotFoundError({ table: this.table, where: args.where });
|
|
2708
|
+
}
|
|
2649
2709
|
return row;
|
|
2650
2710
|
});
|
|
2651
2711
|
}
|
|
@@ -2653,8 +2713,9 @@ class PowqlInterface {
|
|
|
2653
2713
|
return this.withMiddleware('updateMany', args, async () => {
|
|
2654
2714
|
const params = [];
|
|
2655
2715
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2656
|
-
|
|
2716
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2657
2717
|
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'updateMany');
|
|
2718
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2658
2719
|
const setClause = this.buildUpdateAssignments(args.data, params);
|
|
2659
2720
|
const filter = where ? ` filter ${where}` : '';
|
|
2660
2721
|
const { rowCount } = await this.exec(`${this.qt}${filter} update { ${setClause} }`, params, args.timeout, 'updateMany');
|
|
@@ -2789,8 +2850,9 @@ class PowqlInterface {
|
|
|
2789
2850
|
return this.withMiddleware('delete', args, async () => {
|
|
2790
2851
|
const params = [];
|
|
2791
2852
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2792
|
-
|
|
2793
|
-
this.assertCompiledWhere(where,
|
|
2853
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2854
|
+
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'delete');
|
|
2855
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2794
2856
|
// `returning` hands back the deleted row(s), no separate pre-image reselect needed.
|
|
2795
2857
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
2796
2858
|
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
@@ -2803,8 +2865,9 @@ class PowqlInterface {
|
|
|
2803
2865
|
return this.withMiddleware('deleteMany', args, async () => {
|
|
2804
2866
|
const params = [];
|
|
2805
2867
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2806
|
-
|
|
2868
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2807
2869
|
this.assertCompiledWhere(where, (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan'), 'deleteMany');
|
|
2870
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2808
2871
|
const filter = where ? ` filter ${where}` : '';
|
|
2809
2872
|
const { rowCount } = await this.exec(`${this.qt}${filter} delete`, params, args.timeout, 'deleteMany');
|
|
2810
2873
|
return { count: rowCount };
|
|
@@ -2875,7 +2938,8 @@ class PowqlInterface {
|
|
|
2875
2938
|
return this.withMiddleware('count', (args ?? {}), async () => {
|
|
2876
2939
|
const params = [];
|
|
2877
2940
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2878
|
-
|
|
2941
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2942
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2879
2943
|
const filter = where ? ` filter ${where}` : '';
|
|
2880
2944
|
const { rows } = await this.exec(`count(${this.qt}${filter})`, params, args.timeout, 'count');
|
|
2881
2945
|
return Number((rows[0]?.value ?? rows[0]?.count ?? 0));
|
|
@@ -2916,7 +2980,8 @@ class PowqlInterface {
|
|
|
2916
2980
|
const result = {};
|
|
2917
2981
|
const filterParams = [];
|
|
2918
2982
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2919
|
-
|
|
2983
|
+
let where = this.buildWhere(resolvedWhere, filterParams);
|
|
2984
|
+
where = this.applyGlobalFilter(where, filterParams, args.skipGlobalFilters);
|
|
2920
2985
|
const filter = where ? ` filter ${where}` : '';
|
|
2921
2986
|
const scalar = async (expr) => {
|
|
2922
2987
|
const params = [...filterParams];
|
|
@@ -2978,7 +3043,8 @@ class PowqlInterface {
|
|
|
2978
3043
|
const emitNative = Boolean(this.capabilities.nativeRaw);
|
|
2979
3044
|
const params = [];
|
|
2980
3045
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2981
|
-
|
|
3046
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
3047
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2982
3048
|
const filter = where ? ` filter ${where}` : '';
|
|
2983
3049
|
// Result-key namespace, mirroring the SQL builder's `claimResultKey`
|
|
2984
3050
|
// (query/builder.ts): a group-key / aggregate output-name collision (with
|
|
@@ -3331,6 +3397,36 @@ class PowqlInterface {
|
|
|
3331
3397
|
* `{ OR: [{ f: undefined }] }`, compiles to the empty string and is refused,
|
|
3332
3398
|
* because emitting a filter-less write would hit every row.
|
|
3333
3399
|
*/
|
|
3400
|
+
/**
|
|
3401
|
+
* AND this table's configured global filter (soft-delete / multi-tenancy)
|
|
3402
|
+
* onto an ALREADY-COMPILED user filter, pushing its params after the user's.
|
|
3403
|
+
*
|
|
3404
|
+
* Before 0.74.0 `PowqlInterface` did not read `globalFilters` at all, so a
|
|
3405
|
+
* client configured for multi-tenancy applied its tenant predicate on every
|
|
3406
|
+
* SQL engine and NONE on PowDB, silently, and `skipGlobalFilters` was
|
|
3407
|
+
* accepted and inert. The rule itself is shared
|
|
3408
|
+
* ({@link resolveGlobalFilterFrom}) rather than restated here.
|
|
3409
|
+
*
|
|
3410
|
+
* It takes a COMPILED user filter, and that ordering is the contract, not an
|
|
3411
|
+
* implementation detail: the empty-`where` guard must see the USER filter
|
|
3412
|
+
* alone, or a configured global filter would silently satisfy it and turn a
|
|
3413
|
+
* refused mass mutation into an accepted one. Every write call site therefore
|
|
3414
|
+
* calls {@link assertCompiledWhere} first and this second.
|
|
3415
|
+
*
|
|
3416
|
+
* `alias` is forwarded so the nested/native-join paths qualify the filter's
|
|
3417
|
+
* column references the same way they qualify the user's.
|
|
3418
|
+
*/
|
|
3419
|
+
applyGlobalFilter(compiledUserWhere, params, skip, alias) {
|
|
3420
|
+
const gf = (0, where_js_1.resolveGlobalFilterFrom)(this.options.globalFilters, this.table, (0, types_js_1.resolveSkipGlobalFilters)(skip));
|
|
3421
|
+
if (!gf)
|
|
3422
|
+
return compiledUserWhere;
|
|
3423
|
+
const gfWhere = this.buildWhere(gf, params, alias);
|
|
3424
|
+
if (!gfWhere)
|
|
3425
|
+
return compiledUserWhere;
|
|
3426
|
+
if (!compiledUserWhere)
|
|
3427
|
+
return gfWhere;
|
|
3428
|
+
return `(${compiledUserWhere}) and (${gfWhere})`;
|
|
3429
|
+
}
|
|
3334
3430
|
assertCompiledWhere(compiledWhere, allow, action) {
|
|
3335
3431
|
if (allow === true)
|
|
3336
3432
|
return;
|
|
@@ -444,14 +444,8 @@ function unlockNestedWriteTx(tx) {
|
|
|
444
444
|
* orderBy is never overwritten while an empty `{}` / `[]` still gets the
|
|
445
445
|
* synthesized PK order.
|
|
446
446
|
*/
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return orderBy.length === 0;
|
|
450
|
-
if (orderBy && typeof orderBy === 'object') {
|
|
451
|
-
return Object.values(orderBy).every((v) => v === undefined);
|
|
452
|
-
}
|
|
453
|
-
return orderBy === undefined || orderBy === null;
|
|
454
|
-
}
|
|
447
|
+
// Moved to ./filters.ts in 0.74.0 so PowqlInterface consumes the SAME
|
|
448
|
+
// predicate rather than a second copy of it. See applyStableRelationOrderTo.
|
|
455
449
|
/** The two accepted {@link JsonEncoding} values, frozen so the check is total. */
|
|
456
450
|
const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
457
451
|
/**
|
|
@@ -1155,46 +1149,10 @@ class QueryInterface {
|
|
|
1155
1149
|
* nothing stable to order by, so both are left untouched.
|
|
1156
1150
|
*/
|
|
1157
1151
|
applyStableRelationOrder(withClause, table, depth = 0) {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
return withClause;
|
|
1163
|
-
let out;
|
|
1164
|
-
for (const [relName, spec] of Object.entries(withClause)) {
|
|
1165
|
-
if (relName === '_count' || !spec)
|
|
1166
|
-
continue; // `_count` is a count, not a row load
|
|
1167
|
-
const rel = (0, utils_js_1.resolveRelationDef)(meta.relations, relName);
|
|
1168
|
-
if (!rel)
|
|
1169
|
-
continue; // unknown relation, let the build path surface E005
|
|
1170
|
-
const options = spec === true ? {} : spec;
|
|
1171
|
-
// Recurse first so a nested change alone still clones this level.
|
|
1172
|
-
const nestedWith = options.with;
|
|
1173
|
-
const newNested = nestedWith ? this.applyStableRelationOrder(nestedWith, rel.to, depth + 1) : undefined;
|
|
1174
|
-
const nestedChanged = newNested !== undefined && newNested !== nestedWith;
|
|
1175
|
-
const isToMany = rel.type === 'hasMany' || rel.type === 'manyToMany';
|
|
1176
|
-
const hasOrder = options.orderBy !== undefined && !isEmptyOrderBy(options.orderBy);
|
|
1177
|
-
let synthOrder;
|
|
1178
|
-
if (isToMany && !hasOrder) {
|
|
1179
|
-
const targetMeta = this.schema.tables[rel.to];
|
|
1180
|
-
const pk = targetMeta?.primaryKey ?? [];
|
|
1181
|
-
if (targetMeta && pk.length > 0) {
|
|
1182
|
-
const pkFields = pk.map((c) => targetMeta.reverseColumnMap[c] ?? c);
|
|
1183
|
-
synthOrder =
|
|
1184
|
-
pkFields.length === 1 ? { [pkFields[0]]: 'asc' } : pkFields.map((f) => ({ [f]: 'asc' }));
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
if (!synthOrder && !nestedChanged)
|
|
1188
|
-
continue; // nothing to change, keep the ref
|
|
1189
|
-
out ??= { ...withClause };
|
|
1190
|
-
const clonedSpec = { ...options };
|
|
1191
|
-
if (synthOrder)
|
|
1192
|
-
clonedSpec.orderBy = synthOrder;
|
|
1193
|
-
if (nestedChanged)
|
|
1194
|
-
clonedSpec.with = newNested;
|
|
1195
|
-
out[relName] = clonedSpec;
|
|
1196
|
-
}
|
|
1197
|
-
return out ?? withClause;
|
|
1152
|
+
// The transform itself lives in ./relation-names.ts as of 0.74.0, so PowDB
|
|
1153
|
+
// runs the SAME rule instead of a second copy that accepted the option and
|
|
1154
|
+
// did nothing.
|
|
1155
|
+
return (0, relation_names_js_1.applyStableRelationOrderTo)(this.schema, withClause, table, depth);
|
|
1198
1156
|
}
|
|
1199
1157
|
// -------------------------------------------------------------------------
|
|
1200
1158
|
// Deterministic pagination (unordered LIMIT/OFFSET)
|
|
@@ -1281,7 +1239,7 @@ class QueryInterface {
|
|
|
1281
1239
|
return false;
|
|
1282
1240
|
if (args.distinct !== undefined)
|
|
1283
1241
|
return false;
|
|
1284
|
-
if (!isEmptyOrderBy(args.orderBy))
|
|
1242
|
+
if (!(0, filters_js_1.isEmptyOrderBy)(args.orderBy))
|
|
1285
1243
|
return false;
|
|
1286
1244
|
if (args.limit !== undefined || args.offset !== undefined)
|
|
1287
1245
|
return true;
|
|
@@ -2672,7 +2630,7 @@ class QueryInterface {
|
|
|
2672
2630
|
// array conditionally end up passing `[]`. Normalized here, before the
|
|
2673
2631
|
// implicit-ordering and fingerprinting steps, so every downstream path sees
|
|
2674
2632
|
// one shape.
|
|
2675
|
-
if (args?.orderBy !== undefined && isEmptyOrderBy(args.orderBy)) {
|
|
2633
|
+
if (args?.orderBy !== undefined && (0, filters_js_1.isEmptyOrderBy)(args.orderBy)) {
|
|
2676
2634
|
args = { ...args, orderBy: undefined };
|
|
2677
2635
|
}
|
|
2678
2636
|
// Drop `orderBy` terms that sort by an expression an earlier term already
|
|
@@ -387,3 +387,11 @@ export declare function normalizeOrderBy(value: OrderDirection | OrderBySpec): {
|
|
|
387
387
|
dir: 'ASC' | 'DESC';
|
|
388
388
|
nulls?: 'first' | 'last';
|
|
389
389
|
};
|
|
390
|
+
/**
|
|
391
|
+
* Is this `orderBy` effectively absent?
|
|
392
|
+
*
|
|
393
|
+
* `{}` and `[]` and `{ id: undefined }` all mean "no ordering asked for". Used
|
|
394
|
+
* by the stable-relation-order transform so an explicit (non-empty) orderBy is
|
|
395
|
+
* never overwritten while an empty one still gets the synthesized PK order.
|
|
396
|
+
*/
|
|
397
|
+
export declare function isEmptyOrderBy(orderBy: unknown): boolean;
|
|
@@ -33,6 +33,7 @@ exports.orderKeyIdentity = orderKeyIdentity;
|
|
|
33
33
|
exports.dedupeOrderEntries = dedupeOrderEntries;
|
|
34
34
|
exports.dedupeColumnList = dedupeColumnList;
|
|
35
35
|
exports.normalizeOrderBy = normalizeOrderBy;
|
|
36
|
+
exports.isEmptyOrderBy = isEmptyOrderBy;
|
|
36
37
|
const errors_js_1 = require("../errors.js");
|
|
37
38
|
const types_js_1 = require("./types.js");
|
|
38
39
|
const utils_js_1 = require("./utils.js");
|
|
@@ -701,3 +702,18 @@ function normalizeOrderBy(value) {
|
|
|
701
702
|
}
|
|
702
703
|
return { dir: String(value).toLowerCase() === 'desc' ? 'DESC' : 'ASC' };
|
|
703
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* Is this `orderBy` effectively absent?
|
|
707
|
+
*
|
|
708
|
+
* `{}` and `[]` and `{ id: undefined }` all mean "no ordering asked for". Used
|
|
709
|
+
* by the stable-relation-order transform so an explicit (non-empty) orderBy is
|
|
710
|
+
* never overwritten while an empty one still gets the synthesized PK order.
|
|
711
|
+
*/
|
|
712
|
+
function isEmptyOrderBy(orderBy) {
|
|
713
|
+
if (Array.isArray(orderBy))
|
|
714
|
+
return orderBy.length === 0;
|
|
715
|
+
if (orderBy && typeof orderBy === 'object') {
|
|
716
|
+
return Object.values(orderBy).every((v) => v === undefined);
|
|
717
|
+
}
|
|
718
|
+
return orderBy === undefined || orderBy === null;
|
|
719
|
+
}
|
|
@@ -50,3 +50,21 @@ import type { WithClause } from './types.js';
|
|
|
50
50
|
* Returns the input by reference when no key changed.
|
|
51
51
|
*/
|
|
52
52
|
export declare function normalizeWithClause(schema: SchemaMetadata, table: string, withClause: WithClause | undefined, depth?: number): WithClause | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Fill in a deterministic `orderBy` for every to-many relation in a `with`
|
|
55
|
+
* clause that did not ask for one, using the target table's primary key.
|
|
56
|
+
*
|
|
57
|
+
* THE single authority for `stableRelationOrder`, on every engine. It was a
|
|
58
|
+
* private method on `QueryInterface` until 0.74.0, which meant PowDB accepted
|
|
59
|
+
* the option and did nothing with it: relation rows came back in whatever order
|
|
60
|
+
* the engine produced, from the option whose entire purpose is that they do
|
|
61
|
+
* not. Restating the rule in `powql.ts` would have been the 0.64 projection
|
|
62
|
+
* resolver again, so it moved here instead, into the same "normalize the whole
|
|
63
|
+
* `with` tree once, before anything reads it" slot as
|
|
64
|
+
* {@link normalizeWithClause}.
|
|
65
|
+
*
|
|
66
|
+
* Returns the input BY REFERENCE when nothing changed, so a query that already
|
|
67
|
+
* orders every relation allocates nothing and its SQL/fingerprint stay
|
|
68
|
+
* byte-identical.
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyStableRelationOrderTo(schema: SchemaMetadata, withClause: WithClause, table: string, depth?: number): WithClause;
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
*/
|
|
45
45
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
46
|
exports.normalizeWithClause = normalizeWithClause;
|
|
47
|
+
exports.applyStableRelationOrderTo = applyStableRelationOrderTo;
|
|
48
|
+
const filters_js_1 = require("./filters.js");
|
|
47
49
|
const utils_js_1 = require("./utils.js");
|
|
48
50
|
/** Depth cap mirroring the builder's own, so a cyclic `with` cannot spin here. */
|
|
49
51
|
const MAX_DEPTH = 12;
|
|
@@ -118,3 +120,61 @@ function normalizeCount(meta, count) {
|
|
|
118
120
|
* already a documented single authority; wrapping them here would add a second
|
|
119
121
|
* name for the same rule without removing a caller.
|
|
120
122
|
*/
|
|
123
|
+
/**
|
|
124
|
+
* Fill in a deterministic `orderBy` for every to-many relation in a `with`
|
|
125
|
+
* clause that did not ask for one, using the target table's primary key.
|
|
126
|
+
*
|
|
127
|
+
* THE single authority for `stableRelationOrder`, on every engine. It was a
|
|
128
|
+
* private method on `QueryInterface` until 0.74.0, which meant PowDB accepted
|
|
129
|
+
* the option and did nothing with it: relation rows came back in whatever order
|
|
130
|
+
* the engine produced, from the option whose entire purpose is that they do
|
|
131
|
+
* not. Restating the rule in `powql.ts` would have been the 0.64 projection
|
|
132
|
+
* resolver again, so it moved here instead, into the same "normalize the whole
|
|
133
|
+
* `with` tree once, before anything reads it" slot as
|
|
134
|
+
* {@link normalizeWithClause}.
|
|
135
|
+
*
|
|
136
|
+
* Returns the input BY REFERENCE when nothing changed, so a query that already
|
|
137
|
+
* orders every relation allocates nothing and its SQL/fingerprint stay
|
|
138
|
+
* byte-identical.
|
|
139
|
+
*/
|
|
140
|
+
function applyStableRelationOrderTo(schema, withClause, table, depth = 0) {
|
|
141
|
+
if (depth >= 10)
|
|
142
|
+
return withClause; // parity with the build depth cap
|
|
143
|
+
const meta = schema.tables[table];
|
|
144
|
+
if (!meta)
|
|
145
|
+
return withClause;
|
|
146
|
+
let out;
|
|
147
|
+
for (const [relName, spec] of Object.entries(withClause)) {
|
|
148
|
+
if (relName === '_count' || !spec)
|
|
149
|
+
continue; // `_count` is a count, not a row load
|
|
150
|
+
const rel = (0, utils_js_1.resolveRelationDef)(meta.relations, relName);
|
|
151
|
+
if (!rel)
|
|
152
|
+
continue; // unknown relation, let the build path surface E005
|
|
153
|
+
const options = spec === true ? {} : spec;
|
|
154
|
+
// Recurse first so a nested change alone still clones this level.
|
|
155
|
+
const nestedWith = options.with;
|
|
156
|
+
const newNested = nestedWith ? applyStableRelationOrderTo(schema, nestedWith, rel.to, depth + 1) : undefined;
|
|
157
|
+
const nestedChanged = newNested !== undefined && newNested !== nestedWith;
|
|
158
|
+
const isToMany = rel.type === 'hasMany' || rel.type === 'manyToMany';
|
|
159
|
+
const hasOrder = options.orderBy !== undefined && !(0, filters_js_1.isEmptyOrderBy)(options.orderBy);
|
|
160
|
+
let synthOrder;
|
|
161
|
+
if (isToMany && !hasOrder) {
|
|
162
|
+
const targetMeta = schema.tables[rel.to];
|
|
163
|
+
const pk = targetMeta?.primaryKey ?? [];
|
|
164
|
+
if (targetMeta && pk.length > 0) {
|
|
165
|
+
const pkFields = pk.map((c) => targetMeta.reverseColumnMap[c] ?? c);
|
|
166
|
+
synthOrder = pkFields.length === 1 ? { [pkFields[0]]: 'asc' } : pkFields.map((f) => ({ [f]: 'asc' }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!synthOrder && !nestedChanged)
|
|
170
|
+
continue; // nothing to change, keep the ref
|
|
171
|
+
out ??= { ...withClause };
|
|
172
|
+
const clonedSpec = { ...options };
|
|
173
|
+
if (synthOrder)
|
|
174
|
+
clonedSpec.orderBy = synthOrder;
|
|
175
|
+
if (nestedChanged)
|
|
176
|
+
clonedSpec.with = newNested;
|
|
177
|
+
out[relName] = clonedSpec;
|
|
178
|
+
}
|
|
179
|
+
return out ?? withClause;
|
|
180
|
+
}
|
|
@@ -285,6 +285,20 @@ export declare function buildWhere<T extends object>(qi: BuilderCtx, where: Wher
|
|
|
285
285
|
sql: string;
|
|
286
286
|
params: unknown[];
|
|
287
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* THE global-filter resolution rule, with no dependency on a `BuilderCtx`.
|
|
290
|
+
*
|
|
291
|
+
* Split out from {@link resolveGlobalFilter} so `PowqlInterface` can consume
|
|
292
|
+
* the SAME rule rather than a transcription of it. It is a parallel
|
|
293
|
+
* implementation of the public surface, and this repository's history is
|
|
294
|
+
* unambiguous that a rule written twice is how two engines come to disagree
|
|
295
|
+
* (the 0.64 projection resolver, the 0.73 findUnique guard). Every clause here
|
|
296
|
+
* is load-bearing on both engines: a function filter is evaluated PER BUILD so
|
|
297
|
+
* per-request tenancy works, an all-undefined filter is treated as absent so it
|
|
298
|
+
* cannot emit a dangling clause, and `skip` is honoured in both its whole-query
|
|
299
|
+
* and per-table forms.
|
|
300
|
+
*/
|
|
301
|
+
export declare function resolveGlobalFilterFrom(filters: GlobalFilters | undefined, table: string, skip: ResolvedSkipGlobalFilters | undefined): Record<string, unknown> | null;
|
|
288
302
|
/**
|
|
289
303
|
* Resolve the configured global filter for `table`, evaluating a function
|
|
290
304
|
* filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
|
package/dist/cjs/query/where.js
CHANGED
|
@@ -24,6 +24,7 @@ exports.collectJsonFilterParams = collectJsonFilterParams;
|
|
|
24
24
|
exports.collectArrayFilterParams = collectArrayFilterParams;
|
|
25
25
|
exports.collectVectorFilterParams = collectVectorFilterParams;
|
|
26
26
|
exports.buildWhere = buildWhere;
|
|
27
|
+
exports.resolveGlobalFilterFrom = resolveGlobalFilterFrom;
|
|
27
28
|
exports.resolveGlobalFilter = resolveGlobalFilter;
|
|
28
29
|
exports.mergeGlobalFilter = mergeGlobalFilter;
|
|
29
30
|
exports.targetGlobalFilterAlias = targetGlobalFilterAlias;
|
|
@@ -436,12 +437,19 @@ function buildWhere(qi, where) {
|
|
|
436
437
|
return { sql: ` WHERE ${clause}`, params };
|
|
437
438
|
}
|
|
438
439
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
440
|
+
* THE global-filter resolution rule, with no dependency on a `BuilderCtx`.
|
|
441
|
+
*
|
|
442
|
+
* Split out from {@link resolveGlobalFilter} so `PowqlInterface` can consume
|
|
443
|
+
* the SAME rule rather than a transcription of it. It is a parallel
|
|
444
|
+
* implementation of the public surface, and this repository's history is
|
|
445
|
+
* unambiguous that a rule written twice is how two engines come to disagree
|
|
446
|
+
* (the 0.64 projection resolver, the 0.73 findUnique guard). Every clause here
|
|
447
|
+
* is load-bearing on both engines: a function filter is evaluated PER BUILD so
|
|
448
|
+
* per-request tenancy works, an all-undefined filter is treated as absent so it
|
|
449
|
+
* cannot emit a dangling clause, and `skip` is honoured in both its whole-query
|
|
450
|
+
* and per-table forms.
|
|
442
451
|
*/
|
|
443
|
-
function
|
|
444
|
-
const filters = qi.globalFilters;
|
|
452
|
+
function resolveGlobalFilterFrom(filters, table, skip) {
|
|
445
453
|
if (!filters)
|
|
446
454
|
return null;
|
|
447
455
|
if (skip === true)
|
|
@@ -461,6 +469,14 @@ function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
|
|
|
461
469
|
return null;
|
|
462
470
|
return obj;
|
|
463
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Resolve the configured global filter for `table`, evaluating a function
|
|
474
|
+
* filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
|
|
475
|
+
* no filter applies, the query opted out, or the filter is empty.
|
|
476
|
+
*/
|
|
477
|
+
function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
|
|
478
|
+
return resolveGlobalFilterFrom(qi.globalFilters, table, skip);
|
|
479
|
+
}
|
|
464
480
|
/**
|
|
465
481
|
* AND-merge this table's resolved global filter into a user `where`. Either
|
|
466
482
|
* side may be absent. When no filter applies the user where is returned by
|
package/dist/powql.d.ts
CHANGED
|
@@ -196,7 +196,25 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
196
196
|
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
197
197
|
* not `skip` pages differently from one that reads both.
|
|
198
198
|
*/
|
|
199
|
+
/**
|
|
200
|
+
* Fill in the deterministic per-relation ordering `stableRelationOrder` asks
|
|
201
|
+
* for, through the shared transform the SQL engines use.
|
|
202
|
+
*
|
|
203
|
+
* PowDB accepted this option and did nothing with it before 0.74.0, so
|
|
204
|
+
* relation rows came back in whatever order the engine produced, from the one
|
|
205
|
+
* option whose entire purpose is that they do not. Same resolution as
|
|
206
|
+
* QueryInterface: a per-query flag wins, else the client-level default.
|
|
207
|
+
*/
|
|
208
|
+
private withStableRelationOrder;
|
|
199
209
|
private normalizeArgs;
|
|
210
|
+
/**
|
|
211
|
+
* `jsonEncoding` selects between PostgreSQL's `json_build_object` and
|
|
212
|
+
* `json_build_array` row encodings for relation subqueries. PowQL emits no
|
|
213
|
+
* JSON row encoding at all, so there is nothing for the option to select and
|
|
214
|
+
* it was simply dropped: the caller asked for a wire format and got silence.
|
|
215
|
+
* Refused for the same reason `forceCustomPlan` is.
|
|
216
|
+
*/
|
|
217
|
+
private assertNoJsonEncoding;
|
|
200
218
|
private assertNoForceCustomPlan;
|
|
201
219
|
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
202
220
|
private assertIdentifiesOneRow;
|
|
@@ -883,5 +901,25 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
883
901
|
* `{ OR: [{ f: undefined }] }`, compiles to the empty string and is refused,
|
|
884
902
|
* because emitting a filter-less write would hit every row.
|
|
885
903
|
*/
|
|
904
|
+
/**
|
|
905
|
+
* AND this table's configured global filter (soft-delete / multi-tenancy)
|
|
906
|
+
* onto an ALREADY-COMPILED user filter, pushing its params after the user's.
|
|
907
|
+
*
|
|
908
|
+
* Before 0.74.0 `PowqlInterface` did not read `globalFilters` at all, so a
|
|
909
|
+
* client configured for multi-tenancy applied its tenant predicate on every
|
|
910
|
+
* SQL engine and NONE on PowDB, silently, and `skipGlobalFilters` was
|
|
911
|
+
* accepted and inert. The rule itself is shared
|
|
912
|
+
* ({@link resolveGlobalFilterFrom}) rather than restated here.
|
|
913
|
+
*
|
|
914
|
+
* It takes a COMPILED user filter, and that ordering is the contract, not an
|
|
915
|
+
* implementation detail: the empty-`where` guard must see the USER filter
|
|
916
|
+
* alone, or a configured global filter would silently satisfy it and turn a
|
|
917
|
+
* refused mass mutation into an accepted one. Every write call site therefore
|
|
918
|
+
* calls {@link assertCompiledWhere} first and this second.
|
|
919
|
+
*
|
|
920
|
+
* `alias` is forwarded so the nested/native-join paths qualify the filter's
|
|
921
|
+
* column references the same way they qualify the user's.
|
|
922
|
+
*/
|
|
923
|
+
private applyGlobalFilter;
|
|
886
924
|
private assertCompiledWhere;
|
|
887
925
|
}
|
package/dist/powql.js
CHANGED
|
@@ -35,20 +35,20 @@
|
|
|
35
35
|
* @module
|
|
36
36
|
*/
|
|
37
37
|
import { randomUUID } from 'node:crypto';
|
|
38
|
-
import { NotFoundError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError } from './errors.js';
|
|
38
|
+
import { NotFoundError, OptimisticLockError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
39
39
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
|
|
40
40
|
import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb.js';
|
|
41
41
|
import { assertAggregatePiiOptIn } from './query/aggregates.js';
|
|
42
42
|
import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './query/compound-unique.js';
|
|
43
43
|
import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
|
|
44
44
|
import { warnUnknownQueryOptions } from './query/option-surface.js';
|
|
45
|
-
import { normalizeWithClause } from './query/relation-names.js';
|
|
45
|
+
import { applyStableRelationOrderTo, normalizeWithClause } from './query/relation-names.js';
|
|
46
46
|
// The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
|
|
47
47
|
// are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
|
|
48
48
|
// engines, so a spread request body cannot turn either on here either.
|
|
49
|
-
import { assertDirectionToken, resolveUnsafeFlag, UNSAFE } from './query/types.js';
|
|
49
|
+
import { assertDirectionToken, resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './query/types.js';
|
|
50
50
|
import { escapeLike, normalizePagination, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, } from './query/utils.js';
|
|
51
|
-
import { assertJsonFilterKeys, jsonStringEntries } from './query/where.js';
|
|
51
|
+
import { assertJsonFilterKeys, jsonStringEntries, resolveGlobalFilterFrom } from './query/where.js';
|
|
52
52
|
import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
53
53
|
/**
|
|
54
54
|
* Max parent keys per relation-loader `in (…)` query. A `with` over a large
|
|
@@ -394,8 +394,38 @@ export class PowqlInterface {
|
|
|
394
394
|
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
395
395
|
* not `skip` pages differently from one that reads both.
|
|
396
396
|
*/
|
|
397
|
+
/**
|
|
398
|
+
* Fill in the deterministic per-relation ordering `stableRelationOrder` asks
|
|
399
|
+
* for, through the shared transform the SQL engines use.
|
|
400
|
+
*
|
|
401
|
+
* PowDB accepted this option and did nothing with it before 0.74.0, so
|
|
402
|
+
* relation rows came back in whatever order the engine produced, from the one
|
|
403
|
+
* option whose entire purpose is that they do not. Same resolution as
|
|
404
|
+
* QueryInterface: a per-query flag wins, else the client-level default.
|
|
405
|
+
*/
|
|
406
|
+
withStableRelationOrder(args) {
|
|
407
|
+
if (!args?.with)
|
|
408
|
+
return args;
|
|
409
|
+
const on = args.stableRelationOrder ?? this.options.stableRelationOrder === true;
|
|
410
|
+
if (!on)
|
|
411
|
+
return args;
|
|
412
|
+
const ordered = applyStableRelationOrderTo(this.schema, args.with, this.table);
|
|
413
|
+
return ordered === args.with ? args : { ...args, with: ordered };
|
|
414
|
+
}
|
|
397
415
|
normalizeArgs(args) {
|
|
398
|
-
return normalizePagination(this.withDeclaredRelationNames(args));
|
|
416
|
+
return normalizePagination(this.withStableRelationOrder(this.withDeclaredRelationNames(args)));
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* `jsonEncoding` selects between PostgreSQL's `json_build_object` and
|
|
420
|
+
* `json_build_array` row encodings for relation subqueries. PowQL emits no
|
|
421
|
+
* JSON row encoding at all, so there is nothing for the option to select and
|
|
422
|
+
* it was simply dropped: the caller asked for a wire format and got silence.
|
|
423
|
+
* Refused for the same reason `forceCustomPlan` is.
|
|
424
|
+
*/
|
|
425
|
+
assertNoJsonEncoding(args) {
|
|
426
|
+
if (args?.jsonEncoding === undefined)
|
|
427
|
+
return;
|
|
428
|
+
throw new UnsupportedFeatureError('jsonEncoding', 'PowDB', 'PowQL emits no JSON row encoding for relations, so there is nothing to select; omit the option');
|
|
399
429
|
}
|
|
400
430
|
assertNoForceCustomPlan(args) {
|
|
401
431
|
if (args?.forceCustomPlan !== true)
|
|
@@ -1350,6 +1380,7 @@ export class PowqlInterface {
|
|
|
1350
1380
|
// -------------------------------------------------------------------------
|
|
1351
1381
|
async findMany(args = {}) {
|
|
1352
1382
|
this.assertNoForceCustomPlan(args);
|
|
1383
|
+
this.assertNoJsonEncoding(args);
|
|
1353
1384
|
args = this.normalizeArgs(args);
|
|
1354
1385
|
return this.withMiddleware('findMany', args, async () => {
|
|
1355
1386
|
// `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
|
|
@@ -1431,7 +1462,8 @@ export class PowqlInterface {
|
|
|
1431
1462
|
}
|
|
1432
1463
|
const nest = nestedPlans.length > 0 || linkPlans.length > 0;
|
|
1433
1464
|
const alias = nest ? 't0' : undefined;
|
|
1434
|
-
|
|
1465
|
+
let where = this.buildWhere(resolvedWhere, params, alias);
|
|
1466
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters, alias);
|
|
1435
1467
|
// PowQL's `distinct` is row-wide, so these names never reach the emitted
|
|
1436
1468
|
// statement. They are still caller-supplied names, and a name resolves or
|
|
1437
1469
|
// throws: reading the array for its LENGTH alone let `distinct: ['nope']`
|
|
@@ -1508,6 +1540,7 @@ export class PowqlInterface {
|
|
|
1508
1540
|
}
|
|
1509
1541
|
async findUnique(args) {
|
|
1510
1542
|
this.assertNoForceCustomPlan(args);
|
|
1543
|
+
this.assertNoJsonEncoding(args);
|
|
1511
1544
|
args = this.normalizeArgs(args);
|
|
1512
1545
|
// Prisma compound-unique selector → column conjunction (engine parity with
|
|
1513
1546
|
// the SQL findUnique family; pure metadata, so this is a one-line adoption).
|
|
@@ -2602,14 +2635,41 @@ export class PowqlInterface {
|
|
|
2602
2635
|
}
|
|
2603
2636
|
const params = [];
|
|
2604
2637
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2638
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2639
|
+
// `false` here refused an empty where even WITH the opt-in, while every
|
|
2640
|
+
// SQL engine accepted it: verified by probe on buildUpdate/buildDelete.
|
|
2641
|
+
this.assertCompiledWhere(where, resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'), 'update');
|
|
2642
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2643
|
+
let setClause = this.buildUpdateAssignments(args.data, params);
|
|
2644
|
+
// Optimistic locking, matching the SQL engines exactly: bump the version
|
|
2645
|
+
// column in the SET, add `version = expected` to the filter, and treat
|
|
2646
|
+
// "matched no row" as a lost race rather than a missing row.
|
|
2647
|
+
//
|
|
2648
|
+
// PowqlInterface ignored `optimisticLock` outright before 0.74.0, which is
|
|
2649
|
+
// worse than not supporting it: the update applied, the caller's version
|
|
2650
|
+
// check never happened, and a concurrent writer's change was overwritten
|
|
2651
|
+
// by a caller who believed they held the lock.
|
|
2652
|
+
const lock = args.optimisticLock;
|
|
2653
|
+
if (lock) {
|
|
2654
|
+
const versionCol = quotePowqlIdent(this.column(lock.field).name);
|
|
2655
|
+
setClause += `, ${versionCol} := ${this.ref(lock.field)} + 1`;
|
|
2656
|
+
params.push(lock.expected);
|
|
2657
|
+
const check = `${this.ref(lock.field)} = $${params.length}`;
|
|
2658
|
+
where = where ? `(${where}) and (${check})` : check;
|
|
2659
|
+
}
|
|
2608
2660
|
// `returning` hands back the post-update row(s); take the first (single-row contract).
|
|
2609
2661
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout, 'update');
|
|
2610
2662
|
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
2611
|
-
if (!row)
|
|
2663
|
+
if (!row) {
|
|
2664
|
+
if (lock) {
|
|
2665
|
+
throw new OptimisticLockError({
|
|
2666
|
+
table: this.table,
|
|
2667
|
+
versionField: lock.field,
|
|
2668
|
+
expectedVersion: lock.expected,
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2612
2671
|
throw new NotFoundError({ table: this.table, where: args.where });
|
|
2672
|
+
}
|
|
2613
2673
|
return row;
|
|
2614
2674
|
});
|
|
2615
2675
|
}
|
|
@@ -2617,8 +2677,9 @@ export class PowqlInterface {
|
|
|
2617
2677
|
return this.withMiddleware('updateMany', args, async () => {
|
|
2618
2678
|
const params = [];
|
|
2619
2679
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2620
|
-
|
|
2680
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2621
2681
|
this.assertCompiledWhere(where, resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'), 'updateMany');
|
|
2682
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2622
2683
|
const setClause = this.buildUpdateAssignments(args.data, params);
|
|
2623
2684
|
const filter = where ? ` filter ${where}` : '';
|
|
2624
2685
|
const { rowCount } = await this.exec(`${this.qt}${filter} update { ${setClause} }`, params, args.timeout, 'updateMany');
|
|
@@ -2753,8 +2814,9 @@ export class PowqlInterface {
|
|
|
2753
2814
|
return this.withMiddleware('delete', args, async () => {
|
|
2754
2815
|
const params = [];
|
|
2755
2816
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2756
|
-
|
|
2757
|
-
this.assertCompiledWhere(where,
|
|
2817
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2818
|
+
this.assertCompiledWhere(where, resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'), 'delete');
|
|
2819
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2758
2820
|
// `returning` hands back the deleted row(s), no separate pre-image reselect needed.
|
|
2759
2821
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
2760
2822
|
const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
|
|
@@ -2767,8 +2829,9 @@ export class PowqlInterface {
|
|
|
2767
2829
|
return this.withMiddleware('deleteMany', args, async () => {
|
|
2768
2830
|
const params = [];
|
|
2769
2831
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2770
|
-
|
|
2832
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2771
2833
|
this.assertCompiledWhere(where, resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan'), 'deleteMany');
|
|
2834
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2772
2835
|
const filter = where ? ` filter ${where}` : '';
|
|
2773
2836
|
const { rowCount } = await this.exec(`${this.qt}${filter} delete`, params, args.timeout, 'deleteMany');
|
|
2774
2837
|
return { count: rowCount };
|
|
@@ -2839,7 +2902,8 @@ export class PowqlInterface {
|
|
|
2839
2902
|
return this.withMiddleware('count', (args ?? {}), async () => {
|
|
2840
2903
|
const params = [];
|
|
2841
2904
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2842
|
-
|
|
2905
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
2906
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2843
2907
|
const filter = where ? ` filter ${where}` : '';
|
|
2844
2908
|
const { rows } = await this.exec(`count(${this.qt}${filter})`, params, args.timeout, 'count');
|
|
2845
2909
|
return Number((rows[0]?.value ?? rows[0]?.count ?? 0));
|
|
@@ -2880,7 +2944,8 @@ export class PowqlInterface {
|
|
|
2880
2944
|
const result = {};
|
|
2881
2945
|
const filterParams = [];
|
|
2882
2946
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2883
|
-
|
|
2947
|
+
let where = this.buildWhere(resolvedWhere, filterParams);
|
|
2948
|
+
where = this.applyGlobalFilter(where, filterParams, args.skipGlobalFilters);
|
|
2884
2949
|
const filter = where ? ` filter ${where}` : '';
|
|
2885
2950
|
const scalar = async (expr) => {
|
|
2886
2951
|
const params = [...filterParams];
|
|
@@ -2942,7 +3007,8 @@ export class PowqlInterface {
|
|
|
2942
3007
|
const emitNative = Boolean(this.capabilities.nativeRaw);
|
|
2943
3008
|
const params = [];
|
|
2944
3009
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
2945
|
-
|
|
3010
|
+
let where = this.buildWhere(resolvedWhere, params);
|
|
3011
|
+
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2946
3012
|
const filter = where ? ` filter ${where}` : '';
|
|
2947
3013
|
// Result-key namespace, mirroring the SQL builder's `claimResultKey`
|
|
2948
3014
|
// (query/builder.ts): a group-key / aggregate output-name collision (with
|
|
@@ -3295,6 +3361,36 @@ export class PowqlInterface {
|
|
|
3295
3361
|
* `{ OR: [{ f: undefined }] }`, compiles to the empty string and is refused,
|
|
3296
3362
|
* because emitting a filter-less write would hit every row.
|
|
3297
3363
|
*/
|
|
3364
|
+
/**
|
|
3365
|
+
* AND this table's configured global filter (soft-delete / multi-tenancy)
|
|
3366
|
+
* onto an ALREADY-COMPILED user filter, pushing its params after the user's.
|
|
3367
|
+
*
|
|
3368
|
+
* Before 0.74.0 `PowqlInterface` did not read `globalFilters` at all, so a
|
|
3369
|
+
* client configured for multi-tenancy applied its tenant predicate on every
|
|
3370
|
+
* SQL engine and NONE on PowDB, silently, and `skipGlobalFilters` was
|
|
3371
|
+
* accepted and inert. The rule itself is shared
|
|
3372
|
+
* ({@link resolveGlobalFilterFrom}) rather than restated here.
|
|
3373
|
+
*
|
|
3374
|
+
* It takes a COMPILED user filter, and that ordering is the contract, not an
|
|
3375
|
+
* implementation detail: the empty-`where` guard must see the USER filter
|
|
3376
|
+
* alone, or a configured global filter would silently satisfy it and turn a
|
|
3377
|
+
* refused mass mutation into an accepted one. Every write call site therefore
|
|
3378
|
+
* calls {@link assertCompiledWhere} first and this second.
|
|
3379
|
+
*
|
|
3380
|
+
* `alias` is forwarded so the nested/native-join paths qualify the filter's
|
|
3381
|
+
* column references the same way they qualify the user's.
|
|
3382
|
+
*/
|
|
3383
|
+
applyGlobalFilter(compiledUserWhere, params, skip, alias) {
|
|
3384
|
+
const gf = resolveGlobalFilterFrom(this.options.globalFilters, this.table, resolveSkipGlobalFilters(skip));
|
|
3385
|
+
if (!gf)
|
|
3386
|
+
return compiledUserWhere;
|
|
3387
|
+
const gfWhere = this.buildWhere(gf, params, alias);
|
|
3388
|
+
if (!gfWhere)
|
|
3389
|
+
return compiledUserWhere;
|
|
3390
|
+
if (!compiledUserWhere)
|
|
3391
|
+
return gfWhere;
|
|
3392
|
+
return `(${compiledUserWhere}) and (${gfWhere})`;
|
|
3393
|
+
}
|
|
3298
3394
|
assertCompiledWhere(compiledWhere, allow, action) {
|
|
3299
3395
|
if (allow === true)
|
|
3300
3396
|
return;
|
package/dist/query/builder.js
CHANGED
|
@@ -18,9 +18,9 @@ import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
|
18
18
|
import * as aggMod from './aggregates.js';
|
|
19
19
|
import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
|
|
20
20
|
import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './compound-unique.js';
|
|
21
|
-
import { dedupeColumnList, dedupeOrderEntries, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
21
|
+
import { dedupeColumnList, dedupeOrderEntries, isEmptyOrderBy, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
22
22
|
import { warnUnknownQueryOptions } from './option-surface.js';
|
|
23
|
-
import { normalizeWithClause } from './relation-names.js';
|
|
23
|
+
import { applyStableRelationOrderTo, normalizeWithClause } from './relation-names.js';
|
|
24
24
|
import * as relationsMod from './relations.js';
|
|
25
25
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
26
26
|
import { isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
@@ -406,14 +406,8 @@ export function unlockNestedWriteTx(tx) {
|
|
|
406
406
|
* orderBy is never overwritten while an empty `{}` / `[]` still gets the
|
|
407
407
|
* synthesized PK order.
|
|
408
408
|
*/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return orderBy.length === 0;
|
|
412
|
-
if (orderBy && typeof orderBy === 'object') {
|
|
413
|
-
return Object.values(orderBy).every((v) => v === undefined);
|
|
414
|
-
}
|
|
415
|
-
return orderBy === undefined || orderBy === null;
|
|
416
|
-
}
|
|
409
|
+
// Moved to ./filters.ts in 0.74.0 so PowqlInterface consumes the SAME
|
|
410
|
+
// predicate rather than a second copy of it. See applyStableRelationOrderTo.
|
|
417
411
|
/** The two accepted {@link JsonEncoding} values, frozen so the check is total. */
|
|
418
412
|
const JSON_ENCODINGS = Object.freeze(['object', 'positional']);
|
|
419
413
|
/**
|
|
@@ -1117,46 +1111,10 @@ export class QueryInterface {
|
|
|
1117
1111
|
* nothing stable to order by, so both are left untouched.
|
|
1118
1112
|
*/
|
|
1119
1113
|
applyStableRelationOrder(withClause, table, depth = 0) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
return withClause;
|
|
1125
|
-
let out;
|
|
1126
|
-
for (const [relName, spec] of Object.entries(withClause)) {
|
|
1127
|
-
if (relName === '_count' || !spec)
|
|
1128
|
-
continue; // `_count` is a count, not a row load
|
|
1129
|
-
const rel = resolveRelationDef(meta.relations, relName);
|
|
1130
|
-
if (!rel)
|
|
1131
|
-
continue; // unknown relation, let the build path surface E005
|
|
1132
|
-
const options = spec === true ? {} : spec;
|
|
1133
|
-
// Recurse first so a nested change alone still clones this level.
|
|
1134
|
-
const nestedWith = options.with;
|
|
1135
|
-
const newNested = nestedWith ? this.applyStableRelationOrder(nestedWith, rel.to, depth + 1) : undefined;
|
|
1136
|
-
const nestedChanged = newNested !== undefined && newNested !== nestedWith;
|
|
1137
|
-
const isToMany = rel.type === 'hasMany' || rel.type === 'manyToMany';
|
|
1138
|
-
const hasOrder = options.orderBy !== undefined && !isEmptyOrderBy(options.orderBy);
|
|
1139
|
-
let synthOrder;
|
|
1140
|
-
if (isToMany && !hasOrder) {
|
|
1141
|
-
const targetMeta = this.schema.tables[rel.to];
|
|
1142
|
-
const pk = targetMeta?.primaryKey ?? [];
|
|
1143
|
-
if (targetMeta && pk.length > 0) {
|
|
1144
|
-
const pkFields = pk.map((c) => targetMeta.reverseColumnMap[c] ?? c);
|
|
1145
|
-
synthOrder =
|
|
1146
|
-
pkFields.length === 1 ? { [pkFields[0]]: 'asc' } : pkFields.map((f) => ({ [f]: 'asc' }));
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
if (!synthOrder && !nestedChanged)
|
|
1150
|
-
continue; // nothing to change, keep the ref
|
|
1151
|
-
out ??= { ...withClause };
|
|
1152
|
-
const clonedSpec = { ...options };
|
|
1153
|
-
if (synthOrder)
|
|
1154
|
-
clonedSpec.orderBy = synthOrder;
|
|
1155
|
-
if (nestedChanged)
|
|
1156
|
-
clonedSpec.with = newNested;
|
|
1157
|
-
out[relName] = clonedSpec;
|
|
1158
|
-
}
|
|
1159
|
-
return out ?? withClause;
|
|
1114
|
+
// The transform itself lives in ./relation-names.ts as of 0.74.0, so PowDB
|
|
1115
|
+
// runs the SAME rule instead of a second copy that accepted the option and
|
|
1116
|
+
// did nothing.
|
|
1117
|
+
return applyStableRelationOrderTo(this.schema, withClause, table, depth);
|
|
1160
1118
|
}
|
|
1161
1119
|
// -------------------------------------------------------------------------
|
|
1162
1120
|
// Deterministic pagination (unordered LIMIT/OFFSET)
|
package/dist/query/filters.d.ts
CHANGED
|
@@ -387,3 +387,11 @@ export declare function normalizeOrderBy(value: OrderDirection | OrderBySpec): {
|
|
|
387
387
|
dir: 'ASC' | 'DESC';
|
|
388
388
|
nulls?: 'first' | 'last';
|
|
389
389
|
};
|
|
390
|
+
/**
|
|
391
|
+
* Is this `orderBy` effectively absent?
|
|
392
|
+
*
|
|
393
|
+
* `{}` and `[]` and `{ id: undefined }` all mean "no ordering asked for". Used
|
|
394
|
+
* by the stable-relation-order transform so an explicit (non-empty) orderBy is
|
|
395
|
+
* never overwritten while an empty one still gets the synthesized PK order.
|
|
396
|
+
*/
|
|
397
|
+
export declare function isEmptyOrderBy(orderBy: unknown): boolean;
|
package/dist/query/filters.js
CHANGED
|
@@ -673,3 +673,18 @@ export function normalizeOrderBy(value) {
|
|
|
673
673
|
}
|
|
674
674
|
return { dir: String(value).toLowerCase() === 'desc' ? 'DESC' : 'ASC' };
|
|
675
675
|
}
|
|
676
|
+
/**
|
|
677
|
+
* Is this `orderBy` effectively absent?
|
|
678
|
+
*
|
|
679
|
+
* `{}` and `[]` and `{ id: undefined }` all mean "no ordering asked for". Used
|
|
680
|
+
* by the stable-relation-order transform so an explicit (non-empty) orderBy is
|
|
681
|
+
* never overwritten while an empty one still gets the synthesized PK order.
|
|
682
|
+
*/
|
|
683
|
+
export function isEmptyOrderBy(orderBy) {
|
|
684
|
+
if (Array.isArray(orderBy))
|
|
685
|
+
return orderBy.length === 0;
|
|
686
|
+
if (orderBy && typeof orderBy === 'object') {
|
|
687
|
+
return Object.values(orderBy).every((v) => v === undefined);
|
|
688
|
+
}
|
|
689
|
+
return orderBy === undefined || orderBy === null;
|
|
690
|
+
}
|
|
@@ -50,3 +50,21 @@ import type { WithClause } from './types.js';
|
|
|
50
50
|
* Returns the input by reference when no key changed.
|
|
51
51
|
*/
|
|
52
52
|
export declare function normalizeWithClause(schema: SchemaMetadata, table: string, withClause: WithClause | undefined, depth?: number): WithClause | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Fill in a deterministic `orderBy` for every to-many relation in a `with`
|
|
55
|
+
* clause that did not ask for one, using the target table's primary key.
|
|
56
|
+
*
|
|
57
|
+
* THE single authority for `stableRelationOrder`, on every engine. It was a
|
|
58
|
+
* private method on `QueryInterface` until 0.74.0, which meant PowDB accepted
|
|
59
|
+
* the option and did nothing with it: relation rows came back in whatever order
|
|
60
|
+
* the engine produced, from the option whose entire purpose is that they do
|
|
61
|
+
* not. Restating the rule in `powql.ts` would have been the 0.64 projection
|
|
62
|
+
* resolver again, so it moved here instead, into the same "normalize the whole
|
|
63
|
+
* `with` tree once, before anything reads it" slot as
|
|
64
|
+
* {@link normalizeWithClause}.
|
|
65
|
+
*
|
|
66
|
+
* Returns the input BY REFERENCE when nothing changed, so a query that already
|
|
67
|
+
* orders every relation allocates nothing and its SQL/fingerprint stay
|
|
68
|
+
* byte-identical.
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyStableRelationOrderTo(schema: SchemaMetadata, withClause: WithClause, table: string, depth?: number): WithClause;
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
* available relations; rejecting it here would move that error away from its
|
|
42
42
|
* context and change which error type callers see.
|
|
43
43
|
*/
|
|
44
|
-
import {
|
|
44
|
+
import { isEmptyOrderBy } from './filters.js';
|
|
45
|
+
import { resolveRelation, resolveRelationDef } from './utils.js';
|
|
45
46
|
/** Depth cap mirroring the builder's own, so a cyclic `with` cannot spin here. */
|
|
46
47
|
const MAX_DEPTH = 12;
|
|
47
48
|
/**
|
|
@@ -115,3 +116,61 @@ function normalizeCount(meta, count) {
|
|
|
115
116
|
* already a documented single authority; wrapping them here would add a second
|
|
116
117
|
* name for the same rule without removing a caller.
|
|
117
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* Fill in a deterministic `orderBy` for every to-many relation in a `with`
|
|
121
|
+
* clause that did not ask for one, using the target table's primary key.
|
|
122
|
+
*
|
|
123
|
+
* THE single authority for `stableRelationOrder`, on every engine. It was a
|
|
124
|
+
* private method on `QueryInterface` until 0.74.0, which meant PowDB accepted
|
|
125
|
+
* the option and did nothing with it: relation rows came back in whatever order
|
|
126
|
+
* the engine produced, from the option whose entire purpose is that they do
|
|
127
|
+
* not. Restating the rule in `powql.ts` would have been the 0.64 projection
|
|
128
|
+
* resolver again, so it moved here instead, into the same "normalize the whole
|
|
129
|
+
* `with` tree once, before anything reads it" slot as
|
|
130
|
+
* {@link normalizeWithClause}.
|
|
131
|
+
*
|
|
132
|
+
* Returns the input BY REFERENCE when nothing changed, so a query that already
|
|
133
|
+
* orders every relation allocates nothing and its SQL/fingerprint stay
|
|
134
|
+
* byte-identical.
|
|
135
|
+
*/
|
|
136
|
+
export function applyStableRelationOrderTo(schema, withClause, table, depth = 0) {
|
|
137
|
+
if (depth >= 10)
|
|
138
|
+
return withClause; // parity with the build depth cap
|
|
139
|
+
const meta = schema.tables[table];
|
|
140
|
+
if (!meta)
|
|
141
|
+
return withClause;
|
|
142
|
+
let out;
|
|
143
|
+
for (const [relName, spec] of Object.entries(withClause)) {
|
|
144
|
+
if (relName === '_count' || !spec)
|
|
145
|
+
continue; // `_count` is a count, not a row load
|
|
146
|
+
const rel = resolveRelationDef(meta.relations, relName);
|
|
147
|
+
if (!rel)
|
|
148
|
+
continue; // unknown relation, let the build path surface E005
|
|
149
|
+
const options = spec === true ? {} : spec;
|
|
150
|
+
// Recurse first so a nested change alone still clones this level.
|
|
151
|
+
const nestedWith = options.with;
|
|
152
|
+
const newNested = nestedWith ? applyStableRelationOrderTo(schema, nestedWith, rel.to, depth + 1) : undefined;
|
|
153
|
+
const nestedChanged = newNested !== undefined && newNested !== nestedWith;
|
|
154
|
+
const isToMany = rel.type === 'hasMany' || rel.type === 'manyToMany';
|
|
155
|
+
const hasOrder = options.orderBy !== undefined && !isEmptyOrderBy(options.orderBy);
|
|
156
|
+
let synthOrder;
|
|
157
|
+
if (isToMany && !hasOrder) {
|
|
158
|
+
const targetMeta = schema.tables[rel.to];
|
|
159
|
+
const pk = targetMeta?.primaryKey ?? [];
|
|
160
|
+
if (targetMeta && pk.length > 0) {
|
|
161
|
+
const pkFields = pk.map((c) => targetMeta.reverseColumnMap[c] ?? c);
|
|
162
|
+
synthOrder = pkFields.length === 1 ? { [pkFields[0]]: 'asc' } : pkFields.map((f) => ({ [f]: 'asc' }));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!synthOrder && !nestedChanged)
|
|
166
|
+
continue; // nothing to change, keep the ref
|
|
167
|
+
out ??= { ...withClause };
|
|
168
|
+
const clonedSpec = { ...options };
|
|
169
|
+
if (synthOrder)
|
|
170
|
+
clonedSpec.orderBy = synthOrder;
|
|
171
|
+
if (nestedChanged)
|
|
172
|
+
clonedSpec.with = newNested;
|
|
173
|
+
out[relName] = clonedSpec;
|
|
174
|
+
}
|
|
175
|
+
return out ?? withClause;
|
|
176
|
+
}
|
package/dist/query/where.d.ts
CHANGED
|
@@ -285,6 +285,20 @@ export declare function buildWhere<T extends object>(qi: BuilderCtx, where: Wher
|
|
|
285
285
|
sql: string;
|
|
286
286
|
params: unknown[];
|
|
287
287
|
};
|
|
288
|
+
/**
|
|
289
|
+
* THE global-filter resolution rule, with no dependency on a `BuilderCtx`.
|
|
290
|
+
*
|
|
291
|
+
* Split out from {@link resolveGlobalFilter} so `PowqlInterface` can consume
|
|
292
|
+
* the SAME rule rather than a transcription of it. It is a parallel
|
|
293
|
+
* implementation of the public surface, and this repository's history is
|
|
294
|
+
* unambiguous that a rule written twice is how two engines come to disagree
|
|
295
|
+
* (the 0.64 projection resolver, the 0.73 findUnique guard). Every clause here
|
|
296
|
+
* is load-bearing on both engines: a function filter is evaluated PER BUILD so
|
|
297
|
+
* per-request tenancy works, an all-undefined filter is treated as absent so it
|
|
298
|
+
* cannot emit a dangling clause, and `skip` is honoured in both its whole-query
|
|
299
|
+
* and per-table forms.
|
|
300
|
+
*/
|
|
301
|
+
export declare function resolveGlobalFilterFrom(filters: GlobalFilters | undefined, table: string, skip: ResolvedSkipGlobalFilters | undefined): Record<string, unknown> | null;
|
|
288
302
|
/**
|
|
289
303
|
* Resolve the configured global filter for `table`, evaluating a function
|
|
290
304
|
* filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
|
package/dist/query/where.js
CHANGED
|
@@ -369,12 +369,19 @@ export function buildWhere(qi, where) {
|
|
|
369
369
|
return { sql: ` WHERE ${clause}`, params };
|
|
370
370
|
}
|
|
371
371
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
372
|
+
* THE global-filter resolution rule, with no dependency on a `BuilderCtx`.
|
|
373
|
+
*
|
|
374
|
+
* Split out from {@link resolveGlobalFilter} so `PowqlInterface` can consume
|
|
375
|
+
* the SAME rule rather than a transcription of it. It is a parallel
|
|
376
|
+
* implementation of the public surface, and this repository's history is
|
|
377
|
+
* unambiguous that a rule written twice is how two engines come to disagree
|
|
378
|
+
* (the 0.64 projection resolver, the 0.73 findUnique guard). Every clause here
|
|
379
|
+
* is load-bearing on both engines: a function filter is evaluated PER BUILD so
|
|
380
|
+
* per-request tenancy works, an all-undefined filter is treated as absent so it
|
|
381
|
+
* cannot emit a dangling clause, and `skip` is honoured in both its whole-query
|
|
382
|
+
* and per-table forms.
|
|
375
383
|
*/
|
|
376
|
-
export function
|
|
377
|
-
const filters = qi.globalFilters;
|
|
384
|
+
export function resolveGlobalFilterFrom(filters, table, skip) {
|
|
378
385
|
if (!filters)
|
|
379
386
|
return null;
|
|
380
387
|
if (skip === true)
|
|
@@ -394,6 +401,14 @@ export function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
|
|
|
394
401
|
return null;
|
|
395
402
|
return obj;
|
|
396
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Resolve the configured global filter for `table`, evaluating a function
|
|
406
|
+
* filter, honoring the active query's `skipGlobalFilters`. Returns `null` when
|
|
407
|
+
* no filter applies, the query opted out, or the filter is empty.
|
|
408
|
+
*/
|
|
409
|
+
export function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
|
|
410
|
+
return resolveGlobalFilterFrom(qi.globalFilters, table, skip);
|
|
411
|
+
}
|
|
397
412
|
/**
|
|
398
413
|
* AND-merge this table's resolved global filter into a user `where`. Either
|
|
399
414
|
* side may be absent. When no filter applies the user where is returned by
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.74.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM, runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"//exports": "Each subpath declares its types PER CONDITION. A single shared top-level \"types\" resolves to the ESM declarations for `require` too, which is TS1479 (\"is an ES module ... cannot be require()d\") for any CJS consumer on moduleResolution node16/nodenext. The require condition points at dist/cjs, which ships its own {\"type\":\"commonjs\"} package.json, so those declarations are CJS declarations. Gated in CI by publint + @arethetypeswrong/cli + a real .cts consumer typecheck (see the package-types job in ci.yml).",
|