@aetherframework/database 1.1.0 → 1.1.2

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 (47) hide show
  1. package/examples/mysql-test-pressure.js +1530 -0
  2. package/examples/test-direct.js +116 -0
  3. package/examples/transaction_example.js +127 -0
  4. package/package.json +3 -1
  5. package/src/DatabaseManager.js +565 -0
  6. package/src/core/ConnectionManager.js +351 -0
  7. package/src/core/DatabaseFactory.js +188 -0
  8. package/src/core/MongoQueryBuilder.js +576 -0
  9. package/src/core/PluginManager.js +968 -0
  10. package/src/core/QueryBuilder.js +4394 -0
  11. package/src/core/TransactionManager.js +40 -0
  12. package/src/drivers/clickhouse-driver.js +272 -0
  13. package/src/drivers/index.js +273 -0
  14. package/src/drivers/mongodb-driver.js +87 -0
  15. package/src/drivers/mssql-driver.js +117 -0
  16. package/src/drivers/mysql-driver.js +169 -0
  17. package/src/drivers/oracle-driver.js +101 -0
  18. package/src/drivers/postgres-driver.js +234 -0
  19. package/src/drivers/redis-driver.js +52 -0
  20. package/src/drivers/sqlite-driver.js +67 -0
  21. package/src/middleware/connection-pool.js +455 -0
  22. package/src/middleware/performance-monitor.js +652 -0
  23. package/src/middleware/query-cache.js +500 -0
  24. package/src/middleware/query-logger.js +262 -0
  25. package/src/plugins/AuditPlugin.js +447 -0
  26. package/src/plugins/BasePlugin.js +418 -0
  27. package/src/plugins/BatchOperationPlugin.js +165 -0
  28. package/src/plugins/CachePlugin.js +407 -0
  29. package/src/plugins/CtePlugin.js +523 -0
  30. package/src/plugins/DistributedPlugin.js +543 -0
  31. package/src/plugins/EncryptionPlugin.js +211 -0
  32. package/src/plugins/FullTextSearchPlugin.js +164 -0
  33. package/src/plugins/GeospatialPlugin.js +219 -0
  34. package/src/plugins/GraphQLPlugin.js +162 -0
  35. package/src/plugins/HookPlugin.js +211 -0
  36. package/src/plugins/JsonPlugin.js +366 -0
  37. package/src/plugins/OptimisticLockPlugin.js +374 -0
  38. package/src/plugins/PerformancePlugin.js +175 -0
  39. package/src/plugins/ResiliencePlugin.js +114 -0
  40. package/src/plugins/ShardingPlugin.js +227 -0
  41. package/src/plugins/SoftDeletePlugin.js +258 -0
  42. package/src/plugins/SyncPlugin.js +373 -0
  43. package/src/plugins/VersioningPlugin.js +314 -0
  44. package/src/plugins/WindowFunctionPlugin.js +343 -0
  45. package/src/utils/config-loader.js +632 -0
  46. package/src/utils/error-handler.js +724 -0
  47. package/src/utils/migration-runner.js +1066 -0
