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
package/dist/query/builder.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Schema-driven: all column names, types, and relations come from introspected
|
|
11
11
|
* metadata — nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
|
+
import { postgresDialect } from '../dialect.js';
|
|
13
14
|
import { CircularRelationError, NotFoundError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
|
|
14
15
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
15
|
-
import { escapeLike,
|
|
16
|
+
import { escapeLike, LRUCache, OPERATOR_KEYS, sqlToPreparedName } from './utils.js';
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Internal detection helpers — used by QueryInterface
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
@@ -96,6 +97,7 @@ function findArrayUniqueKey(value) {
|
|
|
96
97
|
}
|
|
97
98
|
return null;
|
|
98
99
|
}
|
|
100
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
|
|
99
101
|
export class QueryInterface {
|
|
100
102
|
pool;
|
|
101
103
|
table;
|
|
@@ -108,6 +110,7 @@ export class QueryInterface {
|
|
|
108
110
|
warnOnUnlimited;
|
|
109
111
|
preparedStatementsEnabled;
|
|
110
112
|
sqlCacheEnabled;
|
|
113
|
+
dialect;
|
|
111
114
|
/**
|
|
112
115
|
* Tracks tables that have already triggered an unlimited-query warning so
|
|
113
116
|
* the user is not spammed once per row. Per-instance state — each
|
|
@@ -142,6 +145,7 @@ export class QueryInterface {
|
|
|
142
145
|
this.warnOnUnlimited = options?.warnOnUnlimited !== false;
|
|
143
146
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
144
147
|
this.sqlCacheEnabled = options?.sqlCache !== false;
|
|
148
|
+
this.dialect = options?.dialect ?? postgresDialect;
|
|
145
149
|
// Pre-compute column type lookup maps (TASK-26)
|
|
146
150
|
this.columnPgTypeMap = new Map();
|
|
147
151
|
this.columnArrayTypeMap = new Map();
|
|
@@ -150,6 +154,14 @@ export class QueryInterface {
|
|
|
150
154
|
this.columnArrayTypeMap.set(col.name, col.pgArrayType);
|
|
151
155
|
}
|
|
152
156
|
}
|
|
157
|
+
/** Quote an identifier through the active SQL dialect. */
|
|
158
|
+
q(name) {
|
|
159
|
+
return this.dialect.quoteIdentifier(name);
|
|
160
|
+
}
|
|
161
|
+
/** Return the active dialect's placeholder for a 1-indexed parameter position. */
|
|
162
|
+
p(index) {
|
|
163
|
+
return this.dialect.paramPlaceholder(index);
|
|
164
|
+
}
|
|
153
165
|
/**
|
|
154
166
|
* Return cache hit/miss statistics for this QueryInterface instance.
|
|
155
167
|
* Useful for monitoring and benchmarking.
|
|
@@ -258,6 +270,7 @@ export class QueryInterface {
|
|
|
258
270
|
// -------------------------------------------------------------------------
|
|
259
271
|
// findUnique
|
|
260
272
|
// -------------------------------------------------------------------------
|
|
273
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
261
274
|
async findUnique(args) {
|
|
262
275
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
263
276
|
const deferred = this.buildFindUnique(args);
|
|
@@ -265,6 +278,7 @@ export class QueryInterface {
|
|
|
265
278
|
return deferred.transform(result);
|
|
266
279
|
});
|
|
267
280
|
}
|
|
281
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
268
282
|
buildFindUnique(args) {
|
|
269
283
|
const columnsList = this.resolveColumns(args.select, args.omit);
|
|
270
284
|
const whereObj = args.where;
|
|
@@ -285,11 +299,11 @@ export class QueryInterface {
|
|
|
285
299
|
// Simple path: plain equality, no operators/null/OR
|
|
286
300
|
if (!args.with && isSimpleWhere) {
|
|
287
301
|
const entry = this.acquireSql(ck, () => {
|
|
288
|
-
const qt =
|
|
302
|
+
const qt = this.q(this.table);
|
|
289
303
|
const tempParams = whereKeys.map((k) => whereObj[k]);
|
|
290
|
-
const whereClauses = whereKeys.map((k, i) => `${this.toSqlColumn(k)} =
|
|
304
|
+
const whereClauses = whereKeys.map((k, i) => `${this.toSqlColumn(k)} = ${this.p(i + 1)}`);
|
|
291
305
|
const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
|
|
292
|
-
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${
|
|
306
|
+
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
293
307
|
void tempParams; // params are positional, SQL is value-invariant
|
|
294
308
|
return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
|
|
295
309
|
});
|
|
@@ -314,8 +328,8 @@ export class QueryInterface {
|
|
|
314
328
|
const freshParams = [];
|
|
315
329
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
316
330
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
317
|
-
const qt =
|
|
318
|
-
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${
|
|
331
|
+
const qt = this.q(this.table);
|
|
332
|
+
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
319
333
|
return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
|
|
320
334
|
});
|
|
321
335
|
// Collect params
|
|
@@ -341,7 +355,7 @@ export class QueryInterface {
|
|
|
341
355
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
342
356
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
343
357
|
const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
344
|
-
return `SELECT ${selectClause} FROM ${
|
|
358
|
+
return `SELECT ${selectClause} FROM ${this.q(this.table)}${whereSql} LIMIT 1`;
|
|
345
359
|
});
|
|
346
360
|
// Collect params in exact build order: where first, then with-clause relations
|
|
347
361
|
this.collectWhereParams(whereObj, params);
|
|
@@ -360,6 +374,7 @@ export class QueryInterface {
|
|
|
360
374
|
// -------------------------------------------------------------------------
|
|
361
375
|
// findMany
|
|
362
376
|
// -------------------------------------------------------------------------
|
|
377
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
363
378
|
async findMany(args) {
|
|
364
379
|
this.maybeWarnUnlimited(args);
|
|
365
380
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -417,6 +432,7 @@ export class QueryInterface {
|
|
|
417
432
|
}
|
|
418
433
|
return maxDepth;
|
|
419
434
|
}
|
|
435
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
420
436
|
buildFindMany(args) {
|
|
421
437
|
const columnsList = this.resolveColumns(args?.select, args?.omit);
|
|
422
438
|
const colKey = columnsList ? columnsList.join(',') : '*';
|
|
@@ -450,7 +466,7 @@ export class QueryInterface {
|
|
|
450
466
|
return { sql: clause ? ` WHERE ${clause}` : '' };
|
|
451
467
|
})()
|
|
452
468
|
: { sql: '' };
|
|
453
|
-
const qt =
|
|
469
|
+
const qt = this.q(this.table);
|
|
454
470
|
let distinctPrefix = '';
|
|
455
471
|
if (args?.distinct && args.distinct.length > 0) {
|
|
456
472
|
const distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
|
|
@@ -461,7 +477,7 @@ export class QueryInterface {
|
|
|
461
477
|
selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
|
|
462
478
|
}
|
|
463
479
|
else if (columnsList) {
|
|
464
|
-
selectClause = columnsList.map((c) => `${qt}.${
|
|
480
|
+
selectClause = columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ');
|
|
465
481
|
}
|
|
466
482
|
else {
|
|
467
483
|
selectClause = `${qt}.*`;
|
|
@@ -475,7 +491,7 @@ export class QueryInterface {
|
|
|
475
491
|
const dir = args.orderBy?.[k] ?? 'asc';
|
|
476
492
|
const op = dir === 'desc' ? '<' : '>';
|
|
477
493
|
freshParams.push(v);
|
|
478
|
-
return `${qt}.${col} ${op}
|
|
494
|
+
return `${qt}.${col} ${op} ${this.p(freshParams.length)}`;
|
|
479
495
|
});
|
|
480
496
|
if (freshWhereSql) {
|
|
481
497
|
sql += ` AND ${cursorConditions.join(' AND ')}`;
|
|
@@ -490,11 +506,11 @@ export class QueryInterface {
|
|
|
490
506
|
}
|
|
491
507
|
if (effectiveLimit !== undefined) {
|
|
492
508
|
freshParams.push(Number(effectiveLimit));
|
|
493
|
-
sql += ` LIMIT
|
|
509
|
+
sql += ` LIMIT ${this.p(freshParams.length)}`;
|
|
494
510
|
}
|
|
495
511
|
if (args?.offset !== undefined) {
|
|
496
512
|
freshParams.push(Number(args.offset));
|
|
497
|
-
sql += ` OFFSET
|
|
513
|
+
sql += ` OFFSET ${this.p(freshParams.length)}`;
|
|
498
514
|
}
|
|
499
515
|
return sql;
|
|
500
516
|
});
|
|
@@ -560,6 +576,7 @@ export class QueryInterface {
|
|
|
560
576
|
* }
|
|
561
577
|
* ```
|
|
562
578
|
*/
|
|
579
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
563
580
|
async *findManyStream(args) {
|
|
564
581
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
565
582
|
const hasRelations = !!args?.with;
|
|
@@ -581,7 +598,7 @@ export class QueryInterface {
|
|
|
581
598
|
// Acquire a dedicated connection — cursors require a single connection in a transaction
|
|
582
599
|
const client = await this.pool.connect();
|
|
583
600
|
const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
584
|
-
const quotedCursor =
|
|
601
|
+
const quotedCursor = this.q(cursorName);
|
|
585
602
|
try {
|
|
586
603
|
await client.query('BEGIN');
|
|
587
604
|
await client.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${deferred.sql}`, deferred.params);
|
|
@@ -616,6 +633,7 @@ export class QueryInterface {
|
|
|
616
633
|
// -------------------------------------------------------------------------
|
|
617
634
|
// findFirst — like findMany but returns a single row or null
|
|
618
635
|
// -------------------------------------------------------------------------
|
|
636
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
619
637
|
async findFirst(args) {
|
|
620
638
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
621
639
|
const deferred = this.buildFindFirst(args);
|
|
@@ -623,6 +641,7 @@ export class QueryInterface {
|
|
|
623
641
|
return deferred.transform(result);
|
|
624
642
|
});
|
|
625
643
|
}
|
|
644
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
626
645
|
buildFindFirst(args) {
|
|
627
646
|
// Reuse findMany's SQL builder but force LIMIT 1
|
|
628
647
|
const findManyArgs = { ...args, limit: 1 };
|
|
@@ -640,6 +659,7 @@ export class QueryInterface {
|
|
|
640
659
|
// -------------------------------------------------------------------------
|
|
641
660
|
// findFirstOrThrow — like findFirst but throws if no record found
|
|
642
661
|
// -------------------------------------------------------------------------
|
|
662
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
643
663
|
async findFirstOrThrow(args) {
|
|
644
664
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
645
665
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
@@ -647,6 +667,7 @@ export class QueryInterface {
|
|
|
647
667
|
return deferred.transform(result);
|
|
648
668
|
});
|
|
649
669
|
}
|
|
670
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
650
671
|
buildFindFirstOrThrow(args) {
|
|
651
672
|
const inner = this.buildFindFirst(args);
|
|
652
673
|
return {
|
|
@@ -669,6 +690,7 @@ export class QueryInterface {
|
|
|
669
690
|
// -------------------------------------------------------------------------
|
|
670
691
|
// findUniqueOrThrow — like findUnique but throws if no record found
|
|
671
692
|
// -------------------------------------------------------------------------
|
|
693
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
672
694
|
async findUniqueOrThrow(args) {
|
|
673
695
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
674
696
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
@@ -676,6 +698,7 @@ export class QueryInterface {
|
|
|
676
698
|
return deferred.transform(result);
|
|
677
699
|
});
|
|
678
700
|
}
|
|
701
|
+
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
679
702
|
buildFindUniqueOrThrow(args) {
|
|
680
703
|
const inner = this.buildFindUnique(args);
|
|
681
704
|
return {
|
|
@@ -709,8 +732,8 @@ export class QueryInterface {
|
|
|
709
732
|
const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
|
|
710
733
|
const columns = entries.map(([k]) => this.toSqlColumn(k));
|
|
711
734
|
const params = entries.map(([, v]) => v);
|
|
712
|
-
const placeholders = entries.map((_, i) =>
|
|
713
|
-
const sql = `INSERT INTO ${
|
|
735
|
+
const placeholders = entries.map((_, i) => `${this.p(i + 1)}`);
|
|
736
|
+
const sql = `INSERT INTO ${this.q(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
|
|
714
737
|
return {
|
|
715
738
|
sql,
|
|
716
739
|
params,
|
|
@@ -739,7 +762,7 @@ export class QueryInterface {
|
|
|
739
762
|
});
|
|
740
763
|
}
|
|
741
764
|
buildCreateMany(args) {
|
|
742
|
-
const qt =
|
|
765
|
+
const qt = this.q(this.table);
|
|
743
766
|
if (args.data.length === 0) {
|
|
744
767
|
return {
|
|
745
768
|
sql: `SELECT * FROM ${qt} WHERE false`,
|
|
@@ -760,8 +783,8 @@ export class QueryInterface {
|
|
|
760
783
|
}
|
|
761
784
|
// Use actual Postgres types for array casts
|
|
762
785
|
const typeCasts = columns.map((col) => this.getColumnArrayType(col));
|
|
763
|
-
const unnestArgs = columnArrays.map((_, i) =>
|
|
764
|
-
const quotedColumns = columns.map((c) =>
|
|
786
|
+
const unnestArgs = columnArrays.map((_, i) => `${this.p(i + 1)}::${typeCasts[i]}`);
|
|
787
|
+
const quotedColumns = columns.map((c) => this.q(c));
|
|
765
788
|
let sql = `INSERT INTO ${qt} (${quotedColumns.join(', ')}) SELECT * FROM UNNEST(${unnestArgs.join(', ')})`;
|
|
766
789
|
// skipDuplicates: add ON CONFLICT DO NOTHING
|
|
767
790
|
if (args.skipDuplicates) {
|
|
@@ -799,7 +822,7 @@ export class QueryInterface {
|
|
|
799
822
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
800
823
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
801
824
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
802
|
-
return `UPDATE ${
|
|
825
|
+
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
|
|
803
826
|
});
|
|
804
827
|
// On cache hit, validate predicate
|
|
805
828
|
if (whereFp === '') {
|
|
@@ -848,7 +871,7 @@ export class QueryInterface {
|
|
|
848
871
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
849
872
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
850
873
|
this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
|
|
851
|
-
return `DELETE FROM ${
|
|
874
|
+
return `DELETE FROM ${this.q(this.table)}${whereSql} RETURNING *`;
|
|
852
875
|
});
|
|
853
876
|
// On cache hit, still validate the predicate
|
|
854
877
|
if (whereFp === '') {
|
|
@@ -888,7 +911,7 @@ export class QueryInterface {
|
|
|
888
911
|
const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
|
|
889
912
|
const columns = createEntries.map(([k]) => this.toSqlColumn(k));
|
|
890
913
|
const createParams = createEntries.map(([, v]) => v);
|
|
891
|
-
const placeholders = createEntries.map((_, i) =>
|
|
914
|
+
const placeholders = createEntries.map((_, i) => `${this.p(i + 1)}`);
|
|
892
915
|
// The conflict target comes from `where` keys — must be unique/PK columns
|
|
893
916
|
const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
|
|
894
917
|
const conflictColumns = conflictKeys.map((k) => this.toSqlColumn(k));
|
|
@@ -896,13 +919,13 @@ export class QueryInterface {
|
|
|
896
919
|
const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
|
|
897
920
|
let paramIdx = createParams.length + 1;
|
|
898
921
|
const setClauses = updateEntries.map(([k]) => {
|
|
899
|
-
const clause = `${this.toSqlColumn(k)} =
|
|
922
|
+
const clause = `${this.toSqlColumn(k)} = ${this.p(paramIdx)}`;
|
|
900
923
|
paramIdx++;
|
|
901
924
|
return clause;
|
|
902
925
|
});
|
|
903
926
|
const updateParams = updateEntries.map(([, v]) => v);
|
|
904
927
|
const params = [...createParams, ...updateParams];
|
|
905
|
-
const sql = `INSERT INTO ${
|
|
928
|
+
const sql = `INSERT INTO ${this.q(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
|
|
906
929
|
` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${setClauses.join(', ')}` +
|
|
907
930
|
` RETURNING *`;
|
|
908
931
|
return {
|
|
@@ -947,7 +970,7 @@ export class QueryInterface {
|
|
|
947
970
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
948
971
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
949
972
|
this.assertMutationHasPredicate('updateMany', whereSql, args.allowFullTableScan);
|
|
950
|
-
return `UPDATE ${
|
|
973
|
+
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
|
|
951
974
|
});
|
|
952
975
|
if (whereFp === '') {
|
|
953
976
|
this.assertMutationHasPredicate('updateMany', '', args.allowFullTableScan);
|
|
@@ -982,7 +1005,7 @@ export class QueryInterface {
|
|
|
982
1005
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
983
1006
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
984
1007
|
this.assertMutationHasPredicate('deleteMany', whereSql, args.allowFullTableScan);
|
|
985
|
-
return `DELETE FROM ${
|
|
1008
|
+
return `DELETE FROM ${this.q(this.table)}${whereSql}`;
|
|
986
1009
|
});
|
|
987
1010
|
if (whereFp === '') {
|
|
988
1011
|
this.assertMutationHasPredicate('deleteMany', '', args.allowFullTableScan);
|
|
@@ -1015,7 +1038,7 @@ export class QueryInterface {
|
|
|
1015
1038
|
const freshParams = [];
|
|
1016
1039
|
const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
|
|
1017
1040
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1018
|
-
return `SELECT COUNT(*)::int AS count FROM ${
|
|
1041
|
+
return `SELECT COUNT(*)::int AS count FROM ${this.q(this.table)}${whereSql}`;
|
|
1019
1042
|
});
|
|
1020
1043
|
if (args?.where) {
|
|
1021
1044
|
this.collectWhereParams(whereObj, params);
|
|
@@ -1048,7 +1071,7 @@ export class QueryInterface {
|
|
|
1048
1071
|
}
|
|
1049
1072
|
}
|
|
1050
1073
|
const groupColsRaw = args.by.map((k) => this.toColumn(k));
|
|
1051
|
-
const groupCols = groupColsRaw.map((c) =>
|
|
1074
|
+
const groupCols = groupColsRaw.map((c) => this.q(c));
|
|
1052
1075
|
const { sql: whereSql, params } = args.where ? this.buildWhere(args.where) : { sql: '', params: [] };
|
|
1053
1076
|
// Build SELECT expressions: group-by columns + aggregate functions
|
|
1054
1077
|
const selectExprs = [...groupCols];
|
|
@@ -1062,7 +1085,7 @@ export class QueryInterface {
|
|
|
1062
1085
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
1063
1086
|
if (enabled) {
|
|
1064
1087
|
const col = this.toColumn(field);
|
|
1065
|
-
selectExprs.push(`SUM(${
|
|
1088
|
+
selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
|
|
1066
1089
|
}
|
|
1067
1090
|
}
|
|
1068
1091
|
}
|
|
@@ -1071,7 +1094,7 @@ export class QueryInterface {
|
|
|
1071
1094
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1072
1095
|
if (enabled) {
|
|
1073
1096
|
const col = this.toColumn(field);
|
|
1074
|
-
selectExprs.push(`AVG(${
|
|
1097
|
+
selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
|
|
1075
1098
|
}
|
|
1076
1099
|
}
|
|
1077
1100
|
}
|
|
@@ -1080,7 +1103,7 @@ export class QueryInterface {
|
|
|
1080
1103
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
1081
1104
|
if (enabled) {
|
|
1082
1105
|
const col = this.toColumn(field);
|
|
1083
|
-
selectExprs.push(`MIN(${
|
|
1106
|
+
selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
|
|
1084
1107
|
}
|
|
1085
1108
|
}
|
|
1086
1109
|
}
|
|
@@ -1089,11 +1112,11 @@ export class QueryInterface {
|
|
|
1089
1112
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
1090
1113
|
if (enabled) {
|
|
1091
1114
|
const col = this.toColumn(field);
|
|
1092
|
-
selectExprs.push(`MAX(${
|
|
1115
|
+
selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
|
|
1093
1116
|
}
|
|
1094
1117
|
}
|
|
1095
1118
|
}
|
|
1096
|
-
let sql = `SELECT ${selectExprs.join(', ')} FROM ${
|
|
1119
|
+
let sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
|
|
1097
1120
|
// ORDER BY
|
|
1098
1121
|
if (args.orderBy) {
|
|
1099
1122
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
|
|
@@ -1201,7 +1224,7 @@ export class QueryInterface {
|
|
|
1201
1224
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
1202
1225
|
if (enabled) {
|
|
1203
1226
|
const col = this.toColumn(field);
|
|
1204
|
-
selectExprs.push(`COUNT(${
|
|
1227
|
+
selectExprs.push(`COUNT(${this.q(col)})::int AS ${this.q(`_count_${col}`)}`);
|
|
1205
1228
|
}
|
|
1206
1229
|
}
|
|
1207
1230
|
}
|
|
@@ -1210,7 +1233,7 @@ export class QueryInterface {
|
|
|
1210
1233
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
1211
1234
|
if (enabled) {
|
|
1212
1235
|
const col = this.toColumn(field);
|
|
1213
|
-
selectExprs.push(`SUM(${
|
|
1236
|
+
selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
|
|
1214
1237
|
}
|
|
1215
1238
|
}
|
|
1216
1239
|
}
|
|
@@ -1219,7 +1242,7 @@ export class QueryInterface {
|
|
|
1219
1242
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1220
1243
|
if (enabled) {
|
|
1221
1244
|
const col = this.toColumn(field);
|
|
1222
|
-
selectExprs.push(`AVG(${
|
|
1245
|
+
selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
|
|
1223
1246
|
}
|
|
1224
1247
|
}
|
|
1225
1248
|
}
|
|
@@ -1228,7 +1251,7 @@ export class QueryInterface {
|
|
|
1228
1251
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
1229
1252
|
if (enabled) {
|
|
1230
1253
|
const col = this.toColumn(field);
|
|
1231
|
-
selectExprs.push(`MIN(${
|
|
1254
|
+
selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
|
|
1232
1255
|
}
|
|
1233
1256
|
}
|
|
1234
1257
|
}
|
|
@@ -1237,14 +1260,14 @@ export class QueryInterface {
|
|
|
1237
1260
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
1238
1261
|
if (enabled) {
|
|
1239
1262
|
const col = this.toColumn(field);
|
|
1240
|
-
selectExprs.push(`MAX(${
|
|
1263
|
+
selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
|
|
1241
1264
|
}
|
|
1242
1265
|
}
|
|
1243
1266
|
}
|
|
1244
1267
|
if (selectExprs.length === 0) {
|
|
1245
1268
|
selectExprs.push('COUNT(*)::int AS _count');
|
|
1246
1269
|
}
|
|
1247
|
-
const sql = `SELECT ${selectExprs.join(', ')} FROM ${
|
|
1270
|
+
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
|
|
1248
1271
|
return {
|
|
1249
1272
|
sql,
|
|
1250
1273
|
params,
|
|
@@ -1361,7 +1384,7 @@ export class QueryInterface {
|
|
|
1361
1384
|
}
|
|
1362
1385
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
1363
1386
|
toSqlColumn(field) {
|
|
1364
|
-
return
|
|
1387
|
+
return this.q(this.toColumn(field));
|
|
1365
1388
|
}
|
|
1366
1389
|
/**
|
|
1367
1390
|
* Build a single SET clause entry for update/updateMany.
|
|
@@ -1394,7 +1417,7 @@ export class QueryInterface {
|
|
|
1394
1417
|
const opValue = v[op];
|
|
1395
1418
|
if (op === 'set') {
|
|
1396
1419
|
params.push(opValue);
|
|
1397
|
-
return `${col} =
|
|
1420
|
+
return `${col} = ${this.p(params.length)}`;
|
|
1398
1421
|
}
|
|
1399
1422
|
// Arithmetic operators: must be finite numbers
|
|
1400
1423
|
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
@@ -1402,19 +1425,19 @@ export class QueryInterface {
|
|
|
1402
1425
|
}
|
|
1403
1426
|
if (op === 'increment') {
|
|
1404
1427
|
params.push(opValue);
|
|
1405
|
-
return `${col} = ${col} +
|
|
1428
|
+
return `${col} = ${col} + ${this.p(params.length)}`;
|
|
1406
1429
|
}
|
|
1407
1430
|
if (op === 'decrement') {
|
|
1408
1431
|
params.push(opValue);
|
|
1409
|
-
return `${col} = ${col} -
|
|
1432
|
+
return `${col} = ${col} - ${this.p(params.length)}`;
|
|
1410
1433
|
}
|
|
1411
1434
|
if (op === 'multiply') {
|
|
1412
1435
|
params.push(opValue);
|
|
1413
|
-
return `${col} = ${col} *
|
|
1436
|
+
return `${col} = ${col} * ${this.p(params.length)}`;
|
|
1414
1437
|
}
|
|
1415
1438
|
if (op === 'divide') {
|
|
1416
1439
|
params.push(opValue);
|
|
1417
|
-
return `${col} = ${col} /
|
|
1440
|
+
return `${col} = ${col} / ${this.p(params.length)}`;
|
|
1418
1441
|
}
|
|
1419
1442
|
}
|
|
1420
1443
|
// Fall through: multi-key objects or non-operator single-key objects
|
|
@@ -1422,7 +1445,7 @@ export class QueryInterface {
|
|
|
1422
1445
|
}
|
|
1423
1446
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
1424
1447
|
params.push(value);
|
|
1425
|
-
return `${col} =
|
|
1448
|
+
return `${col} = ${this.p(params.length)}`;
|
|
1426
1449
|
}
|
|
1427
1450
|
// =========================================================================
|
|
1428
1451
|
// Fingerprinting — value-invariant shape keys for SQL cache lookup
|
|
@@ -1945,7 +1968,7 @@ export class QueryInterface {
|
|
|
1945
1968
|
}
|
|
1946
1969
|
}
|
|
1947
1970
|
const rawColumn = this.toColumn(key);
|
|
1948
|
-
const column =
|
|
1971
|
+
const column = this.q(rawColumn);
|
|
1949
1972
|
// Handle null → IS NULL
|
|
1950
1973
|
if (value === null) {
|
|
1951
1974
|
andClauses.push(`${column} IS NULL`);
|
|
@@ -1995,7 +2018,7 @@ export class QueryInterface {
|
|
|
1995
2018
|
}
|
|
1996
2019
|
// Plain equality
|
|
1997
2020
|
params.push(value);
|
|
1998
|
-
andClauses.push(`${column} =
|
|
2021
|
+
andClauses.push(`${column} = ${this.p(params.length)}`);
|
|
1999
2022
|
}
|
|
2000
2023
|
if (andClauses.length === 0)
|
|
2001
2024
|
return null;
|
|
@@ -2010,18 +2033,18 @@ export class QueryInterface {
|
|
|
2010
2033
|
const targetMeta = this.schema.tables[targetTable];
|
|
2011
2034
|
if (!targetMeta)
|
|
2012
2035
|
return null;
|
|
2013
|
-
const qt =
|
|
2014
|
-
const qSelf =
|
|
2036
|
+
const qt = this.q(targetTable);
|
|
2037
|
+
const qSelf = this.q(this.table);
|
|
2015
2038
|
const clauses = [];
|
|
2016
|
-
// Correlation: link child table to parent table
|
|
2039
|
+
// Correlation: link child table to parent table (supports composite FKs)
|
|
2017
2040
|
let correlation;
|
|
2018
2041
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
2019
2042
|
// parent.pk = child.fk
|
|
2020
|
-
correlation =
|
|
2043
|
+
correlation = this.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
|
|
2021
2044
|
}
|
|
2022
2045
|
else {
|
|
2023
2046
|
// belongsTo: parent.fk = child.pk
|
|
2024
|
-
correlation =
|
|
2047
|
+
correlation = this.dialect.buildCorrelation(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
|
|
2025
2048
|
}
|
|
2026
2049
|
// "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
|
|
2027
2050
|
if (filterObj.some !== undefined) {
|
|
@@ -2058,13 +2081,17 @@ export class QueryInterface {
|
|
|
2058
2081
|
const meta = this.schema.tables[targetTable];
|
|
2059
2082
|
if (!meta)
|
|
2060
2083
|
return null;
|
|
2061
|
-
const qt =
|
|
2084
|
+
const qt = this.q(targetTable);
|
|
2062
2085
|
const conditions = [];
|
|
2063
2086
|
for (const [field, value] of Object.entries(subWhere)) {
|
|
2064
2087
|
if (value === undefined)
|
|
2065
2088
|
continue;
|
|
2066
2089
|
const col = meta.columnMap[field] ?? camelToSnake(field);
|
|
2067
|
-
|
|
2090
|
+
if (!meta.allColumns.includes(col)) {
|
|
2091
|
+
throw new ValidationError(`[turbine] Unknown field "${field}" in relation filter for table "${targetTable}". ` +
|
|
2092
|
+
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
2093
|
+
}
|
|
2094
|
+
const qCol = `${qt}.${this.q(col)}`;
|
|
2068
2095
|
if (value === null) {
|
|
2069
2096
|
conditions.push(`${qCol} IS NULL`);
|
|
2070
2097
|
continue;
|
|
@@ -2075,7 +2102,7 @@ export class QueryInterface {
|
|
|
2075
2102
|
continue;
|
|
2076
2103
|
}
|
|
2077
2104
|
params.push(value);
|
|
2078
|
-
conditions.push(`${qCol} =
|
|
2105
|
+
conditions.push(`${qCol} = ${this.p(params.length)}`);
|
|
2079
2106
|
}
|
|
2080
2107
|
return conditions.length > 0 ? conditions.join(' AND ') : null;
|
|
2081
2108
|
}
|
|
@@ -2087,19 +2114,19 @@ export class QueryInterface {
|
|
|
2087
2114
|
const clauses = [];
|
|
2088
2115
|
if (op.gt !== undefined) {
|
|
2089
2116
|
params.push(op.gt);
|
|
2090
|
-
clauses.push(`${column} >
|
|
2117
|
+
clauses.push(`${column} > ${this.p(params.length)}`);
|
|
2091
2118
|
}
|
|
2092
2119
|
if (op.gte !== undefined) {
|
|
2093
2120
|
params.push(op.gte);
|
|
2094
|
-
clauses.push(`${column} >=
|
|
2121
|
+
clauses.push(`${column} >= ${this.p(params.length)}`);
|
|
2095
2122
|
}
|
|
2096
2123
|
if (op.lt !== undefined) {
|
|
2097
2124
|
params.push(op.lt);
|
|
2098
|
-
clauses.push(`${column} <
|
|
2125
|
+
clauses.push(`${column} < ${this.p(params.length)}`);
|
|
2099
2126
|
}
|
|
2100
2127
|
if (op.lte !== undefined) {
|
|
2101
2128
|
params.push(op.lte);
|
|
2102
|
-
clauses.push(`${column} <=
|
|
2129
|
+
clauses.push(`${column} <= ${this.p(params.length)}`);
|
|
2103
2130
|
}
|
|
2104
2131
|
if (op.not !== undefined) {
|
|
2105
2132
|
if (op.not === null) {
|
|
@@ -2107,30 +2134,29 @@ export class QueryInterface {
|
|
|
2107
2134
|
}
|
|
2108
2135
|
else {
|
|
2109
2136
|
params.push(op.not);
|
|
2110
|
-
clauses.push(`${column} !=
|
|
2137
|
+
clauses.push(`${column} != ${this.p(params.length)}`);
|
|
2111
2138
|
}
|
|
2112
2139
|
}
|
|
2113
2140
|
if (op.in !== undefined) {
|
|
2114
2141
|
params.push(op.in);
|
|
2115
|
-
clauses.push(`${column} = ANY(
|
|
2142
|
+
clauses.push(`${column} = ANY(${this.p(params.length)})`);
|
|
2116
2143
|
}
|
|
2117
2144
|
if (op.notIn !== undefined) {
|
|
2118
2145
|
params.push(op.notIn);
|
|
2119
|
-
clauses.push(`${column} != ALL(
|
|
2146
|
+
clauses.push(`${column} != ALL(${this.p(params.length)})`);
|
|
2120
2147
|
}
|
|
2121
|
-
|
|
2122
|
-
const likeOp = op.mode === 'insensitive' ? 'ILIKE' : 'LIKE';
|
|
2148
|
+
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
2123
2149
|
if (op.contains !== undefined) {
|
|
2124
2150
|
params.push(`%${escapeLike(op.contains)}%`);
|
|
2125
|
-
clauses.push(`${
|
|
2151
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2126
2152
|
}
|
|
2127
2153
|
if (op.startsWith !== undefined) {
|
|
2128
2154
|
params.push(`${escapeLike(op.startsWith)}%`);
|
|
2129
|
-
clauses.push(`${
|
|
2155
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2130
2156
|
}
|
|
2131
2157
|
if (op.endsWith !== undefined) {
|
|
2132
2158
|
params.push(`%${escapeLike(op.endsWith)}`);
|
|
2133
|
-
clauses.push(`${
|
|
2159
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2134
2160
|
}
|
|
2135
2161
|
return clauses;
|
|
2136
2162
|
}
|
|
@@ -2290,8 +2316,8 @@ export class QueryInterface {
|
|
|
2290
2316
|
if (!meta)
|
|
2291
2317
|
throw new ValidationError(`[turbine] Unknown table "${table}"`);
|
|
2292
2318
|
const cols = columnsList ?? meta.allColumns;
|
|
2293
|
-
const qtbl =
|
|
2294
|
-
const baseCols = cols.map((col) => `${qtbl}.${
|
|
2319
|
+
const qtbl = this.q(table);
|
|
2320
|
+
const baseCols = cols.map((col) => `${qtbl}.${this.q(col)}`).join(', ');
|
|
2295
2321
|
const relationSelects = [];
|
|
2296
2322
|
const aliasCounter = { n: 0 };
|
|
2297
2323
|
for (const [relName, relSpec] of Object.entries(withClause)) {
|
|
@@ -2302,7 +2328,7 @@ export class QueryInterface {
|
|
|
2302
2328
|
}
|
|
2303
2329
|
// The main table is not aliased, so pass table name as parentRef
|
|
2304
2330
|
const subquery = this.buildRelationSubquery(relDef, relSpec, params, table, aliasCounter, depth, path);
|
|
2305
|
-
relationSelects.push(`(${subquery}) AS ${
|
|
2331
|
+
relationSelects.push(`(${subquery}) AS ${this.q(relName)}`);
|
|
2306
2332
|
}
|
|
2307
2333
|
return [baseCols, ...relationSelects].join(', ');
|
|
2308
2334
|
}
|
|
@@ -2364,7 +2390,7 @@ export class QueryInterface {
|
|
|
2364
2390
|
* 8. **Parameter threading:** All user-supplied values (where filters, limit) are
|
|
2365
2391
|
* pushed to the shared `params` array with `$N` placeholders. No string
|
|
2366
2392
|
* interpolation of user data ever occurs -- all identifiers go through
|
|
2367
|
-
* `
|
|
2393
|
+
* `this.q()` and all values are parameterized.
|
|
2368
2394
|
*
|
|
2369
2395
|
* ### Example output (hasMany with nested relation)
|
|
2370
2396
|
* ```sql
|
|
@@ -2428,8 +2454,11 @@ export class QueryInterface {
|
|
|
2428
2454
|
.map(([k]) => targetMeta.columnMap[k] ?? camelToSnake(k)));
|
|
2429
2455
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
2430
2456
|
}
|
|
2431
|
-
// Build
|
|
2432
|
-
const jsonPairs = targetColumns.map((col) =>
|
|
2457
|
+
// Build JSON object pairs for resolved columns
|
|
2458
|
+
const jsonPairs = targetColumns.map((col) => [
|
|
2459
|
+
targetMeta.reverseColumnMap[col] ?? snakeToCamel(col),
|
|
2460
|
+
`${alias}.${this.q(col)}`,
|
|
2461
|
+
]);
|
|
2433
2462
|
// Determine if this hasMany will take the wrapped subquery path (LIMIT or ORDER BY).
|
|
2434
2463
|
// When wrapping, nested relations are built in the wrapped path referencing innerAlias,
|
|
2435
2464
|
// so we must NOT build them here (they would push orphaned params).
|
|
@@ -2445,14 +2474,14 @@ export class QueryInterface {
|
|
|
2445
2474
|
// Recursively build nested subquery, passing THIS alias as the parent reference
|
|
2446
2475
|
const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
2447
2476
|
// Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
|
|
2448
|
-
const fallback = nestedRelDef.type === 'hasMany' ?
|
|
2449
|
-
jsonPairs.push(
|
|
2477
|
+
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
2478
|
+
jsonPairs.push([nestedRelName, `COALESCE((${nestedSubquery}), ${fallback})`]);
|
|
2450
2479
|
}
|
|
2451
2480
|
}
|
|
2452
|
-
const jsonObj =
|
|
2481
|
+
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|
|
2453
2482
|
// Quote parent ref — can be a table name or auto-generated alias
|
|
2454
|
-
const qParent =
|
|
2455
|
-
const qTarget =
|
|
2483
|
+
const qParent = this.q(parentRef);
|
|
2484
|
+
const qTarget = this.q(targetTable);
|
|
2456
2485
|
// Build ORDER BY for json_agg
|
|
2457
2486
|
let orderClause = '';
|
|
2458
2487
|
if (spec !== true && spec.orderBy) {
|
|
@@ -2463,7 +2492,7 @@ export class QueryInterface {
|
|
|
2463
2492
|
throw new ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
|
|
2464
2493
|
}
|
|
2465
2494
|
const safeDir = dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
2466
|
-
return `${alias}.${
|
|
2495
|
+
return `${alias}.${this.q(col)} ${safeDir}`;
|
|
2467
2496
|
})
|
|
2468
2497
|
.join(', ');
|
|
2469
2498
|
orderClause = ` ORDER BY ${orders}`;
|
|
@@ -2471,12 +2500,13 @@ export class QueryInterface {
|
|
|
2471
2500
|
// Build WHERE — correlate to parent via parentRef (alias or table name).
|
|
2472
2501
|
// For hasMany: target has FK, so alias.fk = parentRef.pk
|
|
2473
2502
|
// For belongsTo: source has FK, so alias.pk = parentRef.fk (reversed)
|
|
2503
|
+
// Supports composite foreign keys (string[]) via buildCorrelation.
|
|
2474
2504
|
let whereClause;
|
|
2475
2505
|
if (relDef.type === 'belongsTo' || relDef.type === 'hasOne') {
|
|
2476
|
-
whereClause =
|
|
2506
|
+
whereClause = this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
|
|
2477
2507
|
}
|
|
2478
2508
|
else {
|
|
2479
|
-
whereClause =
|
|
2509
|
+
whereClause = this.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
|
|
2480
2510
|
}
|
|
2481
2511
|
// Additional filters — properly parameterized
|
|
2482
2512
|
if (spec !== true && spec.where) {
|
|
@@ -2486,14 +2516,14 @@ export class QueryInterface {
|
|
|
2486
2516
|
throw new ValidationError(`[turbine] Unknown column "${k}" in where for table "${targetTable}"`);
|
|
2487
2517
|
}
|
|
2488
2518
|
params.push(v);
|
|
2489
|
-
whereClause += ` AND ${alias}.${
|
|
2519
|
+
whereClause += ` AND ${alias}.${this.q(col)} = ${this.p(params.length)}`;
|
|
2490
2520
|
}
|
|
2491
2521
|
}
|
|
2492
2522
|
// LIMIT
|
|
2493
2523
|
let limitClause = '';
|
|
2494
2524
|
if (spec !== true && spec.limit) {
|
|
2495
2525
|
params.push(Number(spec.limit));
|
|
2496
|
-
limitClause = ` LIMIT
|
|
2526
|
+
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
2497
2527
|
}
|
|
2498
2528
|
if (relDef.type === 'hasMany') {
|
|
2499
2529
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -2502,9 +2532,12 @@ export class QueryInterface {
|
|
|
2502
2532
|
const innerAlias = `${alias}i`;
|
|
2503
2533
|
// Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
|
|
2504
2534
|
// Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
|
|
2505
|
-
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${
|
|
2535
|
+
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${this.q(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
|
|
2506
2536
|
// For the json_build_object, reference the inner alias — only include resolved columns
|
|
2507
|
-
const innerJsonPairs = targetColumns.map((col) =>
|
|
2537
|
+
const innerJsonPairs = targetColumns.map((col) => [
|
|
2538
|
+
targetMeta.reverseColumnMap[col] ?? snakeToCamel(col),
|
|
2539
|
+
`${innerAlias}.${this.q(col)}`,
|
|
2540
|
+
]);
|
|
2508
2541
|
// Build nested relation subqueries referencing innerAlias
|
|
2509
2542
|
if (spec !== true && spec.with) {
|
|
2510
2543
|
for (const [nestedRelName, nestedSpec] of Object.entries(spec.with)) {
|
|
@@ -2514,14 +2547,14 @@ export class QueryInterface {
|
|
|
2514
2547
|
`Available: ${Object.keys(targetMeta.relations).join(', ')}`);
|
|
2515
2548
|
}
|
|
2516
2549
|
const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
2517
|
-
const fallback = nestedRelDef.type === 'hasMany' ?
|
|
2518
|
-
innerJsonPairs.push(
|
|
2550
|
+
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
2551
|
+
innerJsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
|
|
2519
2552
|
}
|
|
2520
2553
|
}
|
|
2521
|
-
const innerJsonObj =
|
|
2522
|
-
return `SELECT
|
|
2554
|
+
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
2555
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
|
|
2523
2556
|
}
|
|
2524
|
-
return `SELECT
|
|
2557
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, orderClause.trim() || undefined)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
2525
2558
|
}
|
|
2526
2559
|
// belongsTo / hasOne — return single object
|
|
2527
2560
|
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
@@ -2568,22 +2601,22 @@ export class QueryInterface {
|
|
|
2568
2601
|
params.push(filter.path);
|
|
2569
2602
|
const pathParam = params.length;
|
|
2570
2603
|
params.push(String(filter.equals));
|
|
2571
|
-
clauses.push(`${column
|
|
2604
|
+
clauses.push(`${this.dialect.buildJsonPathExtract(column, this.p(pathParam))} = ${this.p(params.length)}`);
|
|
2572
2605
|
}
|
|
2573
2606
|
else if (filter.equals !== undefined) {
|
|
2574
2607
|
// Containment equality: column @> $N::jsonb
|
|
2575
2608
|
params.push(JSON.stringify(filter.equals));
|
|
2576
|
-
clauses.push(
|
|
2609
|
+
clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
|
|
2577
2610
|
}
|
|
2578
2611
|
if (filter.contains !== undefined) {
|
|
2579
2612
|
// Containment: column @> $N::jsonb
|
|
2580
2613
|
params.push(JSON.stringify(filter.contains));
|
|
2581
|
-
clauses.push(
|
|
2614
|
+
clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
|
|
2582
2615
|
}
|
|
2583
2616
|
if (filter.hasKey !== undefined) {
|
|
2584
2617
|
// Key existence: column ? $N
|
|
2585
2618
|
params.push(filter.hasKey);
|
|
2586
|
-
clauses.push(`${column} ?
|
|
2619
|
+
clauses.push(`${column} ? ${this.p(params.length)}`);
|
|
2587
2620
|
}
|
|
2588
2621
|
return clauses;
|
|
2589
2622
|
}
|
|
@@ -2597,17 +2630,17 @@ export class QueryInterface {
|
|
|
2597
2630
|
if (filter.has !== undefined) {
|
|
2598
2631
|
// value = ANY(column)
|
|
2599
2632
|
params.push(filter.has);
|
|
2600
|
-
clauses.push(
|
|
2633
|
+
clauses.push(`${this.p(params.length)} = ANY(${column})`);
|
|
2601
2634
|
}
|
|
2602
2635
|
if (filter.hasEvery !== undefined) {
|
|
2603
2636
|
// column @> ARRAY[...]::type[]
|
|
2604
2637
|
params.push(filter.hasEvery);
|
|
2605
|
-
clauses.push(`${column} @>
|
|
2638
|
+
clauses.push(`${column} @> ${this.p(params.length)}::${elementType}[]`);
|
|
2606
2639
|
}
|
|
2607
2640
|
if (filter.hasSome !== undefined) {
|
|
2608
2641
|
// column && ARRAY[...]::type[]
|
|
2609
2642
|
params.push(filter.hasSome);
|
|
2610
|
-
clauses.push(`${column} &&
|
|
2643
|
+
clauses.push(`${column} && ${this.p(params.length)}::${elementType}[]`);
|
|
2611
2644
|
}
|
|
2612
2645
|
if (filter.isEmpty === true) {
|
|
2613
2646
|
// array_length(column, 1) IS NULL
|