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.
Files changed (45) hide show
  1. package/README.md +34 -16
  2. package/dist/adapters/cockroachdb.d.ts +40 -0
  3. package/dist/adapters/cockroachdb.js +172 -0
  4. package/dist/adapters/index.d.ts +107 -0
  5. package/dist/adapters/index.js +83 -0
  6. package/dist/adapters/yugabytedb.d.ts +52 -0
  7. package/dist/adapters/yugabytedb.js +156 -0
  8. package/dist/cjs/adapters/cockroachdb.js +174 -0
  9. package/dist/cjs/adapters/index.js +87 -0
  10. package/dist/cjs/adapters/yugabytedb.js +158 -0
  11. package/dist/cjs/cli/index.js +2 -1
  12. package/dist/cjs/cli/migrate.js +18 -12
  13. package/dist/cjs/cli/studio.js +5 -4
  14. package/dist/cjs/client.js +1 -0
  15. package/dist/cjs/dialect.js +57 -0
  16. package/dist/cjs/generate.js +8 -1
  17. package/dist/cjs/index.js +12 -3
  18. package/dist/cjs/introspect.js +46 -18
  19. package/dist/cjs/query/builder.js +129 -96
  20. package/dist/cjs/query/index.js +4 -1
  21. package/dist/cjs/query/utils.js +18 -0
  22. package/dist/cjs/schema.js +8 -0
  23. package/dist/cli/config.d.ts +11 -0
  24. package/dist/cli/index.js +2 -1
  25. package/dist/cli/migrate.d.ts +3 -0
  26. package/dist/cli/migrate.js +16 -10
  27. package/dist/cli/studio.d.ts +4 -0
  28. package/dist/cli/studio.js +5 -4
  29. package/dist/client.d.ts +3 -0
  30. package/dist/client.js +1 -0
  31. package/dist/dialect.d.ts +61 -0
  32. package/dist/dialect.js +55 -0
  33. package/dist/generate.js +8 -1
  34. package/dist/index.d.ts +5 -1
  35. package/dist/index.js +3 -1
  36. package/dist/introspect.js +46 -18
  37. package/dist/query/builder.d.ts +9 -1
  38. package/dist/query/builder.js +130 -97
  39. package/dist/query/index.d.ts +3 -1
  40. package/dist/query/index.js +2 -1
  41. package/dist/query/utils.d.ts +8 -0
  42. package/dist/query/utils.js +17 -0
  43. package/dist/schema.d.ts +6 -4
  44. package/dist/schema.js +7 -0
  45. package/package.json +8 -3