@@ -0,0 +1,4394 @@
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
+
1036
+ // 自动优化建议
1037
+ if (this.query.type === "select" && this.query.columns.includes("*")) {
1038
+ console.warn(
1039
+ "Consider specifying columns instead of using SELECT *",
1040
+ );
1041
+ }
1042
+
1043
+ if (this.query.where.length > 10) {
1044
+ console.warn("Consider adding indexes for WHERE conditions");
1045
+ }
1046
+
1047
+ if (this.query.joins.length > 3) {
1048
+ console.warn("Multiple JOINs detected, ensure proper indexes exist");
1049
+ }
1050
+ }
1051
+
1052
+ return this;
1053
+ }
1054
+
1055
+ /**
1056
+ * Execute the DELETE query with performance monitoring.
1057
+ * Logs duration, memory usage, and affected rows.
1058
+ * Warns if the operation takes longer than 1 second.
1059
+ * @returns {Promise<Object>} The result of the execution including performance metrics.
1060
+ */
1061
+ async deleteWithMonitoring() {
1062
+ const startTime = Date.now();
1063
+ const startMemory = process.memoryUsage();
1064
+
1065
+ try {
1066
+ // Execute the actual query
1067
+ const result = await this.execute();
1068
+
1069
+ const endTime = Date.now();
1070
+ const endMemory = process.memoryUsage();
1071
+
1072
+ // Calculate performance metrics
1073
+ const performance = {
1074
+ duration: endTime - startTime,
1075
+ memoryDiff: {
1076
+ rss: endMemory.rss - startMemory.rss,
1077
+ heapUsed: endMemory.heapUsed - startMemory.heapUsed,
1078
+ },
1079
+ rowsAffected: result.affectedRows || 0,
1080
+ };
1081
+
1082
+ // Warn if the operation is slow
1083
+ if (performance.duration > 1000) {
1084
+ console.warn(
1085
+ "DELETE operation took over 1 second. Consider optimizing indexes or using batch deletion.",
1086
+ );
1087
+ }
1088
+
1089
+ return { result, performance };
1090
+ } catch (error) {
1091
+ console.error("DELETE Execution Failed:", error);
1092
+ throw error;
1093
+ }
1094
+ }
1095
+
1096
+ /**
1097
+ * Smart Delete: Automatically decides between single execution and batch deletion.
1098
+ * Analyzes the estimated row count and switches to batch mode if the threshold is exceeded.
1099
+ * @param {Object} options - Configuration options for batch deletion.
1100
+ * @param {number} options.batchSize - Number of rows to delete per batch (default: 1000).
1101
+ * @param {number} options.delay - Delay in ms between batches to reduce lock contention (default: 100).
1102
+ * @param {boolean} options.useTransaction - Whether to wrap each batch in a transaction (default: true).
1103
+ * @param {number} options.threshold - Row count threshold to trigger batch mode (default: 1000).
1104
+ * @returns {Promise<Object>} Result of the deletion operation.
1105
+ */
1106
+ async smartDelete(options = {}) {
1107
+ const {
1108
+ batchSize = 1000,
1109
+ delay = 100,
1110
+ useTransaction = true,
1111
+ threshold = 1000,
1112
+ } = options;
1113
+
1114
+ // Estimate the number of rows to be deleted
1115
+ const plan = await this.getExecutionPlan();
1116
+
1117
+ // If estimated rows exceed threshold, use batch deletion
1118
+ if (plan.metrics.estimatedRows > threshold) {
1119
+ return this.deleteInBatches(batchSize, delay, useTransaction);
1120
+ }
1121
+
1122
+ // Otherwise, execute as a single standard delete
1123
+ return this.execute();
1124
+ }
1125
+
1126
+ /**
1127
+ * Delete records in batches to prevent locking issues and high memory usage.
1128
+ * @param {number} batchSize - Number of rows to delete per batch.
1129
+ * @param {number} delay - Delay in milliseconds between batches.
1130
+ * @param {boolean} useTransaction - Whether to use transactions for each batch.
1131
+ * @returns {Promise<Object>} Summary of the deletion process.
1132
+ */
1133
+ async deleteInBatches(batchSize = 1000, delay = 100, useTransaction = true) {
1134
+ // Clone the current query to get the total count without modifying the original builder
1135
+ const countQuery = this.clone();
1136
+ countQuery.query.columns = ["COUNT(*) as total"];
1137
+ // Clear order by and limit for accurate counting
1138
+ countQuery.query.orderBy = [];
1139
+ countQuery.query.limit = null;
1140
+
1141
+ const countResult = await countQuery.first();
1142
+ const total = parseInt(countResult.total);
1143
+
1144
+ let deleted = 0;
1145
+ const primaryKey = this.getPrimaryKeyColumn(); // Assumes a method exists to get PK
1146
+
1147
+ while (deleted < total) {
1148
+ // Create a batch query
1149
+ const batchQuery = this.clone()
1150
+ .limit(batchSize)
1151
+ .orderBy(primaryKey, "ASC");
1152
+
1153
+ // Execute the batch
1154
+ if (useTransaction) {
1155
+ await batchQuery.executeInTransaction();
1156
+ } else {
1157
+ await batchQuery.execute();
1158
+ }
1159
+
1160
+ deleted += batchSize;
1161
+
1162
+ // Optional delay to reduce database load and lock contention
1163
+ if (deleted < total) {
1164
+ await new Promise((resolve) => setTimeout(resolve, delay));
1165
+ }
1166
+ }
1167
+
1168
+ return { total, deleted };
1169
+ }
1170
+
1171
+ /**
1172
+ * Build WHERE clause
1173
+ * @returns {string} WHERE clause
1174
+ */
1175
+ buildWhereClause() {
1176
+ const whereBindings = [];
1177
+
1178
+ const whereClause = this.query.where
1179
+ .map((condition, index) => {
1180
+ if (condition.raw) {
1181
+ return condition.raw;
1182
+ }
1183
+
1184
+ const prefix = index > 0 ? `${condition.boolean.toUpperCase()} ` : "";
1185
+
1186
+ if (condition.type === "null" || condition.type === "notnull") {
1187
+ return `${prefix}${this.wrapColumn(condition.column)} ${condition.operator} NULL`;
1188
+ }
1189
+
1190
+ if (condition.type === "in" || condition.type === "notin") {
1191
+ const placeholders = condition.value.map(() => "?").join(", ");
1192
+ whereBindings.push(...condition.value);
1193
+ return `${prefix}${this.wrapColumn(condition.column)} ${condition.operator} (${placeholders})`;
1194
+ }
1195
+
1196
+ if (condition.type === "between") {
1197
+ const operator = condition.not ? "NOT BETWEEN" : "BETWEEN";
1198
+ whereBindings.push(...condition.values);
1199
+ return `${prefix}${this.wrapColumn(condition.column)} ${operator} ? AND ?`;
1200
+ }
1201
+
1202
+ whereBindings.push(condition.value);
1203
+ return `${prefix}${this.wrapColumn(condition.column)} ${condition.operator} ?`;
1204
+ })
1205
+ .join(" ");
1206
+
1207
+ this.bindings.push(...whereBindings);
1208
+ return whereClause;
1209
+ }
1210
+
1211
+ /**
1212
+ * Build JOIN clause
1213
+ * @returns {string} JOIN clause
1214
+ */
1215
+ buildJoinClause() {
1216
+ const joinClauses = this.query.joins.map((join) => {
1217
+ if (join.type === "cross") {
1218
+ return `CROSS JOIN ${this.wrapTable(join.table)}`;
1219
+ }
1220
+ return `${join.type.toUpperCase()} JOIN ${this.wrapTable(join.table)} ON ${this.wrapColumn(join.first)} ${join.operator} ${this.wrapColumn(join.second)}`;
1221
+ });
1222
+
1223
+ return " " + joinClauses.join(" ");
1224
+ }
1225
+
1226
+ /**
1227
+ * Build GROUP BY clause
1228
+ * @returns {string} GROUP BY clause
1229
+ */
1230
+ buildGroupByClause() {
1231
+ const groupByClause = this.query.groupBy
1232
+ .map((item) =>
1233
+ typeof item === "object" && item.raw ? item.raw : this.wrapColumn(item),
1234
+ )
1235
+ .join(", ");
1236
+
1237
+ return ` GROUP BY ${groupByClause}`;
1238
+ }
1239
+
1240
+ /**
1241
+ * Build HAVING clause
1242
+ * @returns {string} HAVING clause
1243
+ */
1244
+ buildHavingClause() {
1245
+ const havingConditions = this.query.having.map((condition) => {
1246
+ if (condition.raw) {
1247
+ return condition.raw;
1248
+ }
1249
+ if (condition.value === null) {
1250
+ return `${condition.column} ${condition.operator} NULL`;
1251
+ }
1252
+ return `${condition.column} ${condition.operator} ?`;
1253
+ });
1254
+
1255
+ return ` HAVING ${havingConditions.join(" AND ")}`;
1256
+ }
1257
+
1258
+ // ==================== HELPER METHODS ====================
1259
+ /**
1260
+ * Wrap column name for SQL using dialect adapter
1261
+ * @param {string} column - Column name
1262
+ * @returns {string} Wrapped column name
1263
+ */
1264
+ wrapColumn(column) {
1265
+ if (
1266
+ column.includes("(") ||
1267
+ column.includes(")") ||
1268
+ column.includes(" as ") ||
1269
+ column.toLowerCase().includes("distinct") ||
1270
+ column.includes("*")
1271
+ ) {
1272
+ return column;
1273
+ }
1274
+
1275
+ if (column.includes(".")) {
1276
+ return column
1277
+ .split(".")
1278
+ .map((part) => {
1279
+ if (part === "*") return "*";
1280
+ return this.adapter.quoteIdentifier(part);
1281
+ })
1282
+ .join(".");
1283
+ }
1284
+
1285
+ return this.adapter.quoteIdentifier(column);
1286
+ }
1287
+
1288
+ /**
1289
+ * Wrap table name for SQL using dialect adapter
1290
+ * @param {string} table - Table name
1291
+ * @returns {string} Wrapped table name
1292
+ */
1293
+ wrapTable(table) {
1294
+ if (table.includes(".")) {
1295
+ return table
1296
+ .split(".")
1297
+ .map((part) => this.adapter.quoteIdentifier(part))
1298
+ .join(".");
1299
+ }
1300
+
1301
+ return this.adapter.quoteIdentifier(table);
1302
+ }
1303
+
1304
+ /**
1305
+ * Quote identifier using dialect adapter
1306
+ * @param {string} identifier - Identifier to quote
1307
+ * @returns {string} Quoted identifier
1308
+ */
1309
+ quoteIdentifier(identifier) {
1310
+ return this.adapter.quoteIdentifier(identifier);
1311
+ }
1312
+
1313
+ /**
1314
+ * Execute query and return results in random order using dialect-specific random function
1315
+ * @returns {Promise<Array>} Randomly ordered results
1316
+ */
1317
+ async inRandomOrder() {
1318
+ this.orderByRaw(this.adapter.getRandomFunction());
1319
+ return this.get();
1320
+ }
1321
+
1322
+ /**
1323
+ * Get the appropriate closing quote character for the current dialect
1324
+ * @returns {string} Closing quote character
1325
+ */
1326
+ getQuoteEndChar() {
1327
+ switch (this.dialect) {
1328
+ case "mysql":
1329
+ case "mariadb":
1330
+ case "clickhouse":
1331
+ return "`";
1332
+ case "postgresql":
1333
+ case "postgres":
1334
+ case "pg":
1335
+ case "cockroachdb":
1336
+ case "cockroach":
1337
+ case "sqlite":
1338
+ case "sqlite3":
1339
+ case "oracle":
1340
+ return '"';
1341
+ case "mssql":
1342
+ case "sqlserver":
1343
+ return "]";
1344
+ default:
1345
+ return "";
1346
+ }
1347
+ }
1348
+
1349
+ /**
1350
+ * Check if an identifier needs quoting
1351
+ * @param {string} identifier - Identifier to check
1352
+ * @returns {boolean} True if quoting is needed
1353
+ */
1354
+ needsQuoting(identifier) {
1355
+ // Check for reserved keywords
1356
+ const keywords = this.getReservedKeywords();
1357
+ const upperIdentifier = identifier.toUpperCase();
1358
+
1359
+ // Check for special characters or spaces
1360
+ if (/[^a-zA-Z0-9_]/.test(identifier)) {
1361
+ return true;
1362
+ }
1363
+
1364
+ // Check if it's a reserved keyword
1365
+ if (keywords.includes(upperIdentifier)) {
1366
+ return true;
1367
+ }
1368
+
1369
+ // Check if it starts with a number
1370
+ if (/^\d/.test(identifier)) {
1371
+ return true;
1372
+ }
1373
+
1374
+ return false;
1375
+ }
1376
+
1377
+ /**
1378
+ * Get reserved keywords for the current dialect
1379
+ * @returns {string[]} Array of reserved keywords
1380
+ */
1381
+ getReservedKeywords() {
1382
+ // Common SQL reserved words
1383
+ const commonKeywords = [
1384
+ "SELECT",
1385
+ "FROM",
1386
+ "WHERE",
1387
+ "INSERT",
1388
+ "UPDATE",
1389
+ "DELETE",
1390
+ "CREATE",
1391
+ "DROP",
1392
+ "ALTER",
1393
+ "TABLE",
1394
+ "INDEX",
1395
+ "VIEW",
1396
+ "JOIN",
1397
+ "LEFT",
1398
+ "RIGHT",
1399
+ "INNER",
1400
+ "OUTER",
1401
+ "GROUP",
1402
+ "BY",
1403
+ "ORDER",
1404
+ "HAVING",
1405
+ "LIMIT",
1406
+ "OFFSET",
1407
+ "UNION",
1408
+ "ALL",
1409
+ "DISTINCT",
1410
+ "AS",
1411
+ "ON",
1412
+ "AND",
1413
+ "OR",
1414
+ "NOT",
1415
+ "NULL",
1416
+ "IS",
1417
+ "IN",
1418
+ "BETWEEN",
1419
+ "LIKE",
1420
+ "EXISTS",
1421
+ ];
1422
+
1423
+ // Dialect-specific reserved words
1424
+ const dialectKeywords = {
1425
+ mysql: ["AUTO_INCREMENT", "ENGINE", "CHARSET", "COLLATE", "RAND"],
1426
+ mariadb: ["AUTO_INCREMENT", "ENGINE", "CHARSET", "COLLATE", "RAND"],
1427
+ postgresql: ["SERIAL", "BIGSERIAL", "RETURNING", "ILIKE", "RANDOM"],
1428
+ postgres: ["SERIAL", "BIGSERIAL", "RETURNING", "ILIKE", "RANDOM"],
1429
+ pg: ["SERIAL", "BIGSERIAL", "RETURNING", "ILIKE", "RANDOM"],
1430
+ sqlite: ["AUTOINCREMENT", "TEMPORARY", "RANDOM"],
1431
+ sqlite3: ["AUTOINCREMENT", "TEMPORARY", "RANDOM"],
1432
+ mssql: ["IDENTITY", "TOP", "OUTPUT", "WITH"],
1433
+ sqlserver: ["IDENTITY", "TOP", "OUTPUT", "WITH"],
1434
+ oracle: ["SEQUENCE", "DUAL", "ROWNUM", "SYSDATE"],
1435
+ cockroachdb: ["RETURNING", "RANDOM"],
1436
+ cockroach: ["RETURNING", "RANDOM"],
1437
+ clickhouse: ["ENGINE", "ORDER BY", "PRIMARY KEY"],
1438
+ };
1439
+
1440
+ return [...commonKeywords, ...(dialectKeywords[this.dialect] || [])];
1441
+ }
1442
+
1443
+ /**
1444
+ * Safely escape identifier to prevent SQL injection
1445
+ * @param {string} identifier - Raw identifier
1446
+ * @returns {string} Safe identifier
1447
+ */
1448
+ safeIdentifier(identifier) {
1449
+ // Remove potentially dangerous characters
1450
+ let safeId = identifier.replace(/[;'"\\]/g, "");
1451
+
1452
+ // Apply dialect-specific escaping
1453
+ switch (this.dialect) {
1454
+ case "mysql":
1455
+ case "mariadb":
1456
+ safeId = safeId.replace(/`/g, "``");
1457
+ break;
1458
+ case "postgresql":
1459
+ case "postgres":
1460
+ case "pg":
1461
+ case "cockroachdb":
1462
+ case "cockroach":
1463
+ safeId = safeId.replace(/"/g, '""');
1464
+ break;
1465
+ case "mssql":
1466
+ case "sqlserver":
1467
+ safeId = safeId.replace(/\]/g, "]]");
1468
+ break;
1469
+ case "oracle":
1470
+ safeId = safeId.replace(/"/g, '""');
1471
+ break;
1472
+ }
1473
+
1474
+ return safeId;
1475
+ }
1476
+
1477
+ /**
1478
+ * Get parameter placeholder for the current dialect
1479
+ * @param {number} index - Parameter index (1-based)
1480
+ * @returns {string} Parameter placeholder
1481
+ */
1482
+ getParameterPlaceholder(index) {
1483
+ switch (this.dialect) {
1484
+ case "postgresql":
1485
+ case "postgres":
1486
+ case "pg":
1487
+ case "cockroachdb":
1488
+ case "cockroach":
1489
+ return `$${index}`;
1490
+ case "mssql":
1491
+ case "sqlserver":
1492
+ return `@p${index}`;
1493
+ case "oracle":
1494
+ return `:p${index}`;
1495
+ default:
1496
+ return "?";
1497
+ }
1498
+ }
1499
+
1500
+ /**
1501
+ * Build LIMIT/OFFSET clause with dialect-specific syntax
1502
+ * @param {number|null} limit - Limit value
1503
+ * @param {number|null} offset - Offset value
1504
+ * @returns {string} LIMIT/OFFSET clause
1505
+ */
1506
+ buildLimitOffset(limit, offset) {
1507
+ let clause = "";
1508
+
1509
+ if (this.dialect === "mssql" || this.dialect === "sqlserver") {
1510
+ // SQL Server uses OFFSET/FETCH syntax
1511
+ if (offset !== null) {
1512
+ clause += ` OFFSET ${offset} ROWS`;
1513
+ }
1514
+ if (limit !== null) {
1515
+ if (offset !== null) {
1516
+ clause += ` FETCH NEXT ${limit} ROWS ONLY`;
1517
+ } else {
1518
+ clause += ` FETCH FIRST ${limit} ROWS ONLY`;
1519
+ }
1520
+ }
1521
+ } else if (this.dialect === "oracle") {
1522
+ // Oracle uses ROWNUM or FETCH FIRST syntax
1523
+ if (limit !== null) {
1524
+ const limitValue = limit;
1525
+ const offsetValue = offset || 0;
1526
+ clause = ` FETCH FIRST ${limitValue} ROWS ONLY`;
1527
+ if (offsetValue > 0) {
1528
+ clause = ` OFFSET ${offsetValue} ROWS FETCH NEXT ${limitValue} ROWS ONLY`;
1529
+ }
1530
+ }
1531
+ } else {
1532
+ // Standard SQL syntax for MySQL, PostgreSQL, SQLite, etc.
1533
+ if (limit !== null) {
1534
+ clause += ` LIMIT ${limit}`;
1535
+ }
1536
+ if (offset !== null) {
1537
+ clause += ` OFFSET ${offset}`;
1538
+ }
1539
+ }
1540
+
1541
+ return clause;
1542
+ }
1543
+
1544
+ /**
1545
+ * Get random function for the current dialect
1546
+ * @returns {string} Random function name
1547
+ */
1548
+ getRandomFunction() {
1549
+ switch (this.dialect) {
1550
+ case "mysql":
1551
+ case "mariadb":
1552
+ return "RAND()";
1553
+ case "postgresql":
1554
+ case "postgres":
1555
+ case "pg":
1556
+ case "cockroachdb":
1557
+ case "cockroach":
1558
+ case "sqlite":
1559
+ case "sqlite3":
1560
+ return "RANDOM()";
1561
+ case "mssql":
1562
+ case "sqlserver":
1563
+ return "NEWID()";
1564
+ case "oracle":
1565
+ return "DBMS_RANDOM.VALUE";
1566
+ case "clickhouse":
1567
+ return "rand()";
1568
+ default:
1569
+ return "RAND()";
1570
+ }
1571
+ }
1572
+
1573
+ /**
1574
+ * Check if RETURNING clause is supported
1575
+ * @returns {boolean} True if RETURNING is supported
1576
+ */
1577
+ supportsReturning() {
1578
+ return [
1579
+ "postgresql",
1580
+ "postgres",
1581
+ "pg",
1582
+ "cockroachdb",
1583
+ "cockroach",
1584
+ "oracle",
1585
+ ].includes(this.dialect);
1586
+ }
1587
+
1588
+ /**
1589
+ * Check if WITH clause (CTE) is supported
1590
+ * @returns {boolean} True if WITH clause is supported
1591
+ */
1592
+ supportsWithClause() {
1593
+ return [
1594
+ "postgresql",
1595
+ "postgres",
1596
+ "pg",
1597
+ "cockroachdb",
1598
+ "cockroach",
1599
+ "mssql",
1600
+ "sqlserver",
1601
+ "oracle",
1602
+ ].includes(this.dialect);
1603
+ }
1604
+
1605
+ /**
1606
+ * Check if specific feature is supported
1607
+ * @param {string} feature - Feature to check
1608
+ * @returns {boolean} True if feature is supported
1609
+ */
1610
+ supportsFeature(feature) {
1611
+ const featureSupport = {
1612
+ returning: this.supportsReturning(),
1613
+ with: this.supportsWithClause(),
1614
+ window_functions: [
1615
+ "postgresql",
1616
+ "postgres",
1617
+ "pg",
1618
+ "cockroachdb",
1619
+ "cockroach",
1620
+ "mssql",
1621
+ "sqlserver",
1622
+ "oracle",
1623
+ ].includes(this.dialect),
1624
+ json_functions: [
1625
+ "mysql",
1626
+ "mariadb",
1627
+ "postgresql",
1628
+ "postgres",
1629
+ "pg",
1630
+ "cockroachdb",
1631
+ "cockroach",
1632
+ ].includes(this.dialect),
1633
+ fulltext_search: [
1634
+ "mysql",
1635
+ "mariadb",
1636
+ "postgresql",
1637
+ "postgres",
1638
+ "pg",
1639
+ ].includes(this.dialect),
1640
+ spatial_data: [
1641
+ "mysql",
1642
+ "mariadb",
1643
+ "postgresql",
1644
+ "postgres",
1645
+ "pg",
1646
+ ].includes(this.dialect),
1647
+ };
1648
+
1649
+ return featureSupport[feature] || false;
1650
+ }
1651
+
1652
+ /**
1653
+ * Quote identifier based on database dialect
1654
+ * @param {string} identifier - Identifier to quote
1655
+ * @returns {string} Quoted identifier
1656
+ */
1657
+ quoteIdentifier(identifier) {
1658
+ // Handle raw expressions or already quoted identifiers
1659
+ if (
1660
+ identifier.includes("(") ||
1661
+ identifier.includes(")") ||
1662
+ identifier.includes(" as ") ||
1663
+ identifier.includes("`") ||
1664
+ identifier.includes('"') ||
1665
+ identifier.includes("[") ||
1666
+ identifier.includes("]")
1667
+ ) {
1668
+ return identifier;
1669
+ }
1670
+
1671
+ // Handle qualified identifiers (e.g., "database.table.column")
1672
+ if (identifier.includes(".")) {
1673
+ return identifier
1674
+ .split(".")
1675
+ .map((part) => this._quoteIdentifierPart(part))
1676
+ .join(".");
1677
+ }
1678
+
1679
+ return this._quoteIdentifierPart(identifier);
1680
+ }
1681
+
1682
+ /**
1683
+ * Internal method to quote a single identifier part
1684
+ * @param {string} identifier - Identifier part to quote
1685
+ * @returns {string} Quoted identifier part
1686
+ * @private
1687
+ */
1688
+ _quoteIdentifierPart(identifier) {
1689
+ // Trim whitespace
1690
+ identifier = identifier.trim();
1691
+
1692
+ // Return if already quoted
1693
+ if (
1694
+ (identifier.startsWith("`") && identifier.endsWith("`")) ||
1695
+ (identifier.startsWith('"') && identifier.endsWith('"')) ||
1696
+ (identifier.startsWith("[") && identifier.endsWith("]"))
1697
+ ) {
1698
+ return identifier;
1699
+ }
1700
+
1701
+ // Apply dialect-specific quoting rules
1702
+ switch (this.dialect) {
1703
+ case "mysql":
1704
+ case "mariadb":
1705
+ case "clickhouse":
1706
+ return `\`${identifier}\``;
1707
+
1708
+ case "postgresql":
1709
+ case "postgres":
1710
+ case "pg":
1711
+ case "cockroachdb":
1712
+ case "cockroach":
1713
+ return `"${identifier}"`;
1714
+
1715
+ case "sqlite":
1716
+ case "sqlite3":
1717
+ // SQLite supports both backticks and double quotes, but double quotes are standard
1718
+ return `"${identifier}"`;
1719
+
1720
+ case "mssql":
1721
+ case "sqlserver":
1722
+ return `[${identifier}]`;
1723
+
1724
+ case "oracle":
1725
+ // Oracle typically uses uppercase and double quotes
1726
+ return `"${identifier.toUpperCase()}"`;
1727
+
1728
+ default:
1729
+ // For unsupported dialects, return unquoted identifier with warning
1730
+ console.warn(
1731
+ `Unsupported dialect: ${this.dialect}, returning unquoted identifier`,
1732
+ );
1733
+ return identifier;
1734
+ }
1735
+ }
1736
+
1737
+ /**
1738
+ * Execute raw SQL query
1739
+ * @param {string} sql - SQL query
1740
+ * @param {Array} bindings - Query bindings
1741
+ * @returns {Promise<Object>} Query result
1742
+ */
1743
+ async executeQuery(sql, bindings = []) {
1744
+ try {
1745
+ if (!this.connection?.query && !this.connection?.execute) {
1746
+ throw new Error(
1747
+ "Database connection does not have query or execute method",
1748
+ );
1749
+ }
1750
+
1751
+ const executeMethod = this.connection.execute || this.connection.query;
1752
+ const result = await executeMethod.call(this.connection, sql, bindings);
1753
+
1754
+ // Emit query event
1755
+ this.emit("query", { sql, bindings, result });
1756
+
1757
+ return result;
1758
+ } catch (error) {
1759
+ this.emit("query:error", { sql, bindings, error });
1760
+ throw error;
1761
+ }
1762
+ }
1763
+
1764
+ /**
1765
+ * Clone the query builder
1766
+ * @returns {QueryBuilder} Cloned query builder instance
1767
+ */
1768
+ clone() {
1769
+ const cloned = new QueryBuilder(
1770
+ this.tableName,
1771
+ this.connection,
1772
+ this.dialect,
1773
+ );
1774
+
1775
+ cloned.query = JSON.parse(JSON.stringify(this.query));
1776
+ cloned.bindings = [...this.bindings];
1777
+ cloned.paramIndex = this.paramIndex;
1778
+ cloned.subQueries = new Map(this.subQueries);
1779
+
1780
+ return cloned;
1781
+ }
1782
+
1783
+ /**
1784
+ * Add raw SQL to SELECT clause
1785
+ * @param {string} expression - Raw SQL expression
1786
+ * @param {Array} bindings - Bindings for raw SQL
1787
+ * @returns {QueryBuilder} Query builder instance
1788
+ */
1789
+ selectRaw(expression, bindings = []) {
1790
+ this.query.columns.push({ raw: expression });
1791
+ this.bindings.push(...bindings);
1792
+ return this;
1793
+ }
1794
+
1795
+ /**
1796
+ * Add raw value
1797
+ * @param {string} value - Raw value
1798
+ * @returns {Object} Raw value object
1799
+ */
1800
+ raw(value) {
1801
+ return { raw: value };
1802
+ }
1803
+
1804
+ // ==================== UTILITY METHODS ====================
1805
+
1806
+ /**
1807
+ * Paginate results
1808
+ * @param {number} page - Page number (1-based)
1809
+ * @param {number} perPage - Items per page
1810
+ * @returns {QueryBuilder} Query builder instance
1811
+ */
1812
+ paginate(page = 1, perPage = 20) {
1813
+ const safePage = Math.max(1, page);
1814
+ const safePerPage = Math.min(Math.max(1, perPage), 100);
1815
+
1816
+ this.query.limit = safePerPage;
1817
+ this.query.offset = (safePage - 1) * safePerPage;
1818
+
1819
+ return this;
1820
+ }
1821
+
1822
+ /**
1823
+ * Get paginated results with metadata
1824
+ * @param {number} page - Page number (1-based)
1825
+ * @param {number} perPage - Items per page
1826
+ * @returns {Promise<Object>} Paginated results with metadata
1827
+ */
1828
+ async paginateWithMetadata(page = 1, perPage = 20) {
1829
+ const countQuery = this.clone();
1830
+ countQuery.query.columns = ["COUNT(*) as total"];
1831
+ countQuery.query.limit = null;
1832
+ countQuery.query.offset = null;
1833
+ countQuery.query.orderBy = [];
1834
+
1835
+ const countResult = await countQuery.first();
1836
+ const total = parseInt(countResult.total);
1837
+
1838
+ this.paginate(page, perPage);
1839
+ const data = await this.get();
1840
+
1841
+ const totalPages = Math.ceil(total / perPage);
1842
+ const hasNextPage = page < totalPages;
1843
+ const hasPrevPage = page > 1;
1844
+
1845
+ return {
1846
+ data,
1847
+ pagination: {
1848
+ page,
1849
+ perPage,
1850
+ total,
1851
+ totalPages,
1852
+ hasNextPage,
1853
+ hasPrevPage,
1854
+ nextPage: hasNextPage ? page + 1 : null,
1855
+ prevPage: hasPrevPage ? page - 1 : null,
1856
+ },
1857
+ };
1858
+ }
1859
+
1860
+ /**
1861
+ * Increment column value
1862
+ * @param {string} column - Column name
1863
+ * @param {number} amount - Amount to increment
1864
+ * @returns {QueryBuilder} Query builder instance
1865
+ */
1866
+ increment(column, amount = 1) {
1867
+ this.query.type = "update";
1868
+ if (!this.query.data) this.query.data = {};
1869
+ this.query.data[column] = this.raw(
1870
+ `${this.wrapColumn(column)} + ${amount}`,
1871
+ );
1872
+ return this;
1873
+ }
1874
+
1875
+ /**
1876
+ * Decrement column value
1877
+ * @param {string} column - Column name
1878
+ * @param {number} amount - Amount to decrement
1879
+ * @returns {QueryBuilder} Query builder instance
1880
+ */
1881
+ decrement(column, amount = 1) {
1882
+ this.query.type = "update";
1883
+ if (!this.query.data) this.query.data = {};
1884
+ this.query.data[column] = this.raw(
1885
+ `${this.wrapColumn(column)} - ${amount}`,
1886
+ );
1887
+ return this;
1888
+ }
1889
+
1890
+ /**
1891
+ * Add raw GROUP BY clause
1892
+ * @param {string} expression - Raw GROUP BY expression
1893
+ * @param {Array} bindings - Bindings for raw expression
1894
+ * @returns {QueryBuilder} Query builder instance
1895
+ */
1896
+ groupByRaw(expression, bindings = []) {
1897
+ this.query.groupBy.push({ raw: expression });
1898
+ this.bindings.push(...bindings);
1899
+ return this;
1900
+ }
1901
+
1902
+ /**
1903
+ * Add raw HAVING clause
1904
+ * @param {string} sql - Raw SQL HAVING clause
1905
+ * @param {Array} bindings - Bindings for raw SQL
1906
+ * @param {string} boolean - Boolean operator (and, or)
1907
+ * @returns {QueryBuilder} Query builder instance
1908
+ */
1909
+ havingRaw(sql, bindings = [], boolean = "and") {
1910
+ this.query.having.push({
1911
+ raw: sql,
1912
+ boolean,
1913
+ });
1914
+
1915
+ this.bindings.push(...bindings);
1916
+ return this;
1917
+ }
1918
+
1919
+ /**
1920
+ * Add WHERE column comparison
1921
+ * @param {string} first - First column
1922
+ * @param {string} operator - Comparison operator
1923
+ * @param {string} second - Second column
1924
+ * @param {string} boolean - Boolean operator (and, or)
1925
+ * @returns {QueryBuilder} Query builder instance
1926
+ */
1927
+ whereColumn(first, operator, second, boolean = "and") {
1928
+ this.query.where.push({
1929
+ type: "column",
1930
+ first,
1931
+ operator,
1932
+ second,
1933
+ boolean,
1934
+ });
1935
+ return this;
1936
+ }
1937
+
1938
+ /**
1939
+ * Add WHERE EXISTS subquery
1940
+ * @param {Function} callback - Subquery callback
1941
+ * @param {string} boolean - Boolean operator (and, or)
1942
+ * @param {boolean} not - Whether to use NOT EXISTS
1943
+ * @returns {QueryBuilder} Query builder instance
1944
+ */
1945
+ whereExists(callback, boolean = "and", not = false) {
1946
+ const subQuery = new QueryBuilder("", this.connection, this.dialect);
1947
+ callback(subQuery);
1948
+ const { sql, bindings } = subQuery.toSQL();
1949
+
1950
+ this.query.where.push({
1951
+ type: "exists",
1952
+ sql: `(${sql})`,
1953
+ boolean,
1954
+ not,
1955
+ });
1956
+
1957
+ this.bindings.push(...bindings);
1958
+ return this;
1959
+ }
1960
+
1961
+ /**
1962
+ * Add WHERE NOT EXISTS subquery
1963
+ * @param {Function} callback - Subquery callback
1964
+ * @param {string} boolean - Boolean operator (and, or)
1965
+ * @returns {QueryBuilder} Query builder instance
1966
+ */
1967
+ whereNotExists(callback, boolean = "and") {
1968
+ return this.whereExists(callback, boolean, true);
1969
+ }
1970
+
1971
+ /**
1972
+ * Add WHERE subquery condition
1973
+ * @param {string} column - Column name
1974
+ * @param {string} operator - Comparison operator
1975
+ * @param {Function} callback - Subquery callback
1976
+ * @param {string} boolean - Boolean operator (and, or)
1977
+ * @returns {QueryBuilder} Query builder instance
1978
+ */
1979
+ whereSub(column, operator, callback, boolean = "and") {
1980
+ const subQuery = new QueryBuilder("", this.connection, this.dialect);
1981
+ callback(subQuery);
1982
+ const { sql, bindings } = subQuery.toSQL();
1983
+
1984
+ this.query.where.push({
1985
+ type: "subquery",
1986
+ column,
1987
+ operator,
1988
+ sql: `(${sql})`,
1989
+ boolean,
1990
+ });
1991
+ }
1992
+
1993
+ /**
1994
+ * Add OR WHERE column comparison
1995
+ * @param {string} first - First column
1996
+ * @param {string} operator - Comparison operator
1997
+ * @param {string} second - Second column
1998
+ * @returns {QueryBuilder} Query builder instance
1999
+ */
2000
+ orWhereColumn(first, operator, second) {
2001
+ return this.whereColumn(first, operator, second, "or");
2002
+ }
2003
+
2004
+ /**
2005
+ * Add WHERE date condition
2006
+ * @param {string} column - Column name
2007
+ * @param {string} operator - Comparison operator
2008
+ * @param {Date|string} value - Date value
2009
+ * @param {string} boolean - Boolean operator (and, or)
2010
+ * @returns {QueryBuilder} Query builder instance
2011
+ */
2012
+ whereDate(column, operator, value, boolean = "and") {
2013
+ const dateValue =
2014
+ value instanceof Date ? value.toISOString().split("T") : value;
2015
+ this.query.where.push({
2016
+ type: "date",
2017
+ column,
2018
+ operator,
2019
+ value: dateValue,
2020
+ boolean,
2021
+ });
2022
+ this.bindings.push(dateValue);
2023
+ return this;
2024
+ }
2025
+
2026
+ /**
2027
+ * Add WHERE time condition
2028
+ * @param {string} column - Column
2029
+ * @param {string} operator - Comparison operator
2030
+ * @param {Date|string} value - Time value
2031
+ * @param {string} boolean - Boolean operator (and, or)
2032
+ * @returns {QueryBuilder} Query builder instance
2033
+ */
2034
+ whereTime(column, operator, value, boolean = "and") {
2035
+ const timeValue =
2036
+ value instanceof Date ? value.toISOString().split("T").split(".") : value;
2037
+ this.query.where.push({
2038
+ type: "time",
2039
+ column,
2040
+ operator,
2041
+ value: timeValue,
2042
+ boolean,
2043
+ });
2044
+ this.bindings.push(timeValue);
2045
+ return this;
2046
+ }
2047
+
2048
+ /**
2049
+ * Add WHERE year condition
2050
+ * @param {string} column - Column name
2051
+ * @param {string} operator - Comparison operator
2052
+ * @param {number|string} value - Year value
2053
+ * @param {string} boolean - Boolean operator (and, or)
2054
+ * @returns {QueryBuilder} Query builder instance
2055
+ */
2056
+ whereYear(column, operator, value, boolean = "and") {
2057
+ this.query.where.push({
2058
+ type: "year",
2059
+ column,
2060
+ operator,
2061
+ value,
2062
+ boolean,
2063
+ });
2064
+ this.bindings.push(value);
2065
+ return this;
2066
+ }
2067
+
2068
+ /**
2069
+ * Add WHERE month condition
2070
+ * @param {string} column - Column name
2071
+ * @param {string} operator - Comparison operator
2072
+ * @param {number|string} value - Month value
2073
+ * @param {string} boolean - Boolean operator (and, or)
2074
+ * @returns {QueryBuilder} Query builder instance
2075
+ */
2076
+ whereMonth(column, operator, value, boolean = "and") {
2077
+ this.query.where.push({
2078
+ type: "month",
2079
+ column,
2080
+ operator,
2081
+ value,
2082
+ boolean,
2083
+ });
2084
+ this.bindings.push(value);
2085
+ return this;
2086
+ }
2087
+
2088
+ /**
2089
+ * Add WHERE day condition
2090
+ * @param {string} column - Column name
2091
+ * @param {string} operator - Comparison operator
2092
+ * @param {number|string} value - Day value
2093
+ * @param {string} boolean - Boolean operator (and, or)
2094
+ * @returns {QueryBuilder} Query builder instance
2095
+ */
2096
+ whereDay(column, operator, value, boolean = "and") {
2097
+ this.query.where.push({
2098
+ type: "day",
2099
+ column,
2100
+ operator,
2101
+ value,
2102
+ boolean,
2103
+ });
2104
+ this.bindings.push(value);
2105
+ return this;
2106
+ }
2107
+
2108
+ /**
2109
+ * Add WHERE condition group
2110
+ * @param {Function} callback - Group callback
2111
+ * @param {string} boolean - Boolean operator (and, or)
2112
+ * @returns {QueryBuilder} Query builder instance
2113
+ */
2114
+ whereGroup(callback, boolean = "and") {
2115
+ const groupQuery = new QueryBuilder("", this.connection, this.dialect);
2116
+ callback(groupQuery);
2117
+ const { sql, bindings } = groupQuery.toSQL();
2118
+
2119
+ this.query.where.push({
2120
+ type: "group",
2121
+ sql: `(${sql})`,
2122
+ boolean,
2123
+ });
2124
+
2125
+ this.bindings.push(...bindings);
2126
+ return this;
2127
+ }
2128
+
2129
+ /**
2130
+ * Add OR WHERE condition group
2131
+ * @param {Function} callback - Group callback
2132
+ * @returns {QueryBuilder} Query builder instance
2133
+ */
2134
+ orWhereGroup(callback) {
2135
+ return this.whereGroup(callback, "or");
2136
+ }
2137
+
2138
+ /**
2139
+ * Add UNION query
2140
+ * @param {Function|QueryBuilder} query - Query to union
2141
+ * @param {boolean} all - Whether to use UNION ALL
2142
+ * @returns {QueryBuilder} Query builder instance
2143
+ */
2144
+ union(query, all = false) {
2145
+ const unionQuery =
2146
+ typeof query === "function"
2147
+ ? new QueryBuilder("", this.connection, this.dialect)
2148
+ : query;
2149
+
2150
+ if (typeof query === "function") {
2151
+ query(unionQuery);
2152
+ }
2153
+
2154
+ const { sql, bindings } = unionQuery.toSQL();
2155
+
2156
+ this.query.union.push({
2157
+ query: sql,
2158
+ all,
2159
+ bindings,
2160
+ });
2161
+
2162
+ this.bindings.push(...bindings);
2163
+ return this;
2164
+ }
2165
+
2166
+ /**
2167
+ * Add UNION ALL query
2168
+ * @param {Function|QueryBuilder} query - Query to union
2169
+ * @returns {QueryBuilder} Query builder instance
2170
+ */
2171
+ unionAll(query) {
2172
+ return this.union(query, true);
2173
+ }
2174
+
2175
+ /**
2176
+ * Add WITH clause (Common Table Expression)
2177
+ * @param {string} name - CTE name
2178
+ * @param {Function|string} query - CTE query or SQL
2179
+ * @param {Array} columns - CTE column names
2180
+ * @param {boolean} recursive - Whether CTE is recursive
2181
+ * @returns {QueryBuilder} Query builder instance
2182
+ */
2183
+ with(name, query, columns = [], recursive = false) {
2184
+ let cteQuery;
2185
+ let cteBindings = [];
2186
+
2187
+ if (typeof query === "function") {
2188
+ const qb = new QueryBuilder("", this.connection, this.dialect);
2189
+ query(qb);
2190
+ const result = qb.toSQL();
2191
+ cteQuery = result.sql;
2192
+ cteBindings = result.bindings;
2193
+ } else if (typeof query === "string") {
2194
+ cteQuery = query;
2195
+ } else if (query instanceof QueryBuilder) {
2196
+ const result = query.toSQL();
2197
+ cteQuery = result.sql;
2198
+ cteBindings = result.bindings;
2199
+ } else {
2200
+ throw new Error(
2201
+ "CTE query must be a function, string, or QueryBuilder instance",
2202
+ );
2203
+ }
2204
+
2205
+ this.query.with.push({
2206
+ name,
2207
+ query: cteQuery,
2208
+ columns,
2209
+ recursive,
2210
+ });
2211
+
2212
+ this.bindings.push(...cteBindings);
2213
+ return this;
2214
+ }
2215
+
2216
+ /**
2217
+ * Add recursive WITH clause
2218
+ * @param {string} name - CTE name
2219
+ * @param {Function} query - CTE query function
2220
+ * @param {Array} columns - CTE column names
2221
+ * @returns {QueryBuilder} Query builder instance
2222
+ */
2223
+ withRecursive(name, query, columns = []) {
2224
+ return this.with(name, query, columns, true);
2225
+ }
2226
+
2227
+ /**
2228
+ * Add LOCK clause
2229
+ * @param {string} lock - Lock type (FOR UPDATE, FOR SHARE, etc.)
2230
+ * @returns {QueryBuilder} Query builder instance
2231
+ */
2232
+ lock(lock = "FOR UPDATE") {
2233
+ this.query.lock = lock;
2234
+ return this;
2235
+ }
2236
+
2237
+ /**
2238
+ * Add LOCK FOR UPDATE clause
2239
+ * @returns {QueryBuilder} Query builder instance
2240
+ */
2241
+ lockForUpdate() {
2242
+ return this.lock("FOR UPDATE");
2243
+ }
2244
+
2245
+ /**
2246
+ * Add LOCK FOR SHARE clause
2247
+ * @returns {QueryBuilder} Query builder instance
2248
+ */
2249
+ lockForShare() {
2250
+ return this.lock("FOR SHARE");
2251
+ }
2252
+
2253
+ /**
2254
+ * Add RETURNING clause (PostgreSQL only)
2255
+ * @param {...string} columns - Columns to return
2256
+ * @returns {QueryBuilder} Query builder instance
2257
+ */
2258
+ returning(...columns) {
2259
+ if (
2260
+ !["postgresql", "postgres", "pg", "cockroachdb", "cockroach"].includes(
2261
+ this.dialect,
2262
+ )
2263
+ ) {
2264
+ console.warn(
2265
+ "RETURNING clause is only supported in PostgreSQL and CockroachDB",
2266
+ );
2267
+ }
2268
+ this.query.returning = columns;
2269
+ return this;
2270
+ }
2271
+
2272
+ /**
2273
+ * Execute query and return first result or default value
2274
+ * @param {*} defaultValue - Default value if no result
2275
+ * @returns {Promise<*>} First result or default value
2276
+ */
2277
+ async firstOr(defaultValue = null) {
2278
+ const result = await this.first();
2279
+ return result || defaultValue;
2280
+ }
2281
+
2282
+ /**
2283
+ * Execute query and return first result or throw error
2284
+ * @param {string} message - Error message
2285
+ * @returns {Promise<Object>} First result
2286
+ * @throws {Error} If no result found
2287
+ */
2288
+ async firstOrFail(message = "No records found") {
2289
+ const result = await this.first();
2290
+ if (!result) {
2291
+ throw new Error(message);
2292
+ }
2293
+ return result;
2294
+ }
2295
+
2296
+ /**
2297
+ * Execute query and return value of a single column
2298
+ * @param {string} column - Column name
2299
+ * @returns {Promise<*>} Column value
2300
+ */
2301
+ async value(column) {
2302
+ const result = await this.first();
2303
+ return result ? result[column] : null;
2304
+ }
2305
+
2306
+ /**
2307
+ * Execute query and return plucked values
2308
+ * @param {string} column - Column name
2309
+ * @returns {Promise<Array>} Array of column values
2310
+ */
2311
+ async pluck(column) {
2312
+ const results = await this.get();
2313
+ return results.map((row) => row[column]);
2314
+ }
2315
+
2316
+ /**
2317
+ * Execute query and return key-value pairs
2318
+ * @param {string} keyColumn - Key column name
2319
+ * @param {string} valueColumn - Value column name
2320
+ * @returns {Promise<Object>} Key-value object
2321
+ */
2322
+ async keyBy(keyColumn, valueColumn = null) {
2323
+ const results = await this.get();
2324
+ const keyValuePairs = {};
2325
+
2326
+ results.forEach((row) => {
2327
+ const key = row[keyColumn];
2328
+ const value = valueColumn ? row[valueColumn] : row;
2329
+ keyValuePairs[key] = value;
2330
+ });
2331
+
2332
+ return keyValuePairs;
2333
+ }
2334
+
2335
+ /**
2336
+ * Execute query and return chunked results
2337
+ * @param {number} size - Chunk size
2338
+ * @param {Function} callback - Callback for each chunk
2339
+ * @returns {Promise<void>}
2340
+ */
2341
+ async chunk(size, callback) {
2342
+ let page = 1;
2343
+ let hasMore = true;
2344
+
2345
+ while (hasMore) {
2346
+ const chunkQuery = this.clone();
2347
+ chunkQuery.paginate(page, size);
2348
+ const results = await chunkQuery.get();
2349
+
2350
+ if (results.length > 0) {
2351
+ await callback(results, page);
2352
+ page++;
2353
+ } else {
2354
+ hasMore = false;
2355
+ }
2356
+ }
2357
+ }
2358
+
2359
+ /**
2360
+ * Execute query and return cursor for streaming
2361
+ * @param {number} chunkSize - Chunk size for streaming
2362
+ * @returns {AsyncGenerator} Async generator for streaming results
2363
+ */
2364
+ async *cursor(chunkSize = 100) {
2365
+ let page = 1;
2366
+ let hasMore = true;
2367
+
2368
+ while (hasMore) {
2369
+ const chunkQuery = this.clone();
2370
+ chunkQuery.paginate(page, chunkSize);
2371
+ const results = await chunkQuery.get();
2372
+
2373
+ if (results.length > 0) {
2374
+ for (const result of results) {
2375
+ yield result;
2376
+ }
2377
+ page++;
2378
+ } else {
2379
+ hasMore = false;
2380
+ }
2381
+ }
2382
+ }
2383
+
2384
+ /**
2385
+ * Execute query and return aggregated results
2386
+ * @param {string} keyColumn - Column to group by
2387
+ * @param {Function} aggregator - Aggregation function
2388
+ * @returns {Promise<Object>} Aggregated results
2389
+ */
2390
+ async aggregate(keyColumn, aggregator) {
2391
+ const results = await this.get();
2392
+ const aggregated = {};
2393
+
2394
+ results.forEach((row) => {
2395
+ const key = row[keyColumn];
2396
+ if (!aggregated[key]) {
2397
+ aggregated[key] = [];
2398
+ }
2399
+ aggregated[key].push(row);
2400
+ });
2401
+
2402
+ for (const key in aggregated) {
2403
+ aggregated[key] = aggregator(aggregated[key]);
2404
+ }
2405
+
2406
+ return aggregated;
2407
+ }
2408
+
2409
+ /**
2410
+ * Execute query and return results as map
2411
+ * @param {string} keyColumn - Column to use as key
2412
+ * @returns {Promise<Map>} Map of results
2413
+ */
2414
+ async map(keyColumn) {
2415
+ const results = await this.get();
2416
+ const map = new Map();
2417
+
2418
+ results.forEach((row) => {
2419
+ map.set(row[keyColumn], row);
2420
+ });
2421
+
2422
+ return map;
2423
+ }
2424
+
2425
+ /**
2426
+ * Execute query and return results grouped by column
2427
+ * @param {string} groupColumn - Column to group by
2428
+ * @returns {Promise<Object>} Grouped results
2429
+ */
2430
+ async groupByColumn(groupColumn) {
2431
+ const results = await this.get();
2432
+ const grouped = {};
2433
+
2434
+ results.forEach((row) => {
2435
+ const key = row[groupColumn];
2436
+ if (!grouped[key]) {
2437
+ grouped[key] = [];
2438
+ }
2439
+ grouped[key].push(row);
2440
+ });
2441
+
2442
+ return grouped;
2443
+ }
2444
+
2445
+ /**
2446
+ * Execute query and return results with index
2447
+ * @param {Function} indexer - Function to create index key
2448
+ * @returns {Promise<Object>} Indexed results
2449
+ */
2450
+ async indexBy(indexer) {
2451
+ const results = await this.get();
2452
+ const indexed = {};
2453
+
2454
+ results.forEach((row) => {
2455
+ const key = indexer(row);
2456
+ indexed[key] = row;
2457
+ });
2458
+
2459
+ return indexed;
2460
+ }
2461
+
2462
+ /**
2463
+ * Execute query and return only distinct results
2464
+ * @param {...string} columns - Columns to distinct by
2465
+ * @returns {Promise<Array>} Distinct results
2466
+ */
2467
+ async distinctResults(...columns) {
2468
+ this.distinct();
2469
+ if (columns.length > 0) {
2470
+ this.select(...columns);
2471
+ }
2472
+ return this.get();
2473
+ }
2474
+
2475
+ /**
2476
+ * Execute query and return results with limit
2477
+ * @param {number} limit - Maximum number of results
2478
+ * @returns {Promise<Array>} Limited results
2479
+ */
2480
+ async take(limit) {
2481
+ this.limit(limit);
2482
+ return this.get();
2483
+ }
2484
+
2485
+ /**
2486
+ * Execute query and skip first N results
2487
+ * @param {number} offset - Number of results to skip
2488
+ * @returns {Promise<Array>} Results after skipping
2489
+ */
2490
+ async skip(offset) {
2491
+ this.offset(offset);
2492
+ return this.get();
2493
+ }
2494
+
2495
+ /**
2496
+ * Execute query and return results in random order
2497
+ * @returns {Promise<Array>} Randomly ordered results
2498
+ */
2499
+ async inRandomOrder() {
2500
+ this.orderByRaw(this.dialect === "mysql" ? "RAND()" : "RANDOM()");
2501
+ return this.get();
2502
+ }
2503
+
2504
+ /**
2505
+ * Execute query and return results with specific columns only
2506
+ * @param {...string} columns - Columns to select
2507
+ * @returns {Promise<Array>} Results with selected columns
2508
+ */
2509
+ async only(...columns) {
2510
+ this.select(...columns);
2511
+ return this.get();
2512
+ }
2513
+
2514
+ /**
2515
+ * Execute query and return results without specific columns
2516
+ * @param {...string} columns - Columns to exclude
2517
+ * @returns {Promise<Array>} Results without excluded columns
2518
+ */
2519
+ async except(...columns) {
2520
+ const allColumns = await this.getColumnNames();
2521
+ const selectedColumns = allColumns.filter((col) => !columns.includes(col));
2522
+ this.select(...selectedColumns);
2523
+ return this.get();
2524
+ }
2525
+
2526
+ /**
2527
+ * Get column names from table
2528
+ * @returns {Promise<Array>} Array of column names
2529
+ */
2530
+ async getColumnNames() {
2531
+ const originalColumns = this.query.columns;
2532
+ const originalType = this.query.type;
2533
+
2534
+ this.query.type = "select";
2535
+ this.query.columns = ["*"];
2536
+ this.query.limit = 1;
2537
+
2538
+ const result = await this.execute();
2539
+ const columns = result.fields
2540
+ ? result.fields.map((f) => f.name)
2541
+ : Object.keys(result || {});
2542
+
2543
+ // Restore original state
2544
+ this.query.columns = originalColumns;
2545
+ this.query.type = originalType;
2546
+
2547
+ return columns;
2548
+ }
2549
+
2550
+ /**
2551
+ * Execute query and return results as JSON string
2552
+ * @returns {Promise<string>} JSON string of results
2553
+ */
2554
+ async toJson() {
2555
+ const results = await this.get();
2556
+ return JSON.stringify(results, null, 2);
2557
+ }
2558
+
2559
+ /**
2560
+ * Execute query and return results as CSV string
2561
+ * @returns {Promise<string>} CSV string of results
2562
+ */
2563
+ async toCsv() {
2564
+ const results = await this.get();
2565
+ if (results.length === 0) {
2566
+ return "";
2567
+ }
2568
+
2569
+ const headers = Object.keys(results);
2570
+ const csvRows = [];
2571
+
2572
+ // Add headers
2573
+ csvRows.push(headers.join(","));
2574
+
2575
+ // Add data rows
2576
+ results.forEach((row) => {
2577
+ const values = headers.map((header) => {
2578
+ const value = row[header];
2579
+ if (value === null || value === undefined) {
2580
+ return "";
2581
+ }
2582
+ const stringValue = String(value);
2583
+ // Escape quotes and wrap in quotes if contains comma or quotes
2584
+ if (stringValue.includes(",") || stringValue.includes('"')) {
2585
+ return `"${stringValue.replace(/"/g, '""')}"`;
2586
+ }
2587
+ return stringValue;
2588
+ });
2589
+ csvRows.push(values.join(","));
2590
+ });
2591
+
2592
+ return csvRows.join("\n");
2593
+ }
2594
+
2595
+ /**
2596
+ * Execute query and return results as array of arrays
2597
+ * @returns {Promise<Array>} Array of arrays
2598
+ */
2599
+ async toArray() {
2600
+ const results = await this.get();
2601
+ if (results.length === 0) {
2602
+ return [];
2603
+ }
2604
+
2605
+ const headers = Object.keys(results);
2606
+ const array = [headers];
2607
+
2608
+ results.forEach((row) => {
2609
+ const values = headers.map((header) => row[header]);
2610
+ array.push(values);
2611
+ });
2612
+
2613
+ return array;
2614
+ }
2615
+
2616
+ /**
2617
+ * Execute query and return results as key-value pairs
2618
+ * @param {string} keyColumn - Column to use as key
2619
+ * @param {string} valueColumn - Column to use as value
2620
+ * @returns {Promise<Object>} Key-value pairs
2621
+ */
2622
+ async toKeyValue(keyColumn, valueColumn) {
2623
+ const results = await this.get();
2624
+ const keyValue = {};
2625
+
2626
+ results.forEach((row) => {
2627
+ keyValue[row[keyColumn]] = row[valueColumn];
2628
+ });
2629
+
2630
+ return keyValue;
2631
+ }
2632
+
2633
+ /**
2634
+ * Execute query and return results with applied transformation
2635
+ * @param {Function} transformer - Transformation function
2636
+ * @returns {Promise<Array>} Transformed results
2637
+ */
2638
+ async transform(transformer) {
2639
+ const results = await this.get();
2640
+ return results.map(transformer);
2641
+ }
2642
+
2643
+ /**
2644
+ * Execute query and return results filtered by condition
2645
+ * @param {Function} filter - Filter function
2646
+ * @returns {Promise<Array>} Filtered results
2647
+ */
2648
+ async filter(filter) {
2649
+ const results = await this.get();
2650
+ return results.filter(filter);
2651
+ }
2652
+
2653
+ /**
2654
+ * Execute query and return results sorted by comparator
2655
+ * @param {Function} comparator - Comparison function
2656
+ * @returns {Promise<Array>} Sorted results
2657
+ */
2658
+ async sort(comparator) {
2659
+ const results = await this.get();
2660
+ return results.sort(comparator);
2661
+ }
2662
+
2663
+ /**
2664
+ * Execute query and return results reduced by reducer
2665
+ * @param {Function} reducer - Reduce function
2666
+ * @param {*} initialValue - Initial value for reduction
2667
+ * @returns {Promise<*>} Reduced value
2668
+ */
2669
+ async reduce(reducer, initialValue) {
2670
+ const results = await this.get();
2671
+ return results.reduce(reducer, initialValue);
2672
+ }
2673
+
2674
+ /**
2675
+ * Execute query and return results mapped to new structure
2676
+ * @param {Function} mapper - Mapping function
2677
+ * @returns {Promise<Array>} Mapped results
2678
+ */
2679
+ async mapResults(mapper) {
2680
+ const results = await this.get();
2681
+ return results.map(mapper);
2682
+ }
2683
+
2684
+ /**
2685
+ * Execute query and return results with applied side effect
2686
+ * @param {Function} sideEffect - Side effect function
2687
+ * @returns {Promise<Array>} Results after side effect
2688
+ */
2689
+ async tap(sideEffect) {
2690
+ const results = await this.get();
2691
+ sideEffect(results);
2692
+ return results;
2693
+ }
2694
+
2695
+ /**
2696
+ * Execute query and return results with timing information
2697
+ * @returns {Promise<Object>} Results with timing
2698
+ */
2699
+ async withTiming() {
2700
+ const startTime = Date.now();
2701
+ const results = await this.get();
2702
+ const endTime = Date.now();
2703
+
2704
+ return {
2705
+ results,
2706
+ timing: {
2707
+ startTime,
2708
+ endTime,
2709
+ duration: endTime - startTime,
2710
+ },
2711
+ };
2712
+ }
2713
+
2714
+ /**
2715
+ * Execute query and return results with memory usage information
2716
+ * @returns {Promise<Object>} Results with memory usage
2717
+ */
2718
+ async withMemoryUsage() {
2719
+ const startMemory = process.memoryUsage();
2720
+ const results = await this.get();
2721
+ const endMemory = process.memoryUsage();
2722
+
2723
+ return {
2724
+ results,
2725
+ memory: {
2726
+ start: startMemory,
2727
+ end: endMemory,
2728
+ diff: {
2729
+ rss: endMemory.rss - startMemory.rss,
2730
+ heapTotal: endMemory.heapTotal - startMemory.heapTotal,
2731
+ heapUsed: endMemory.heapUsed - startMemory.heapUsed,
2732
+ external: endMemory.external - startMemory.external,
2733
+ },
2734
+ },
2735
+ };
2736
+ }
2737
+
2738
+ /**
2739
+ * Execute query and return results with execution plan
2740
+ * @returns {Promise<Object>} Results with execution plan
2741
+ */
2742
+ async withExplain() {
2743
+ const explainResult = await this.explain();
2744
+ const results = await this.get();
2745
+
2746
+ return {
2747
+ results,
2748
+ explain: explainResult,
2749
+ };
2750
+ }
2751
+
2752
+ /**
2753
+ * Execute query and return results with count
2754
+ * @returns {Promise<Object>} Results with count
2755
+ */
2756
+ async withCount() {
2757
+ const countQuery = this.clone();
2758
+ countQuery.query.columns = ["COUNT(*) as total"];
2759
+ countQuery.query.limit = null;
2760
+ countQuery.query.offset = null;
2761
+ countQuery.query.orderBy = [];
2762
+
2763
+ const countResult = await countQuery.first();
2764
+ const results = await this.get();
2765
+
2766
+ return {
2767
+ results,
2768
+ count: parseInt(countResult.total),
2769
+ };
2770
+ }
2771
+
2772
+ /**
2773
+ * Execute query and return results with pagination metadata
2774
+ * @param {number} page - Page number
2775
+ * @param {number} perPage - Items per page
2776
+ * @returns {Promise<Object>} Results with pagination
2777
+ */
2778
+ async withPagination(page = 1, perPage = 20) {
2779
+ return this.paginateWithMetadata(page, perPage);
2780
+ }
2781
+
2782
+ /**
2783
+ * Execute query and return results with applied transformations
2784
+ * @param {Array<Function>} transformers - Array of transformer functions
2785
+ * @returns {Promise<Array>} Transformed results
2786
+ */
2787
+ async pipe(...transformers) {
2788
+ let results = await this.get();
2789
+
2790
+ for (const transformer of transformers) {
2791
+ results = transformer(results);
2792
+ }
2793
+
2794
+ return results;
2795
+ }
2796
+
2797
+ /**
2798
+ * Execute query and return results with caching
2799
+ * @param {string} cacheKey - Cache key
2800
+ * @param {number} ttl - Time to live in seconds
2801
+ * @param {Function} cacheGetter - Cache getter function
2802
+ * @param {Function} cacheSetter - Cache setter function
2803
+ * @returns {Promise<Array>} Cached results
2804
+ */
2805
+ async withCache(cacheKey, ttl = 300, cacheGetter = null, cacheSetter = null) {
2806
+ if (cacheGetter) {
2807
+ const cached = await cacheGetter(cacheKey);
2808
+ if (cached !== null && cached !== undefined) {
2809
+ return cached;
2810
+ }
2811
+ }
2812
+
2813
+ const results = await this.get();
2814
+
2815
+ if (cacheSetter) {
2816
+ await cacheSetter(cacheKey, results, ttl);
2817
+ }
2818
+
2819
+ return results;
2820
+ }
2821
+
2822
+ /**
2823
+ * Execute query and return results with retry logic
2824
+ * @param {number} maxRetries - Maximum number of retries
2825
+ * @param {number} delay - Delay between retries in milliseconds
2826
+ * @param {Function} retryCondition - Condition for retry
2827
+ * @returns {Promise<Array>} Results with retry
2828
+ */
2829
+ async withRetry(maxRetries = 3, delay = 1000, retryCondition = null) {
2830
+ let lastError;
2831
+
2832
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
2833
+ try {
2834
+ return await this.get();
2835
+ } catch (error) {
2836
+ lastError = error;
2837
+
2838
+ if (retryCondition && !retryCondition(error)) {
2839
+ throw error;
2840
+ }
2841
+
2842
+ if (attempt < maxRetries) {
2843
+ await new Promise((resolve) => setTimeout(resolve, delay));
2844
+ delay *= 2; // Exponential backoff
2845
+ }
2846
+ }
2847
+ }
2848
+
2849
+ throw lastError;
2850
+ }
2851
+
2852
+ /**
2853
+ * Execute query and return results with timeout
2854
+ * @param {number} timeout - Timeout in milliseconds
2855
+ * @returns {Promise<Array>} Results with timeout
2856
+ */
2857
+ async withTimeout(timeout = 5000) {
2858
+ return Promise.race([
2859
+ this.get(),
2860
+ new Promise((_, reject) => {
2861
+ setTimeout(
2862
+ () => reject(new Error(`Query timeout after ${timeout}ms`)),
2863
+ timeout,
2864
+ );
2865
+ }),
2866
+ ]);
2867
+ }
2868
+
2869
+ /**
2870
+ * Execute query and return results with transaction
2871
+ * @param {Function} transactionCallback - Transaction callback
2872
+ * @returns {Promise<Array>} Results within transaction
2873
+ */
2874
+ async withTransaction(transactionCallback) {
2875
+ if (!this.connection.beginTransaction) {
2876
+ throw new Error("Database connection does not support transactions");
2877
+ }
2878
+
2879
+ await this.connection.beginTransaction();
2880
+
2881
+ try {
2882
+ const results = await this.get();
2883
+ await transactionCallback(results);
2884
+ await this.connection.commit();
2885
+ return results;
2886
+ } catch (error) {
2887
+ await this.connection.rollback();
2888
+ throw error;
2889
+ }
2890
+ }
2891
+
2892
+ /**
2893
+ * Execute query and return results with error handling
2894
+ * @param {Function} errorHandler - Error handler function
2895
+ * @returns {Promise<Array>} Results or error handling result
2896
+ */
2897
+ async withErrorHandling(errorHandler) {
2898
+ try {
2899
+ return await this.get();
2900
+ } catch (error) {
2901
+ return errorHandler(error);
2902
+ }
2903
+ }
2904
+
2905
+ /**
2906
+ * Execute query and return results with validation
2907
+ * @param {Function} validator - Validation function
2908
+ * @returns {Promise<Array>} Validated results
2909
+ */
2910
+ async withValidation(validator) {
2911
+ const results = await this.get();
2912
+ const validationResult = validator(results);
2913
+
2914
+ if (validationResult !== true) {
2915
+ throw new Error(`Validation failed: ${validationResult}`);
2916
+ }
2917
+
2918
+ return results;
2919
+ }
2920
+
2921
+ /**
2922
+ * Execute query and return results with logging
2923
+ * @param {Function} logger - Logger function
2924
+ * @returns {Promise<Array>} Results with logging
2925
+ */
2926
+ async withLogging(logger = console.log) {
2927
+ const startTime = Date.now();
2928
+ const { sql, bindings } = this.toSQL();
2929
+
2930
+ logger(`Executing query: ${sql}`);
2931
+ logger(`Bindings: ${JSON.stringify(bindings)}`);
2932
+
2933
+ try {
2934
+ const results = await this.get();
2935
+ const endTime = Date.now();
2936
+
2937
+ logger(`Query completed in ${endTime - startTime}ms`);
2938
+ logger(`Results count: ${results.length}`);
2939
+
2940
+ return results;
2941
+ } catch (error) {
2942
+ logger(`Query failed: ${error.message}`);
2943
+ throw error;
2944
+ }
2945
+ }
2946
+
2947
+ /**
2948
+ * Execute query and return results with profiling
2949
+ * @returns {Promise<Object>} Results with profiling information
2950
+ */
2951
+ async withProfiling() {
2952
+ const startTime = Date.now();
2953
+ const startMemory = process.memoryUsage();
2954
+
2955
+ const { sql, bindings } = this.toSQL();
2956
+ const results = await this.get();
2957
+
2958
+ const endTime = Date.now();
2959
+ const endMemory = process.memoryUsage();
2960
+
2961
+ return {
2962
+ results,
2963
+ profile: {
2964
+ sql,
2965
+ bindings,
2966
+ timing: {
2967
+ startTime,
2968
+ endTime,
2969
+ duration: endTime - startTime,
2970
+ },
2971
+ memory: {
2972
+ start: startMemory,
2973
+ end: endMemory,
2974
+ diff: {
2975
+ rss: endMemory.rss - startMemory.rss,
2976
+ heapTotal: endMemory.heapTotal - startMemory.heapTotal,
2977
+ heapUsed: endMemory.heapUsed - startMemory.heapUsed,
2978
+ external: endMemory.external - startMemory.external,
2979
+ },
2980
+ },
2981
+ resultCount: results.length,
2982
+ },
2983
+ };
2984
+ }
2985
+
2986
+ /**
2987
+ * Execute query and return results with all metadata
2988
+ * @returns {Promise<Object>} Results with full metadata
2989
+ */
2990
+ async withMetadata() {
2991
+ const profile = await this.withProfiling();
2992
+ const explain = await this.explain();
2993
+ const count = await this.count();
2994
+
2995
+ return {
2996
+ ...profile,
2997
+ explain,
2998
+ count,
2999
+ query: this.query,
3000
+ dialect: this.dialect,
3001
+ table: this.tableName,
3002
+ timestamp: new Date().toISOString(),
3003
+ };
3004
+ }
3005
+
3006
+ /**
3007
+ * Dump query information for debugging
3008
+ * @returns {Object} Query information
3009
+ */
3010
+ dump() {
3011
+ const { sql, bindings } = this.toSQL();
3012
+
3013
+ return {
3014
+ sql,
3015
+ bindings,
3016
+ query: this.query,
3017
+ dialect: this.dialect,
3018
+ table: this.tableName,
3019
+ bindingsCount: bindings.length,
3020
+ timestamp: new Date().toISOString(),
3021
+ };
3022
+ }
3023
+
3024
+ /**
3025
+ * Get SQL string for debugging
3026
+ * @returns {string} SQL string
3027
+ */
3028
+ toSql() {
3029
+ return this.toSQL().sql;
3030
+ }
3031
+
3032
+ /**
3033
+ * Get query as string for logging
3034
+ * @returns {string} Query string representation
3035
+ */
3036
+ toString() {
3037
+ const { sql, bindings } = this.toSQL();
3038
+ return `QueryBuilder: ${sql} [${bindings.join(", ")}]`;
3039
+ }
3040
+
3041
+ /**
3042
+ * Check if query has WHERE conditions
3043
+ * @returns {boolean} True if has WHERE conditions
3044
+ */
3045
+ hasWhere() {
3046
+ return this.query.where.length > 0;
3047
+ }
3048
+
3049
+ /**
3050
+ * Check if query has JOINs
3051
+ * @returns {boolean} True if has JOINs
3052
+ */
3053
+ hasJoins() {
3054
+ return this.query.joins.length > 0;
3055
+ }
3056
+
3057
+ /**
3058
+ * Check if query has GROUP BY
3059
+ * @returns {boolean} True if has GROUP BY
3060
+ */
3061
+ hasGroupBy() {
3062
+ return this.query.groupBy.length > 0;
3063
+ }
3064
+
3065
+ /**
3066
+ * Check if query has ORDER BY
3067
+ * @returns {boolean} True if has ORDER BY
3068
+ */
3069
+ hasOrderBy() {
3070
+ return this.query.orderBy.length > 0;
3071
+ }
3072
+
3073
+ /**
3074
+ * Check if query has LIMIT
3075
+ * @returns {boolean} True if has LIMIT
3076
+ */
3077
+ hasLimit() {
3078
+ return this.query.limit !== null;
3079
+ }
3080
+
3081
+ /**
3082
+ * Check if query has OFFSET
3083
+ * @returns {boolean} True if has OFFSET
3084
+ */
3085
+ hasOffset() {
3086
+ return this.query.offset !== null;
3087
+ }
3088
+
3089
+ /**
3090
+ * Get connection
3091
+ * @returns {Object} Database connection
3092
+ */
3093
+ getConnection() {
3094
+ return this.connection;
3095
+ }
3096
+
3097
+ /**
3098
+ * Set connection
3099
+ * @param {Object} connection - Database connection
3100
+ * @returns {QueryBuilder} Query builder instance
3101
+ */
3102
+ setConnection(connection) {
3103
+ this.connection = connection;
3104
+ return this;
3105
+ }
3106
+
3107
+ /**
3108
+ * Get bindings count
3109
+ * @returns {number} Number of bindings
3110
+ */
3111
+ getBindingsCount() {
3112
+ return this.bindings.length;
3113
+ }
3114
+
3115
+ /**
3116
+ * Get query bindings
3117
+ * @returns {Array} Query bindings
3118
+ */
3119
+ getBindings() {
3120
+ return [...this.bindings];
3121
+ }
3122
+
3123
+ /**
3124
+ * Clear all bindings
3125
+ * @returns {QueryBuilder} Query builder instance
3126
+ */
3127
+ clearBindings() {
3128
+ this.bindings = [];
3129
+ return this;
3130
+ }
3131
+
3132
+ /**
3133
+ * Add binding to query
3134
+ * @param {*} value - Value to bind
3135
+ * @returns {QueryBuilder} Query builder instance
3136
+ */
3137
+ addBinding(value) {
3138
+ this.bindings.push(value);
3139
+ return this;
3140
+ }
3141
+
3142
+ /**
3143
+ * Add multiple bindings to query
3144
+ * @param {Array} values - Values to bind
3145
+ * @returns {QueryBuilder} Query builder instance
3146
+ */
3147
+ addBindings(values) {
3148
+ if (Array.isArray(values) && values.length > 50) {
3149
+ this.bindings = this.bindings.concat(values);
3150
+ } else {
3151
+ this.bindings.push(...values);
3152
+ }
3153
+ return this;
3154
+ }
3155
+ /**
3156
+ * Set bindings for query
3157
+ * @param {Array} bindings - Bindings to set
3158
+ * @returns {QueryBuilder} Query builder instance
3159
+ */
3160
+ setBindings(bindings) {
3161
+ this.bindings = [...bindings];
3162
+ return this;
3163
+ }
3164
+
3165
+ /**
3166
+ * Merge bindings from another query builder
3167
+ * @param {QueryBuilder} queryBuilder - Query builder to merge bindings from
3168
+ * @returns {QueryBuilder} Query builder instance
3169
+ */
3170
+ mergeBindings(queryBuilder) {
3171
+ this.bindings.push(...queryBuilder.getBindings());
3172
+ return this;
3173
+ }
3174
+
3175
+ /**
3176
+ * Get query type
3177
+ * @returns {string} Query type
3178
+ */
3179
+ getQueryType() {
3180
+ return this.query.type;
3181
+ }
3182
+
3183
+ /**
3184
+ * Set query type
3185
+ * @param {string} type - Query type (select, insert, update, delete)
3186
+ * @returns {QueryBuilder} Query builder instance
3187
+ */
3188
+ setQueryType(type) {
3189
+ const validTypes = ["select", "insert", "update", "delete"];
3190
+ if (!validTypes.includes(type)) {
3191
+ throw new Error(
3192
+ `Invalid query type: ${type}. Must be one of: ${validTypes.join(", ")}`,
3193
+ );
3194
+ }
3195
+ this.query.type = type;
3196
+ return this;
3197
+ }
3198
+
3199
+ /**
3200
+ * Get table name
3201
+ * @returns {string} Table name
3202
+ */
3203
+ getTable() {
3204
+ return this.tableName;
3205
+ }
3206
+
3207
+ /**
3208
+ * Set table name
3209
+ * @param {string} tableName - Table name
3210
+ * @returns {QueryBuilder} Query builder instance
3211
+ */
3212
+ setTable(tableName) {
3213
+ this.tableName = tableName;
3214
+ return this;
3215
+ }
3216
+
3217
+ /**
3218
+ * Get database dialect
3219
+ * @returns {string} Database dialect
3220
+ */
3221
+ getDialect() {
3222
+ return this.dialect;
3223
+ }
3224
+
3225
+ /**
3226
+ * Set database dialect
3227
+ * @param {string} dialect - Database dialect
3228
+ * @returns {QueryBuilder} Query builder instance
3229
+ */
3230
+ setDialect(dialect) {
3231
+ const supportedDialects = [
3232
+ "mysql",
3233
+ "mariadb",
3234
+ "postgresql",
3235
+ "postgres",
3236
+ "pg",
3237
+ "sqlite",
3238
+ "sqlite3",
3239
+ "mssql",
3240
+ "sqlserver",
3241
+ "oracle",
3242
+ "cockroachdb",
3243
+ "cockroach",
3244
+ "clickhouse",
3245
+ ];
3246
+
3247
+ const normalizedDialect = dialect.toLowerCase();
3248
+ if (!supportedDialects.includes(normalizedDialect)) {
3249
+ throw new Error(`Unsupported database dialect: ${dialect}`);
3250
+ }
3251
+
3252
+ this.dialect = normalizedDialect;
3253
+ return this;
3254
+ }
3255
+
3256
+ /**
3257
+ * Get query structure
3258
+ * @returns {Object} Query structure
3259
+ */
3260
+ getQuery() {
3261
+ return JSON.parse(JSON.stringify(this.query));
3262
+ }
3263
+
3264
+ /**
3265
+ * Set query structure
3266
+ * @param {Object} query - Query structure
3267
+ * @returns {QueryBuilder} Query builder instance
3268
+ */
3269
+ setQuery(query) {
3270
+ this.query = JSON.parse(JSON.stringify(query));
3271
+ return this;
3272
+ }
3273
+
3274
+ /**
3275
+ * Get query columns
3276
+ * @returns {Array} Query columns
3277
+ */
3278
+ getColumns() {
3279
+ return [...this.query.columns];
3280
+ }
3281
+
3282
+ /**
3283
+ * Set query columns
3284
+ * @param {Array} columns - Columns to select
3285
+ * @returns {QueryBuilder} Query builder instance
3286
+ */
3287
+ setColumns(columns) {
3288
+ this.query.columns = Array.isArray(columns) ? columns : [columns];
3289
+ return this;
3290
+ }
3291
+
3292
+ /**
3293
+ * Get WHERE conditions
3294
+ * @returns {Array} WHERE conditions
3295
+ */
3296
+ getWhereConditions() {
3297
+ return JSON.parse(JSON.stringify(this.query.where));
3298
+ }
3299
+
3300
+ /**
3301
+ * Get ORDER BY clauses
3302
+ * @returns {Array} ORDER BY clauses
3303
+ */
3304
+ getOrderBy() {
3305
+ return JSON.parse(JSON.stringify(this.query.orderBy));
3306
+ }
3307
+
3308
+ /**
3309
+ * Get LIMIT value
3310
+ * @returns {number|null} LIMIT value
3311
+ */
3312
+ getLimit() {
3313
+ return this.query.limit;
3314
+ }
3315
+
3316
+ /**
3317
+ * Get OFFSET value
3318
+ * @returns {number|null} OFFSET value
3319
+ */
3320
+ getOffset() {
3321
+ return this.query.offset;
3322
+ }
3323
+
3324
+ /**
3325
+ * Get JOIN clauses
3326
+ * @returns {Array} JOIN clauses
3327
+ */
3328
+ getJoins() {
3329
+ return JSON.parse(JSON.stringify(this.query.joins));
3330
+ }
3331
+
3332
+ /**
3333
+ * Get GROUP BY clauses
3334
+ * @returns {Array} GROUP BY clauses
3335
+ */
3336
+ getGroupBy() {
3337
+ return JSON.parse(JSON.stringify(this.query.groupBy));
3338
+ }
3339
+
3340
+ /**
3341
+ * Get HAVING conditions
3342
+ * @returns {Array} HAVING conditions
3343
+ */
3344
+ getHaving() {
3345
+ return JSON.parse(JSON.stringify(this.query.having));
3346
+ }
3347
+
3348
+ /**
3349
+ * Check if query is SELECT
3350
+ * @returns {boolean} True if query is SELECT
3351
+ */
3352
+ isSelect() {
3353
+ return this.query.type === "select";
3354
+ }
3355
+
3356
+ /**
3357
+ * Check if query is INSERT
3358
+ * @returns {boolean} True if query is INSERT
3359
+ */
3360
+ isInsert() {
3361
+ return this.query.type === "insert";
3362
+ }
3363
+
3364
+ /**
3365
+ * Check if query is UPDATE
3366
+ * @returns {boolean} True if query is UPDATE
3367
+ */
3368
+ isUpdate() {
3369
+ return this.query.type === "update";
3370
+ }
3371
+
3372
+ /**
3373
+ * Check if query is DELETE
3374
+ * @returns {boolean} True if query is DELETE
3375
+ */
3376
+ isDelete() {
3377
+ return this.query.type === "delete";
3378
+ }
3379
+
3380
+ /**
3381
+ * Check if query is DISTINCT
3382
+ * @returns {boolean} True if query is DISTINCT
3383
+ */
3384
+ isDistinct() {
3385
+ return this.query.distinct === true;
3386
+ }
3387
+
3388
+ /**
3389
+ * Check if query has LOCK
3390
+ * @returns {boolean} True if query has LOCK
3391
+ */
3392
+ hasLock() {
3393
+ return this.query.lock !== null;
3394
+ }
3395
+
3396
+ /**
3397
+ * Check if query has RETURNING clause
3398
+ * @returns {boolean} True if query has RETURNING clause
3399
+ */
3400
+ hasReturning() {
3401
+ return this.query.returning !== null && this.query.returning.length > 0;
3402
+ }
3403
+
3404
+ /**
3405
+ * Check if query has UNION
3406
+ * @returns {boolean} True if query has UNION
3407
+ */
3408
+ hasUnion() {
3409
+ return this.query.union.length > 0;
3410
+ }
3411
+
3412
+ /**
3413
+ * Check if query has WITH clause
3414
+ * @returns {boolean} True if query has WITH clause
3415
+ */
3416
+ hasWith() {
3417
+ return this.query.with.length > 0;
3418
+ }
3419
+
3420
+ /**
3421
+ * Check if query has CTE
3422
+ * @returns {boolean} True if query has CTE
3423
+ */
3424
+ hasCte() {
3425
+ return this.query.cte.length > 0;
3426
+ }
3427
+
3428
+ /**
3429
+ * Get query statistics
3430
+ * @returns {Object} Query statistics
3431
+ */
3432
+ getStats() {
3433
+ return {
3434
+ type: this.query.type,
3435
+ table: this.tableName,
3436
+ dialect: this.dialect,
3437
+ columns: this.query.columns.length,
3438
+ whereConditions: this.query.where.length,
3439
+ joins: this.query.joins.length,
3440
+ groupBy: this.query.groupBy.length,
3441
+ havingConditions: this.query.having.length,
3442
+ orderBy: this.query.orderBy.length,
3443
+ limit: this.query.limit,
3444
+ offset: this.query.offset,
3445
+ distinct: this.query.distinct,
3446
+ bindingsCount: this.bindings.length,
3447
+ hasLock: this.query.lock !== null,
3448
+ hasReturning:
3449
+ this.query.returning !== null && this.query.returning.length > 0,
3450
+ unionCount: this.query.union.length,
3451
+ withCount: this.query.with.length,
3452
+ cteCount: this.query.cte.length,
3453
+ };
3454
+ }
3455
+
3456
+ /**
3457
+ * Get query summary
3458
+ * @returns {Object} Query summary
3459
+ */
3460
+ getSummary() {
3461
+ const { sql, bindings } = this.toSQL();
3462
+ return {
3463
+ sql,
3464
+ bindingsCount: bindings.length,
3465
+ type: this.query.type,
3466
+ table: this.tableName,
3467
+ dialect: this.dialect,
3468
+ complexity: this.calculateComplexity(),
3469
+ estimatedRows: this.estimateRows(),
3470
+ hasSubqueries: this.hasSubqueries(),
3471
+ };
3472
+ }
3473
+
3474
+ /**
3475
+ * Calculate query complexity score
3476
+ * @returns {number} Complexity score
3477
+ */
3478
+ calculateComplexity() {
3479
+ let score = 0;
3480
+
3481
+ // Base complexity
3482
+ score += 1;
3483
+
3484
+ // WHERE conditions
3485
+ score += this.query.where.length * 0.5;
3486
+
3487
+ // JOINs
3488
+ score += this.query.joins.length * 1;
3489
+
3490
+ // GROUP BY
3491
+ score += this.query.groupBy.length * 0.5;
3492
+
3493
+ // HAVING conditions
3494
+ score += this.query.having.length * 0.5;
3495
+
3496
+ // ORDER BY
3497
+ score += this.query.orderBy.length * 0.3;
3498
+
3499
+ // Subqueries
3500
+ if (this.hasSubqueries()) {
3501
+ score += 2;
3502
+ }
3503
+
3504
+ // UNION
3505
+ if (this.hasUnion()) {
3506
+ score += 1;
3507
+ }
3508
+
3509
+ // WITH/CTE
3510
+ if (this.hasWith() || this.hasCte()) {
3511
+ score += 1.5;
3512
+ }
3513
+
3514
+ return Math.round(score * 10) / 10;
3515
+ }
3516
+
3517
+ /**
3518
+ * Estimate number of rows affected/returned
3519
+ * @returns {number|null} Estimated row count
3520
+ */
3521
+ estimateRows() {
3522
+ if (this.query.type === "select") {
3523
+ if (this.query.limit !== null) {
3524
+ return Math.min(this.query.limit, 1000);
3525
+ }
3526
+ return 1000; // Default estimate for SELECT
3527
+ } else if (this.query.type === "insert") {
3528
+ return this.query.data
3529
+ ? Array.isArray(this.query.data)
3530
+ ? this.query.data.length
3531
+ : 1
3532
+ : 1;
3533
+ } else if (this.query.type === "update" || this.query.type === "delete") {
3534
+ return this.query.where.length > 0 ? 10 : null; // Warning: no WHERE clause
3535
+ }
3536
+ return null;
3537
+ }
3538
+
3539
+ /**
3540
+ * Check if query contains subqueries
3541
+ * @returns {boolean} True if contains subqueries
3542
+ */
3543
+ hasSubqueries() {
3544
+ // Check WHERE conditions for subqueries
3545
+ const hasSubqueryInWhere = this.query.where.some(
3546
+ (condition) =>
3547
+ condition.type === "exists" || condition.type === "subquery",
3548
+ );
3549
+
3550
+ // Check HAVING conditions for subqueries
3551
+ const hasSubqueryInHaving = this.query.having.some(
3552
+ (condition) =>
3553
+ condition.type === "exists" || condition.type === "subquery",
3554
+ );
3555
+
3556
+ return (
3557
+ hasSubqueryInWhere || hasSubqueryInHaving || this.query.union.length > 0
3558
+ );
3559
+ }
3560
+
3561
+ /**
3562
+ * Validate query structure
3563
+ * @returns {Object} Validation result
3564
+ */
3565
+ validate() {
3566
+ const errors = [];
3567
+ const warnings = [];
3568
+
3569
+ // Check for DELETE without WHERE clause
3570
+ if (this.query.type === "delete" && this.query.where.length === 0) {
3571
+ warnings.push("DELETE query without WHERE clause may affect all rows");
3572
+ }
3573
+
3574
+ // Check for UPDATE without WHERE clause
3575
+ if (this.query.type === "update" && this.query.where.length === 0) {
3576
+ warnings.push("UPDATE query without WHERE clause may affect all rows");
3577
+ }
3578
+
3579
+ // Check for SELECT * without LIMIT on large tables
3580
+ if (
3581
+ this.query.type === "select" &&
3582
+ this.query.columns.includes("*") &&
3583
+ this.query.limit === null
3584
+ ) {
3585
+ warnings.push("SELECT * without LIMIT may return large result set");
3586
+ }
3587
+
3588
+ // Check for missing JOIN conditions
3589
+ const invalidJoins = this.query.joins.filter(
3590
+ (join) =>
3591
+ join.type !== "cross" &&
3592
+ (!join.first || !join.operator || !join.second),
3593
+ );
3594
+ if (invalidJoins.length > 0) {
3595
+ errors.push("Invalid JOIN conditions detected");
3596
+ }
3597
+
3598
+ // Check for GROUP BY without aggregate functions
3599
+ if (this.query.groupBy.length > 0) {
3600
+ const hasAggregate = this.query.columns.some((col) => {
3601
+ const colStr = typeof col === "string" ? col : "";
3602
+ return (
3603
+ colStr.includes("COUNT(") ||
3604
+ colStr.includes("SUM(") ||
3605
+ colStr.includes("AVG(") ||
3606
+ colStr.includes("MIN(") ||
3607
+ colStr.includes("MAX(")
3608
+ );
3609
+ });
3610
+ if (!hasAggregate) {
3611
+ warnings.push("GROUP BY used without aggregate functions");
3612
+ }
3613
+ }
3614
+
3615
+ // Check dialect-specific limitations
3616
+ if (this.dialect === "sqlite" || this.dialect === "sqlite3") {
3617
+ if (this.query.returning) {
3618
+ errors.push("RETURNING clause not supported in SQLite");
3619
+ }
3620
+ }
3621
+
3622
+ return {
3623
+ valid: errors.length === 0,
3624
+ errors,
3625
+ warnings,
3626
+ complexity: this.calculateComplexity(),
3627
+ estimatedRows: this.estimateRows(),
3628
+ };
3629
+ }
3630
+
3631
+ /**
3632
+ * Explain query execution plan
3633
+ * @returns {Promise<Object>} Explain plan
3634
+ */
3635
+ async explain() {
3636
+ const { sql, bindings } = this.toSQL();
3637
+ const explainSql = `EXPLAIN ${sql}`;
3638
+
3639
+ try {
3640
+ const result = await this.executeQuery(explainSql, bindings);
3641
+ return {
3642
+ sql: explainSql,
3643
+ plan: result,
3644
+ dialect: this.dialect,
3645
+ };
3646
+ } catch (error) {
3647
+ return {
3648
+ sql: explainSql,
3649
+ error: error.message,
3650
+ dialect: this.dialect,
3651
+ };
3652
+ }
3653
+ }
3654
+
3655
+ /**
3656
+ * Analyze query performance
3657
+ * @returns {Promise<Object>} Analysis results
3658
+ */
3659
+ async analyze() {
3660
+ if (
3661
+ this.dialect === "postgresql" ||
3662
+ this.dialect === "postgres" ||
3663
+ this.dialect === "pg"
3664
+ ) {
3665
+ const { sql, bindings } = this.toSQL();
3666
+ const analyzeSql = `EXPLAIN ANALYZE ${sql}`;
3667
+
3668
+ try {
3669
+ const result = await this.executeQuery(analyzeSql, bindings);
3670
+ return {
3671
+ sql: analyzeSql,
3672
+ analysis: result,
3673
+ dialect: this.dialect,
3674
+ };
3675
+ } catch (error) {
3676
+ return {
3677
+ sql: analyzeSql,
3678
+ error: error.message,
3679
+ dialect: this.dialect,
3680
+ };
3681
+ }
3682
+ } else {
3683
+ return {
3684
+ error: "ANALYZE not supported for this dialect",
3685
+ dialect: this.dialect,
3686
+ };
3687
+ }
3688
+ }
3689
+
3690
+ /**
3691
+ * Get query performance metrics
3692
+ * @returns {Object} Performance metrics
3693
+ */
3694
+ getPerformanceMetrics() {
3695
+ const stats = this.getStats();
3696
+ const complexity = this.calculateComplexity();
3697
+ const estimatedRows = this.estimateRows();
3698
+
3699
+ return {
3700
+ complexityScore: complexity,
3701
+ estimatedRows,
3702
+ conditionCount: stats.whereConditions,
3703
+ joinCount: stats.joins,
3704
+ groupByCount: stats.groupBy,
3705
+ orderByCount: stats.orderBy,
3706
+ hasSubqueries: this.hasSubqueries(),
3707
+ hasUnion: stats.unionCount > 0,
3708
+ hasCte: stats.cteCount > 0,
3709
+ isComplex: complexity > 5,
3710
+ needsOptimization: complexity > 8 || stats.whereConditions > 10,
3711
+ };
3712
+ }
3713
+
3714
+ /**
3715
+ * Get query optimization suggestions
3716
+ * @returns {Array} Optimization suggestions
3717
+ */
3718
+ getOptimizationSuggestions() {
3719
+ const suggestions = [];
3720
+ const metrics = this.getPerformanceMetrics();
3721
+
3722
+ if (metrics.complexityScore > 8) {
3723
+ suggestions.push(
3724
+ "Query is complex. Consider breaking it into smaller queries.",
3725
+ );
3726
+ }
3727
+
3728
+ if (metrics.whereConditions > 10) {
3729
+ suggestions.push(
3730
+ "Too many WHERE conditions. Consider using indexes or restructuring the query.",
3731
+ );
3732
+ }
3733
+
3734
+ if (this.query.type === "select" && this.query.columns.includes("*")) {
3735
+ suggestions.push(
3736
+ "Using SELECT * may impact performance. Specify only needed columns.",
3737
+ );
3738
+ }
3739
+
3740
+ if (
3741
+ this.query.type === "select" &&
3742
+ this.query.limit === null &&
3743
+ metrics.estimatedRows > 1000
3744
+ ) {
3745
+ suggestions.push(
3746
+ "Consider adding LIMIT clause to prevent large result sets.",
3747
+ );
3748
+ }
3749
+
3750
+ if (this.query.joins.length > 3) {
3751
+ suggestions.push(
3752
+ "Multiple JOINs detected. Ensure proper indexes exist on join columns.",
3753
+ );
3754
+ }
3755
+
3756
+ if (this.query.groupBy.length > 0 && !this.hasAggregateFunctions()) {
3757
+ suggestions.push(
3758
+ "GROUP BY without aggregate functions may not be necessary.",
3759
+ );
3760
+ }
3761
+
3762
+ if (this.query.orderBy.length > 2) {
3763
+ suggestions.push(
3764
+ "Multiple ORDER BY clauses may impact performance. Consider if all are necessary.",
3765
+ );
3766
+ }
3767
+
3768
+ return suggestions;
3769
+ }
3770
+
3771
+ /**
3772
+ * Check if query has aggregate functions
3773
+ * @returns {boolean} True if has aggregate functions
3774
+ */
3775
+ hasAggregateFunctions() {
3776
+ return this.query.columns.some((col) => {
3777
+ const colStr = typeof col === "string" ? col : "";
3778
+ return (
3779
+ colStr.includes("COUNT(") ||
3780
+ colStr.includes("SUM(") ||
3781
+ colStr.includes("AVG(") ||
3782
+ colStr.includes("MIN(") ||
3783
+ colStr.includes("MAX(")
3784
+ );
3785
+ });
3786
+ }
3787
+
3788
+ /**
3789
+ * Get query execution plan
3790
+ * @returns {Promise<Object>} Execution plan
3791
+ */
3792
+ async getExecutionPlan() {
3793
+ const explain = await this.explain();
3794
+ const analysis = await this.analyze();
3795
+ const metrics = this.getPerformanceMetrics();
3796
+ const suggestions = this.getOptimizationSuggestions();
3797
+
3798
+ return {
3799
+ query: this.getSummary(),
3800
+ explain,
3801
+ analysis,
3802
+ metrics,
3803
+ suggestions,
3804
+ validation: this.validate(),
3805
+ timestamp: new Date().toISOString(),
3806
+ };
3807
+ }
3808
+
3809
+ /**
3810
+ * Reset query to initial state
3811
+ * @returns {QueryBuilder} Query builder instance
3812
+ */
3813
+ reset() {
3814
+ this.query = {
3815
+ type: "select",
3816
+ columns: ["*"],
3817
+ where: [],
3818
+ orderBy: [],
3819
+ limit: null,
3820
+ offset: null,
3821
+ joins: [],
3822
+ groupBy: [],
3823
+ having: [],
3824
+ distinct: false,
3825
+ lock: null,
3826
+ data: null,
3827
+ returning: null,
3828
+ union: [],
3829
+ with: [],
3830
+ cte: [],
3831
+ };
3832
+ this.bindings = [];
3833
+ this.paramIndex = 1;
3834
+ this.subQueries.clear();
3835
+
3836
+ return this;
3837
+ }
3838
+
3839
+ /**
3840
+ * Create new query builder for same table
3841
+ * @returns {QueryBuilder} New query builder instance
3842
+ */
3843
+ newQuery() {
3844
+ return new QueryBuilder(this.tableName, this.connection, this.dialect);
3845
+ }
3846
+
3847
+ /**
3848
+ * Create new query builder for different table
3849
+ * @param {string} tableName - Table name
3850
+ * @returns {QueryBuilder} New query builder instance
3851
+ */
3852
+ table(tableName) {
3853
+ return new QueryBuilder(tableName, this.connection, this.dialect);
3854
+ }
3855
+
3856
+ /**
3857
+ * Log query for debugging
3858
+ * @param {Function} logger - Logger function (default: console.log)
3859
+ * @returns {QueryBuilder} Query builder instance
3860
+ */
3861
+ log(logger = console.log) {
3862
+ const { sql, bindings } = this.toSQL();
3863
+ logger(`SQL: ${sql}`);
3864
+ logger(`Bindings: ${JSON.stringify(bindings)}`);
3865
+ logger(`Dialect: ${this.dialect}`);
3866
+ logger(`Type: ${this.query.type}`);
3867
+ return this;
3868
+ }
3869
+
3870
+ /**
3871
+ * Debug query by logging and returning self
3872
+ * @returns {QueryBuilder} Query builder instance
3873
+ */
3874
+ debug() {
3875
+ return this.log();
3876
+ }
3877
+
3878
+ /**
3879
+ * Get query as JSON for serialization
3880
+ * @returns {Object} JSON representation of query
3881
+ */
3882
+ toJSON() {
3883
+ return {
3884
+ table: this.tableName,
3885
+ dialect: this.dialect,
3886
+ query: this.query,
3887
+ bindings: this.bindings,
3888
+ paramIndex: this.paramIndex,
3889
+ subQueries: Array.from(this.subQueries.entries()),
3890
+ };
3891
+ }
3892
+
3893
+ /**
3894
+ * Create QueryBuilder from JSON
3895
+ * @param {Object} json - JSON representation
3896
+ * @param {Object} connection - Database connection
3897
+ * @returns {QueryBuilder} Query builder instance
3898
+ */
3899
+ static fromJSON(json, connection) {
3900
+ const qb = new QueryBuilder(json.table, connection, json.dialect);
3901
+ qb.query = json.query;
3902
+ qb.bindings = json.bindings;
3903
+ qb.paramIndex = json.paramIndex;
3904
+ qb.subQueries = new Map(json.subQueries);
3905
+ return qb;
3906
+ }
3907
+
3908
+ /**
3909
+ * Execute query with error handling and retry logic
3910
+ * @param {Object} options - Options for execution
3911
+ * @param {number} options.maxRetries - Maximum retry attempts
3912
+ * @param {number} options.retryDelay - Delay between retries in ms
3913
+ * @param {Function} options.onRetry - Callback on retry
3914
+ * @returns {Promise<Object>} Query result
3915
+ */
3916
+ async executeWithRetry(options = {}) {
3917
+ const maxRetries = options.maxRetries || 3;
3918
+ const retryDelay = options.retryDelay || 1000;
3919
+ const onRetry = options.onRetry || (() => {});
3920
+
3921
+ let lastError;
3922
+
3923
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
3924
+ try {
3925
+ return await this.execute();
3926
+ } catch (error) {
3927
+ lastError = error;
3928
+
3929
+ // Check if error is retryable
3930
+ const isRetryable = this.isRetryableError(error);
3931
+ if (!isRetryable || attempt === maxRetries) {
3932
+ throw error;
3933
+ }
3934
+
3935
+ onRetry(attempt, error, retryDelay);
3936
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
3937
+
3938
+ // Exponential backoff
3939
+ retryDelay *= 2;
3940
+ }
3941
+ }
3942
+
3943
+ throw lastError;
3944
+ }
3945
+
3946
+ /**
3947
+ * Check if error is retryable
3948
+ * @param {Error} error - Error to check
3949
+ * @returns {boolean} True if error is retryable
3950
+ */
3951
+ isRetryableError(error) {
3952
+ const retryableMessages = [
3953
+ "deadlock",
3954
+ "timeout",
3955
+ "connection",
3956
+ "lock",
3957
+ "busy",
3958
+ "try again",
3959
+ "retry",
3960
+ ];
3961
+
3962
+ const errorMessage = error.message.toLowerCase();
3963
+ return retryableMessages.some((msg) => errorMessage.includes(msg));
3964
+ }
3965
+
3966
+ /**
3967
+ * Execute query with timeout
3968
+ * @param {number} timeout - Timeout in milliseconds
3969
+ * @returns {Promise<Object>} Query result
3970
+ */
3971
+ async executeWithTimeout(timeout = 5000) {
3972
+ return Promise.race([
3973
+ this.execute(),
3974
+ new Promise((_, reject) => {
3975
+ setTimeout(
3976
+ () => reject(new Error(`Query timeout after ${timeout}ms`)),
3977
+ timeout,
3978
+ );
3979
+ }),
3980
+ ]);
3981
+ }
3982
+
3983
+ /**
3984
+ * Execute query in transaction
3985
+ * @param {Function} callback - Transaction callback
3986
+ * @returns {Promise<Object>} Query result
3987
+ */
3988
+ async executeInTransaction(callback) {
3989
+ if (!this.connection.beginTransaction) {
3990
+ throw new Error("Database connection does not support transactions");
3991
+ }
3992
+
3993
+ await this.connection.beginTransaction();
3994
+
3995
+ try {
3996
+ const result = await this.execute();
3997
+ if (callback) {
3998
+ await callback(result);
3999
+ }
4000
+ await this.connection.commit();
4001
+ return result;
4002
+ } catch (error) {
4003
+ await this.connection.rollback();
4004
+ throw error;
4005
+ }
4006
+ }
4007
+
4008
+ /**
4009
+ * Execute query with profiling
4010
+ * @returns {Promise<Object>} Query result with profiling info
4011
+ */
4012
+ async executeWithProfiling() {
4013
+ const startTime = Date.now();
4014
+ const startMemory = process.memoryUsage();
4015
+
4016
+ const { sql, bindings } = this.toSQL();
4017
+
4018
+ try {
4019
+ const result = await this.execute();
4020
+ const endTime = Date.now();
4021
+ const endMemory = process.memoryUsage();
4022
+
4023
+ return {
4024
+ result,
4025
+ profile: {
4026
+ sql,
4027
+ bindings,
4028
+ timing: {
4029
+ startTime,
4030
+ endTime,
4031
+ duration: endTime - startTime,
4032
+ },
4033
+ memory: {
4034
+ start: startMemory,
4035
+ end: endMemory,
4036
+ diff: {
4037
+ rss: endMemory.rss - startMemory.rss,
4038
+ heapTotal: endMemory.heapTotal - startMemory.heapTotal,
4039
+ heapUsed: endMemory.heapUsed - startMemory.heapUsed,
4040
+ external: endMemory.external - startMemory.external,
4041
+ },
4042
+ },
4043
+ },
4044
+ };
4045
+ } catch (error) {
4046
+ const endTime = Date.now();
4047
+ throw {
4048
+ error,
4049
+ profile: {
4050
+ sql,
4051
+ bindings,
4052
+ timing: {
4053
+ startTime,
4054
+ endTime,
4055
+ duration: endTime - startTime,
4056
+ },
4057
+ },
4058
+ };
4059
+ }
4060
+ }
4061
+ }
4062
+ /**
4063
+ * Dialect adapter for handling database-specific SQL syntax
4064
+ */
4065
+ class DialectAdapter {
4066
+ constructor(dialect) {
4067
+ this.dialect = dialect.toLowerCase();
4068
+ }
4069
+
4070
+ /**
4071
+ * Quote identifier based on dialect
4072
+ * @param {string} identifier - Identifier to quote
4073
+ * @returns {string} Quoted identifier
4074
+ */
4075
+ quoteIdentifier(identifier) {
4076
+ // Handle raw expressions or already quoted identifiers
4077
+ if (
4078
+ identifier.includes("(") ||
4079
+ identifier.includes(")") ||
4080
+ identifier.includes(" as ") ||
4081
+ identifier.includes("`") ||
4082
+ identifier.includes('"') ||
4083
+ identifier.includes("[") ||
4084
+ identifier.includes("]")
4085
+ ) {
4086
+ return identifier;
4087
+ }
4088
+
4089
+ // Handle qualified identifiers
4090
+ if (identifier.includes(".")) {
4091
+ return identifier
4092
+ .split(".")
4093
+ .map((part) => this._quoteIdentifierPart(part))
4094
+ .join(".");
4095
+ }
4096
+
4097
+ return this._quoteIdentifierPart(identifier);
4098
+ }
4099
+
4100
+ /**
4101
+ * Internal method to quote a single identifier part
4102
+ * @param {string} identifier - Identifier part to quote
4103
+ * @returns {string} Quoted identifier part
4104
+ * @private
4105
+ */
4106
+ _quoteIdentifierPart(identifier) {
4107
+ identifier = identifier.trim();
4108
+
4109
+ // Return if already quoted
4110
+ if (
4111
+ (identifier.startsWith("`") && identifier.endsWith("`")) ||
4112
+ (identifier.startsWith('"') && identifier.endsWith('"')) ||
4113
+ (identifier.startsWith("[") && identifier.endsWith("]"))
4114
+ ) {
4115
+ return identifier;
4116
+ }
4117
+
4118
+ switch (this.dialect) {
4119
+ case "mysql":
4120
+ case "mariadb":
4121
+ case "clickhouse":
4122
+ return `\`${identifier}\``;
4123
+
4124
+ case "postgresql":
4125
+ case "postgres":
4126
+ case "pg":
4127
+ case "cockroachdb":
4128
+ case "cockroach":
4129
+ case "sqlite":
4130
+ case "sqlite3":
4131
+ return `"${identifier}"`;
4132
+
4133
+ case "mssql":
4134
+ case "sqlserver":
4135
+ return `[${identifier}]`;
4136
+
4137
+ case "oracle":
4138
+ return `"${identifier.toUpperCase()}"`;
4139
+
4140
+ default:
4141
+ console.warn(
4142
+ `Unsupported dialect: ${this.dialect}, returning unquoted identifier`,
4143
+ );
4144
+ return identifier;
4145
+ }
4146
+ }
4147
+
4148
+ /**
4149
+ * Get parameter placeholder for the current dialect
4150
+ * @param {number} index - Parameter index (1-based)
4151
+ * @returns {string} Parameter placeholder
4152
+ */
4153
+ getParameterPlaceholder(index) {
4154
+ switch (this.dialect) {
4155
+ case "postgresql":
4156
+ case "postgres":
4157
+ case "pg":
4158
+ case "cockroachdb":
4159
+ case "cockroach":
4160
+ return `$${index}`;
4161
+ case "mssql":
4162
+ case "sqlserver":
4163
+ return `@p${index}`;
4164
+ case "oracle":
4165
+ return `:p${index}`;
4166
+ default:
4167
+ return "?";
4168
+ }
4169
+ }
4170
+
4171
+ /**
4172
+ * Build LIMIT/OFFSET clause with dialect-specific syntax
4173
+ * @param {number|null} limit - Limit value
4174
+ * @param {number|null} offset - Offset value
4175
+ * @returns {string} LIMIT/OFFSET clause
4176
+ */
4177
+ buildLimitOffset(limit, offset) {
4178
+ let clause = "";
4179
+
4180
+ if (this.dialect === "mssql" || this.dialect === "sqlserver") {
4181
+ // SQL Server uses OFFSET/FETCH syntax
4182
+ if (offset !== null) {
4183
+ clause += ` OFFSET ${offset} ROWS`;
4184
+ }
4185
+ if (limit !== null) {
4186
+ if (offset !== null) {
4187
+ clause += ` FETCH NEXT ${limit} ROWS ONLY`;
4188
+ } else {
4189
+ clause += ` FETCH FIRST ${limit} ROWS ONLY`;
4190
+ }
4191
+ }
4192
+ } else if (this.dialect === "oracle") {
4193
+ // Oracle uses FETCH FIRST syntax
4194
+ if (limit !== null) {
4195
+ const limitValue = limit;
4196
+ const offsetValue = offset || 0;
4197
+ if (offsetValue > 0) {
4198
+ clause = ` OFFSET ${offsetValue} ROWS FETCH NEXT ${limitValue} ROWS ONLY`;
4199
+ } else {
4200
+ clause = ` FETCH FIRST ${limitValue} ROWS ONLY`;
4201
+ }
4202
+ }
4203
+ } else {
4204
+ // Standard SQL syntax for MySQL, PostgreSQL, SQLite, etc.
4205
+ if (limit !== null) {
4206
+ clause += ` LIMIT ${limit}`;
4207
+ }
4208
+ if (offset !== null) {
4209
+ clause += ` OFFSET ${offset}`;
4210
+ }
4211
+ }
4212
+
4213
+ return clause;
4214
+ }
4215
+
4216
+ /**
4217
+ * Get random function for the current dialect
4218
+ * @returns {string} Random function name
4219
+ */
4220
+ getRandomFunction() {
4221
+ switch (this.dialect) {
4222
+ case "mysql":
4223
+ case "mariadb":
4224
+ return "RAND()";
4225
+ case "postgresql":
4226
+ case "postgres":
4227
+ case "pg":
4228
+ case "cockroachdb":
4229
+ case "cockroach":
4230
+ case "sqlite":
4231
+ case "sqlite3":
4232
+ return "RANDOM()";
4233
+ case "mssql":
4234
+ case "sqlserver":
4235
+ return "NEWID()";
4236
+ case "oracle":
4237
+ return "DBMS_RANDOM.VALUE";
4238
+ case "clickhouse":
4239
+ return "rand()";
4240
+ default:
4241
+ return "RAND()";
4242
+ }
4243
+ }
4244
+
4245
+ /**
4246
+ * Check if RETURNING clause is supported
4247
+ * @returns {boolean} True if RETURNING is supported
4248
+ */
4249
+ supportsReturning() {
4250
+ return [
4251
+ "postgresql",
4252
+ "postgres",
4253
+ "pg",
4254
+ "cockroachdb",
4255
+ "cockroach",
4256
+ "oracle",
4257
+ ].includes(this.dialect);
4258
+ }
4259
+
4260
+ /**
4261
+ * Check if WITH clause (CTE) is supported
4262
+ * @returns {boolean} True if WITH clause is supported
4263
+ */
4264
+ supportsWithClause() {
4265
+ return [
4266
+ "postgresql",
4267
+ "postgres",
4268
+ "pg",
4269
+ "cockroachdb",
4270
+ "cockroach",
4271
+ "mssql",
4272
+ "sqlserver",
4273
+ "oracle",
4274
+ ].includes(this.dialect);
4275
+ }
4276
+
4277
+ /**
4278
+ * Get current timestamp function
4279
+ * @returns {string} Current timestamp function
4280
+ */
4281
+ getCurrentTimestamp() {
4282
+ switch (this.dialect) {
4283
+ case "mysql":
4284
+ case "mariadb":
4285
+ return "NOW()";
4286
+ case "postgresql":
4287
+ case "postgres":
4288
+ case "pg":
4289
+ case "cockroachdb":
4290
+ case "cockroach":
4291
+ return "CURRENT_TIMESTAMP";
4292
+ case "sqlite":
4293
+ case "sqlite3":
4294
+ return "CURRENT_TIMESTAMP";
4295
+ case "mssql":
4296
+ case "sqlserver":
4297
+ return "GETDATE()";
4298
+ case "oracle":
4299
+ return "SYSDATE";
4300
+ case "clickhouse":
4301
+ return "now()";
4302
+ default:
4303
+ return "NOW()";
4304
+ }
4305
+ }
4306
+
4307
+ /**
4308
+ * Get auto-increment keyword
4309
+ * @returns {string} Auto-increment keyword
4310
+ */
4311
+ getAutoIncrementKeyword() {
4312
+ switch (this.dialect) {
4313
+ case "mysql":
4314
+ case "mariadb":
4315
+ return "AUTO_INCREMENT";
4316
+ case "postgresql":
4317
+ case "postgres":
4318
+ case "pg":
4319
+ case "cockroachdb":
4320
+ case "cockroach":
4321
+ return "SERIAL";
4322
+ case "sqlite":
4323
+ case "sqlite3":
4324
+ return "AUTOINCREMENT";
4325
+ case "mssql":
4326
+ case "sqlserver":
4327
+ return "IDENTITY(1,1)";
4328
+ case "oracle":
4329
+ return "GENERATED BY DEFAULT AS IDENTITY";
4330
+ default:
4331
+ return "AUTO_INCREMENT";
4332
+ }
4333
+ }
4334
+
4335
+ /**
4336
+ * Get boolean true value
4337
+ * @returns {string} Boolean true value
4338
+ */
4339
+ getBooleanTrue() {
4340
+ switch (this.dialect) {
4341
+ case "mysql":
4342
+ case "mariadb":
4343
+ case "sqlite":
4344
+ case "sqlite3":
4345
+ return "1";
4346
+ case "postgresql":
4347
+ case "postgres":
4348
+ case "pg":
4349
+ case "cockroachdb":
4350
+ case "cockroach":
4351
+ return "TRUE";
4352
+ case "mssql":
4353
+ case "sqlserver":
4354
+ return "1";
4355
+ case "oracle":
4356
+ return "1";
4357
+ case "clickhouse":
4358
+ return "1";
4359
+ default:
4360
+ return "1";
4361
+ }
4362
+ }
4363
+
4364
+ /**
4365
+ * Get boolean false value
4366
+ * @returns {string} Boolean false value
4367
+ */
4368
+ getBooleanFalse() {
4369
+ switch (this.dialect) {
4370
+ case "mysql":
4371
+ case "mariadb":
4372
+ case "sqlite":
4373
+ case "sqlite3":
4374
+ return "0";
4375
+ case "postgresql":
4376
+ case "postgres":
4377
+ case "pg":
4378
+ case "cockroachdb":
4379
+ case "cockroach":
4380
+ return "FALSE";
4381
+ case "mssql":
4382
+ case "sqlserver":
4383
+ return "0";
4384
+ case "oracle":
4385
+ return "0";
4386
+ case "clickhouse":
4387
+ return "0";
4388
+ default:
4389
+ return "0";
4390
+ }
4391
+ }
4392
+ }
4393
+
4394
+ export default QueryBuilder;