turbine-orm 0.10.0 → 0.12.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.
@@ -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");
@@ -112,6 +113,7 @@ class QueryInterface {
112
113
  warnOnUnlimited;
113
114
  preparedStatementsEnabled;
114
115
  sqlCacheEnabled;
116
+ dialect;
115
117
  /**
116
118
  * Tracks tables that have already triggered an unlimited-query warning so
117
119
  * the user is not spammed once per row. Per-instance state — each
@@ -146,6 +148,7 @@ class QueryInterface {
146
148
  this.warnOnUnlimited = options?.warnOnUnlimited !== false;
147
149
  this.preparedStatementsEnabled = options?.preparedStatements ?? true;
148
150
  this.sqlCacheEnabled = options?.sqlCache !== false;
151
+ this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
149
152
  // Pre-compute column type lookup maps (TASK-26)
150
153
  this.columnPgTypeMap = new Map();
151
154
  this.columnArrayTypeMap = new Map();
@@ -154,6 +157,14 @@ class QueryInterface {
154
157
  this.columnArrayTypeMap.set(col.name, col.pgArrayType);
155
158
  }
156
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
+ }
157
168
  /**
158
169
  * Return cache hit/miss statistics for this QueryInterface instance.
159
170
  * Useful for monitoring and benchmarking.
@@ -291,11 +302,11 @@ class QueryInterface {
291
302
  // Simple path: plain equality, no operators/null/OR
292
303
  if (!args.with && isSimpleWhere) {
293
304
  const entry = this.acquireSql(ck, () => {
294
- const qt = (0, utils_js_1.quoteIdent)(this.table);
305
+ const qt = this.q(this.table);
295
306
  const tempParams = whereKeys.map((k) => whereObj[k]);
296
- 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)}`);
297
308
  const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
298
- 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}.*`;
299
310
  void tempParams; // params are positional, SQL is value-invariant
300
311
  return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
301
312
  });
@@ -320,8 +331,8 @@ class QueryInterface {
320
331
  const freshParams = [];
321
332
  const clause = this.buildWhereClause(whereObj, freshParams);
322
333
  const whereSql = clause ? ` WHERE ${clause}` : '';
323
- const qt = (0, utils_js_1.quoteIdent)(this.table);
324
- 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}.*`;
325
336
  return `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
326
337
  });
327
338
  // Collect params
@@ -347,7 +358,7 @@ class QueryInterface {
347
358
  const clause = this.buildWhereClause(whereObj, freshParams);
348
359
  const whereSql = clause ? ` WHERE ${clause}` : '';
349
360
  const selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
350
- 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`;
351
362
  });
352
363
  // Collect params in exact build order: where first, then with-clause relations
353
364
  this.collectWhereParams(whereObj, params);
@@ -458,7 +469,7 @@ class QueryInterface {
458
469
  return { sql: clause ? ` WHERE ${clause}` : '' };
459
470
  })()
460
471
  : { sql: '' };
461
- const qt = (0, utils_js_1.quoteIdent)(this.table);
472
+ const qt = this.q(this.table);
462
473
  let distinctPrefix = '';
463
474
  if (args?.distinct && args.distinct.length > 0) {
464
475
  const distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
@@ -469,7 +480,7 @@ class QueryInterface {
469
480
  selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList);
470
481
  }
471
482
  else if (columnsList) {
472
- selectClause = columnsList.map((c) => `${qt}.${(0, utils_js_1.quoteIdent)(c)}`).join(', ');
483
+ selectClause = columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ');
473
484
  }
474
485
  else {
475
486
  selectClause = `${qt}.*`;
@@ -483,7 +494,7 @@ class QueryInterface {
483
494
  const dir = args.orderBy?.[k] ?? 'asc';
484
495
  const op = dir === 'desc' ? '<' : '>';
485
496
  freshParams.push(v);
486
- return `${qt}.${col} ${op} $${freshParams.length}`;
497
+ return `${qt}.${col} ${op} ${this.p(freshParams.length)}`;
487
498
  });
488
499
  if (freshWhereSql) {
489
500
  sql += ` AND ${cursorConditions.join(' AND ')}`;
@@ -498,11 +509,11 @@ class QueryInterface {
498
509
  }
499
510
  if (effectiveLimit !== undefined) {
500
511
  freshParams.push(Number(effectiveLimit));
501
- sql += ` LIMIT $${freshParams.length}`;
512
+ sql += ` LIMIT ${this.p(freshParams.length)}`;
502
513
  }
503
514
  if (args?.offset !== undefined) {
504
515
  freshParams.push(Number(args.offset));
505
- sql += ` OFFSET $${freshParams.length}`;
516
+ sql += ` OFFSET ${this.p(freshParams.length)}`;
506
517
  }
507
518
  return sql;
508
519
  });
@@ -590,7 +601,7 @@ class QueryInterface {
590
601
  // Acquire a dedicated connection — cursors require a single connection in a transaction
591
602
  const client = await this.pool.connect();
592
603
  const cursorName = `turbine_cursor_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
593
- const quotedCursor = (0, utils_js_1.quoteIdent)(cursorName);
604
+ const quotedCursor = this.q(cursorName);
594
605
  try {
595
606
  await client.query('BEGIN');
596
607
  await client.query(`DECLARE ${quotedCursor} NO SCROLL CURSOR FOR ${deferred.sql}`, deferred.params);
@@ -724,8 +735,13 @@ class QueryInterface {
724
735
  const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
725
736
  const columns = entries.map(([k]) => this.toSqlColumn(k));
726
737
  const params = entries.map(([, v]) => v);
727
- const placeholders = entries.map((_, i) => `$${i + 1}`);
728
- 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 = this.dialect.buildInsertStatement({
740
+ table: this.q(this.table),
741
+ columns,
742
+ valuePlaceholders: placeholders,
743
+ returning: '*',
744
+ });
729
745
  return {
730
746
  sql,
731
747
  params,
@@ -754,7 +770,7 @@ class QueryInterface {
754
770
  });
755
771
  }
756
772
  buildCreateMany(args) {
757
- const qt = (0, utils_js_1.quoteIdent)(this.table);
773
+ const qt = this.q(this.table);
758
774
  if (args.data.length === 0) {
759
775
  return {
760
776
  sql: `SELECT * FROM ${qt} WHERE false`,
@@ -765,27 +781,24 @@ class QueryInterface {
765
781
  }
766
782
  const keys = Object.keys(args.data[0]).filter((k) => args.data[0][k] !== undefined);
767
783
  const columns = keys.map((k) => this.toColumn(k));
768
- // Build column arrays for UNNEST
769
- const columnArrays = keys.map(() => []);
770
- for (const row of args.data) {
784
+ const rowValues = args.data.map((row) => {
771
785
  const record = row;
772
- keys.forEach((key, i) => {
773
- columnArrays[i].push(record[key]);
774
- });
775
- }
776
- // Use actual Postgres types for array casts
786
+ return keys.map((key) => record[key]);
787
+ });
788
+ // Use actual Postgres types for array casts in the default PostgreSQL dialect.
777
789
  const typeCasts = columns.map((col) => this.getColumnArrayType(col));
778
- const unnestArgs = columnArrays.map((_, i) => `$${i + 1}::${typeCasts[i]}`);
779
- const quotedColumns = columns.map((c) => (0, utils_js_1.quoteIdent)(c));
780
- let sql = `INSERT INTO ${qt} (${quotedColumns.join(', ')}) SELECT * FROM UNNEST(${unnestArgs.join(', ')})`;
781
- // skipDuplicates: add ON CONFLICT DO NOTHING
782
- if (args.skipDuplicates) {
783
- sql += ` ON CONFLICT DO NOTHING`;
784
- }
785
- sql += ` RETURNING *`;
790
+ const quotedColumns = columns.map((c) => this.q(c));
791
+ const built = this.dialect.buildBulkInsertStatement({
792
+ table: qt,
793
+ columns: quotedColumns,
794
+ rowValues,
795
+ columnArrayTypes: typeCasts,
796
+ skipDuplicates: args.skipDuplicates,
797
+ returning: '*',
798
+ });
786
799
  return {
787
- sql,
788
- params: columnArrays,
800
+ sql: built.sql,
801
+ params: built.params,
789
802
  transform: (result) => result.rows.map((row) => this.parseRow(row, this.table)),
790
803
  tag: `${this.table}.createMany`,
791
804
  };
@@ -814,7 +827,7 @@ class QueryInterface {
814
827
  const whereClause = this.buildWhereClause(whereObj, freshParams);
815
828
  const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
816
829
  this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
817
- return `UPDATE ${(0, utils_js_1.quoteIdent)(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
830
+ return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
818
831
  });
819
832
  // On cache hit, validate predicate
820
833
  if (whereFp === '') {
@@ -863,7 +876,7 @@ class QueryInterface {
863
876
  const clause = this.buildWhereClause(whereObj, freshParams);
864
877
  const whereSql = clause ? ` WHERE ${clause}` : '';
865
878
  this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
866
- return `DELETE FROM ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql} RETURNING *`;
879
+ return `DELETE FROM ${this.q(this.table)}${whereSql}${this.dialect.buildReturningClause('*')}`;
867
880
  });
868
881
  // On cache hit, still validate the predicate
869
882
  if (whereFp === '') {
@@ -903,7 +916,7 @@ class QueryInterface {
903
916
  const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
904
917
  const columns = createEntries.map(([k]) => this.toSqlColumn(k));
905
918
  const createParams = createEntries.map(([, v]) => v);
906
- const placeholders = createEntries.map((_, i) => `$${i + 1}`);
919
+ const placeholders = createEntries.map((_, i) => `${this.p(i + 1)}`);
907
920
  // The conflict target comes from `where` keys — must be unique/PK columns
908
921
  const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
909
922
  const conflictColumns = conflictKeys.map((k) => this.toSqlColumn(k));
@@ -911,15 +924,20 @@ class QueryInterface {
911
924
  const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
912
925
  let paramIdx = createParams.length + 1;
913
926
  const setClauses = updateEntries.map(([k]) => {
914
- const clause = `${this.toSqlColumn(k)} = $${paramIdx}`;
927
+ const clause = `${this.toSqlColumn(k)} = ${this.p(paramIdx)}`;
915
928
  paramIdx++;
916
929
  return clause;
917
930
  });
918
931
  const updateParams = updateEntries.map(([, v]) => v);
919
932
  const params = [...createParams, ...updateParams];
920
- const sql = `INSERT INTO ${(0, utils_js_1.quoteIdent)(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
921
- ` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${setClauses.join(', ')}` +
922
- ` RETURNING *`;
933
+ const sql = this.dialect.buildUpsertStatement({
934
+ table: this.q(this.table),
935
+ insertColumns: columns,
936
+ valuePlaceholders: placeholders,
937
+ conflictColumns,
938
+ updateSetClauses: setClauses,
939
+ returning: '*',
940
+ });
923
941
  return {
924
942
  sql,
925
943
  params,
@@ -962,7 +980,7 @@ class QueryInterface {
962
980
  const whereClause = this.buildWhereClause(whereObj, freshParams);
963
981
  const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
964
982
  this.assertMutationHasPredicate('updateMany', whereSql, args.allowFullTableScan);
965
- return `UPDATE ${(0, utils_js_1.quoteIdent)(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
983
+ return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
966
984
  });
967
985
  if (whereFp === '') {
968
986
  this.assertMutationHasPredicate('updateMany', '', args.allowFullTableScan);
@@ -997,7 +1015,7 @@ class QueryInterface {
997
1015
  const clause = this.buildWhereClause(whereObj, freshParams);
998
1016
  const whereSql = clause ? ` WHERE ${clause}` : '';
999
1017
  this.assertMutationHasPredicate('deleteMany', whereSql, args.allowFullTableScan);
1000
- return `DELETE FROM ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql}`;
1018
+ return `DELETE FROM ${this.q(this.table)}${whereSql}`;
1001
1019
  });
1002
1020
  if (whereFp === '') {
1003
1021
  this.assertMutationHasPredicate('deleteMany', '', args.allowFullTableScan);
@@ -1030,7 +1048,7 @@ class QueryInterface {
1030
1048
  const freshParams = [];
1031
1049
  const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
1032
1050
  const whereSql = clause ? ` WHERE ${clause}` : '';
1033
- return `SELECT COUNT(*)::int AS count FROM ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql}`;
1051
+ return `SELECT COUNT(*)::int AS count FROM ${this.q(this.table)}${whereSql}`;
1034
1052
  });
1035
1053
  if (args?.where) {
1036
1054
  this.collectWhereParams(whereObj, params);
@@ -1063,7 +1081,7 @@ class QueryInterface {
1063
1081
  }
1064
1082
  }
1065
1083
  const groupColsRaw = args.by.map((k) => this.toColumn(k));
1066
- const groupCols = groupColsRaw.map((c) => (0, utils_js_1.quoteIdent)(c));
1084
+ const groupCols = groupColsRaw.map((c) => this.q(c));
1067
1085
  const { sql: whereSql, params } = args.where ? this.buildWhere(args.where) : { sql: '', params: [] };
1068
1086
  // Build SELECT expressions: group-by columns + aggregate functions
1069
1087
  const selectExprs = [...groupCols];
@@ -1077,7 +1095,7 @@ class QueryInterface {
1077
1095
  for (const [field, enabled] of Object.entries(args._sum)) {
1078
1096
  if (enabled) {
1079
1097
  const col = this.toColumn(field);
1080
- selectExprs.push(`SUM(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_sum_${col}`)}`);
1098
+ selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
1081
1099
  }
1082
1100
  }
1083
1101
  }
@@ -1086,7 +1104,7 @@ class QueryInterface {
1086
1104
  for (const [field, enabled] of Object.entries(args._avg)) {
1087
1105
  if (enabled) {
1088
1106
  const col = this.toColumn(field);
1089
- selectExprs.push(`AVG(${(0, utils_js_1.quoteIdent)(col)})::float AS ${(0, utils_js_1.quoteIdent)(`_avg_${col}`)}`);
1107
+ selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
1090
1108
  }
1091
1109
  }
1092
1110
  }
@@ -1095,7 +1113,7 @@ class QueryInterface {
1095
1113
  for (const [field, enabled] of Object.entries(args._min)) {
1096
1114
  if (enabled) {
1097
1115
  const col = this.toColumn(field);
1098
- selectExprs.push(`MIN(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_min_${col}`)}`);
1116
+ selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
1099
1117
  }
1100
1118
  }
1101
1119
  }
@@ -1104,11 +1122,11 @@ class QueryInterface {
1104
1122
  for (const [field, enabled] of Object.entries(args._max)) {
1105
1123
  if (enabled) {
1106
1124
  const col = this.toColumn(field);
1107
- selectExprs.push(`MAX(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_max_${col}`)}`);
1125
+ selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
1108
1126
  }
1109
1127
  }
1110
1128
  }
1111
- let sql = `SELECT ${selectExprs.join(', ')} FROM ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
1129
+ let sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
1112
1130
  // ORDER BY
1113
1131
  if (args.orderBy) {
1114
1132
  sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
@@ -1216,7 +1234,7 @@ class QueryInterface {
1216
1234
  for (const [field, enabled] of Object.entries(args._count)) {
1217
1235
  if (enabled) {
1218
1236
  const col = this.toColumn(field);
1219
- selectExprs.push(`COUNT(${(0, utils_js_1.quoteIdent)(col)})::int AS ${(0, utils_js_1.quoteIdent)(`_count_${col}`)}`);
1237
+ selectExprs.push(`COUNT(${this.q(col)})::int AS ${this.q(`_count_${col}`)}`);
1220
1238
  }
1221
1239
  }
1222
1240
  }
@@ -1225,7 +1243,7 @@ class QueryInterface {
1225
1243
  for (const [field, enabled] of Object.entries(args._sum)) {
1226
1244
  if (enabled) {
1227
1245
  const col = this.toColumn(field);
1228
- selectExprs.push(`SUM(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_sum_${col}`)}`);
1246
+ selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
1229
1247
  }
1230
1248
  }
1231
1249
  }
@@ -1234,7 +1252,7 @@ class QueryInterface {
1234
1252
  for (const [field, enabled] of Object.entries(args._avg)) {
1235
1253
  if (enabled) {
1236
1254
  const col = this.toColumn(field);
1237
- selectExprs.push(`AVG(${(0, utils_js_1.quoteIdent)(col)})::float AS ${(0, utils_js_1.quoteIdent)(`_avg_${col}`)}`);
1255
+ selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
1238
1256
  }
1239
1257
  }
1240
1258
  }
@@ -1243,7 +1261,7 @@ class QueryInterface {
1243
1261
  for (const [field, enabled] of Object.entries(args._min)) {
1244
1262
  if (enabled) {
1245
1263
  const col = this.toColumn(field);
1246
- selectExprs.push(`MIN(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_min_${col}`)}`);
1264
+ selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
1247
1265
  }
1248
1266
  }
1249
1267
  }
@@ -1252,14 +1270,14 @@ class QueryInterface {
1252
1270
  for (const [field, enabled] of Object.entries(args._max)) {
1253
1271
  if (enabled) {
1254
1272
  const col = this.toColumn(field);
1255
- selectExprs.push(`MAX(${(0, utils_js_1.quoteIdent)(col)}) AS ${(0, utils_js_1.quoteIdent)(`_max_${col}`)}`);
1273
+ selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
1256
1274
  }
1257
1275
  }
1258
1276
  }
1259
1277
  if (selectExprs.length === 0) {
1260
1278
  selectExprs.push('COUNT(*)::int AS _count');
1261
1279
  }
1262
- const sql = `SELECT ${selectExprs.join(', ')} FROM ${(0, utils_js_1.quoteIdent)(this.table)}${whereSql}`;
1280
+ const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
1263
1281
  return {
1264
1282
  sql,
1265
1283
  params,
@@ -1376,7 +1394,7 @@ class QueryInterface {
1376
1394
  }
1377
1395
  /** Convert camelCase field name to a double-quoted SQL identifier */
1378
1396
  toSqlColumn(field) {
1379
- return (0, utils_js_1.quoteIdent)(this.toColumn(field));
1397
+ return this.q(this.toColumn(field));
1380
1398
  }
1381
1399
  /**
1382
1400
  * Build a single SET clause entry for update/updateMany.
@@ -1409,7 +1427,7 @@ class QueryInterface {
1409
1427
  const opValue = v[op];
1410
1428
  if (op === 'set') {
1411
1429
  params.push(opValue);
1412
- return `${col} = $${params.length}`;
1430
+ return `${col} = ${this.p(params.length)}`;
1413
1431
  }
1414
1432
  // Arithmetic operators: must be finite numbers
1415
1433
  if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
@@ -1417,19 +1435,19 @@ class QueryInterface {
1417
1435
  }
1418
1436
  if (op === 'increment') {
1419
1437
  params.push(opValue);
1420
- return `${col} = ${col} + $${params.length}`;
1438
+ return `${col} = ${col} + ${this.p(params.length)}`;
1421
1439
  }
1422
1440
  if (op === 'decrement') {
1423
1441
  params.push(opValue);
1424
- return `${col} = ${col} - $${params.length}`;
1442
+ return `${col} = ${col} - ${this.p(params.length)}`;
1425
1443
  }
1426
1444
  if (op === 'multiply') {
1427
1445
  params.push(opValue);
1428
- return `${col} = ${col} * $${params.length}`;
1446
+ return `${col} = ${col} * ${this.p(params.length)}`;
1429
1447
  }
1430
1448
  if (op === 'divide') {
1431
1449
  params.push(opValue);
1432
- return `${col} = ${col} / $${params.length}`;
1450
+ return `${col} = ${col} / ${this.p(params.length)}`;
1433
1451
  }
1434
1452
  }
1435
1453
  // Fall through: multi-key objects or non-operator single-key objects
@@ -1437,7 +1455,7 @@ class QueryInterface {
1437
1455
  }
1438
1456
  // Plain value (including null, Date, Buffer, arrays, JSON objects)
1439
1457
  params.push(value);
1440
- return `${col} = $${params.length}`;
1458
+ return `${col} = ${this.p(params.length)}`;
1441
1459
  }
1442
1460
  // =========================================================================
1443
1461
  // Fingerprinting — value-invariant shape keys for SQL cache lookup
@@ -1960,7 +1978,7 @@ class QueryInterface {
1960
1978
  }
1961
1979
  }
1962
1980
  const rawColumn = this.toColumn(key);
1963
- const column = (0, utils_js_1.quoteIdent)(rawColumn);
1981
+ const column = this.q(rawColumn);
1964
1982
  // Handle null → IS NULL
1965
1983
  if (value === null) {
1966
1984
  andClauses.push(`${column} IS NULL`);
@@ -2010,7 +2028,7 @@ class QueryInterface {
2010
2028
  }
2011
2029
  // Plain equality
2012
2030
  params.push(value);
2013
- andClauses.push(`${column} = $${params.length}`);
2031
+ andClauses.push(`${column} = ${this.p(params.length)}`);
2014
2032
  }
2015
2033
  if (andClauses.length === 0)
2016
2034
  return null;
@@ -2025,18 +2043,18 @@ class QueryInterface {
2025
2043
  const targetMeta = this.schema.tables[targetTable];
2026
2044
  if (!targetMeta)
2027
2045
  return null;
2028
- const qt = (0, utils_js_1.quoteIdent)(targetTable);
2029
- const qSelf = (0, utils_js_1.quoteIdent)(this.table);
2046
+ const qt = this.q(targetTable);
2047
+ const qSelf = this.q(this.table);
2030
2048
  const clauses = [];
2031
2049
  // Correlation: link child table to parent table (supports composite FKs)
2032
2050
  let correlation;
2033
2051
  if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
2034
2052
  // parent.pk = child.fk
2035
- correlation = (0, utils_js_1.buildCorrelation)(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
2053
+ correlation = this.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
2036
2054
  }
2037
2055
  else {
2038
2056
  // belongsTo: parent.fk = child.pk
2039
- correlation = (0, utils_js_1.buildCorrelation)(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
2057
+ correlation = this.dialect.buildCorrelation(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
2040
2058
  }
2041
2059
  // "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
2042
2060
  if (filterObj.some !== undefined) {
@@ -2073,7 +2091,7 @@ class QueryInterface {
2073
2091
  const meta = this.schema.tables[targetTable];
2074
2092
  if (!meta)
2075
2093
  return null;
2076
- const qt = (0, utils_js_1.quoteIdent)(targetTable);
2094
+ const qt = this.q(targetTable);
2077
2095
  const conditions = [];
2078
2096
  for (const [field, value] of Object.entries(subWhere)) {
2079
2097
  if (value === undefined)
@@ -2083,7 +2101,7 @@ class QueryInterface {
2083
2101
  throw new errors_js_1.ValidationError(`[turbine] Unknown field "${field}" in relation filter for table "${targetTable}". ` +
2084
2102
  `Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
2085
2103
  }
2086
- const qCol = `${qt}.${(0, utils_js_1.quoteIdent)(col)}`;
2104
+ const qCol = `${qt}.${this.q(col)}`;
2087
2105
  if (value === null) {
2088
2106
  conditions.push(`${qCol} IS NULL`);
2089
2107
  continue;
@@ -2094,7 +2112,7 @@ class QueryInterface {
2094
2112
  continue;
2095
2113
  }
2096
2114
  params.push(value);
2097
- conditions.push(`${qCol} = $${params.length}`);
2115
+ conditions.push(`${qCol} = ${this.p(params.length)}`);
2098
2116
  }
2099
2117
  return conditions.length > 0 ? conditions.join(' AND ') : null;
2100
2118
  }
@@ -2106,19 +2124,19 @@ class QueryInterface {
2106
2124
  const clauses = [];
2107
2125
  if (op.gt !== undefined) {
2108
2126
  params.push(op.gt);
2109
- clauses.push(`${column} > $${params.length}`);
2127
+ clauses.push(`${column} > ${this.p(params.length)}`);
2110
2128
  }
2111
2129
  if (op.gte !== undefined) {
2112
2130
  params.push(op.gte);
2113
- clauses.push(`${column} >= $${params.length}`);
2131
+ clauses.push(`${column} >= ${this.p(params.length)}`);
2114
2132
  }
2115
2133
  if (op.lt !== undefined) {
2116
2134
  params.push(op.lt);
2117
- clauses.push(`${column} < $${params.length}`);
2135
+ clauses.push(`${column} < ${this.p(params.length)}`);
2118
2136
  }
2119
2137
  if (op.lte !== undefined) {
2120
2138
  params.push(op.lte);
2121
- clauses.push(`${column} <= $${params.length}`);
2139
+ clauses.push(`${column} <= ${this.p(params.length)}`);
2122
2140
  }
2123
2141
  if (op.not !== undefined) {
2124
2142
  if (op.not === null) {
@@ -2126,30 +2144,29 @@ class QueryInterface {
2126
2144
  }
2127
2145
  else {
2128
2146
  params.push(op.not);
2129
- clauses.push(`${column} != $${params.length}`);
2147
+ clauses.push(`${column} != ${this.p(params.length)}`);
2130
2148
  }
2131
2149
  }
2132
2150
  if (op.in !== undefined) {
2133
2151
  params.push(op.in);
2134
- clauses.push(`${column} = ANY($${params.length})`);
2152
+ clauses.push(`${column} = ANY(${this.p(params.length)})`);
2135
2153
  }
2136
2154
  if (op.notIn !== undefined) {
2137
2155
  params.push(op.notIn);
2138
- clauses.push(`${column} != ALL($${params.length})`);
2156
+ clauses.push(`${column} != ALL(${this.p(params.length)})`);
2139
2157
  }
2140
- // Use ILIKE for case-insensitive mode, LIKE otherwise
2141
- const likeOp = op.mode === 'insensitive' ? 'ILIKE' : 'LIKE';
2158
+ const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
2142
2159
  if (op.contains !== undefined) {
2143
2160
  params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
2144
- clauses.push(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
2161
+ clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
2145
2162
  }
2146
2163
  if (op.startsWith !== undefined) {
2147
2164
  params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
2148
- clauses.push(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
2165
+ clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
2149
2166
  }
2150
2167
  if (op.endsWith !== undefined) {
2151
2168
  params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
2152
- clauses.push(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
2169
+ clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
2153
2170
  }
2154
2171
  return clauses;
2155
2172
  }
@@ -2309,8 +2326,8 @@ class QueryInterface {
2309
2326
  if (!meta)
2310
2327
  throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}"`);
2311
2328
  const cols = columnsList ?? meta.allColumns;
2312
- const qtbl = (0, utils_js_1.quoteIdent)(table);
2313
- const baseCols = cols.map((col) => `${qtbl}.${(0, utils_js_1.quoteIdent)(col)}`).join(', ');
2329
+ const qtbl = this.q(table);
2330
+ const baseCols = cols.map((col) => `${qtbl}.${this.q(col)}`).join(', ');
2314
2331
  const relationSelects = [];
2315
2332
  const aliasCounter = { n: 0 };
2316
2333
  for (const [relName, relSpec] of Object.entries(withClause)) {
@@ -2321,7 +2338,7 @@ class QueryInterface {
2321
2338
  }
2322
2339
  // The main table is not aliased, so pass table name as parentRef
2323
2340
  const subquery = this.buildRelationSubquery(relDef, relSpec, params, table, aliasCounter, depth, path);
2324
- relationSelects.push(`(${subquery}) AS ${(0, utils_js_1.quoteIdent)(relName)}`);
2341
+ relationSelects.push(`(${subquery}) AS ${this.q(relName)}`);
2325
2342
  }
2326
2343
  return [baseCols, ...relationSelects].join(', ');
2327
2344
  }
@@ -2383,7 +2400,7 @@ class QueryInterface {
2383
2400
  * 8. **Parameter threading:** All user-supplied values (where filters, limit) are
2384
2401
  * pushed to the shared `params` array with `$N` placeholders. No string
2385
2402
  * interpolation of user data ever occurs -- all identifiers go through
2386
- * `quoteIdent()` and all values are parameterized.
2403
+ * `this.q()` and all values are parameterized.
2387
2404
  *
2388
2405
  * ### Example output (hasMany with nested relation)
2389
2406
  * ```sql
@@ -2447,8 +2464,11 @@ class QueryInterface {
2447
2464
  .map(([k]) => targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k)));
2448
2465
  targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
2449
2466
  }
2450
- // Build json_build_object pairs for resolved columns
2451
- 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)}`);
2467
+ // Build JSON object pairs for resolved columns
2468
+ const jsonPairs = targetColumns.map((col) => [
2469
+ targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
2470
+ `${alias}.${this.q(col)}`,
2471
+ ]);
2452
2472
  // Determine if this hasMany will take the wrapped subquery path (LIMIT or ORDER BY).
2453
2473
  // When wrapping, nested relations are built in the wrapped path referencing innerAlias,
2454
2474
  // so we must NOT build them here (they would push orphaned params).
@@ -2464,14 +2484,14 @@ class QueryInterface {
2464
2484
  // Recursively build nested subquery, passing THIS alias as the parent reference
2465
2485
  const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
2466
2486
  // Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
2467
- const fallback = nestedRelDef.type === 'hasMany' ? "'[]'::json" : 'NULL';
2468
- jsonPairs.push(`'${(0, utils_js_1.escSingleQuote)(nestedRelName)}', COALESCE((${nestedSubquery}), ${fallback})`);
2487
+ const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
2488
+ jsonPairs.push([nestedRelName, `COALESCE((${nestedSubquery}), ${fallback})`]);
2469
2489
  }
2470
2490
  }
2471
- const jsonObj = `json_build_object(${jsonPairs.join(', ')})`;
2491
+ const jsonObj = this.dialect.buildJsonObject(jsonPairs);
2472
2492
  // Quote parent ref — can be a table name or auto-generated alias
2473
- const qParent = (0, utils_js_1.quoteIdent)(parentRef);
2474
- const qTarget = (0, utils_js_1.quoteIdent)(targetTable);
2493
+ const qParent = this.q(parentRef);
2494
+ const qTarget = this.q(targetTable);
2475
2495
  // Build ORDER BY for json_agg
2476
2496
  let orderClause = '';
2477
2497
  if (spec !== true && spec.orderBy) {
@@ -2482,7 +2502,7 @@ class QueryInterface {
2482
2502
  throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
2483
2503
  }
2484
2504
  const safeDir = dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
2485
- return `${alias}.${(0, utils_js_1.quoteIdent)(col)} ${safeDir}`;
2505
+ return `${alias}.${this.q(col)} ${safeDir}`;
2486
2506
  })
2487
2507
  .join(', ');
2488
2508
  orderClause = ` ORDER BY ${orders}`;
@@ -2493,10 +2513,10 @@ class QueryInterface {
2493
2513
  // Supports composite foreign keys (string[]) via buildCorrelation.
2494
2514
  let whereClause;
2495
2515
  if (relDef.type === 'belongsTo' || relDef.type === 'hasOne') {
2496
- whereClause = (0, utils_js_1.buildCorrelation)(alias, relDef.referenceKey, qParent, relDef.foreignKey);
2516
+ whereClause = this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
2497
2517
  }
2498
2518
  else {
2499
- whereClause = (0, utils_js_1.buildCorrelation)(alias, relDef.foreignKey, qParent, relDef.referenceKey);
2519
+ whereClause = this.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
2500
2520
  }
2501
2521
  // Additional filters — properly parameterized
2502
2522
  if (spec !== true && spec.where) {
@@ -2506,14 +2526,14 @@ class QueryInterface {
2506
2526
  throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in where for table "${targetTable}"`);
2507
2527
  }
2508
2528
  params.push(v);
2509
- whereClause += ` AND ${alias}.${(0, utils_js_1.quoteIdent)(col)} = $${params.length}`;
2529
+ whereClause += ` AND ${alias}.${this.q(col)} = ${this.p(params.length)}`;
2510
2530
  }
2511
2531
  }
2512
2532
  // LIMIT
2513
2533
  let limitClause = '';
2514
2534
  if (spec !== true && spec.limit) {
2515
2535
  params.push(Number(spec.limit));
2516
- limitClause = ` LIMIT $${params.length}`;
2536
+ limitClause = ` LIMIT ${this.p(params.length)}`;
2517
2537
  }
2518
2538
  if (relDef.type === 'hasMany') {
2519
2539
  // When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
@@ -2522,9 +2542,12 @@ class QueryInterface {
2522
2542
  const innerAlias = `${alias}i`;
2523
2543
  // Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
2524
2544
  // Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
2525
- const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${(0, utils_js_1.quoteIdent)(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
2545
+ const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${this.q(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
2526
2546
  // For the json_build_object, reference the inner alias — only include resolved columns
2527
- 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)}`);
2547
+ const innerJsonPairs = targetColumns.map((col) => [
2548
+ targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
2549
+ `${innerAlias}.${this.q(col)}`,
2550
+ ]);
2528
2551
  // Build nested relation subqueries referencing innerAlias
2529
2552
  if (spec !== true && spec.with) {
2530
2553
  for (const [nestedRelName, nestedSpec] of Object.entries(spec.with)) {
@@ -2534,14 +2557,14 @@ class QueryInterface {
2534
2557
  `Available: ${Object.keys(targetMeta.relations).join(', ')}`);
2535
2558
  }
2536
2559
  const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
2537
- const fallback = nestedRelDef.type === 'hasMany' ? "'[]'::json" : 'NULL';
2538
- innerJsonPairs.push(`'${(0, utils_js_1.escSingleQuote)(nestedRelName)}', COALESCE((${nestedSub}), ${fallback})`);
2560
+ const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
2561
+ innerJsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
2539
2562
  }
2540
2563
  }
2541
- const innerJsonObj = `json_build_object(${innerJsonPairs.join(', ')})`;
2542
- return `SELECT COALESCE(json_agg(${innerJsonObj}), '[]'::json) FROM (${innerSql}) ${innerAlias}`;
2564
+ const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
2565
+ return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
2543
2566
  }
2544
- return `SELECT COALESCE(json_agg(${jsonObj}${orderClause}), '[]'::json) FROM ${qTarget} ${alias} WHERE ${whereClause}`;
2567
+ return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, orderClause.trim() || undefined)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
2545
2568
  }
2546
2569
  // belongsTo / hasOne — return single object
2547
2570
  return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
@@ -2588,22 +2611,22 @@ class QueryInterface {
2588
2611
  params.push(filter.path);
2589
2612
  const pathParam = params.length;
2590
2613
  params.push(String(filter.equals));
2591
- clauses.push(`${column} #>> $${pathParam}::text[] = $${params.length}`);
2614
+ clauses.push(`${this.dialect.buildJsonPathExtract(column, this.p(pathParam))} = ${this.p(params.length)}`);
2592
2615
  }
2593
2616
  else if (filter.equals !== undefined) {
2594
2617
  // Containment equality: column @> $N::jsonb
2595
2618
  params.push(JSON.stringify(filter.equals));
2596
- clauses.push(`${column} @> $${params.length}::jsonb`);
2619
+ clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
2597
2620
  }
2598
2621
  if (filter.contains !== undefined) {
2599
2622
  // Containment: column @> $N::jsonb
2600
2623
  params.push(JSON.stringify(filter.contains));
2601
- clauses.push(`${column} @> $${params.length}::jsonb`);
2624
+ clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
2602
2625
  }
2603
2626
  if (filter.hasKey !== undefined) {
2604
2627
  // Key existence: column ? $N
2605
2628
  params.push(filter.hasKey);
2606
- clauses.push(`${column} ? $${params.length}`);
2629
+ clauses.push(`${column} ? ${this.p(params.length)}`);
2607
2630
  }
2608
2631
  return clauses;
2609
2632
  }
@@ -2617,17 +2640,17 @@ class QueryInterface {
2617
2640
  if (filter.has !== undefined) {
2618
2641
  // value = ANY(column)
2619
2642
  params.push(filter.has);
2620
- clauses.push(`$${params.length} = ANY(${column})`);
2643
+ clauses.push(`${this.p(params.length)} = ANY(${column})`);
2621
2644
  }
2622
2645
  if (filter.hasEvery !== undefined) {
2623
2646
  // column @> ARRAY[...]::type[]
2624
2647
  params.push(filter.hasEvery);
2625
- clauses.push(`${column} @> $${params.length}::${elementType}[]`);
2648
+ clauses.push(`${column} @> ${this.p(params.length)}::${elementType}[]`);
2626
2649
  }
2627
2650
  if (filter.hasSome !== undefined) {
2628
2651
  // column && ARRAY[...]::type[]
2629
2652
  params.push(filter.hasSome);
2630
- clauses.push(`${column} && $${params.length}::${elementType}[]`);
2653
+ clauses.push(`${column} && ${this.p(params.length)}::${elementType}[]`);
2631
2654
  }
2632
2655
  if (filter.isEmpty === true) {
2633
2656
  // array_length(column, 1) IS NULL