@aetherframework/database 1.0.9 → 1.1.1
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/package.json +1 -2
- package/src/DatabaseManager.js +0 -565
- package/src/core/ConnectionManager.js +0 -351
- package/src/core/DatabaseFactory.js +0 -188
- package/src/core/MongoQueryBuilder.js +0 -576
- package/src/core/PluginManager.js +0 -968
- package/src/core/QueryBuilder.js +0 -4398
- package/src/core/TransactionManager.js +0 -40
- package/src/drivers/clickhouse-driver.js +0 -272
- package/src/drivers/index.js +0 -273
- package/src/drivers/mongodb-driver.js +0 -87
- package/src/drivers/mssql-driver.js +0 -117
- package/src/drivers/mysql-driver.js +0 -169
- package/src/drivers/oracle-driver.js +0 -101
- package/src/drivers/postgres-driver.js +0 -234
- package/src/drivers/redis-driver.js +0 -52
- package/src/drivers/sqlite-driver.js +0 -67
- package/src/middleware/connection-pool.js +0 -455
- package/src/middleware/performance-monitor.js +0 -652
- package/src/middleware/query-cache.js +0 -500
- package/src/middleware/query-logger.js +0 -262
- package/src/plugins/AuditPlugin.js +0 -447
- package/src/plugins/BasePlugin.js +0 -418
- package/src/plugins/BatchOperationPlugin.js +0 -165
- package/src/plugins/CachePlugin.js +0 -407
- package/src/plugins/CtePlugin.js +0 -523
- package/src/plugins/DistributedPlugin.js +0 -543
- package/src/plugins/EncryptionPlugin.js +0 -211
- package/src/plugins/FullTextSearchPlugin.js +0 -164
- package/src/plugins/GeospatialPlugin.js +0 -219
- package/src/plugins/GraphQLPlugin.js +0 -162
- package/src/plugins/HookPlugin.js +0 -211
- package/src/plugins/JsonPlugin.js +0 -366
- package/src/plugins/OptimisticLockPlugin.js +0 -374
- package/src/plugins/PerformancePlugin.js +0 -175
- package/src/plugins/ResiliencePlugin.js +0 -114
- package/src/plugins/ShardingPlugin.js +0 -227
- package/src/plugins/SoftDeletePlugin.js +0 -258
- package/src/plugins/SyncPlugin.js +0 -373
- package/src/plugins/VersioningPlugin.js +0 -314
- package/src/plugins/WindowFunctionPlugin.js +0 -343
- package/src/utils/config-loader.js +0 -632
- package/src/utils/error-handler.js +0 -724
- package/src/utils/migration-runner.js +0 -1066
package/src/core/QueryBuilder.js
DELETED
|
@@ -1,4398 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/database/core/QueryBuilder
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { EventEmitter } from "events";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Core Query Builder - Provides fluent interface for building SQL queries
|
|
12
|
-
* Supports multiple database dialects with clean, focused API
|
|
13
|
-
*/
|
|
14
|
-
class QueryBuilder extends EventEmitter {
|
|
15
|
-
/**
|
|
16
|
-
* Create a new QueryBuilder instance
|
|
17
|
-
* @param {string} tableName - Table name
|
|
18
|
-
* @param {Object} connection - Database connection
|
|
19
|
-
* @param {string} dialect - Database dialect (mysql, postgresql, sqlite, etc.)
|
|
20
|
-
*/
|
|
21
|
-
constructor(tableName, connection, dialect = "mysql") {
|
|
22
|
-
super();
|
|
23
|
-
|
|
24
|
-
this.dialect = dialect.toLowerCase();
|
|
25
|
-
this.tableName = tableName;
|
|
26
|
-
this.connection = connection;
|
|
27
|
-
|
|
28
|
-
// Supported database dialects
|
|
29
|
-
const supportedDialects = [
|
|
30
|
-
"mysql",
|
|
31
|
-
"mariadb",
|
|
32
|
-
"postgresql",
|
|
33
|
-
"postgres",
|
|
34
|
-
"pg",
|
|
35
|
-
"sqlite",
|
|
36
|
-
"sqlite3",
|
|
37
|
-
"mssql",
|
|
38
|
-
"sqlserver",
|
|
39
|
-
"oracle",
|
|
40
|
-
"cockroachdb",
|
|
41
|
-
"cockroach",
|
|
42
|
-
"clickhouse",
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
if (!supportedDialects.includes(this.dialect)) {
|
|
46
|
-
throw new Error(`Unsupported database dialect: ${this.dialect}`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Initialize dialect adapter
|
|
50
|
-
this.adapter = new DialectAdapter(this.dialect);
|
|
51
|
-
|
|
52
|
-
// Initialize query structure
|
|
53
|
-
this.query = {
|
|
54
|
-
type: "select",
|
|
55
|
-
columns: ["*"],
|
|
56
|
-
where: [],
|
|
57
|
-
orderBy: [],
|
|
58
|
-
limit: null,
|
|
59
|
-
offset: null,
|
|
60
|
-
joins: [],
|
|
61
|
-
groupBy: [],
|
|
62
|
-
having: [],
|
|
63
|
-
distinct: false,
|
|
64
|
-
lock: null,
|
|
65
|
-
data: null,
|
|
66
|
-
returning: null,
|
|
67
|
-
union: [],
|
|
68
|
-
with: [],
|
|
69
|
-
cte: [],
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
this.bindings = [];
|
|
73
|
-
this.paramIndex = 1;
|
|
74
|
-
this.subQueries = new Map();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ==================== CORE CHAINING METHODS ====================
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Select columns
|
|
81
|
-
* @param {...string} columns - Columns to select
|
|
82
|
-
* @returns {QueryBuilder} Query builder instance
|
|
83
|
-
*/
|
|
84
|
-
select(...columns) {
|
|
85
|
-
if (columns.length === 0) {
|
|
86
|
-
this.query.columns = ["*"];
|
|
87
|
-
} else {
|
|
88
|
-
this.query.columns = columns.flat();
|
|
89
|
-
}
|
|
90
|
-
return this;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Add WHERE condition
|
|
95
|
-
* @param {string} column - Column name
|
|
96
|
-
* @param {string} operator - Comparison operator
|
|
97
|
-
* @param {*} value - Value to compare
|
|
98
|
-
* @returns {QueryBuilder} Query builder instance
|
|
99
|
-
*/
|
|
100
|
-
where(column, operatorOrValue, value) {
|
|
101
|
-
let operator = operatorOrValue;
|
|
102
|
-
let val = value;
|
|
103
|
-
|
|
104
|
-
if (arguments.length === 2) {
|
|
105
|
-
val = operatorOrValue;
|
|
106
|
-
operator = "=";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
this.query.where.push({
|
|
110
|
-
column,
|
|
111
|
-
operator,
|
|
112
|
-
value: val,
|
|
113
|
-
boolean: "and",
|
|
114
|
-
type: "basic",
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
if (val !== undefined && val !== null) {
|
|
118
|
-
this.bindings.push(val);
|
|
119
|
-
}
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Add OR WHERE condition
|
|
125
|
-
* @param {string} column - Column name
|
|
126
|
-
* @param {string} operator - Comparison operator
|
|
127
|
-
* @param {*} value - Value to compare
|
|
128
|
-
* @returns {QueryBuilder} Query builder instance
|
|
129
|
-
*/
|
|
130
|
-
orWhere(column, operator, value) {
|
|
131
|
-
this.query.where.push({
|
|
132
|
-
column,
|
|
133
|
-
operator,
|
|
134
|
-
value,
|
|
135
|
-
boolean: "or",
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
if (value !== undefined && value !== null) {
|
|
139
|
-
this.bindings.push(value);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return this;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Add WHERE NULL condition
|
|
147
|
-
* @param {string} column - Column name
|
|
148
|
-
* @returns {QueryBuilder} Query builder instance
|
|
149
|
-
*/
|
|
150
|
-
whereNull(column) {
|
|
151
|
-
this.query.where.push({
|
|
152
|
-
column,
|
|
153
|
-
operator: "IS",
|
|
154
|
-
value: null,
|
|
155
|
-
boolean: "and",
|
|
156
|
-
type: "null",
|
|
157
|
-
});
|
|
158
|
-
return this;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Add WHERE NOT NULL condition
|
|
163
|
-
* @param {string} column - Column name
|
|
164
|
-
* @returns {QueryBuilder} Query builder instance
|
|
165
|
-
*/
|
|
166
|
-
whereNotNull(column) {
|
|
167
|
-
this.query.where.push({
|
|
168
|
-
column,
|
|
169
|
-
operator: "IS NOT",
|
|
170
|
-
value: null,
|
|
171
|
-
boolean: "and",
|
|
172
|
-
type: "notnull",
|
|
173
|
-
});
|
|
174
|
-
return this;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Add WHERE IN condition
|
|
179
|
-
* @param {string} column - Column name
|
|
180
|
-
* @param {Array} values - Array of values
|
|
181
|
-
* @returns {QueryBuilder} Query builder instance
|
|
182
|
-
*/
|
|
183
|
-
whereIn(column, values) {
|
|
184
|
-
this.query.where.push({
|
|
185
|
-
column,
|
|
186
|
-
operator: "IN",
|
|
187
|
-
value: values,
|
|
188
|
-
boolean: "and",
|
|
189
|
-
type: "in",
|
|
190
|
-
});
|
|
191
|
-
this.bindings.push(...values);
|
|
192
|
-
return this;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Add WHERE NOT IN condition
|
|
197
|
-
* @param {string} column - Column name
|
|
198
|
-
* @param {Array} values - Array of values
|
|
199
|
-
* @returns {QueryBuilder} Query builder instance
|
|
200
|
-
*/
|
|
201
|
-
whereNotIn(column, values) {
|
|
202
|
-
this.query.where.push({
|
|
203
|
-
column,
|
|
204
|
-
operator: "NOT IN",
|
|
205
|
-
value: values,
|
|
206
|
-
boolean: "and",
|
|
207
|
-
type: "notin",
|
|
208
|
-
});
|
|
209
|
-
this.bindings.push(...values);
|
|
210
|
-
return this;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Add WHERE BETWEEN condition
|
|
215
|
-
* @param {string} column - Column name
|
|
216
|
-
* @param {Array} values - Array with [min, max] values
|
|
217
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
218
|
-
* @param {boolean} not - Whether to use NOT BETWEEN
|
|
219
|
-
* @returns {QueryBuilder} Query builder instance
|
|
220
|
-
*/
|
|
221
|
-
whereBetween(column, values, boolean = "and", not = false) {
|
|
222
|
-
if (!Array.isArray(values) || values.length !== 2) {
|
|
223
|
-
throw new Error("whereBetween requires an array with exactly two values");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
this.query.where.push({
|
|
227
|
-
type: "between",
|
|
228
|
-
column,
|
|
229
|
-
values,
|
|
230
|
-
boolean,
|
|
231
|
-
not,
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
this.bindings.push(...values);
|
|
235
|
-
return this;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Add WHERE LIKE condition
|
|
240
|
-
* @param {string} column - Column name
|
|
241
|
-
* @param {string} pattern - LIKE pattern
|
|
242
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
243
|
-
* @returns {QueryBuilder} Query builder instance
|
|
244
|
-
*/
|
|
245
|
-
whereLike(column, pattern, boolean = "and") {
|
|
246
|
-
this.query.where.push({
|
|
247
|
-
column,
|
|
248
|
-
operator: "LIKE",
|
|
249
|
-
value: pattern,
|
|
250
|
-
boolean,
|
|
251
|
-
});
|
|
252
|
-
this.bindings.push(pattern);
|
|
253
|
-
return this;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Add raw WHERE condition
|
|
258
|
-
* @param {string} sql - Raw SQL condition
|
|
259
|
-
* @param {Array} bindings - Bindings for raw SQL
|
|
260
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
261
|
-
* @returns {QueryBuilder} Query builder instance
|
|
262
|
-
*/
|
|
263
|
-
whereRaw(sql, bindings = [], boolean = "and") {
|
|
264
|
-
this.query.where.push({
|
|
265
|
-
raw: sql,
|
|
266
|
-
boolean,
|
|
267
|
-
});
|
|
268
|
-
this.bindings.push(...bindings);
|
|
269
|
-
return this;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Add ORDER BY clause
|
|
274
|
-
* @param {string|Object|Array} column - Column name or ordering object
|
|
275
|
-
* @param {string} direction - Sort direction (ASC, DESC)
|
|
276
|
-
* @returns {QueryBuilder} Query builder instance
|
|
277
|
-
*/
|
|
278
|
-
orderBy(column, direction = "ASC") {
|
|
279
|
-
if (!this.query.orderBy) this.query.orderBy = [];
|
|
280
|
-
|
|
281
|
-
// Support orderBy({ age: 'desc', name: 'asc' })
|
|
282
|
-
if (
|
|
283
|
-
typeof column === "object" &&
|
|
284
|
-
column !== null &&
|
|
285
|
-
!Array.isArray(column)
|
|
286
|
-
) {
|
|
287
|
-
Object.entries(column).forEach(([col, dir]) => {
|
|
288
|
-
this.orderBy(col, dir);
|
|
289
|
-
});
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Support orderBy(['age', 'name'])
|
|
294
|
-
if (Array.isArray(column)) {
|
|
295
|
-
column.forEach((item) => this.orderBy(item));
|
|
296
|
-
return this;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Support orderBy({column: 'age', direction: 'desc'})
|
|
300
|
-
if (typeof column === "object" && column.column) {
|
|
301
|
-
const dir = (column.direction || direction).toUpperCase();
|
|
302
|
-
this.query.orderBy.push({
|
|
303
|
-
column: column.column,
|
|
304
|
-
direction: ["ASC", "DESC"].includes(dir) ? dir : "ASC",
|
|
305
|
-
});
|
|
306
|
-
return this;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Regular call: orderBy('age', 'desc')
|
|
310
|
-
if (typeof column === "string") {
|
|
311
|
-
const dir = String(direction).toUpperCase();
|
|
312
|
-
this.query.orderBy.push({
|
|
313
|
-
column,
|
|
314
|
-
direction: ["ASC", "DESC"].includes(dir) ? dir : "ASC",
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return this;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Add raw ORDER BY clause
|
|
323
|
-
* @param {string} sql - Raw SQL ORDER BY expression
|
|
324
|
-
* @param {Array} bindings - Bindings for raw SQL
|
|
325
|
-
* @returns {QueryBuilder} Query builder instance
|
|
326
|
-
*/
|
|
327
|
-
orderByRaw(sql, bindings = []) {
|
|
328
|
-
if (!this.query.orderBy) this.query.orderBy = [];
|
|
329
|
-
this.query.orderBy.push({ raw: sql });
|
|
330
|
-
if (bindings.length > 0) this.bindings.push(...bindings);
|
|
331
|
-
return this;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Add LIMIT clause
|
|
336
|
-
* @param {number} value - Limit value
|
|
337
|
-
* @returns {QueryBuilder} Query builder instance
|
|
338
|
-
*/
|
|
339
|
-
limit(value) {
|
|
340
|
-
this.query.limit = value;
|
|
341
|
-
return this;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Add OFFSET clause
|
|
346
|
-
* @param {number} value - Offset value
|
|
347
|
-
* @returns {QueryBuilder} Query builder instance
|
|
348
|
-
*/
|
|
349
|
-
offset(value) {
|
|
350
|
-
this.query.offset = value;
|
|
351
|
-
return this;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Add DISTINCT clause
|
|
356
|
-
* @returns {QueryBuilder} Query builder instance
|
|
357
|
-
*/
|
|
358
|
-
distinct() {
|
|
359
|
-
this.query.distinct = true;
|
|
360
|
-
return this;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Add GROUP BY clause
|
|
365
|
-
* @param {...string} columns - Columns to group by
|
|
366
|
-
* @returns {QueryBuilder} Query builder instance
|
|
367
|
-
*/
|
|
368
|
-
groupBy(...columns) {
|
|
369
|
-
this.query.groupBy.push(...columns);
|
|
370
|
-
return this;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Add HAVING condition
|
|
375
|
-
* @param {string} column - Column name
|
|
376
|
-
* @param {string} operator - Comparison operator
|
|
377
|
-
* @param {*} value - Value to compare
|
|
378
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
379
|
-
* @returns {QueryBuilder} Query builder instance
|
|
380
|
-
*/
|
|
381
|
-
having(column, operator, value, boolean = "and") {
|
|
382
|
-
this.query.having.push({
|
|
383
|
-
column,
|
|
384
|
-
operator,
|
|
385
|
-
value,
|
|
386
|
-
boolean,
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
if (value !== undefined && value !== null) {
|
|
390
|
-
this.bindings.push(value);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return this;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ==================== JOIN METHODS ====================
|
|
397
|
-
|
|
398
|
-
/**
|
|
399
|
-
* Join tables
|
|
400
|
-
* @param {string} table - Table to join
|
|
401
|
-
* @param {string} first - First column
|
|
402
|
-
* @param {string} operator - Comparison operator
|
|
403
|
-
* @param {string} second - Second column
|
|
404
|
-
* @param {string} type - Join type (inner, left, right, cross)
|
|
405
|
-
* @returns {QueryBuilder} Query builder instance
|
|
406
|
-
*/
|
|
407
|
-
join(table, first, operator, second, type = "inner") {
|
|
408
|
-
this.query.joins.push({
|
|
409
|
-
type,
|
|
410
|
-
table,
|
|
411
|
-
first,
|
|
412
|
-
operator,
|
|
413
|
-
second,
|
|
414
|
-
});
|
|
415
|
-
return this;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Left join tables
|
|
420
|
-
* @param {string} table - Table to join
|
|
421
|
-
* @param {string} first - First column
|
|
422
|
-
* @param {string} operator - Comparison operator
|
|
423
|
-
* @param {string} second - Second column
|
|
424
|
-
* @returns {QueryBuilder} Query builder instance
|
|
425
|
-
*/
|
|
426
|
-
leftJoin(table, first, operator, second) {
|
|
427
|
-
return this.join(table, first, operator, second, "left");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Right join tables
|
|
432
|
-
* @param {string} table - Table to join
|
|
433
|
-
* @param {string} first - First column
|
|
434
|
-
* @param {string} operator - Comparison operator
|
|
435
|
-
* @param {string} second - Second column
|
|
436
|
-
* @returns {QueryBuilder} Query builder instance
|
|
437
|
-
*/
|
|
438
|
-
rightJoin(table, first, operator, second) {
|
|
439
|
-
return this.join(table, first, operator, second, "right");
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Cross join tables
|
|
444
|
-
* @param {string} table - Table to join
|
|
445
|
-
* @returns {QueryBuilder} Query builder instance
|
|
446
|
-
*/
|
|
447
|
-
crossJoin(table) {
|
|
448
|
-
return this.join(table, null, null, null, "cross");
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ==================== CRUD OPERATIONS ====================
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Set query type to INSERT
|
|
455
|
-
* @param {Object|Array} data - Data to insert
|
|
456
|
-
* @returns {QueryBuilder} Query builder instance
|
|
457
|
-
*/
|
|
458
|
-
insert(data) {
|
|
459
|
-
this.query.type = "insert";
|
|
460
|
-
this.query.data = null;
|
|
461
|
-
this.query.isBatch = false;
|
|
462
|
-
|
|
463
|
-
if (Array.isArray(data)) {
|
|
464
|
-
if (data.length === 0) {
|
|
465
|
-
throw new Error("Batch insert data array cannot be empty");
|
|
466
|
-
}
|
|
467
|
-
this.query.data = data;
|
|
468
|
-
this.query.isBatch = true;
|
|
469
|
-
} else if (typeof data === "object" && data !== null) {
|
|
470
|
-
this.query.data = [data];
|
|
471
|
-
this.query.isBatch = false;
|
|
472
|
-
} else {
|
|
473
|
-
throw new Error(
|
|
474
|
-
"insert() method must accept an object or array of objects",
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
return this;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Set query type to UPDATE
|
|
483
|
-
* @param {Object} data - Data to update
|
|
484
|
-
* @returns {QueryBuilder} Query builder instance
|
|
485
|
-
*/
|
|
486
|
-
update(data) {
|
|
487
|
-
this.query.type = "update";
|
|
488
|
-
this.query.data = data;
|
|
489
|
-
return this;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ==================== AGGREGATE FUNCTIONS ====================
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Count rows
|
|
496
|
-
* @param {string} column - Column to count (default: '*')
|
|
497
|
-
* @param {string} alias - Column alias
|
|
498
|
-
* @returns {QueryBuilder} Query builder instance
|
|
499
|
-
*/
|
|
500
|
-
count(column = "*", alias = null) {
|
|
501
|
-
const countExpr = `COUNT(${column === "*" ? "*" : this.wrapColumn(column)})`;
|
|
502
|
-
const selectExpr = alias ? `${countExpr} as ${alias}` : countExpr;
|
|
503
|
-
|
|
504
|
-
if (this.query.columns.length === 1 && this.query.columns === "*") {
|
|
505
|
-
this.query.columns = [selectExpr];
|
|
506
|
-
} else {
|
|
507
|
-
this.query.columns.push(selectExpr);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return this;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Sum of column values
|
|
515
|
-
* @param {string} column - Column to sum
|
|
516
|
-
* @param {string} alias - Column alias
|
|
517
|
-
* @returns {QueryBuilder} Query builder instance
|
|
518
|
-
*/
|
|
519
|
-
sum(column, alias = null) {
|
|
520
|
-
const sumExpr = `SUM(${this.wrapColumn(column)})`;
|
|
521
|
-
const selectExpr = alias ? `${sumExpr} as ${alias}` : sumExpr;
|
|
522
|
-
|
|
523
|
-
if (this.query.columns.length === 1 && this.query.columns === "*") {
|
|
524
|
-
this.query.columns = [selectExpr];
|
|
525
|
-
} else {
|
|
526
|
-
this.query.columns.push(selectExpr);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return this;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Average of column values
|
|
534
|
-
* @param {string} column - Column to average
|
|
535
|
-
* @param {string} alias - Column alias
|
|
536
|
-
* @returns {QueryBuilder} Query builder instance
|
|
537
|
-
*/
|
|
538
|
-
avg(column, alias = null) {
|
|
539
|
-
const avgExpr = `AVG(${this.wrapColumn(column)})`;
|
|
540
|
-
const selectExpr = alias ? `${avgExpr} as ${alias}` : avgExpr;
|
|
541
|
-
|
|
542
|
-
if (this.query.columns.length === 1 && this.query.columns === "*") {
|
|
543
|
-
this.query.columns = [selectExpr];
|
|
544
|
-
} else {
|
|
545
|
-
this.query.columns.push(selectExpr);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return this;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Minimum value of column
|
|
553
|
-
* @param {string} column - Column to get minimum
|
|
554
|
-
* @param {string} alias - Column alias
|
|
555
|
-
* @returns {QueryBuilder} Query builder instance
|
|
556
|
-
*/
|
|
557
|
-
min(column, alias = null) {
|
|
558
|
-
const minExpr = `MIN(${this.wrapColumn(column)})`;
|
|
559
|
-
const selectExpr = alias ? `${minExpr} as ${alias}` : minExpr;
|
|
560
|
-
|
|
561
|
-
if (this.query.columns.length === 1 && this.query.columns === "*") {
|
|
562
|
-
this.query.columns = [selectExpr];
|
|
563
|
-
} else {
|
|
564
|
-
this.query.columns.push(selectExpr);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return this;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Maximum value of column
|
|
572
|
-
* @param {string} column - Column to get maximum
|
|
573
|
-
* @param {string} alias - Column alias
|
|
574
|
-
* @returns {QueryBuilder} Query builder instance
|
|
575
|
-
*/
|
|
576
|
-
max(column, alias = null) {
|
|
577
|
-
const maxExpr = `MAX(${this.wrapColumn(column)})`;
|
|
578
|
-
const selectExpr = alias ? `${maxExpr} as ${alias}` : maxExpr;
|
|
579
|
-
|
|
580
|
-
if (this.query.columns.length === 1 && this.query.columns === "*") {
|
|
581
|
-
this.query.columns = [selectExpr];
|
|
582
|
-
} else {
|
|
583
|
-
this.query.columns.push(selectExpr);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
return this;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
// ==================== EXECUTION METHODS ====================
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Execute the query
|
|
593
|
-
* @returns {Promise<Object>} Query result
|
|
594
|
-
*/
|
|
595
|
-
async execute() {
|
|
596
|
-
const { sql, bindings } = this.toSQL();
|
|
597
|
-
return this.executeQuery(sql, bindings);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* Execute SELECT query and get all results
|
|
602
|
-
* @returns {Promise<Array>} Query results
|
|
603
|
-
*/
|
|
604
|
-
async get() {
|
|
605
|
-
this.query.type = "select";
|
|
606
|
-
const result = await this.execute();
|
|
607
|
-
return result.rows || result || [];
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Execute SELECT query and get first result
|
|
612
|
-
* @returns {Promise<Object|null>} First result or null
|
|
613
|
-
*/
|
|
614
|
-
async first() {
|
|
615
|
-
this.query.type = "select";
|
|
616
|
-
this.limit(1);
|
|
617
|
-
const result = await this.execute();
|
|
618
|
-
|
|
619
|
-
// 修复语法:正确处理数组和对象格式的结果
|
|
620
|
-
if (Array.isArray(result)) {
|
|
621
|
-
return result.length > 0 ? result : null;
|
|
622
|
-
} else if (result && result.rows) {
|
|
623
|
-
return result.rows.length > 0 ? result.rows : null;
|
|
624
|
-
} else if (result && Array.isArray(result)) {
|
|
625
|
-
return result.length > 0 ? result : null;
|
|
626
|
-
}
|
|
627
|
-
return null;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Execute COUNT query
|
|
632
|
-
* @returns {Promise<number>} Count result
|
|
633
|
-
*/
|
|
634
|
-
async count() {
|
|
635
|
-
this.query.type = "select";
|
|
636
|
-
this.query.columns = ["COUNT(*) as count"];
|
|
637
|
-
const result = await this.execute();
|
|
638
|
-
|
|
639
|
-
// 修复语法:正确处理不同的结果格式
|
|
640
|
-
let count = 0;
|
|
641
|
-
|
|
642
|
-
if (result && result.rows && result.rows.length > 0) {
|
|
643
|
-
// 格式:{ rows: [{ count: 5 }] }
|
|
644
|
-
count = result.rows.count;
|
|
645
|
-
} else if (result && Array.isArray(result) && result.length > 0) {
|
|
646
|
-
// 格式:[{ count: 5 }]
|
|
647
|
-
count = result.count;
|
|
648
|
-
} else if (result && result.count !== undefined) {
|
|
649
|
-
// 格式:{ count: 5 }
|
|
650
|
-
count = result.count;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return parseInt(count) || 0;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Check if record exists
|
|
658
|
-
* @returns {Promise<boolean>} True if exists
|
|
659
|
-
*/
|
|
660
|
-
async exists() {
|
|
661
|
-
this.query.type = "select";
|
|
662
|
-
this.query.columns = ["1 as exists"];
|
|
663
|
-
this.query.limit = 1;
|
|
664
|
-
const result = await this.execute();
|
|
665
|
-
return (result.rows?.length || result.length) > 0;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
// ==================== SQL BUILDING METHODS ====================
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* Generate SQL and bindings
|
|
672
|
-
* @returns {Object} SQL and bindings
|
|
673
|
-
*/
|
|
674
|
-
toSQL() {
|
|
675
|
-
let sql = "";
|
|
676
|
-
const originalBindings = [...this.bindings];
|
|
677
|
-
this.bindings = [];
|
|
678
|
-
|
|
679
|
-
switch (this.query.type) {
|
|
680
|
-
case "select":
|
|
681
|
-
sql = this.buildSelectSQL();
|
|
682
|
-
break;
|
|
683
|
-
case "insert":
|
|
684
|
-
sql = this.buildInsertSQL();
|
|
685
|
-
break;
|
|
686
|
-
case "update":
|
|
687
|
-
sql = this.buildUpdateSQL();
|
|
688
|
-
break;
|
|
689
|
-
case "delete":
|
|
690
|
-
sql = this.buildDeleteSQL();
|
|
691
|
-
break;
|
|
692
|
-
default:
|
|
693
|
-
throw new Error(`Unsupported query type: ${this.query.type}`);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
const result = { sql, bindings: [...this.bindings] };
|
|
697
|
-
this.bindings = originalBindings;
|
|
698
|
-
return result;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Build SELECT SQL with dialect-specific optimizations
|
|
703
|
-
* @returns {string} SELECT SQL
|
|
704
|
-
*/
|
|
705
|
-
buildSelectSQL() {
|
|
706
|
-
const columns = this.query.columns
|
|
707
|
-
.map((col) =>
|
|
708
|
-
typeof col === "object" && col.raw ? col.raw : this.wrapColumn(col),
|
|
709
|
-
)
|
|
710
|
-
.join(", ");
|
|
711
|
-
|
|
712
|
-
let sql = `SELECT ${this.query.distinct ? "DISTINCT " : ""}${columns} FROM ${this.wrapTable(this.tableName)}`;
|
|
713
|
-
|
|
714
|
-
// Add JOIN clause
|
|
715
|
-
if (this.query.joins.length > 0) {
|
|
716
|
-
sql += this.buildJoinClause();
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Add WHERE clause
|
|
720
|
-
if (this.query.where.length > 0) {
|
|
721
|
-
const whereClause = this.buildWhereClause();
|
|
722
|
-
sql += ` WHERE ${whereClause}`;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
// Add GROUP BY clause
|
|
726
|
-
if (this.query.groupBy.length > 0) {
|
|
727
|
-
sql += this.buildGroupByClause();
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
// Add HAVING clause
|
|
731
|
-
if (this.query.having.length > 0) {
|
|
732
|
-
sql += this.buildHavingClause();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Add ORDER BY clause
|
|
736
|
-
if (this.query.orderBy.length > 0) {
|
|
737
|
-
const orderByClause = this.query.orderBy
|
|
738
|
-
.map((order) =>
|
|
739
|
-
typeof order === "object" && order.raw
|
|
740
|
-
? order.raw
|
|
741
|
-
: `${this.wrapColumn(order.column)} ${order.direction}`,
|
|
742
|
-
)
|
|
743
|
-
.join(", ");
|
|
744
|
-
sql += ` ORDER BY ${orderByClause}`;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// Add LIMIT and OFFSET with dialect-specific syntax
|
|
748
|
-
if (this.query.limit !== null || this.query.offset !== null) {
|
|
749
|
-
sql += this.buildLimitOffset(this.query.limit, this.query.offset);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Add LOCK clause if specified
|
|
753
|
-
if (this.query.lock) {
|
|
754
|
-
sql += ` ${this.query.lock}`;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
return sql;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Build INSERT SQL with flexible batch insert support
|
|
762
|
-
* @returns {string} INSERT SQL
|
|
763
|
-
*/
|
|
764
|
-
buildInsertSQL() {
|
|
765
|
-
if (
|
|
766
|
-
!this.query.data ||
|
|
767
|
-
!Array.isArray(this.query.data) ||
|
|
768
|
-
this.query.data.length === 0
|
|
769
|
-
) {
|
|
770
|
-
throw new Error("No data to insert");
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const data = this.query.data;
|
|
774
|
-
const isBatch = data.length > 1;
|
|
775
|
-
|
|
776
|
-
if (isBatch) {
|
|
777
|
-
return this._buildBatchInsertSQL(data);
|
|
778
|
-
} else {
|
|
779
|
-
return this._buildSingleInsertSQL(data[0]);
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* Build SQL for single row insert (optimized)
|
|
785
|
-
* @param {Object} row - Single row data
|
|
786
|
-
* @returns {string} INSERT SQL
|
|
787
|
-
* @private
|
|
788
|
-
*/
|
|
789
|
-
_buildSingleInsertSQL(row) {
|
|
790
|
-
const columns = Object.keys(row);
|
|
791
|
-
const columnString = columns.map((col) => this.wrapColumn(col)).join(", ");
|
|
792
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
793
|
-
|
|
794
|
-
this.bindings = Object.values(row);
|
|
795
|
-
|
|
796
|
-
return `INSERT INTO ${this.wrapTable(this.tableName)} (${columnString}) VALUES (${placeholders})`;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Build SQL for batch insert (optimized)
|
|
801
|
-
* @param {Array} rows - Array of row data
|
|
802
|
-
* @returns {string} Batch INSERT SQL
|
|
803
|
-
* @private
|
|
804
|
-
*/
|
|
805
|
-
_buildBatchInsertSQL(rows) {
|
|
806
|
-
const firstRow = rows[0];
|
|
807
|
-
const columns = Object.keys(firstRow);
|
|
808
|
-
const columnString = columns.map((col) => this.wrapColumn(col)).join(", ");
|
|
809
|
-
|
|
810
|
-
const placeholderTemplate = `(${columns.map(() => "?").join(", ")})`;
|
|
811
|
-
|
|
812
|
-
const placeholders = [];
|
|
813
|
-
this.bindings = [];
|
|
814
|
-
|
|
815
|
-
for (let i = 0; i < rows.length; i++) {
|
|
816
|
-
const row = rows[i];
|
|
817
|
-
placeholders.push(placeholderTemplate);
|
|
818
|
-
|
|
819
|
-
// 按列顺序添加绑定值
|
|
820
|
-
for (const col of columns) {
|
|
821
|
-
this.bindings.push(row[col] !== undefined ? row[col] : null);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return `INSERT INTO ${this.wrapTable(this.tableName)} (${columnString}) VALUES ${placeholders.join(", ")}`;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* Build UPDATE SQL with dialect-specific features
|
|
830
|
-
* @returns {string} UPDATE SQL
|
|
831
|
-
*/
|
|
832
|
-
buildUpdateSQL() {
|
|
833
|
-
if (!this.query.data) {
|
|
834
|
-
throw new Error("No data to update");
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
const setParts = Object.keys(this.query.data).map(
|
|
838
|
-
(col) => `${this.wrapColumn(col)} = ?`,
|
|
839
|
-
);
|
|
840
|
-
const setValues = Object.values(this.query.data);
|
|
841
|
-
|
|
842
|
-
const originalWhereBindings = [...this.bindings];
|
|
843
|
-
this.bindings = [];
|
|
844
|
-
|
|
845
|
-
let whereClause = "";
|
|
846
|
-
if (this.query.where && this.query.where.length > 0) {
|
|
847
|
-
whereClause = this.buildWhereClause();
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const whereBindings = [...this.bindings];
|
|
851
|
-
this.bindings = [...setValues, ...whereBindings];
|
|
852
|
-
|
|
853
|
-
let sql = `UPDATE ${this.wrapTable(this.tableName)} SET ${setParts.join(", ")}`;
|
|
854
|
-
if (whereClause) {
|
|
855
|
-
sql += ` WHERE ${whereClause}`;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
// Add RETURNING clause for supported databases
|
|
859
|
-
if (this.query.returning && this.supportsReturning()) {
|
|
860
|
-
const returningColumns = this.query.returning
|
|
861
|
-
.map((col) => this.wrapColumn(col))
|
|
862
|
-
.join(", ");
|
|
863
|
-
sql += ` RETURNING ${returningColumns}`;
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
return sql;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
/**
|
|
870
|
-
* Set the query type to 'delete'.
|
|
871
|
-
* This initiates the construction of a DELETE statement.
|
|
872
|
-
* @returns {QueryBuilder} The current QueryBuilder instance for chaining.
|
|
873
|
-
*/
|
|
874
|
-
delete() {
|
|
875
|
-
this.query.type = "delete";
|
|
876
|
-
return this;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* Force delete without triggering warnings for missing WHERE clauses.
|
|
881
|
-
* Use with caution as it can lead to accidental full table deletes.
|
|
882
|
-
* @returns {QueryBuilder} The current QueryBuilder instance for chaining.
|
|
883
|
-
*/
|
|
884
|
-
forceDelete() {
|
|
885
|
-
this.query.type = "delete";
|
|
886
|
-
this.query.skipWhereWarning = true;
|
|
887
|
-
return this;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Prepare a delete operation for a batch of IDs.
|
|
892
|
-
* Automatically handles large arrays by splitting them into batches if necessary.
|
|
893
|
-
* @param {string} column - The column name to match against (usually 'id').
|
|
894
|
-
* @param {Array} values - An array of values to delete.
|
|
895
|
-
* @param {number} batchSize - The size of each batch for large arrays (default: 100).
|
|
896
|
-
* @returns {QueryBuilder} The current QueryBuilder instance for chaining.
|
|
897
|
-
*/
|
|
898
|
-
deleteInBatch(column, values, batchSize = 100) {
|
|
899
|
-
this.query.type = "delete";
|
|
900
|
-
// Reuse the optimized whereIn logic which handles batching automatically
|
|
901
|
-
return this.whereIn(column, values, batchSize);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Specify an index hint for the DELETE operation.
|
|
906
|
-
* This can significantly improve performance by forcing the database to use a specific index.
|
|
907
|
-
* @param {string} indexName - The name of the index to use.
|
|
908
|
-
* @returns {QueryBuilder} The current QueryBuilder instance for chaining.
|
|
909
|
-
*/
|
|
910
|
-
useIndex(indexName) {
|
|
911
|
-
this.query.useIndex = indexName;
|
|
912
|
-
return this;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Build DELETE SQL with dialect-specific features
|
|
917
|
-
* @returns {string} DELETE SQL
|
|
918
|
-
*/
|
|
919
|
-
buildDeleteSQL() {
|
|
920
|
-
// Reset bindings for the new query construction
|
|
921
|
-
this.bindings = [];
|
|
922
|
-
|
|
923
|
-
// Start building the base DELETE statement
|
|
924
|
-
let sql = `DELETE FROM ${this.wrapTable(this.tableName)}`;
|
|
925
|
-
|
|
926
|
-
// Apply index hint if specified
|
|
927
|
-
if (this.query.useIndex) {
|
|
928
|
-
if (this.dialect === "mysql" || this.dialect === "mariadb") {
|
|
929
|
-
sql += ` USE INDEX (${this.query.useIndex})`;
|
|
930
|
-
} else if (
|
|
931
|
-
this.dialect === "postgresql" ||
|
|
932
|
-
this.dialect === "postgres" ||
|
|
933
|
-
this.dialect === "pg"
|
|
934
|
-
) {
|
|
935
|
-
// PostgreSQL uses different syntax or planner hints
|
|
936
|
-
sql += ` /*+ Index(${this.tableName} ${this.query.useIndex}) */`;
|
|
937
|
-
} else if (this.dialect === "mssql" || this.dialect === "sqlserver") {
|
|
938
|
-
sql += ` WITH (INDEX(${this.query.useIndex}))`;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Append WHERE clause if conditions exist
|
|
943
|
-
if (this.query.where.length > 0) {
|
|
944
|
-
sql += ` WHERE ${this.buildWhereClause()}`;
|
|
945
|
-
} else if (!this.query.skipWhereWarning) {
|
|
946
|
-
// 优化:只在开发环境显示警告
|
|
947
|
-
if (process.env.NODE_ENV === "development") {
|
|
948
|
-
console.warn(
|
|
949
|
-
"[Warning] Executing DELETE without WHERE clause. Use forceDelete() to suppress this warning.",
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// Add database-specific optimization hints
|
|
955
|
-
if (this.dialect === "mysql" || this.dialect === "mariadb") {
|
|
956
|
-
// Hint to MySQL to prioritize this delete operation
|
|
957
|
-
sql += " /*+ DELETE_PRIORITY(HIGH) */";
|
|
958
|
-
} else if (
|
|
959
|
-
this.dialect === "postgresql" ||
|
|
960
|
-
this.dialect === "postgres" ||
|
|
961
|
-
this.dialect === "pg"
|
|
962
|
-
) {
|
|
963
|
-
// Hint for PostgreSQL query planner
|
|
964
|
-
sql += " /*+ IndexScan */";
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Add RETURNING clause for supported databases
|
|
968
|
-
if (this.query.returning && this.supportsReturning()) {
|
|
969
|
-
const returningColumns = this.query.returning
|
|
970
|
-
.map((col) => this.wrapColumn(col))
|
|
971
|
-
.join(", ");
|
|
972
|
-
sql += ` RETURNING ${returningColumns}`;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
return sql;
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
/**
|
|
979
|
-
* Execute query with performance monitoring
|
|
980
|
-
* @returns {Promise<Object>} Query result with performance metrics
|
|
981
|
-
*/
|
|
982
|
-
async executeWithPerformance() {
|
|
983
|
-
const startTime = Date.now();
|
|
984
|
-
const startMemory = process.memoryUsage();
|
|
985
|
-
|
|
986
|
-
try {
|
|
987
|
-
const result = await this.execute();
|
|
988
|
-
|
|
989
|
-
const endTime = Date.now();
|
|
990
|
-
const endMemory = process.memoryUsage();
|
|
991
|
-
|
|
992
|
-
const performance = {
|
|
993
|
-
duration: endTime - startTime,
|
|
994
|
-
memory: {
|
|
995
|
-
rss: endMemory.rss - startMemory.rss,
|
|
996
|
-
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
|
|
997
|
-
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
|
|
998
|
-
},
|
|
999
|
-
queryType: this.query.type,
|
|
1000
|
-
rowsAffected: result.affectedRows || result.rowCount || 0,
|
|
1001
|
-
timestamp: new Date().toISOString(),
|
|
1002
|
-
};
|
|
1003
|
-
|
|
1004
|
-
// 记录慢查询
|
|
1005
|
-
if (performance.duration > 100) {
|
|
1006
|
-
// 超过100ms视为慢查询
|
|
1007
|
-
console.warn(`Slow query detected: ${performance.duration}ms`, {
|
|
1008
|
-
sql: this.toSQL().sql,
|
|
1009
|
-
...performance,
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
return {
|
|
1014
|
-
result,
|
|
1015
|
-
performance,
|
|
1016
|
-
};
|
|
1017
|
-
} catch (error) {
|
|
1018
|
-
const endTime = Date.now();
|
|
1019
|
-
console.error(
|
|
1020
|
-
`Query failed after ${endTime - startTime}ms:`,
|
|
1021
|
-
error.message,
|
|
1022
|
-
);
|
|
1023
|
-
throw error;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
/**
|
|
1028
|
-
* Optimize query based on performance analysis
|
|
1029
|
-
* @returns {QueryBuilder} Optimized query builder
|
|
1030
|
-
*/
|
|
1031
|
-
optimize() {
|
|
1032
|
-
const metrics = this.getPerformanceMetrics();
|
|
1033
|
-
|
|
1034
|
-
if (metrics.needsOptimization) {
|
|
1035
|
-
console.log(
|
|
1036
|
-
`Query optimization suggestions:`,
|
|
1037
|
-
this.getOptimizationSuggestions(),
|
|
1038
|
-
);
|
|
1039
|
-
|
|
1040
|
-
// 自动优化建议
|
|
1041
|
-
if (this.query.type === "select" && this.query.columns.includes("*")) {
|
|
1042
|
-
console.warn(
|
|
1043
|
-
"Consider specifying columns instead of using SELECT *",
|
|
1044
|
-
);
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
if (this.query.where.length > 10) {
|
|
1048
|
-
console.warn("Consider adding indexes for WHERE conditions");
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (this.query.joins.length > 3) {
|
|
1052
|
-
console.warn("Multiple JOINs detected, ensure proper indexes exist");
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
return this;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
/**
|
|
1060
|
-
* Execute the DELETE query with performance monitoring.
|
|
1061
|
-
* Logs duration, memory usage, and affected rows.
|
|
1062
|
-
* Warns if the operation takes longer than 1 second.
|
|
1063
|
-
* @returns {Promise<Object>} The result of the execution including performance metrics.
|
|
1064
|
-
*/
|
|
1065
|
-
async deleteWithMonitoring() {
|
|
1066
|
-
const startTime = Date.now();
|
|
1067
|
-
const startMemory = process.memoryUsage();
|
|
1068
|
-
|
|
1069
|
-
try {
|
|
1070
|
-
// Execute the actual query
|
|
1071
|
-
const result = await this.execute();
|
|
1072
|
-
|
|
1073
|
-
const endTime = Date.now();
|
|
1074
|
-
const endMemory = process.memoryUsage();
|
|
1075
|
-
|
|
1076
|
-
// Calculate performance metrics
|
|
1077
|
-
const performance = {
|
|
1078
|
-
duration: endTime - startTime,
|
|
1079
|
-
memoryDiff: {
|
|
1080
|
-
rss: endMemory.rss - startMemory.rss,
|
|
1081
|
-
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
|
|
1082
|
-
},
|
|
1083
|
-
rowsAffected: result.affectedRows || 0,
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
// Warn if the operation is slow
|
|
1087
|
-
if (performance.duration > 1000) {
|
|
1088
|
-
console.warn(
|
|
1089
|
-
"DELETE operation took over 1 second. Consider optimizing indexes or using batch deletion.",
|
|
1090
|
-
);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
return { result, performance };
|
|
1094
|
-
} catch (error) {
|
|
1095
|
-
console.error("DELETE Execution Failed:", error);
|
|
1096
|
-
throw error;
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
/**
|
|
1101
|
-
* Smart Delete: Automatically decides between single execution and batch deletion.
|
|
1102
|
-
* Analyzes the estimated row count and switches to batch mode if the threshold is exceeded.
|
|
1103
|
-
* @param {Object} options - Configuration options for batch deletion.
|
|
1104
|
-
* @param {number} options.batchSize - Number of rows to delete per batch (default: 1000).
|
|
1105
|
-
* @param {number} options.delay - Delay in ms between batches to reduce lock contention (default: 100).
|
|
1106
|
-
* @param {boolean} options.useTransaction - Whether to wrap each batch in a transaction (default: true).
|
|
1107
|
-
* @param {number} options.threshold - Row count threshold to trigger batch mode (default: 1000).
|
|
1108
|
-
* @returns {Promise<Object>} Result of the deletion operation.
|
|
1109
|
-
*/
|
|
1110
|
-
async smartDelete(options = {}) {
|
|
1111
|
-
const {
|
|
1112
|
-
batchSize = 1000,
|
|
1113
|
-
delay = 100,
|
|
1114
|
-
useTransaction = true,
|
|
1115
|
-
threshold = 1000,
|
|
1116
|
-
} = options;
|
|
1117
|
-
|
|
1118
|
-
// Estimate the number of rows to be deleted
|
|
1119
|
-
const plan = await this.getExecutionPlan();
|
|
1120
|
-
|
|
1121
|
-
// If estimated rows exceed threshold, use batch deletion
|
|
1122
|
-
if (plan.metrics.estimatedRows > threshold) {
|
|
1123
|
-
return this.deleteInBatches(batchSize, delay, useTransaction);
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
// Otherwise, execute as a single standard delete
|
|
1127
|
-
return this.execute();
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
/**
|
|
1131
|
-
* Delete records in batches to prevent locking issues and high memory usage.
|
|
1132
|
-
* @param {number} batchSize - Number of rows to delete per batch.
|
|
1133
|
-
* @param {number} delay - Delay in milliseconds between batches.
|
|
1134
|
-
* @param {boolean} useTransaction - Whether to use transactions for each batch.
|
|
1135
|
-
* @returns {Promise<Object>} Summary of the deletion process.
|
|
1136
|
-
*/
|
|
1137
|
-
async deleteInBatches(batchSize = 1000, delay = 100, useTransaction = true) {
|
|
1138
|
-
// Clone the current query to get the total count without modifying the original builder
|
|
1139
|
-
const countQuery = this.clone();
|
|
1140
|
-
countQuery.query.columns = ["COUNT(*) as total"];
|
|
1141
|
-
// Clear order by and limit for accurate counting
|
|
1142
|
-
countQuery.query.orderBy = [];
|
|
1143
|
-
countQuery.query.limit = null;
|
|
1144
|
-
|
|
1145
|
-
const countResult = await countQuery.first();
|
|
1146
|
-
const total = parseInt(countResult.total);
|
|
1147
|
-
|
|
1148
|
-
let deleted = 0;
|
|
1149
|
-
const primaryKey = this.getPrimaryKeyColumn(); // Assumes a method exists to get PK
|
|
1150
|
-
|
|
1151
|
-
while (deleted < total) {
|
|
1152
|
-
// Create a batch query
|
|
1153
|
-
const batchQuery = this.clone()
|
|
1154
|
-
.limit(batchSize)
|
|
1155
|
-
.orderBy(primaryKey, "ASC");
|
|
1156
|
-
|
|
1157
|
-
// Execute the batch
|
|
1158
|
-
if (useTransaction) {
|
|
1159
|
-
await batchQuery.executeInTransaction();
|
|
1160
|
-
} else {
|
|
1161
|
-
await batchQuery.execute();
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
deleted += batchSize;
|
|
1165
|
-
|
|
1166
|
-
// Optional delay to reduce database load and lock contention
|
|
1167
|
-
if (deleted < total) {
|
|
1168
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
return { total, deleted };
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
/**
|
|
1176
|
-
* Build WHERE clause
|
|
1177
|
-
* @returns {string} WHERE clause
|
|
1178
|
-
*/
|
|
1179
|
-
buildWhereClause() {
|
|
1180
|
-
const whereBindings = [];
|
|
1181
|
-
|
|
1182
|
-
const whereClause = this.query.where
|
|
1183
|
-
.map((condition, index) => {
|
|
1184
|
-
if (condition.raw) {
|
|
1185
|
-
return condition.raw;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
const prefix = index > 0 ? `${condition.boolean.toUpperCase()} ` : "";
|
|
1189
|
-
|
|
1190
|
-
if (condition.type === "null" || condition.type === "notnull") {
|
|
1191
|
-
return `${prefix}${this.wrapColumn(condition.column)} ${condition.operator} NULL`;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
if (condition.type === "in" || condition.type === "notin") {
|
|
1195
|
-
const placeholders = condition.value.map(() => "?").join(", ");
|
|
1196
|
-
whereBindings.push(...condition.value);
|
|
1197
|
-
return `${prefix}${this.wrapColumn(condition.column)} ${condition.operator} (${placeholders})`;
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
if (condition.type === "between") {
|
|
1201
|
-
const operator = condition.not ? "NOT BETWEEN" : "BETWEEN";
|
|
1202
|
-
whereBindings.push(...condition.values);
|
|
1203
|
-
return `${prefix}${this.wrapColumn(condition.column)} ${operator} ? AND ?`;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
whereBindings.push(condition.value);
|
|
1207
|
-
return `${prefix}${this.wrapColumn(condition.column)} ${condition.operator} ?`;
|
|
1208
|
-
})
|
|
1209
|
-
.join(" ");
|
|
1210
|
-
|
|
1211
|
-
this.bindings.push(...whereBindings);
|
|
1212
|
-
return whereClause;
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
/**
|
|
1216
|
-
* Build JOIN clause
|
|
1217
|
-
* @returns {string} JOIN clause
|
|
1218
|
-
*/
|
|
1219
|
-
buildJoinClause() {
|
|
1220
|
-
const joinClauses = this.query.joins.map((join) => {
|
|
1221
|
-
if (join.type === "cross") {
|
|
1222
|
-
return `CROSS JOIN ${this.wrapTable(join.table)}`;
|
|
1223
|
-
}
|
|
1224
|
-
return `${join.type.toUpperCase()} JOIN ${this.wrapTable(join.table)} ON ${this.wrapColumn(join.first)} ${join.operator} ${this.wrapColumn(join.second)}`;
|
|
1225
|
-
});
|
|
1226
|
-
|
|
1227
|
-
return " " + joinClauses.join(" ");
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
/**
|
|
1231
|
-
* Build GROUP BY clause
|
|
1232
|
-
* @returns {string} GROUP BY clause
|
|
1233
|
-
*/
|
|
1234
|
-
buildGroupByClause() {
|
|
1235
|
-
const groupByClause = this.query.groupBy
|
|
1236
|
-
.map((item) =>
|
|
1237
|
-
typeof item === "object" && item.raw ? item.raw : this.wrapColumn(item),
|
|
1238
|
-
)
|
|
1239
|
-
.join(", ");
|
|
1240
|
-
|
|
1241
|
-
return ` GROUP BY ${groupByClause}`;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
/**
|
|
1245
|
-
* Build HAVING clause
|
|
1246
|
-
* @returns {string} HAVING clause
|
|
1247
|
-
*/
|
|
1248
|
-
buildHavingClause() {
|
|
1249
|
-
const havingConditions = this.query.having.map((condition) => {
|
|
1250
|
-
if (condition.raw) {
|
|
1251
|
-
return condition.raw;
|
|
1252
|
-
}
|
|
1253
|
-
if (condition.value === null) {
|
|
1254
|
-
return `${condition.column} ${condition.operator} NULL`;
|
|
1255
|
-
}
|
|
1256
|
-
return `${condition.column} ${condition.operator} ?`;
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
return ` HAVING ${havingConditions.join(" AND ")}`;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// ==================== HELPER METHODS ====================
|
|
1263
|
-
/**
|
|
1264
|
-
* Wrap column name for SQL using dialect adapter
|
|
1265
|
-
* @param {string} column - Column name
|
|
1266
|
-
* @returns {string} Wrapped column name
|
|
1267
|
-
*/
|
|
1268
|
-
wrapColumn(column) {
|
|
1269
|
-
if (
|
|
1270
|
-
column.includes("(") ||
|
|
1271
|
-
column.includes(")") ||
|
|
1272
|
-
column.includes(" as ") ||
|
|
1273
|
-
column.toLowerCase().includes("distinct") ||
|
|
1274
|
-
column.includes("*")
|
|
1275
|
-
) {
|
|
1276
|
-
return column;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
if (column.includes(".")) {
|
|
1280
|
-
return column
|
|
1281
|
-
.split(".")
|
|
1282
|
-
.map((part) => {
|
|
1283
|
-
if (part === "*") return "*";
|
|
1284
|
-
return this.adapter.quoteIdentifier(part);
|
|
1285
|
-
})
|
|
1286
|
-
.join(".");
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
return this.adapter.quoteIdentifier(column);
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
/**
|
|
1293
|
-
* Wrap table name for SQL using dialect adapter
|
|
1294
|
-
* @param {string} table - Table name
|
|
1295
|
-
* @returns {string} Wrapped table name
|
|
1296
|
-
*/
|
|
1297
|
-
wrapTable(table) {
|
|
1298
|
-
if (table.includes(".")) {
|
|
1299
|
-
return table
|
|
1300
|
-
.split(".")
|
|
1301
|
-
.map((part) => this.adapter.quoteIdentifier(part))
|
|
1302
|
-
.join(".");
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
return this.adapter.quoteIdentifier(table);
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
/**
|
|
1309
|
-
* Quote identifier using dialect adapter
|
|
1310
|
-
* @param {string} identifier - Identifier to quote
|
|
1311
|
-
* @returns {string} Quoted identifier
|
|
1312
|
-
*/
|
|
1313
|
-
quoteIdentifier(identifier) {
|
|
1314
|
-
return this.adapter.quoteIdentifier(identifier);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
/**
|
|
1318
|
-
* Execute query and return results in random order using dialect-specific random function
|
|
1319
|
-
* @returns {Promise<Array>} Randomly ordered results
|
|
1320
|
-
*/
|
|
1321
|
-
async inRandomOrder() {
|
|
1322
|
-
this.orderByRaw(this.adapter.getRandomFunction());
|
|
1323
|
-
return this.get();
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
/**
|
|
1327
|
-
* Get the appropriate closing quote character for the current dialect
|
|
1328
|
-
* @returns {string} Closing quote character
|
|
1329
|
-
*/
|
|
1330
|
-
getQuoteEndChar() {
|
|
1331
|
-
switch (this.dialect) {
|
|
1332
|
-
case "mysql":
|
|
1333
|
-
case "mariadb":
|
|
1334
|
-
case "clickhouse":
|
|
1335
|
-
return "`";
|
|
1336
|
-
case "postgresql":
|
|
1337
|
-
case "postgres":
|
|
1338
|
-
case "pg":
|
|
1339
|
-
case "cockroachdb":
|
|
1340
|
-
case "cockroach":
|
|
1341
|
-
case "sqlite":
|
|
1342
|
-
case "sqlite3":
|
|
1343
|
-
case "oracle":
|
|
1344
|
-
return '"';
|
|
1345
|
-
case "mssql":
|
|
1346
|
-
case "sqlserver":
|
|
1347
|
-
return "]";
|
|
1348
|
-
default:
|
|
1349
|
-
return "";
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
/**
|
|
1354
|
-
* Check if an identifier needs quoting
|
|
1355
|
-
* @param {string} identifier - Identifier to check
|
|
1356
|
-
* @returns {boolean} True if quoting is needed
|
|
1357
|
-
*/
|
|
1358
|
-
needsQuoting(identifier) {
|
|
1359
|
-
// Check for reserved keywords
|
|
1360
|
-
const keywords = this.getReservedKeywords();
|
|
1361
|
-
const upperIdentifier = identifier.toUpperCase();
|
|
1362
|
-
|
|
1363
|
-
// Check for special characters or spaces
|
|
1364
|
-
if (/[^a-zA-Z0-9_]/.test(identifier)) {
|
|
1365
|
-
return true;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// Check if it's a reserved keyword
|
|
1369
|
-
if (keywords.includes(upperIdentifier)) {
|
|
1370
|
-
return true;
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
// Check if it starts with a number
|
|
1374
|
-
if (/^\d/.test(identifier)) {
|
|
1375
|
-
return true;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
return false;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
/**
|
|
1382
|
-
* Get reserved keywords for the current dialect
|
|
1383
|
-
* @returns {string[]} Array of reserved keywords
|
|
1384
|
-
*/
|
|
1385
|
-
getReservedKeywords() {
|
|
1386
|
-
// Common SQL reserved words
|
|
1387
|
-
const commonKeywords = [
|
|
1388
|
-
"SELECT",
|
|
1389
|
-
"FROM",
|
|
1390
|
-
"WHERE",
|
|
1391
|
-
"INSERT",
|
|
1392
|
-
"UPDATE",
|
|
1393
|
-
"DELETE",
|
|
1394
|
-
"CREATE",
|
|
1395
|
-
"DROP",
|
|
1396
|
-
"ALTER",
|
|
1397
|
-
"TABLE",
|
|
1398
|
-
"INDEX",
|
|
1399
|
-
"VIEW",
|
|
1400
|
-
"JOIN",
|
|
1401
|
-
"LEFT",
|
|
1402
|
-
"RIGHT",
|
|
1403
|
-
"INNER",
|
|
1404
|
-
"OUTER",
|
|
1405
|
-
"GROUP",
|
|
1406
|
-
"BY",
|
|
1407
|
-
"ORDER",
|
|
1408
|
-
"HAVING",
|
|
1409
|
-
"LIMIT",
|
|
1410
|
-
"OFFSET",
|
|
1411
|
-
"UNION",
|
|
1412
|
-
"ALL",
|
|
1413
|
-
"DISTINCT",
|
|
1414
|
-
"AS",
|
|
1415
|
-
"ON",
|
|
1416
|
-
"AND",
|
|
1417
|
-
"OR",
|
|
1418
|
-
"NOT",
|
|
1419
|
-
"NULL",
|
|
1420
|
-
"IS",
|
|
1421
|
-
"IN",
|
|
1422
|
-
"BETWEEN",
|
|
1423
|
-
"LIKE",
|
|
1424
|
-
"EXISTS",
|
|
1425
|
-
];
|
|
1426
|
-
|
|
1427
|
-
// Dialect-specific reserved words
|
|
1428
|
-
const dialectKeywords = {
|
|
1429
|
-
mysql: ["AUTO_INCREMENT", "ENGINE", "CHARSET", "COLLATE", "RAND"],
|
|
1430
|
-
mariadb: ["AUTO_INCREMENT", "ENGINE", "CHARSET", "COLLATE", "RAND"],
|
|
1431
|
-
postgresql: ["SERIAL", "BIGSERIAL", "RETURNING", "ILIKE", "RANDOM"],
|
|
1432
|
-
postgres: ["SERIAL", "BIGSERIAL", "RETURNING", "ILIKE", "RANDOM"],
|
|
1433
|
-
pg: ["SERIAL", "BIGSERIAL", "RETURNING", "ILIKE", "RANDOM"],
|
|
1434
|
-
sqlite: ["AUTOINCREMENT", "TEMPORARY", "RANDOM"],
|
|
1435
|
-
sqlite3: ["AUTOINCREMENT", "TEMPORARY", "RANDOM"],
|
|
1436
|
-
mssql: ["IDENTITY", "TOP", "OUTPUT", "WITH"],
|
|
1437
|
-
sqlserver: ["IDENTITY", "TOP", "OUTPUT", "WITH"],
|
|
1438
|
-
oracle: ["SEQUENCE", "DUAL", "ROWNUM", "SYSDATE"],
|
|
1439
|
-
cockroachdb: ["RETURNING", "RANDOM"],
|
|
1440
|
-
cockroach: ["RETURNING", "RANDOM"],
|
|
1441
|
-
clickhouse: ["ENGINE", "ORDER BY", "PRIMARY KEY"],
|
|
1442
|
-
};
|
|
1443
|
-
|
|
1444
|
-
return [...commonKeywords, ...(dialectKeywords[this.dialect] || [])];
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
/**
|
|
1448
|
-
* Safely escape identifier to prevent SQL injection
|
|
1449
|
-
* @param {string} identifier - Raw identifier
|
|
1450
|
-
* @returns {string} Safe identifier
|
|
1451
|
-
*/
|
|
1452
|
-
safeIdentifier(identifier) {
|
|
1453
|
-
// Remove potentially dangerous characters
|
|
1454
|
-
let safeId = identifier.replace(/[;'"\\]/g, "");
|
|
1455
|
-
|
|
1456
|
-
// Apply dialect-specific escaping
|
|
1457
|
-
switch (this.dialect) {
|
|
1458
|
-
case "mysql":
|
|
1459
|
-
case "mariadb":
|
|
1460
|
-
safeId = safeId.replace(/`/g, "``");
|
|
1461
|
-
break;
|
|
1462
|
-
case "postgresql":
|
|
1463
|
-
case "postgres":
|
|
1464
|
-
case "pg":
|
|
1465
|
-
case "cockroachdb":
|
|
1466
|
-
case "cockroach":
|
|
1467
|
-
safeId = safeId.replace(/"/g, '""');
|
|
1468
|
-
break;
|
|
1469
|
-
case "mssql":
|
|
1470
|
-
case "sqlserver":
|
|
1471
|
-
safeId = safeId.replace(/\]/g, "]]");
|
|
1472
|
-
break;
|
|
1473
|
-
case "oracle":
|
|
1474
|
-
safeId = safeId.replace(/"/g, '""');
|
|
1475
|
-
break;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
return safeId;
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
/**
|
|
1482
|
-
* Get parameter placeholder for the current dialect
|
|
1483
|
-
* @param {number} index - Parameter index (1-based)
|
|
1484
|
-
* @returns {string} Parameter placeholder
|
|
1485
|
-
*/
|
|
1486
|
-
getParameterPlaceholder(index) {
|
|
1487
|
-
switch (this.dialect) {
|
|
1488
|
-
case "postgresql":
|
|
1489
|
-
case "postgres":
|
|
1490
|
-
case "pg":
|
|
1491
|
-
case "cockroachdb":
|
|
1492
|
-
case "cockroach":
|
|
1493
|
-
return `$${index}`;
|
|
1494
|
-
case "mssql":
|
|
1495
|
-
case "sqlserver":
|
|
1496
|
-
return `@p${index}`;
|
|
1497
|
-
case "oracle":
|
|
1498
|
-
return `:p${index}`;
|
|
1499
|
-
default:
|
|
1500
|
-
return "?";
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
/**
|
|
1505
|
-
* Build LIMIT/OFFSET clause with dialect-specific syntax
|
|
1506
|
-
* @param {number|null} limit - Limit value
|
|
1507
|
-
* @param {number|null} offset - Offset value
|
|
1508
|
-
* @returns {string} LIMIT/OFFSET clause
|
|
1509
|
-
*/
|
|
1510
|
-
buildLimitOffset(limit, offset) {
|
|
1511
|
-
let clause = "";
|
|
1512
|
-
|
|
1513
|
-
if (this.dialect === "mssql" || this.dialect === "sqlserver") {
|
|
1514
|
-
// SQL Server uses OFFSET/FETCH syntax
|
|
1515
|
-
if (offset !== null) {
|
|
1516
|
-
clause += ` OFFSET ${offset} ROWS`;
|
|
1517
|
-
}
|
|
1518
|
-
if (limit !== null) {
|
|
1519
|
-
if (offset !== null) {
|
|
1520
|
-
clause += ` FETCH NEXT ${limit} ROWS ONLY`;
|
|
1521
|
-
} else {
|
|
1522
|
-
clause += ` FETCH FIRST ${limit} ROWS ONLY`;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
} else if (this.dialect === "oracle") {
|
|
1526
|
-
// Oracle uses ROWNUM or FETCH FIRST syntax
|
|
1527
|
-
if (limit !== null) {
|
|
1528
|
-
const limitValue = limit;
|
|
1529
|
-
const offsetValue = offset || 0;
|
|
1530
|
-
clause = ` FETCH FIRST ${limitValue} ROWS ONLY`;
|
|
1531
|
-
if (offsetValue > 0) {
|
|
1532
|
-
clause = ` OFFSET ${offsetValue} ROWS FETCH NEXT ${limitValue} ROWS ONLY`;
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
} else {
|
|
1536
|
-
// Standard SQL syntax for MySQL, PostgreSQL, SQLite, etc.
|
|
1537
|
-
if (limit !== null) {
|
|
1538
|
-
clause += ` LIMIT ${limit}`;
|
|
1539
|
-
}
|
|
1540
|
-
if (offset !== null) {
|
|
1541
|
-
clause += ` OFFSET ${offset}`;
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
return clause;
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
/**
|
|
1549
|
-
* Get random function for the current dialect
|
|
1550
|
-
* @returns {string} Random function name
|
|
1551
|
-
*/
|
|
1552
|
-
getRandomFunction() {
|
|
1553
|
-
switch (this.dialect) {
|
|
1554
|
-
case "mysql":
|
|
1555
|
-
case "mariadb":
|
|
1556
|
-
return "RAND()";
|
|
1557
|
-
case "postgresql":
|
|
1558
|
-
case "postgres":
|
|
1559
|
-
case "pg":
|
|
1560
|
-
case "cockroachdb":
|
|
1561
|
-
case "cockroach":
|
|
1562
|
-
case "sqlite":
|
|
1563
|
-
case "sqlite3":
|
|
1564
|
-
return "RANDOM()";
|
|
1565
|
-
case "mssql":
|
|
1566
|
-
case "sqlserver":
|
|
1567
|
-
return "NEWID()";
|
|
1568
|
-
case "oracle":
|
|
1569
|
-
return "DBMS_RANDOM.VALUE";
|
|
1570
|
-
case "clickhouse":
|
|
1571
|
-
return "rand()";
|
|
1572
|
-
default:
|
|
1573
|
-
return "RAND()";
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
/**
|
|
1578
|
-
* Check if RETURNING clause is supported
|
|
1579
|
-
* @returns {boolean} True if RETURNING is supported
|
|
1580
|
-
*/
|
|
1581
|
-
supportsReturning() {
|
|
1582
|
-
return [
|
|
1583
|
-
"postgresql",
|
|
1584
|
-
"postgres",
|
|
1585
|
-
"pg",
|
|
1586
|
-
"cockroachdb",
|
|
1587
|
-
"cockroach",
|
|
1588
|
-
"oracle",
|
|
1589
|
-
].includes(this.dialect);
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
/**
|
|
1593
|
-
* Check if WITH clause (CTE) is supported
|
|
1594
|
-
* @returns {boolean} True if WITH clause is supported
|
|
1595
|
-
*/
|
|
1596
|
-
supportsWithClause() {
|
|
1597
|
-
return [
|
|
1598
|
-
"postgresql",
|
|
1599
|
-
"postgres",
|
|
1600
|
-
"pg",
|
|
1601
|
-
"cockroachdb",
|
|
1602
|
-
"cockroach",
|
|
1603
|
-
"mssql",
|
|
1604
|
-
"sqlserver",
|
|
1605
|
-
"oracle",
|
|
1606
|
-
].includes(this.dialect);
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
/**
|
|
1610
|
-
* Check if specific feature is supported
|
|
1611
|
-
* @param {string} feature - Feature to check
|
|
1612
|
-
* @returns {boolean} True if feature is supported
|
|
1613
|
-
*/
|
|
1614
|
-
supportsFeature(feature) {
|
|
1615
|
-
const featureSupport = {
|
|
1616
|
-
returning: this.supportsReturning(),
|
|
1617
|
-
with: this.supportsWithClause(),
|
|
1618
|
-
window_functions: [
|
|
1619
|
-
"postgresql",
|
|
1620
|
-
"postgres",
|
|
1621
|
-
"pg",
|
|
1622
|
-
"cockroachdb",
|
|
1623
|
-
"cockroach",
|
|
1624
|
-
"mssql",
|
|
1625
|
-
"sqlserver",
|
|
1626
|
-
"oracle",
|
|
1627
|
-
].includes(this.dialect),
|
|
1628
|
-
json_functions: [
|
|
1629
|
-
"mysql",
|
|
1630
|
-
"mariadb",
|
|
1631
|
-
"postgresql",
|
|
1632
|
-
"postgres",
|
|
1633
|
-
"pg",
|
|
1634
|
-
"cockroachdb",
|
|
1635
|
-
"cockroach",
|
|
1636
|
-
].includes(this.dialect),
|
|
1637
|
-
fulltext_search: [
|
|
1638
|
-
"mysql",
|
|
1639
|
-
"mariadb",
|
|
1640
|
-
"postgresql",
|
|
1641
|
-
"postgres",
|
|
1642
|
-
"pg",
|
|
1643
|
-
].includes(this.dialect),
|
|
1644
|
-
spatial_data: [
|
|
1645
|
-
"mysql",
|
|
1646
|
-
"mariadb",
|
|
1647
|
-
"postgresql",
|
|
1648
|
-
"postgres",
|
|
1649
|
-
"pg",
|
|
1650
|
-
].includes(this.dialect),
|
|
1651
|
-
};
|
|
1652
|
-
|
|
1653
|
-
return featureSupport[feature] || false;
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
/**
|
|
1657
|
-
* Quote identifier based on database dialect
|
|
1658
|
-
* @param {string} identifier - Identifier to quote
|
|
1659
|
-
* @returns {string} Quoted identifier
|
|
1660
|
-
*/
|
|
1661
|
-
quoteIdentifier(identifier) {
|
|
1662
|
-
// Handle raw expressions or already quoted identifiers
|
|
1663
|
-
if (
|
|
1664
|
-
identifier.includes("(") ||
|
|
1665
|
-
identifier.includes(")") ||
|
|
1666
|
-
identifier.includes(" as ") ||
|
|
1667
|
-
identifier.includes("`") ||
|
|
1668
|
-
identifier.includes('"') ||
|
|
1669
|
-
identifier.includes("[") ||
|
|
1670
|
-
identifier.includes("]")
|
|
1671
|
-
) {
|
|
1672
|
-
return identifier;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
// Handle qualified identifiers (e.g., "database.table.column")
|
|
1676
|
-
if (identifier.includes(".")) {
|
|
1677
|
-
return identifier
|
|
1678
|
-
.split(".")
|
|
1679
|
-
.map((part) => this._quoteIdentifierPart(part))
|
|
1680
|
-
.join(".");
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
return this._quoteIdentifierPart(identifier);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
/**
|
|
1687
|
-
* Internal method to quote a single identifier part
|
|
1688
|
-
* @param {string} identifier - Identifier part to quote
|
|
1689
|
-
* @returns {string} Quoted identifier part
|
|
1690
|
-
* @private
|
|
1691
|
-
*/
|
|
1692
|
-
_quoteIdentifierPart(identifier) {
|
|
1693
|
-
// Trim whitespace
|
|
1694
|
-
identifier = identifier.trim();
|
|
1695
|
-
|
|
1696
|
-
// Return if already quoted
|
|
1697
|
-
if (
|
|
1698
|
-
(identifier.startsWith("`") && identifier.endsWith("`")) ||
|
|
1699
|
-
(identifier.startsWith('"') && identifier.endsWith('"')) ||
|
|
1700
|
-
(identifier.startsWith("[") && identifier.endsWith("]"))
|
|
1701
|
-
) {
|
|
1702
|
-
return identifier;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// Apply dialect-specific quoting rules
|
|
1706
|
-
switch (this.dialect) {
|
|
1707
|
-
case "mysql":
|
|
1708
|
-
case "mariadb":
|
|
1709
|
-
case "clickhouse":
|
|
1710
|
-
return `\`${identifier}\``;
|
|
1711
|
-
|
|
1712
|
-
case "postgresql":
|
|
1713
|
-
case "postgres":
|
|
1714
|
-
case "pg":
|
|
1715
|
-
case "cockroachdb":
|
|
1716
|
-
case "cockroach":
|
|
1717
|
-
return `"${identifier}"`;
|
|
1718
|
-
|
|
1719
|
-
case "sqlite":
|
|
1720
|
-
case "sqlite3":
|
|
1721
|
-
// SQLite supports both backticks and double quotes, but double quotes are standard
|
|
1722
|
-
return `"${identifier}"`;
|
|
1723
|
-
|
|
1724
|
-
case "mssql":
|
|
1725
|
-
case "sqlserver":
|
|
1726
|
-
return `[${identifier}]`;
|
|
1727
|
-
|
|
1728
|
-
case "oracle":
|
|
1729
|
-
// Oracle typically uses uppercase and double quotes
|
|
1730
|
-
return `"${identifier.toUpperCase()}"`;
|
|
1731
|
-
|
|
1732
|
-
default:
|
|
1733
|
-
// For unsupported dialects, return unquoted identifier with warning
|
|
1734
|
-
console.warn(
|
|
1735
|
-
`Unsupported dialect: ${this.dialect}, returning unquoted identifier`,
|
|
1736
|
-
);
|
|
1737
|
-
return identifier;
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
/**
|
|
1742
|
-
* Execute raw SQL query
|
|
1743
|
-
* @param {string} sql - SQL query
|
|
1744
|
-
* @param {Array} bindings - Query bindings
|
|
1745
|
-
* @returns {Promise<Object>} Query result
|
|
1746
|
-
*/
|
|
1747
|
-
async executeQuery(sql, bindings = []) {
|
|
1748
|
-
try {
|
|
1749
|
-
if (!this.connection?.query && !this.connection?.execute) {
|
|
1750
|
-
throw new Error(
|
|
1751
|
-
"Database connection does not have query or execute method",
|
|
1752
|
-
);
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
const executeMethod = this.connection.execute || this.connection.query;
|
|
1756
|
-
const result = await executeMethod.call(this.connection, sql, bindings);
|
|
1757
|
-
|
|
1758
|
-
// Emit query event
|
|
1759
|
-
this.emit("query", { sql, bindings, result });
|
|
1760
|
-
|
|
1761
|
-
return result;
|
|
1762
|
-
} catch (error) {
|
|
1763
|
-
this.emit("query:error", { sql, bindings, error });
|
|
1764
|
-
throw error;
|
|
1765
|
-
}
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
/**
|
|
1769
|
-
* Clone the query builder
|
|
1770
|
-
* @returns {QueryBuilder} Cloned query builder instance
|
|
1771
|
-
*/
|
|
1772
|
-
clone() {
|
|
1773
|
-
const cloned = new QueryBuilder(
|
|
1774
|
-
this.tableName,
|
|
1775
|
-
this.connection,
|
|
1776
|
-
this.dialect,
|
|
1777
|
-
);
|
|
1778
|
-
|
|
1779
|
-
cloned.query = JSON.parse(JSON.stringify(this.query));
|
|
1780
|
-
cloned.bindings = [...this.bindings];
|
|
1781
|
-
cloned.paramIndex = this.paramIndex;
|
|
1782
|
-
cloned.subQueries = new Map(this.subQueries);
|
|
1783
|
-
|
|
1784
|
-
return cloned;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
/**
|
|
1788
|
-
* Add raw SQL to SELECT clause
|
|
1789
|
-
* @param {string} expression - Raw SQL expression
|
|
1790
|
-
* @param {Array} bindings - Bindings for raw SQL
|
|
1791
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1792
|
-
*/
|
|
1793
|
-
selectRaw(expression, bindings = []) {
|
|
1794
|
-
this.query.columns.push({ raw: expression });
|
|
1795
|
-
this.bindings.push(...bindings);
|
|
1796
|
-
return this;
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
/**
|
|
1800
|
-
* Add raw value
|
|
1801
|
-
* @param {string} value - Raw value
|
|
1802
|
-
* @returns {Object} Raw value object
|
|
1803
|
-
*/
|
|
1804
|
-
raw(value) {
|
|
1805
|
-
return { raw: value };
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
// ==================== UTILITY METHODS ====================
|
|
1809
|
-
|
|
1810
|
-
/**
|
|
1811
|
-
* Paginate results
|
|
1812
|
-
* @param {number} page - Page number (1-based)
|
|
1813
|
-
* @param {number} perPage - Items per page
|
|
1814
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1815
|
-
*/
|
|
1816
|
-
paginate(page = 1, perPage = 20) {
|
|
1817
|
-
const safePage = Math.max(1, page);
|
|
1818
|
-
const safePerPage = Math.min(Math.max(1, perPage), 100);
|
|
1819
|
-
|
|
1820
|
-
this.query.limit = safePerPage;
|
|
1821
|
-
this.query.offset = (safePage - 1) * safePerPage;
|
|
1822
|
-
|
|
1823
|
-
return this;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
/**
|
|
1827
|
-
* Get paginated results with metadata
|
|
1828
|
-
* @param {number} page - Page number (1-based)
|
|
1829
|
-
* @param {number} perPage - Items per page
|
|
1830
|
-
* @returns {Promise<Object>} Paginated results with metadata
|
|
1831
|
-
*/
|
|
1832
|
-
async paginateWithMetadata(page = 1, perPage = 20) {
|
|
1833
|
-
const countQuery = this.clone();
|
|
1834
|
-
countQuery.query.columns = ["COUNT(*) as total"];
|
|
1835
|
-
countQuery.query.limit = null;
|
|
1836
|
-
countQuery.query.offset = null;
|
|
1837
|
-
countQuery.query.orderBy = [];
|
|
1838
|
-
|
|
1839
|
-
const countResult = await countQuery.first();
|
|
1840
|
-
const total = parseInt(countResult.total);
|
|
1841
|
-
|
|
1842
|
-
this.paginate(page, perPage);
|
|
1843
|
-
const data = await this.get();
|
|
1844
|
-
|
|
1845
|
-
const totalPages = Math.ceil(total / perPage);
|
|
1846
|
-
const hasNextPage = page < totalPages;
|
|
1847
|
-
const hasPrevPage = page > 1;
|
|
1848
|
-
|
|
1849
|
-
return {
|
|
1850
|
-
data,
|
|
1851
|
-
pagination: {
|
|
1852
|
-
page,
|
|
1853
|
-
perPage,
|
|
1854
|
-
total,
|
|
1855
|
-
totalPages,
|
|
1856
|
-
hasNextPage,
|
|
1857
|
-
hasPrevPage,
|
|
1858
|
-
nextPage: hasNextPage ? page + 1 : null,
|
|
1859
|
-
prevPage: hasPrevPage ? page - 1 : null,
|
|
1860
|
-
},
|
|
1861
|
-
};
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
/**
|
|
1865
|
-
* Increment column value
|
|
1866
|
-
* @param {string} column - Column name
|
|
1867
|
-
* @param {number} amount - Amount to increment
|
|
1868
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1869
|
-
*/
|
|
1870
|
-
increment(column, amount = 1) {
|
|
1871
|
-
this.query.type = "update";
|
|
1872
|
-
if (!this.query.data) this.query.data = {};
|
|
1873
|
-
this.query.data[column] = this.raw(
|
|
1874
|
-
`${this.wrapColumn(column)} + ${amount}`,
|
|
1875
|
-
);
|
|
1876
|
-
return this;
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
/**
|
|
1880
|
-
* Decrement column value
|
|
1881
|
-
* @param {string} column - Column name
|
|
1882
|
-
* @param {number} amount - Amount to decrement
|
|
1883
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1884
|
-
*/
|
|
1885
|
-
decrement(column, amount = 1) {
|
|
1886
|
-
this.query.type = "update";
|
|
1887
|
-
if (!this.query.data) this.query.data = {};
|
|
1888
|
-
this.query.data[column] = this.raw(
|
|
1889
|
-
`${this.wrapColumn(column)} - ${amount}`,
|
|
1890
|
-
);
|
|
1891
|
-
return this;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
/**
|
|
1895
|
-
* Add raw GROUP BY clause
|
|
1896
|
-
* @param {string} expression - Raw GROUP BY expression
|
|
1897
|
-
* @param {Array} bindings - Bindings for raw expression
|
|
1898
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1899
|
-
*/
|
|
1900
|
-
groupByRaw(expression, bindings = []) {
|
|
1901
|
-
this.query.groupBy.push({ raw: expression });
|
|
1902
|
-
this.bindings.push(...bindings);
|
|
1903
|
-
return this;
|
|
1904
|
-
}
|
|
1905
|
-
|
|
1906
|
-
/**
|
|
1907
|
-
* Add raw HAVING clause
|
|
1908
|
-
* @param {string} sql - Raw SQL HAVING clause
|
|
1909
|
-
* @param {Array} bindings - Bindings for raw SQL
|
|
1910
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
1911
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1912
|
-
*/
|
|
1913
|
-
havingRaw(sql, bindings = [], boolean = "and") {
|
|
1914
|
-
this.query.having.push({
|
|
1915
|
-
raw: sql,
|
|
1916
|
-
boolean,
|
|
1917
|
-
});
|
|
1918
|
-
|
|
1919
|
-
this.bindings.push(...bindings);
|
|
1920
|
-
return this;
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
/**
|
|
1924
|
-
* Add WHERE column comparison
|
|
1925
|
-
* @param {string} first - First column
|
|
1926
|
-
* @param {string} operator - Comparison operator
|
|
1927
|
-
* @param {string} second - Second column
|
|
1928
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
1929
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1930
|
-
*/
|
|
1931
|
-
whereColumn(first, operator, second, boolean = "and") {
|
|
1932
|
-
this.query.where.push({
|
|
1933
|
-
type: "column",
|
|
1934
|
-
first,
|
|
1935
|
-
operator,
|
|
1936
|
-
second,
|
|
1937
|
-
boolean,
|
|
1938
|
-
});
|
|
1939
|
-
return this;
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
/**
|
|
1943
|
-
* Add WHERE EXISTS subquery
|
|
1944
|
-
* @param {Function} callback - Subquery callback
|
|
1945
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
1946
|
-
* @param {boolean} not - Whether to use NOT EXISTS
|
|
1947
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1948
|
-
*/
|
|
1949
|
-
whereExists(callback, boolean = "and", not = false) {
|
|
1950
|
-
const subQuery = new QueryBuilder("", this.connection, this.dialect);
|
|
1951
|
-
callback(subQuery);
|
|
1952
|
-
const { sql, bindings } = subQuery.toSQL();
|
|
1953
|
-
|
|
1954
|
-
this.query.where.push({
|
|
1955
|
-
type: "exists",
|
|
1956
|
-
sql: `(${sql})`,
|
|
1957
|
-
boolean,
|
|
1958
|
-
not,
|
|
1959
|
-
});
|
|
1960
|
-
|
|
1961
|
-
this.bindings.push(...bindings);
|
|
1962
|
-
return this;
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
/**
|
|
1966
|
-
* Add WHERE NOT EXISTS subquery
|
|
1967
|
-
* @param {Function} callback - Subquery callback
|
|
1968
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
1969
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1970
|
-
*/
|
|
1971
|
-
whereNotExists(callback, boolean = "and") {
|
|
1972
|
-
return this.whereExists(callback, boolean, true);
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
/**
|
|
1976
|
-
* Add WHERE subquery condition
|
|
1977
|
-
* @param {string} column - Column name
|
|
1978
|
-
* @param {string} operator - Comparison operator
|
|
1979
|
-
* @param {Function} callback - Subquery callback
|
|
1980
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
1981
|
-
* @returns {QueryBuilder} Query builder instance
|
|
1982
|
-
*/
|
|
1983
|
-
whereSub(column, operator, callback, boolean = "and") {
|
|
1984
|
-
const subQuery = new QueryBuilder("", this.connection, this.dialect);
|
|
1985
|
-
callback(subQuery);
|
|
1986
|
-
const { sql, bindings } = subQuery.toSQL();
|
|
1987
|
-
|
|
1988
|
-
this.query.where.push({
|
|
1989
|
-
type: "subquery",
|
|
1990
|
-
column,
|
|
1991
|
-
operator,
|
|
1992
|
-
sql: `(${sql})`,
|
|
1993
|
-
boolean,
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
/**
|
|
1998
|
-
* Add OR WHERE column comparison
|
|
1999
|
-
* @param {string} first - First column
|
|
2000
|
-
* @param {string} operator - Comparison operator
|
|
2001
|
-
* @param {string} second - Second column
|
|
2002
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2003
|
-
*/
|
|
2004
|
-
orWhereColumn(first, operator, second) {
|
|
2005
|
-
return this.whereColumn(first, operator, second, "or");
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
/**
|
|
2009
|
-
* Add WHERE date condition
|
|
2010
|
-
* @param {string} column - Column name
|
|
2011
|
-
* @param {string} operator - Comparison operator
|
|
2012
|
-
* @param {Date|string} value - Date value
|
|
2013
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
2014
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2015
|
-
*/
|
|
2016
|
-
whereDate(column, operator, value, boolean = "and") {
|
|
2017
|
-
const dateValue =
|
|
2018
|
-
value instanceof Date ? value.toISOString().split("T") : value;
|
|
2019
|
-
this.query.where.push({
|
|
2020
|
-
type: "date",
|
|
2021
|
-
column,
|
|
2022
|
-
operator,
|
|
2023
|
-
value: dateValue,
|
|
2024
|
-
boolean,
|
|
2025
|
-
});
|
|
2026
|
-
this.bindings.push(dateValue);
|
|
2027
|
-
return this;
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
/**
|
|
2031
|
-
* Add WHERE time condition
|
|
2032
|
-
* @param {string} column - Column
|
|
2033
|
-
* @param {string} operator - Comparison operator
|
|
2034
|
-
* @param {Date|string} value - Time value
|
|
2035
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
2036
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2037
|
-
*/
|
|
2038
|
-
whereTime(column, operator, value, boolean = "and") {
|
|
2039
|
-
const timeValue =
|
|
2040
|
-
value instanceof Date ? value.toISOString().split("T").split(".") : value;
|
|
2041
|
-
this.query.where.push({
|
|
2042
|
-
type: "time",
|
|
2043
|
-
column,
|
|
2044
|
-
operator,
|
|
2045
|
-
value: timeValue,
|
|
2046
|
-
boolean,
|
|
2047
|
-
});
|
|
2048
|
-
this.bindings.push(timeValue);
|
|
2049
|
-
return this;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
/**
|
|
2053
|
-
* Add WHERE year condition
|
|
2054
|
-
* @param {string} column - Column name
|
|
2055
|
-
* @param {string} operator - Comparison operator
|
|
2056
|
-
* @param {number|string} value - Year value
|
|
2057
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
2058
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2059
|
-
*/
|
|
2060
|
-
whereYear(column, operator, value, boolean = "and") {
|
|
2061
|
-
this.query.where.push({
|
|
2062
|
-
type: "year",
|
|
2063
|
-
column,
|
|
2064
|
-
operator,
|
|
2065
|
-
value,
|
|
2066
|
-
boolean,
|
|
2067
|
-
});
|
|
2068
|
-
this.bindings.push(value);
|
|
2069
|
-
return this;
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
/**
|
|
2073
|
-
* Add WHERE month condition
|
|
2074
|
-
* @param {string} column - Column name
|
|
2075
|
-
* @param {string} operator - Comparison operator
|
|
2076
|
-
* @param {number|string} value - Month value
|
|
2077
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
2078
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2079
|
-
*/
|
|
2080
|
-
whereMonth(column, operator, value, boolean = "and") {
|
|
2081
|
-
this.query.where.push({
|
|
2082
|
-
type: "month",
|
|
2083
|
-
column,
|
|
2084
|
-
operator,
|
|
2085
|
-
value,
|
|
2086
|
-
boolean,
|
|
2087
|
-
});
|
|
2088
|
-
this.bindings.push(value);
|
|
2089
|
-
return this;
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
/**
|
|
2093
|
-
* Add WHERE day condition
|
|
2094
|
-
* @param {string} column - Column name
|
|
2095
|
-
* @param {string} operator - Comparison operator
|
|
2096
|
-
* @param {number|string} value - Day value
|
|
2097
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
2098
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2099
|
-
*/
|
|
2100
|
-
whereDay(column, operator, value, boolean = "and") {
|
|
2101
|
-
this.query.where.push({
|
|
2102
|
-
type: "day",
|
|
2103
|
-
column,
|
|
2104
|
-
operator,
|
|
2105
|
-
value,
|
|
2106
|
-
boolean,
|
|
2107
|
-
});
|
|
2108
|
-
this.bindings.push(value);
|
|
2109
|
-
return this;
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
/**
|
|
2113
|
-
* Add WHERE condition group
|
|
2114
|
-
* @param {Function} callback - Group callback
|
|
2115
|
-
* @param {string} boolean - Boolean operator (and, or)
|
|
2116
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2117
|
-
*/
|
|
2118
|
-
whereGroup(callback, boolean = "and") {
|
|
2119
|
-
const groupQuery = new QueryBuilder("", this.connection, this.dialect);
|
|
2120
|
-
callback(groupQuery);
|
|
2121
|
-
const { sql, bindings } = groupQuery.toSQL();
|
|
2122
|
-
|
|
2123
|
-
this.query.where.push({
|
|
2124
|
-
type: "group",
|
|
2125
|
-
sql: `(${sql})`,
|
|
2126
|
-
boolean,
|
|
2127
|
-
});
|
|
2128
|
-
|
|
2129
|
-
this.bindings.push(...bindings);
|
|
2130
|
-
return this;
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
/**
|
|
2134
|
-
* Add OR WHERE condition group
|
|
2135
|
-
* @param {Function} callback - Group callback
|
|
2136
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2137
|
-
*/
|
|
2138
|
-
orWhereGroup(callback) {
|
|
2139
|
-
return this.whereGroup(callback, "or");
|
|
2140
|
-
}
|
|
2141
|
-
|
|
2142
|
-
/**
|
|
2143
|
-
* Add UNION query
|
|
2144
|
-
* @param {Function|QueryBuilder} query - Query to union
|
|
2145
|
-
* @param {boolean} all - Whether to use UNION ALL
|
|
2146
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2147
|
-
*/
|
|
2148
|
-
union(query, all = false) {
|
|
2149
|
-
const unionQuery =
|
|
2150
|
-
typeof query === "function"
|
|
2151
|
-
? new QueryBuilder("", this.connection, this.dialect)
|
|
2152
|
-
: query;
|
|
2153
|
-
|
|
2154
|
-
if (typeof query === "function") {
|
|
2155
|
-
query(unionQuery);
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
const { sql, bindings } = unionQuery.toSQL();
|
|
2159
|
-
|
|
2160
|
-
this.query.union.push({
|
|
2161
|
-
query: sql,
|
|
2162
|
-
all,
|
|
2163
|
-
bindings,
|
|
2164
|
-
});
|
|
2165
|
-
|
|
2166
|
-
this.bindings.push(...bindings);
|
|
2167
|
-
return this;
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
/**
|
|
2171
|
-
* Add UNION ALL query
|
|
2172
|
-
* @param {Function|QueryBuilder} query - Query to union
|
|
2173
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2174
|
-
*/
|
|
2175
|
-
unionAll(query) {
|
|
2176
|
-
return this.union(query, true);
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
/**
|
|
2180
|
-
* Add WITH clause (Common Table Expression)
|
|
2181
|
-
* @param {string} name - CTE name
|
|
2182
|
-
* @param {Function|string} query - CTE query or SQL
|
|
2183
|
-
* @param {Array} columns - CTE column names
|
|
2184
|
-
* @param {boolean} recursive - Whether CTE is recursive
|
|
2185
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2186
|
-
*/
|
|
2187
|
-
with(name, query, columns = [], recursive = false) {
|
|
2188
|
-
let cteQuery;
|
|
2189
|
-
let cteBindings = [];
|
|
2190
|
-
|
|
2191
|
-
if (typeof query === "function") {
|
|
2192
|
-
const qb = new QueryBuilder("", this.connection, this.dialect);
|
|
2193
|
-
query(qb);
|
|
2194
|
-
const result = qb.toSQL();
|
|
2195
|
-
cteQuery = result.sql;
|
|
2196
|
-
cteBindings = result.bindings;
|
|
2197
|
-
} else if (typeof query === "string") {
|
|
2198
|
-
cteQuery = query;
|
|
2199
|
-
} else if (query instanceof QueryBuilder) {
|
|
2200
|
-
const result = query.toSQL();
|
|
2201
|
-
cteQuery = result.sql;
|
|
2202
|
-
cteBindings = result.bindings;
|
|
2203
|
-
} else {
|
|
2204
|
-
throw new Error(
|
|
2205
|
-
"CTE query must be a function, string, or QueryBuilder instance",
|
|
2206
|
-
);
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
this.query.with.push({
|
|
2210
|
-
name,
|
|
2211
|
-
query: cteQuery,
|
|
2212
|
-
columns,
|
|
2213
|
-
recursive,
|
|
2214
|
-
});
|
|
2215
|
-
|
|
2216
|
-
this.bindings.push(...cteBindings);
|
|
2217
|
-
return this;
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
/**
|
|
2221
|
-
* Add recursive WITH clause
|
|
2222
|
-
* @param {string} name - CTE name
|
|
2223
|
-
* @param {Function} query - CTE query function
|
|
2224
|
-
* @param {Array} columns - CTE column names
|
|
2225
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2226
|
-
*/
|
|
2227
|
-
withRecursive(name, query, columns = []) {
|
|
2228
|
-
return this.with(name, query, columns, true);
|
|
2229
|
-
}
|
|
2230
|
-
|
|
2231
|
-
/**
|
|
2232
|
-
* Add LOCK clause
|
|
2233
|
-
* @param {string} lock - Lock type (FOR UPDATE, FOR SHARE, etc.)
|
|
2234
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2235
|
-
*/
|
|
2236
|
-
lock(lock = "FOR UPDATE") {
|
|
2237
|
-
this.query.lock = lock;
|
|
2238
|
-
return this;
|
|
2239
|
-
}
|
|
2240
|
-
|
|
2241
|
-
/**
|
|
2242
|
-
* Add LOCK FOR UPDATE clause
|
|
2243
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2244
|
-
*/
|
|
2245
|
-
lockForUpdate() {
|
|
2246
|
-
return this.lock("FOR UPDATE");
|
|
2247
|
-
}
|
|
2248
|
-
|
|
2249
|
-
/**
|
|
2250
|
-
* Add LOCK FOR SHARE clause
|
|
2251
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2252
|
-
*/
|
|
2253
|
-
lockForShare() {
|
|
2254
|
-
return this.lock("FOR SHARE");
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
/**
|
|
2258
|
-
* Add RETURNING clause (PostgreSQL only)
|
|
2259
|
-
* @param {...string} columns - Columns to return
|
|
2260
|
-
* @returns {QueryBuilder} Query builder instance
|
|
2261
|
-
*/
|
|
2262
|
-
returning(...columns) {
|
|
2263
|
-
if (
|
|
2264
|
-
!["postgresql", "postgres", "pg", "cockroachdb", "cockroach"].includes(
|
|
2265
|
-
this.dialect,
|
|
2266
|
-
)
|
|
2267
|
-
) {
|
|
2268
|
-
console.warn(
|
|
2269
|
-
"RETURNING clause is only supported in PostgreSQL and CockroachDB",
|
|
2270
|
-
);
|
|
2271
|
-
}
|
|
2272
|
-
this.query.returning = columns;
|
|
2273
|
-
return this;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
/**
|
|
2277
|
-
* Execute query and return first result or default value
|
|
2278
|
-
* @param {*} defaultValue - Default value if no result
|
|
2279
|
-
* @returns {Promise<*>} First result or default value
|
|
2280
|
-
*/
|
|
2281
|
-
async firstOr(defaultValue = null) {
|
|
2282
|
-
const result = await this.first();
|
|
2283
|
-
return result || defaultValue;
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
/**
|
|
2287
|
-
* Execute query and return first result or throw error
|
|
2288
|
-
* @param {string} message - Error message
|
|
2289
|
-
* @returns {Promise<Object>} First result
|
|
2290
|
-
* @throws {Error} If no result found
|
|
2291
|
-
*/
|
|
2292
|
-
async firstOrFail(message = "No records found") {
|
|
2293
|
-
const result = await this.first();
|
|
2294
|
-
if (!result) {
|
|
2295
|
-
throw new Error(message);
|
|
2296
|
-
}
|
|
2297
|
-
return result;
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
/**
|
|
2301
|
-
* Execute query and return value of a single column
|
|
2302
|
-
* @param {string} column - Column name
|
|
2303
|
-
* @returns {Promise<*>} Column value
|
|
2304
|
-
*/
|
|
2305
|
-
async value(column) {
|
|
2306
|
-
const result = await this.first();
|
|
2307
|
-
return result ? result[column] : null;
|
|
2308
|
-
}
|
|
2309
|
-
|
|
2310
|
-
/**
|
|
2311
|
-
* Execute query and return plucked values
|
|
2312
|
-
* @param {string} column - Column name
|
|
2313
|
-
* @returns {Promise<Array>} Array of column values
|
|
2314
|
-
*/
|
|
2315
|
-
async pluck(column) {
|
|
2316
|
-
const results = await this.get();
|
|
2317
|
-
return results.map((row) => row[column]);
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
/**
|
|
2321
|
-
* Execute query and return key-value pairs
|
|
2322
|
-
* @param {string} keyColumn - Key column name
|
|
2323
|
-
* @param {string} valueColumn - Value column name
|
|
2324
|
-
* @returns {Promise<Object>} Key-value object
|
|
2325
|
-
*/
|
|
2326
|
-
async keyBy(keyColumn, valueColumn = null) {
|
|
2327
|
-
const results = await this.get();
|
|
2328
|
-
const keyValuePairs = {};
|
|
2329
|
-
|
|
2330
|
-
results.forEach((row) => {
|
|
2331
|
-
const key = row[keyColumn];
|
|
2332
|
-
const value = valueColumn ? row[valueColumn] : row;
|
|
2333
|
-
keyValuePairs[key] = value;
|
|
2334
|
-
});
|
|
2335
|
-
|
|
2336
|
-
return keyValuePairs;
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
/**
|
|
2340
|
-
* Execute query and return chunked results
|
|
2341
|
-
* @param {number} size - Chunk size
|
|
2342
|
-
* @param {Function} callback - Callback for each chunk
|
|
2343
|
-
* @returns {Promise<void>}
|
|
2344
|
-
*/
|
|
2345
|
-
async chunk(size, callback) {
|
|
2346
|
-
let page = 1;
|
|
2347
|
-
let hasMore = true;
|
|
2348
|
-
|
|
2349
|
-
while (hasMore) {
|
|
2350
|
-
const chunkQuery = this.clone();
|
|
2351
|
-
chunkQuery.paginate(page, size);
|
|
2352
|
-
const results = await chunkQuery.get();
|
|
2353
|
-
|
|
2354
|
-
if (results.length > 0) {
|
|
2355
|
-
await callback(results, page);
|
|
2356
|
-
page++;
|
|
2357
|
-
} else {
|
|
2358
|
-
hasMore = false;
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
/**
|
|
2364
|
-
* Execute query and return cursor for streaming
|
|
2365
|
-
* @param {number} chunkSize - Chunk size for streaming
|
|
2366
|
-
* @returns {AsyncGenerator} Async generator for streaming results
|
|
2367
|
-
*/
|
|
2368
|
-
async *cursor(chunkSize = 100) {
|
|
2369
|
-
let page = 1;
|
|
2370
|
-
let hasMore = true;
|
|
2371
|
-
|
|
2372
|
-
while (hasMore) {
|
|
2373
|
-
const chunkQuery = this.clone();
|
|
2374
|
-
chunkQuery.paginate(page, chunkSize);
|
|
2375
|
-
const results = await chunkQuery.get();
|
|
2376
|
-
|
|
2377
|
-
if (results.length > 0) {
|
|
2378
|
-
for (const result of results) {
|
|
2379
|
-
yield result;
|
|
2380
|
-
}
|
|
2381
|
-
page++;
|
|
2382
|
-
} else {
|
|
2383
|
-
hasMore = false;
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
}
|
|
2387
|
-
|
|
2388
|
-
/**
|
|
2389
|
-
* Execute query and return aggregated results
|
|
2390
|
-
* @param {string} keyColumn - Column to group by
|
|
2391
|
-
* @param {Function} aggregator - Aggregation function
|
|
2392
|
-
* @returns {Promise<Object>} Aggregated results
|
|
2393
|
-
*/
|
|
2394
|
-
async aggregate(keyColumn, aggregator) {
|
|
2395
|
-
const results = await this.get();
|
|
2396
|
-
const aggregated = {};
|
|
2397
|
-
|
|
2398
|
-
results.forEach((row) => {
|
|
2399
|
-
const key = row[keyColumn];
|
|
2400
|
-
if (!aggregated[key]) {
|
|
2401
|
-
aggregated[key] = [];
|
|
2402
|
-
}
|
|
2403
|
-
aggregated[key].push(row);
|
|
2404
|
-
});
|
|
2405
|
-
|
|
2406
|
-
for (const key in aggregated) {
|
|
2407
|
-
aggregated[key] = aggregator(aggregated[key]);
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
return aggregated;
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
/**
|
|
2414
|
-
* Execute query and return results as map
|
|
2415
|
-
* @param {string} keyColumn - Column to use as key
|
|
2416
|
-
* @returns {Promise<Map>} Map of results
|
|
2417
|
-
*/
|
|
2418
|
-
async map(keyColumn) {
|
|
2419
|
-
const results = await this.get();
|
|
2420
|
-
const map = new Map();
|
|
2421
|
-
|
|
2422
|
-
results.forEach((row) => {
|
|
2423
|
-
map.set(row[keyColumn], row);
|
|
2424
|
-
});
|
|
2425
|
-
|
|
2426
|
-
return map;
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
/**
|
|
2430
|
-
* Execute query and return results grouped by column
|
|
2431
|
-
* @param {string} groupColumn - Column to group by
|
|
2432
|
-
* @returns {Promise<Object>} Grouped results
|
|
2433
|
-
*/
|
|
2434
|
-
async groupByColumn(groupColumn) {
|
|
2435
|
-
const results = await this.get();
|
|
2436
|
-
const grouped = {};
|
|
2437
|
-
|
|
2438
|
-
results.forEach((row) => {
|
|
2439
|
-
const key = row[groupColumn];
|
|
2440
|
-
if (!grouped[key]) {
|
|
2441
|
-
grouped[key] = [];
|
|
2442
|
-
}
|
|
2443
|
-
grouped[key].push(row);
|
|
2444
|
-
});
|
|
2445
|
-
|
|
2446
|
-
return grouped;
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
/**
|
|
2450
|
-
* Execute query and return results with index
|
|
2451
|
-
* @param {Function} indexer - Function to create index key
|
|
2452
|
-
* @returns {Promise<Object>} Indexed results
|
|
2453
|
-
*/
|
|
2454
|
-
async indexBy(indexer) {
|
|
2455
|
-
const results = await this.get();
|
|
2456
|
-
const indexed = {};
|
|
2457
|
-
|
|
2458
|
-
results.forEach((row) => {
|
|
2459
|
-
const key = indexer(row);
|
|
2460
|
-
indexed[key] = row;
|
|
2461
|
-
});
|
|
2462
|
-
|
|
2463
|
-
return indexed;
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
/**
|
|
2467
|
-
* Execute query and return only distinct results
|
|
2468
|
-
* @param {...string} columns - Columns to distinct by
|
|
2469
|
-
* @returns {Promise<Array>} Distinct results
|
|
2470
|
-
*/
|
|
2471
|
-
async distinctResults(...columns) {
|
|
2472
|
-
this.distinct();
|
|
2473
|
-
if (columns.length > 0) {
|
|
2474
|
-
this.select(...columns);
|
|
2475
|
-
}
|
|
2476
|
-
return this.get();
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
/**
|
|
2480
|
-
* Execute query and return results with limit
|
|
2481
|
-
* @param {number} limit - Maximum number of results
|
|
2482
|
-
* @returns {Promise<Array>} Limited results
|
|
2483
|
-
*/
|
|
2484
|
-
async take(limit) {
|
|
2485
|
-
this.limit(limit);
|
|
2486
|
-
return this.get();
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
/**
|
|
2490
|
-
* Execute query and skip first N results
|
|
2491
|
-
* @param {number} offset - Number of results to skip
|
|
2492
|
-
* @returns {Promise<Array>} Results after skipping
|
|
2493
|
-
*/
|
|
2494
|
-
async skip(offset) {
|
|
2495
|
-
this.offset(offset);
|
|
2496
|
-
return this.get();
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
/**
|
|
2500
|
-
* Execute query and return results in random order
|
|
2501
|
-
* @returns {Promise<Array>} Randomly ordered results
|
|
2502
|
-
*/
|
|
2503
|
-
async inRandomOrder() {
|
|
2504
|
-
this.orderByRaw(this.dialect === "mysql" ? "RAND()" : "RANDOM()");
|
|
2505
|
-
return this.get();
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
/**
|
|
2509
|
-
* Execute query and return results with specific columns only
|
|
2510
|
-
* @param {...string} columns - Columns to select
|
|
2511
|
-
* @returns {Promise<Array>} Results with selected columns
|
|
2512
|
-
*/
|
|
2513
|
-
async only(...columns) {
|
|
2514
|
-
this.select(...columns);
|
|
2515
|
-
return this.get();
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
/**
|
|
2519
|
-
* Execute query and return results without specific columns
|
|
2520
|
-
* @param {...string} columns - Columns to exclude
|
|
2521
|
-
* @returns {Promise<Array>} Results without excluded columns
|
|
2522
|
-
*/
|
|
2523
|
-
async except(...columns) {
|
|
2524
|
-
const allColumns = await this.getColumnNames();
|
|
2525
|
-
const selectedColumns = allColumns.filter((col) => !columns.includes(col));
|
|
2526
|
-
this.select(...selectedColumns);
|
|
2527
|
-
return this.get();
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
/**
|
|
2531
|
-
* Get column names from table
|
|
2532
|
-
* @returns {Promise<Array>} Array of column names
|
|
2533
|
-
*/
|
|
2534
|
-
async getColumnNames() {
|
|
2535
|
-
const originalColumns = this.query.columns;
|
|
2536
|
-
const originalType = this.query.type;
|
|
2537
|
-
|
|
2538
|
-
this.query.type = "select";
|
|
2539
|
-
this.query.columns = ["*"];
|
|
2540
|
-
this.query.limit = 1;
|
|
2541
|
-
|
|
2542
|
-
const result = await this.execute();
|
|
2543
|
-
const columns = result.fields
|
|
2544
|
-
? result.fields.map((f) => f.name)
|
|
2545
|
-
: Object.keys(result || {});
|
|
2546
|
-
|
|
2547
|
-
// Restore original state
|
|
2548
|
-
this.query.columns = originalColumns;
|
|
2549
|
-
this.query.type = originalType;
|
|
2550
|
-
|
|
2551
|
-
return columns;
|
|
2552
|
-
}
|
|
2553
|
-
|
|
2554
|
-
/**
|
|
2555
|
-
* Execute query and return results as JSON string
|
|
2556
|
-
* @returns {Promise<string>} JSON string of results
|
|
2557
|
-
*/
|
|
2558
|
-
async toJson() {
|
|
2559
|
-
const results = await this.get();
|
|
2560
|
-
return JSON.stringify(results, null, 2);
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
/**
|
|
2564
|
-
* Execute query and return results as CSV string
|
|
2565
|
-
* @returns {Promise<string>} CSV string of results
|
|
2566
|
-
*/
|
|
2567
|
-
async toCsv() {
|
|
2568
|
-
const results = await this.get();
|
|
2569
|
-
if (results.length === 0) {
|
|
2570
|
-
return "";
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
const headers = Object.keys(results);
|
|
2574
|
-
const csvRows = [];
|
|
2575
|
-
|
|
2576
|
-
// Add headers
|
|
2577
|
-
csvRows.push(headers.join(","));
|
|
2578
|
-
|
|
2579
|
-
// Add data rows
|
|
2580
|
-
results.forEach((row) => {
|
|
2581
|
-
const values = headers.map((header) => {
|
|
2582
|
-
const value = row[header];
|
|
2583
|
-
if (value === null || value === undefined) {
|
|
2584
|
-
return "";
|
|
2585
|
-
}
|
|
2586
|
-
const stringValue = String(value);
|
|
2587
|
-
// Escape quotes and wrap in quotes if contains comma or quotes
|
|
2588
|
-
if (stringValue.includes(",") || stringValue.includes('"')) {
|
|
2589
|
-
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
2590
|
-
}
|
|
2591
|
-
return stringValue;
|
|
2592
|
-
});
|
|
2593
|
-
csvRows.push(values.join(","));
|
|
2594
|
-
});
|
|
2595
|
-
|
|
2596
|
-
return csvRows.join("\n");
|
|
2597
|
-
}
|
|
2598
|
-
|
|
2599
|
-
/**
|
|
2600
|
-
* Execute query and return results as array of arrays
|
|
2601
|
-
* @returns {Promise<Array>} Array of arrays
|
|
2602
|
-
*/
|
|
2603
|
-
async toArray() {
|
|
2604
|
-
const results = await this.get();
|
|
2605
|
-
if (results.length === 0) {
|
|
2606
|
-
return [];
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
const headers = Object.keys(results);
|
|
2610
|
-
const array = [headers];
|
|
2611
|
-
|
|
2612
|
-
results.forEach((row) => {
|
|
2613
|
-
const values = headers.map((header) => row[header]);
|
|
2614
|
-
array.push(values);
|
|
2615
|
-
});
|
|
2616
|
-
|
|
2617
|
-
return array;
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
/**
|
|
2621
|
-
* Execute query and return results as key-value pairs
|
|
2622
|
-
* @param {string} keyColumn - Column to use as key
|
|
2623
|
-
* @param {string} valueColumn - Column to use as value
|
|
2624
|
-
* @returns {Promise<Object>} Key-value pairs
|
|
2625
|
-
*/
|
|
2626
|
-
async toKeyValue(keyColumn, valueColumn) {
|
|
2627
|
-
const results = await this.get();
|
|
2628
|
-
const keyValue = {};
|
|
2629
|
-
|
|
2630
|
-
results.forEach((row) => {
|
|
2631
|
-
keyValue[row[keyColumn]] = row[valueColumn];
|
|
2632
|
-
});
|
|
2633
|
-
|
|
2634
|
-
return keyValue;
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
/**
|
|
2638
|
-
* Execute query and return results with applied transformation
|
|
2639
|
-
* @param {Function} transformer - Transformation function
|
|
2640
|
-
* @returns {Promise<Array>} Transformed results
|
|
2641
|
-
*/
|
|
2642
|
-
async transform(transformer) {
|
|
2643
|
-
const results = await this.get();
|
|
2644
|
-
return results.map(transformer);
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
|
-
/**
|
|
2648
|
-
* Execute query and return results filtered by condition
|
|
2649
|
-
* @param {Function} filter - Filter function
|
|
2650
|
-
* @returns {Promise<Array>} Filtered results
|
|
2651
|
-
*/
|
|
2652
|
-
async filter(filter) {
|
|
2653
|
-
const results = await this.get();
|
|
2654
|
-
return results.filter(filter);
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
/**
|
|
2658
|
-
* Execute query and return results sorted by comparator
|
|
2659
|
-
* @param {Function} comparator - Comparison function
|
|
2660
|
-
* @returns {Promise<Array>} Sorted results
|
|
2661
|
-
*/
|
|
2662
|
-
async sort(comparator) {
|
|
2663
|
-
const results = await this.get();
|
|
2664
|
-
return results.sort(comparator);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
/**
|
|
2668
|
-
* Execute query and return results reduced by reducer
|
|
2669
|
-
* @param {Function} reducer - Reduce function
|
|
2670
|
-
* @param {*} initialValue - Initial value for reduction
|
|
2671
|
-
* @returns {Promise<*>} Reduced value
|
|
2672
|
-
*/
|
|
2673
|
-
async reduce(reducer, initialValue) {
|
|
2674
|
-
const results = await this.get();
|
|
2675
|
-
return results.reduce(reducer, initialValue);
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
/**
|
|
2679
|
-
* Execute query and return results mapped to new structure
|
|
2680
|
-
* @param {Function} mapper - Mapping function
|
|
2681
|
-
* @returns {Promise<Array>} Mapped results
|
|
2682
|
-
*/
|
|
2683
|
-
async mapResults(mapper) {
|
|
2684
|
-
const results = await this.get();
|
|
2685
|
-
return results.map(mapper);
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
/**
|
|
2689
|
-
* Execute query and return results with applied side effect
|
|
2690
|
-
* @param {Function} sideEffect - Side effect function
|
|
2691
|
-
* @returns {Promise<Array>} Results after side effect
|
|
2692
|
-
*/
|
|
2693
|
-
async tap(sideEffect) {
|
|
2694
|
-
const results = await this.get();
|
|
2695
|
-
sideEffect(results);
|
|
2696
|
-
return results;
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
/**
|
|
2700
|
-
* Execute query and return results with timing information
|
|
2701
|
-
* @returns {Promise<Object>} Results with timing
|
|
2702
|
-
*/
|
|
2703
|
-
async withTiming() {
|
|
2704
|
-
const startTime = Date.now();
|
|
2705
|
-
const results = await this.get();
|
|
2706
|
-
const endTime = Date.now();
|
|
2707
|
-
|
|
2708
|
-
return {
|
|
2709
|
-
results,
|
|
2710
|
-
timing: {
|
|
2711
|
-
startTime,
|
|
2712
|
-
endTime,
|
|
2713
|
-
duration: endTime - startTime,
|
|
2714
|
-
},
|
|
2715
|
-
};
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
/**
|
|
2719
|
-
* Execute query and return results with memory usage information
|
|
2720
|
-
* @returns {Promise<Object>} Results with memory usage
|
|
2721
|
-
*/
|
|
2722
|
-
async withMemoryUsage() {
|
|
2723
|
-
const startMemory = process.memoryUsage();
|
|
2724
|
-
const results = await this.get();
|
|
2725
|
-
const endMemory = process.memoryUsage();
|
|
2726
|
-
|
|
2727
|
-
return {
|
|
2728
|
-
results,
|
|
2729
|
-
memory: {
|
|
2730
|
-
start: startMemory,
|
|
2731
|
-
end: endMemory,
|
|
2732
|
-
diff: {
|
|
2733
|
-
rss: endMemory.rss - startMemory.rss,
|
|
2734
|
-
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
|
|
2735
|
-
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
|
|
2736
|
-
external: endMemory.external - startMemory.external,
|
|
2737
|
-
},
|
|
2738
|
-
},
|
|
2739
|
-
};
|
|
2740
|
-
}
|
|
2741
|
-
|
|
2742
|
-
/**
|
|
2743
|
-
* Execute query and return results with execution plan
|
|
2744
|
-
* @returns {Promise<Object>} Results with execution plan
|
|
2745
|
-
*/
|
|
2746
|
-
async withExplain() {
|
|
2747
|
-
const explainResult = await this.explain();
|
|
2748
|
-
const results = await this.get();
|
|
2749
|
-
|
|
2750
|
-
return {
|
|
2751
|
-
results,
|
|
2752
|
-
explain: explainResult,
|
|
2753
|
-
};
|
|
2754
|
-
}
|
|
2755
|
-
|
|
2756
|
-
/**
|
|
2757
|
-
* Execute query and return results with count
|
|
2758
|
-
* @returns {Promise<Object>} Results with count
|
|
2759
|
-
*/
|
|
2760
|
-
async withCount() {
|
|
2761
|
-
const countQuery = this.clone();
|
|
2762
|
-
countQuery.query.columns = ["COUNT(*) as total"];
|
|
2763
|
-
countQuery.query.limit = null;
|
|
2764
|
-
countQuery.query.offset = null;
|
|
2765
|
-
countQuery.query.orderBy = [];
|
|
2766
|
-
|
|
2767
|
-
const countResult = await countQuery.first();
|
|
2768
|
-
const results = await this.get();
|
|
2769
|
-
|
|
2770
|
-
return {
|
|
2771
|
-
results,
|
|
2772
|
-
count: parseInt(countResult.total),
|
|
2773
|
-
};
|
|
2774
|
-
}
|
|
2775
|
-
|
|
2776
|
-
/**
|
|
2777
|
-
* Execute query and return results with pagination metadata
|
|
2778
|
-
* @param {number} page - Page number
|
|
2779
|
-
* @param {number} perPage - Items per page
|
|
2780
|
-
* @returns {Promise<Object>} Results with pagination
|
|
2781
|
-
*/
|
|
2782
|
-
async withPagination(page = 1, perPage = 20) {
|
|
2783
|
-
return this.paginateWithMetadata(page, perPage);
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
/**
|
|
2787
|
-
* Execute query and return results with applied transformations
|
|
2788
|
-
* @param {Array<Function>} transformers - Array of transformer functions
|
|
2789
|
-
* @returns {Promise<Array>} Transformed results
|
|
2790
|
-
*/
|
|
2791
|
-
async pipe(...transformers) {
|
|
2792
|
-
let results = await this.get();
|
|
2793
|
-
|
|
2794
|
-
for (const transformer of transformers) {
|
|
2795
|
-
results = transformer(results);
|
|
2796
|
-
}
|
|
2797
|
-
|
|
2798
|
-
return results;
|
|
2799
|
-
}
|
|
2800
|
-
|
|
2801
|
-
/**
|
|
2802
|
-
* Execute query and return results with caching
|
|
2803
|
-
* @param {string} cacheKey - Cache key
|
|
2804
|
-
* @param {number} ttl - Time to live in seconds
|
|
2805
|
-
* @param {Function} cacheGetter - Cache getter function
|
|
2806
|
-
* @param {Function} cacheSetter - Cache setter function
|
|
2807
|
-
* @returns {Promise<Array>} Cached results
|
|
2808
|
-
*/
|
|
2809
|
-
async withCache(cacheKey, ttl = 300, cacheGetter = null, cacheSetter = null) {
|
|
2810
|
-
if (cacheGetter) {
|
|
2811
|
-
const cached = await cacheGetter(cacheKey);
|
|
2812
|
-
if (cached !== null && cached !== undefined) {
|
|
2813
|
-
return cached;
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
const results = await this.get();
|
|
2818
|
-
|
|
2819
|
-
if (cacheSetter) {
|
|
2820
|
-
await cacheSetter(cacheKey, results, ttl);
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
return results;
|
|
2824
|
-
}
|
|
2825
|
-
|
|
2826
|
-
/**
|
|
2827
|
-
* Execute query and return results with retry logic
|
|
2828
|
-
* @param {number} maxRetries - Maximum number of retries
|
|
2829
|
-
* @param {number} delay - Delay between retries in milliseconds
|
|
2830
|
-
* @param {Function} retryCondition - Condition for retry
|
|
2831
|
-
* @returns {Promise<Array>} Results with retry
|
|
2832
|
-
*/
|
|
2833
|
-
async withRetry(maxRetries = 3, delay = 1000, retryCondition = null) {
|
|
2834
|
-
let lastError;
|
|
2835
|
-
|
|
2836
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
2837
|
-
try {
|
|
2838
|
-
return await this.get();
|
|
2839
|
-
} catch (error) {
|
|
2840
|
-
lastError = error;
|
|
2841
|
-
|
|
2842
|
-
if (retryCondition && !retryCondition(error)) {
|
|
2843
|
-
throw error;
|
|
2844
|
-
}
|
|
2845
|
-
|
|
2846
|
-
if (attempt < maxRetries) {
|
|
2847
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2848
|
-
delay *= 2; // Exponential backoff
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
throw lastError;
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
/**
|
|
2857
|
-
* Execute query and return results with timeout
|
|
2858
|
-
* @param {number} timeout - Timeout in milliseconds
|
|
2859
|
-
* @returns {Promise<Array>} Results with timeout
|
|
2860
|
-
*/
|
|
2861
|
-
async withTimeout(timeout = 5000) {
|
|
2862
|
-
return Promise.race([
|
|
2863
|
-
this.get(),
|
|
2864
|
-
new Promise((_, reject) => {
|
|
2865
|
-
setTimeout(
|
|
2866
|
-
() => reject(new Error(`Query timeout after ${timeout}ms`)),
|
|
2867
|
-
timeout,
|
|
2868
|
-
);
|
|
2869
|
-
}),
|
|
2870
|
-
]);
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
/**
|
|
2874
|
-
* Execute query and return results with transaction
|
|
2875
|
-
* @param {Function} transactionCallback - Transaction callback
|
|
2876
|
-
* @returns {Promise<Array>} Results within transaction
|
|
2877
|
-
*/
|
|
2878
|
-
async withTransaction(transactionCallback) {
|
|
2879
|
-
if (!this.connection.beginTransaction) {
|
|
2880
|
-
throw new Error("Database connection does not support transactions");
|
|
2881
|
-
}
|
|
2882
|
-
|
|
2883
|
-
await this.connection.beginTransaction();
|
|
2884
|
-
|
|
2885
|
-
try {
|
|
2886
|
-
const results = await this.get();
|
|
2887
|
-
await transactionCallback(results);
|
|
2888
|
-
await this.connection.commit();
|
|
2889
|
-
return results;
|
|
2890
|
-
} catch (error) {
|
|
2891
|
-
await this.connection.rollback();
|
|
2892
|
-
throw error;
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
/**
|
|
2897
|
-
* Execute query and return results with error handling
|
|
2898
|
-
* @param {Function} errorHandler - Error handler function
|
|
2899
|
-
* @returns {Promise<Array>} Results or error handling result
|
|
2900
|
-
*/
|
|
2901
|
-
async withErrorHandling(errorHandler) {
|
|
2902
|
-
try {
|
|
2903
|
-
return await this.get();
|
|
2904
|
-
} catch (error) {
|
|
2905
|
-
return errorHandler(error);
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
|
|
2909
|
-
/**
|
|
2910
|
-
* Execute query and return results with validation
|
|
2911
|
-
* @param {Function} validator - Validation function
|
|
2912
|
-
* @returns {Promise<Array>} Validated results
|
|
2913
|
-
*/
|
|
2914
|
-
async withValidation(validator) {
|
|
2915
|
-
const results = await this.get();
|
|
2916
|
-
const validationResult = validator(results);
|
|
2917
|
-
|
|
2918
|
-
if (validationResult !== true) {
|
|
2919
|
-
throw new Error(`Validation failed: ${validationResult}`);
|
|
2920
|
-
}
|
|
2921
|
-
|
|
2922
|
-
return results;
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
/**
|
|
2926
|
-
* Execute query and return results with logging
|
|
2927
|
-
* @param {Function} logger - Logger function
|
|
2928
|
-
* @returns {Promise<Array>} Results with logging
|
|
2929
|
-
*/
|
|
2930
|
-
async withLogging(logger = console.log) {
|
|
2931
|
-
const startTime = Date.now();
|
|
2932
|
-
const { sql, bindings } = this.toSQL();
|
|
2933
|
-
|
|
2934
|
-
logger(`Executing query: ${sql}`);
|
|
2935
|
-
logger(`Bindings: ${JSON.stringify(bindings)}`);
|
|
2936
|
-
|
|
2937
|
-
try {
|
|
2938
|
-
const results = await this.get();
|
|
2939
|
-
const endTime = Date.now();
|
|
2940
|
-
|
|
2941
|
-
logger(`Query completed in ${endTime - startTime}ms`);
|
|
2942
|
-
logger(`Results count: ${results.length}`);
|
|
2943
|
-
|
|
2944
|
-
return results;
|
|
2945
|
-
} catch (error) {
|
|
2946
|
-
logger(`Query failed: ${error.message}`);
|
|
2947
|
-
throw error;
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
/**
|
|
2952
|
-
* Execute query and return results with profiling
|
|
2953
|
-
* @returns {Promise<Object>} Results with profiling information
|
|
2954
|
-
*/
|
|
2955
|
-
async withProfiling() {
|
|
2956
|
-
const startTime = Date.now();
|
|
2957
|
-
const startMemory = process.memoryUsage();
|
|
2958
|
-
|
|
2959
|
-
const { sql, bindings } = this.toSQL();
|
|
2960
|
-
const results = await this.get();
|
|
2961
|
-
|
|
2962
|
-
const endTime = Date.now();
|
|
2963
|
-
const endMemory = process.memoryUsage();
|
|
2964
|
-
|
|
2965
|
-
return {
|
|
2966
|
-
results,
|
|
2967
|
-
profile: {
|
|
2968
|
-
sql,
|
|
2969
|
-
bindings,
|
|
2970
|
-
timing: {
|
|
2971
|
-
startTime,
|
|
2972
|
-
endTime,
|
|
2973
|
-
duration: endTime - startTime,
|
|
2974
|
-
},
|
|
2975
|
-
memory: {
|
|
2976
|
-
start: startMemory,
|
|
2977
|
-
end: endMemory,
|
|
2978
|
-
diff: {
|
|
2979
|
-
rss: endMemory.rss - startMemory.rss,
|
|
2980
|
-
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
|
|
2981
|
-
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
|
|
2982
|
-
external: endMemory.external - startMemory.external,
|
|
2983
|
-
},
|
|
2984
|
-
},
|
|
2985
|
-
resultCount: results.length,
|
|
2986
|
-
},
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
|
|
2990
|
-
/**
|
|
2991
|
-
* Execute query and return results with all metadata
|
|
2992
|
-
* @returns {Promise<Object>} Results with full metadata
|
|
2993
|
-
*/
|
|
2994
|
-
async withMetadata() {
|
|
2995
|
-
const profile = await this.withProfiling();
|
|
2996
|
-
const explain = await this.explain();
|
|
2997
|
-
const count = await this.count();
|
|
2998
|
-
|
|
2999
|
-
return {
|
|
3000
|
-
...profile,
|
|
3001
|
-
explain,
|
|
3002
|
-
count,
|
|
3003
|
-
query: this.query,
|
|
3004
|
-
dialect: this.dialect,
|
|
3005
|
-
table: this.tableName,
|
|
3006
|
-
timestamp: new Date().toISOString(),
|
|
3007
|
-
};
|
|
3008
|
-
}
|
|
3009
|
-
|
|
3010
|
-
/**
|
|
3011
|
-
* Dump query information for debugging
|
|
3012
|
-
* @returns {Object} Query information
|
|
3013
|
-
*/
|
|
3014
|
-
dump() {
|
|
3015
|
-
const { sql, bindings } = this.toSQL();
|
|
3016
|
-
|
|
3017
|
-
return {
|
|
3018
|
-
sql,
|
|
3019
|
-
bindings,
|
|
3020
|
-
query: this.query,
|
|
3021
|
-
dialect: this.dialect,
|
|
3022
|
-
table: this.tableName,
|
|
3023
|
-
bindingsCount: bindings.length,
|
|
3024
|
-
timestamp: new Date().toISOString(),
|
|
3025
|
-
};
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
/**
|
|
3029
|
-
* Get SQL string for debugging
|
|
3030
|
-
* @returns {string} SQL string
|
|
3031
|
-
*/
|
|
3032
|
-
toSql() {
|
|
3033
|
-
return this.toSQL().sql;
|
|
3034
|
-
}
|
|
3035
|
-
|
|
3036
|
-
/**
|
|
3037
|
-
* Get query as string for logging
|
|
3038
|
-
* @returns {string} Query string representation
|
|
3039
|
-
*/
|
|
3040
|
-
toString() {
|
|
3041
|
-
const { sql, bindings } = this.toSQL();
|
|
3042
|
-
return `QueryBuilder: ${sql} [${bindings.join(", ")}]`;
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
/**
|
|
3046
|
-
* Check if query has WHERE conditions
|
|
3047
|
-
* @returns {boolean} True if has WHERE conditions
|
|
3048
|
-
*/
|
|
3049
|
-
hasWhere() {
|
|
3050
|
-
return this.query.where.length > 0;
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
/**
|
|
3054
|
-
* Check if query has JOINs
|
|
3055
|
-
* @returns {boolean} True if has JOINs
|
|
3056
|
-
*/
|
|
3057
|
-
hasJoins() {
|
|
3058
|
-
return this.query.joins.length > 0;
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
/**
|
|
3062
|
-
* Check if query has GROUP BY
|
|
3063
|
-
* @returns {boolean} True if has GROUP BY
|
|
3064
|
-
*/
|
|
3065
|
-
hasGroupBy() {
|
|
3066
|
-
return this.query.groupBy.length > 0;
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
/**
|
|
3070
|
-
* Check if query has ORDER BY
|
|
3071
|
-
* @returns {boolean} True if has ORDER BY
|
|
3072
|
-
*/
|
|
3073
|
-
hasOrderBy() {
|
|
3074
|
-
return this.query.orderBy.length > 0;
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
/**
|
|
3078
|
-
* Check if query has LIMIT
|
|
3079
|
-
* @returns {boolean} True if has LIMIT
|
|
3080
|
-
*/
|
|
3081
|
-
hasLimit() {
|
|
3082
|
-
return this.query.limit !== null;
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
/**
|
|
3086
|
-
* Check if query has OFFSET
|
|
3087
|
-
* @returns {boolean} True if has OFFSET
|
|
3088
|
-
*/
|
|
3089
|
-
hasOffset() {
|
|
3090
|
-
return this.query.offset !== null;
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
/**
|
|
3094
|
-
* Get connection
|
|
3095
|
-
* @returns {Object} Database connection
|
|
3096
|
-
*/
|
|
3097
|
-
getConnection() {
|
|
3098
|
-
return this.connection;
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
/**
|
|
3102
|
-
* Set connection
|
|
3103
|
-
* @param {Object} connection - Database connection
|
|
3104
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3105
|
-
*/
|
|
3106
|
-
setConnection(connection) {
|
|
3107
|
-
this.connection = connection;
|
|
3108
|
-
return this;
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
/**
|
|
3112
|
-
* Get bindings count
|
|
3113
|
-
* @returns {number} Number of bindings
|
|
3114
|
-
*/
|
|
3115
|
-
getBindingsCount() {
|
|
3116
|
-
return this.bindings.length;
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
/**
|
|
3120
|
-
* Get query bindings
|
|
3121
|
-
* @returns {Array} Query bindings
|
|
3122
|
-
*/
|
|
3123
|
-
getBindings() {
|
|
3124
|
-
return [...this.bindings];
|
|
3125
|
-
}
|
|
3126
|
-
|
|
3127
|
-
/**
|
|
3128
|
-
* Clear all bindings
|
|
3129
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3130
|
-
*/
|
|
3131
|
-
clearBindings() {
|
|
3132
|
-
this.bindings = [];
|
|
3133
|
-
return this;
|
|
3134
|
-
}
|
|
3135
|
-
|
|
3136
|
-
/**
|
|
3137
|
-
* Add binding to query
|
|
3138
|
-
* @param {*} value - Value to bind
|
|
3139
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3140
|
-
*/
|
|
3141
|
-
addBinding(value) {
|
|
3142
|
-
this.bindings.push(value);
|
|
3143
|
-
return this;
|
|
3144
|
-
}
|
|
3145
|
-
|
|
3146
|
-
/**
|
|
3147
|
-
* Add multiple bindings to query
|
|
3148
|
-
* @param {Array} values - Values to bind
|
|
3149
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3150
|
-
*/
|
|
3151
|
-
addBindings(values) {
|
|
3152
|
-
if (Array.isArray(values) && values.length > 50) {
|
|
3153
|
-
this.bindings = this.bindings.concat(values);
|
|
3154
|
-
} else {
|
|
3155
|
-
this.bindings.push(...values);
|
|
3156
|
-
}
|
|
3157
|
-
return this;
|
|
3158
|
-
}
|
|
3159
|
-
/**
|
|
3160
|
-
* Set bindings for query
|
|
3161
|
-
* @param {Array} bindings - Bindings to set
|
|
3162
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3163
|
-
*/
|
|
3164
|
-
setBindings(bindings) {
|
|
3165
|
-
this.bindings = [...bindings];
|
|
3166
|
-
return this;
|
|
3167
|
-
}
|
|
3168
|
-
|
|
3169
|
-
/**
|
|
3170
|
-
* Merge bindings from another query builder
|
|
3171
|
-
* @param {QueryBuilder} queryBuilder - Query builder to merge bindings from
|
|
3172
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3173
|
-
*/
|
|
3174
|
-
mergeBindings(queryBuilder) {
|
|
3175
|
-
this.bindings.push(...queryBuilder.getBindings());
|
|
3176
|
-
return this;
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
/**
|
|
3180
|
-
* Get query type
|
|
3181
|
-
* @returns {string} Query type
|
|
3182
|
-
*/
|
|
3183
|
-
getQueryType() {
|
|
3184
|
-
return this.query.type;
|
|
3185
|
-
}
|
|
3186
|
-
|
|
3187
|
-
/**
|
|
3188
|
-
* Set query type
|
|
3189
|
-
* @param {string} type - Query type (select, insert, update, delete)
|
|
3190
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3191
|
-
*/
|
|
3192
|
-
setQueryType(type) {
|
|
3193
|
-
const validTypes = ["select", "insert", "update", "delete"];
|
|
3194
|
-
if (!validTypes.includes(type)) {
|
|
3195
|
-
throw new Error(
|
|
3196
|
-
`Invalid query type: ${type}. Must be one of: ${validTypes.join(", ")}`,
|
|
3197
|
-
);
|
|
3198
|
-
}
|
|
3199
|
-
this.query.type = type;
|
|
3200
|
-
return this;
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
|
-
/**
|
|
3204
|
-
* Get table name
|
|
3205
|
-
* @returns {string} Table name
|
|
3206
|
-
*/
|
|
3207
|
-
getTable() {
|
|
3208
|
-
return this.tableName;
|
|
3209
|
-
}
|
|
3210
|
-
|
|
3211
|
-
/**
|
|
3212
|
-
* Set table name
|
|
3213
|
-
* @param {string} tableName - Table name
|
|
3214
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3215
|
-
*/
|
|
3216
|
-
setTable(tableName) {
|
|
3217
|
-
this.tableName = tableName;
|
|
3218
|
-
return this;
|
|
3219
|
-
}
|
|
3220
|
-
|
|
3221
|
-
/**
|
|
3222
|
-
* Get database dialect
|
|
3223
|
-
* @returns {string} Database dialect
|
|
3224
|
-
*/
|
|
3225
|
-
getDialect() {
|
|
3226
|
-
return this.dialect;
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
/**
|
|
3230
|
-
* Set database dialect
|
|
3231
|
-
* @param {string} dialect - Database dialect
|
|
3232
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3233
|
-
*/
|
|
3234
|
-
setDialect(dialect) {
|
|
3235
|
-
const supportedDialects = [
|
|
3236
|
-
"mysql",
|
|
3237
|
-
"mariadb",
|
|
3238
|
-
"postgresql",
|
|
3239
|
-
"postgres",
|
|
3240
|
-
"pg",
|
|
3241
|
-
"sqlite",
|
|
3242
|
-
"sqlite3",
|
|
3243
|
-
"mssql",
|
|
3244
|
-
"sqlserver",
|
|
3245
|
-
"oracle",
|
|
3246
|
-
"cockroachdb",
|
|
3247
|
-
"cockroach",
|
|
3248
|
-
"clickhouse",
|
|
3249
|
-
];
|
|
3250
|
-
|
|
3251
|
-
const normalizedDialect = dialect.toLowerCase();
|
|
3252
|
-
if (!supportedDialects.includes(normalizedDialect)) {
|
|
3253
|
-
throw new Error(`Unsupported database dialect: ${dialect}`);
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
this.dialect = normalizedDialect;
|
|
3257
|
-
return this;
|
|
3258
|
-
}
|
|
3259
|
-
|
|
3260
|
-
/**
|
|
3261
|
-
* Get query structure
|
|
3262
|
-
* @returns {Object} Query structure
|
|
3263
|
-
*/
|
|
3264
|
-
getQuery() {
|
|
3265
|
-
return JSON.parse(JSON.stringify(this.query));
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
/**
|
|
3269
|
-
* Set query structure
|
|
3270
|
-
* @param {Object} query - Query structure
|
|
3271
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3272
|
-
*/
|
|
3273
|
-
setQuery(query) {
|
|
3274
|
-
this.query = JSON.parse(JSON.stringify(query));
|
|
3275
|
-
return this;
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
/**
|
|
3279
|
-
* Get query columns
|
|
3280
|
-
* @returns {Array} Query columns
|
|
3281
|
-
*/
|
|
3282
|
-
getColumns() {
|
|
3283
|
-
return [...this.query.columns];
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
|
-
/**
|
|
3287
|
-
* Set query columns
|
|
3288
|
-
* @param {Array} columns - Columns to select
|
|
3289
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3290
|
-
*/
|
|
3291
|
-
setColumns(columns) {
|
|
3292
|
-
this.query.columns = Array.isArray(columns) ? columns : [columns];
|
|
3293
|
-
return this;
|
|
3294
|
-
}
|
|
3295
|
-
|
|
3296
|
-
/**
|
|
3297
|
-
* Get WHERE conditions
|
|
3298
|
-
* @returns {Array} WHERE conditions
|
|
3299
|
-
*/
|
|
3300
|
-
getWhereConditions() {
|
|
3301
|
-
return JSON.parse(JSON.stringify(this.query.where));
|
|
3302
|
-
}
|
|
3303
|
-
|
|
3304
|
-
/**
|
|
3305
|
-
* Get ORDER BY clauses
|
|
3306
|
-
* @returns {Array} ORDER BY clauses
|
|
3307
|
-
*/
|
|
3308
|
-
getOrderBy() {
|
|
3309
|
-
return JSON.parse(JSON.stringify(this.query.orderBy));
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
/**
|
|
3313
|
-
* Get LIMIT value
|
|
3314
|
-
* @returns {number|null} LIMIT value
|
|
3315
|
-
*/
|
|
3316
|
-
getLimit() {
|
|
3317
|
-
return this.query.limit;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
/**
|
|
3321
|
-
* Get OFFSET value
|
|
3322
|
-
* @returns {number|null} OFFSET value
|
|
3323
|
-
*/
|
|
3324
|
-
getOffset() {
|
|
3325
|
-
return this.query.offset;
|
|
3326
|
-
}
|
|
3327
|
-
|
|
3328
|
-
/**
|
|
3329
|
-
* Get JOIN clauses
|
|
3330
|
-
* @returns {Array} JOIN clauses
|
|
3331
|
-
*/
|
|
3332
|
-
getJoins() {
|
|
3333
|
-
return JSON.parse(JSON.stringify(this.query.joins));
|
|
3334
|
-
}
|
|
3335
|
-
|
|
3336
|
-
/**
|
|
3337
|
-
* Get GROUP BY clauses
|
|
3338
|
-
* @returns {Array} GROUP BY clauses
|
|
3339
|
-
*/
|
|
3340
|
-
getGroupBy() {
|
|
3341
|
-
return JSON.parse(JSON.stringify(this.query.groupBy));
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
|
-
/**
|
|
3345
|
-
* Get HAVING conditions
|
|
3346
|
-
* @returns {Array} HAVING conditions
|
|
3347
|
-
*/
|
|
3348
|
-
getHaving() {
|
|
3349
|
-
return JSON.parse(JSON.stringify(this.query.having));
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
/**
|
|
3353
|
-
* Check if query is SELECT
|
|
3354
|
-
* @returns {boolean} True if query is SELECT
|
|
3355
|
-
*/
|
|
3356
|
-
isSelect() {
|
|
3357
|
-
return this.query.type === "select";
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
/**
|
|
3361
|
-
* Check if query is INSERT
|
|
3362
|
-
* @returns {boolean} True if query is INSERT
|
|
3363
|
-
*/
|
|
3364
|
-
isInsert() {
|
|
3365
|
-
return this.query.type === "insert";
|
|
3366
|
-
}
|
|
3367
|
-
|
|
3368
|
-
/**
|
|
3369
|
-
* Check if query is UPDATE
|
|
3370
|
-
* @returns {boolean} True if query is UPDATE
|
|
3371
|
-
*/
|
|
3372
|
-
isUpdate() {
|
|
3373
|
-
return this.query.type === "update";
|
|
3374
|
-
}
|
|
3375
|
-
|
|
3376
|
-
/**
|
|
3377
|
-
* Check if query is DELETE
|
|
3378
|
-
* @returns {boolean} True if query is DELETE
|
|
3379
|
-
*/
|
|
3380
|
-
isDelete() {
|
|
3381
|
-
return this.query.type === "delete";
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
/**
|
|
3385
|
-
* Check if query is DISTINCT
|
|
3386
|
-
* @returns {boolean} True if query is DISTINCT
|
|
3387
|
-
*/
|
|
3388
|
-
isDistinct() {
|
|
3389
|
-
return this.query.distinct === true;
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
/**
|
|
3393
|
-
* Check if query has LOCK
|
|
3394
|
-
* @returns {boolean} True if query has LOCK
|
|
3395
|
-
*/
|
|
3396
|
-
hasLock() {
|
|
3397
|
-
return this.query.lock !== null;
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
/**
|
|
3401
|
-
* Check if query has RETURNING clause
|
|
3402
|
-
* @returns {boolean} True if query has RETURNING clause
|
|
3403
|
-
*/
|
|
3404
|
-
hasReturning() {
|
|
3405
|
-
return this.query.returning !== null && this.query.returning.length > 0;
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
/**
|
|
3409
|
-
* Check if query has UNION
|
|
3410
|
-
* @returns {boolean} True if query has UNION
|
|
3411
|
-
*/
|
|
3412
|
-
hasUnion() {
|
|
3413
|
-
return this.query.union.length > 0;
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
/**
|
|
3417
|
-
* Check if query has WITH clause
|
|
3418
|
-
* @returns {boolean} True if query has WITH clause
|
|
3419
|
-
*/
|
|
3420
|
-
hasWith() {
|
|
3421
|
-
return this.query.with.length > 0;
|
|
3422
|
-
}
|
|
3423
|
-
|
|
3424
|
-
/**
|
|
3425
|
-
* Check if query has CTE
|
|
3426
|
-
* @returns {boolean} True if query has CTE
|
|
3427
|
-
*/
|
|
3428
|
-
hasCte() {
|
|
3429
|
-
return this.query.cte.length > 0;
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
/**
|
|
3433
|
-
* Get query statistics
|
|
3434
|
-
* @returns {Object} Query statistics
|
|
3435
|
-
*/
|
|
3436
|
-
getStats() {
|
|
3437
|
-
return {
|
|
3438
|
-
type: this.query.type,
|
|
3439
|
-
table: this.tableName,
|
|
3440
|
-
dialect: this.dialect,
|
|
3441
|
-
columns: this.query.columns.length,
|
|
3442
|
-
whereConditions: this.query.where.length,
|
|
3443
|
-
joins: this.query.joins.length,
|
|
3444
|
-
groupBy: this.query.groupBy.length,
|
|
3445
|
-
havingConditions: this.query.having.length,
|
|
3446
|
-
orderBy: this.query.orderBy.length,
|
|
3447
|
-
limit: this.query.limit,
|
|
3448
|
-
offset: this.query.offset,
|
|
3449
|
-
distinct: this.query.distinct,
|
|
3450
|
-
bindingsCount: this.bindings.length,
|
|
3451
|
-
hasLock: this.query.lock !== null,
|
|
3452
|
-
hasReturning:
|
|
3453
|
-
this.query.returning !== null && this.query.returning.length > 0,
|
|
3454
|
-
unionCount: this.query.union.length,
|
|
3455
|
-
withCount: this.query.with.length,
|
|
3456
|
-
cteCount: this.query.cte.length,
|
|
3457
|
-
};
|
|
3458
|
-
}
|
|
3459
|
-
|
|
3460
|
-
/**
|
|
3461
|
-
* Get query summary
|
|
3462
|
-
* @returns {Object} Query summary
|
|
3463
|
-
*/
|
|
3464
|
-
getSummary() {
|
|
3465
|
-
const { sql, bindings } = this.toSQL();
|
|
3466
|
-
return {
|
|
3467
|
-
sql,
|
|
3468
|
-
bindingsCount: bindings.length,
|
|
3469
|
-
type: this.query.type,
|
|
3470
|
-
table: this.tableName,
|
|
3471
|
-
dialect: this.dialect,
|
|
3472
|
-
complexity: this.calculateComplexity(),
|
|
3473
|
-
estimatedRows: this.estimateRows(),
|
|
3474
|
-
hasSubqueries: this.hasSubqueries(),
|
|
3475
|
-
};
|
|
3476
|
-
}
|
|
3477
|
-
|
|
3478
|
-
/**
|
|
3479
|
-
* Calculate query complexity score
|
|
3480
|
-
* @returns {number} Complexity score
|
|
3481
|
-
*/
|
|
3482
|
-
calculateComplexity() {
|
|
3483
|
-
let score = 0;
|
|
3484
|
-
|
|
3485
|
-
// Base complexity
|
|
3486
|
-
score += 1;
|
|
3487
|
-
|
|
3488
|
-
// WHERE conditions
|
|
3489
|
-
score += this.query.where.length * 0.5;
|
|
3490
|
-
|
|
3491
|
-
// JOINs
|
|
3492
|
-
score += this.query.joins.length * 1;
|
|
3493
|
-
|
|
3494
|
-
// GROUP BY
|
|
3495
|
-
score += this.query.groupBy.length * 0.5;
|
|
3496
|
-
|
|
3497
|
-
// HAVING conditions
|
|
3498
|
-
score += this.query.having.length * 0.5;
|
|
3499
|
-
|
|
3500
|
-
// ORDER BY
|
|
3501
|
-
score += this.query.orderBy.length * 0.3;
|
|
3502
|
-
|
|
3503
|
-
// Subqueries
|
|
3504
|
-
if (this.hasSubqueries()) {
|
|
3505
|
-
score += 2;
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
// UNION
|
|
3509
|
-
if (this.hasUnion()) {
|
|
3510
|
-
score += 1;
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
// WITH/CTE
|
|
3514
|
-
if (this.hasWith() || this.hasCte()) {
|
|
3515
|
-
score += 1.5;
|
|
3516
|
-
}
|
|
3517
|
-
|
|
3518
|
-
return Math.round(score * 10) / 10;
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
|
-
/**
|
|
3522
|
-
* Estimate number of rows affected/returned
|
|
3523
|
-
* @returns {number|null} Estimated row count
|
|
3524
|
-
*/
|
|
3525
|
-
estimateRows() {
|
|
3526
|
-
if (this.query.type === "select") {
|
|
3527
|
-
if (this.query.limit !== null) {
|
|
3528
|
-
return Math.min(this.query.limit, 1000);
|
|
3529
|
-
}
|
|
3530
|
-
return 1000; // Default estimate for SELECT
|
|
3531
|
-
} else if (this.query.type === "insert") {
|
|
3532
|
-
return this.query.data
|
|
3533
|
-
? Array.isArray(this.query.data)
|
|
3534
|
-
? this.query.data.length
|
|
3535
|
-
: 1
|
|
3536
|
-
: 1;
|
|
3537
|
-
} else if (this.query.type === "update" || this.query.type === "delete") {
|
|
3538
|
-
return this.query.where.length > 0 ? 10 : null; // Warning: no WHERE clause
|
|
3539
|
-
}
|
|
3540
|
-
return null;
|
|
3541
|
-
}
|
|
3542
|
-
|
|
3543
|
-
/**
|
|
3544
|
-
* Check if query contains subqueries
|
|
3545
|
-
* @returns {boolean} True if contains subqueries
|
|
3546
|
-
*/
|
|
3547
|
-
hasSubqueries() {
|
|
3548
|
-
// Check WHERE conditions for subqueries
|
|
3549
|
-
const hasSubqueryInWhere = this.query.where.some(
|
|
3550
|
-
(condition) =>
|
|
3551
|
-
condition.type === "exists" || condition.type === "subquery",
|
|
3552
|
-
);
|
|
3553
|
-
|
|
3554
|
-
// Check HAVING conditions for subqueries
|
|
3555
|
-
const hasSubqueryInHaving = this.query.having.some(
|
|
3556
|
-
(condition) =>
|
|
3557
|
-
condition.type === "exists" || condition.type === "subquery",
|
|
3558
|
-
);
|
|
3559
|
-
|
|
3560
|
-
return (
|
|
3561
|
-
hasSubqueryInWhere || hasSubqueryInHaving || this.query.union.length > 0
|
|
3562
|
-
);
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3565
|
-
/**
|
|
3566
|
-
* Validate query structure
|
|
3567
|
-
* @returns {Object} Validation result
|
|
3568
|
-
*/
|
|
3569
|
-
validate() {
|
|
3570
|
-
const errors = [];
|
|
3571
|
-
const warnings = [];
|
|
3572
|
-
|
|
3573
|
-
// Check for DELETE without WHERE clause
|
|
3574
|
-
if (this.query.type === "delete" && this.query.where.length === 0) {
|
|
3575
|
-
warnings.push("DELETE query without WHERE clause may affect all rows");
|
|
3576
|
-
}
|
|
3577
|
-
|
|
3578
|
-
// Check for UPDATE without WHERE clause
|
|
3579
|
-
if (this.query.type === "update" && this.query.where.length === 0) {
|
|
3580
|
-
warnings.push("UPDATE query without WHERE clause may affect all rows");
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
// Check for SELECT * without LIMIT on large tables
|
|
3584
|
-
if (
|
|
3585
|
-
this.query.type === "select" &&
|
|
3586
|
-
this.query.columns.includes("*") &&
|
|
3587
|
-
this.query.limit === null
|
|
3588
|
-
) {
|
|
3589
|
-
warnings.push("SELECT * without LIMIT may return large result set");
|
|
3590
|
-
}
|
|
3591
|
-
|
|
3592
|
-
// Check for missing JOIN conditions
|
|
3593
|
-
const invalidJoins = this.query.joins.filter(
|
|
3594
|
-
(join) =>
|
|
3595
|
-
join.type !== "cross" &&
|
|
3596
|
-
(!join.first || !join.operator || !join.second),
|
|
3597
|
-
);
|
|
3598
|
-
if (invalidJoins.length > 0) {
|
|
3599
|
-
errors.push("Invalid JOIN conditions detected");
|
|
3600
|
-
}
|
|
3601
|
-
|
|
3602
|
-
// Check for GROUP BY without aggregate functions
|
|
3603
|
-
if (this.query.groupBy.length > 0) {
|
|
3604
|
-
const hasAggregate = this.query.columns.some((col) => {
|
|
3605
|
-
const colStr = typeof col === "string" ? col : "";
|
|
3606
|
-
return (
|
|
3607
|
-
colStr.includes("COUNT(") ||
|
|
3608
|
-
colStr.includes("SUM(") ||
|
|
3609
|
-
colStr.includes("AVG(") ||
|
|
3610
|
-
colStr.includes("MIN(") ||
|
|
3611
|
-
colStr.includes("MAX(")
|
|
3612
|
-
);
|
|
3613
|
-
});
|
|
3614
|
-
if (!hasAggregate) {
|
|
3615
|
-
warnings.push("GROUP BY used without aggregate functions");
|
|
3616
|
-
}
|
|
3617
|
-
}
|
|
3618
|
-
|
|
3619
|
-
// Check dialect-specific limitations
|
|
3620
|
-
if (this.dialect === "sqlite" || this.dialect === "sqlite3") {
|
|
3621
|
-
if (this.query.returning) {
|
|
3622
|
-
errors.push("RETURNING clause not supported in SQLite");
|
|
3623
|
-
}
|
|
3624
|
-
}
|
|
3625
|
-
|
|
3626
|
-
return {
|
|
3627
|
-
valid: errors.length === 0,
|
|
3628
|
-
errors,
|
|
3629
|
-
warnings,
|
|
3630
|
-
complexity: this.calculateComplexity(),
|
|
3631
|
-
estimatedRows: this.estimateRows(),
|
|
3632
|
-
};
|
|
3633
|
-
}
|
|
3634
|
-
|
|
3635
|
-
/**
|
|
3636
|
-
* Explain query execution plan
|
|
3637
|
-
* @returns {Promise<Object>} Explain plan
|
|
3638
|
-
*/
|
|
3639
|
-
async explain() {
|
|
3640
|
-
const { sql, bindings } = this.toSQL();
|
|
3641
|
-
const explainSql = `EXPLAIN ${sql}`;
|
|
3642
|
-
|
|
3643
|
-
try {
|
|
3644
|
-
const result = await this.executeQuery(explainSql, bindings);
|
|
3645
|
-
return {
|
|
3646
|
-
sql: explainSql,
|
|
3647
|
-
plan: result,
|
|
3648
|
-
dialect: this.dialect,
|
|
3649
|
-
};
|
|
3650
|
-
} catch (error) {
|
|
3651
|
-
return {
|
|
3652
|
-
sql: explainSql,
|
|
3653
|
-
error: error.message,
|
|
3654
|
-
dialect: this.dialect,
|
|
3655
|
-
};
|
|
3656
|
-
}
|
|
3657
|
-
}
|
|
3658
|
-
|
|
3659
|
-
/**
|
|
3660
|
-
* Analyze query performance
|
|
3661
|
-
* @returns {Promise<Object>} Analysis results
|
|
3662
|
-
*/
|
|
3663
|
-
async analyze() {
|
|
3664
|
-
if (
|
|
3665
|
-
this.dialect === "postgresql" ||
|
|
3666
|
-
this.dialect === "postgres" ||
|
|
3667
|
-
this.dialect === "pg"
|
|
3668
|
-
) {
|
|
3669
|
-
const { sql, bindings } = this.toSQL();
|
|
3670
|
-
const analyzeSql = `EXPLAIN ANALYZE ${sql}`;
|
|
3671
|
-
|
|
3672
|
-
try {
|
|
3673
|
-
const result = await this.executeQuery(analyzeSql, bindings);
|
|
3674
|
-
return {
|
|
3675
|
-
sql: analyzeSql,
|
|
3676
|
-
analysis: result,
|
|
3677
|
-
dialect: this.dialect,
|
|
3678
|
-
};
|
|
3679
|
-
} catch (error) {
|
|
3680
|
-
return {
|
|
3681
|
-
sql: analyzeSql,
|
|
3682
|
-
error: error.message,
|
|
3683
|
-
dialect: this.dialect,
|
|
3684
|
-
};
|
|
3685
|
-
}
|
|
3686
|
-
} else {
|
|
3687
|
-
return {
|
|
3688
|
-
error: "ANALYZE not supported for this dialect",
|
|
3689
|
-
dialect: this.dialect,
|
|
3690
|
-
};
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
/**
|
|
3695
|
-
* Get query performance metrics
|
|
3696
|
-
* @returns {Object} Performance metrics
|
|
3697
|
-
*/
|
|
3698
|
-
getPerformanceMetrics() {
|
|
3699
|
-
const stats = this.getStats();
|
|
3700
|
-
const complexity = this.calculateComplexity();
|
|
3701
|
-
const estimatedRows = this.estimateRows();
|
|
3702
|
-
|
|
3703
|
-
return {
|
|
3704
|
-
complexityScore: complexity,
|
|
3705
|
-
estimatedRows,
|
|
3706
|
-
conditionCount: stats.whereConditions,
|
|
3707
|
-
joinCount: stats.joins,
|
|
3708
|
-
groupByCount: stats.groupBy,
|
|
3709
|
-
orderByCount: stats.orderBy,
|
|
3710
|
-
hasSubqueries: this.hasSubqueries(),
|
|
3711
|
-
hasUnion: stats.unionCount > 0,
|
|
3712
|
-
hasCte: stats.cteCount > 0,
|
|
3713
|
-
isComplex: complexity > 5,
|
|
3714
|
-
needsOptimization: complexity > 8 || stats.whereConditions > 10,
|
|
3715
|
-
};
|
|
3716
|
-
}
|
|
3717
|
-
|
|
3718
|
-
/**
|
|
3719
|
-
* Get query optimization suggestions
|
|
3720
|
-
* @returns {Array} Optimization suggestions
|
|
3721
|
-
*/
|
|
3722
|
-
getOptimizationSuggestions() {
|
|
3723
|
-
const suggestions = [];
|
|
3724
|
-
const metrics = this.getPerformanceMetrics();
|
|
3725
|
-
|
|
3726
|
-
if (metrics.complexityScore > 8) {
|
|
3727
|
-
suggestions.push(
|
|
3728
|
-
"Query is complex. Consider breaking it into smaller queries.",
|
|
3729
|
-
);
|
|
3730
|
-
}
|
|
3731
|
-
|
|
3732
|
-
if (metrics.whereConditions > 10) {
|
|
3733
|
-
suggestions.push(
|
|
3734
|
-
"Too many WHERE conditions. Consider using indexes or restructuring the query.",
|
|
3735
|
-
);
|
|
3736
|
-
}
|
|
3737
|
-
|
|
3738
|
-
if (this.query.type === "select" && this.query.columns.includes("*")) {
|
|
3739
|
-
suggestions.push(
|
|
3740
|
-
"Using SELECT * may impact performance. Specify only needed columns.",
|
|
3741
|
-
);
|
|
3742
|
-
}
|
|
3743
|
-
|
|
3744
|
-
if (
|
|
3745
|
-
this.query.type === "select" &&
|
|
3746
|
-
this.query.limit === null &&
|
|
3747
|
-
metrics.estimatedRows > 1000
|
|
3748
|
-
) {
|
|
3749
|
-
suggestions.push(
|
|
3750
|
-
"Consider adding LIMIT clause to prevent large result sets.",
|
|
3751
|
-
);
|
|
3752
|
-
}
|
|
3753
|
-
|
|
3754
|
-
if (this.query.joins.length > 3) {
|
|
3755
|
-
suggestions.push(
|
|
3756
|
-
"Multiple JOINs detected. Ensure proper indexes exist on join columns.",
|
|
3757
|
-
);
|
|
3758
|
-
}
|
|
3759
|
-
|
|
3760
|
-
if (this.query.groupBy.length > 0 && !this.hasAggregateFunctions()) {
|
|
3761
|
-
suggestions.push(
|
|
3762
|
-
"GROUP BY without aggregate functions may not be necessary.",
|
|
3763
|
-
);
|
|
3764
|
-
}
|
|
3765
|
-
|
|
3766
|
-
if (this.query.orderBy.length > 2) {
|
|
3767
|
-
suggestions.push(
|
|
3768
|
-
"Multiple ORDER BY clauses may impact performance. Consider if all are necessary.",
|
|
3769
|
-
);
|
|
3770
|
-
}
|
|
3771
|
-
|
|
3772
|
-
return suggestions;
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
/**
|
|
3776
|
-
* Check if query has aggregate functions
|
|
3777
|
-
* @returns {boolean} True if has aggregate functions
|
|
3778
|
-
*/
|
|
3779
|
-
hasAggregateFunctions() {
|
|
3780
|
-
return this.query.columns.some((col) => {
|
|
3781
|
-
const colStr = typeof col === "string" ? col : "";
|
|
3782
|
-
return (
|
|
3783
|
-
colStr.includes("COUNT(") ||
|
|
3784
|
-
colStr.includes("SUM(") ||
|
|
3785
|
-
colStr.includes("AVG(") ||
|
|
3786
|
-
colStr.includes("MIN(") ||
|
|
3787
|
-
colStr.includes("MAX(")
|
|
3788
|
-
);
|
|
3789
|
-
});
|
|
3790
|
-
}
|
|
3791
|
-
|
|
3792
|
-
/**
|
|
3793
|
-
* Get query execution plan
|
|
3794
|
-
* @returns {Promise<Object>} Execution plan
|
|
3795
|
-
*/
|
|
3796
|
-
async getExecutionPlan() {
|
|
3797
|
-
const explain = await this.explain();
|
|
3798
|
-
const analysis = await this.analyze();
|
|
3799
|
-
const metrics = this.getPerformanceMetrics();
|
|
3800
|
-
const suggestions = this.getOptimizationSuggestions();
|
|
3801
|
-
|
|
3802
|
-
return {
|
|
3803
|
-
query: this.getSummary(),
|
|
3804
|
-
explain,
|
|
3805
|
-
analysis,
|
|
3806
|
-
metrics,
|
|
3807
|
-
suggestions,
|
|
3808
|
-
validation: this.validate(),
|
|
3809
|
-
timestamp: new Date().toISOString(),
|
|
3810
|
-
};
|
|
3811
|
-
}
|
|
3812
|
-
|
|
3813
|
-
/**
|
|
3814
|
-
* Reset query to initial state
|
|
3815
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3816
|
-
*/
|
|
3817
|
-
reset() {
|
|
3818
|
-
this.query = {
|
|
3819
|
-
type: "select",
|
|
3820
|
-
columns: ["*"],
|
|
3821
|
-
where: [],
|
|
3822
|
-
orderBy: [],
|
|
3823
|
-
limit: null,
|
|
3824
|
-
offset: null,
|
|
3825
|
-
joins: [],
|
|
3826
|
-
groupBy: [],
|
|
3827
|
-
having: [],
|
|
3828
|
-
distinct: false,
|
|
3829
|
-
lock: null,
|
|
3830
|
-
data: null,
|
|
3831
|
-
returning: null,
|
|
3832
|
-
union: [],
|
|
3833
|
-
with: [],
|
|
3834
|
-
cte: [],
|
|
3835
|
-
};
|
|
3836
|
-
this.bindings = [];
|
|
3837
|
-
this.paramIndex = 1;
|
|
3838
|
-
this.subQueries.clear();
|
|
3839
|
-
|
|
3840
|
-
return this;
|
|
3841
|
-
}
|
|
3842
|
-
|
|
3843
|
-
/**
|
|
3844
|
-
* Create new query builder for same table
|
|
3845
|
-
* @returns {QueryBuilder} New query builder instance
|
|
3846
|
-
*/
|
|
3847
|
-
newQuery() {
|
|
3848
|
-
return new QueryBuilder(this.tableName, this.connection, this.dialect);
|
|
3849
|
-
}
|
|
3850
|
-
|
|
3851
|
-
/**
|
|
3852
|
-
* Create new query builder for different table
|
|
3853
|
-
* @param {string} tableName - Table name
|
|
3854
|
-
* @returns {QueryBuilder} New query builder instance
|
|
3855
|
-
*/
|
|
3856
|
-
table(tableName) {
|
|
3857
|
-
return new QueryBuilder(tableName, this.connection, this.dialect);
|
|
3858
|
-
}
|
|
3859
|
-
|
|
3860
|
-
/**
|
|
3861
|
-
* Log query for debugging
|
|
3862
|
-
* @param {Function} logger - Logger function (default: console.log)
|
|
3863
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3864
|
-
*/
|
|
3865
|
-
log(logger = console.log) {
|
|
3866
|
-
const { sql, bindings } = this.toSQL();
|
|
3867
|
-
logger(`SQL: ${sql}`);
|
|
3868
|
-
logger(`Bindings: ${JSON.stringify(bindings)}`);
|
|
3869
|
-
logger(`Dialect: ${this.dialect}`);
|
|
3870
|
-
logger(`Type: ${this.query.type}`);
|
|
3871
|
-
return this;
|
|
3872
|
-
}
|
|
3873
|
-
|
|
3874
|
-
/**
|
|
3875
|
-
* Debug query by logging and returning self
|
|
3876
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3877
|
-
*/
|
|
3878
|
-
debug() {
|
|
3879
|
-
return this.log();
|
|
3880
|
-
}
|
|
3881
|
-
|
|
3882
|
-
/**
|
|
3883
|
-
* Get query as JSON for serialization
|
|
3884
|
-
* @returns {Object} JSON representation of query
|
|
3885
|
-
*/
|
|
3886
|
-
toJSON() {
|
|
3887
|
-
return {
|
|
3888
|
-
table: this.tableName,
|
|
3889
|
-
dialect: this.dialect,
|
|
3890
|
-
query: this.query,
|
|
3891
|
-
bindings: this.bindings,
|
|
3892
|
-
paramIndex: this.paramIndex,
|
|
3893
|
-
subQueries: Array.from(this.subQueries.entries()),
|
|
3894
|
-
};
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
/**
|
|
3898
|
-
* Create QueryBuilder from JSON
|
|
3899
|
-
* @param {Object} json - JSON representation
|
|
3900
|
-
* @param {Object} connection - Database connection
|
|
3901
|
-
* @returns {QueryBuilder} Query builder instance
|
|
3902
|
-
*/
|
|
3903
|
-
static fromJSON(json, connection) {
|
|
3904
|
-
const qb = new QueryBuilder(json.table, connection, json.dialect);
|
|
3905
|
-
qb.query = json.query;
|
|
3906
|
-
qb.bindings = json.bindings;
|
|
3907
|
-
qb.paramIndex = json.paramIndex;
|
|
3908
|
-
qb.subQueries = new Map(json.subQueries);
|
|
3909
|
-
return qb;
|
|
3910
|
-
}
|
|
3911
|
-
|
|
3912
|
-
/**
|
|
3913
|
-
* Execute query with error handling and retry logic
|
|
3914
|
-
* @param {Object} options - Options for execution
|
|
3915
|
-
* @param {number} options.maxRetries - Maximum retry attempts
|
|
3916
|
-
* @param {number} options.retryDelay - Delay between retries in ms
|
|
3917
|
-
* @param {Function} options.onRetry - Callback on retry
|
|
3918
|
-
* @returns {Promise<Object>} Query result
|
|
3919
|
-
*/
|
|
3920
|
-
async executeWithRetry(options = {}) {
|
|
3921
|
-
const maxRetries = options.maxRetries || 3;
|
|
3922
|
-
const retryDelay = options.retryDelay || 1000;
|
|
3923
|
-
const onRetry = options.onRetry || (() => {});
|
|
3924
|
-
|
|
3925
|
-
let lastError;
|
|
3926
|
-
|
|
3927
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
3928
|
-
try {
|
|
3929
|
-
return await this.execute();
|
|
3930
|
-
} catch (error) {
|
|
3931
|
-
lastError = error;
|
|
3932
|
-
|
|
3933
|
-
// Check if error is retryable
|
|
3934
|
-
const isRetryable = this.isRetryableError(error);
|
|
3935
|
-
if (!isRetryable || attempt === maxRetries) {
|
|
3936
|
-
throw error;
|
|
3937
|
-
}
|
|
3938
|
-
|
|
3939
|
-
onRetry(attempt, error, retryDelay);
|
|
3940
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
3941
|
-
|
|
3942
|
-
// Exponential backoff
|
|
3943
|
-
retryDelay *= 2;
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
|
-
throw lastError;
|
|
3948
|
-
}
|
|
3949
|
-
|
|
3950
|
-
/**
|
|
3951
|
-
* Check if error is retryable
|
|
3952
|
-
* @param {Error} error - Error to check
|
|
3953
|
-
* @returns {boolean} True if error is retryable
|
|
3954
|
-
*/
|
|
3955
|
-
isRetryableError(error) {
|
|
3956
|
-
const retryableMessages = [
|
|
3957
|
-
"deadlock",
|
|
3958
|
-
"timeout",
|
|
3959
|
-
"connection",
|
|
3960
|
-
"lock",
|
|
3961
|
-
"busy",
|
|
3962
|
-
"try again",
|
|
3963
|
-
"retry",
|
|
3964
|
-
];
|
|
3965
|
-
|
|
3966
|
-
const errorMessage = error.message.toLowerCase();
|
|
3967
|
-
return retryableMessages.some((msg) => errorMessage.includes(msg));
|
|
3968
|
-
}
|
|
3969
|
-
|
|
3970
|
-
/**
|
|
3971
|
-
* Execute query with timeout
|
|
3972
|
-
* @param {number} timeout - Timeout in milliseconds
|
|
3973
|
-
* @returns {Promise<Object>} Query result
|
|
3974
|
-
*/
|
|
3975
|
-
async executeWithTimeout(timeout = 5000) {
|
|
3976
|
-
return Promise.race([
|
|
3977
|
-
this.execute(),
|
|
3978
|
-
new Promise((_, reject) => {
|
|
3979
|
-
setTimeout(
|
|
3980
|
-
() => reject(new Error(`Query timeout after ${timeout}ms`)),
|
|
3981
|
-
timeout,
|
|
3982
|
-
);
|
|
3983
|
-
}),
|
|
3984
|
-
]);
|
|
3985
|
-
}
|
|
3986
|
-
|
|
3987
|
-
/**
|
|
3988
|
-
* Execute query in transaction
|
|
3989
|
-
* @param {Function} callback - Transaction callback
|
|
3990
|
-
* @returns {Promise<Object>} Query result
|
|
3991
|
-
*/
|
|
3992
|
-
async executeInTransaction(callback) {
|
|
3993
|
-
if (!this.connection.beginTransaction) {
|
|
3994
|
-
throw new Error("Database connection does not support transactions");
|
|
3995
|
-
}
|
|
3996
|
-
|
|
3997
|
-
await this.connection.beginTransaction();
|
|
3998
|
-
|
|
3999
|
-
try {
|
|
4000
|
-
const result = await this.execute();
|
|
4001
|
-
if (callback) {
|
|
4002
|
-
await callback(result);
|
|
4003
|
-
}
|
|
4004
|
-
await this.connection.commit();
|
|
4005
|
-
return result;
|
|
4006
|
-
} catch (error) {
|
|
4007
|
-
await this.connection.rollback();
|
|
4008
|
-
throw error;
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
4011
|
-
|
|
4012
|
-
/**
|
|
4013
|
-
* Execute query with profiling
|
|
4014
|
-
* @returns {Promise<Object>} Query result with profiling info
|
|
4015
|
-
*/
|
|
4016
|
-
async executeWithProfiling() {
|
|
4017
|
-
const startTime = Date.now();
|
|
4018
|
-
const startMemory = process.memoryUsage();
|
|
4019
|
-
|
|
4020
|
-
const { sql, bindings } = this.toSQL();
|
|
4021
|
-
|
|
4022
|
-
try {
|
|
4023
|
-
const result = await this.execute();
|
|
4024
|
-
const endTime = Date.now();
|
|
4025
|
-
const endMemory = process.memoryUsage();
|
|
4026
|
-
|
|
4027
|
-
return {
|
|
4028
|
-
result,
|
|
4029
|
-
profile: {
|
|
4030
|
-
sql,
|
|
4031
|
-
bindings,
|
|
4032
|
-
timing: {
|
|
4033
|
-
startTime,
|
|
4034
|
-
endTime,
|
|
4035
|
-
duration: endTime - startTime,
|
|
4036
|
-
},
|
|
4037
|
-
memory: {
|
|
4038
|
-
start: startMemory,
|
|
4039
|
-
end: endMemory,
|
|
4040
|
-
diff: {
|
|
4041
|
-
rss: endMemory.rss - startMemory.rss,
|
|
4042
|
-
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
|
|
4043
|
-
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
|
|
4044
|
-
external: endMemory.external - startMemory.external,
|
|
4045
|
-
},
|
|
4046
|
-
},
|
|
4047
|
-
},
|
|
4048
|
-
};
|
|
4049
|
-
} catch (error) {
|
|
4050
|
-
const endTime = Date.now();
|
|
4051
|
-
throw {
|
|
4052
|
-
error,
|
|
4053
|
-
profile: {
|
|
4054
|
-
sql,
|
|
4055
|
-
bindings,
|
|
4056
|
-
timing: {
|
|
4057
|
-
startTime,
|
|
4058
|
-
endTime,
|
|
4059
|
-
duration: endTime - startTime,
|
|
4060
|
-
},
|
|
4061
|
-
},
|
|
4062
|
-
};
|
|
4063
|
-
}
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
/**
|
|
4067
|
-
* Dialect adapter for handling database-specific SQL syntax
|
|
4068
|
-
*/
|
|
4069
|
-
class DialectAdapter {
|
|
4070
|
-
constructor(dialect) {
|
|
4071
|
-
this.dialect = dialect.toLowerCase();
|
|
4072
|
-
}
|
|
4073
|
-
|
|
4074
|
-
/**
|
|
4075
|
-
* Quote identifier based on dialect
|
|
4076
|
-
* @param {string} identifier - Identifier to quote
|
|
4077
|
-
* @returns {string} Quoted identifier
|
|
4078
|
-
*/
|
|
4079
|
-
quoteIdentifier(identifier) {
|
|
4080
|
-
// Handle raw expressions or already quoted identifiers
|
|
4081
|
-
if (
|
|
4082
|
-
identifier.includes("(") ||
|
|
4083
|
-
identifier.includes(")") ||
|
|
4084
|
-
identifier.includes(" as ") ||
|
|
4085
|
-
identifier.includes("`") ||
|
|
4086
|
-
identifier.includes('"') ||
|
|
4087
|
-
identifier.includes("[") ||
|
|
4088
|
-
identifier.includes("]")
|
|
4089
|
-
) {
|
|
4090
|
-
return identifier;
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
// Handle qualified identifiers
|
|
4094
|
-
if (identifier.includes(".")) {
|
|
4095
|
-
return identifier
|
|
4096
|
-
.split(".")
|
|
4097
|
-
.map((part) => this._quoteIdentifierPart(part))
|
|
4098
|
-
.join(".");
|
|
4099
|
-
}
|
|
4100
|
-
|
|
4101
|
-
return this._quoteIdentifierPart(identifier);
|
|
4102
|
-
}
|
|
4103
|
-
|
|
4104
|
-
/**
|
|
4105
|
-
* Internal method to quote a single identifier part
|
|
4106
|
-
* @param {string} identifier - Identifier part to quote
|
|
4107
|
-
* @returns {string} Quoted identifier part
|
|
4108
|
-
* @private
|
|
4109
|
-
*/
|
|
4110
|
-
_quoteIdentifierPart(identifier) {
|
|
4111
|
-
identifier = identifier.trim();
|
|
4112
|
-
|
|
4113
|
-
// Return if already quoted
|
|
4114
|
-
if (
|
|
4115
|
-
(identifier.startsWith("`") && identifier.endsWith("`")) ||
|
|
4116
|
-
(identifier.startsWith('"') && identifier.endsWith('"')) ||
|
|
4117
|
-
(identifier.startsWith("[") && identifier.endsWith("]"))
|
|
4118
|
-
) {
|
|
4119
|
-
return identifier;
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
switch (this.dialect) {
|
|
4123
|
-
case "mysql":
|
|
4124
|
-
case "mariadb":
|
|
4125
|
-
case "clickhouse":
|
|
4126
|
-
return `\`${identifier}\``;
|
|
4127
|
-
|
|
4128
|
-
case "postgresql":
|
|
4129
|
-
case "postgres":
|
|
4130
|
-
case "pg":
|
|
4131
|
-
case "cockroachdb":
|
|
4132
|
-
case "cockroach":
|
|
4133
|
-
case "sqlite":
|
|
4134
|
-
case "sqlite3":
|
|
4135
|
-
return `"${identifier}"`;
|
|
4136
|
-
|
|
4137
|
-
case "mssql":
|
|
4138
|
-
case "sqlserver":
|
|
4139
|
-
return `[${identifier}]`;
|
|
4140
|
-
|
|
4141
|
-
case "oracle":
|
|
4142
|
-
return `"${identifier.toUpperCase()}"`;
|
|
4143
|
-
|
|
4144
|
-
default:
|
|
4145
|
-
console.warn(
|
|
4146
|
-
`Unsupported dialect: ${this.dialect}, returning unquoted identifier`,
|
|
4147
|
-
);
|
|
4148
|
-
return identifier;
|
|
4149
|
-
}
|
|
4150
|
-
}
|
|
4151
|
-
|
|
4152
|
-
/**
|
|
4153
|
-
* Get parameter placeholder for the current dialect
|
|
4154
|
-
* @param {number} index - Parameter index (1-based)
|
|
4155
|
-
* @returns {string} Parameter placeholder
|
|
4156
|
-
*/
|
|
4157
|
-
getParameterPlaceholder(index) {
|
|
4158
|
-
switch (this.dialect) {
|
|
4159
|
-
case "postgresql":
|
|
4160
|
-
case "postgres":
|
|
4161
|
-
case "pg":
|
|
4162
|
-
case "cockroachdb":
|
|
4163
|
-
case "cockroach":
|
|
4164
|
-
return `$${index}`;
|
|
4165
|
-
case "mssql":
|
|
4166
|
-
case "sqlserver":
|
|
4167
|
-
return `@p${index}`;
|
|
4168
|
-
case "oracle":
|
|
4169
|
-
return `:p${index}`;
|
|
4170
|
-
default:
|
|
4171
|
-
return "?";
|
|
4172
|
-
}
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
/**
|
|
4176
|
-
* Build LIMIT/OFFSET clause with dialect-specific syntax
|
|
4177
|
-
* @param {number|null} limit - Limit value
|
|
4178
|
-
* @param {number|null} offset - Offset value
|
|
4179
|
-
* @returns {string} LIMIT/OFFSET clause
|
|
4180
|
-
*/
|
|
4181
|
-
buildLimitOffset(limit, offset) {
|
|
4182
|
-
let clause = "";
|
|
4183
|
-
|
|
4184
|
-
if (this.dialect === "mssql" || this.dialect === "sqlserver") {
|
|
4185
|
-
// SQL Server uses OFFSET/FETCH syntax
|
|
4186
|
-
if (offset !== null) {
|
|
4187
|
-
clause += ` OFFSET ${offset} ROWS`;
|
|
4188
|
-
}
|
|
4189
|
-
if (limit !== null) {
|
|
4190
|
-
if (offset !== null) {
|
|
4191
|
-
clause += ` FETCH NEXT ${limit} ROWS ONLY`;
|
|
4192
|
-
} else {
|
|
4193
|
-
clause += ` FETCH FIRST ${limit} ROWS ONLY`;
|
|
4194
|
-
}
|
|
4195
|
-
}
|
|
4196
|
-
} else if (this.dialect === "oracle") {
|
|
4197
|
-
// Oracle uses FETCH FIRST syntax
|
|
4198
|
-
if (limit !== null) {
|
|
4199
|
-
const limitValue = limit;
|
|
4200
|
-
const offsetValue = offset || 0;
|
|
4201
|
-
if (offsetValue > 0) {
|
|
4202
|
-
clause = ` OFFSET ${offsetValue} ROWS FETCH NEXT ${limitValue} ROWS ONLY`;
|
|
4203
|
-
} else {
|
|
4204
|
-
clause = ` FETCH FIRST ${limitValue} ROWS ONLY`;
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
} else {
|
|
4208
|
-
// Standard SQL syntax for MySQL, PostgreSQL, SQLite, etc.
|
|
4209
|
-
if (limit !== null) {
|
|
4210
|
-
clause += ` LIMIT ${limit}`;
|
|
4211
|
-
}
|
|
4212
|
-
if (offset !== null) {
|
|
4213
|
-
clause += ` OFFSET ${offset}`;
|
|
4214
|
-
}
|
|
4215
|
-
}
|
|
4216
|
-
|
|
4217
|
-
return clause;
|
|
4218
|
-
}
|
|
4219
|
-
|
|
4220
|
-
/**
|
|
4221
|
-
* Get random function for the current dialect
|
|
4222
|
-
* @returns {string} Random function name
|
|
4223
|
-
*/
|
|
4224
|
-
getRandomFunction() {
|
|
4225
|
-
switch (this.dialect) {
|
|
4226
|
-
case "mysql":
|
|
4227
|
-
case "mariadb":
|
|
4228
|
-
return "RAND()";
|
|
4229
|
-
case "postgresql":
|
|
4230
|
-
case "postgres":
|
|
4231
|
-
case "pg":
|
|
4232
|
-
case "cockroachdb":
|
|
4233
|
-
case "cockroach":
|
|
4234
|
-
case "sqlite":
|
|
4235
|
-
case "sqlite3":
|
|
4236
|
-
return "RANDOM()";
|
|
4237
|
-
case "mssql":
|
|
4238
|
-
case "sqlserver":
|
|
4239
|
-
return "NEWID()";
|
|
4240
|
-
case "oracle":
|
|
4241
|
-
return "DBMS_RANDOM.VALUE";
|
|
4242
|
-
case "clickhouse":
|
|
4243
|
-
return "rand()";
|
|
4244
|
-
default:
|
|
4245
|
-
return "RAND()";
|
|
4246
|
-
}
|
|
4247
|
-
}
|
|
4248
|
-
|
|
4249
|
-
/**
|
|
4250
|
-
* Check if RETURNING clause is supported
|
|
4251
|
-
* @returns {boolean} True if RETURNING is supported
|
|
4252
|
-
*/
|
|
4253
|
-
supportsReturning() {
|
|
4254
|
-
return [
|
|
4255
|
-
"postgresql",
|
|
4256
|
-
"postgres",
|
|
4257
|
-
"pg",
|
|
4258
|
-
"cockroachdb",
|
|
4259
|
-
"cockroach",
|
|
4260
|
-
"oracle",
|
|
4261
|
-
].includes(this.dialect);
|
|
4262
|
-
}
|
|
4263
|
-
|
|
4264
|
-
/**
|
|
4265
|
-
* Check if WITH clause (CTE) is supported
|
|
4266
|
-
* @returns {boolean} True if WITH clause is supported
|
|
4267
|
-
*/
|
|
4268
|
-
supportsWithClause() {
|
|
4269
|
-
return [
|
|
4270
|
-
"postgresql",
|
|
4271
|
-
"postgres",
|
|
4272
|
-
"pg",
|
|
4273
|
-
"cockroachdb",
|
|
4274
|
-
"cockroach",
|
|
4275
|
-
"mssql",
|
|
4276
|
-
"sqlserver",
|
|
4277
|
-
"oracle",
|
|
4278
|
-
].includes(this.dialect);
|
|
4279
|
-
}
|
|
4280
|
-
|
|
4281
|
-
/**
|
|
4282
|
-
* Get current timestamp function
|
|
4283
|
-
* @returns {string} Current timestamp function
|
|
4284
|
-
*/
|
|
4285
|
-
getCurrentTimestamp() {
|
|
4286
|
-
switch (this.dialect) {
|
|
4287
|
-
case "mysql":
|
|
4288
|
-
case "mariadb":
|
|
4289
|
-
return "NOW()";
|
|
4290
|
-
case "postgresql":
|
|
4291
|
-
case "postgres":
|
|
4292
|
-
case "pg":
|
|
4293
|
-
case "cockroachdb":
|
|
4294
|
-
case "cockroach":
|
|
4295
|
-
return "CURRENT_TIMESTAMP";
|
|
4296
|
-
case "sqlite":
|
|
4297
|
-
case "sqlite3":
|
|
4298
|
-
return "CURRENT_TIMESTAMP";
|
|
4299
|
-
case "mssql":
|
|
4300
|
-
case "sqlserver":
|
|
4301
|
-
return "GETDATE()";
|
|
4302
|
-
case "oracle":
|
|
4303
|
-
return "SYSDATE";
|
|
4304
|
-
case "clickhouse":
|
|
4305
|
-
return "now()";
|
|
4306
|
-
default:
|
|
4307
|
-
return "NOW()";
|
|
4308
|
-
}
|
|
4309
|
-
}
|
|
4310
|
-
|
|
4311
|
-
/**
|
|
4312
|
-
* Get auto-increment keyword
|
|
4313
|
-
* @returns {string} Auto-increment keyword
|
|
4314
|
-
*/
|
|
4315
|
-
getAutoIncrementKeyword() {
|
|
4316
|
-
switch (this.dialect) {
|
|
4317
|
-
case "mysql":
|
|
4318
|
-
case "mariadb":
|
|
4319
|
-
return "AUTO_INCREMENT";
|
|
4320
|
-
case "postgresql":
|
|
4321
|
-
case "postgres":
|
|
4322
|
-
case "pg":
|
|
4323
|
-
case "cockroachdb":
|
|
4324
|
-
case "cockroach":
|
|
4325
|
-
return "SERIAL";
|
|
4326
|
-
case "sqlite":
|
|
4327
|
-
case "sqlite3":
|
|
4328
|
-
return "AUTOINCREMENT";
|
|
4329
|
-
case "mssql":
|
|
4330
|
-
case "sqlserver":
|
|
4331
|
-
return "IDENTITY(1,1)";
|
|
4332
|
-
case "oracle":
|
|
4333
|
-
return "GENERATED BY DEFAULT AS IDENTITY";
|
|
4334
|
-
default:
|
|
4335
|
-
return "AUTO_INCREMENT";
|
|
4336
|
-
}
|
|
4337
|
-
}
|
|
4338
|
-
|
|
4339
|
-
/**
|
|
4340
|
-
* Get boolean true value
|
|
4341
|
-
* @returns {string} Boolean true value
|
|
4342
|
-
*/
|
|
4343
|
-
getBooleanTrue() {
|
|
4344
|
-
switch (this.dialect) {
|
|
4345
|
-
case "mysql":
|
|
4346
|
-
case "mariadb":
|
|
4347
|
-
case "sqlite":
|
|
4348
|
-
case "sqlite3":
|
|
4349
|
-
return "1";
|
|
4350
|
-
case "postgresql":
|
|
4351
|
-
case "postgres":
|
|
4352
|
-
case "pg":
|
|
4353
|
-
case "cockroachdb":
|
|
4354
|
-
case "cockroach":
|
|
4355
|
-
return "TRUE";
|
|
4356
|
-
case "mssql":
|
|
4357
|
-
case "sqlserver":
|
|
4358
|
-
return "1";
|
|
4359
|
-
case "oracle":
|
|
4360
|
-
return "1";
|
|
4361
|
-
case "clickhouse":
|
|
4362
|
-
return "1";
|
|
4363
|
-
default:
|
|
4364
|
-
return "1";
|
|
4365
|
-
}
|
|
4366
|
-
}
|
|
4367
|
-
|
|
4368
|
-
/**
|
|
4369
|
-
* Get boolean false value
|
|
4370
|
-
* @returns {string} Boolean false value
|
|
4371
|
-
*/
|
|
4372
|
-
getBooleanFalse() {
|
|
4373
|
-
switch (this.dialect) {
|
|
4374
|
-
case "mysql":
|
|
4375
|
-
case "mariadb":
|
|
4376
|
-
case "sqlite":
|
|
4377
|
-
case "sqlite3":
|
|
4378
|
-
return "0";
|
|
4379
|
-
case "postgresql":
|
|
4380
|
-
case "postgres":
|
|
4381
|
-
case "pg":
|
|
4382
|
-
case "cockroachdb":
|
|
4383
|
-
case "cockroach":
|
|
4384
|
-
return "FALSE";
|
|
4385
|
-
case "mssql":
|
|
4386
|
-
case "sqlserver":
|
|
4387
|
-
return "0";
|
|
4388
|
-
case "oracle":
|
|
4389
|
-
return "0";
|
|
4390
|
-
case "clickhouse":
|
|
4391
|
-
return "0";
|
|
4392
|
-
default:
|
|
4393
|
-
return "0";
|
|
4394
|
-
}
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
|
|
4398
|
-
export default QueryBuilder;
|