@aetherframework/database 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/package.json +1 -2
  2. package/src/DatabaseManager.js +0 -565
  3. package/src/core/ConnectionManager.js +0 -351
  4. package/src/core/DatabaseFactory.js +0 -188
  5. package/src/core/MongoQueryBuilder.js +0 -576
  6. package/src/core/PluginManager.js +0 -968
  7. package/src/core/QueryBuilder.js +0 -4398
  8. package/src/core/TransactionManager.js +0 -40
  9. package/src/drivers/clickhouse-driver.js +0 -272
  10. package/src/drivers/index.js +0 -273
  11. package/src/drivers/mongodb-driver.js +0 -87
  12. package/src/drivers/mssql-driver.js +0 -117
  13. package/src/drivers/mysql-driver.js +0 -169
  14. package/src/drivers/oracle-driver.js +0 -101
  15. package/src/drivers/postgres-driver.js +0 -234
  16. package/src/drivers/redis-driver.js +0 -52
  17. package/src/drivers/sqlite-driver.js +0 -67
  18. package/src/middleware/connection-pool.js +0 -455
  19. package/src/middleware/performance-monitor.js +0 -652
  20. package/src/middleware/query-cache.js +0 -500
  21. package/src/middleware/query-logger.js +0 -262
  22. package/src/plugins/AuditPlugin.js +0 -447
  23. package/src/plugins/BasePlugin.js +0 -418
  24. package/src/plugins/BatchOperationPlugin.js +0 -165
  25. package/src/plugins/CachePlugin.js +0 -407
  26. package/src/plugins/CtePlugin.js +0 -523
  27. package/src/plugins/DistributedPlugin.js +0 -543
  28. package/src/plugins/EncryptionPlugin.js +0 -211
  29. package/src/plugins/FullTextSearchPlugin.js +0 -164
  30. package/src/plugins/GeospatialPlugin.js +0 -219
  31. package/src/plugins/GraphQLPlugin.js +0 -162
  32. package/src/plugins/HookPlugin.js +0 -211
  33. package/src/plugins/JsonPlugin.js +0 -366
  34. package/src/plugins/OptimisticLockPlugin.js +0 -374
  35. package/src/plugins/PerformancePlugin.js +0 -175
  36. package/src/plugins/ResiliencePlugin.js +0 -114
  37. package/src/plugins/ShardingPlugin.js +0 -227
  38. package/src/plugins/SoftDeletePlugin.js +0 -258
  39. package/src/plugins/SyncPlugin.js +0 -373
  40. package/src/plugins/VersioningPlugin.js +0 -314
  41. package/src/plugins/WindowFunctionPlugin.js +0 -343
  42. package/src/utils/config-loader.js +0 -632
  43. package/src/utils/error-handler.js +0 -724
  44. package/src/utils/migration-runner.js +0 -1066
@@ -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;