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
|
@@ -337,6 +337,89 @@ class QueryInterface {
|
|
|
337
337
|
p(index) {
|
|
338
338
|
return this.dialect.paramPlaceholder(index);
|
|
339
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
|
+
}
|
|
340
423
|
/**
|
|
341
424
|
* Return cache hit/miss statistics for this QueryInterface instance.
|
|
342
425
|
* Useful for monitoring and benchmarking.
|
|
@@ -437,6 +520,49 @@ class QueryInterface {
|
|
|
437
520
|
clearTimeout(timer);
|
|
438
521
|
}
|
|
439
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
|
+
}
|
|
440
566
|
/**
|
|
441
567
|
* Execute a query through the middleware chain.
|
|
442
568
|
* If no middlewares are registered, executes directly.
|
|
@@ -507,7 +633,7 @@ class QueryInterface {
|
|
|
507
633
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
508
634
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
509
635
|
void tempParams; // params are positional, SQL is value-invariant
|
|
510
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}
|
|
636
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
511
637
|
});
|
|
512
638
|
// Collect params (same order as build)
|
|
513
639
|
for (const k of whereKeys) {
|
|
@@ -532,7 +658,7 @@ class QueryInterface {
|
|
|
532
658
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
533
659
|
const qt = this.q(this.table);
|
|
534
660
|
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
535
|
-
return `SELECT ${selectExpr} FROM ${qt}${whereSql}
|
|
661
|
+
return `SELECT ${selectExpr} FROM ${qt}${whereSql}${this.limitOneClause()}`;
|
|
536
662
|
});
|
|
537
663
|
// Collect params
|
|
538
664
|
this.collectWhereParams(whereObj, params);
|
|
@@ -557,7 +683,7 @@ class QueryInterface {
|
|
|
557
683
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
558
684
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
559
685
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
560
|
-
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}
|
|
686
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql}${this.limitOneClause()}`;
|
|
561
687
|
});
|
|
562
688
|
// Collect params in exact build order: where first, then with-clause relations
|
|
563
689
|
this.collectWhereParams(whereObj, params);
|
|
@@ -717,14 +843,18 @@ class QueryInterface {
|
|
|
717
843
|
// vector at the correct position (after cursor params, before LIMIT).
|
|
718
844
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams)}`;
|
|
719
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;
|
|
720
851
|
if (effectiveLimit !== undefined) {
|
|
721
|
-
|
|
722
|
-
sql += ` LIMIT ${this.p(freshParams.length)}`;
|
|
852
|
+
limitPh = this.paginationRef(effectiveLimit, freshParams);
|
|
723
853
|
}
|
|
724
854
|
if (args?.offset !== undefined) {
|
|
725
|
-
|
|
726
|
-
sql += ` OFFSET ${this.p(freshParams.length)}`;
|
|
855
|
+
offsetPh = this.paginationRef(args.offset, freshParams);
|
|
727
856
|
}
|
|
857
|
+
sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
|
|
728
858
|
return sql;
|
|
729
859
|
});
|
|
730
860
|
// Collect params in exact build order:
|
|
@@ -748,12 +878,13 @@ class QueryInterface {
|
|
|
748
878
|
if (args?.orderBy) {
|
|
749
879
|
this.collectOrderByParams(args.orderBy, params);
|
|
750
880
|
}
|
|
751
|
-
// 5. LIMIT param
|
|
752
|
-
|
|
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) {
|
|
753
884
|
params.push(Number(effectiveLimit));
|
|
754
885
|
}
|
|
755
|
-
// 6. OFFSET param
|
|
756
|
-
if (args?.offset !== undefined) {
|
|
886
|
+
// 6. OFFSET param — same inline gate as LIMIT above.
|
|
887
|
+
if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
|
|
757
888
|
params.push(Number(args.offset));
|
|
758
889
|
}
|
|
759
890
|
return {
|
|
@@ -813,34 +944,19 @@ class QueryInterface {
|
|
|
813
944
|
}
|
|
814
945
|
// --- Overflow: fall back to cursor path from scratch ---
|
|
815
946
|
const deferred = this.buildFindMany(args);
|
|
816
|
-
// 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.
|
|
817
951
|
const client = await this.pool.connect();
|
|
818
|
-
const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
819
|
-
const quotedCursor = this.q(cursorName);
|
|
820
952
|
try {
|
|
821
|
-
await client.
|
|
822
|
-
|
|
823
|
-
while (true) {
|
|
824
|
-
const batch = await client.query(`FETCH ${batchSize} FROM ${quotedCursor}`);
|
|
825
|
-
if (batch.rows.length === 0)
|
|
826
|
-
break;
|
|
827
|
-
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) {
|
|
828
955
|
yield (hasRelations ? this.parseNestedRow(row, this.table) : this.parseRow(row, this.table));
|
|
829
956
|
}
|
|
830
|
-
if (batch.rows.length < batchSize)
|
|
831
|
-
break;
|
|
832
957
|
}
|
|
833
|
-
await client.query(`CLOSE ${quotedCursor}`);
|
|
834
|
-
await client.query('COMMIT');
|
|
835
958
|
}
|
|
836
959
|
catch (err) {
|
|
837
|
-
// Rollback on error (also closes cursor implicitly)
|
|
838
|
-
try {
|
|
839
|
-
await client.query('ROLLBACK');
|
|
840
|
-
}
|
|
841
|
-
catch {
|
|
842
|
-
// Connection may already be broken — ignore rollback error
|
|
843
|
-
}
|
|
844
960
|
// Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
|
|
845
961
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
846
962
|
}
|
|
@@ -942,8 +1058,7 @@ class QueryInterface {
|
|
|
942
1058
|
return this.nestedCreate(args);
|
|
943
1059
|
}
|
|
944
1060
|
const deferred = this.buildCreate(args);
|
|
945
|
-
|
|
946
|
-
return deferred.transform(result);
|
|
1061
|
+
return this.executeMutation(deferred, args.timeout);
|
|
947
1062
|
});
|
|
948
1063
|
}
|
|
949
1064
|
buildCreate(args) {
|
|
@@ -972,6 +1087,33 @@ class QueryInterface {
|
|
|
972
1087
|
return this.parseRow(row, this.table);
|
|
973
1088
|
},
|
|
974
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);
|
|
975
1117
|
};
|
|
976
1118
|
}
|
|
977
1119
|
// -------------------------------------------------------------------------
|
|
@@ -1027,8 +1169,7 @@ class QueryInterface {
|
|
|
1027
1169
|
return this.nestedUpdate(args);
|
|
1028
1170
|
}
|
|
1029
1171
|
const deferred = this.buildUpdate(args);
|
|
1030
|
-
|
|
1031
|
-
return deferred.transform(result);
|
|
1172
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1032
1173
|
});
|
|
1033
1174
|
}
|
|
1034
1175
|
buildUpdate(args) {
|
|
@@ -1056,7 +1197,12 @@ class QueryInterface {
|
|
|
1056
1197
|
whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
|
|
1057
1198
|
}
|
|
1058
1199
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
1059
|
-
|
|
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('*')}`;
|
|
1060
1206
|
};
|
|
1061
1207
|
let sql;
|
|
1062
1208
|
let preparedName;
|
|
@@ -1100,6 +1246,26 @@ class QueryInterface {
|
|
|
1100
1246
|
},
|
|
1101
1247
|
tag: `${this.table}.update`,
|
|
1102
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,
|
|
1103
1269
|
};
|
|
1104
1270
|
}
|
|
1105
1271
|
// -------------------------------------------------------------------------
|
|
@@ -1131,19 +1297,19 @@ class QueryInterface {
|
|
|
1131
1297
|
async runInImplicitTx(fn) {
|
|
1132
1298
|
const client = await this.pool.connect();
|
|
1133
1299
|
try {
|
|
1134
|
-
await client.query(
|
|
1300
|
+
await client.query(this.dialect.beginStatement());
|
|
1135
1301
|
const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('../client.js')));
|
|
1136
1302
|
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
1137
1303
|
const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
|
|
1138
1304
|
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
1139
1305
|
const ctx = { schema: this.schema, tx: tx };
|
|
1140
1306
|
const result = await fn(ctx);
|
|
1141
|
-
await client.query(
|
|
1307
|
+
await client.query(this.dialect.commitStatement());
|
|
1142
1308
|
return result;
|
|
1143
1309
|
}
|
|
1144
1310
|
catch (err) {
|
|
1145
1311
|
try {
|
|
1146
|
-
await client.query(
|
|
1312
|
+
await client.query(this.dialect.rollbackStatement());
|
|
1147
1313
|
}
|
|
1148
1314
|
catch {
|
|
1149
1315
|
// Best-effort rollback — connection may have died.
|
|
@@ -1176,8 +1342,7 @@ class QueryInterface {
|
|
|
1176
1342
|
async delete(args) {
|
|
1177
1343
|
return this.executeWithMiddleware('delete', args, async () => {
|
|
1178
1344
|
const deferred = this.buildDelete(args);
|
|
1179
|
-
|
|
1180
|
-
return deferred.transform(result);
|
|
1345
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1181
1346
|
});
|
|
1182
1347
|
}
|
|
1183
1348
|
buildDelete(args) {
|
|
@@ -1192,7 +1357,11 @@ class QueryInterface {
|
|
|
1192
1357
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
1193
1358
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1194
1359
|
this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
|
|
1195
|
-
|
|
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('*')}`;
|
|
1196
1365
|
});
|
|
1197
1366
|
// On cache hit, still validate the predicate
|
|
1198
1367
|
if (whereFp === '') {
|
|
@@ -1215,6 +1384,16 @@ class QueryInterface {
|
|
|
1215
1384
|
},
|
|
1216
1385
|
tag: `${this.table}.delete`,
|
|
1217
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,
|
|
1218
1397
|
};
|
|
1219
1398
|
}
|
|
1220
1399
|
// -------------------------------------------------------------------------
|
|
@@ -1223,8 +1402,7 @@ class QueryInterface {
|
|
|
1223
1402
|
async upsert(args) {
|
|
1224
1403
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
1225
1404
|
const deferred = this.buildUpsert(args);
|
|
1226
|
-
|
|
1227
|
-
return deferred.transform(result);
|
|
1405
|
+
return this.executeMutation(deferred, args.timeout);
|
|
1228
1406
|
});
|
|
1229
1407
|
}
|
|
1230
1408
|
buildUpsert(args) {
|
|
@@ -1270,6 +1448,14 @@ class QueryInterface {
|
|
|
1270
1448
|
return this.parseRow(row, this.table);
|
|
1271
1449
|
},
|
|
1272
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,
|
|
1273
1459
|
};
|
|
1274
1460
|
}
|
|
1275
1461
|
// -------------------------------------------------------------------------
|
|
@@ -1364,7 +1550,7 @@ class QueryInterface {
|
|
|
1364
1550
|
const freshParams = [];
|
|
1365
1551
|
const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
|
|
1366
1552
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1367
|
-
return `SELECT COUNT(*)
|
|
1553
|
+
return `SELECT ${this.castAgg('COUNT(*)', 'int')} AS count FROM ${this.q(this.table)}${whereSql}`;
|
|
1368
1554
|
});
|
|
1369
1555
|
if (args?.where) {
|
|
1370
1556
|
this.collectWhereParams(whereObj, params);
|
|
@@ -1404,7 +1590,7 @@ class QueryInterface {
|
|
|
1404
1590
|
// _count
|
|
1405
1591
|
if (args._count === true || args._count === undefined) {
|
|
1406
1592
|
// default: always include count
|
|
1407
|
-
selectExprs.push('COUNT(*)
|
|
1593
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1408
1594
|
}
|
|
1409
1595
|
// _sum
|
|
1410
1596
|
if (args._sum) {
|
|
@@ -1420,7 +1606,7 @@ class QueryInterface {
|
|
|
1420
1606
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1421
1607
|
if (enabled) {
|
|
1422
1608
|
const col = this.toColumn(field);
|
|
1423
|
-
selectExprs.push(`AVG(${this.q(col)})
|
|
1609
|
+
selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
|
|
1424
1610
|
}
|
|
1425
1611
|
}
|
|
1426
1612
|
}
|
|
@@ -1621,12 +1807,12 @@ class QueryInterface {
|
|
|
1621
1807
|
clauses.push(`${expr} <= ${this.p(params.length)}`);
|
|
1622
1808
|
}
|
|
1623
1809
|
if (op.in !== undefined) {
|
|
1624
|
-
params.push(op.in);
|
|
1625
|
-
clauses.push(
|
|
1810
|
+
params.push(this.inParam(op.in));
|
|
1811
|
+
clauses.push(this.inClause(expr, this.p(params.length), false));
|
|
1626
1812
|
}
|
|
1627
1813
|
if (op.notIn !== undefined) {
|
|
1628
|
-
params.push(op.notIn);
|
|
1629
|
-
clauses.push(
|
|
1814
|
+
params.push(this.inParam(op.notIn));
|
|
1815
|
+
clauses.push(this.inClause(expr, this.p(params.length), true));
|
|
1630
1816
|
}
|
|
1631
1817
|
return clauses;
|
|
1632
1818
|
}
|
|
@@ -1664,13 +1850,13 @@ class QueryInterface {
|
|
|
1664
1850
|
const selectExprs = [];
|
|
1665
1851
|
// _count
|
|
1666
1852
|
if (args._count === true) {
|
|
1667
|
-
selectExprs.push('COUNT(*)
|
|
1853
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1668
1854
|
}
|
|
1669
1855
|
else if (args._count && typeof args._count === 'object') {
|
|
1670
1856
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
1671
1857
|
if (enabled) {
|
|
1672
1858
|
const col = this.toColumn(field);
|
|
1673
|
-
selectExprs.push(`COUNT(${this.q(col)})
|
|
1859
|
+
selectExprs.push(`${this.castAgg(`COUNT(${this.q(col)})`, 'int')} AS ${this.q(`_count_${col}`)}`);
|
|
1674
1860
|
}
|
|
1675
1861
|
}
|
|
1676
1862
|
}
|
|
@@ -1688,7 +1874,7 @@ class QueryInterface {
|
|
|
1688
1874
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1689
1875
|
if (enabled) {
|
|
1690
1876
|
const col = this.toColumn(field);
|
|
1691
|
-
selectExprs.push(`AVG(${this.q(col)})
|
|
1877
|
+
selectExprs.push(`${this.castAgg(`AVG(${this.q(col)})`, 'float')} AS ${this.q(`_avg_${col}`)}`);
|
|
1692
1878
|
}
|
|
1693
1879
|
}
|
|
1694
1880
|
}
|
|
@@ -1711,7 +1897,7 @@ class QueryInterface {
|
|
|
1711
1897
|
}
|
|
1712
1898
|
}
|
|
1713
1899
|
if (selectExprs.length === 0) {
|
|
1714
|
-
selectExprs.push('COUNT(*)
|
|
1900
|
+
selectExprs.push(`${this.castAgg('COUNT(*)', 'int')} AS _count`);
|
|
1715
1901
|
}
|
|
1716
1902
|
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
|
|
1717
1903
|
return {
|
|
@@ -2205,9 +2391,9 @@ class QueryInterface {
|
|
|
2205
2391
|
if (op.not !== undefined && op.not !== null)
|
|
2206
2392
|
params.push(op.not);
|
|
2207
2393
|
if (op.in !== undefined)
|
|
2208
|
-
params.push(op.in);
|
|
2394
|
+
params.push(this.inParam(op.in));
|
|
2209
2395
|
if (op.notIn !== undefined)
|
|
2210
|
-
params.push(op.notIn);
|
|
2396
|
+
params.push(this.inParam(op.notIn));
|
|
2211
2397
|
if (op.contains !== undefined)
|
|
2212
2398
|
params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
|
|
2213
2399
|
if (op.startsWith !== undefined)
|
|
@@ -2372,7 +2558,7 @@ class QueryInterface {
|
|
|
2372
2558
|
if (spec.where) {
|
|
2373
2559
|
this.collectAliasWhereParams(targetTable, targetMeta, spec.where, params);
|
|
2374
2560
|
}
|
|
2375
|
-
if (spec.limit !== undefined) {
|
|
2561
|
+
if (spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
2376
2562
|
params.push(Number(spec.limit));
|
|
2377
2563
|
}
|
|
2378
2564
|
if (spec.with) {
|
|
@@ -2405,7 +2591,7 @@ class QueryInterface {
|
|
|
2405
2591
|
// buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
|
|
2406
2592
|
// pushing one here would orphan a param and desync the collect path.
|
|
2407
2593
|
// `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
|
|
2408
|
-
if (relDef.type === 'hasMany' && spec.limit !== undefined) {
|
|
2594
|
+
if (relDef.type === 'hasMany' && spec.limit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
2409
2595
|
params.push(Number(spec.limit));
|
|
2410
2596
|
}
|
|
2411
2597
|
// Wrapped path: nested relations AFTER where/limit (inside inner subquery)
|
|
@@ -2924,12 +3110,12 @@ class QueryInterface {
|
|
|
2924
3110
|
}
|
|
2925
3111
|
}
|
|
2926
3112
|
if (op.in !== undefined) {
|
|
2927
|
-
params.push(op.in);
|
|
2928
|
-
clauses.push(
|
|
3113
|
+
params.push(this.inParam(op.in));
|
|
3114
|
+
clauses.push(this.inClause(column, this.p(params.length), false));
|
|
2929
3115
|
}
|
|
2930
3116
|
if (op.notIn !== undefined) {
|
|
2931
|
-
params.push(op.notIn);
|
|
2932
|
-
clauses.push(
|
|
3117
|
+
params.push(this.inParam(op.notIn));
|
|
3118
|
+
clauses.push(this.inClause(column, this.p(params.length), true));
|
|
2933
3119
|
}
|
|
2934
3120
|
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
2935
3121
|
if (op.contains !== undefined) {
|
|
@@ -3000,6 +3186,9 @@ class QueryInterface {
|
|
|
3000
3186
|
* non-vector column — a user-supplied string can never become a SQL operator.
|
|
3001
3187
|
*/
|
|
3002
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
|
+
}
|
|
3003
3192
|
const colType = this.getColumnPgType(rawColumn);
|
|
3004
3193
|
if (colType !== 'vector') {
|
|
3005
3194
|
throw new errors_js_1.ValidationError(`[turbine] Column "${field}" on table "${this.table}" is not a vector column ` +
|
|
@@ -3020,6 +3209,9 @@ class QueryInterface {
|
|
|
3020
3209
|
* placeholder string.
|
|
3021
3210
|
*/
|
|
3022
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
|
+
}
|
|
3023
3215
|
if (!Array.isArray(to) || to.length === 0) {
|
|
3024
3216
|
throw new errors_js_1.ValidationError(`[turbine] Vector distance on "${field}" requires a non-empty array of numbers for "to".`);
|
|
3025
3217
|
}
|
|
@@ -3330,6 +3522,32 @@ class QueryInterface {
|
|
|
3330
3522
|
.map(([k]) => targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k)));
|
|
3331
3523
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
3332
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
|
+
}
|
|
3333
3551
|
// Build JSON object pairs for resolved columns
|
|
3334
3552
|
const jsonPairs = targetColumns.map((col) => [
|
|
3335
3553
|
targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
@@ -3361,7 +3579,7 @@ class QueryInterface {
|
|
|
3361
3579
|
const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
3362
3580
|
// Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
|
|
3363
3581
|
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
3364
|
-
jsonPairs.push([nestedRelName,
|
|
3582
|
+
jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSubquery, fallback)]);
|
|
3365
3583
|
}
|
|
3366
3584
|
}
|
|
3367
3585
|
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|
|
@@ -3409,8 +3627,7 @@ class QueryInterface {
|
|
|
3409
3627
|
// `limit: 0` is honored (LIMIT 0 → empty array), so check !== undefined.
|
|
3410
3628
|
let limitClause = '';
|
|
3411
3629
|
if (relDef.type === 'hasMany' && spec !== true && spec.limit !== undefined) {
|
|
3412
|
-
|
|
3413
|
-
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
3630
|
+
limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
|
|
3414
3631
|
}
|
|
3415
3632
|
if (relDef.type === 'hasMany') {
|
|
3416
3633
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -3435,13 +3652,17 @@ class QueryInterface {
|
|
|
3435
3652
|
}
|
|
3436
3653
|
const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
3437
3654
|
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
3438
|
-
innerJsonPairs.push([nestedRelName,
|
|
3655
|
+
innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3439
3656
|
}
|
|
3440
3657
|
}
|
|
3441
3658
|
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
3442
3659
|
return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
|
|
3443
3660
|
}
|
|
3444
|
-
|
|
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}`;
|
|
3445
3666
|
}
|
|
3446
3667
|
// belongsTo / hasOne — return single object
|
|
3447
3668
|
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
@@ -3529,8 +3750,7 @@ class QueryInterface {
|
|
|
3529
3750
|
// LIMIT — `limit: 0` is honored (LIMIT 0 → empty array)
|
|
3530
3751
|
let limitClause = '';
|
|
3531
3752
|
if (spec !== true && spec.limit !== undefined) {
|
|
3532
|
-
|
|
3533
|
-
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
3753
|
+
limitClause = ` LIMIT ${this.paginationRef(spec.limit, params)}`;
|
|
3534
3754
|
}
|
|
3535
3755
|
const fromJoin = `FROM ${qTarget} ${talias} JOIN ${qJunction} ${jalias} ON ${joinOn}`;
|
|
3536
3756
|
// When LIMIT or ORDER BY is present, wrap the joined rows in an inner subquery
|
|
@@ -3555,7 +3775,7 @@ class QueryInterface {
|
|
|
3555
3775
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
3556
3776
|
? this.dialect.nullJsonLiteral
|
|
3557
3777
|
: this.dialect.emptyJsonArrayLiteral;
|
|
3558
|
-
innerJsonPairs.push([nestedRelName,
|
|
3778
|
+
innerJsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3559
3779
|
}
|
|
3560
3780
|
}
|
|
3561
3781
|
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
@@ -3578,7 +3798,7 @@ class QueryInterface {
|
|
|
3578
3798
|
const fallback = nestedRelDef.type === 'belongsTo' || nestedRelDef.type === 'hasOne'
|
|
3579
3799
|
? this.dialect.nullJsonLiteral
|
|
3580
3800
|
: this.dialect.emptyJsonArrayLiteral;
|
|
3581
|
-
jsonPairs.push([nestedRelName,
|
|
3801
|
+
jsonPairs.push([nestedRelName, this.dialect.wrapJsonSubresult(nestedSub, fallback)]);
|
|
3582
3802
|
}
|
|
3583
3803
|
}
|
|
3584
3804
|
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|