turbine-orm 0.9.2 → 0.11.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 +34 -16
- package/dist/adapters/cockroachdb.d.ts +40 -0
- package/dist/adapters/cockroachdb.js +172 -0
- package/dist/adapters/index.d.ts +107 -0
- package/dist/adapters/index.js +83 -0
- package/dist/adapters/yugabytedb.d.ts +52 -0
- package/dist/adapters/yugabytedb.js +156 -0
- package/dist/cjs/adapters/cockroachdb.js +174 -0
- package/dist/cjs/adapters/index.js +87 -0
- package/dist/cjs/adapters/yugabytedb.js +158 -0
- package/dist/cjs/cli/index.js +2 -1
- package/dist/cjs/cli/migrate.js +18 -12
- package/dist/cjs/cli/studio.js +5 -4
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/dialect.js +57 -0
- package/dist/cjs/generate.js +8 -1
- package/dist/cjs/index.js +12 -3
- package/dist/cjs/introspect.js +46 -18
- package/dist/cjs/query/builder.js +129 -96
- package/dist/cjs/query/index.js +4 -1
- package/dist/cjs/query/utils.js +18 -0
- package/dist/cjs/schema.js +8 -0
- package/dist/cli/config.d.ts +11 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/migrate.d.ts +3 -0
- package/dist/cli/migrate.js +16 -10
- package/dist/cli/studio.d.ts +4 -0
- package/dist/cli/studio.js +5 -4
- package/dist/client.d.ts +3 -0
- package/dist/client.js +1 -0
- package/dist/dialect.d.ts +61 -0
- package/dist/dialect.js +55 -0
- package/dist/generate.js +8 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/introspect.js +46 -18
- package/dist/query/builder.d.ts +9 -1
- package/dist/query/builder.js +130 -97
- package/dist/query/index.d.ts +3 -1
- package/dist/query/index.js +2 -1
- package/dist/query/utils.d.ts +8 -0
- package/dist/query/utils.js +17 -0
- package/dist/schema.d.ts +6 -4
- package/dist/schema.js +7 -0
- package/package.json +8 -3
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.QueryInterface = void 0;
|
|
16
|
+
const dialect_js_1 = require("../dialect.js");
|
|
16
17
|
const errors_js_1 = require("../errors.js");
|
|
17
18
|
const schema_js_1 = require("../schema.js");
|
|
18
19
|
const utils_js_1 = require("./utils.js");
|
|
@@ -99,6 +100,7 @@ function findArrayUniqueKey(value) {
|
|
|
99
100
|
}
|
|
100
101
|
return null;
|
|
101
102
|
}
|
|
103
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
|
|
102
104
|
class QueryInterface {
|
|
103
105
|
pool;
|
|
104
106
|
table;
|
|
@@ -111,6 +113,7 @@ class QueryInterface {
|
|
|
111
113
|
warnOnUnlimited;
|
|
112
114
|
preparedStatementsEnabled;
|
|
113
115
|
sqlCacheEnabled;
|
|
116
|
+
dialect;
|
|
114
117
|
/**
|
|
115
118
|
* Tracks tables that have already triggered an unlimited-query warning so
|
|
116
119
|
* the user is not spammed once per row. Per-instance state — each
|
|
@@ -145,6 +148,7 @@ class QueryInterface {
|
|
|
145
148
|
this.warnOnUnlimited = options?.warnOnUnlimited !== false;
|
|
146
149
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
147
150
|
this.sqlCacheEnabled = options?.sqlCache !== false;
|
|
151
|
+
this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
148
152
|
// Pre-compute column type lookup maps (TASK-26)
|
|
149
153
|
this.columnPgTypeMap = new Map();
|
|
150
154
|
this.columnArrayTypeMap = new Map();
|
|
@@ -153,6 +157,14 @@ class QueryInterface {
|
|
|
153
157
|
this.columnArrayTypeMap.set(col.name, col.pgArrayType);
|
|
154
158
|
}
|
|
155
159
|
}
|
|
160
|
+
/** Quote an identifier through the active SQL dialect. */
|
|
161
|
+
q(name) {
|
|
162
|
+
return this.dialect.quoteIdentifier(name);
|
|
163
|
+
}
|
|
164
|
+
/** Return the active dialect's placeholder for a 1-indexed parameter position. */
|
|
165
|
+
p(index) {
|
|
166
|
+
return this.dialect.paramPlaceholder(index);
|
|
167
|
+
}
|
|
156
168
|
/**
|
|
157
169
|
* Return cache hit/miss statistics for this QueryInterface instance.
|
|
158
170
|
* Useful for monitoring and benchmarking.
|
|
@@ -261,6 +273,7 @@ class QueryInterface {
|
|
|
261
273
|
// -------------------------------------------------------------------------
|
|
262
274
|
// findUnique
|
|
263
275
|
// -------------------------------------------------------------------------
|
|
276
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
264
277
|
async findUnique(args) {
|
|
265
278
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
266
279
|
const deferred = this.buildFindUnique(args);
|
|
@@ -268,6 +281,7 @@ class QueryInterface {
|
|
|
268
281
|
return deferred.transform(result);
|
|
269
282
|
});
|
|
270
283
|
}
|
|
284
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
271
285
|
buildFindUnique(args) {
|
|
272
286
|
const columnsList = this.resolveColumns(args.select, args.omit);
|
|
273
287
|
const whereObj = args.where;
|
|
@@ -288,11 +302,11 @@ class QueryInterface {
|
|
|
288
302
|
// Simple path: plain equality, no operators/null/OR
|
|
289
303
|
if (!args.with && isSimpleWhere) {
|
|
290
304
|
const entry = this.acquireSql(ck, () => {
|
|
291
|
-
const qt =
|
|
305
|
+
const qt = this.q(this.table);
|
|
292
306
|
const tempParams = whereKeys.map((k) => whereObj[k]);
|
|
293
|
-
const whereClauses = whereKeys.map((k, i) => `${this.toSqlColumn(k)} =
|
|
307
|
+
const whereClauses = whereKeys.map((k, i) => `${this.toSqlColumn(k)} = ${this.p(i + 1)}`);
|
|
294
308
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
295
|
-
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${
|
|
309
|
+
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
296
310
|
void tempParams; // params are positional, SQL is value-invariant
|
|
297
311
|
return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
|
|
298
312
|
});
|
|
@@ -317,8 +331,8 @@ class QueryInterface {
|
|
|
317
331
|
const freshParams = [];
|
|
318
332
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
319
333
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
320
|
-
const qt =
|
|
321
|
-
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${
|
|
334
|
+
const qt = this.q(this.table);
|
|
335
|
+
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
322
336
|
return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
|
|
323
337
|
});
|
|
324
338
|
// Collect params
|
|
@@ -344,7 +358,7 @@ class QueryInterface {
|
|
|
344
358
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
345
359
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
346
360
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
347
|
-
return `SELECT ${selectClause} FROM ${
|
|
361
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql} LIMIT 1`;
|
|
348
362
|
});
|
|
349
363
|
// Collect params in exact build order: where first, then with-clause relations
|
|
350
364
|
this.collectWhereParams(whereObj, params);
|
|
@@ -363,6 +377,7 @@ class QueryInterface {
|
|
|
363
377
|
// -------------------------------------------------------------------------
|
|
364
378
|
// findMany
|
|
365
379
|
// -------------------------------------------------------------------------
|
|
380
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
366
381
|
async findMany(args) {
|
|
367
382
|
this.maybeWarnUnlimited(args);
|
|
368
383
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -420,6 +435,7 @@ class QueryInterface {
|
|
|
420
435
|
}
|
|
421
436
|
return maxDepth;
|
|
422
437
|
}
|
|
438
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
423
439
|
buildFindMany(args) {
|
|
424
440
|
const columnsList = this.resolveColumns(args?.select, args?.omit);
|
|
425
441
|
const colKey = columnsList ? columnsList.join(',') : '*';
|
|
@@ -453,7 +469,7 @@ class QueryInterface {
|
|
|
453
469
|
return { sql: clause ? ` WHERE ${clause}` : '' };
|
|
454
470
|
})()
|
|
455
471
|
: { sql: '' };
|
|
456
|
-
const qt =
|
|
472
|
+
const qt = this.q(this.table);
|
|
457
473
|
let distinctPrefix = '';
|
|
458
474
|
if (args?.distinct && args.distinct.length > 0) {
|
|
459
475
|
const distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
|
|
@@ -464,7 +480,7 @@ class QueryInterface {
|
|
|
464
480
|
selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
465
481
|
}
|
|
466
482
|
else if (columnsList) {
|
|
467
|
-
selectClause = columnsList.map((c) => `${qt}.${
|
|
483
|
+
selectClause = columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ');
|
|
468
484
|
}
|
|
469
485
|
else {
|
|
470
486
|
selectClause = `${qt}.*`;
|
|
@@ -478,7 +494,7 @@ class QueryInterface {
|
|
|
478
494
|
const dir = args.orderBy?.[k] ?? 'asc';
|
|
479
495
|
const op = dir === 'desc' ? '<' : '>';
|
|
480
496
|
freshParams.push(v);
|
|
481
|
-
return `${qt}.${col} ${op}
|
|
497
|
+
return `${qt}.${col} ${op} ${this.p(freshParams.length)}`;
|
|
482
498
|
});
|
|
483
499
|
if (freshWhereSql) {
|
|
484
500
|
sql += ` AND ${cursorConditions.join(' AND ')}`;
|
|
@@ -493,11 +509,11 @@ class QueryInterface {
|
|
|
493
509
|
}
|
|
494
510
|
if (effectiveLimit !== undefined) {
|
|
495
511
|
freshParams.push(Number(effectiveLimit));
|
|
496
|
-
sql += ` LIMIT
|
|
512
|
+
sql += ` LIMIT ${this.p(freshParams.length)}`;
|
|
497
513
|
}
|
|
498
514
|
if (args?.offset !== undefined) {
|
|
499
515
|
freshParams.push(Number(args.offset));
|
|
500
|
-
sql += ` OFFSET
|
|
516
|
+
sql += ` OFFSET ${this.p(freshParams.length)}`;
|
|
501
517
|
}
|
|
502
518
|
return sql;
|
|
503
519
|
});
|
|
@@ -563,6 +579,7 @@ class QueryInterface {
|
|
|
563
579
|
* }
|
|
564
580
|
* ```
|
|
565
581
|
*/
|
|
582
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
566
583
|
async *findManyStream(args) {
|
|
567
584
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
568
585
|
const hasRelations = !!args?.with;
|
|
@@ -584,7 +601,7 @@ class QueryInterface {
|
|
|
584
601
|
// Acquire a dedicated connection — cursors require a single connection in a transaction
|
|
585
602
|
const client = await this.pool.connect();
|
|
586
603
|
const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
587
|
-
const quotedCursor =
|
|
604
|
+
const quotedCursor = this.q(cursorName);
|
|
588
605
|
try {
|
|
589
606
|
await client.query('BEGIN');
|
|
590
607
|
await client.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${deferred.sql}`, deferred.params);
|
|
@@ -619,6 +636,7 @@ class QueryInterface {
|
|
|
619
636
|
// -------------------------------------------------------------------------
|
|
620
637
|
// findFirst — like findMany but returns a single row or null
|
|
621
638
|
// -------------------------------------------------------------------------
|
|
639
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
622
640
|
async findFirst(args) {
|
|
623
641
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
624
642
|
const deferred = this.buildFindFirst(args);
|
|
@@ -626,6 +644,7 @@ class QueryInterface {
|
|
|
626
644
|
return deferred.transform(result);
|
|
627
645
|
});
|
|
628
646
|
}
|
|
647
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
629
648
|
buildFindFirst(args) {
|
|
630
649
|
// Reuse findMany's SQL builder but force LIMIT 1
|
|
631
650
|
const findManyArgs = { ...args, limit: 1 };
|
|
@@ -643,6 +662,7 @@ class QueryInterface {
|
|
|
643
662
|
// -------------------------------------------------------------------------
|
|
644
663
|
// findFirstOrThrow — like findFirst but throws if no record found
|
|
645
664
|
// -------------------------------------------------------------------------
|
|
665
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
646
666
|
async findFirstOrThrow(args) {
|
|
647
667
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
648
668
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
@@ -650,6 +670,7 @@ class QueryInterface {
|
|
|
650
670
|
return deferred.transform(result);
|
|
651
671
|
});
|
|
652
672
|
}
|
|
673
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
653
674
|
buildFindFirstOrThrow(args) {
|
|
654
675
|
const inner = this.buildFindFirst(args);
|
|
655
676
|
return {
|
|
@@ -672,6 +693,7 @@ class QueryInterface {
|
|
|
672
693
|
// -------------------------------------------------------------------------
|
|
673
694
|
// findUniqueOrThrow — like findUnique but throws if no record found
|
|
674
695
|
// -------------------------------------------------------------------------
|
|
696
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
675
697
|
async findUniqueOrThrow(args) {
|
|
676
698
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
677
699
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
@@ -679,6 +701,7 @@ class QueryInterface {
|
|
|
679
701
|
return deferred.transform(result);
|
|
680
702
|
});
|
|
681
703
|
}
|
|
704
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
682
705
|
buildFindUniqueOrThrow(args) {
|
|
683
706
|
const inner = this.buildFindUnique(args);
|
|
684
707
|
return {
|
|
@@ -712,8 +735,8 @@ class QueryInterface {
|
|
|
712
735
|
const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
|
|
713
736
|
const columns = entries.map(([k]) => this.toSqlColumn(k));
|
|
714
737
|
const params = entries.map(([, v]) => v);
|
|
715
|
-
const placeholders = entries.map((_, i) =>
|
|
716
|
-
const sql = `INSERT INTO ${
|
|
738
|
+
const placeholders = entries.map((_, i) => `${this.p(i + 1)}`);
|
|
739
|
+
const sql = `INSERT INTO ${this.q(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
|
|
717
740
|
return {
|
|
718
741
|
sql,
|
|
719
742
|
params,
|
|
@@ -742,7 +765,7 @@ class QueryInterface {
|
|
|
742
765
|
});
|
|
743
766
|
}
|
|
744
767
|
buildCreateMany(args) {
|
|
745
|
-
const qt =
|
|
768
|
+
const qt = this.q(this.table);
|
|
746
769
|
if (args.data.length === 0) {
|
|
747
770
|
return {
|
|
748
771
|
sql: `SELECT * FROM ${qt} WHERE false`,
|
|
@@ -763,8 +786,8 @@ class QueryInterface {
|
|
|
763
786
|
}
|
|
764
787
|
// Use actual Postgres types for array casts
|
|
765
788
|
const typeCasts = columns.map((col) => this.getColumnArrayType(col));
|
|
766
|
-
const unnestArgs = columnArrays.map((_, i) =>
|
|
767
|
-
const quotedColumns = columns.map((c) =>
|
|
789
|
+
const unnestArgs = columnArrays.map((_, i) => `${this.p(i + 1)}::${typeCasts[i]}`);
|
|
790
|
+
const quotedColumns = columns.map((c) => this.q(c));
|
|
768
791
|
let sql = `INSERT INTO ${qt} (${quotedColumns.join(', ')}) SELECT * FROM UNNEST(${unnestArgs.join(', ')})`;
|
|
769
792
|
// skipDuplicates: add ON CONFLICT DO NOTHING
|
|
770
793
|
if (args.skipDuplicates) {
|
|
@@ -802,7 +825,7 @@ class QueryInterface {
|
|
|
802
825
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
803
826
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
804
827
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
805
|
-
return `UPDATE ${
|
|
828
|
+
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
|
|
806
829
|
});
|
|
807
830
|
// On cache hit, validate predicate
|
|
808
831
|
if (whereFp === '') {
|
|
@@ -851,7 +874,7 @@ class QueryInterface {
|
|
|
851
874
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
852
875
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
853
876
|
this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
|
|
854
|
-
return `DELETE FROM ${
|
|
877
|
+
return `DELETE FROM ${this.q(this.table)}${whereSql} RETURNING *`;
|
|
855
878
|
});
|
|
856
879
|
// On cache hit, still validate the predicate
|
|
857
880
|
if (whereFp === '') {
|
|
@@ -891,7 +914,7 @@ class QueryInterface {
|
|
|
891
914
|
const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
|
|
892
915
|
const columns = createEntries.map(([k]) => this.toSqlColumn(k));
|
|
893
916
|
const createParams = createEntries.map(([, v]) => v);
|
|
894
|
-
const placeholders = createEntries.map((_, i) =>
|
|
917
|
+
const placeholders = createEntries.map((_, i) => `${this.p(i + 1)}`);
|
|
895
918
|
// The conflict target comes from `where` keys — must be unique/PK columns
|
|
896
919
|
const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
|
|
897
920
|
const conflictColumns = conflictKeys.map((k) => this.toSqlColumn(k));
|
|
@@ -899,13 +922,13 @@ class QueryInterface {
|
|
|
899
922
|
const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
|
|
900
923
|
let paramIdx = createParams.length + 1;
|
|
901
924
|
const setClauses = updateEntries.map(([k]) => {
|
|
902
|
-
const clause = `${this.toSqlColumn(k)} =
|
|
925
|
+
const clause = `${this.toSqlColumn(k)} = ${this.p(paramIdx)}`;
|
|
903
926
|
paramIdx++;
|
|
904
927
|
return clause;
|
|
905
928
|
});
|
|
906
929
|
const updateParams = updateEntries.map(([, v]) => v);
|
|
907
930
|
const params = [...createParams, ...updateParams];
|
|
908
|
-
const sql = `INSERT INTO ${
|
|
931
|
+
const sql = `INSERT INTO ${this.q(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
|
|
909
932
|
` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${setClauses.join(', ')}` +
|
|
910
933
|
` RETURNING *`;
|
|
911
934
|
return {
|
|
@@ -950,7 +973,7 @@ class QueryInterface {
|
|
|
950
973
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
951
974
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
952
975
|
this.assertMutationHasPredicate('updateMany', whereSql, args.allowFullTableScan);
|
|
953
|
-
return `UPDATE ${
|
|
976
|
+
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
|
|
954
977
|
});
|
|
955
978
|
if (whereFp === '') {
|
|
956
979
|
this.assertMutationHasPredicate('updateMany', '', args.allowFullTableScan);
|
|
@@ -985,7 +1008,7 @@ class QueryInterface {
|
|
|
985
1008
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
986
1009
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
987
1010
|
this.assertMutationHasPredicate('deleteMany', whereSql, args.allowFullTableScan);
|
|
988
|
-
return `DELETE FROM ${
|
|
1011
|
+
return `DELETE FROM ${this.q(this.table)}${whereSql}`;
|
|
989
1012
|
});
|
|
990
1013
|
if (whereFp === '') {
|
|
991
1014
|
this.assertMutationHasPredicate('deleteMany', '', args.allowFullTableScan);
|
|
@@ -1018,7 +1041,7 @@ class QueryInterface {
|
|
|
1018
1041
|
const freshParams = [];
|
|
1019
1042
|
const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
|
|
1020
1043
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1021
|
-
return `SELECT COUNT(*)::int AS count FROM ${
|
|
1044
|
+
return `SELECT COUNT(*)::int AS count FROM ${this.q(this.table)}${whereSql}`;
|
|
1022
1045
|
});
|
|
1023
1046
|
if (args?.where) {
|
|
1024
1047
|
this.collectWhereParams(whereObj, params);
|
|
@@ -1051,7 +1074,7 @@ class QueryInterface {
|
|
|
1051
1074
|
}
|
|
1052
1075
|
}
|
|
1053
1076
|
const groupColsRaw = args.by.map((k) => this.toColumn(k));
|
|
1054
|
-
const groupCols = groupColsRaw.map((c) =>
|
|
1077
|
+
const groupCols = groupColsRaw.map((c) => this.q(c));
|
|
1055
1078
|
const { sql: whereSql, params } = args.where ? this.buildWhere(args.where) : { sql: '', params: [] };
|
|
1056
1079
|
// Build SELECT expressions: group-by columns + aggregate functions
|
|
1057
1080
|
const selectExprs = [...groupCols];
|
|
@@ -1065,7 +1088,7 @@ class QueryInterface {
|
|
|
1065
1088
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
1066
1089
|
if (enabled) {
|
|
1067
1090
|
const col = this.toColumn(field);
|
|
1068
|
-
selectExprs.push(`SUM(${
|
|
1091
|
+
selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
|
|
1069
1092
|
}
|
|
1070
1093
|
}
|
|
1071
1094
|
}
|
|
@@ -1074,7 +1097,7 @@ class QueryInterface {
|
|
|
1074
1097
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1075
1098
|
if (enabled) {
|
|
1076
1099
|
const col = this.toColumn(field);
|
|
1077
|
-
selectExprs.push(`AVG(${
|
|
1100
|
+
selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
|
|
1078
1101
|
}
|
|
1079
1102
|
}
|
|
1080
1103
|
}
|
|
@@ -1083,7 +1106,7 @@ class QueryInterface {
|
|
|
1083
1106
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
1084
1107
|
if (enabled) {
|
|
1085
1108
|
const col = this.toColumn(field);
|
|
1086
|
-
selectExprs.push(`MIN(${
|
|
1109
|
+
selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
|
|
1087
1110
|
}
|
|
1088
1111
|
}
|
|
1089
1112
|
}
|
|
@@ -1092,11 +1115,11 @@ class QueryInterface {
|
|
|
1092
1115
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
1093
1116
|
if (enabled) {
|
|
1094
1117
|
const col = this.toColumn(field);
|
|
1095
|
-
selectExprs.push(`MAX(${
|
|
1118
|
+
selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
|
|
1096
1119
|
}
|
|
1097
1120
|
}
|
|
1098
1121
|
}
|
|
1099
|
-
let sql = `SELECT ${selectExprs.join(', ')} FROM ${
|
|
1122
|
+
let sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
|
|
1100
1123
|
// ORDER BY
|
|
1101
1124
|
if (args.orderBy) {
|
|
1102
1125
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
|
|
@@ -1204,7 +1227,7 @@ class QueryInterface {
|
|
|
1204
1227
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
1205
1228
|
if (enabled) {
|
|
1206
1229
|
const col = this.toColumn(field);
|
|
1207
|
-
selectExprs.push(`COUNT(${
|
|
1230
|
+
selectExprs.push(`COUNT(${this.q(col)})::int AS ${this.q(`_count_${col}`)}`);
|
|
1208
1231
|
}
|
|
1209
1232
|
}
|
|
1210
1233
|
}
|
|
@@ -1213,7 +1236,7 @@ class QueryInterface {
|
|
|
1213
1236
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
1214
1237
|
if (enabled) {
|
|
1215
1238
|
const col = this.toColumn(field);
|
|
1216
|
-
selectExprs.push(`SUM(${
|
|
1239
|
+
selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
|
|
1217
1240
|
}
|
|
1218
1241
|
}
|
|
1219
1242
|
}
|
|
@@ -1222,7 +1245,7 @@ class QueryInterface {
|
|
|
1222
1245
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1223
1246
|
if (enabled) {
|
|
1224
1247
|
const col = this.toColumn(field);
|
|
1225
|
-
selectExprs.push(`AVG(${
|
|
1248
|
+
selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
|
|
1226
1249
|
}
|
|
1227
1250
|
}
|
|
1228
1251
|
}
|
|
@@ -1231,7 +1254,7 @@ class QueryInterface {
|
|
|
1231
1254
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
1232
1255
|
if (enabled) {
|
|
1233
1256
|
const col = this.toColumn(field);
|
|
1234
|
-
selectExprs.push(`MIN(${
|
|
1257
|
+
selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
|
|
1235
1258
|
}
|
|
1236
1259
|
}
|
|
1237
1260
|
}
|
|
@@ -1240,14 +1263,14 @@ class QueryInterface {
|
|
|
1240
1263
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
1241
1264
|
if (enabled) {
|
|
1242
1265
|
const col = this.toColumn(field);
|
|
1243
|
-
selectExprs.push(`MAX(${
|
|
1266
|
+
selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
|
|
1244
1267
|
}
|
|
1245
1268
|
}
|
|
1246
1269
|
}
|
|
1247
1270
|
if (selectExprs.length === 0) {
|
|
1248
1271
|
selectExprs.push('COUNT(*)::int AS _count');
|
|
1249
1272
|
}
|
|
1250
|
-
const sql = `SELECT ${selectExprs.join(', ')} FROM ${
|
|
1273
|
+
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
|
|
1251
1274
|
return {
|
|
1252
1275
|
sql,
|
|
1253
1276
|
params,
|
|
@@ -1364,7 +1387,7 @@ class QueryInterface {
|
|
|
1364
1387
|
}
|
|
1365
1388
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
1366
1389
|
toSqlColumn(field) {
|
|
1367
|
-
return
|
|
1390
|
+
return this.q(this.toColumn(field));
|
|
1368
1391
|
}
|
|
1369
1392
|
/**
|
|
1370
1393
|
* Build a single SET clause entry for update/updateMany.
|
|
@@ -1397,7 +1420,7 @@ class QueryInterface {
|
|
|
1397
1420
|
const opValue = v[op];
|
|
1398
1421
|
if (op === 'set') {
|
|
1399
1422
|
params.push(opValue);
|
|
1400
|
-
return `${col} =
|
|
1423
|
+
return `${col} = ${this.p(params.length)}`;
|
|
1401
1424
|
}
|
|
1402
1425
|
// Arithmetic operators: must be finite numbers
|
|
1403
1426
|
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
@@ -1405,19 +1428,19 @@ class QueryInterface {
|
|
|
1405
1428
|
}
|
|
1406
1429
|
if (op === 'increment') {
|
|
1407
1430
|
params.push(opValue);
|
|
1408
|
-
return `${col} = ${col} +
|
|
1431
|
+
return `${col} = ${col} + ${this.p(params.length)}`;
|
|
1409
1432
|
}
|
|
1410
1433
|
if (op === 'decrement') {
|
|
1411
1434
|
params.push(opValue);
|
|
1412
|
-
return `${col} = ${col} -
|
|
1435
|
+
return `${col} = ${col} - ${this.p(params.length)}`;
|
|
1413
1436
|
}
|
|
1414
1437
|
if (op === 'multiply') {
|
|
1415
1438
|
params.push(opValue);
|
|
1416
|
-
return `${col} = ${col} *
|
|
1439
|
+
return `${col} = ${col} * ${this.p(params.length)}`;
|
|
1417
1440
|
}
|
|
1418
1441
|
if (op === 'divide') {
|
|
1419
1442
|
params.push(opValue);
|
|
1420
|
-
return `${col} = ${col} /
|
|
1443
|
+
return `${col} = ${col} / ${this.p(params.length)}`;
|
|
1421
1444
|
}
|
|
1422
1445
|
}
|
|
1423
1446
|
// Fall through: multi-key objects or non-operator single-key objects
|
|
@@ -1425,7 +1448,7 @@ class QueryInterface {
|
|
|
1425
1448
|
}
|
|
1426
1449
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
1427
1450
|
params.push(value);
|
|
1428
|
-
return `${col} =
|
|
1451
|
+
return `${col} = ${this.p(params.length)}`;
|
|
1429
1452
|
}
|
|
1430
1453
|
// =========================================================================
|
|
1431
1454
|
// Fingerprinting — value-invariant shape keys for SQL cache lookup
|
|
@@ -1948,7 +1971,7 @@ class QueryInterface {
|
|
|
1948
1971
|
}
|
|
1949
1972
|
}
|
|
1950
1973
|
const rawColumn = this.toColumn(key);
|
|
1951
|
-
const column =
|
|
1974
|
+
const column = this.q(rawColumn);
|
|
1952
1975
|
// Handle null → IS NULL
|
|
1953
1976
|
if (value === null) {
|
|
1954
1977
|
andClauses.push(`${column} IS NULL`);
|
|
@@ -1998,7 +2021,7 @@ class QueryInterface {
|
|
|
1998
2021
|
}
|
|
1999
2022
|
// Plain equality
|
|
2000
2023
|
params.push(value);
|
|
2001
|
-
andClauses.push(`${column} =
|
|
2024
|
+
andClauses.push(`${column} = ${this.p(params.length)}`);
|
|
2002
2025
|
}
|
|
2003
2026
|
if (andClauses.length === 0)
|
|
2004
2027
|
return null;
|
|
@@ -2013,18 +2036,18 @@ class QueryInterface {
|
|
|
2013
2036
|
const targetMeta = this.schema.tables[targetTable];
|
|
2014
2037
|
if (!targetMeta)
|
|
2015
2038
|
return null;
|
|
2016
|
-
const qt =
|
|
2017
|
-
const qSelf =
|
|
2039
|
+
const qt = this.q(targetTable);
|
|
2040
|
+
const qSelf = this.q(this.table);
|
|
2018
2041
|
const clauses = [];
|
|
2019
|
-
// Correlation: link child table to parent table
|
|
2042
|
+
// Correlation: link child table to parent table (supports composite FKs)
|
|
2020
2043
|
let correlation;
|
|
2021
2044
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
2022
2045
|
// parent.pk = child.fk
|
|
2023
|
-
correlation =
|
|
2046
|
+
correlation = this.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
|
|
2024
2047
|
}
|
|
2025
2048
|
else {
|
|
2026
2049
|
// belongsTo: parent.fk = child.pk
|
|
2027
|
-
correlation =
|
|
2050
|
+
correlation = this.dialect.buildCorrelation(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
|
|
2028
2051
|
}
|
|
2029
2052
|
// "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
|
|
2030
2053
|
if (filterObj.some !== undefined) {
|
|
@@ -2061,13 +2084,17 @@ class QueryInterface {
|
|
|
2061
2084
|
const meta = this.schema.tables[targetTable];
|
|
2062
2085
|
if (!meta)
|
|
2063
2086
|
return null;
|
|
2064
|
-
const qt =
|
|
2087
|
+
const qt = this.q(targetTable);
|
|
2065
2088
|
const conditions = [];
|
|
2066
2089
|
for (const [field, value] of Object.entries(subWhere)) {
|
|
2067
2090
|
if (value === undefined)
|
|
2068
2091
|
continue;
|
|
2069
2092
|
const col = meta.columnMap[field] ?? (0, schema_js_1.camelToSnake)(field);
|
|
2070
|
-
|
|
2093
|
+
if (!meta.allColumns.includes(col)) {
|
|
2094
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${field}" in relation filter for table "${targetTable}". ` +
|
|
2095
|
+
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
2096
|
+
}
|
|
2097
|
+
const qCol = `${qt}.${this.q(col)}`;
|
|
2071
2098
|
if (value === null) {
|
|
2072
2099
|
conditions.push(`${qCol} IS NULL`);
|
|
2073
2100
|
continue;
|
|
@@ -2078,7 +2105,7 @@ class QueryInterface {
|
|
|
2078
2105
|
continue;
|
|
2079
2106
|
}
|
|
2080
2107
|
params.push(value);
|
|
2081
|
-
conditions.push(`${qCol} =
|
|
2108
|
+
conditions.push(`${qCol} = ${this.p(params.length)}`);
|
|
2082
2109
|
}
|
|
2083
2110
|
return conditions.length > 0 ? conditions.join(' AND ') : null;
|
|
2084
2111
|
}
|
|
@@ -2090,19 +2117,19 @@ class QueryInterface {
|
|
|
2090
2117
|
const clauses = [];
|
|
2091
2118
|
if (op.gt !== undefined) {
|
|
2092
2119
|
params.push(op.gt);
|
|
2093
|
-
clauses.push(`${column} >
|
|
2120
|
+
clauses.push(`${column} > ${this.p(params.length)}`);
|
|
2094
2121
|
}
|
|
2095
2122
|
if (op.gte !== undefined) {
|
|
2096
2123
|
params.push(op.gte);
|
|
2097
|
-
clauses.push(`${column} >=
|
|
2124
|
+
clauses.push(`${column} >= ${this.p(params.length)}`);
|
|
2098
2125
|
}
|
|
2099
2126
|
if (op.lt !== undefined) {
|
|
2100
2127
|
params.push(op.lt);
|
|
2101
|
-
clauses.push(`${column} <
|
|
2128
|
+
clauses.push(`${column} < ${this.p(params.length)}`);
|
|
2102
2129
|
}
|
|
2103
2130
|
if (op.lte !== undefined) {
|
|
2104
2131
|
params.push(op.lte);
|
|
2105
|
-
clauses.push(`${column} <=
|
|
2132
|
+
clauses.push(`${column} <= ${this.p(params.length)}`);
|
|
2106
2133
|
}
|
|
2107
2134
|
if (op.not !== undefined) {
|
|
2108
2135
|
if (op.not === null) {
|
|
@@ -2110,30 +2137,29 @@ class QueryInterface {
|
|
|
2110
2137
|
}
|
|
2111
2138
|
else {
|
|
2112
2139
|
params.push(op.not);
|
|
2113
|
-
clauses.push(`${column} !=
|
|
2140
|
+
clauses.push(`${column} != ${this.p(params.length)}`);
|
|
2114
2141
|
}
|
|
2115
2142
|
}
|
|
2116
2143
|
if (op.in !== undefined) {
|
|
2117
2144
|
params.push(op.in);
|
|
2118
|
-
clauses.push(`${column} = ANY(
|
|
2145
|
+
clauses.push(`${column} = ANY(${this.p(params.length)})`);
|
|
2119
2146
|
}
|
|
2120
2147
|
if (op.notIn !== undefined) {
|
|
2121
2148
|
params.push(op.notIn);
|
|
2122
|
-
clauses.push(`${column} != ALL(
|
|
2149
|
+
clauses.push(`${column} != ALL(${this.p(params.length)})`);
|
|
2123
2150
|
}
|
|
2124
|
-
|
|
2125
|
-
const likeOp = op.mode === 'insensitive' ? 'ILIKE' : 'LIKE';
|
|
2151
|
+
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
2126
2152
|
if (op.contains !== undefined) {
|
|
2127
2153
|
params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
|
|
2128
|
-
clauses.push(`${
|
|
2154
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2129
2155
|
}
|
|
2130
2156
|
if (op.startsWith !== undefined) {
|
|
2131
2157
|
params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
|
|
2132
|
-
clauses.push(`${
|
|
2158
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2133
2159
|
}
|
|
2134
2160
|
if (op.endsWith !== undefined) {
|
|
2135
2161
|
params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
|
|
2136
|
-
clauses.push(`${
|
|
2162
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2137
2163
|
}
|
|
2138
2164
|
return clauses;
|
|
2139
2165
|
}
|
|
@@ -2293,8 +2319,8 @@ class QueryInterface {
|
|
|
2293
2319
|
if (!meta)
|
|
2294
2320
|
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}"`);
|
|
2295
2321
|
const cols = columnsList ?? meta.allColumns;
|
|
2296
|
-
const qtbl =
|
|
2297
|
-
const baseCols = cols.map((col) => `${qtbl}.${
|
|
2322
|
+
const qtbl = this.q(table);
|
|
2323
|
+
const baseCols = cols.map((col) => `${qtbl}.${this.q(col)}`).join(', ');
|
|
2298
2324
|
const relationSelects = [];
|
|
2299
2325
|
const aliasCounter = { n: 0 };
|
|
2300
2326
|
for (const [relName, relSpec] of Object.entries(withClause)) {
|
|
@@ -2305,7 +2331,7 @@ class QueryInterface {
|
|
|
2305
2331
|
}
|
|
2306
2332
|
// The main table is not aliased, so pass table name as parentRef
|
|
2307
2333
|
const subquery = this.buildRelationSubquery(relDef, relSpec, params, table, aliasCounter, depth, path);
|
|
2308
|
-
relationSelects.push(`(${subquery}) AS ${
|
|
2334
|
+
relationSelects.push(`(${subquery}) AS ${this.q(relName)}`);
|
|
2309
2335
|
}
|
|
2310
2336
|
return [baseCols, ...relationSelects].join(', ');
|
|
2311
2337
|
}
|
|
@@ -2367,7 +2393,7 @@ class QueryInterface {
|
|
|
2367
2393
|
* 8. **Parameter threading:** All user-supplied values (where filters, limit) are
|
|
2368
2394
|
* pushed to the shared `params` array with `$N` placeholders. No string
|
|
2369
2395
|
* interpolation of user data ever occurs -- all identifiers go through
|
|
2370
|
-
* `
|
|
2396
|
+
* `this.q()` and all values are parameterized.
|
|
2371
2397
|
*
|
|
2372
2398
|
* ### Example output (hasMany with nested relation)
|
|
2373
2399
|
* ```sql
|
|
@@ -2431,8 +2457,11 @@ class QueryInterface {
|
|
|
2431
2457
|
.map(([k]) => targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k)));
|
|
2432
2458
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
2433
2459
|
}
|
|
2434
|
-
// Build
|
|
2435
|
-
const jsonPairs = targetColumns.map((col) =>
|
|
2460
|
+
// Build JSON object pairs for resolved columns
|
|
2461
|
+
const jsonPairs = targetColumns.map((col) => [
|
|
2462
|
+
targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
2463
|
+
`${alias}.${this.q(col)}`,
|
|
2464
|
+
]);
|
|
2436
2465
|
// Determine if this hasMany will take the wrapped subquery path (LIMIT or ORDER BY).
|
|
2437
2466
|
// When wrapping, nested relations are built in the wrapped path referencing innerAlias,
|
|
2438
2467
|
// so we must NOT build them here (they would push orphaned params).
|
|
@@ -2448,14 +2477,14 @@ class QueryInterface {
|
|
|
2448
2477
|
// Recursively build nested subquery, passing THIS alias as the parent reference
|
|
2449
2478
|
const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
2450
2479
|
// Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
|
|
2451
|
-
const fallback = nestedRelDef.type === 'hasMany' ?
|
|
2452
|
-
jsonPairs.push(
|
|
2480
|
+
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
2481
|
+
jsonPairs.push([nestedRelName, `COALESCE((${nestedSubquery}), ${fallback})`]);
|
|
2453
2482
|
}
|
|
2454
2483
|
}
|
|
2455
|
-
const jsonObj =
|
|
2484
|
+
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|
|
2456
2485
|
// Quote parent ref — can be a table name or auto-generated alias
|
|
2457
|
-
const qParent =
|
|
2458
|
-
const qTarget =
|
|
2486
|
+
const qParent = this.q(parentRef);
|
|
2487
|
+
const qTarget = this.q(targetTable);
|
|
2459
2488
|
// Build ORDER BY for json_agg
|
|
2460
2489
|
let orderClause = '';
|
|
2461
2490
|
if (spec !== true && spec.orderBy) {
|
|
@@ -2466,7 +2495,7 @@ class QueryInterface {
|
|
|
2466
2495
|
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
|
|
2467
2496
|
}
|
|
2468
2497
|
const safeDir = dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
2469
|
-
return `${alias}.${
|
|
2498
|
+
return `${alias}.${this.q(col)} ${safeDir}`;
|
|
2470
2499
|
})
|
|
2471
2500
|
.join(', ');
|
|
2472
2501
|
orderClause = ` ORDER BY ${orders}`;
|
|
@@ -2474,12 +2503,13 @@ class QueryInterface {
|
|
|
2474
2503
|
// Build WHERE — correlate to parent via parentRef (alias or table name).
|
|
2475
2504
|
// For hasMany: target has FK, so alias.fk = parentRef.pk
|
|
2476
2505
|
// For belongsTo: source has FK, so alias.pk = parentRef.fk (reversed)
|
|
2506
|
+
// Supports composite foreign keys (string[]) via buildCorrelation.
|
|
2477
2507
|
let whereClause;
|
|
2478
2508
|
if (relDef.type === 'belongsTo' || relDef.type === 'hasOne') {
|
|
2479
|
-
whereClause =
|
|
2509
|
+
whereClause = this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
|
|
2480
2510
|
}
|
|
2481
2511
|
else {
|
|
2482
|
-
whereClause =
|
|
2512
|
+
whereClause = this.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
|
|
2483
2513
|
}
|
|
2484
2514
|
// Additional filters — properly parameterized
|
|
2485
2515
|
if (spec !== true && spec.where) {
|
|
@@ -2489,14 +2519,14 @@ class QueryInterface {
|
|
|
2489
2519
|
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in where for table "${targetTable}"`);
|
|
2490
2520
|
}
|
|
2491
2521
|
params.push(v);
|
|
2492
|
-
whereClause += ` AND ${alias}.${
|
|
2522
|
+
whereClause += ` AND ${alias}.${this.q(col)} = ${this.p(params.length)}`;
|
|
2493
2523
|
}
|
|
2494
2524
|
}
|
|
2495
2525
|
// LIMIT
|
|
2496
2526
|
let limitClause = '';
|
|
2497
2527
|
if (spec !== true && spec.limit) {
|
|
2498
2528
|
params.push(Number(spec.limit));
|
|
2499
|
-
limitClause = ` LIMIT
|
|
2529
|
+
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
2500
2530
|
}
|
|
2501
2531
|
if (relDef.type === 'hasMany') {
|
|
2502
2532
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -2505,9 +2535,12 @@ class QueryInterface {
|
|
|
2505
2535
|
const innerAlias = `${alias}i`;
|
|
2506
2536
|
// Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
|
|
2507
2537
|
// Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
|
|
2508
|
-
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${
|
|
2538
|
+
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${this.q(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
|
|
2509
2539
|
// For the json_build_object, reference the inner alias — only include resolved columns
|
|
2510
|
-
const innerJsonPairs = targetColumns.map((col) =>
|
|
2540
|
+
const innerJsonPairs = targetColumns.map((col) => [
|
|
2541
|
+
targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
2542
|
+
`${innerAlias}.${this.q(col)}`,
|
|
2543
|
+
]);
|
|
2511
2544
|
// Build nested relation subqueries referencing innerAlias
|
|
2512
2545
|
if (spec !== true && spec.with) {
|
|
2513
2546
|
for (const [nestedRelName, nestedSpec] of Object.entries(spec.with)) {
|
|
@@ -2517,14 +2550,14 @@ class QueryInterface {
|
|
|
2517
2550
|
`Available: ${Object.keys(targetMeta.relations).join(', ')}`);
|
|
2518
2551
|
}
|
|
2519
2552
|
const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
2520
|
-
const fallback = nestedRelDef.type === 'hasMany' ?
|
|
2521
|
-
innerJsonPairs.push(
|
|
2553
|
+
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
2554
|
+
innerJsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
|
|
2522
2555
|
}
|
|
2523
2556
|
}
|
|
2524
|
-
const innerJsonObj =
|
|
2525
|
-
return `SELECT
|
|
2557
|
+
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
2558
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
|
|
2526
2559
|
}
|
|
2527
|
-
return `SELECT
|
|
2560
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, orderClause.trim() || undefined)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
2528
2561
|
}
|
|
2529
2562
|
// belongsTo / hasOne — return single object
|
|
2530
2563
|
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
@@ -2571,22 +2604,22 @@ class QueryInterface {
|
|
|
2571
2604
|
params.push(filter.path);
|
|
2572
2605
|
const pathParam = params.length;
|
|
2573
2606
|
params.push(String(filter.equals));
|
|
2574
|
-
clauses.push(`${column
|
|
2607
|
+
clauses.push(`${this.dialect.buildJsonPathExtract(column, this.p(pathParam))} = ${this.p(params.length)}`);
|
|
2575
2608
|
}
|
|
2576
2609
|
else if (filter.equals !== undefined) {
|
|
2577
2610
|
// Containment equality: column @> $N::jsonb
|
|
2578
2611
|
params.push(JSON.stringify(filter.equals));
|
|
2579
|
-
clauses.push(
|
|
2612
|
+
clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
|
|
2580
2613
|
}
|
|
2581
2614
|
if (filter.contains !== undefined) {
|
|
2582
2615
|
// Containment: column @> $N::jsonb
|
|
2583
2616
|
params.push(JSON.stringify(filter.contains));
|
|
2584
|
-
clauses.push(
|
|
2617
|
+
clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
|
|
2585
2618
|
}
|
|
2586
2619
|
if (filter.hasKey !== undefined) {
|
|
2587
2620
|
// Key existence: column ? $N
|
|
2588
2621
|
params.push(filter.hasKey);
|
|
2589
|
-
clauses.push(`${column} ?
|
|
2622
|
+
clauses.push(`${column} ? ${this.p(params.length)}`);
|
|
2590
2623
|
}
|
|
2591
2624
|
return clauses;
|
|
2592
2625
|
}
|
|
@@ -2600,17 +2633,17 @@ class QueryInterface {
|
|
|
2600
2633
|
if (filter.has !== undefined) {
|
|
2601
2634
|
// value = ANY(column)
|
|
2602
2635
|
params.push(filter.has);
|
|
2603
|
-
clauses.push(
|
|
2636
|
+
clauses.push(`${this.p(params.length)} = ANY(${column})`);
|
|
2604
2637
|
}
|
|
2605
2638
|
if (filter.hasEvery !== undefined) {
|
|
2606
2639
|
// column @> ARRAY[...]::type[]
|
|
2607
2640
|
params.push(filter.hasEvery);
|
|
2608
|
-
clauses.push(`${column} @>
|
|
2641
|
+
clauses.push(`${column} @> ${this.p(params.length)}::${elementType}[]`);
|
|
2609
2642
|
}
|
|
2610
2643
|
if (filter.hasSome !== undefined) {
|
|
2611
2644
|
// column && ARRAY[...]::type[]
|
|
2612
2645
|
params.push(filter.hasSome);
|
|
2613
|
-
clauses.push(`${column} &&
|
|
2646
|
+
clauses.push(`${column} && ${this.p(params.length)}::${elementType}[]`);
|
|
2614
2647
|
}
|
|
2615
2648
|
if (filter.isEmpty === true) {
|
|
2616
2649
|
// array_length(column, 1) IS NULL
|