@@ -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, escSingleQuote, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
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 = quoteIdent(this.table);
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)} = $${i + 1}`);
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}.${quoteIdent(c)}`).join(', ') : `${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 = quoteIdent(this.table);
318
- const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${quoteIdent(c)}`).join(', ') : `${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 ${quoteIdent(this.table)}${whereSql} LIMIT 1`;
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 = quoteIdent(this.table);
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}.${quoteIdent(c)}`).join(', ');
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} $${freshParams.length}`;
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 $${freshParams.length}`;
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 $${freshParams.length}`;
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 = quoteIdent(cursorName);
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) => `$${i + 1}`);
713
- const sql = `INSERT INTO ${quoteIdent(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
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 = quoteIdent(this.table);
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) => `$${i + 1}::${typeCasts[i]}`);
764
- const quotedColumns = columns.map((c) => quoteIdent(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 ${quoteIdent(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
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 ${quoteIdent(this.table)}${whereSql} RETURNING *`;
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) => `$${i + 1}`);
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)} = $${paramIdx}`;
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 ${quoteIdent(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
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 ${quoteIdent(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
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 ${quoteIdent(this.table)}${whereSql}`;
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 ${quoteIdent(this.table)}${whereSql}`;
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) => quoteIdent(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(${quoteIdent(col)}) AS ${quoteIdent(`_sum_${col}`)}`);
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(${quoteIdent(col)})::float AS ${quoteIdent(`_avg_${col}`)}`);
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(${quoteIdent(col)}) AS ${quoteIdent(`_min_${col}`)}`);
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(${quoteIdent(col)}) AS ${quoteIdent(`_max_${col}`)}`);
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 ${quoteIdent(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
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(${quoteIdent(col)})::int AS ${quoteIdent(`_count_${col}`)}`);
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(${quoteIdent(col)}) AS ${quoteIdent(`_sum_${col}`)}`);
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(${quoteIdent(col)})::float AS ${quoteIdent(`_avg_${col}`)}`);
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(${quoteIdent(col)}) AS ${quoteIdent(`_min_${col}`)}`);
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(${quoteIdent(col)}) AS ${quoteIdent(`_max_${col}`)}`);
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 ${quoteIdent(this.table)}${whereSql}`;
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 quoteIdent(this.toColumn(field));
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} = $${params.length}`;
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} + $${params.length}`;
1428
+ return `${col} = ${col} + ${this.p(params.length)}`;
1406
1429
  }
1407
1430
  if (op === 'decrement') {
1408
1431
  params.push(opValue);
1409
- return `${col} = ${col} - $${params.length}`;
1432
+ return `${col} = ${col} - ${this.p(params.length)}`;
1410
1433
  }
1411
1434
  if (op === 'multiply') {
1412
1435
  params.push(opValue);
1413
- return `${col} = ${col} * $${params.length}`;
1436
+ return `${col} = ${col} * ${this.p(params.length)}`;
1414
1437
  }
1415
1438
  if (op === 'divide') {
1416
1439
  params.push(opValue);
1417
- return `${col} = ${col} / $${params.length}`;
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} = $${params.length}`;
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 = quoteIdent(rawColumn);
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} = $${params.length}`);
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 = quoteIdent(targetTable);
2014
- const qSelf = quoteIdent(this.table);
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 = `${qt}.${quoteIdent(relDef.foreignKey)} = ${qSelf}.${quoteIdent(relDef.referenceKey)}`;
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 = `${qt}.${quoteIdent(relDef.referenceKey)} = ${qSelf}.${quoteIdent(relDef.foreignKey)}`;
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 = quoteIdent(targetTable);
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
- const qCol = `${qt}.${quoteIdent(col)}`;
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} = $${params.length}`);
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} > $${params.length}`);
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} >= $${params.length}`);
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} < $${params.length}`);
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} <= $${params.length}`);
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} != $${params.length}`);
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($${params.length})`);
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($${params.length})`);
2146
+ clauses.push(`${column} != ALL(${this.p(params.length)})`);
2120
2147
  }
2121
- // Use ILIKE for case-insensitive mode, LIKE otherwise
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(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
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(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
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(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
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 = quoteIdent(table);
2294
- const baseCols = cols.map((col) => `${qtbl}.${quoteIdent(col)}`).join(', ');
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 ${quoteIdent(relName)}`);
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
- * `quoteIdent()` and all values are parameterized.
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 json_build_object pairs for resolved columns
2432
- const jsonPairs = targetColumns.map((col) => `'${escSingleQuote(targetMeta.reverseColumnMap[col] ?? snakeToCamel(col))}', ${alias}.${quoteIdent(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' ? "'[]'::json" : 'NULL';
2449
- jsonPairs.push(`'${escSingleQuote(nestedRelName)}', COALESCE((${nestedSubquery}), ${fallback})`);
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 = `json_build_object(${jsonPairs.join(', ')})`;
2481
+ const jsonObj = this.dialect.buildJsonObject(jsonPairs);
2453
2482
  // Quote parent ref — can be a table name or auto-generated alias
2454
- const qParent = quoteIdent(parentRef);
2455
- const qTarget = quoteIdent(targetTable);
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}.${quoteIdent(col)} ${safeDir}`;
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 = `${alias}.${quoteIdent(relDef.referenceKey)} = ${qParent}.${quoteIdent(relDef.foreignKey)}`;
2506
+ whereClause = this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
2477
2507
  }
2478
2508
  else {
2479
- whereClause = `${alias}.${quoteIdent(relDef.foreignKey)} = ${qParent}.${quoteIdent(relDef.referenceKey)}`;
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}.${quoteIdent(col)} = $${params.length}`;
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 $${params.length}`;
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}.${quoteIdent(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
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) => `'${escSingleQuote(targetMeta.reverseColumnMap[col] ?? snakeToCamel(col))}', ${innerAlias}.${quoteIdent(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' ? "'[]'::json" : 'NULL';
2518
- innerJsonPairs.push(`'${escSingleQuote(nestedRelName)}', COALESCE((${nestedSub}), ${fallback})`);
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 = `json_build_object(${innerJsonPairs.join(', ')})`;
2522
- return `SELECT COALESCE(json_agg(${innerJsonObj}), '[]'::json) FROM (${innerSql}) ${innerAlias}`;
2554
+ const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
2555
+ return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
2523
2556
  }
2524
- return `SELECT COALESCE(json_agg(${jsonObj}${orderClause}), '[]'::json) FROM ${qTarget} ${alias} WHERE ${whereClause}`;
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} #>> $${pathParam}::text[] = $${params.length}`);
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(`${column} @> $${params.length}::jsonb`);
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(`${column} @> $${params.length}::jsonb`);
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} ? $${params.length}`);
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(`$${params.length} = ANY(${column})`);
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} @> $${params.length}::${elementType}[]`);
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} && $${params.length}::${elementType}[]`);
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