turbine-orm 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/dialect.js +57 -0
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/query/builder.js +111 -95
- package/dist/cjs/query/index.js +3 -1
- package/dist/client.d.ts +3 -0
- package/dist/client.js +1 -0
- package/dist/dialect.d.ts +61 -0
- package/dist/dialect.js +55 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/query/builder.d.ts +9 -1
- package/dist/query/builder.js +112 -96
- package/dist/query/index.d.ts +2 -0
- package/dist/query/index.js +1 -0
- package/package.json +3 -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");
|
|
@@ -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 =
|
|
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)} =
|
|
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}.${
|
|
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 =
|
|
324
|
-
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${
|
|
334
|
+
const qt = this.q(this.table);
|
|
335
|
+
const selectExpr = columnsList ? columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ') : `${qt}.*`;
|
|
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 ${
|
|
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 =
|
|
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}.${
|
|
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}
|
|
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
|
|
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
|
|
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 =
|
|
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,8 @@ 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) =>
|
|
728
|
-
const sql = `INSERT INTO ${
|
|
738
|
+
const placeholders = entries.map((_, i) => `${this.p(i + 1)}`);
|
|
739
|
+
const sql = `INSERT INTO ${this.q(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')}) RETURNING *`;
|
|
729
740
|
return {
|
|
730
741
|
sql,
|
|
731
742
|
params,
|
|
@@ -754,7 +765,7 @@ class QueryInterface {
|
|
|
754
765
|
});
|
|
755
766
|
}
|
|
756
767
|
buildCreateMany(args) {
|
|
757
|
-
const qt =
|
|
768
|
+
const qt = this.q(this.table);
|
|
758
769
|
if (args.data.length === 0) {
|
|
759
770
|
return {
|
|
760
771
|
sql: `SELECT * FROM ${qt} WHERE false`,
|
|
@@ -775,8 +786,8 @@ class QueryInterface {
|
|
|
775
786
|
}
|
|
776
787
|
// Use actual Postgres types for array casts
|
|
777
788
|
const typeCasts = columns.map((col) => this.getColumnArrayType(col));
|
|
778
|
-
const unnestArgs = columnArrays.map((_, i) =>
|
|
779
|
-
const quotedColumns = columns.map((c) =>
|
|
789
|
+
const unnestArgs = columnArrays.map((_, i) => `${this.p(i + 1)}::${typeCasts[i]}`);
|
|
790
|
+
const quotedColumns = columns.map((c) => this.q(c));
|
|
780
791
|
let sql = `INSERT INTO ${qt} (${quotedColumns.join(', ')}) SELECT * FROM UNNEST(${unnestArgs.join(', ')})`;
|
|
781
792
|
// skipDuplicates: add ON CONFLICT DO NOTHING
|
|
782
793
|
if (args.skipDuplicates) {
|
|
@@ -814,7 +825,7 @@ class QueryInterface {
|
|
|
814
825
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
815
826
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
816
827
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
817
|
-
return `UPDATE ${
|
|
828
|
+
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql} RETURNING *`;
|
|
818
829
|
});
|
|
819
830
|
// On cache hit, validate predicate
|
|
820
831
|
if (whereFp === '') {
|
|
@@ -863,7 +874,7 @@ class QueryInterface {
|
|
|
863
874
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
864
875
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
865
876
|
this.assertMutationHasPredicate('delete', whereSql, args.allowFullTableScan);
|
|
866
|
-
return `DELETE FROM ${
|
|
877
|
+
return `DELETE FROM ${this.q(this.table)}${whereSql} RETURNING *`;
|
|
867
878
|
});
|
|
868
879
|
// On cache hit, still validate the predicate
|
|
869
880
|
if (whereFp === '') {
|
|
@@ -903,7 +914,7 @@ class QueryInterface {
|
|
|
903
914
|
const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
|
|
904
915
|
const columns = createEntries.map(([k]) => this.toSqlColumn(k));
|
|
905
916
|
const createParams = createEntries.map(([, v]) => v);
|
|
906
|
-
const placeholders = createEntries.map((_, i) =>
|
|
917
|
+
const placeholders = createEntries.map((_, i) => `${this.p(i + 1)}`);
|
|
907
918
|
// The conflict target comes from `where` keys — must be unique/PK columns
|
|
908
919
|
const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
|
|
909
920
|
const conflictColumns = conflictKeys.map((k) => this.toSqlColumn(k));
|
|
@@ -911,13 +922,13 @@ class QueryInterface {
|
|
|
911
922
|
const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
|
|
912
923
|
let paramIdx = createParams.length + 1;
|
|
913
924
|
const setClauses = updateEntries.map(([k]) => {
|
|
914
|
-
const clause = `${this.toSqlColumn(k)} =
|
|
925
|
+
const clause = `${this.toSqlColumn(k)} = ${this.p(paramIdx)}`;
|
|
915
926
|
paramIdx++;
|
|
916
927
|
return clause;
|
|
917
928
|
});
|
|
918
929
|
const updateParams = updateEntries.map(([, v]) => v);
|
|
919
930
|
const params = [...createParams, ...updateParams];
|
|
920
|
-
const sql = `INSERT INTO ${
|
|
931
|
+
const sql = `INSERT INTO ${this.q(this.table)} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})` +
|
|
921
932
|
` ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${setClauses.join(', ')}` +
|
|
922
933
|
` RETURNING *`;
|
|
923
934
|
return {
|
|
@@ -962,7 +973,7 @@ class QueryInterface {
|
|
|
962
973
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
963
974
|
const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
964
975
|
this.assertMutationHasPredicate('updateMany', whereSql, args.allowFullTableScan);
|
|
965
|
-
return `UPDATE ${
|
|
976
|
+
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}`;
|
|
966
977
|
});
|
|
967
978
|
if (whereFp === '') {
|
|
968
979
|
this.assertMutationHasPredicate('updateMany', '', args.allowFullTableScan);
|
|
@@ -997,7 +1008,7 @@ class QueryInterface {
|
|
|
997
1008
|
const clause = this.buildWhereClause(whereObj, freshParams);
|
|
998
1009
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
999
1010
|
this.assertMutationHasPredicate('deleteMany', whereSql, args.allowFullTableScan);
|
|
1000
|
-
return `DELETE FROM ${
|
|
1011
|
+
return `DELETE FROM ${this.q(this.table)}${whereSql}`;
|
|
1001
1012
|
});
|
|
1002
1013
|
if (whereFp === '') {
|
|
1003
1014
|
this.assertMutationHasPredicate('deleteMany', '', args.allowFullTableScan);
|
|
@@ -1030,7 +1041,7 @@ class QueryInterface {
|
|
|
1030
1041
|
const freshParams = [];
|
|
1031
1042
|
const clause = args?.where ? this.buildWhereClause(whereObj, freshParams) : null;
|
|
1032
1043
|
const whereSql = clause ? ` WHERE ${clause}` : '';
|
|
1033
|
-
return `SELECT COUNT(*)::int AS count FROM ${
|
|
1044
|
+
return `SELECT COUNT(*)::int AS count FROM ${this.q(this.table)}${whereSql}`;
|
|
1034
1045
|
});
|
|
1035
1046
|
if (args?.where) {
|
|
1036
1047
|
this.collectWhereParams(whereObj, params);
|
|
@@ -1063,7 +1074,7 @@ class QueryInterface {
|
|
|
1063
1074
|
}
|
|
1064
1075
|
}
|
|
1065
1076
|
const groupColsRaw = args.by.map((k) => this.toColumn(k));
|
|
1066
|
-
const groupCols = groupColsRaw.map((c) =>
|
|
1077
|
+
const groupCols = groupColsRaw.map((c) => this.q(c));
|
|
1067
1078
|
const { sql: whereSql, params } = args.where ? this.buildWhere(args.where) : { sql: '', params: [] };
|
|
1068
1079
|
// Build SELECT expressions: group-by columns + aggregate functions
|
|
1069
1080
|
const selectExprs = [...groupCols];
|
|
@@ -1077,7 +1088,7 @@ class QueryInterface {
|
|
|
1077
1088
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
1078
1089
|
if (enabled) {
|
|
1079
1090
|
const col = this.toColumn(field);
|
|
1080
|
-
selectExprs.push(`SUM(${
|
|
1091
|
+
selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
|
|
1081
1092
|
}
|
|
1082
1093
|
}
|
|
1083
1094
|
}
|
|
@@ -1086,7 +1097,7 @@ class QueryInterface {
|
|
|
1086
1097
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1087
1098
|
if (enabled) {
|
|
1088
1099
|
const col = this.toColumn(field);
|
|
1089
|
-
selectExprs.push(`AVG(${
|
|
1100
|
+
selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
|
|
1090
1101
|
}
|
|
1091
1102
|
}
|
|
1092
1103
|
}
|
|
@@ -1095,7 +1106,7 @@ class QueryInterface {
|
|
|
1095
1106
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
1096
1107
|
if (enabled) {
|
|
1097
1108
|
const col = this.toColumn(field);
|
|
1098
|
-
selectExprs.push(`MIN(${
|
|
1109
|
+
selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
|
|
1099
1110
|
}
|
|
1100
1111
|
}
|
|
1101
1112
|
}
|
|
@@ -1104,11 +1115,11 @@ class QueryInterface {
|
|
|
1104
1115
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
1105
1116
|
if (enabled) {
|
|
1106
1117
|
const col = this.toColumn(field);
|
|
1107
|
-
selectExprs.push(`MAX(${
|
|
1118
|
+
selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
|
|
1108
1119
|
}
|
|
1109
1120
|
}
|
|
1110
1121
|
}
|
|
1111
|
-
let sql = `SELECT ${selectExprs.join(', ')} FROM ${
|
|
1122
|
+
let sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql} GROUP BY ${groupCols.join(', ')}`;
|
|
1112
1123
|
// ORDER BY
|
|
1113
1124
|
if (args.orderBy) {
|
|
1114
1125
|
sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
|
|
@@ -1216,7 +1227,7 @@ class QueryInterface {
|
|
|
1216
1227
|
for (const [field, enabled] of Object.entries(args._count)) {
|
|
1217
1228
|
if (enabled) {
|
|
1218
1229
|
const col = this.toColumn(field);
|
|
1219
|
-
selectExprs.push(`COUNT(${
|
|
1230
|
+
selectExprs.push(`COUNT(${this.q(col)})::int AS ${this.q(`_count_${col}`)}`);
|
|
1220
1231
|
}
|
|
1221
1232
|
}
|
|
1222
1233
|
}
|
|
@@ -1225,7 +1236,7 @@ class QueryInterface {
|
|
|
1225
1236
|
for (const [field, enabled] of Object.entries(args._sum)) {
|
|
1226
1237
|
if (enabled) {
|
|
1227
1238
|
const col = this.toColumn(field);
|
|
1228
|
-
selectExprs.push(`SUM(${
|
|
1239
|
+
selectExprs.push(`SUM(${this.q(col)}) AS ${this.q(`_sum_${col}`)}`);
|
|
1229
1240
|
}
|
|
1230
1241
|
}
|
|
1231
1242
|
}
|
|
@@ -1234,7 +1245,7 @@ class QueryInterface {
|
|
|
1234
1245
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
1235
1246
|
if (enabled) {
|
|
1236
1247
|
const col = this.toColumn(field);
|
|
1237
|
-
selectExprs.push(`AVG(${
|
|
1248
|
+
selectExprs.push(`AVG(${this.q(col)})::float AS ${this.q(`_avg_${col}`)}`);
|
|
1238
1249
|
}
|
|
1239
1250
|
}
|
|
1240
1251
|
}
|
|
@@ -1243,7 +1254,7 @@ class QueryInterface {
|
|
|
1243
1254
|
for (const [field, enabled] of Object.entries(args._min)) {
|
|
1244
1255
|
if (enabled) {
|
|
1245
1256
|
const col = this.toColumn(field);
|
|
1246
|
-
selectExprs.push(`MIN(${
|
|
1257
|
+
selectExprs.push(`MIN(${this.q(col)}) AS ${this.q(`_min_${col}`)}`);
|
|
1247
1258
|
}
|
|
1248
1259
|
}
|
|
1249
1260
|
}
|
|
@@ -1252,14 +1263,14 @@ class QueryInterface {
|
|
|
1252
1263
|
for (const [field, enabled] of Object.entries(args._max)) {
|
|
1253
1264
|
if (enabled) {
|
|
1254
1265
|
const col = this.toColumn(field);
|
|
1255
|
-
selectExprs.push(`MAX(${
|
|
1266
|
+
selectExprs.push(`MAX(${this.q(col)}) AS ${this.q(`_max_${col}`)}`);
|
|
1256
1267
|
}
|
|
1257
1268
|
}
|
|
1258
1269
|
}
|
|
1259
1270
|
if (selectExprs.length === 0) {
|
|
1260
1271
|
selectExprs.push('COUNT(*)::int AS _count');
|
|
1261
1272
|
}
|
|
1262
|
-
const sql = `SELECT ${selectExprs.join(', ')} FROM ${
|
|
1273
|
+
const sql = `SELECT ${selectExprs.join(', ')} FROM ${this.q(this.table)}${whereSql}`;
|
|
1263
1274
|
return {
|
|
1264
1275
|
sql,
|
|
1265
1276
|
params,
|
|
@@ -1376,7 +1387,7 @@ class QueryInterface {
|
|
|
1376
1387
|
}
|
|
1377
1388
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
1378
1389
|
toSqlColumn(field) {
|
|
1379
|
-
return
|
|
1390
|
+
return this.q(this.toColumn(field));
|
|
1380
1391
|
}
|
|
1381
1392
|
/**
|
|
1382
1393
|
* Build a single SET clause entry for update/updateMany.
|
|
@@ -1409,7 +1420,7 @@ class QueryInterface {
|
|
|
1409
1420
|
const opValue = v[op];
|
|
1410
1421
|
if (op === 'set') {
|
|
1411
1422
|
params.push(opValue);
|
|
1412
|
-
return `${col} =
|
|
1423
|
+
return `${col} = ${this.p(params.length)}`;
|
|
1413
1424
|
}
|
|
1414
1425
|
// Arithmetic operators: must be finite numbers
|
|
1415
1426
|
if (typeof opValue !== 'number' || !Number.isFinite(opValue)) {
|
|
@@ -1417,19 +1428,19 @@ class QueryInterface {
|
|
|
1417
1428
|
}
|
|
1418
1429
|
if (op === 'increment') {
|
|
1419
1430
|
params.push(opValue);
|
|
1420
|
-
return `${col} = ${col} +
|
|
1431
|
+
return `${col} = ${col} + ${this.p(params.length)}`;
|
|
1421
1432
|
}
|
|
1422
1433
|
if (op === 'decrement') {
|
|
1423
1434
|
params.push(opValue);
|
|
1424
|
-
return `${col} = ${col} -
|
|
1435
|
+
return `${col} = ${col} - ${this.p(params.length)}`;
|
|
1425
1436
|
}
|
|
1426
1437
|
if (op === 'multiply') {
|
|
1427
1438
|
params.push(opValue);
|
|
1428
|
-
return `${col} = ${col} *
|
|
1439
|
+
return `${col} = ${col} * ${this.p(params.length)}`;
|
|
1429
1440
|
}
|
|
1430
1441
|
if (op === 'divide') {
|
|
1431
1442
|
params.push(opValue);
|
|
1432
|
-
return `${col} = ${col} /
|
|
1443
|
+
return `${col} = ${col} / ${this.p(params.length)}`;
|
|
1433
1444
|
}
|
|
1434
1445
|
}
|
|
1435
1446
|
// Fall through: multi-key objects or non-operator single-key objects
|
|
@@ -1437,7 +1448,7 @@ class QueryInterface {
|
|
|
1437
1448
|
}
|
|
1438
1449
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
1439
1450
|
params.push(value);
|
|
1440
|
-
return `${col} =
|
|
1451
|
+
return `${col} = ${this.p(params.length)}`;
|
|
1441
1452
|
}
|
|
1442
1453
|
// =========================================================================
|
|
1443
1454
|
// Fingerprinting — value-invariant shape keys for SQL cache lookup
|
|
@@ -1960,7 +1971,7 @@ class QueryInterface {
|
|
|
1960
1971
|
}
|
|
1961
1972
|
}
|
|
1962
1973
|
const rawColumn = this.toColumn(key);
|
|
1963
|
-
const column =
|
|
1974
|
+
const column = this.q(rawColumn);
|
|
1964
1975
|
// Handle null → IS NULL
|
|
1965
1976
|
if (value === null) {
|
|
1966
1977
|
andClauses.push(`${column} IS NULL`);
|
|
@@ -2010,7 +2021,7 @@ class QueryInterface {
|
|
|
2010
2021
|
}
|
|
2011
2022
|
// Plain equality
|
|
2012
2023
|
params.push(value);
|
|
2013
|
-
andClauses.push(`${column} =
|
|
2024
|
+
andClauses.push(`${column} = ${this.p(params.length)}`);
|
|
2014
2025
|
}
|
|
2015
2026
|
if (andClauses.length === 0)
|
|
2016
2027
|
return null;
|
|
@@ -2025,18 +2036,18 @@ class QueryInterface {
|
|
|
2025
2036
|
const targetMeta = this.schema.tables[targetTable];
|
|
2026
2037
|
if (!targetMeta)
|
|
2027
2038
|
return null;
|
|
2028
|
-
const qt =
|
|
2029
|
-
const qSelf =
|
|
2039
|
+
const qt = this.q(targetTable);
|
|
2040
|
+
const qSelf = this.q(this.table);
|
|
2030
2041
|
const clauses = [];
|
|
2031
2042
|
// Correlation: link child table to parent table (supports composite FKs)
|
|
2032
2043
|
let correlation;
|
|
2033
2044
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
2034
2045
|
// parent.pk = child.fk
|
|
2035
|
-
correlation =
|
|
2046
|
+
correlation = this.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
|
|
2036
2047
|
}
|
|
2037
2048
|
else {
|
|
2038
2049
|
// belongsTo: parent.fk = child.pk
|
|
2039
|
-
correlation =
|
|
2050
|
+
correlation = this.dialect.buildCorrelation(qt, relDef.referenceKey, qSelf, relDef.foreignKey);
|
|
2040
2051
|
}
|
|
2041
2052
|
// "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter)
|
|
2042
2053
|
if (filterObj.some !== undefined) {
|
|
@@ -2073,7 +2084,7 @@ class QueryInterface {
|
|
|
2073
2084
|
const meta = this.schema.tables[targetTable];
|
|
2074
2085
|
if (!meta)
|
|
2075
2086
|
return null;
|
|
2076
|
-
const qt =
|
|
2087
|
+
const qt = this.q(targetTable);
|
|
2077
2088
|
const conditions = [];
|
|
2078
2089
|
for (const [field, value] of Object.entries(subWhere)) {
|
|
2079
2090
|
if (value === undefined)
|
|
@@ -2083,7 +2094,7 @@ class QueryInterface {
|
|
|
2083
2094
|
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${field}" in relation filter for table "${targetTable}". ` +
|
|
2084
2095
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
2085
2096
|
}
|
|
2086
|
-
const qCol = `${qt}.${
|
|
2097
|
+
const qCol = `${qt}.${this.q(col)}`;
|
|
2087
2098
|
if (value === null) {
|
|
2088
2099
|
conditions.push(`${qCol} IS NULL`);
|
|
2089
2100
|
continue;
|
|
@@ -2094,7 +2105,7 @@ class QueryInterface {
|
|
|
2094
2105
|
continue;
|
|
2095
2106
|
}
|
|
2096
2107
|
params.push(value);
|
|
2097
|
-
conditions.push(`${qCol} =
|
|
2108
|
+
conditions.push(`${qCol} = ${this.p(params.length)}`);
|
|
2098
2109
|
}
|
|
2099
2110
|
return conditions.length > 0 ? conditions.join(' AND ') : null;
|
|
2100
2111
|
}
|
|
@@ -2106,19 +2117,19 @@ class QueryInterface {
|
|
|
2106
2117
|
const clauses = [];
|
|
2107
2118
|
if (op.gt !== undefined) {
|
|
2108
2119
|
params.push(op.gt);
|
|
2109
|
-
clauses.push(`${column} >
|
|
2120
|
+
clauses.push(`${column} > ${this.p(params.length)}`);
|
|
2110
2121
|
}
|
|
2111
2122
|
if (op.gte !== undefined) {
|
|
2112
2123
|
params.push(op.gte);
|
|
2113
|
-
clauses.push(`${column} >=
|
|
2124
|
+
clauses.push(`${column} >= ${this.p(params.length)}`);
|
|
2114
2125
|
}
|
|
2115
2126
|
if (op.lt !== undefined) {
|
|
2116
2127
|
params.push(op.lt);
|
|
2117
|
-
clauses.push(`${column} <
|
|
2128
|
+
clauses.push(`${column} < ${this.p(params.length)}`);
|
|
2118
2129
|
}
|
|
2119
2130
|
if (op.lte !== undefined) {
|
|
2120
2131
|
params.push(op.lte);
|
|
2121
|
-
clauses.push(`${column} <=
|
|
2132
|
+
clauses.push(`${column} <= ${this.p(params.length)}`);
|
|
2122
2133
|
}
|
|
2123
2134
|
if (op.not !== undefined) {
|
|
2124
2135
|
if (op.not === null) {
|
|
@@ -2126,30 +2137,29 @@ class QueryInterface {
|
|
|
2126
2137
|
}
|
|
2127
2138
|
else {
|
|
2128
2139
|
params.push(op.not);
|
|
2129
|
-
clauses.push(`${column} !=
|
|
2140
|
+
clauses.push(`${column} != ${this.p(params.length)}`);
|
|
2130
2141
|
}
|
|
2131
2142
|
}
|
|
2132
2143
|
if (op.in !== undefined) {
|
|
2133
2144
|
params.push(op.in);
|
|
2134
|
-
clauses.push(`${column} = ANY(
|
|
2145
|
+
clauses.push(`${column} = ANY(${this.p(params.length)})`);
|
|
2135
2146
|
}
|
|
2136
2147
|
if (op.notIn !== undefined) {
|
|
2137
2148
|
params.push(op.notIn);
|
|
2138
|
-
clauses.push(`${column} != ALL(
|
|
2149
|
+
clauses.push(`${column} != ALL(${this.p(params.length)})`);
|
|
2139
2150
|
}
|
|
2140
|
-
|
|
2141
|
-
const likeOp = op.mode === 'insensitive' ? 'ILIKE' : 'LIKE';
|
|
2151
|
+
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? this.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
2142
2152
|
if (op.contains !== undefined) {
|
|
2143
2153
|
params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
|
|
2144
|
-
clauses.push(`${
|
|
2154
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2145
2155
|
}
|
|
2146
2156
|
if (op.startsWith !== undefined) {
|
|
2147
2157
|
params.push(`${(0, utils_js_1.escapeLike)(op.startsWith)}%`);
|
|
2148
|
-
clauses.push(`${
|
|
2158
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2149
2159
|
}
|
|
2150
2160
|
if (op.endsWith !== undefined) {
|
|
2151
2161
|
params.push(`%${(0, utils_js_1.escapeLike)(op.endsWith)}`);
|
|
2152
|
-
clauses.push(`${
|
|
2162
|
+
clauses.push(`${buildLikeClause(this.p(params.length))} ESCAPE '\\'`);
|
|
2153
2163
|
}
|
|
2154
2164
|
return clauses;
|
|
2155
2165
|
}
|
|
@@ -2309,8 +2319,8 @@ class QueryInterface {
|
|
|
2309
2319
|
if (!meta)
|
|
2310
2320
|
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}"`);
|
|
2311
2321
|
const cols = columnsList ?? meta.allColumns;
|
|
2312
|
-
const qtbl =
|
|
2313
|
-
const baseCols = cols.map((col) => `${qtbl}.${
|
|
2322
|
+
const qtbl = this.q(table);
|
|
2323
|
+
const baseCols = cols.map((col) => `${qtbl}.${this.q(col)}`).join(', ');
|
|
2314
2324
|
const relationSelects = [];
|
|
2315
2325
|
const aliasCounter = { n: 0 };
|
|
2316
2326
|
for (const [relName, relSpec] of Object.entries(withClause)) {
|
|
@@ -2321,7 +2331,7 @@ class QueryInterface {
|
|
|
2321
2331
|
}
|
|
2322
2332
|
// The main table is not aliased, so pass table name as parentRef
|
|
2323
2333
|
const subquery = this.buildRelationSubquery(relDef, relSpec, params, table, aliasCounter, depth, path);
|
|
2324
|
-
relationSelects.push(`(${subquery}) AS ${
|
|
2334
|
+
relationSelects.push(`(${subquery}) AS ${this.q(relName)}`);
|
|
2325
2335
|
}
|
|
2326
2336
|
return [baseCols, ...relationSelects].join(', ');
|
|
2327
2337
|
}
|
|
@@ -2383,7 +2393,7 @@ class QueryInterface {
|
|
|
2383
2393
|
* 8. **Parameter threading:** All user-supplied values (where filters, limit) are
|
|
2384
2394
|
* pushed to the shared `params` array with `$N` placeholders. No string
|
|
2385
2395
|
* interpolation of user data ever occurs -- all identifiers go through
|
|
2386
|
-
* `
|
|
2396
|
+
* `this.q()` and all values are parameterized.
|
|
2387
2397
|
*
|
|
2388
2398
|
* ### Example output (hasMany with nested relation)
|
|
2389
2399
|
* ```sql
|
|
@@ -2447,8 +2457,11 @@ class QueryInterface {
|
|
|
2447
2457
|
.map(([k]) => targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k)));
|
|
2448
2458
|
targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
|
|
2449
2459
|
}
|
|
2450
|
-
// Build
|
|
2451
|
-
const jsonPairs = targetColumns.map((col) =>
|
|
2460
|
+
// Build JSON object pairs for resolved columns
|
|
2461
|
+
const jsonPairs = targetColumns.map((col) => [
|
|
2462
|
+
targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
2463
|
+
`${alias}.${this.q(col)}`,
|
|
2464
|
+
]);
|
|
2452
2465
|
// Determine if this hasMany will take the wrapped subquery path (LIMIT or ORDER BY).
|
|
2453
2466
|
// When wrapping, nested relations are built in the wrapped path referencing innerAlias,
|
|
2454
2467
|
// so we must NOT build them here (they would push orphaned params).
|
|
@@ -2464,14 +2477,14 @@ class QueryInterface {
|
|
|
2464
2477
|
// Recursively build nested subquery, passing THIS alias as the parent reference
|
|
2465
2478
|
const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
2466
2479
|
// Use '[]'::json for hasMany (empty array), NULL for belongsTo/hasOne (no object)
|
|
2467
|
-
const fallback = nestedRelDef.type === 'hasMany' ?
|
|
2468
|
-
jsonPairs.push(
|
|
2480
|
+
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
2481
|
+
jsonPairs.push([nestedRelName, `COALESCE((${nestedSubquery}), ${fallback})`]);
|
|
2469
2482
|
}
|
|
2470
2483
|
}
|
|
2471
|
-
const jsonObj =
|
|
2484
|
+
const jsonObj = this.dialect.buildJsonObject(jsonPairs);
|
|
2472
2485
|
// Quote parent ref — can be a table name or auto-generated alias
|
|
2473
|
-
const qParent =
|
|
2474
|
-
const qTarget =
|
|
2486
|
+
const qParent = this.q(parentRef);
|
|
2487
|
+
const qTarget = this.q(targetTable);
|
|
2475
2488
|
// Build ORDER BY for json_agg
|
|
2476
2489
|
let orderClause = '';
|
|
2477
2490
|
if (spec !== true && spec.orderBy) {
|
|
@@ -2482,7 +2495,7 @@ class QueryInterface {
|
|
|
2482
2495
|
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in orderBy for table "${targetTable}"`);
|
|
2483
2496
|
}
|
|
2484
2497
|
const safeDir = dir.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
2485
|
-
return `${alias}.${
|
|
2498
|
+
return `${alias}.${this.q(col)} ${safeDir}`;
|
|
2486
2499
|
})
|
|
2487
2500
|
.join(', ');
|
|
2488
2501
|
orderClause = ` ORDER BY ${orders}`;
|
|
@@ -2493,10 +2506,10 @@ class QueryInterface {
|
|
|
2493
2506
|
// Supports composite foreign keys (string[]) via buildCorrelation.
|
|
2494
2507
|
let whereClause;
|
|
2495
2508
|
if (relDef.type === 'belongsTo' || relDef.type === 'hasOne') {
|
|
2496
|
-
whereClause =
|
|
2509
|
+
whereClause = this.dialect.buildCorrelation(alias, relDef.referenceKey, qParent, relDef.foreignKey);
|
|
2497
2510
|
}
|
|
2498
2511
|
else {
|
|
2499
|
-
whereClause =
|
|
2512
|
+
whereClause = this.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
|
|
2500
2513
|
}
|
|
2501
2514
|
// Additional filters — properly parameterized
|
|
2502
2515
|
if (spec !== true && spec.where) {
|
|
@@ -2506,14 +2519,14 @@ class QueryInterface {
|
|
|
2506
2519
|
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${k}" in where for table "${targetTable}"`);
|
|
2507
2520
|
}
|
|
2508
2521
|
params.push(v);
|
|
2509
|
-
whereClause += ` AND ${alias}.${
|
|
2522
|
+
whereClause += ` AND ${alias}.${this.q(col)} = ${this.p(params.length)}`;
|
|
2510
2523
|
}
|
|
2511
2524
|
}
|
|
2512
2525
|
// LIMIT
|
|
2513
2526
|
let limitClause = '';
|
|
2514
2527
|
if (spec !== true && spec.limit) {
|
|
2515
2528
|
params.push(Number(spec.limit));
|
|
2516
|
-
limitClause = ` LIMIT
|
|
2529
|
+
limitClause = ` LIMIT ${this.p(params.length)}`;
|
|
2517
2530
|
}
|
|
2518
2531
|
if (relDef.type === 'hasMany') {
|
|
2519
2532
|
// When LIMIT or ORDER BY is used, wrap in a subquery so LIMIT applies to rows
|
|
@@ -2522,9 +2535,12 @@ class QueryInterface {
|
|
|
2522
2535
|
const innerAlias = `${alias}i`;
|
|
2523
2536
|
// Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
|
|
2524
2537
|
// 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}.${
|
|
2538
|
+
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${this.q(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
|
|
2526
2539
|
// For the json_build_object, reference the inner alias — only include resolved columns
|
|
2527
|
-
const innerJsonPairs = targetColumns.map((col) =>
|
|
2540
|
+
const innerJsonPairs = targetColumns.map((col) => [
|
|
2541
|
+
targetMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col),
|
|
2542
|
+
`${innerAlias}.${this.q(col)}`,
|
|
2543
|
+
]);
|
|
2528
2544
|
// Build nested relation subqueries referencing innerAlias
|
|
2529
2545
|
if (spec !== true && spec.with) {
|
|
2530
2546
|
for (const [nestedRelName, nestedSpec] of Object.entries(spec.with)) {
|
|
@@ -2534,14 +2550,14 @@ class QueryInterface {
|
|
|
2534
2550
|
`Available: ${Object.keys(targetMeta.relations).join(', ')}`);
|
|
2535
2551
|
}
|
|
2536
2552
|
const nestedSub = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, innerAlias, aliasCounter, currentDepth + 1, [...currentPath, relDef.name]);
|
|
2537
|
-
const fallback = nestedRelDef.type === 'hasMany' ?
|
|
2538
|
-
innerJsonPairs.push(
|
|
2553
|
+
const fallback = nestedRelDef.type === 'hasMany' ? this.dialect.emptyJsonArrayLiteral : this.dialect.nullJsonLiteral;
|
|
2554
|
+
innerJsonPairs.push([nestedRelName, `COALESCE((${nestedSub}), ${fallback})`]);
|
|
2539
2555
|
}
|
|
2540
2556
|
}
|
|
2541
|
-
const innerJsonObj =
|
|
2542
|
-
return `SELECT
|
|
2557
|
+
const innerJsonObj = this.dialect.buildJsonObject(innerJsonPairs);
|
|
2558
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(innerJsonObj)} FROM (${innerSql}) ${innerAlias}`;
|
|
2543
2559
|
}
|
|
2544
|
-
return `SELECT
|
|
2560
|
+
return `SELECT ${this.dialect.buildJsonArrayAgg(jsonObj, orderClause.trim() || undefined)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
2545
2561
|
}
|
|
2546
2562
|
// belongsTo / hasOne — return single object
|
|
2547
2563
|
return `SELECT ${jsonObj} FROM ${qTarget} ${alias} WHERE ${whereClause} LIMIT 1`;
|
|
@@ -2588,22 +2604,22 @@ class QueryInterface {
|
|
|
2588
2604
|
params.push(filter.path);
|
|
2589
2605
|
const pathParam = params.length;
|
|
2590
2606
|
params.push(String(filter.equals));
|
|
2591
|
-
clauses.push(`${column
|
|
2607
|
+
clauses.push(`${this.dialect.buildJsonPathExtract(column, this.p(pathParam))} = ${this.p(params.length)}`);
|
|
2592
2608
|
}
|
|
2593
2609
|
else if (filter.equals !== undefined) {
|
|
2594
2610
|
// Containment equality: column @> $N::jsonb
|
|
2595
2611
|
params.push(JSON.stringify(filter.equals));
|
|
2596
|
-
clauses.push(
|
|
2612
|
+
clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
|
|
2597
2613
|
}
|
|
2598
2614
|
if (filter.contains !== undefined) {
|
|
2599
2615
|
// Containment: column @> $N::jsonb
|
|
2600
2616
|
params.push(JSON.stringify(filter.contains));
|
|
2601
|
-
clauses.push(
|
|
2617
|
+
clauses.push(this.dialect.buildJsonContains(column, this.p(params.length)));
|
|
2602
2618
|
}
|
|
2603
2619
|
if (filter.hasKey !== undefined) {
|
|
2604
2620
|
// Key existence: column ? $N
|
|
2605
2621
|
params.push(filter.hasKey);
|
|
2606
|
-
clauses.push(`${column} ?
|
|
2622
|
+
clauses.push(`${column} ? ${this.p(params.length)}`);
|
|
2607
2623
|
}
|
|
2608
2624
|
return clauses;
|
|
2609
2625
|
}
|
|
@@ -2617,17 +2633,17 @@ class QueryInterface {
|
|
|
2617
2633
|
if (filter.has !== undefined) {
|
|
2618
2634
|
// value = ANY(column)
|
|
2619
2635
|
params.push(filter.has);
|
|
2620
|
-
clauses.push(
|
|
2636
|
+
clauses.push(`${this.p(params.length)} = ANY(${column})`);
|
|
2621
2637
|
}
|
|
2622
2638
|
if (filter.hasEvery !== undefined) {
|
|
2623
2639
|
// column @> ARRAY[...]::type[]
|
|
2624
2640
|
params.push(filter.hasEvery);
|
|
2625
|
-
clauses.push(`${column} @>
|
|
2641
|
+
clauses.push(`${column} @> ${this.p(params.length)}::${elementType}[]`);
|
|
2626
2642
|
}
|
|
2627
2643
|
if (filter.hasSome !== undefined) {
|
|
2628
2644
|
// column && ARRAY[...]::type[]
|
|
2629
2645
|
params.push(filter.hasSome);
|
|
2630
|
-
clauses.push(`${column} &&
|
|
2646
|
+
clauses.push(`${column} && ${this.p(params.length)}::${elementType}[]`);
|
|
2631
2647
|
}
|
|
2632
2648
|
if (filter.isEmpty === true) {
|
|
2633
2649
|
// array_length(column, 1) IS NULL
|