turbine-orm 0.71.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cjs/client.d.ts +0 -18
- package/dist/cjs/client.js +2 -29
- package/dist/cjs/connection-url.d.ts +30 -0
- package/dist/cjs/connection-url.js +15 -17
- package/dist/cjs/powql.d.ts +38 -1
- package/dist/cjs/powql.js +106 -18
- package/dist/cjs/query/aggregates.d.ts +0 -13
- package/dist/cjs/query/aggregates.js +81 -33
- package/dist/cjs/query/batched-loader.d.ts +13 -1
- package/dist/cjs/query/batched-loader.js +46 -11
- package/dist/cjs/query/builder.d.ts +13 -0
- package/dist/cjs/query/builder.js +104 -14
- package/dist/cjs/query/compound-unique.js +29 -5
- package/dist/cjs/query/relation-names.d.ts +52 -0
- package/dist/cjs/query/relation-names.js +120 -0
- package/dist/cjs/query/relations.d.ts +11 -6
- package/dist/cjs/query/relations.js +45 -27
- package/dist/cjs/query/utils.d.ts +107 -3
- package/dist/cjs/query/utils.js +408 -7
- package/dist/cjs/query/where-compile.js +9 -4
- package/dist/cjs/query/where.js +9 -5
- package/dist/client.d.ts +0 -18
- package/dist/client.js +2 -29
- package/dist/connection-url.d.ts +30 -0
- package/dist/connection-url.js +15 -18
- package/dist/powql.d.ts +38 -1
- package/dist/powql.js +107 -19
- package/dist/query/aggregates.d.ts +0 -13
- package/dist/query/aggregates.js +82 -34
- package/dist/query/batched-loader.d.ts +13 -1
- package/dist/query/batched-loader.js +47 -12
- package/dist/query/builder.d.ts +13 -0
- package/dist/query/builder.js +105 -15
- package/dist/query/compound-unique.js +30 -6
- package/dist/query/relation-names.d.ts +52 -0
- package/dist/query/relation-names.js +117 -0
- package/dist/query/relations.d.ts +11 -6
- package/dist/query/relations.js +47 -29
- package/dist/query/utils.d.ts +107 -3
- package/dist/query/utils.js +404 -8
- package/dist/query/where-compile.js +10 -5
- package/dist/query/where.js +10 -6
- package/package.json +5 -3
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
57
57
|
import { normalizeKeyColumns } from '../schema.js';
|
|
58
58
|
import { dedupeOrderEntries, isOrderBySpec, isRelationPickOrderBy, orderByEntries, sortedEntries } from './filters.js';
|
|
59
|
-
import { markInternalCombinator, ownLookup, selectNamesNothingMessage, selectOmitExclusiveMessage, sqlToPreparedName, } from './utils.js';
|
|
59
|
+
import { markInternalCombinator, ownLookup, resolveColumnName, selectNamesNothingMessage, selectOmitExclusiveMessage, sqlToPreparedName, } from './utils.js';
|
|
60
60
|
/**
|
|
61
61
|
* Max parent keys per follow-up query. On Postgres the whole key set travels as
|
|
62
62
|
* ONE array parameter (`= ANY($1)`), so this is not a bind-parameter limit, it
|
|
@@ -153,8 +153,12 @@ function partitionOrderBy(meta, orderBy) {
|
|
|
153
153
|
return null;
|
|
154
154
|
const out = [];
|
|
155
155
|
for (const [key, value] of entries) {
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
// The one key-resolution rule. A bare `columnMap` read knows only the
|
|
157
|
+
// FIELD spelling, so a snake-spelled orderBy declined the partition
|
|
158
|
+
// pushdown and silently fell back to fetching every child row and slicing
|
|
159
|
+
// client-side: two strategies, different bytes over the wire, one query.
|
|
160
|
+
const column = resolveColumnName(meta, key);
|
|
161
|
+
if (column === undefined)
|
|
158
162
|
return null;
|
|
159
163
|
let sort;
|
|
160
164
|
let nulls;
|
|
@@ -265,7 +269,18 @@ export function assertProjectionShape(table, select, omit) {
|
|
|
265
269
|
throw new ValidationError(selectOmitExclusiveMessage(table));
|
|
266
270
|
}
|
|
267
271
|
}
|
|
268
|
-
export function includeKeysForBatching(
|
|
272
|
+
export function includeKeysForBatching(
|
|
273
|
+
/**
|
|
274
|
+
* The table the projection is compiled against. `select` / `omit` keys are
|
|
275
|
+
* the CALLER's, and a column has two legal spellings there, so matching by
|
|
276
|
+
* raw key made "is the correlation key already projected?" depend on which
|
|
277
|
+
* was used: `select: { user_id: true }` with a `userId` key looked
|
|
278
|
+
* unprojected, so the key was force-added AND marked stitch-only and
|
|
279
|
+
* `stripFields` deleted the very column the caller asked for, while
|
|
280
|
+
* `omit: { user_id: true }` failed to un-omit and tripped
|
|
281
|
+
* `assertCorrelationKeyProjected`'s "bug in turbine" path on a legal query.
|
|
282
|
+
*/
|
|
283
|
+
meta, select, omit, fields,
|
|
269
284
|
/**
|
|
270
285
|
* The default projection for this table when it is NOT `select`/`omit`-driven:
|
|
271
286
|
* `hidden` are fields the default projection leaves out (today: PII-tagged
|
|
@@ -279,23 +294,43 @@ export function includeKeysForBatching(select, omit, fields,
|
|
|
279
294
|
*/
|
|
280
295
|
defaultProjection) {
|
|
281
296
|
const unique = [...new Set(fields)];
|
|
297
|
+
/**
|
|
298
|
+
* The caller's projection keys indexed by the column each resolves to, so a
|
|
299
|
+
* correlation key is recognized under either spelling. A key resolving to no
|
|
300
|
+
* column is left out: the projection build raises E003, where it belongs.
|
|
301
|
+
*/
|
|
302
|
+
const keyByColumn = (projection) => {
|
|
303
|
+
const byColumn = new Map();
|
|
304
|
+
for (const key of Object.keys(projection)) {
|
|
305
|
+
const column = resolveColumnName(meta, key);
|
|
306
|
+
if (column !== undefined)
|
|
307
|
+
byColumn.set(column, key);
|
|
308
|
+
}
|
|
309
|
+
return byColumn;
|
|
310
|
+
};
|
|
311
|
+
/** `fields` are canonical field names, but resolve them anyway rather than assume. */
|
|
312
|
+
const columnOf = (field) => resolveColumnName(meta, field) ?? field;
|
|
282
313
|
if (select) {
|
|
283
314
|
const next = { ...select };
|
|
284
315
|
const strip = [];
|
|
316
|
+
const selected = keyByColumn(select);
|
|
285
317
|
for (const f of unique) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
318
|
+
const existing = selected.get(columnOf(f));
|
|
319
|
+
if (existing !== undefined && next[existing])
|
|
320
|
+
continue; // already projected, under either spelling
|
|
321
|
+
next[f] = true;
|
|
322
|
+
strip.push(f); // not requested by the caller, added only to stitch
|
|
290
323
|
}
|
|
291
324
|
return { select: next, omit, strip };
|
|
292
325
|
}
|
|
293
326
|
if (omit) {
|
|
294
327
|
const next = { ...omit };
|
|
295
328
|
const strip = [];
|
|
329
|
+
const omitted = keyByColumn(omit);
|
|
296
330
|
for (const f of unique) {
|
|
297
|
-
|
|
298
|
-
|
|
331
|
+
const existing = omitted.get(columnOf(f));
|
|
332
|
+
if (existing !== undefined && next[existing]) {
|
|
333
|
+
delete next[existing]; // un-omit so the key is present; the caller wanted it gone
|
|
299
334
|
strip.push(f);
|
|
300
335
|
}
|
|
301
336
|
}
|
|
@@ -623,7 +658,7 @@ async function loadToOneOrMany(ctx, parents, rel, relName, options, timeout, dep
|
|
|
623
658
|
const orderFields = pushDownLimit
|
|
624
659
|
? (windowOrder ?? []).map((o) => targetMeta.reverseColumnMap[o.column] ?? o.column)
|
|
625
660
|
: [];
|
|
626
|
-
const proj = includeKeysForBatching(options.select, options.omit, [childKeyField, ...orderFields, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
661
|
+
const proj = includeKeysForBatching(targetMeta, options.select, options.omit, [childKeyField, ...orderFields, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
627
662
|
const child = ctx.makeChild(rel.to);
|
|
628
663
|
const buildChunk = (chunk) => child.buildFindMany({
|
|
629
664
|
where: mergeChildWhere(options.where, childKeyField, chunk),
|
|
@@ -826,7 +861,7 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
|
|
|
826
861
|
// reason as the to-many loader above: this level's PK is not the only key the
|
|
827
862
|
// recursion below will ask these rows for.
|
|
828
863
|
assertProjectionShape(targetMeta.name, options.select, options.omit);
|
|
829
|
-
const proj = includeKeysForBatching(options.select, options.omit, [targetPkField, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
864
|
+
const proj = includeKeysForBatching(targetMeta, options.select, options.omit, [targetPkField, ...neededParentKeyFields(targetMeta, (options.with ?? {}))], defaultProjectionFields(targetMeta, ctx.includePii));
|
|
830
865
|
const child = ctx.makeChild(rel.to);
|
|
831
866
|
const buildTargetChunk = (chunk) => child.buildFindMany({
|
|
832
867
|
where: mergeChildWhere(options.where, targetPkField, chunk),
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -538,6 +538,19 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
538
538
|
* so a warm template can never serve a call the cold path would refuse.
|
|
539
539
|
*/
|
|
540
540
|
private resolveJsonEncoding;
|
|
541
|
+
/**
|
|
542
|
+
* `args` with every `with` relation key replaced by the relation's DECLARED
|
|
543
|
+
* spelling, so a snake_case relation name resolves the way a snake_case
|
|
544
|
+
* column name already does.
|
|
545
|
+
*
|
|
546
|
+
* Returns `args` by reference when nothing needed rewriting, which is every
|
|
547
|
+
* query that already spells its relations the declared way. Runs before the
|
|
548
|
+
* stable-order pass and before `withFingerprint`, so the whole pipeline, and
|
|
549
|
+
* the SQL cache key with it, sees one spelling. The rule and the reason it is
|
|
550
|
+
* applied ONCE here rather than at each of the six `with` walkers are in
|
|
551
|
+
* query/relation-names.ts.
|
|
552
|
+
*/
|
|
553
|
+
private withDeclaredRelationNames;
|
|
541
554
|
/**
|
|
542
555
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
543
556
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
package/dist/query/builder.js
CHANGED
|
@@ -19,9 +19,10 @@ import * as aggMod from './aggregates.js';
|
|
|
19
19
|
import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
|
|
20
20
|
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
21
21
|
import { dedupeColumnList, dedupeOrderEntries, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
22
|
+
import { normalizeWithClause } from './relation-names.js';
|
|
22
23
|
import * as relationsMod from './relations.js';
|
|
23
24
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
24
|
-
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
25
|
+
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
25
26
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
26
27
|
import * as whereMod from './where.js';
|
|
27
28
|
import * as writesMod from './writes.js';
|
|
@@ -1064,6 +1065,24 @@ export class QueryInterface {
|
|
|
1064
1065
|
this.currentJsonEncoding = argEncoding;
|
|
1065
1066
|
return argEncoding;
|
|
1066
1067
|
}
|
|
1068
|
+
/**
|
|
1069
|
+
* `args` with every `with` relation key replaced by the relation's DECLARED
|
|
1070
|
+
* spelling, so a snake_case relation name resolves the way a snake_case
|
|
1071
|
+
* column name already does.
|
|
1072
|
+
*
|
|
1073
|
+
* Returns `args` by reference when nothing needed rewriting, which is every
|
|
1074
|
+
* query that already spells its relations the declared way. Runs before the
|
|
1075
|
+
* stable-order pass and before `withFingerprint`, so the whole pipeline, and
|
|
1076
|
+
* the SQL cache key with it, sees one spelling. The rule and the reason it is
|
|
1077
|
+
* applied ONCE here rather than at each of the six `with` walkers are in
|
|
1078
|
+
* query/relation-names.ts.
|
|
1079
|
+
*/
|
|
1080
|
+
withDeclaredRelationNames(args) {
|
|
1081
|
+
if (!args?.with)
|
|
1082
|
+
return args;
|
|
1083
|
+
const normalized = normalizeWithClause(this.schema, this.table, args.with);
|
|
1084
|
+
return normalized === args.with ? args : { ...args, with: normalized };
|
|
1085
|
+
}
|
|
1067
1086
|
/**
|
|
1068
1087
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
1069
1088
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -1084,7 +1103,7 @@ export class QueryInterface {
|
|
|
1084
1103
|
for (const [relName, spec] of Object.entries(withClause)) {
|
|
1085
1104
|
if (relName === '_count' || !spec)
|
|
1086
1105
|
continue; // `_count` is a count, not a row load
|
|
1087
|
-
const rel =
|
|
1106
|
+
const rel = resolveRelationDef(meta.relations, relName);
|
|
1088
1107
|
if (!rel)
|
|
1089
1108
|
continue; // unknown relation, let the build path surface E005
|
|
1090
1109
|
const options = spec === true ? {} : spec;
|
|
@@ -1471,7 +1490,7 @@ export class QueryInterface {
|
|
|
1471
1490
|
}
|
|
1472
1491
|
continue;
|
|
1473
1492
|
}
|
|
1474
|
-
const rel =
|
|
1493
|
+
const rel = resolveRelationDef(this.tableMeta.relations, key);
|
|
1475
1494
|
if (!rel) {
|
|
1476
1495
|
joinWith[key] = spec; // unknown relation, let the join path surface E005
|
|
1477
1496
|
continue;
|
|
@@ -1581,7 +1600,7 @@ export class QueryInterface {
|
|
|
1581
1600
|
const includePii = resolveUnsafeFlag(args.includePii, 'includePii');
|
|
1582
1601
|
const needed = neededParentKeyFields(this.tableMeta, batchedWith);
|
|
1583
1602
|
assertProjectionShape(this.table, args.select, args.omit);
|
|
1584
|
-
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, includePii));
|
|
1603
|
+
const proj = includeKeysForBatching(this.tableMeta, args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, includePii));
|
|
1585
1604
|
const hasJoin = Object.keys(joinWith).length > 0;
|
|
1586
1605
|
// Force the residual `with` onto the join plan so the base query never
|
|
1587
1606
|
// re-enters this auto planning.
|
|
@@ -1719,6 +1738,10 @@ export class QueryInterface {
|
|
|
1719
1738
|
* from the returned rows, so the shape matches the join strategy exactly.
|
|
1720
1739
|
*/
|
|
1721
1740
|
async runFindManyBatched(args) {
|
|
1741
|
+
// Declared relation spellings first, exactly as the join path does in
|
|
1742
|
+
// buildFindMany: the loader reads `args.with` itself, so without this the
|
|
1743
|
+
// two strategies would disagree about which relation names are valid.
|
|
1744
|
+
args = this.withDeclaredRelationNames(args);
|
|
1722
1745
|
// Stable relation order (opt-in): the batched loader forwards each relation's
|
|
1723
1746
|
// orderBy into its follow-up query, so filling the synthesized PK order here
|
|
1724
1747
|
// makes the batched output deterministic exactly like the join path.
|
|
@@ -1753,7 +1776,7 @@ export class QueryInterface {
|
|
|
1753
1776
|
prepareBatchedBase(args, withClause) {
|
|
1754
1777
|
const needed = neededParentKeyFields(this.tableMeta, withClause);
|
|
1755
1778
|
assertProjectionShape(this.table, args.select, args.omit);
|
|
1756
|
-
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
1779
|
+
const proj = includeKeysForBatching(this.tableMeta, args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
1757
1780
|
const baseArgs = {
|
|
1758
1781
|
...args,
|
|
1759
1782
|
with: undefined,
|
|
@@ -2125,6 +2148,15 @@ export class QueryInterface {
|
|
|
2125
2148
|
// findUnique
|
|
2126
2149
|
// -------------------------------------------------------------------------
|
|
2127
2150
|
async findUnique(args) {
|
|
2151
|
+
// BEFORE the strategy branch, not only inside the builders, so the
|
|
2152
|
+
// join-vs-batched decision and its dev warning see the same declared name
|
|
2153
|
+
// every other stage does. `planAuto` splits `with` on the caller's keys and
|
|
2154
|
+
// `runAutoSplit` carries them onward; each downstream consumer normalizes
|
|
2155
|
+
// too, so this is not load-bearing for correctness (measured: removing it
|
|
2156
|
+
// changes the warning text from the declared name back to the caller's and
|
|
2157
|
+
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2158
|
+
// depending on every consumer to re-establish it.
|
|
2159
|
+
args = this.withDeclaredRelationNames(args);
|
|
2128
2160
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
2129
2161
|
if (args.with) {
|
|
2130
2162
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -2152,6 +2184,8 @@ export class QueryInterface {
|
|
|
2152
2184
|
* strategy's shape for the one row.
|
|
2153
2185
|
*/
|
|
2154
2186
|
async runFindUniqueBatched(args) {
|
|
2187
|
+
// Declared relation spellings first, see runFindManyBatched.
|
|
2188
|
+
args = this.withDeclaredRelationNames(args);
|
|
2155
2189
|
// Stable relation order (opt-in), see runFindManyBatched.
|
|
2156
2190
|
const withClause = this.resolveStableOrder(args.stableRelationOrder)
|
|
2157
2191
|
? this.applyStableRelationOrder(args.with, this.table)
|
|
@@ -2160,7 +2194,7 @@ export class QueryInterface {
|
|
|
2160
2194
|
rejectNestedPickOrder(withClause);
|
|
2161
2195
|
const needed = neededParentKeyFields(this.tableMeta, withClause);
|
|
2162
2196
|
assertProjectionShape(this.table, args.select, args.omit);
|
|
2163
|
-
const proj = includeKeysForBatching(args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
2197
|
+
const proj = includeKeysForBatching(this.tableMeta, args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
|
|
2164
2198
|
const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
|
|
2165
2199
|
const deferred = this.buildFindUnique(baseArgs);
|
|
2166
2200
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
|
|
@@ -2205,6 +2239,9 @@ export class QueryInterface {
|
|
|
2205
2239
|
'A key whose value is `undefined` does not count, check that the value you are looking up is defined. ' +
|
|
2206
2240
|
'If you meant "any row matching an optional filter", use `findFirst`.');
|
|
2207
2241
|
}
|
|
2242
|
+
// Declared relation spellings first, before stable-order and the
|
|
2243
|
+
// fingerprint (see buildFindMany).
|
|
2244
|
+
args = this.withDeclaredRelationNames(args);
|
|
2208
2245
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2209
2246
|
// relations before fingerprinting (see buildFindMany).
|
|
2210
2247
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -2247,7 +2284,9 @@ export class QueryInterface {
|
|
|
2247
2284
|
!whereObj.NOT &&
|
|
2248
2285
|
whereKeys.every((k) => {
|
|
2249
2286
|
const v = whereObj[k];
|
|
2250
|
-
|
|
2287
|
+
// Resolved, not looked up: a relation filter spelled the snake_case
|
|
2288
|
+
// way must not read as a plain equality and take the simple path.
|
|
2289
|
+
return v !== null && !isWhereOperator(v) && !resolveRelationDef(this.tableMeta.relations, k);
|
|
2251
2290
|
});
|
|
2252
2291
|
// Simple path: plain equality, no operators/null/OR.
|
|
2253
2292
|
//
|
|
@@ -2343,6 +2382,15 @@ export class QueryInterface {
|
|
|
2343
2382
|
// findMany
|
|
2344
2383
|
// -------------------------------------------------------------------------
|
|
2345
2384
|
async findMany(args) {
|
|
2385
|
+
// BEFORE the strategy branch, not only inside the builders, so the
|
|
2386
|
+
// join-vs-batched decision and its dev warning see the same declared name
|
|
2387
|
+
// every other stage does. `planAuto` splits `with` on the caller's keys and
|
|
2388
|
+
// `runAutoSplit` carries them onward; each downstream consumer normalizes
|
|
2389
|
+
// too, so this is not load-bearing for correctness (measured: removing it
|
|
2390
|
+
// changes the warning text from the declared name back to the caller's and
|
|
2391
|
+
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2392
|
+
// depending on every consumer to re-establish it.
|
|
2393
|
+
args = this.withDeclaredRelationNames(args);
|
|
2346
2394
|
this.maybeWarnUnlimited(args);
|
|
2347
2395
|
this.maybeWarnUnorderedPage(args);
|
|
2348
2396
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -2493,8 +2541,12 @@ export class QueryInterface {
|
|
|
2493
2541
|
const isScalarEquality = value !== null && (typeof value !== 'object' || value instanceof Date) && typeof value !== 'function';
|
|
2494
2542
|
if (!isScalarEquality)
|
|
2495
2543
|
return false;
|
|
2496
|
-
|
|
2497
|
-
|
|
2544
|
+
// The one key-resolution rule: a bare `columnMap` read knows only the
|
|
2545
|
+
// FIELD spelling, so `where: { user_id: 1 }` on a unique column did not
|
|
2546
|
+
// count as pinning it and drew a spurious unlimited-read warning the
|
|
2547
|
+
// camelCase spelling of the same query did not.
|
|
2548
|
+
const column = resolveColumnName(this.tableMeta, field);
|
|
2549
|
+
if (column === undefined)
|
|
2498
2550
|
return false;
|
|
2499
2551
|
pinned.add(column);
|
|
2500
2552
|
}
|
|
@@ -2521,6 +2573,11 @@ export class QueryInterface {
|
|
|
2521
2573
|
this.currentSkip = resolveSkipGlobalFilters(args?.skipGlobalFilters);
|
|
2522
2574
|
// Pinned before the flatten plan and the cache key, both of which read it.
|
|
2523
2575
|
const jsonEncoding = this.resolveJsonEncoding(args?.jsonEncoding);
|
|
2576
|
+
// Relation names to their declared spelling FIRST, before the stable-order
|
|
2577
|
+
// pass and the fingerprint below and before any of the six `with` walkers,
|
|
2578
|
+
// so none of them needs to know a relation has two accepted spellings and
|
|
2579
|
+
// both spellings share one cache entry. See query/relation-names.ts.
|
|
2580
|
+
args = this.withDeclaredRelationNames(args);
|
|
2524
2581
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2525
2582
|
// relations BEFORE fingerprinting, so the two orderings get distinct cache
|
|
2526
2583
|
// entries and every downstream path (SQL build, collect, parser) inherits it.
|
|
@@ -2632,7 +2689,13 @@ export class QueryInterface {
|
|
|
2632
2689
|
// distinct key (correct, it emits a different ORDER BY).
|
|
2633
2690
|
const orderFp = args?.orderBy
|
|
2634
2691
|
? orderByEntries(args.orderBy)
|
|
2635
|
-
|
|
2692
|
+
// Keyed by the DECLARED relation name (falling back to the key
|
|
2693
|
+
// itself for a column), so the two spellings of one orderBy share a
|
|
2694
|
+
// cache entry instead of minting two templates for one query.
|
|
2695
|
+
.map(([k, d]) => {
|
|
2696
|
+
const rel = resolveRelation(this.tableMeta.relations, k);
|
|
2697
|
+
return `${rel?.name ?? k}:${this.orderByEntryFingerprint(d, rel?.def.to)}`;
|
|
2698
|
+
})
|
|
2636
2699
|
.join(',')
|
|
2637
2700
|
: '';
|
|
2638
2701
|
const cursorFp = args?.cursor
|
|
@@ -2754,12 +2817,27 @@ export class QueryInterface {
|
|
|
2754
2817
|
// Resolve the seek direction per cursor field from the flattened
|
|
2755
2818
|
// orderBy entries (last wins, matching object-key semantics), so both
|
|
2756
2819
|
// the object and array orderBy forms drive the cursor comparison.
|
|
2757
|
-
|
|
2820
|
+
//
|
|
2821
|
+
// Indexed by the RESOLVED COLUMN, never the caller's key: both
|
|
2822
|
+
// `cursor` and `orderBy` take either spelling, so a cursor written
|
|
2823
|
+
// `{ created_at }` against `orderBy: { createdAt: 'desc' }` missed
|
|
2824
|
+
// this lookup, defaulted to ascending, and emitted `created_at > $n`
|
|
2825
|
+
// under `ORDER BY created_at DESC` — the wrong page, silently. Same
|
|
2826
|
+
// failure the `{ sort, nulls }` normalization below prevents, reached
|
|
2827
|
+
// through the spelling instead of the value shape. A relation /
|
|
2828
|
+
// JSON-path / vector key resolves to no column and is skipped.
|
|
2829
|
+
const orderDirByColumn = new Map();
|
|
2830
|
+
for (const [ok, od] of orderByEntries(args.orderBy)) {
|
|
2831
|
+
const ocol = resolveColumnName(this.tableMeta, ok);
|
|
2832
|
+
if (ocol !== undefined)
|
|
2833
|
+
orderDirByColumn.set(ocol, od);
|
|
2834
|
+
}
|
|
2758
2835
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
2759
|
-
const
|
|
2836
|
+
const rawCol = this.toColumn(k);
|
|
2837
|
+
const col = this.q(rawCol);
|
|
2760
2838
|
// orderBy values can be the { sort, nulls } spec form: normalize
|
|
2761
2839
|
// before comparing, or a desc spec would seek the ascending side.
|
|
2762
|
-
const dir =
|
|
2840
|
+
const dir = orderDirByColumn.get(rawCol);
|
|
2763
2841
|
const desc = isOrderBySpec(dir) ? dir.sort === 'desc' : dir === 'desc';
|
|
2764
2842
|
const op = desc ? '<' : '>';
|
|
2765
2843
|
freshParams.push(v);
|
|
@@ -3090,6 +3168,15 @@ export class QueryInterface {
|
|
|
3090
3168
|
// findFirst, like findMany but returns a single row or null
|
|
3091
3169
|
// -------------------------------------------------------------------------
|
|
3092
3170
|
async findFirst(args) {
|
|
3171
|
+
// BEFORE the strategy branch, not only inside the builders, so the
|
|
3172
|
+
// join-vs-batched decision and its dev warning see the same declared name
|
|
3173
|
+
// every other stage does. `planAuto` splits `with` on the caller's keys and
|
|
3174
|
+
// `runAutoSplit` carries them onward; each downstream consumer normalizes
|
|
3175
|
+
// too, so this is not load-bearing for correctness (measured: removing it
|
|
3176
|
+
// changes the warning text from the declared name back to the caller's and
|
|
3177
|
+
// nothing else). It is here so the invariant holds at the seam rather than
|
|
3178
|
+
// depending on every consumer to re-establish it.
|
|
3179
|
+
args = this.withDeclaredRelationNames(args);
|
|
3093
3180
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
3094
3181
|
if (args?.with) {
|
|
3095
3182
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -3667,9 +3754,12 @@ export class QueryInterface {
|
|
|
3667
3754
|
// pick.where / pick.orderBy paths). To-one relation orderBy carries the
|
|
3668
3755
|
// target's global filter once per ordered column.
|
|
3669
3756
|
if (this.isRelationOrderByValue(dir)) {
|
|
3670
|
-
|
|
3757
|
+
// Mirrors the build path's resolution, so a cache HIT binds params for
|
|
3758
|
+
// the same relation the cached SQL was built from.
|
|
3759
|
+
const resolvedRel = resolveRelation(this.tableMeta.relations, key);
|
|
3760
|
+
const relDef = resolvedRel?.def;
|
|
3671
3761
|
if (relDef && isRelationPickOrderBy(dir)) {
|
|
3672
|
-
this.collectRelationPickOrderParams(
|
|
3762
|
+
this.collectRelationPickOrderParams(resolvedRel.name, relDef, dir, params);
|
|
3673
3763
|
}
|
|
3674
3764
|
else if (relDef && (relDef.type === 'hasMany' || relDef.type === 'manyToMany')) {
|
|
3675
3765
|
this.collectRelationCountParams(relDef, params);
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import { ValidationError } from '../errors.js';
|
|
42
42
|
import { isArrayFilter, isJsonFilter, isVectorFilter, isWhereOperator } from './filters.js';
|
|
43
|
-
import { ownLookup } from './utils.js';
|
|
43
|
+
import { ownLookup, resolveColumnName, resolveRelationDef } from './utils.js';
|
|
44
44
|
const syntheticKeyCache = new WeakMap();
|
|
45
45
|
/**
|
|
46
46
|
* Build (once per table) the map from a synthetic selector name to the ordered
|
|
@@ -110,7 +110,9 @@ function isRealKey(meta, key) {
|
|
|
110
110
|
return (ownLookup(meta.columnMap, key) !== undefined ||
|
|
111
111
|
ownLookup(meta.reverseColumnMap, key) !== undefined ||
|
|
112
112
|
meta.allColumns.includes(key) ||
|
|
113
|
-
|
|
113
|
+
// Resolved, so a relation named the snake_case way still reads as a real
|
|
114
|
+
// key here and is not mistaken for a compound-unique selector.
|
|
115
|
+
resolveRelationDef(meta.relations, key) !== undefined);
|
|
114
116
|
}
|
|
115
117
|
/**
|
|
116
118
|
* A candidate compound-unique selector value: a plain object that is not a
|
|
@@ -152,16 +154,38 @@ export function expandCompoundUniqueWhere(meta, where) {
|
|
|
152
154
|
continue; // unknown key, falls through to the standard E003
|
|
153
155
|
const selector = value;
|
|
154
156
|
const provided = Object.keys(selector).filter((k) => selector[k] !== undefined);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
// Matched by the COLUMN each member name resolves to, not by the literal
|
|
158
|
+
// key. The selector's own NAME is registered under both spellings (see
|
|
159
|
+
// `register` above), so `{ org_id_user_id: { org_id, user_id } }` found the
|
|
160
|
+
// selector and was then refused for its members. Insertion order is
|
|
161
|
+
// `fields` order, keeping the expansion (and the SQL) stable.
|
|
162
|
+
const expected = new Map();
|
|
163
|
+
for (const f of fields)
|
|
164
|
+
expected.set(resolveColumnName(meta, f) ?? f, f);
|
|
165
|
+
/** column → the key the caller actually wrote for it. */
|
|
166
|
+
const providedByColumn = new Map();
|
|
167
|
+
let ambiguous = false;
|
|
168
|
+
for (const k of provided) {
|
|
169
|
+
const column = resolveColumnName(meta, k);
|
|
170
|
+
// Unresolvable, or two spellings of one column: fall through to the
|
|
171
|
+
// same refusal an incomplete member set gets.
|
|
172
|
+
if (column === undefined || providedByColumn.has(column)) {
|
|
173
|
+
ambiguous = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
providedByColumn.set(column, k);
|
|
177
|
+
}
|
|
178
|
+
const exact = !ambiguous &&
|
|
179
|
+
providedByColumn.size === expected.size &&
|
|
180
|
+
[...providedByColumn.keys()].every((c) => expected.has(c));
|
|
157
181
|
if (!exact) {
|
|
158
182
|
throw new ValidationError(`[turbine] Compound unique selector "${key}" on table "${meta.name}" must supply exactly ` +
|
|
159
183
|
`{ ${fields.join(', ')} }, received { ${provided.join(', ') || '(none)'} }.`);
|
|
160
184
|
}
|
|
161
185
|
result ??= { ...where };
|
|
162
186
|
delete result[key];
|
|
163
|
-
for (const field of
|
|
164
|
-
const v = selector[
|
|
187
|
+
for (const [column, field] of expected) {
|
|
188
|
+
const v = selector[providedByColumn.get(column)];
|
|
165
189
|
if (Object.hasOwn(result, field)) {
|
|
166
190
|
// A member field is ALSO given directly in the outer where: wrap the
|
|
167
191
|
// expansion in AND so neither value is clobbered.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller-supplied RELATION names, normalized to their declared spelling once,
|
|
3
|
+
* before anything reads them.
|
|
4
|
+
*
|
|
5
|
+
* ## The rule
|
|
6
|
+
*
|
|
7
|
+
* A relation has one declared name (`ripeningChecks`), while the DDL anyone
|
|
8
|
+
* reads has only the table name (`ripening_checks`). Writing back what the
|
|
9
|
+
* schema shows therefore failed: E005 in `with`, E003 in a relation filter,
|
|
10
|
+
* E005 in `orderBy`, on names the error text was already computing correctly
|
|
11
|
+
* ("Did you mean ...?"). This accepts the snake_case spelling wherever the
|
|
12
|
+
* declared name is accepted, by the same resolve-then-validate rule
|
|
13
|
+
* {@link resolveRelation} states, which is the relation-level twin of
|
|
14
|
+
* `resolveColumnName`. It is not a guess: `snakeToCamel(key)` is accepted ONLY
|
|
15
|
+
* when it names a real declared relation, so a typo is still a typo, and an
|
|
16
|
+
* exact declared name always wins first.
|
|
17
|
+
*
|
|
18
|
+
* ## Why this is ONE pass up front and not a fix at each lookup
|
|
19
|
+
*
|
|
20
|
+
* The `with` tree is walked by SIX independent functions, each of which decides
|
|
21
|
+
* for itself which keys name relations: `withFingerprint`, `collectWithParams`,
|
|
22
|
+
* `buildRelationShapes`, `planFlattenWith`, `buildSelectWithRelations` and the
|
|
23
|
+
* batched loader, plus the positional row parser. Teaching each of them that a
|
|
24
|
+
* key has two spellings would make seven places that must agree about it, and
|
|
25
|
+
* disagreement is not a clean failure: the fingerprint is the SQL-cache key, so
|
|
26
|
+
* a walker that resolved differently from the builder would serve one query's
|
|
27
|
+
* template to another, silently. That is the drift class this repo has paid for
|
|
28
|
+
* repeatedly (the where-clause walkers, the two projection resolvers).
|
|
29
|
+
*
|
|
30
|
+
* Normalizing before any walker runs makes all seven correct with no knowledge
|
|
31
|
+
* of the second spelling, and keeps ONE authority for the rule.
|
|
32
|
+
*
|
|
33
|
+
* ## Shape
|
|
34
|
+
*
|
|
35
|
+
* Returns the SAME object when nothing needed rewriting, which is every query
|
|
36
|
+
* that already spells its relations the declared way, so the common path
|
|
37
|
+
* allocates nothing and is reference-identical to its input.
|
|
38
|
+
*
|
|
39
|
+
* An UNRESOLVABLE key is left exactly as written, deliberately. Reporting it is
|
|
40
|
+
* the builder's job, and it already names the offending key and lists the
|
|
41
|
+
* available relations; rejecting it here would move that error away from its
|
|
42
|
+
* context and change which error type callers see.
|
|
43
|
+
*/
|
|
44
|
+
import type { SchemaMetadata } from '../schema.js';
|
|
45
|
+
import type { WithClause } from './types.js';
|
|
46
|
+
/**
|
|
47
|
+
* `withClause` with every relation key replaced by its declared spelling,
|
|
48
|
+
* recursively, including the relation names inside a `_count`.
|
|
49
|
+
*
|
|
50
|
+
* Returns the input by reference when no key changed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function normalizeWithClause(schema: SchemaMetadata, table: string, withClause: WithClause | undefined, depth?: number): WithClause | undefined;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caller-supplied RELATION names, normalized to their declared spelling once,
|
|
3
|
+
* before anything reads them.
|
|
4
|
+
*
|
|
5
|
+
* ## The rule
|
|
6
|
+
*
|
|
7
|
+
* A relation has one declared name (`ripeningChecks`), while the DDL anyone
|
|
8
|
+
* reads has only the table name (`ripening_checks`). Writing back what the
|
|
9
|
+
* schema shows therefore failed: E005 in `with`, E003 in a relation filter,
|
|
10
|
+
* E005 in `orderBy`, on names the error text was already computing correctly
|
|
11
|
+
* ("Did you mean ...?"). This accepts the snake_case spelling wherever the
|
|
12
|
+
* declared name is accepted, by the same resolve-then-validate rule
|
|
13
|
+
* {@link resolveRelation} states, which is the relation-level twin of
|
|
14
|
+
* `resolveColumnName`. It is not a guess: `snakeToCamel(key)` is accepted ONLY
|
|
15
|
+
* when it names a real declared relation, so a typo is still a typo, and an
|
|
16
|
+
* exact declared name always wins first.
|
|
17
|
+
*
|
|
18
|
+
* ## Why this is ONE pass up front and not a fix at each lookup
|
|
19
|
+
*
|
|
20
|
+
* The `with` tree is walked by SIX independent functions, each of which decides
|
|
21
|
+
* for itself which keys name relations: `withFingerprint`, `collectWithParams`,
|
|
22
|
+
* `buildRelationShapes`, `planFlattenWith`, `buildSelectWithRelations` and the
|
|
23
|
+
* batched loader, plus the positional row parser. Teaching each of them that a
|
|
24
|
+
* key has two spellings would make seven places that must agree about it, and
|
|
25
|
+
* disagreement is not a clean failure: the fingerprint is the SQL-cache key, so
|
|
26
|
+
* a walker that resolved differently from the builder would serve one query's
|
|
27
|
+
* template to another, silently. That is the drift class this repo has paid for
|
|
28
|
+
* repeatedly (the where-clause walkers, the two projection resolvers).
|
|
29
|
+
*
|
|
30
|
+
* Normalizing before any walker runs makes all seven correct with no knowledge
|
|
31
|
+
* of the second spelling, and keeps ONE authority for the rule.
|
|
32
|
+
*
|
|
33
|
+
* ## Shape
|
|
34
|
+
*
|
|
35
|
+
* Returns the SAME object when nothing needed rewriting, which is every query
|
|
36
|
+
* that already spells its relations the declared way, so the common path
|
|
37
|
+
* allocates nothing and is reference-identical to its input.
|
|
38
|
+
*
|
|
39
|
+
* An UNRESOLVABLE key is left exactly as written, deliberately. Reporting it is
|
|
40
|
+
* the builder's job, and it already names the offending key and lists the
|
|
41
|
+
* available relations; rejecting it here would move that error away from its
|
|
42
|
+
* context and change which error type callers see.
|
|
43
|
+
*/
|
|
44
|
+
import { resolveRelation } from './utils.js';
|
|
45
|
+
/** Depth cap mirroring the builder's own, so a cyclic `with` cannot spin here. */
|
|
46
|
+
const MAX_DEPTH = 12;
|
|
47
|
+
/**
|
|
48
|
+
* `withClause` with every relation key replaced by its declared spelling,
|
|
49
|
+
* recursively, including the relation names inside a `_count`.
|
|
50
|
+
*
|
|
51
|
+
* Returns the input by reference when no key changed.
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeWithClause(schema, table, withClause, depth = 0) {
|
|
54
|
+
if (!withClause || typeof withClause !== 'object' || depth > MAX_DEPTH)
|
|
55
|
+
return withClause;
|
|
56
|
+
const meta = schema.tables[table];
|
|
57
|
+
if (!meta)
|
|
58
|
+
return withClause;
|
|
59
|
+
let changed = false;
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [key, spec] of Object.entries(withClause)) {
|
|
62
|
+
if (key === '_count') {
|
|
63
|
+
const nextCount = normalizeCount(meta, spec);
|
|
64
|
+
if (nextCount !== spec)
|
|
65
|
+
changed = true;
|
|
66
|
+
out[key] = nextCount;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const resolved = resolveRelation(meta.relations, key);
|
|
70
|
+
// Unknown key: keep it verbatim and let the builder raise E005 by name.
|
|
71
|
+
const name = resolved?.name ?? key;
|
|
72
|
+
if (name !== key)
|
|
73
|
+
changed = true;
|
|
74
|
+
const nextSpec = resolved ? normalizeSpec(schema, resolved.def.to, spec, depth) : spec;
|
|
75
|
+
if (nextSpec !== spec)
|
|
76
|
+
changed = true;
|
|
77
|
+
out[name] = nextSpec;
|
|
78
|
+
}
|
|
79
|
+
return changed ? out : withClause;
|
|
80
|
+
}
|
|
81
|
+
/** A relation's `with` options, normalizing its nested `with` against the TARGET table. */
|
|
82
|
+
function normalizeSpec(schema, target, spec, depth) {
|
|
83
|
+
if (spec === true || spec === false || spec === null || typeof spec !== 'object')
|
|
84
|
+
return spec;
|
|
85
|
+
const opts = spec;
|
|
86
|
+
if (!opts.with)
|
|
87
|
+
return spec;
|
|
88
|
+
const nested = normalizeWithClause(schema, target, opts.with, depth + 1);
|
|
89
|
+
return nested === opts.with ? spec : { ...opts, with: nested };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `_count` names relations too. `true` means every relation and has nothing to
|
|
93
|
+
* rename; the record form has one key per counted relation.
|
|
94
|
+
*/
|
|
95
|
+
function normalizeCount(meta, count) {
|
|
96
|
+
if (!count || typeof count !== 'object')
|
|
97
|
+
return count;
|
|
98
|
+
let changed = false;
|
|
99
|
+
const out = {};
|
|
100
|
+
for (const [key, value] of Object.entries(count)) {
|
|
101
|
+
const name = resolveRelation(meta.relations, key)?.name ?? key;
|
|
102
|
+
if (name !== key)
|
|
103
|
+
changed = true;
|
|
104
|
+
out[name] = value;
|
|
105
|
+
}
|
|
106
|
+
return changed ? out : count;
|
|
107
|
+
}
|
|
108
|
+
/*
|
|
109
|
+
* NOTE for the next person: there are deliberately no `declaredRelationName` /
|
|
110
|
+
* `namesRelation` wrappers here. The argument positions where a relation key
|
|
111
|
+
* sits INTERLEAVED with column keys (`where`'s some/every/none, `orderBy`'s
|
|
112
|
+
* relation targets, the simple-where fast path) cannot be normalized up front
|
|
113
|
+
* the way `with` can, so they call `resolveRelation` / `resolveRelationDef`
|
|
114
|
+
* from query/utils.ts directly at their branch point. Each of those branches is
|
|
115
|
+
* already a documented single authority; wrapping them here would add a second
|
|
116
|
+
* name for the same rule without removing a caller.
|
|
117
|
+
*/
|
|
@@ -117,12 +117,17 @@ export declare function isRelationOrderByValue(_qi: BuilderCtx, value: unknown):
|
|
|
117
117
|
*/
|
|
118
118
|
export declare function nullsSuffix(qi: BuilderCtx, nulls: 'first' | 'last' | undefined): string;
|
|
119
119
|
/**
|
|
120
|
-
* Resolve an orderBy key to its snake_case column via
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
120
|
+
* Resolve an orderBy key to its snake_case column via {@link resolveColumnName},
|
|
121
|
+
* throwing the SAME unknown-field E003 the top-level where path uses. Shared by
|
|
122
|
+
* top-level JSON-path ordering and every nested relation orderBy path so nested
|
|
123
|
+
* orderBy accepts exactly what top-level accepts (the 0.30.x bug: nested orderBy
|
|
124
|
+
* skipped the columnMap and rejected camelCase-named DB columns like
|
|
125
|
+
* "sortOrder").
|
|
126
|
+
*
|
|
127
|
+
* The rule is REACHED here, never restated: this used to inline
|
|
128
|
+
* `columnMap ?? camelToSnake` + an `allColumns` check, which agreed with
|
|
129
|
+
* `resolveColumnName` while the top-level scalar path a few lines up disagreed
|
|
130
|
+
* with both.
|
|
126
131
|
*/
|
|
127
132
|
export declare function resolveOrderByColumn(_qi: BuilderCtx, table: string, meta: TableMetadata, key: string): string;
|
|
128
133
|
/**
|