turbine-orm 0.19.1 → 0.21.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 +156 -24
- package/dist/adapters/cockroachdb.js +4 -9
- package/dist/cjs/adapters/cockroachdb.js +4 -9
- package/dist/cjs/cli/index.js +23 -12
- package/dist/cjs/cli/observe-ui.js +12 -2
- package/dist/cjs/cli/observe.js +6 -7
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +6 -37
- package/dist/cjs/client.js +45 -33
- package/dist/cjs/dialect.js +116 -0
- package/dist/cjs/errors.js +19 -1
- package/dist/cjs/generate.js +4 -0
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/introspect.js +20 -0
- package/dist/cjs/mssql.js +1338 -0
- package/dist/cjs/mysql.js +1052 -0
- package/dist/cjs/query/builder.js +408 -122
- package/dist/cjs/query/utils.js +1 -0
- package/dist/cjs/sqlite.js +849 -0
- package/dist/cjs/typed-sql.js +9 -7
- package/dist/cli/index.js +23 -12
- package/dist/cli/migrate.d.ts +2 -2
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +12 -2
- package/dist/cli/observe.js +7 -8
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +0 -10
- package/dist/cli/studio.js +7 -37
- package/dist/client.d.ts +35 -14
- package/dist/client.js +46 -34
- package/dist/dialect.d.ts +258 -1
- package/dist/dialect.js +83 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +17 -0
- package/dist/generate.js +4 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +18 -0
- package/dist/introspect.js +19 -0
- package/dist/mssql.d.ts +233 -0
- package/dist/mssql.js +1298 -0
- package/dist/mysql.d.ts +174 -0
- package/dist/mysql.js +1012 -0
- package/dist/query/builder.d.ts +105 -6
- package/dist/query/builder.js +409 -123
- package/dist/query/index.d.ts +2 -2
- package/dist/query/types.d.ts +62 -12
- package/dist/query/utils.js +1 -0
- package/dist/sqlite.d.ts +144 -0
- package/dist/sqlite.js +842 -0
- package/dist/typed-sql.d.ts +7 -5
- package/dist/typed-sql.js +9 -7
- package/package.json +32 -3
|
@@ -80,6 +80,53 @@ function isUnmatchedPlainObject(value) {
|
|
|
80
80
|
const proto = Object.getPrototypeOf(value);
|
|
81
81
|
return proto === Object.prototype || proto === null;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
|
|
85
|
+
* `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
|
|
86
|
+
* param pushed), so null-ness is part of the shape — without it a cache entry
|
|
87
|
+
* warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
|
|
88
|
+
*/
|
|
89
|
+
function fingerprintOperatorShape(value) {
|
|
90
|
+
const obj = value;
|
|
91
|
+
const opKeys = Object.keys(obj)
|
|
92
|
+
.filter((k) => k !== 'mode')
|
|
93
|
+
.map((k) => ((k === 'equals' || k === 'not') && obj[k] === null ? `${k}:null` : k))
|
|
94
|
+
.sort();
|
|
95
|
+
const modeStr = value.mode === 'insensitive' ? ':i' : '';
|
|
96
|
+
return `op(${opKeys.join(',')}${modeStr})`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Guard for the value of an `equals` operator reaching the plain-equality
|
|
100
|
+
* operator path. A plain object literal can only legitimately be an equality
|
|
101
|
+
* value on a json/jsonb column — and those route to the JSONB filter branch
|
|
102
|
+
* BEFORE the operator branch, so any plain object that reaches here is a
|
|
103
|
+
* mistake (e.g. `{ equals: { foo: 1 } }` on a text column). Shared by the
|
|
104
|
+
* SQL-build path and the cache-hit param-collect path so a warmed cache can
|
|
105
|
+
* never skip the check.
|
|
106
|
+
*/
|
|
107
|
+
function assertBindableEqualsOperand(value, column) {
|
|
108
|
+
if (!isUnmatchedPlainObject(value))
|
|
109
|
+
return;
|
|
110
|
+
throw new errors_js_1.ValidationError(`[turbine] Plain-object value for operator 'equals' on ${column}: ` +
|
|
111
|
+
`objects are only valid 'equals' values on JSON (json/jsonb) columns, ` +
|
|
112
|
+
`where 'equals' is the JSONB containment filter.`);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Object keys in sorted order, mirroring the canonical order used by every
|
|
116
|
+
* cache fingerprint. The SQL-build and cache-hit param-collect paths MUST
|
|
117
|
+
* enumerate object keys in this exact order: fingerprints sort keys, so two
|
|
118
|
+
* where clauses with the same fields in different insertion order share one
|
|
119
|
+
* cache entry — if build/collect iterated insertion order, the cached SQL's
|
|
120
|
+
* `$N` placeholders would bind the wrong values (cross-tenant-leak class).
|
|
121
|
+
* Array order (OR/AND members) is positional and is never sorted.
|
|
122
|
+
*/
|
|
123
|
+
function sortedKeys(obj) {
|
|
124
|
+
return Object.keys(obj).sort();
|
|
125
|
+
}
|
|
126
|
+
/** {@link sortedKeys}, but yielding `[key, value]` pairs. */
|
|
127
|
+
function sortedEntries(obj) {
|
|
128
|
+
return Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
129
|
+
}
|
|
83
130
|
/** Known atomic-update operator keys — used to detect operator objects vs plain JSON values */
|
|
84
131
|
const UPDATE_OPERATOR_KEYS = new Set(['set', 'increment', 'decrement', 'multiply', 'divide']);
|
|
85
132
|
/** Known JSONB operator keys */
|
|
@@ -89,9 +136,11 @@ const JSONB_OPERATOR_KEYS = new Set(['path', 'equals', 'contains', 'hasKey']);
|
|
|
89
136
|
* appear in any other where-filter shape, so the presence of one of these is
|
|
90
137
|
* an unambiguous signal that the user meant a JSON filter. Used by the
|
|
91
138
|
* strict-validation path so that `{ contains: 'foo' }` (which is also a valid
|
|
92
|
-
* `WhereOperator` for LIKE) is not misclassified.
|
|
139
|
+
* `WhereOperator` for LIKE) is not misclassified. Note `equals` is NOT in this
|
|
140
|
+
* set: on non-JSON columns it is a plain equality operator (`WhereOperator`),
|
|
141
|
+
* so it must fall through instead of throwing.
|
|
93
142
|
*/
|
|
94
|
-
const JSONB_UNIQUE_KEYS = new Set(['path', '
|
|
143
|
+
const JSONB_UNIQUE_KEYS = new Set(['path', 'hasKey']);
|
|
95
144
|
/** Check if a value is a JSONB filter object */
|
|
96
145
|
function isJsonFilter(value) {
|
|
97
146
|
if (value === null ||
|
|
@@ -288,6 +337,89 @@ class QueryInterface {
|
|
|
288
337
|
p(index) {
|
|
289
338
|
return this.dialect.paramPlaceholder(index);
|
|
290
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Cast an aggregate expression to an integer/float result type through the
|
|
342
|
+
* active dialect. PostgreSQL keeps the historical postfix cast (`expr::int` /
|
|
343
|
+
* `expr::float`); SQLite (no `::` operator) maps to `CAST(expr AS INTEGER/REAL)`.
|
|
344
|
+
* Falls back to the Postgres postfix cast for dialects without the hook.
|
|
345
|
+
*/
|
|
346
|
+
castAgg(expr, target) {
|
|
347
|
+
return this.dialect.castAggregate?.(expr, target) ?? `${expr}::${target}`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Build the trailing pagination clause for an OUTER SELECT. PostgreSQL/MySQL/
|
|
351
|
+
* SQLite use ` LIMIT <ph>` and/or ` OFFSET <ph>`. SQL Server has no `LIMIT`, so
|
|
352
|
+
* its dialect implements {@link Dialect.buildLimitOffset} to emit
|
|
353
|
+
* `[ORDER BY (SELECT NULL)] OFFSET <off> ROWS [FETCH NEXT <lim> ROWS ONLY]`.
|
|
354
|
+
* Param-push order (limit before offset) is owned by the caller and unchanged —
|
|
355
|
+
* this only varies the SQL text, so PG output stays byte-identical.
|
|
356
|
+
*/
|
|
357
|
+
buildPagination(limitPh, offsetPh, hasOrderBy) {
|
|
358
|
+
if (this.dialect.buildLimitOffset) {
|
|
359
|
+
return this.dialect.buildLimitOffset({ limitPlaceholder: limitPh, offsetPlaceholder: offsetPh, hasOrderBy });
|
|
360
|
+
}
|
|
361
|
+
let s = '';
|
|
362
|
+
if (limitPh !== undefined)
|
|
363
|
+
s += ` LIMIT ${limitPh}`;
|
|
364
|
+
if (offsetPh !== undefined)
|
|
365
|
+
s += ` OFFSET ${offsetPh}`;
|
|
366
|
+
return s;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* The single-row limit appended to findUnique / findFirst-style lookups. ` LIMIT 1`
|
|
370
|
+
* for PG/MySQL/SQLite; SQL Server routes through {@link Dialect.buildLimitOffset}
|
|
371
|
+
* (literal `1`, no params) → ` ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1
|
|
372
|
+
* ROWS ONLY`. No params are pushed, so the collect path is unaffected.
|
|
373
|
+
*/
|
|
374
|
+
limitOneClause() {
|
|
375
|
+
if (this.dialect.buildLimitOffset) {
|
|
376
|
+
return this.dialect.buildLimitOffset({ limitPlaceholder: '1', offsetPlaceholder: undefined, hasOrderBy: false });
|
|
377
|
+
}
|
|
378
|
+
return ' LIMIT 1';
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Validate a LIMIT/OFFSET value as a non-negative integer and return it as an
|
|
382
|
+
* inline SQL literal. Used only on `dialect.inlineLimitOffset` engines (MySQL).
|
|
383
|
+
* The input is always a Turbine-controlled pagination value, never a raw user
|
|
384
|
+
* string — and this guard guarantees the output is `String` of a validated
|
|
385
|
+
* integer, so inlining cannot inject SQL.
|
|
386
|
+
*/
|
|
387
|
+
limitOffsetLiteral(value) {
|
|
388
|
+
const n = Number(value);
|
|
389
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
390
|
+
throw new errors_js_1.ValidationError(`LIMIT/OFFSET must be a non-negative integer, received: ${String(value)}`);
|
|
391
|
+
}
|
|
392
|
+
return String(n);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Resolve a LIMIT/OFFSET value to either an inline literal (no param pushed, on
|
|
396
|
+
* `dialect.inlineLimitOffset` engines) or a bound placeholder (the value is
|
|
397
|
+
* pushed to `params`). Build and collect paths both gate on the same flag so
|
|
398
|
+
* the param order stays mirrored; PG/SQLite/SQL Server keep parameterizing and
|
|
399
|
+
* stay byte-identical.
|
|
400
|
+
*/
|
|
401
|
+
paginationRef(value, params) {
|
|
402
|
+
if (this.dialect.inlineLimitOffset) {
|
|
403
|
+
return this.limitOffsetLiteral(value);
|
|
404
|
+
}
|
|
405
|
+
params.push(Number(value));
|
|
406
|
+
return this.p(params.length);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Build an `IN` / `NOT IN` predicate through the active dialect. PostgreSQL
|
|
410
|
+
* keeps the array-param form (`expr = ANY($n)` / `expr != ALL($n)`); other
|
|
411
|
+
* engines (SQLite) use a length-independent single-placeholder form. Paired
|
|
412
|
+
* with {@link inParam}, which supplies the single bound value.
|
|
413
|
+
*/
|
|
414
|
+
inClause(expr, paramRef, negated) {
|
|
415
|
+
if (this.dialect.buildInClause)
|
|
416
|
+
return this.dialect.buildInClause(expr, paramRef, negated);
|
|
417
|
+
return negated ? `${expr} != ALL(${paramRef})` : `${expr} = ANY(${paramRef})`;
|
|
418
|
+
}
|
|
419
|
+
/** The single bound parameter for an `IN` list (PG: the array; SQLite: a JSON string). */
|
|
420
|
+
inParam(values) {
|
|
421
|
+
return this.dialect.inClauseParam ? this.dialect.inClauseParam(values) : values;
|
|
422
|
+
}
|
|
291
423
|
/**
|
|
292
424
|
* Return cache hit/miss statistics for this QueryInterface instance.
|
|
293
425
|
* Useful for monitoring and benchmarking.
|
|
@@ -388,14 +520,59 @@ class QueryInterface {
|
|
|
388
520
|
clearTimeout(timer);
|
|
389
521
|
}
|
|
390
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Execute a write `DeferredQuery` (create/update/delete/upsert) according to
|
|
525
|
+
* the active dialect's {@link Dialect.resultStrategy}, then apply its
|
|
526
|
+
* transform.
|
|
527
|
+
*
|
|
528
|
+
* - `'returning'` / `'output'`: the statement returns its own affected rows
|
|
529
|
+
* (`RETURNING *` / `OUTPUT INSERTED.*`). Byte-identical to the historical
|
|
530
|
+
* single `queryWithTimeout` + `transform(result)` path — the PostgreSQL
|
|
531
|
+
* route is unchanged.
|
|
532
|
+
* - `'reselect'`: the engine cannot return rows from a write, so the build
|
|
533
|
+
* method attached a {@link DeferredQuery.reselect} plan that runs the
|
|
534
|
+
* write and a follow-up SELECT; the SELECT's rows feed the transform.
|
|
535
|
+
*/
|
|
536
|
+
async executeMutation(deferred, timeout) {
|
|
537
|
+
if (this.dialect.resultStrategy === 'reselect' && deferred.reselect) {
|
|
538
|
+
const exec = (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, preparedName);
|
|
539
|
+
const result = await deferred.reselect(exec);
|
|
540
|
+
return deferred.transform(result);
|
|
541
|
+
}
|
|
542
|
+
const result = await this.queryWithTimeout(deferred.sql, deferred.params, timeout, deferred.preparedName);
|
|
543
|
+
return deferred.transform(result);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
547
|
+
* by a write's `where` clause. Used by the `'reselect'` result strategy to
|
|
548
|
+
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
549
|
+
* builder as reads, so no user value is interpolated.
|
|
550
|
+
*/
|
|
551
|
+
buildReselectByWhere(whereObj) {
|
|
552
|
+
const params = [];
|
|
553
|
+
const clause = this.buildWhereClause(whereObj, params);
|
|
554
|
+
const where = clause ? ` WHERE ${clause}` : '';
|
|
555
|
+
return { sql: `SELECT * FROM ${this.q(this.table)}${where}`, params };
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Best-effort extraction of an auto-generated primary key from a write
|
|
559
|
+
* result for `'reselect'` engines (e.g. mysql2's `insertId`). Returns
|
|
560
|
+
* `undefined` when the driver exposes no such field.
|
|
561
|
+
*/
|
|
562
|
+
mutationInsertId(result) {
|
|
563
|
+
const r = result;
|
|
564
|
+
return r.insertId ?? r.lastID;
|
|
565
|
+
}
|
|
391
566
|
/**
|
|
392
567
|
* Execute a query through the middleware chain.
|
|
393
568
|
* If no middlewares are registered, executes directly.
|
|
394
569
|
*
|
|
395
|
-
* Middleware can inspect and log query parameters,
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
570
|
+
* Middleware can inspect and log query parameters, measure timing, and
|
|
571
|
+
* transform the result returned by `next()`. Note: query SQL is generated
|
|
572
|
+
* BEFORE middleware runs — `params.args` is a read-only snapshot, and
|
|
573
|
+
* mutating it does NOT change the executed SQL. Cross-cutting filters
|
|
574
|
+
* (e.g. soft deletes) belong in the query itself: pass an explicit
|
|
575
|
+
* `where: { deletedAt: null }` or wrap the table accessor in a small helper.
|
|
399
576
|
*/
|
|
400
577
|
async executeWithMiddleware(action, args, executor) {
|
|
401
578
|
this.currentAction = action;
|
|
@@ -434,8 +611,12 @@ class QueryInterface {
|
|
|
434
611
|
const withFp = args.with ? this.withFingerprint(args.with) : '';
|
|
435
612
|
const ck = `fu:${whereFingerprint}|c=${colKey}|w=${withFp}`;
|
|
436
613
|
const params = [];
|
|
437
|
-
// Check if all where values are simple (plain equality, no operators/null/OR)
|
|
438
|
-
|
|
614
|
+
// Check if all where values are simple (plain equality, no operators/null/OR).
|
|
615
|
+
// Keys are sorted to match fingerprintWhere — insertion order here would let
|
|
616
|
+
// permuted where literals share a cache entry with misaligned params.
|
|
617
|
+
const whereKeys = Object.keys(whereObj)
|
|
618
|
+
.filter((k) => whereObj[k] !== undefined)
|
|
619
|
+
.sort();
|
|
439
620
|
const isSimpleWhere = !whereObj.OR &&
|
|
440
621
|
!whereObj.AND &&
|
|
441
622
|
!whereObj.NOT &&
|
|
@@ -452,7 +633,7 @@ class QueryInterface {
|
|
|
452
633
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
453
634
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
454
635
|
void tempParams; // params are positional, SQL is value-invariant
|
|
455
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}
|
|
636
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
456
637
|
});
|
|
457
638
|
// Collect params (same order as build)
|
|
458
639
|
for (const k of whereKeys) {
|
|
@@ -477,7 +658,7 @@ class QueryInterface {
|
|
|
477
658
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
478
659
|
const qt = this.q(this.table);
|
|
479
660
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
480
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}
|
|
661
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
481
662
|
});
|
|
482
663
|
// Collect params
|
|
483
664
|
this.collectWhereParams(whereObj, params);
|
|
@@ -502,7 +683,7 @@ class QueryInterface {
|
|
|
502
683
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
503
684
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
504
685
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
505
|
-
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}
|
|
686
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
|
|
506
687
|
});
|
|
507
688
|
// Collect params in exact build order: where first, then with-clause relations
|
|
508
689
|
this.collectWhereParams(whereObj, params);
|
|
@@ -639,7 +820,8 @@ class QueryInterface {
|
|
|
639
820
|
}
|
|
640
821
|
let sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${freshWhereSql}`;
|
|
641
822
|
if (args?.cursor) {
|
|
642
|
-
|
|
823
|
+
// Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
|
|
824
|
+
const cursorEntries = sortedEntries(args.cursor).filter(([, v]) => v !== undefined);
|
|
643
825
|
if (cursorEntries.length > 0) {
|
|
644
826
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
645
827
|
const col = this.toSqlColumn(k);
|
|
@@ -661,14 +843,18 @@ class QueryInterface {
|
|
|
661
843
|
// vector at the correct position (after cursor params, before LIMIT).
|
|
662
844
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams)}`;
|
|
663
845
|
}
|
|
846
|
+
// Pagination — push params in the same order the collect path mirrors
|
|
847
|
+
// (limit before offset); the SQL TEXT shape is dialect-owned via
|
|
848
|
+
// buildPagination (PG: ` LIMIT $n`/` OFFSET $n`; SQL Server: OFFSET/FETCH).
|
|
849
|
+
let limitPh;
|
|
850
|
+
let offsetPh;
|
|
664
851
|
if (effectiveLimit !== undefined) {
|
|
665
|
-
|
|
666
|
-
sql += ` LIMIT ${this.p(freshParams.length)}`;
|
|
852
|
+
limitPh = this.paginationRef(effectiveLimit, freshParams);
|
|
667
853
|
}
|
|
668
854
|
if (args?.offset !== undefined) {
|
|
669
|
-
|
|
670
|
-
sql += ` OFFSET ${this.p(freshParams.length)}`;
|
|
855
|
+
offsetPh = this.paginationRef(args.offset, freshParams);
|
|
671
856
|
}
|
|
857
|
+
sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
|
|
672
858
|
return sql;
|
|
673
859
|
});
|
|
674
860
|
// Collect params in exact build order:
|
|
@@ -680,9 +866,9 @@ class QueryInterface {
|
|
|
680
866
|
if (args?.with) {
|
|
681
867
|
this.collectWithParams(args.with, params);
|
|
682
868
|
}
|
|
683
|
-
// 3. Cursor params
|
|
869
|
+
// 3. Cursor params — sorted (canonical) order, matching cursorFp and the build path.
|
|
684
870
|
if (args?.cursor) {
|
|
685
|
-
const cursorEntries =
|
|
871
|
+
const cursorEntries = sortedEntries(args.cursor).filter(([, v]) => v !== undefined);
|
|
686
872
|
for (const [, v] of cursorEntries) {
|
|
687
873
|
params.push(v);
|
|
688
874
|
}
|
|
@@ -692,12 +878,13 @@ class QueryInterface {
|
|
|
692
878
|
if (args?.orderBy) {
|
|
693
879
|
this.collectOrderByParams(args.orderBy, params);
|
|
694
880
|
}
|
|
695
|
-
// 5. LIMIT param
|
|
696
|
-
|
|
881
|
+
// 5. LIMIT param — skipped when the dialect inlines pagination (build path
|
|
882
|
+
// mirrors via paginationRef → no placeholder, no param).
|
|
883
|
+
if (effectiveLimit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
697
884
|
params.push(Number(effectiveLimit));
|
|
698
885
|
}
|
|
699
|
-
// 6. OFFSET param
|
|
700
|
-
if (args?.offset !== undefined) {
|
|
886
|
+
// 6. OFFSET param — same inline gate as LIMIT above.
|
|
887
|
+
if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
|
|
701
888
|
params.push(Number(args.offset));
|
|
702
889
|
}
|
|
703
890
|
return {
|
|
@@ -757,34 +944,19 @@ class QueryInterface {
|
|
|
757
944
|
}
|
|
758
945
|
// --- Overflow: fall back to cursor path from scratch ---
|
|
759
946
|
const deferred = this.buildFindMany(args);
|
|
760
|
-
// Acquire a dedicated connection — cursors require a single connection in a
|
|
947
|
+
// Acquire a dedicated connection — cursors require a single connection in a
|
|
948
|
+
// transaction. The dialect owns the streaming SQL (Postgres: BEGIN → DECLARE
|
|
949
|
+
// … NO SCROLL CURSOR FOR → FETCH n → CLOSE → COMMIT, ROLLBACK on error); we
|
|
950
|
+
// just parse + yield the row batches it produces.
|
|
761
951
|
const client = await this.pool.connect();
|
|
762
|
-
const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
763
|
-
const quotedCursor = this.q(cursorName);
|
|
764
952
|
try {
|
|
765
|
-
await client.
|
|
766
|
-
|
|
767
|
-
while (true) {
|
|
768
|
-
const batch = await client.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
|
|
769
|
-
if (batch.rows.length === 0)
|
|
770
|
-
break;
|
|
771
|
-
for (const row of batch.rows) {
|
|
953
|
+
for await (const batch of this.dialect.openStream(client, deferred.sql, deferred.params, batchSize)) {
|
|
954
|
+
for (const row of batch) {
|
|
772
955
|
yield (hasRelations ? this.parseNestedRow(row, this.table) : this.parseRow(row, this.table));
|
|
773
956
|
}
|
|
774
|
-
if (batch.rows.length < batchSize)
|
|
775
|
-
break;
|
|
776
957
|
}
|
|
777
|
-
await client.query(`CLOSE ${quotedCursor}`);
|
|
778
|
-
await client.query('COMMIT');
|
|
779
958
|
}
|
|
780
959
|
catch (err) {
|
|
781
|
-
// Rollback on error (also closes cursor implicitly)
|
|
782
|
-
try {
|
|
783
|
-
await client.query('ROLLBACK');
|
|
784
|
-
}
|
|
785
|
-
catch {
|
|
786
|
-
// Connection may already be broken — ignore rollback error
|
|
787
|
-
}
|
|
788
960
|
// Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
|
|
789
961
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
790
962
|
}
|
|
@@ -886,8 +1058,7 @@ class QueryInterface {
|
|
|
886
1058
|
return this.nestedCreate(args);
|
|
887
1059
|
}
|
|
888
1060
|
const deferred = this.buildCreate(args);
|
|
889
|
-
|
|
890
|
-
return deferred.transform(result);
|
|
1061
|
+
return this.executeMutation(deferred, args.timeout);
|
|
891
1062
|
});
|
|
892
1063
|
}
|
|
893
1064
|
buildCreate(args) {
|
|
@@ -916,6 +1087,33 @@ class QueryInterface {
|
|
|
916
1087
|
return this.parseRow(row, this.table);
|
|
917
1088
|
},
|
|
918
1089
|
tag: `${this.table}.create`,
|
|
1090
|
+
// Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
|
|
1091
|
+
// (provided value, else the driver's generated insert id).
|
|
1092
|
+
reselect: this.makeCreateReselect(sql, params, args.data),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
|
|
1097
|
+
* `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
|
|
1098
|
+
* dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
|
|
1099
|
+
* pays nothing. Not yet wired to a real non-RETURNING engine.
|
|
1100
|
+
*/
|
|
1101
|
+
makeCreateReselect(insertSql, insertParams, data) {
|
|
1102
|
+
if (this.dialect.resultStrategy !== 'reselect')
|
|
1103
|
+
return undefined;
|
|
1104
|
+
return async (exec) => {
|
|
1105
|
+
const writeResult = await exec(insertSql, insertParams);
|
|
1106
|
+
const insertId = this.mutationInsertId(writeResult);
|
|
1107
|
+
const conds = [];
|
|
1108
|
+
const selParams = [];
|
|
1109
|
+
let idx = 1;
|
|
1110
|
+
for (const pk of this.tableMeta.primaryKey) {
|
|
1111
|
+
const field = this.tableMeta.reverseColumnMap[pk] ?? (0, schema_js_1.snakeToCamel)(pk);
|
|
1112
|
+
selParams.push(data[field] ?? data[pk] ?? insertId);
|
|
1113
|
+
conds.push(`${this.q(pk)} = ${this.p(idx++)}`);
|
|
1114
|
+
}
|
|
1115
|
+
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
1116
|
+
return exec(`SELECT * FROM ${this.q(this.table)}${where}`, selParams);
|
|
919
1117
|
};
|
|
920
1118
|
}
|
|
921
1119
|
// -------------------------------------------------------------------------
|
|
@@ -971,8 +1169,7 @@ class QueryInterface {
|
|
|
971
1169
|
return this.nestedUpdate(args);
|
|
972
1170
|
}
|
|
973
1171
|
const deferred = this.buildUpdate(args);
|
|
974
|
-
|
|
975
|
-
return deferred.transform(result);
|
|
1172
|
+
return this.executeMutation(deferred, args.timeout);
|
|
976
1173
|
});
|
|
977
1174
|
}
|
|
978
1175
|
buildUpdate(args) {
|
|
@@ -1000,7 +1197,12 @@ class QueryInterface {
|
|
|
1000
1197
|
whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
|
|
1001
1198
|
}
|
|
1002
1199
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
1003
|
-
|
|
1200
|
+
// Engines that inject their returning shape MID-statement (SQL Server
|
|
1201
|
+
// `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
|
|
1202
|
+
// absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
1203
|
+
return this.dialect.buildUpdateStatement
|
|
1204
|
+
? this.dialect.buildUpdateStatement({ table: this.q(this.table), setClauses, whereSql, returning: '*' })
|
|
1205
|
+
: `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
|
|
1004
1206
|
};
|
|
1005
1207
|
let sql;
|
|
1006
1208
|
let preparedName;
|
|
@@ -1044,6 +1246,26 @@ class QueryInterface {
|
|
|
1044
1246
|
},
|
|
1045
1247
|
tag: `${this.table}.update`,
|
|
1046
1248
|
preparedName,
|
|
1249
|
+
// Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
|
|
1250
|
+
reselect: this.dialect.resultStrategy === 'reselect'
|
|
1251
|
+
? async (exec) => {
|
|
1252
|
+
const writeResult = await exec(sql, params, preparedName);
|
|
1253
|
+
// Optimistic-lock conflict: the version-checked UPDATE matched no
|
|
1254
|
+
// row. The re-fetch below uses `where` WITHOUT the version
|
|
1255
|
+
// predicate, so it would return the stale row and silently mask
|
|
1256
|
+
// the conflict — detect it from affected-rows here instead, to
|
|
1257
|
+
// match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
|
|
1258
|
+
if (lock && (writeResult.rowCount ?? 0) === 0) {
|
|
1259
|
+
throw new errors_js_1.OptimisticLockError({
|
|
1260
|
+
table: this.table,
|
|
1261
|
+
versionField: lock.field,
|
|
1262
|
+
expectedVersion: lock.expected,
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
const sel = this.buildReselectByWhere(whereObj);
|
|
1266
|
+
return exec(sel.sql, sel.params);
|
|
1267
|
+
}
|
|
1268
|
+
: undefined,
|
|
1047
1269
|
};
|
|
1048
1270
|
}
|
|
1049
1271
|
// -------------------------------------------------------------------------
|
|
@@ -1075,19 +1297,19 @@ class QueryInterface {
|
|
|
1075
1297
|
async runInImplicitTx(fn) {
|
|
1076
1298
|
const client = await this.pool.connect();
|
|
1077
1299
|
try {
|
|
1078
|
-
await client.query(
|
|
1300
|
+
await client.query(this.dialect.beginStatement());
|
|
1079
1301
|
const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('../client.js')));
|
|
1080
1302
|
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
1081
1303
|
const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
|
|
1082
1304
|
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
1083
1305
|
const ctx = { schema: this.schema, tx: tx };
|
|
1084
1306
|
const result = await fn(ctx);
|
|
1085
|
-
await client.query(
|
|
1307
|
+
await client.query(this.dialect.commitStatement());
|
|
1086
1308
|
return result;
|
|
1087
1309
|
}
|
|
1088
1310
|
catch (err) {
|
|
1089
1311
|
try {
|
|
1090
|
-
await client.query(
|
|
1312
|
+
await client.query(this.dialect.rollbackStatement());
|
|
1091
1313
|
}
|
|
1092
1314
|
catch {
|
|
1093
1315
|
// Best-effort rollback — connection may have died.
|
|
@@ -1120,8 +1342,7 @@ class QueryInterface {
|
|
|
1120
1342
|
async delete(args) {
|
|
1121
1343
|
return this.executeWithMiddleware('delete', args, async () => {
|
|
1122
1344
|
const deferred = this.buildDelete(args);
|
|
1123
|
-
|
|
1124
|
-
return deferred.transform(result);
|
|
1345
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1125
1346
|
});
|
|
1126
1347
|
}
|
|
1127
1348
|
buildDelete(args) {
|
|
@@ -1136,7 +1357,11 @@ class QueryInterface {
|
|
|
1136
1357
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
1137
1358
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1138
1359
|
this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
|
|
1139
|
-
|
|
1360
|
+
// SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
|
|
1361
|
+
// absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
1362
|
+
return this.dialect.buildDeleteStatement
|
|
1363
|
+
? this.dialect.buildDeleteStatement({ table: this.q(this.table), whereSql, returning: '*' })
|
|
1364
|
+
: `DELETE FROM ${this.q(this.table)}${whereSql}${this.dialect.buildReturningClause('*')}`;
|
|
1140
1365
|
});
|
|
1141
1366
|
// On cache hit, still validate the predicate
|
|
1142
1367
|
if (whereFp === '') {
|
|
@@ -1159,6 +1384,16 @@ class QueryInterface {
|
|
|
1159
1384
|
},
|
|
1160
1385
|
tag: `${this.table}.delete`,
|
|
1161
1386
|
preparedName: entry.name,
|
|
1387
|
+
// Non-RETURNING engines: the row is gone after DELETE, so pre-SELECT it
|
|
1388
|
+
// by the same where, then run the DELETE, returning the captured row.
|
|
1389
|
+
reselect: this.dialect.resultStrategy === 'reselect'
|
|
1390
|
+
? async (exec) => {
|
|
1391
|
+
const sel = this.buildReselectByWhere(whereObj);
|
|
1392
|
+
const pre = await exec(sel.sql, sel.params);
|
|
1393
|
+
await exec(entry.sql, params, entry.name);
|
|
1394
|
+
return pre;
|
|
1395
|
+
}
|
|
1396
|
+
: undefined,
|
|
1162
1397
|
};
|
|
1163
1398
|
}
|
|
1164
1399
|
// -------------------------------------------------------------------------
|
|
@@ -1167,8 +1402,7 @@ class QueryInterface {
|
|
|
1167
1402
|
async upsert(args) {
|
|
1168
1403
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
1169
1404
|
const deferred = this.buildUpsert(args);
|
|
1170
|
-
|
|
1171
|
-
return deferred.transform(result);
|
|
1405
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1172
1406
|
});
|
|
1173
1407
|
}
|
|
1174
1408
|
buildUpsert(args) {
|
|
@@ -1214,6 +1448,14 @@ class QueryInterface {
|
|
|
1214
1448
|
return this.parseRow(row, this.table);
|
|
1215
1449
|
},
|
|
1216
1450
|
tag: `${this.table}.upsert`,
|
|
1451
|
+
// Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
|
|
1452
|
+
reselect: this.dialect.resultStrategy === 'reselect'
|
|
1453
|
+
? async (exec) => {
|
|
1454
|
+
await exec(sql, params);
|
|
1455
|
+
const sel = this.buildReselectByWhere(args.where);
|
|
1456
|
+
return exec(sel.sql, sel.params);
|
|
1457
|
+
}
|
|
1458
|
+
: undefined,
|
|
1217
1459
|
};
|
|
1218
1460
|
}
|
|
1219
1461
|
// -------------------------------------------------------------------------
|
|
@@ -1308,7 +1550,7 @@ class QueryInterface {
|
|
|
1308
1550
|
const freshParams = [];
|
|
1309
1551
|
const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
|
|
1310
1552
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1311
|
-
return `SELECT COUNT(*)
|
|
1553
|
+
return `SELECT ${this.castAgg('COUNT(*)', 'int')} AS count FROM ${this.q(this.table)}${whereSql}`;
|
|
1312
1554
|
});
|
|
1313
1555
|
if (args?.where) {
|
|
1314
1556
|
this.collectWhereParams(whereObj, params);
|
|
@@ -1348,7 +1590,7 @@ class QueryInterface {
|
|
|
1348
1590
|
// _count
|
|
1349
1591
|
if (args._count === true || args._count === undefined) {
|
|
1350
1592
|
// default: always include count
|
|
1351
|
-
selectExprs.push('COUNT(*)
|
|
1593
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1352
1594
|
}
|
|
1353
1595
|
// _sum
|
|
1354
1596
|
if (args._sum) {
|
|
@@ -1364,7 +1606,7 @@ class QueryInterface {
|
|
|
1364
1606
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1365
1607
|
if (enabled) {
|
|
1366
1608
|
const col = this.toColumn(field);
|
|
1367
|
-
selectExprs.push(`AVG(${this.q(col)})
|
|
1609
|
+
selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
|
|
1368
1610
|
}
|
|
1369
1611
|
}
|
|
1370
1612
|
}
|
|
@@ -1565,12 +1807,12 @@ class QueryInterface {
|
|
|
1565
1807
|
clauses.push(`${expr} <= ${this.p(params.length)}`);
|
|
1566
1808
|
}
|
|
1567
1809
|
if (op.in !== undefined) {
|
|
1568
|
-
params.push(op.in);
|
|
1569
|
-
clauses.push(
|
|
1810
|
+
params.push(this.inParam(op.in));
|
|
1811
|
+
clauses.push(this.inClause(expr, this.p(params.length), false));
|
|
1570
1812
|
}
|
|
1571
1813
|
if (op.notIn !== undefined) {
|
|
1572
|
-
params.push(op.notIn);
|
|
1573
|
-
clauses.push(
|
|
1814
|
+
params.push(this.inParam(op.notIn));
|
|
1815
|
+
clauses.push(this.inClause(expr, this.p(params.length), true));
|
|
1574
1816
|
}
|
|
1575
1817
|
return clauses;
|
|
1576
1818
|
}
|
|
@@ -1608,13 +1850,13 @@ class QueryInterface {
|
|
|
1608
1850
|
const selectExprs = [];
|
|
1609
1851
|
// _count
|
|
1610
1852
|
if (args._count === true) {
|
|
1611
|
-
selectExprs.push('COUNT(*)
|
|
1853
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1612
1854
|
}
|
|
1613
1855
|
else if (args._count && typeof args._count === 'object') {
|
|
1614
1856
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
1615
1857
|
if (enabled) {
|
|
1616
1858
|
const col = this.toColumn(field);
|
|
1617
|
-
selectExprs.push(`COUNT(${this.q(col)})
|
|
1859
|
+
selectExprs.push(`${this.castAgg(`COUNT(${this.q(col)})`, 'int')} AS ${this.q(`_count_${col}`)}`);
|
|
1618
1860
|
}
|
|
1619
1861
|
}
|
|
1620
1862
|
}
|
|
@@ -1632,7 +1874,7 @@ class QueryInterface {
|
|
|
1632
1874
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1633
1875
|
if (enabled) {
|
|
1634
1876
|
const col = this.toColumn(field);
|
|
1635
|
-
selectExprs.push(`AVG(${this.q(col)})
|
|
1877
|
+
selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
|
|
1636
1878
|
}
|
|
1637
1879
|
}
|
|
1638
1880
|
}
|
|
@@ -1655,7 +1897,7 @@ class QueryInterface {
|
|
|
1655
1897
|
}
|
|
1656
1898
|
}
|
|
1657
1899
|
if (selectExprs.length === 0) {
|
|
1658
|
-
selectExprs.push('COUNT(*)
|
|
1900
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1659
1901
|
}
|
|
1660
1902
|
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
|
|
1661
1903
|
return {
|
|
@@ -1922,12 +2164,7 @@ class QueryInterface {
|
|
|
1922
2164
|
}
|
|
1923
2165
|
// Operator objects
|
|
1924
2166
|
if (isWhereOperator(value)) {
|
|
1925
|
-
|
|
1926
|
-
.filter((k) => k !== 'mode')
|
|
1927
|
-
.sort();
|
|
1928
|
-
const mode = value.mode;
|
|
1929
|
-
const modeStr = mode === 'insensitive' ? ':i' : '';
|
|
1930
|
-
parts.push(`${key}:op(${opKeys.join(',')}${modeStr})`);
|
|
2167
|
+
parts.push(`${key}:${fingerprintOperatorShape(value)}`);
|
|
1931
2168
|
continue;
|
|
1932
2169
|
}
|
|
1933
2170
|
// Vector distance filter — metric (operator) and present comparators
|
|
@@ -2000,12 +2237,7 @@ class QueryInterface {
|
|
|
2000
2237
|
parts.push(`${key}:null`);
|
|
2001
2238
|
}
|
|
2002
2239
|
else if (isWhereOperator(value)) {
|
|
2003
|
-
|
|
2004
|
-
.filter((k) => k !== 'mode')
|
|
2005
|
-
.sort();
|
|
2006
|
-
const mode = value.mode;
|
|
2007
|
-
const modeStr = mode === 'insensitive' ? ':i' : '';
|
|
2008
|
-
parts.push(`${key}:op(${opKeys.join(',')}${modeStr})`);
|
|
2240
|
+
parts.push(`${key}:${fingerprintOperatorShape(value)}`);
|
|
2009
2241
|
}
|
|
2010
2242
|
else if (isUnmatchedPlainObject(value)) {
|
|
2011
2243
|
parts.push(`${key}:obj(${Object.keys(value)
|
|
@@ -2026,7 +2258,8 @@ class QueryInterface {
|
|
|
2026
2258
|
* @internal Exposed as package-private for testing.
|
|
2027
2259
|
*/
|
|
2028
2260
|
collectWhereParams(where, params) {
|
|
2029
|
-
|
|
2261
|
+
// Sorted (canonical) order — MUST match fingerprintWhere and buildWhereClause.
|
|
2262
|
+
const keys = sortedKeys(where);
|
|
2030
2263
|
for (const key of keys) {
|
|
2031
2264
|
const value = where[key];
|
|
2032
2265
|
if (value === undefined)
|
|
@@ -2111,7 +2344,7 @@ class QueryInterface {
|
|
|
2111
2344
|
}
|
|
2112
2345
|
// Operator objects
|
|
2113
2346
|
if (isWhereOperator(value)) {
|
|
2114
|
-
this.collectOperatorParams(value, params);
|
|
2347
|
+
this.collectOperatorParams(rawColumn, value, params);
|
|
2115
2348
|
continue;
|
|
2116
2349
|
}
|
|
2117
2350
|
// Plain equality — same strict validation as the build path, so a
|
|
@@ -2125,22 +2358,28 @@ class QueryInterface {
|
|
|
2125
2358
|
const meta = this.schema.tables[targetTable];
|
|
2126
2359
|
if (!meta)
|
|
2127
2360
|
return;
|
|
2128
|
-
|
|
2361
|
+
// Sorted (canonical) order — MUST match fingerprintRelFilter and buildSubWhereForRelation.
|
|
2362
|
+
for (const field of sortedKeys(subWhere)) {
|
|
2363
|
+
const value = subWhere[field];
|
|
2129
2364
|
if (value === undefined)
|
|
2130
2365
|
continue;
|
|
2131
2366
|
if (value === null)
|
|
2132
2367
|
continue;
|
|
2368
|
+
const col = meta.columnMap[field] ?? (0, schema_js_1.camelToSnake)(field);
|
|
2133
2369
|
if (isWhereOperator(value)) {
|
|
2134
|
-
this.collectOperatorParams(value, params);
|
|
2370
|
+
this.collectOperatorParams(col, value, params);
|
|
2135
2371
|
continue;
|
|
2136
2372
|
}
|
|
2137
|
-
const col = meta.columnMap[field] ?? (0, schema_js_1.camelToSnake)(field);
|
|
2138
2373
|
this.assertBindableEqualityValue(col, value, this.pgTypeForColumn(meta, col), targetTable);
|
|
2139
2374
|
params.push(value);
|
|
2140
2375
|
}
|
|
2141
2376
|
}
|
|
2142
2377
|
/** Collect params from operator clauses. Mirrors buildOperatorClauses. */
|
|
2143
|
-
collectOperatorParams(op, params) {
|
|
2378
|
+
collectOperatorParams(column, op, params) {
|
|
2379
|
+
if (op.equals !== undefined && op.equals !== null) {
|
|
2380
|
+
assertBindableEqualsOperand(op.equals, `"${column}"`);
|
|
2381
|
+
params.push(op.equals);
|
|
2382
|
+
}
|
|
2144
2383
|
if (op.gt !== undefined)
|
|
2145
2384
|
params.push(op.gt);
|
|
2146
2385
|
if (op.gte !== undefined)
|
|
@@ -2152,9 +2391,9 @@ class QueryInterface {
|
|
|
2152
2391
|
if (op.not !== undefined && op.not !== null)
|
|
2153
2392
|
params.push(op.not);
|
|
2154
2393
|
if (op.in !== undefined)
|
|
2155
|
-
params.push(op.in);
|
|
2394
|
+
params.push(this.inParam(op.in));
|
|
2156
2395
|
if (op.notIn !== undefined)
|
|
2157
|
-
params.push(op.notIn);
|
|
2396
|
+
params.push(this.inParam(op.notIn));
|
|
2158
2397
|
if (op.contains !== undefined)
|
|
2159
2398
|
params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
|
|
2160
2399
|
if (op.startsWith !== undefined)
|
|
@@ -2296,7 +2535,7 @@ class QueryInterface {
|
|
|
2296
2535
|
const meta = this.schema.tables[table ?? this.table];
|
|
2297
2536
|
if (!meta)
|
|
2298
2537
|
return;
|
|
2299
|
-
for (const [relName, relSpec] of
|
|
2538
|
+
for (const [relName, relSpec] of sortedEntries(withClause)) {
|
|
2300
2539
|
const relDef = meta.relations[relName];
|
|
2301
2540
|
if (!relDef)
|
|
2302
2541
|
continue;
|
|
@@ -2319,11 +2558,11 @@ class QueryInterface {
|
|
|
2319
2558
|
if (spec.where) {
|
|
2320
2559
|
this.collectAliasWhereParams(targetTable, targetMeta, spec.where, params);
|
|
2321
2560
|
}
|
|
2322
|
-
if (spec.limit !== undefined) {
|
|
2561
|
+
if (spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
2323
2562
|
params.push(Number(spec.limit));
|
|
2324
2563
|
}
|
|
2325
2564
|
if (spec.with) {
|
|
2326
|
-
for (const [nestedRelName, nestedSpec] of
|
|
2565
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
2327
2566
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
2328
2567
|
if (!nestedRelDef)
|
|
2329
2568
|
continue;
|
|
@@ -2337,7 +2576,7 @@ class QueryInterface {
|
|
|
2337
2576
|
const willWrap = relDef.type === 'hasMany' && (spec.limit !== undefined || hasOrder);
|
|
2338
2577
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
2339
2578
|
if (!willWrap && spec.with) {
|
|
2340
|
-
for (const [nestedRelName, nestedSpec] of
|
|
2579
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
2341
2580
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
2342
2581
|
if (!nestedRelDef)
|
|
2343
2582
|
continue;
|
|
@@ -2352,12 +2591,12 @@ class QueryInterface {
|
|
|
2352
2591
|
// buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
|
|
2353
2592
|
// pushing one here would orphan a param and desync the collect path.
|
|
2354
2593
|
// `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
|
|
2355
|
-
if (relDef.type === 'hasMany' && spec.limit !== undefined) {
|
|
2594
|
+
if (relDef.type === 'hasMany' && spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
2356
2595
|
params.push(Number(spec.limit));
|
|
2357
2596
|
}
|
|
2358
2597
|
// Wrapped path: nested relations AFTER where/limit (inside inner subquery)
|
|
2359
2598
|
if (willWrap && spec.with) {
|
|
2360
|
-
for (const [nestedRelName, nestedSpec] of
|
|
2599
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
2361
2600
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
2362
2601
|
if (!nestedRelDef)
|
|
2363
2602
|
continue;
|
|
@@ -2439,7 +2678,8 @@ class QueryInterface {
|
|
|
2439
2678
|
* Supports: equality, operators, NULL, OR, AND, NOT, relation filters (some/every/none).
|
|
2440
2679
|
*/
|
|
2441
2680
|
buildWhereClause(where, params) {
|
|
2442
|
-
|
|
2681
|
+
// Sorted (canonical) order — MUST match fingerprintWhere and collectWhereParams.
|
|
2682
|
+
const keys = sortedKeys(where);
|
|
2443
2683
|
if (keys.length === 0)
|
|
2444
2684
|
return null;
|
|
2445
2685
|
const andClauses = [];
|
|
@@ -2646,7 +2886,9 @@ class QueryInterface {
|
|
|
2646
2886
|
return null;
|
|
2647
2887
|
const qt = this.q(targetTable);
|
|
2648
2888
|
const conditions = [];
|
|
2649
|
-
|
|
2889
|
+
// Sorted (canonical) order — MUST match fingerprintRelFilter and collectRelFilterParams.
|
|
2890
|
+
for (const field of sortedKeys(subWhere)) {
|
|
2891
|
+
const value = subWhere[field];
|
|
2650
2892
|
if (value === undefined)
|
|
2651
2893
|
continue;
|
|
2652
2894
|
const col = meta.columnMap[field] ?? (0, schema_js_1.camelToSnake)(field);
|
|
@@ -2711,7 +2953,9 @@ class QueryInterface {
|
|
|
2711
2953
|
*/
|
|
2712
2954
|
buildAliasWhere(targetTable, targetMeta, alias, where, params) {
|
|
2713
2955
|
const clauses = [];
|
|
2714
|
-
|
|
2956
|
+
// Sorted (canonical) order — MUST match fingerprintAliasWhere and collectAliasWhereParams.
|
|
2957
|
+
for (const key of sortedKeys(where)) {
|
|
2958
|
+
const value = where[key];
|
|
2715
2959
|
if (value === undefined)
|
|
2716
2960
|
continue;
|
|
2717
2961
|
if (key === 'OR' || key === 'AND') {
|
|
@@ -2753,7 +2997,9 @@ class QueryInterface {
|
|
|
2753
2997
|
}
|
|
2754
2998
|
/** Mirrors {@link buildAliasWhere} param-push order for the cache-hit collect path. */
|
|
2755
2999
|
collectAliasWhereParams(targetTable, targetMeta, where, params) {
|
|
2756
|
-
|
|
3000
|
+
// Sorted (canonical) order — MUST match fingerprintAliasWhere and buildAliasWhere.
|
|
3001
|
+
for (const key of sortedKeys(where)) {
|
|
3002
|
+
const value = where[key];
|
|
2757
3003
|
if (value === undefined)
|
|
2758
3004
|
continue;
|
|
2759
3005
|
if (key === 'OR' || key === 'AND') {
|
|
@@ -2771,11 +3017,11 @@ class QueryInterface {
|
|
|
2771
3017
|
}
|
|
2772
3018
|
if (value === null)
|
|
2773
3019
|
continue;
|
|
3020
|
+
const col = targetMeta.columnMap[key] ?? (0, schema_js_1.camelToSnake)(key);
|
|
2774
3021
|
if (isWhereOperator(value)) {
|
|
2775
|
-
this.collectOperatorParams(value, params);
|
|
3022
|
+
this.collectOperatorParams(col, value, params);
|
|
2776
3023
|
continue;
|
|
2777
3024
|
}
|
|
2778
|
-
const col = targetMeta.columnMap[key] ?? (0, schema_js_1.camelToSnake)(key);
|
|
2779
3025
|
this.assertBindableEqualityValue(col, value, this.pgTypeForColumn(targetMeta, col), targetTable);
|
|
2780
3026
|
params.push(value);
|
|
2781
3027
|
}
|
|
@@ -2809,11 +3055,7 @@ class QueryInterface {
|
|
|
2809
3055
|
continue;
|
|
2810
3056
|
}
|
|
2811
3057
|
if (isWhereOperator(value)) {
|
|
2812
|
-
|
|
2813
|
-
.filter((k) => k !== 'mode')
|
|
2814
|
-
.sort();
|
|
2815
|
-
const mode = value.mode;
|
|
2816
|
-
parts.push(`${key}:op(${opKeys.join(',')}${mode === 'insensitive' ? ':i' : ''})`);
|
|
3058
|
+
parts.push(`${key}:${fingerprintOperatorShape(value)}`);
|
|
2817
3059
|
continue;
|
|
2818
3060
|
}
|
|
2819
3061
|
if (isUnmatchedPlainObject(value)) {
|
|
@@ -2832,6 +3074,16 @@ class QueryInterface {
|
|
|
2832
3074
|
*/
|
|
2833
3075
|
buildOperatorClauses(column, op, params) {
|
|
2834
3076
|
const clauses = [];
|
|
3077
|
+
if (op.equals !== undefined) {
|
|
3078
|
+
if (op.equals === null) {
|
|
3079
|
+
clauses.push(`${column} IS NULL`);
|
|
3080
|
+
}
|
|
3081
|
+
else {
|
|
3082
|
+
assertBindableEqualsOperand(op.equals, column);
|
|
3083
|
+
params.push(op.equals);
|
|
3084
|
+
clauses.push(`${column} = ${this.p(params.length)}`);
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
2835
3087
|
if (op.gt !== undefined) {
|
|
2836
3088
|
params.push(op.gt);
|
|
2837
3089
|
clauses.push(`${column} > ${this.p(params.length)}`);
|
|
@@ -2858,12 +3110,12 @@ class QueryInterface {
|
|
|
2858
3110
|
}
|
|
2859
3111
|
}
|
|
2860
3112
|
if (op.in !== undefined) {
|
|
2861
|
-
params.push(op.in);
|
|
2862
|
-
clauses.push(
|
|
3113
|
+
params.push(this.inParam(op.in));
|
|
3114
|
+
clauses.push(this.inClause(column, this.p(params.length), false));
|
|
2863
3115
|
}
|
|
2864
3116
|
if (op.notIn !== undefined) {
|
|
2865
|
-
params.push(op.notIn);
|
|
2866
|
-
clauses.push(
|
|
3117
|
+
params.push(this.inParam(op.notIn));
|
|
3118
|
+
clauses.push(this.inClause(column, this.p(params.length), true));
|
|
2867
3119
|
}
|
|
2868
3120
|
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
2869
3121
|
if (op.contains !== undefined) {
|
|
@@ -2934,6 +3186,9 @@ class QueryInterface {
|
|
|
2934
3186
|
* non-vector column — a user-supplied string can never become a SQL operator.
|
|
2935
3187
|
*/
|
|
2936
3188
|
vectorOperator(field, rawColumn, metric) {
|
|
3189
|
+
if (!this.dialect.supportsVector) {
|
|
3190
|
+
throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', this.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
|
|
3191
|
+
}
|
|
2937
3192
|
const colType = this.getColumnPgType(rawColumn);
|
|
2938
3193
|
if (colType !== 'vector') {
|
|
2939
3194
|
throw new errors_js_1.ValidationError(`[turbine] Column "${field}" on table "${this.table}" is not a vector column ` +
|
|
@@ -2954,6 +3209,9 @@ class QueryInterface {
|
|
|
2954
3209
|
* placeholder string.
|
|
2955
3210
|
*/
|
|
2956
3211
|
pushVectorParam(field, _rawColumn, to, params) {
|
|
3212
|
+
if (!this.dialect.supportsVector) {
|
|
3213
|
+
throw new errors_js_1.UnsupportedFeatureError('pgvector distance operations', this.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
|
|
3214
|
+
}
|
|
2957
3215
|
if (!Array.isArray(to) || to.length === 0) {
|
|
2958
3216
|
throw new errors_js_1.ValidationError(`[turbine] Vector distance on "${field}" requires a non-empty array of numbers for "to".`);
|
|
2959
3217
|
}
|
|
@@ -3130,7 +3388,7 @@ class QueryInterface {
|
|
|
3130
3388
|
const baseCols = cols.map((col) => `${qtbl}.${this.q(col)}`).join(', ');
|
|
3131
3389
|
const relationSelects = [];
|
|
3132
3390
|
const aliasCounter = { n: 0 };
|
|
3133
|
-
for (const [relName, relSpec] of
|
|
3391
|
+
for (const [relName, relSpec] of sortedEntries(withClause)) {
|
|
3134
3392
|
const relDef = meta.relations[relName];
|
|
3135
3393
|
if (!relDef) {
|
|
3136
3394
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" on table "${table}". ` +
|
|
@@ -3264,6 +3522,32 @@ class QueryInterface {
|
|
|
3264
3522
|
.map(([k]) => targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k)));
|
|
3265
3523
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
3266
3524
|
}
|
|
3525
|
+
// Engine override seam (additive): a dialect whose JSON-aggregation shape does
|
|
3526
|
+
// not map onto buildJsonObject/buildJsonArrayAgg (SQL Server FOR JSON PATH) owns
|
|
3527
|
+
// the WHOLE subquery. Absent for PG/MySQL/SQLite → the native path below runs
|
|
3528
|
+
// unchanged (byte-identical output, all their tests stay green). The override
|
|
3529
|
+
// pushes params per the documented RelationSubqueryContext ordering contract,
|
|
3530
|
+
// which mirrors collectRelationSubqueryParams so the SQL cache / pipeline stay
|
|
3531
|
+
// in sync.
|
|
3532
|
+
if (this.dialect.buildRelationSubquery) {
|
|
3533
|
+
return this.dialect.buildRelationSubquery({
|
|
3534
|
+
relDef,
|
|
3535
|
+
spec,
|
|
3536
|
+
params,
|
|
3537
|
+
parentRef,
|
|
3538
|
+
alias,
|
|
3539
|
+
targetTable,
|
|
3540
|
+
targetMeta,
|
|
3541
|
+
targetColumns,
|
|
3542
|
+
depth: currentDepth,
|
|
3543
|
+
path: currentPath,
|
|
3544
|
+
quote: (name) => this.q(name),
|
|
3545
|
+
buildWhere: (whereAlias) => (spec !== true && spec.where
|
|
3546
|
+
? this.buildAliasWhere(targetTable, targetMeta, whereAlias, spec.where, params)
|
|
3547
|
+
: '') ?? '',
|
|
3548
|
+
recurse: (nRelDef, nSpec, nParent, nDepth, nPath) => this.buildRelationSubquery(nRelDef, nSpec, params, nParent, aliasCounter, nDepth, nPath),
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3267
3551
|
// Build JSON object pairs for resolved columns
|
|
3268
3552
|
const jsonPairs = targetColumns.map((col) => [
|
|
3269
3553
|
targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
@@ -3285,7 +3569,7 @@ class QueryInterface {
|
|
|
3285
3569
|
}
|
|
3286
3570
|
// Nested relations — only in the non-wrapped path (wrapped path builds them separately)
|
|
3287
3571
|
if (!willWrap && spec !== true && spec.with) {
|
|
3288
|
-
for (const [nestedRelName, nestedSpec] of
|
|
3572
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
3289
3573
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
3290
3574
|
if (!nestedRelDef) {
|
|
3291
3575
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
@@ -3295,7 +3579,7 @@ class QueryInterface {
|
|
|
3295
3579
|
const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
3296
3580
|
// Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
|
|
3297
3581
|
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
3298
|
-
jsonPairs.push([nestedRelName,
|
|
3582
|
+
jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSubquery, fallback)]);
|
|
3299
3583
|
}
|
|
3300
3584
|
}
|
|
3301
3585
|
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|
|
@@ -3343,8 +3627,7 @@ class QueryInterface {
|
|
|
3343
3627
|
// `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
|
|
3344
3628
|
let limitClause = '';
|
|
3345
3629
|
if (relDef.type === 'hasMany' && spec !== true && spec.limit !== undefined) {
|
|
3346
|
-
|
|
3347
|
-
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
3630
|
+
limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
|
|
3348
3631
|
}
|
|
3349
3632
|
if (relDef.type === 'hasMany') {
|
|
3350
3633
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -3361,7 +3644,7 @@ class QueryInterface {
|
|
|
3361
3644
|
]);
|
|
3362
3645
|
// Build nested relation subqueries referencing innerAlias
|
|
3363
3646
|
if (spec !== true && spec.with) {
|
|
3364
|
-
for (const [nestedRelName, nestedSpec] of
|
|
3647
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
3365
3648
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
3366
3649
|
if (!nestedRelDef) {
|
|
3367
3650
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
@@ -3369,13 +3652,17 @@ class QueryInterface {
|
|
|
3369
3652
|
}
|
|
3370
3653
|
const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
3371
3654
|
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
3372
|
-
innerJsonPairs.push([nestedRelName,
|
|
3655
|
+
innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3373
3656
|
}
|
|
3374
3657
|
}
|
|
3375
3658
|
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
3376
3659
|
return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
|
|
3377
3660
|
}
|
|
3378
|
-
|
|
3661
|
+
// Inline ORDER BY only when the dialect's array-agg supports it (PG). For
|
|
3662
|
+
// hasMany this path is reached only when there is no orderClause, so the
|
|
3663
|
+
// argument is `undefined` either way — keeping PG output byte-identical.
|
|
3664
|
+
const inlineOrder = this.dialect.aggSupportsInlineOrderBy ? orderClause.trim() || undefined : undefined;
|
|
3665
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, inlineOrder)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
3379
3666
|
}
|
|
3380
3667
|
// belongsTo / hasOne — return single object
|
|
3381
3668
|
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
@@ -3463,8 +3750,7 @@ class QueryInterface {
|
|
|
3463
3750
|
// LIMIT — `limit: 0` is honored (LIMIT 0 → empty array)
|
|
3464
3751
|
let limitClause = '';
|
|
3465
3752
|
if (spec !== true && spec.limit !== undefined) {
|
|
3466
|
-
|
|
3467
|
-
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
3753
|
+
limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
|
|
3468
3754
|
}
|
|
3469
3755
|
const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
|
|
3470
3756
|
// When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
|
|
@@ -3479,7 +3765,7 @@ class QueryInterface {
|
|
|
3479
3765
|
]);
|
|
3480
3766
|
// Nested relations reference the inner alias.
|
|
3481
3767
|
if (spec !== true && spec.with) {
|
|
3482
|
-
for (const [nestedRelName, nestedSpec] of
|
|
3768
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
3483
3769
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
3484
3770
|
if (!nestedRelDef) {
|
|
3485
3771
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
@@ -3489,7 +3775,7 @@ class QueryInterface {
|
|
|
3489
3775
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
3490
3776
|
? this.dialect.nullJsonLiteral
|
|
3491
3777
|
: this.dialect.emptyJsonArrayLiteral;
|
|
3492
|
-
innerJsonPairs.push([nestedRelName,
|
|
3778
|
+
innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3493
3779
|
}
|
|
3494
3780
|
}
|
|
3495
3781
|
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
@@ -3502,7 +3788,7 @@ class QueryInterface {
|
|
|
3502
3788
|
`${talias}.${this.q(col)}`,
|
|
3503
3789
|
]);
|
|
3504
3790
|
if (spec !== true && spec.with) {
|
|
3505
|
-
for (const [nestedRelName, nestedSpec] of
|
|
3791
|
+
for (const [nestedRelName, nestedSpec] of sortedEntries(spec.with)) {
|
|
3506
3792
|
const nestedRelDef = targetMeta.relations[nestedRelName];
|
|
3507
3793
|
if (!nestedRelDef) {
|
|
3508
3794
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${nestedRelName}" on table "${targetTable}". ` +
|
|
@@ -3512,7 +3798,7 @@ class QueryInterface {
|
|
|
3512
3798
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
3513
3799
|
? this.dialect.nullJsonLiteral
|
|
3514
3800
|
: this.dialect.emptyJsonArrayLiteral;
|
|
3515
|
-
jsonPairs.push([nestedRelName,
|
|
3801
|
+
jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3516
3802
|
}
|
|
3517
3803
|
}
|
|
3518
3804
|
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|