turbine-orm 0.19.2 → 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 +82 -18
- package/dist/adapters/cockroachdb.js +4 -9
- package/dist/cjs/adapters/cockroachdb.js +4 -9
- package/dist/cjs/cli/index.js +6 -3
- package/dist/cjs/cli/observe-ui.js +12 -2
- package/dist/cjs/cli/observe.js +6 -7
- package/dist/cjs/cli/studio.js +6 -7
- package/dist/cjs/client.js +33 -20
- 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 +293 -73
- package/dist/cjs/sqlite.js +849 -0
- package/dist/cjs/typed-sql.js +9 -7
- package/dist/cli/index.js +6 -3
- 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.js +7 -8
- package/dist/client.d.ts +23 -1
- package/dist/client.js +34 -21
- 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 +3 -3
- 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 +97 -0
- package/dist/query/builder.js +294 -74
- package/dist/query/index.d.ts +1 -1
- 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 +30 -1
package/dist/query/builder.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* metadata — nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
13
|
import { postgresDialect } from '../dialect.js';
|
|
14
|
-
import { CircularRelationError, NotFoundError, OptimisticLockError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
|
|
14
|
+
import { CircularRelationError, NotFoundError, OptimisticLockError, RelationError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
|
|
15
15
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
|
|
16
16
|
import { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
17
17
|
import { escapeLike, LRUCache, OPERATOR_KEYS, sqlToPreparedName } from './utils.js';
|
|
@@ -301,6 +301,89 @@ export class QueryInterface {
|
|
|
301
301
|
p(index) {
|
|
302
302
|
return this.dialect.paramPlaceholder(index);
|
|
303
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Cast an aggregate expression to an integer/float result type through the
|
|
306
|
+
* active dialect. PostgreSQL keeps the historical postfix cast (`expr::int` /
|
|
307
|
+
* `expr::float`); SQLite (no `::` operator) maps to `CAST(expr AS INTEGER/REAL)`.
|
|
308
|
+
* Falls back to the Postgres postfix cast for dialects without the hook.
|
|
309
|
+
*/
|
|
310
|
+
castAgg(expr, target) {
|
|
311
|
+
return this.dialect.castAggregate?.(expr, target) ?? `${expr}::${target}`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Build the trailing pagination clause for an OUTER SELECT. PostgreSQL/MySQL/
|
|
315
|
+
* SQLite use ` LIMIT <ph>` and/or ` OFFSET <ph>`. SQL Server has no `LIMIT`, so
|
|
316
|
+
* its dialect implements {@link Dialect.buildLimitOffset} to emit
|
|
317
|
+
* `[ORDER BY (SELECT NULL)] OFFSET <off> ROWS [FETCH NEXT <lim> ROWS ONLY]`.
|
|
318
|
+
* Param-push order (limit before offset) is owned by the caller and unchanged —
|
|
319
|
+
* this only varies the SQL text, so PG output stays byte-identical.
|
|
320
|
+
*/
|
|
321
|
+
buildPagination(limitPh, offsetPh, hasOrderBy) {
|
|
322
|
+
if (this.dialect.buildLimitOffset) {
|
|
323
|
+
return this.dialect.buildLimitOffset({ limitPlaceholder: limitPh, offsetPlaceholder: offsetPh, hasOrderBy });
|
|
324
|
+
}
|
|
325
|
+
let s = '';
|
|
326
|
+
if (limitPh !== undefined)
|
|
327
|
+
s += ` LIMIT ${limitPh}`;
|
|
328
|
+
if (offsetPh !== undefined)
|
|
329
|
+
s += ` OFFSET ${offsetPh}`;
|
|
330
|
+
return s;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* The single-row limit appended to findUnique / findFirst-style lookups. ` LIMIT 1`
|
|
334
|
+
* for PG/MySQL/SQLite; SQL Server routes through {@link Dialect.buildLimitOffset}
|
|
335
|
+
* (literal `1`, no params) → ` ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1
|
|
336
|
+
* ROWS ONLY`. No params are pushed, so the collect path is unaffected.
|
|
337
|
+
*/
|
|
338
|
+
limitOneClause() {
|
|
339
|
+
if (this.dialect.buildLimitOffset) {
|
|
340
|
+
return this.dialect.buildLimitOffset({ limitPlaceholder: '1', offsetPlaceholder: undefined, hasOrderBy: false });
|
|
341
|
+
}
|
|
342
|
+
return ' LIMIT 1';
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Validate a LIMIT/OFFSET value as a non-negative integer and return it as an
|
|
346
|
+
* inline SQL literal. Used only on `dialect.inlineLimitOffset` engines (MySQL).
|
|
347
|
+
* The input is always a Turbine-controlled pagination value, never a raw user
|
|
348
|
+
* string — and this guard guarantees the output is `String` of a validated
|
|
349
|
+
* integer, so inlining cannot inject SQL.
|
|
350
|
+
*/
|
|
351
|
+
limitOffsetLiteral(value) {
|
|
352
|
+
const n = Number(value);
|
|
353
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
354
|
+
throw new ValidationError(`LIMIT/OFFSET must be a non-negative integer, received: ${String(value)}`);
|
|
355
|
+
}
|
|
356
|
+
return String(n);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Resolve a LIMIT/OFFSET value to either an inline literal (no param pushed, on
|
|
360
|
+
* `dialect.inlineLimitOffset` engines) or a bound placeholder (the value is
|
|
361
|
+
* pushed to `params`). Build and collect paths both gate on the same flag so
|
|
362
|
+
* the param order stays mirrored; PG/SQLite/SQL Server keep parameterizing and
|
|
363
|
+
* stay byte-identical.
|
|
364
|
+
*/
|
|
365
|
+
paginationRef(value, params) {
|
|
366
|
+
if (this.dialect.inlineLimitOffset) {
|
|
367
|
+
return this.limitOffsetLiteral(value);
|
|
368
|
+
}
|
|
369
|
+
params.push(Number(value));
|
|
370
|
+
return this.p(params.length);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Build an `IN` / `NOT IN` predicate through the active dialect. PostgreSQL
|
|
374
|
+
* keeps the array-param form (`expr = ANY($n)` / `expr != ALL($n)`); other
|
|
375
|
+
* engines (SQLite) use a length-independent single-placeholder form. Paired
|
|
376
|
+
* with {@link inParam}, which supplies the single bound value.
|
|
377
|
+
*/
|
|
378
|
+
inClause(expr, paramRef, negated) {
|
|
379
|
+
if (this.dialect.buildInClause)
|
|
380
|
+
return this.dialect.buildInClause(expr, paramRef, negated);
|
|
381
|
+
return negated ? `${expr} != ALL(${paramRef})` : `${expr} = ANY(${paramRef})`;
|
|
382
|
+
}
|
|
383
|
+
/** The single bound parameter for an `IN` list (PG: the array; SQLite: a JSON string). */
|
|
384
|
+
inParam(values) {
|
|
385
|
+
return this.dialect.inClauseParam ? this.dialect.inClauseParam(values) : values;
|
|
386
|
+
}
|
|
304
387
|
/**
|
|
305
388
|
* Return cache hit/miss statistics for this QueryInterface instance.
|
|
306
389
|
* Useful for monitoring and benchmarking.
|
|
@@ -401,6 +484,49 @@ export class QueryInterface {
|
|
|
401
484
|
clearTimeout(timer);
|
|
402
485
|
}
|
|
403
486
|
}
|
|
487
|
+
/**
|
|
488
|
+
* Execute a write `DeferredQuery` (create/update/delete/upsert) according to
|
|
489
|
+
* the active dialect's {@link Dialect.resultStrategy}, then apply its
|
|
490
|
+
* transform.
|
|
491
|
+
*
|
|
492
|
+
* - `'returning'` / `'output'`: the statement returns its own affected rows
|
|
493
|
+
* (`RETURNING *` / `OUTPUT INSERTED.*`). Byte-identical to the historical
|
|
494
|
+
* single `queryWithTimeout` + `transform(result)` path — the PostgreSQL
|
|
495
|
+
* route is unchanged.
|
|
496
|
+
* - `'reselect'`: the engine cannot return rows from a write, so the build
|
|
497
|
+
* method attached a {@link DeferredQuery.reselect} plan that runs the
|
|
498
|
+
* write and a follow-up SELECT; the SELECT's rows feed the transform.
|
|
499
|
+
*/
|
|
500
|
+
async executeMutation(deferred, timeout) {
|
|
501
|
+
if (this.dialect.resultStrategy === 'reselect' && deferred.reselect) {
|
|
502
|
+
const exec = (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, preparedName);
|
|
503
|
+
const result = await deferred.reselect(exec);
|
|
504
|
+
return deferred.transform(result);
|
|
505
|
+
}
|
|
506
|
+
const result = await this.queryWithTimeout(deferred.sql, deferred.params, timeout, deferred.preparedName);
|
|
507
|
+
return deferred.transform(result);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
511
|
+
* by a write's `where` clause. Used by the `'reselect'` result strategy to
|
|
512
|
+
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
513
|
+
* builder as reads, so no user value is interpolated.
|
|
514
|
+
*/
|
|
515
|
+
buildReselectByWhere(whereObj) {
|
|
516
|
+
const params = [];
|
|
517
|
+
const clause = this.buildWhereClause(whereObj, params);
|
|
518
|
+
const where = clause ? ` WHERE ${clause}` : '';
|
|
519
|
+
return { sql: `SELECT * FROM ${this.q(this.table)}${where}`, params };
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Best-effort extraction of an auto-generated primary key from a write
|
|
523
|
+
* result for `'reselect'` engines (e.g. mysql2's `insertId`). Returns
|
|
524
|
+
* `undefined` when the driver exposes no such field.
|
|
525
|
+
*/
|
|
526
|
+
mutationInsertId(result) {
|
|
527
|
+
const r = result;
|
|
528
|
+
return r.insertId ?? r.lastID;
|
|
529
|
+
}
|
|
404
530
|
/**
|
|
405
531
|
* Execute a query through the middleware chain.
|
|
406
532
|
* If no middlewares are registered, executes directly.
|
|
@@ -471,7 +597,7 @@ export class QueryInterface {
|
|
|
471
597
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
472
598
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
473
599
|
void tempParams; // params are positional, SQL is value-invariant
|
|
474
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}
|
|
600
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
475
601
|
});
|
|
476
602
|
// Collect params (same order as build)
|
|
477
603
|
for (const k of whereKeys) {
|
|
@@ -496,7 +622,7 @@ export class QueryInterface {
|
|
|
496
622
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
497
623
|
const qt = this.q(this.table);
|
|
498
624
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
499
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}
|
|
625
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
500
626
|
});
|
|
501
627
|
// Collect params
|
|
502
628
|
this.collectWhereParams(whereObj, params);
|
|
@@ -521,7 +647,7 @@ export class QueryInterface {
|
|
|
521
647
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
522
648
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
523
649
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
524
|
-
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}
|
|
650
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
|
|
525
651
|
});
|
|
526
652
|
// Collect params in exact build order: where first, then with-clause relations
|
|
527
653
|
this.collectWhereParams(whereObj, params);
|
|
@@ -681,14 +807,18 @@ export class QueryInterface {
|
|
|
681
807
|
// vector at the correct position (after cursor params, before LIMIT).
|
|
682
808
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams)}`;
|
|
683
809
|
}
|
|
810
|
+
// Pagination — push params in the same order the collect path mirrors
|
|
811
|
+
// (limit before offset); the SQL TEXT shape is dialect-owned via
|
|
812
|
+
// buildPagination (PG: ` LIMIT $n`/` OFFSET $n`; SQL Server: OFFSET/FETCH).
|
|
813
|
+
let limitPh;
|
|
814
|
+
let offsetPh;
|
|
684
815
|
if (effectiveLimit !== undefined) {
|
|
685
|
-
|
|
686
|
-
sql += ` LIMIT ${this.p(freshParams.length)}`;
|
|
816
|
+
limitPh = this.paginationRef(effectiveLimit, freshParams);
|
|
687
817
|
}
|
|
688
818
|
if (args?.offset !== undefined) {
|
|
689
|
-
|
|
690
|
-
sql += ` OFFSET ${this.p(freshParams.length)}`;
|
|
819
|
+
offsetPh = this.paginationRef(args.offset, freshParams);
|
|
691
820
|
}
|
|
821
|
+
sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
|
|
692
822
|
return sql;
|
|
693
823
|
});
|
|
694
824
|
// Collect params in exact build order:
|
|
@@ -712,12 +842,13 @@ export class QueryInterface {
|
|
|
712
842
|
if (args?.orderBy) {
|
|
713
843
|
this.collectOrderByParams(args.orderBy, params);
|
|
714
844
|
}
|
|
715
|
-
// 5. LIMIT param
|
|
716
|
-
|
|
845
|
+
// 5. LIMIT param — skipped when the dialect inlines pagination (build path
|
|
846
|
+
// mirrors via paginationRef → no placeholder, no param).
|
|
847
|
+
if (effectiveLimit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
717
848
|
params.push(Number(effectiveLimit));
|
|
718
849
|
}
|
|
719
|
-
// 6. OFFSET param
|
|
720
|
-
if (args?.offset !== undefined) {
|
|
850
|
+
// 6. OFFSET param — same inline gate as LIMIT above.
|
|
851
|
+
if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
|
|
721
852
|
params.push(Number(args.offset));
|
|
722
853
|
}
|
|
723
854
|
return {
|
|
@@ -777,34 +908,19 @@ export class QueryInterface {
|
|
|
777
908
|
}
|
|
778
909
|
// --- Overflow: fall back to cursor path from scratch ---
|
|
779
910
|
const deferred = this.buildFindMany(args);
|
|
780
|
-
// Acquire a dedicated connection — cursors require a single connection in a
|
|
911
|
+
// Acquire a dedicated connection — cursors require a single connection in a
|
|
912
|
+
// transaction. The dialect owns the streaming SQL (Postgres: BEGIN → DECLARE
|
|
913
|
+
// … NO SCROLL CURSOR FOR → FETCH n → CLOSE → COMMIT, ROLLBACK on error); we
|
|
914
|
+
// just parse + yield the row batches it produces.
|
|
781
915
|
const client = await this.pool.connect();
|
|
782
|
-
const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
783
|
-
const quotedCursor = this.q(cursorName);
|
|
784
916
|
try {
|
|
785
|
-
await client.
|
|
786
|
-
|
|
787
|
-
while (true) {
|
|
788
|
-
const batch = await client.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
|
|
789
|
-
if (batch.rows.length === 0)
|
|
790
|
-
break;
|
|
791
|
-
for (const row of batch.rows) {
|
|
917
|
+
for await (const batch of this.dialect.openStream(client, deferred.sql, deferred.params, batchSize)) {
|
|
918
|
+
for (const row of batch) {
|
|
792
919
|
yield (hasRelations ? this.parseNestedRow(row, this.table) : this.parseRow(row, this.table));
|
|
793
920
|
}
|
|
794
|
-
if (batch.rows.length < batchSize)
|
|
795
|
-
break;
|
|
796
921
|
}
|
|
797
|
-
await client.query(`CLOSE ${quotedCursor}`);
|
|
798
|
-
await client.query('COMMIT');
|
|
799
922
|
}
|
|
800
923
|
catch (err) {
|
|
801
|
-
// Rollback on error (also closes cursor implicitly)
|
|
802
|
-
try {
|
|
803
|
-
await client.query('ROLLBACK');
|
|
804
|
-
}
|
|
805
|
-
catch {
|
|
806
|
-
// Connection may already be broken — ignore rollback error
|
|
807
|
-
}
|
|
808
924
|
// Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
|
|
809
925
|
throw wrapPgError(err);
|
|
810
926
|
}
|
|
@@ -906,8 +1022,7 @@ export class QueryInterface {
|
|
|
906
1022
|
return this.nestedCreate(args);
|
|
907
1023
|
}
|
|
908
1024
|
const deferred = this.buildCreate(args);
|
|
909
|
-
|
|
910
|
-
return deferred.transform(result);
|
|
1025
|
+
return this.executeMutation(deferred, args.timeout);
|
|
911
1026
|
});
|
|
912
1027
|
}
|
|
913
1028
|
buildCreate(args) {
|
|
@@ -936,6 +1051,33 @@ export class QueryInterface {
|
|
|
936
1051
|
return this.parseRow(row, this.table);
|
|
937
1052
|
},
|
|
938
1053
|
tag: `${this.table}.create`,
|
|
1054
|
+
// Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
|
|
1055
|
+
// (provided value, else the driver's generated insert id).
|
|
1056
|
+
reselect: this.makeCreateReselect(sql, params, args.data),
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
|
|
1061
|
+
* `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
|
|
1062
|
+
* dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
|
|
1063
|
+
* pays nothing. Not yet wired to a real non-RETURNING engine.
|
|
1064
|
+
*/
|
|
1065
|
+
makeCreateReselect(insertSql, insertParams, data) {
|
|
1066
|
+
if (this.dialect.resultStrategy !== 'reselect')
|
|
1067
|
+
return undefined;
|
|
1068
|
+
return async (exec) => {
|
|
1069
|
+
const writeResult = await exec(insertSql, insertParams);
|
|
1070
|
+
const insertId = this.mutationInsertId(writeResult);
|
|
1071
|
+
const conds = [];
|
|
1072
|
+
const selParams = [];
|
|
1073
|
+
let idx = 1;
|
|
1074
|
+
for (const pk of this.tableMeta.primaryKey) {
|
|
1075
|
+
const field = this.tableMeta.reverseColumnMap[pk] ?? snakeToCamel(pk);
|
|
1076
|
+
selParams.push(data[field] ?? data[pk] ?? insertId);
|
|
1077
|
+
conds.push(`${this.q(pk)} = ${this.p(idx++)}`);
|
|
1078
|
+
}
|
|
1079
|
+
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
1080
|
+
return exec(`SELECT * FROM ${this.q(this.table)}${where}`, selParams);
|
|
939
1081
|
};
|
|
940
1082
|
}
|
|
941
1083
|
// -------------------------------------------------------------------------
|
|
@@ -991,8 +1133,7 @@ export class QueryInterface {
|
|
|
991
1133
|
return this.nestedUpdate(args);
|
|
992
1134
|
}
|
|
993
1135
|
const deferred = this.buildUpdate(args);
|
|
994
|
-
|
|
995
|
-
return deferred.transform(result);
|
|
1136
|
+
return this.executeMutation(deferred, args.timeout);
|
|
996
1137
|
});
|
|
997
1138
|
}
|
|
998
1139
|
buildUpdate(args) {
|
|
@@ -1020,7 +1161,12 @@ export class QueryInterface {
|
|
|
1020
1161
|
whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
|
|
1021
1162
|
}
|
|
1022
1163
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
1023
|
-
|
|
1164
|
+
// Engines that inject their returning shape MID-statement (SQL Server
|
|
1165
|
+
// `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
|
|
1166
|
+
// absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
1167
|
+
return this.dialect.buildUpdateStatement
|
|
1168
|
+
? this.dialect.buildUpdateStatement({ table: this.q(this.table), setClauses, whereSql, returning: '*' })
|
|
1169
|
+
: `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
|
|
1024
1170
|
};
|
|
1025
1171
|
let sql;
|
|
1026
1172
|
let preparedName;
|
|
@@ -1064,6 +1210,26 @@ export class QueryInterface {
|
|
|
1064
1210
|
},
|
|
1065
1211
|
tag: `${this.table}.update`,
|
|
1066
1212
|
preparedName,
|
|
1213
|
+
// Non-RETURNING engines: UPDATE, then re-fetch the row by the same where.
|
|
1214
|
+
reselect: this.dialect.resultStrategy === 'reselect'
|
|
1215
|
+
? async (exec) => {
|
|
1216
|
+
const writeResult = await exec(sql, params, preparedName);
|
|
1217
|
+
// Optimistic-lock conflict: the version-checked UPDATE matched no
|
|
1218
|
+
// row. The re-fetch below uses `where` WITHOUT the version
|
|
1219
|
+
// predicate, so it would return the stale row and silently mask
|
|
1220
|
+
// the conflict — detect it from affected-rows here instead, to
|
|
1221
|
+
// match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
|
|
1222
|
+
if (lock && (writeResult.rowCount ?? 0) === 0) {
|
|
1223
|
+
throw new OptimisticLockError({
|
|
1224
|
+
table: this.table,
|
|
1225
|
+
versionField: lock.field,
|
|
1226
|
+
expectedVersion: lock.expected,
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
const sel = this.buildReselectByWhere(whereObj);
|
|
1230
|
+
return exec(sel.sql, sel.params);
|
|
1231
|
+
}
|
|
1232
|
+
: undefined,
|
|
1067
1233
|
};
|
|
1068
1234
|
}
|
|
1069
1235
|
// -------------------------------------------------------------------------
|
|
@@ -1095,19 +1261,19 @@ export class QueryInterface {
|
|
|
1095
1261
|
async runInImplicitTx(fn) {
|
|
1096
1262
|
const client = await this.pool.connect();
|
|
1097
1263
|
try {
|
|
1098
|
-
await client.query(
|
|
1264
|
+
await client.query(this.dialect.beginStatement());
|
|
1099
1265
|
const { TransactionClient } = await import('../client.js');
|
|
1100
1266
|
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
1101
1267
|
const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
|
|
1102
1268
|
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
1103
1269
|
const ctx = { schema: this.schema, tx: tx };
|
|
1104
1270
|
const result = await fn(ctx);
|
|
1105
|
-
await client.query(
|
|
1271
|
+
await client.query(this.dialect.commitStatement());
|
|
1106
1272
|
return result;
|
|
1107
1273
|
}
|
|
1108
1274
|
catch (err) {
|
|
1109
1275
|
try {
|
|
1110
|
-
await client.query(
|
|
1276
|
+
await client.query(this.dialect.rollbackStatement());
|
|
1111
1277
|
}
|
|
1112
1278
|
catch {
|
|
1113
1279
|
// Best-effort rollback — connection may have died.
|
|
@@ -1140,8 +1306,7 @@ export class QueryInterface {
|
|
|
1140
1306
|
async delete(args) {
|
|
1141
1307
|
return this.executeWithMiddleware('delete', args, async () => {
|
|
1142
1308
|
const deferred = this.buildDelete(args);
|
|
1143
|
-
|
|
1144
|
-
return deferred.transform(result);
|
|
1309
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1145
1310
|
});
|
|
1146
1311
|
}
|
|
1147
1312
|
buildDelete(args) {
|
|
@@ -1156,7 +1321,11 @@ export class QueryInterface {
|
|
|
1156
1321
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
1157
1322
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1158
1323
|
this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
|
|
1159
|
-
|
|
1324
|
+
// SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
|
|
1325
|
+
// absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
|
|
1326
|
+
return this.dialect.buildDeleteStatement
|
|
1327
|
+
? this.dialect.buildDeleteStatement({ table: this.q(this.table), whereSql, returning: '*' })
|
|
1328
|
+
: `DELETE FROM ${this.q(this.table)}${whereSql}${this.dialect.buildReturningClause('*')}`;
|
|
1160
1329
|
});
|
|
1161
1330
|
// On cache hit, still validate the predicate
|
|
1162
1331
|
if (whereFp === '') {
|
|
@@ -1179,6 +1348,16 @@ export class QueryInterface {
|
|
|
1179
1348
|
},
|
|
1180
1349
|
tag: `${this.table}.delete`,
|
|
1181
1350
|
preparedName: entry.name,
|
|
1351
|
+
// Non-RETURNING engines: the row is gone after DELETE, so pre-SELECT it
|
|
1352
|
+
// by the same where, then run the DELETE, returning the captured row.
|
|
1353
|
+
reselect: this.dialect.resultStrategy === 'reselect'
|
|
1354
|
+
? async (exec) => {
|
|
1355
|
+
const sel = this.buildReselectByWhere(whereObj);
|
|
1356
|
+
const pre = await exec(sel.sql, sel.params);
|
|
1357
|
+
await exec(entry.sql, params, entry.name);
|
|
1358
|
+
return pre;
|
|
1359
|
+
}
|
|
1360
|
+
: undefined,
|
|
1182
1361
|
};
|
|
1183
1362
|
}
|
|
1184
1363
|
// -------------------------------------------------------------------------
|
|
@@ -1187,8 +1366,7 @@ export class QueryInterface {
|
|
|
1187
1366
|
async upsert(args) {
|
|
1188
1367
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
1189
1368
|
const deferred = this.buildUpsert(args);
|
|
1190
|
-
|
|
1191
|
-
return deferred.transform(result);
|
|
1369
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1192
1370
|
});
|
|
1193
1371
|
}
|
|
1194
1372
|
buildUpsert(args) {
|
|
@@ -1234,6 +1412,14 @@ export class QueryInterface {
|
|
|
1234
1412
|
return this.parseRow(row, this.table);
|
|
1235
1413
|
},
|
|
1236
1414
|
tag: `${this.table}.upsert`,
|
|
1415
|
+
// Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
|
|
1416
|
+
reselect: this.dialect.resultStrategy === 'reselect'
|
|
1417
|
+
? async (exec) => {
|
|
1418
|
+
await exec(sql, params);
|
|
1419
|
+
const sel = this.buildReselectByWhere(args.where);
|
|
1420
|
+
return exec(sel.sql, sel.params);
|
|
1421
|
+
}
|
|
1422
|
+
: undefined,
|
|
1237
1423
|
};
|
|
1238
1424
|
}
|
|
1239
1425
|
// -------------------------------------------------------------------------
|
|
@@ -1328,7 +1514,7 @@ export class QueryInterface {
|
|
|
1328
1514
|
const freshParams = [];
|
|
1329
1515
|
const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
|
|
1330
1516
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1331
|
-
return `SELECT COUNT(*)
|
|
1517
|
+
return `SELECT ${this.castAgg('COUNT(*)', 'int')} AS count FROM ${this.q(this.table)}${whereSql}`;
|
|
1332
1518
|
});
|
|
1333
1519
|
if (args?.where) {
|
|
1334
1520
|
this.collectWhereParams(whereObj, params);
|
|
@@ -1368,7 +1554,7 @@ export class QueryInterface {
|
|
|
1368
1554
|
// _count
|
|
1369
1555
|
if (args._count === true || args._count === undefined) {
|
|
1370
1556
|
// default: always include count
|
|
1371
|
-
selectExprs.push('COUNT(*)
|
|
1557
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1372
1558
|
}
|
|
1373
1559
|
// _sum
|
|
1374
1560
|
if (args._sum) {
|
|
@@ -1384,7 +1570,7 @@ export class QueryInterface {
|
|
|
1384
1570
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1385
1571
|
if (enabled) {
|
|
1386
1572
|
const col = this.toColumn(field);
|
|
1387
|
-
selectExprs.push(`AVG(${this.q(col)})
|
|
1573
|
+
selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
|
|
1388
1574
|
}
|
|
1389
1575
|
}
|
|
1390
1576
|
}
|
|
@@ -1585,12 +1771,12 @@ export class QueryInterface {
|
|
|
1585
1771
|
clauses.push(`${expr} <= ${this.p(params.length)}`);
|
|
1586
1772
|
}
|
|
1587
1773
|
if (op.in !== undefined) {
|
|
1588
|
-
params.push(op.in);
|
|
1589
|
-
clauses.push(
|
|
1774
|
+
params.push(this.inParam(op.in));
|
|
1775
|
+
clauses.push(this.inClause(expr, this.p(params.length), false));
|
|
1590
1776
|
}
|
|
1591
1777
|
if (op.notIn !== undefined) {
|
|
1592
|
-
params.push(op.notIn);
|
|
1593
|
-
clauses.push(
|
|
1778
|
+
params.push(this.inParam(op.notIn));
|
|
1779
|
+
clauses.push(this.inClause(expr, this.p(params.length), true));
|
|
1594
1780
|
}
|
|
1595
1781
|
return clauses;
|
|
1596
1782
|
}
|
|
@@ -1628,13 +1814,13 @@ export class QueryInterface {
|
|
|
1628
1814
|
const selectExprs = [];
|
|
1629
1815
|
// _count
|
|
1630
1816
|
if (args._count === true) {
|
|
1631
|
-
selectExprs.push('COUNT(*)
|
|
1817
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1632
1818
|
}
|
|
1633
1819
|
else if (args._count && typeof args._count === 'object') {
|
|
1634
1820
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
1635
1821
|
if (enabled) {
|
|
1636
1822
|
const col = this.toColumn(field);
|
|
1637
|
-
selectExprs.push(`COUNT(${this.q(col)})
|
|
1823
|
+
selectExprs.push(`${this.castAgg(`COUNT(${this.q(col)})`, 'int')} AS ${this.q(`_count_${col}`)}`);
|
|
1638
1824
|
}
|
|
1639
1825
|
}
|
|
1640
1826
|
}
|
|
@@ -1652,7 +1838,7 @@ export class QueryInterface {
|
|
|
1652
1838
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1653
1839
|
if (enabled) {
|
|
1654
1840
|
const col = this.toColumn(field);
|
|
1655
|
-
selectExprs.push(`AVG(${this.q(col)})
|
|
1841
|
+
selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
|
|
1656
1842
|
}
|
|
1657
1843
|
}
|
|
1658
1844
|
}
|
|
@@ -1675,7 +1861,7 @@ export class QueryInterface {
|
|
|
1675
1861
|
}
|
|
1676
1862
|
}
|
|
1677
1863
|
if (selectExprs.length === 0) {
|
|
1678
|
-
selectExprs.push('COUNT(*)
|
|
1864
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1679
1865
|
}
|
|
1680
1866
|
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
|
|
1681
1867
|
return {
|
|
@@ -2169,9 +2355,9 @@ export class QueryInterface {
|
|
|
2169
2355
|
if (op.not !== undefined && op.not !== null)
|
|
2170
2356
|
params.push(op.not);
|
|
2171
2357
|
if (op.in !== undefined)
|
|
2172
|
-
params.push(op.in);
|
|
2358
|
+
params.push(this.inParam(op.in));
|
|
2173
2359
|
if (op.notIn !== undefined)
|
|
2174
|
-
params.push(op.notIn);
|
|
2360
|
+
params.push(this.inParam(op.notIn));
|
|
2175
2361
|
if (op.contains !== undefined)
|
|
2176
2362
|
params.push(`%${escapeLike(op.contains)}%`);
|
|
2177
2363
|
if (op.startsWith !== undefined)
|
|
@@ -2336,7 +2522,7 @@ export class QueryInterface {
|
|
|
2336
2522
|
if (spec.where) {
|
|
2337
2523
|
this.collectAliasWhereParams(targetTable, targetMeta, spec.where, params);
|
|
2338
2524
|
}
|
|
2339
|
-
if (spec.limit !== undefined) {
|
|
2525
|
+
if (spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
2340
2526
|
params.push(Number(spec.limit));
|
|
2341
2527
|
}
|
|
2342
2528
|
if (spec.with) {
|
|
@@ -2369,7 +2555,7 @@ export class QueryInterface {
|
|
|
2369
2555
|
// buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
|
|
2370
2556
|
// pushing one here would orphan a param and desync the collect path.
|
|
2371
2557
|
// `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
|
|
2372
|
-
if (relDef.type === 'hasMany' && spec.limit !== undefined) {
|
|
2558
|
+
if (relDef.type === 'hasMany' && spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
2373
2559
|
params.push(Number(spec.limit));
|
|
2374
2560
|
}
|
|
2375
2561
|
// Wrapped path: nested relations AFTER where/limit (inside inner subquery)
|
|
@@ -2888,12 +3074,12 @@ export class QueryInterface {
|
|
|
2888
3074
|
}
|
|
2889
3075
|
}
|
|
2890
3076
|
if (op.in !== undefined) {
|
|
2891
|
-
params.push(op.in);
|
|
2892
|
-
clauses.push(
|
|
3077
|
+
params.push(this.inParam(op.in));
|
|
3078
|
+
clauses.push(this.inClause(column, this.p(params.length), false));
|
|
2893
3079
|
}
|
|
2894
3080
|
if (op.notIn !== undefined) {
|
|
2895
|
-
params.push(op.notIn);
|
|
2896
|
-
clauses.push(
|
|
3081
|
+
params.push(this.inParam(op.notIn));
|
|
3082
|
+
clauses.push(this.inClause(column, this.p(params.length), true));
|
|
2897
3083
|
}
|
|
2898
3084
|
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
2899
3085
|
if (op.contains !== undefined) {
|
|
@@ -2964,6 +3150,9 @@ export class QueryInterface {
|
|
|
2964
3150
|
* non-vector column — a user-supplied string can never become a SQL operator.
|
|
2965
3151
|
*/
|
|
2966
3152
|
vectorOperator(field, rawColumn, metric) {
|
|
3153
|
+
if (!this.dialect.supportsVector) {
|
|
3154
|
+
throw new UnsupportedFeatureError('pgvector distance operations', this.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
|
|
3155
|
+
}
|
|
2967
3156
|
const colType = this.getColumnPgType(rawColumn);
|
|
2968
3157
|
if (colType !== 'vector') {
|
|
2969
3158
|
throw new ValidationError(`[turbine] Column "${field}" on table "${this.table}" is not a vector column ` +
|
|
@@ -2984,6 +3173,9 @@ export class QueryInterface {
|
|
|
2984
3173
|
* placeholder string.
|
|
2985
3174
|
*/
|
|
2986
3175
|
pushVectorParam(field, _rawColumn, to, params) {
|
|
3176
|
+
if (!this.dialect.supportsVector) {
|
|
3177
|
+
throw new UnsupportedFeatureError('pgvector distance operations', this.dialect.name, 'Vector search requires PostgreSQL with the pgvector extension.');
|
|
3178
|
+
}
|
|
2987
3179
|
if (!Array.isArray(to) || to.length === 0) {
|
|
2988
3180
|
throw new ValidationError(`[turbine] Vector distance on "${field}" requires a non-empty array of numbers for "to".`);
|
|
2989
3181
|
}
|
|
@@ -3294,6 +3486,32 @@ export class QueryInterface {
|
|
|
3294
3486
|
.map(([k]) => targetMeta.columnMap[k] ?? camelToSnake(k)));
|
|
3295
3487
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
3296
3488
|
}
|
|
3489
|
+
// Engine override seam (additive): a dialect whose JSON-aggregation shape does
|
|
3490
|
+
// not map onto buildJsonObject/buildJsonArrayAgg (SQL Server FOR JSON PATH) owns
|
|
3491
|
+
// the WHOLE subquery. Absent for PG/MySQL/SQLite → the native path below runs
|
|
3492
|
+
// unchanged (byte-identical output, all their tests stay green). The override
|
|
3493
|
+
// pushes params per the documented RelationSubqueryContext ordering contract,
|
|
3494
|
+
// which mirrors collectRelationSubqueryParams so the SQL cache / pipeline stay
|
|
3495
|
+
// in sync.
|
|
3496
|
+
if (this.dialect.buildRelationSubquery) {
|
|
3497
|
+
return this.dialect.buildRelationSubquery({
|
|
3498
|
+
relDef,
|
|
3499
|
+
spec,
|
|
3500
|
+
params,
|
|
3501
|
+
parentRef,
|
|
3502
|
+
alias,
|
|
3503
|
+
targetTable,
|
|
3504
|
+
targetMeta,
|
|
3505
|
+
targetColumns,
|
|
3506
|
+
depth: currentDepth,
|
|
3507
|
+
path: currentPath,
|
|
3508
|
+
quote: (name) => this.q(name),
|
|
3509
|
+
buildWhere: (whereAlias) => (spec !== true && spec.where
|
|
3510
|
+
? this.buildAliasWhere(targetTable, targetMeta, whereAlias, spec.where, params)
|
|
3511
|
+
: '') ?? '',
|
|
3512
|
+
recurse: (nRelDef, nSpec, nParent, nDepth, nPath) => this.buildRelationSubquery(nRelDef, nSpec, params, nParent, aliasCounter, nDepth, nPath),
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3297
3515
|
// Build JSON object pairs for resolved columns
|
|
3298
3516
|
const jsonPairs = targetColumns.map((col) => [
|
|
3299
3517
|
targetMeta.reverseColumnMap[col] ?? snakeToCamel(col),
|
|
@@ -3325,7 +3543,7 @@ export class QueryInterface {
|
|
|
3325
3543
|
const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
3326
3544
|
// Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
|
|
3327
3545
|
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
3328
|
-
jsonPairs.push([nestedRelName,
|
|
3546
|
+
jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSubquery, fallback)]);
|
|
3329
3547
|
}
|
|
3330
3548
|
}
|
|
3331
3549
|
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|
|
@@ -3373,8 +3591,7 @@ export class QueryInterface {
|
|
|
3373
3591
|
// `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
|
|
3374
3592
|
let limitClause = '';
|
|
3375
3593
|
if (relDef.type === 'hasMany' && spec !== true && spec.limit !== undefined) {
|
|
3376
|
-
|
|
3377
|
-
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
3594
|
+
limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
|
|
3378
3595
|
}
|
|
3379
3596
|
if (relDef.type === 'hasMany') {
|
|
3380
3597
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -3399,13 +3616,17 @@ export class QueryInterface {
|
|
|
3399
3616
|
}
|
|
3400
3617
|
const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
3401
3618
|
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
3402
|
-
innerJsonPairs.push([nestedRelName,
|
|
3619
|
+
innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3403
3620
|
}
|
|
3404
3621
|
}
|
|
3405
3622
|
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
3406
3623
|
return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
|
|
3407
3624
|
}
|
|
3408
|
-
|
|
3625
|
+
// Inline ORDER BY only when the dialect's array-agg supports it (PG). For
|
|
3626
|
+
// hasMany this path is reached only when there is no orderClause, so the
|
|
3627
|
+
// argument is `undefined` either way — keeping PG output byte-identical.
|
|
3628
|
+
const inlineOrder = this.dialect.aggSupportsInlineOrderBy ? orderClause.trim() || undefined : undefined;
|
|
3629
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, inlineOrder)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
3409
3630
|
}
|
|
3410
3631
|
// belongsTo / hasOne — return single object
|
|
3411
3632
|
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
@@ -3493,8 +3714,7 @@ export class QueryInterface {
|
|
|
3493
3714
|
// LIMIT — `limit: 0` is honored (LIMIT 0 → empty array)
|
|
3494
3715
|
let limitClause = '';
|
|
3495
3716
|
if (spec !== true && spec.limit !== undefined) {
|
|
3496
|
-
|
|
3497
|
-
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
3717
|
+
limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
|
|
3498
3718
|
}
|
|
3499
3719
|
const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
|
|
3500
3720
|
// When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
|
|
@@ -3519,7 +3739,7 @@ export class QueryInterface {
|
|
|
3519
3739
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
3520
3740
|
? this.dialect.nullJsonLiteral
|
|
3521
3741
|
: this.dialect.emptyJsonArrayLiteral;
|
|
3522
|
-
innerJsonPairs.push([nestedRelName,
|
|
3742
|
+
innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3523
3743
|
}
|
|
3524
3744
|
}
|
|
3525
3745
|
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
@@ -3542,7 +3762,7 @@ export class QueryInterface {
|
|
|
3542
3762
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
3543
3763
|
? this.dialect.nullJsonLiteral
|
|
3544
3764
|
: this.dialect.emptyJsonArrayLiteral;
|
|
3545
|
-
jsonPairs.push([nestedRelName,
|
|
3765
|
+
jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3546
3766
|
}
|
|
3547
3767
|
}
|
|
3548
3768
|
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|