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
@@ -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 = (0, utils_js_1.quoteIdent)(this.table);
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)} = $${i + 1}`);
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}.${(0, utils_js_1.quoteIdent)(c)}`).join(', ') : `${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 = (0, utils_js_1.quoteIdent)(this.table);
321
- const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${(0, utils_js_1.quoteIdent)(c)}`).join(', ') : `${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 ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql} LIMIT 1`;
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 = (0, utils_js_1.quoteIdent)(this.table);
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}.${(0, utils_js_1.quoteIdent)(c)}`).join(', ');
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} $${freshParams.length}`;
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 $${freshParams.length}`;
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 $${freshParams.length}`;
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 = (0, utils_js_1.quoteIdent)(cursorName);
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) => `$${i + 1}`);
716
- const sql = `INSERT INTO ${(0, utils_js_1.quoteIdent)(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
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 = (0, utils_js_1.quoteIdent)(this.table);
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) => `$${i + 1}::${typeCasts[i]}`);
767
- const quotedColumns = columns.map((c) => (0, utils_js_1.quoteIdent)(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 ${(0, utils_js_1.quoteIdent)(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
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 ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql} RETURNING *`;
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) => `$${i + 1}`);
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)} = $${paramIdx}`;
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 ${(0, utils_js_1.quoteIdent)(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
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 ${(0, utils_js_1.quoteIdent)(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
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 ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql}`;
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 ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql}`;
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) => (0, utils_js_1.quoteIdent)(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(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_sum_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)})::float AS ${(0, utils_js_1.quoteIdent)(`_avg_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_min_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_max_${col}`)}`);
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 ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
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(${(0, utils_js_1.quoteIdent)(col)})::int AS ${(0, utils_js_1.quoteIdent)(`_count_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_sum_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)})::float AS ${(0, utils_js_1.quoteIdent)(`_avg_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_min_${col}`)}`);
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(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_max_${col}`)}`);
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 ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql}`;
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 (0, utils_js_1.quoteIdent)(this.toColumn(field));
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} = $${params.length}`;
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} + $${params.length}`;
1431
+ return `${col} = ${col} + ${this.p(params.length)}`;
1409
1432
  }
1410
1433
  if (op === 'decrement') {
1411
1434
  params.push(opValue);
1412
- return `${col} = ${col} - $${params.length}`;
1435
+ return `${col} = ${col} - ${this.p(params.length)}`;
1413
1436
  }
1414
1437
  if (op === 'multiply') {
1415
1438
  params.push(opValue);
1416
- return `${col} = ${col} * $${params.length}`;
1439
+ return `${col} = ${col} * ${this.p(params.length)}`;
1417
1440
  }
1418
1441
  if (op === 'divide') {
1419
1442
  params.push(opValue);
1420
- return `${col} = ${col} / $${params.length}`;
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} = $${params.length}`;
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 = (0, utils_js_1.quoteIdent)(rawColumn);
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} = $${params.length}`);
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 = (0, utils_js_1.quoteIdent)(targetTable);
2017
- const qSelf = (0, utils_js_1.quoteIdent)(this.table);
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 = `${qt}.${(0, utils_js_1.quoteIdent)(relDef.foreignKey)} = ${qSelf}.${(0, utils_js_1.quoteIdent)(relDef.referenceKey)}`;
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 = `${qt}.${(0, utils_js_1.quoteIdent)(relDef.referenceKey)} = ${qSelf}.${(0, utils_js_1.quoteIdent)(relDef.foreignKey)}`;
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 = (0, utils_js_1.quoteIdent)(targetTable);
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
- const qCol = `${qt}.${(0, utils_js_1.quoteIdent)(col)}`;
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} = $${params.length}`);
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} > $${params.length}`);
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} >= $${params.length}`);
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} < $${params.length}`);
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} <= $${params.length}`);
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} != $${params.length}`);
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($${params.length})`);
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($${params.length})`);
2149
+ clauses.push(`${column} != ALL(${this.p(params.length)})`);
2123
2150
  }
2124
- // Use ILIKE for case-insensitive mode, LIKE otherwise
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(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
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(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
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(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
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 = (0, utils_js_1.quoteIdent)(table);
2297
- const baseCols = cols.map((col) => `${qtbl}.${(0, utils_js_1.quoteIdent)(col)}`).join(', ');
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 ${(0, utils_js_1.quoteIdent)(relName)}`);
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
- * `quoteIdent()` and all values are parameterized.
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 json_build_object pairs for resolved columns
2435
- const jsonPairs = targetColumns.map((col) => `'${(0, utils_js_1.escSingleQuote)(targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col))}', ${alias}.${(0, utils_js_1.quoteIdent)(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' ? "'[]'::json" : 'NULL';
2452
- jsonPairs.push(`'${(0, utils_js_1.escSingleQuote)(nestedRelName)}', COALESCE((${nestedSubquery}), ${fallback})`);
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 = `json_build_object(${jsonPairs.join(', ')})`;
2484
+ const jsonObj = this.dialect.buildJsonObject(jsonPairs);
2456
2485
  // Quote parent ref — can be a table name or auto-generated alias
2457
- const qParent = (0, utils_js_1.quoteIdent)(parentRef);
2458
- const qTarget = (0, utils_js_1.quoteIdent)(targetTable);
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}.${(0, utils_js_1.quoteIdent)(col)} ${safeDir}`;
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 = `${alias}.${(0, utils_js_1.quoteIdent)(relDef.referenceKey)} = ${qParent}.${(0, utils_js_1.quoteIdent)(relDef.foreignKey)}`;
2509
+ whereClause = this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
2480
2510
  }
2481
2511
  else {
2482
- whereClause = `${alias}.${(0, utils_js_1.quoteIdent)(relDef.foreignKey)} = ${qParent}.${(0, utils_js_1.quoteIdent)(relDef.referenceKey)}`;
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}.${(0, utils_js_1.quoteIdent)(col)} = $${params.length}`;
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 $${params.length}`;
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}.${(0, utils_js_1.quoteIdent)(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
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) => `'${(0, utils_js_1.escSingleQuote)(targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col))}', ${innerAlias}.${(0, utils_js_1.quoteIdent)(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' ? "'[]'::json" : 'NULL';
2521
- innerJsonPairs.push(`'${(0, utils_js_1.escSingleQuote)(nestedRelName)}', COALESCE((${nestedSub}), ${fallback})`);
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 = `json_build_object(${innerJsonPairs.join(', ')})`;
2525
- return `SELECT COALESCE(json_agg(${innerJsonObj}), '[]'::json) FROM (${innerSql}) ${innerAlias}`;
2557
+ const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
2558
+ return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
2526
2559
  }
2527
- return `SELECT COALESCE(json_agg(${jsonObj}${orderClause}), '[]'::json) FROM ${qTarget} ${alias} WHERE ${whereClause}`;
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} #>> $${pathParam}::text[] = $${params.length}`);
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(`${column} @> $${params.length}::jsonb`);
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(`${column} @> $${params.length}::jsonb`);
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} ? $${params.length}`);
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(`$${params.length} = ANY(${column})`);
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} @> $${params.length}::${elementType}[]`);
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} && $${params.length}::${elementType}[]`);
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