jsql-neo 1.3.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * JSQL Date Types — DATE / DATETIME / TIMESTAMP 类型处理
4
+ */
5
+
6
+ const { createError } = require('./errors');
7
+
8
+ /**
9
+ * 验证并规范化日期类型值
10
+ * @param {*} value - 输入值
11
+ * @param {string} type - 'date' | 'datetime' | 'timestamp'
12
+ * @param {string} fieldName - 字段名(用于错误提示)
13
+ * @returns {*} 规范化后的值
14
+ */
15
+ function validateDateType(value, type, fieldName) {
16
+ if (value === null || value === undefined) return value;
17
+
18
+ switch (type) {
19
+ case 'date':
20
+ return validateDate(value, fieldName);
21
+ case 'datetime':
22
+ return validateDateTime(value, fieldName);
23
+ case 'timestamp':
24
+ return validateTimestamp(value, fieldName);
25
+ case 'time':
26
+ return validateTime(value, fieldName);
27
+ default:
28
+ return value;
29
+ }
30
+ }
31
+
32
+ // ============================================================
33
+ // DATE: YYYY-MM-DD
34
+ // ============================================================
35
+
36
+ function validateDate(value, fieldName) {
37
+ if (value instanceof Date) {
38
+ return value.toISOString().slice(0, 10);
39
+ }
40
+ if (typeof value === 'number') {
41
+ // Unix timestamp → date
42
+ return new Date(value * 1000).toISOString().slice(0, 10);
43
+ }
44
+ if (typeof value === 'string') {
45
+ const d = parseDateString(value);
46
+ if (d) return d.toISOString().slice(0, 10);
47
+ }
48
+ throw createError('ER_INVALID_DATE', fieldName, String(value));
49
+ }
50
+
51
+ function parseDateString(str) {
52
+ // YYYY-MM-DD
53
+ let m = str.match(/^(\d{4})-(\d{2})-(\d{2})$/);
54
+ if (m) {
55
+ const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
56
+ if (d.getFullYear() === parseInt(m[1])) return d;
57
+ }
58
+ // YYYY/MM/DD
59
+ m = str.match(/^(\d{4})\/(\d{2})\/(\d{2})$/);
60
+ if (m) {
61
+ const d = new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]));
62
+ if (d.getFullYear() === parseInt(m[1])) return d;
63
+ }
64
+ // MM/DD/YYYY
65
+ m = str.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
66
+ if (m) {
67
+ const d = new Date(parseInt(m[3]), parseInt(m[1]) - 1, parseInt(m[2]));
68
+ if (d.getFullYear() === parseInt(m[3])) return d;
69
+ }
70
+ // Try native parse
71
+ const d = new Date(str);
72
+ if (!isNaN(d.getTime())) return d;
73
+ return null;
74
+ }
75
+
76
+ // ============================================================
77
+ // DATETIME: YYYY-MM-DD HH:MM:SS
78
+ // ============================================================
79
+
80
+ function validateDateTime(value, fieldName) {
81
+ if (value instanceof Date) {
82
+ return value.toISOString().replace('T', ' ').slice(0, 19);
83
+ }
84
+ if (typeof value === 'number') {
85
+ return new Date(value * 1000).toISOString().replace('T', ' ').slice(0, 19);
86
+ }
87
+ if (typeof value === 'string') {
88
+ // ISO format: 2024-01-15T10:30:00.000Z
89
+ const isoMatch = value.match(/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
90
+ if (isoMatch) {
91
+ return isoMatch[1] + ' ' + isoMatch[2];
92
+ }
93
+ // MySQL format: YYYY-MM-DD HH:MM:SS
94
+ const myMatch = value.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})$/);
95
+ if (myMatch) {
96
+ const d = new Date(value.replace(' ', 'T') + 'Z');
97
+ if (!isNaN(d.getTime())) return value;
98
+ }
99
+ const d = new Date(value);
100
+ if (!isNaN(d.getTime())) {
101
+ return d.toISOString().replace('T', ' ').slice(0, 19);
102
+ }
103
+ }
104
+ throw createError('ER_INVALID_DATE', fieldName, String(value));
105
+ }
106
+
107
+ // ============================================================
108
+ // TIMESTAMP: Unix 时间戳(秒)
109
+ // ============================================================
110
+
111
+ function validateTimestamp(value, fieldName) {
112
+ if (value instanceof Date) {
113
+ return Math.floor(value.getTime() / 1000);
114
+ }
115
+ if (typeof value === 'number') {
116
+ // 自动检测毫秒时间戳
117
+ if (value > 1e12) return Math.floor(value / 1000);
118
+ return value;
119
+ }
120
+ if (typeof value === 'string') {
121
+ const d = new Date(value);
122
+ if (!isNaN(d.getTime())) {
123
+ return Math.floor(d.getTime() / 1000);
124
+ }
125
+ const n = parseInt(value, 10);
126
+ if (!isNaN(n)) {
127
+ if (n > 1e12) return Math.floor(n / 1000);
128
+ return n;
129
+ }
130
+ }
131
+ throw createError('ER_INVALID_DATE', fieldName, String(value));
132
+ }
133
+
134
+ // ============================================================
135
+ // TIME: HH:MM:SS
136
+ // ============================================================
137
+
138
+ function validateTime(value, fieldName) {
139
+ if (typeof value === 'string') {
140
+ const m = value.match(/^(\d{2}):(\d{2}):(\d{2})$/);
141
+ if (m) {
142
+ const h = parseInt(m[1]), min = parseInt(m[2]), sec = parseInt(m[3]);
143
+ if (h >= 0 && h < 24 && min >= 0 && min < 60 && sec >= 0 && sec < 60) {
144
+ return value;
145
+ }
146
+ }
147
+ // 负时间: -HH:MM:SS
148
+ const neg = value.match(/^-(\d{2}):(\d{2}):(\d{2})$/);
149
+ if (neg) return value;
150
+ }
151
+ throw createError('ER_INVALID_DATE', fieldName, String(value));
152
+ }
153
+
154
+ // ============================================================
155
+ // 类型转换辅助
156
+ // ============================================================
157
+
158
+ function now(type = 'datetime') {
159
+ const d = new Date();
160
+ switch (type) {
161
+ case 'date': return d.toISOString().slice(0, 10);
162
+ case 'timestamp': return Math.floor(d.getTime() / 1000);
163
+ case 'time': return d.toISOString().slice(11, 19);
164
+ default: return d.toISOString().replace('T', ' ').slice(0, 19);
165
+ }
166
+ }
167
+
168
+ function formatDate(value, format = 'YYYY-MM-DD') {
169
+ if (!value) return value;
170
+ const d = value instanceof Date ? value : new Date(value);
171
+ if (isNaN(d.getTime())) return String(value);
172
+
173
+ const pad = n => String(n).padStart(2, '0');
174
+ return format
175
+ .replace('YYYY', d.getFullYear())
176
+ .replace('MM', pad(d.getMonth() + 1))
177
+ .replace('DD', pad(d.getDate()))
178
+ .replace('HH', pad(d.getHours()))
179
+ .replace('mm', pad(d.getMinutes()))
180
+ .replace('ss', pad(d.getSeconds()));
181
+ }
182
+
183
+ module.exports = { validateDateType, now, formatDate };
package/lib/errors.js ADDED
@@ -0,0 +1,84 @@
1
+ // © Vexify 2026 All Rights Reserved.
2
+ /**
3
+ * JSQL Error Codes — MySQL 风格错误码体系
4
+ */
5
+
6
+ class JSQL_Error extends Error {
7
+ constructor(code, message, details = {}) {
8
+ super(message);
9
+ this.name = 'JSQL_Error';
10
+ this.code = code;
11
+ this.details = details;
12
+ this.timestamp = new Date().toISOString();
13
+ }
14
+
15
+ toJSON() {
16
+ return {
17
+ error: true,
18
+ code: this.code,
19
+ message: this.message,
20
+ details: this.details,
21
+ timestamp: this.timestamp
22
+ };
23
+ }
24
+ }
25
+
26
+ // MySQL 兼容错误码
27
+ const ErrorCodes = {
28
+ // 表/库操作
29
+ ER_TABLE_EXISTS: { code: 1050, msg: "Table '%s' already exists" },
30
+ ER_NO_SUCH_TABLE: { code: 1146, msg: "Table '%s' doesn't exist" },
31
+ ER_DB_CREATE_EXISTS: { code: 1007, msg: "Can't create database '%s'; database exists" },
32
+
33
+ // 列操作
34
+ ER_DUP_FIELDNAME: { code: 1060, msg: "Duplicate column name '%s'" },
35
+ ER_BAD_FIELD_ERROR: { code: 1054, msg: "Unknown column '%s' in '%s'" },
36
+ ER_CANT_DROP_FIELD: { code: 1091, msg: "Can't DROP '%s'; check that column exists" },
37
+
38
+ // 约束
39
+ ER_DUP_ENTRY: { code: 1062, msg: "Duplicate entry '%s' for key '%s'" },
40
+ ER_NO_DEFAULT_FOR_FIELD: { code: 1364, msg: "Field '%s' doesn't have a default value" },
41
+ ER_BAD_NULL_ERROR: { code: 1048, msg: "Column '%s' cannot be null" },
42
+ ER_CHECK_CONSTRAINT: { code: 3819, msg: "Check constraint '%s' is violated" },
43
+ ER_NO_REFERENCED_ROW: { code: 1216, msg: "Cannot add or update: foreign key constraint fails" },
44
+ ER_ROW_IS_REFERENCED: { code: 1217, msg: "Cannot delete or update: a foreign key constraint fails" },
45
+
46
+ // 数据类型
47
+ ER_TRUNCATED_WRONG_VALUE: { code: 1292, msg: "Truncated incorrect %s value: '%s'" },
48
+ ER_INVALID_DATE: { code: 1292, msg: "Incorrect date value: '%s'" },
49
+ ER_DATA_TOO_LONG: { code: 1406, msg: "Data too long for column '%s'" },
50
+ ER_OUT_OF_RANGE: { code: 1264, msg: "Out of range value for column '%s'" },
51
+
52
+ // 事务
53
+ ER_TRANSACTION_ACTIVE: { code: 1568, msg: "Transaction already in progress" },
54
+ ER_NO_TRANSACTION: { code: 1569, msg: "No transaction in progress" },
55
+
56
+ // 其他
57
+ ER_NOT_SUPPORTED: { code: 1235, msg: "This version doesn't yet support '%s'" },
58
+ ER_PARSE_ERROR: { code: 1064, msg: "You have an error in your syntax: %s" },
59
+ ER_LOCK_WAIT_TIMEOUT: { code: 1205, msg: "Lock wait timeout exceeded; try restarting transaction" },
60
+ ER_FILE_NOT_FOUND: { code: 1017, msg: "Can't find file: '%s'" },
61
+ ER_VIEW_EXISTS: { code: 1050, msg: "View '%s' already exists" },
62
+ ER_TRIGGER_EXISTS: { code: 1359, msg: "Trigger '%s' already exists" },
63
+ ER_TRIGGER_NOT_FOUND: { code: 1360, msg: "Trigger '%s' does not exist" },
64
+ ER_PLUGIN_ERR: { code: 1125, msg: "Plugin error: %s" },
65
+ };
66
+
67
+ /**
68
+ * 创建错误
69
+ */
70
+ function createError(codeKey, ...args) {
71
+ const template = ErrorCodes[codeKey];
72
+ if (!template) {
73
+ return new JSQL_Error('UNKNOWN', `Unknown error: ${codeKey}`, { codeKey, args });
74
+ }
75
+
76
+ let msg = template.msg;
77
+ for (let i = 0; i < args.length; i++) {
78
+ msg = msg.replace('%s', String(args[i]));
79
+ }
80
+
81
+ return new JSQL_Error(template.code, msg, { args });
82
+ }
83
+
84
+ module.exports = { JSQL_Error, ErrorCodes, createError };
package/lib/query.js CHANGED
@@ -1,9 +1,11 @@
1
1
  // © Vexify 2026 All Rights Reserved.
2
2
  /**
3
- * JSQL Query — 链式查询构建器
4
- * 支持 JOIN、DISTINCT、HAVING、UNION、窗口函数、缓存、CASE WHEN、toSQL
3
+ * JSQL Query — 链式查询构建器 v2.0
4
+ * 哈希 JOIN 优化、B-Tree 加速、字符串表名、查询优化器
5
5
  */
6
6
 
7
+ const { createError } = require('./errors');
8
+
7
9
  class Query {
8
10
  constructor(table, conditions = {}) {
9
11
  this._table = table;
@@ -22,6 +24,7 @@ class Query {
22
24
  this._window = null;
23
25
  this._cache = null; // { ttl, result, timestamp }
24
26
  this._caseExpr = null; // [{ when, then }, ...] + else
27
+ this._optimizerHints = {}; // { useHashJoin: true }
25
28
  }
26
29
 
27
30
  // ============================================================
@@ -139,9 +142,36 @@ class Query {
139
142
  }
140
143
 
141
144
  // ============================================================
142
- // JOIN
145
+ // 查询优化器提示
146
+ // ============================================================
147
+
148
+ /**
149
+ * 强制使用哈希 JOIN
150
+ */
151
+ useHashJoin() {
152
+ this._optimizerHints.useHashJoin = true;
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * 强制使用嵌套循环 JOIN
158
+ */
159
+ useNestedLoop() {
160
+ this._optimizerHints.useHashJoin = false;
161
+ return this;
162
+ }
163
+
164
+ // ============================================================
165
+ // JOIN(支持字符串表名 + 哈希 JOIN 优化)
143
166
  // ============================================================
144
167
 
168
+ /**
169
+ * INNER JOIN
170
+ * @param {string|Table} otherTable - 表名或 Table 对象
171
+ * @param {string} localField - 本地字段
172
+ * @param {string} foreignField - 外键字段
173
+ * @param {string} as - 别名前缀
174
+ */
145
175
  join(otherTable, localField, foreignField, as) {
146
176
  this._joins.push({ type: 'inner', table: otherTable, localField, foreignField, as });
147
177
  return this;
@@ -200,11 +230,11 @@ class Query {
200
230
  return this._cache.result;
201
231
  }
202
232
 
203
- let rows = this._table._applyFilter(this._table._rows, this._conditions);
233
+ let rows = this._table._applyFilterOptimized(this._table._rows, this._conditions);
204
234
 
205
235
  // JOIN
206
236
  for (const join of this._joins) {
207
- rows = this._applyJoin(rows, join);
237
+ rows = this._applyJoinOptimized(rows, join);
208
238
  }
209
239
 
210
240
  // UNION
@@ -331,8 +361,8 @@ class Query {
331
361
  }
332
362
 
333
363
  groupStats(field) {
334
- if (!this._groupBy) throw new Error('groupStats() requires groupBy()');
335
- const rows = this._table._applyFilter(this._table._rows, this._conditions);
364
+ if (!this._groupBy) throw createError('ER_PARSE_ERROR', 'groupStats() requires groupBy()');
365
+ const rows = this._table._applyFilterOptimized(this._table._rows, this._conditions);
336
366
  const groups = {};
337
367
 
338
368
  for (const row of rows) {
@@ -396,6 +426,12 @@ class Query {
396
426
  parts.push('WHERE', JSON.stringify(this._conditions));
397
427
  }
398
428
 
429
+ for (const join of this._joins) {
430
+ const tableName = typeof join.table === 'string' ? join.table : join.table._name;
431
+ parts.push(join.type.toUpperCase() + ' JOIN', tableName,
432
+ 'ON', `${this._table._name}.${join.localField} = ${tableName}.${join.foreignField}`);
433
+ }
434
+
399
435
  if (this._groupBy) {
400
436
  parts.push('GROUP BY', this._groupBy);
401
437
  if (this._having) parts.push('HAVING', JSON.stringify(this._having));
@@ -421,13 +457,172 @@ class Query {
421
457
  // ============================================================
422
458
 
423
459
  _getFilteredRows() {
424
- return this._table._applyFilter(this._table._rows, this._conditions);
460
+ return this._table._applyFilterOptimized(this._table._rows, this._conditions);
425
461
  }
426
462
 
427
463
  _countRaw() {
428
- return this._table._applyFilter(this._table._rows, this._conditions).length;
464
+ return this._table._applyFilterOptimized(this._table._rows, this._conditions).length;
465
+ }
466
+
467
+ // ============================================================
468
+ // 内部: JOIN 优化
469
+ // ============================================================
470
+
471
+ /**
472
+ * 解析 JOIN 表引用(支持字符串表名和 Table 对象)
473
+ */
474
+ _resolveJoinTable(tableRef) {
475
+ if (typeof tableRef === 'string') {
476
+ const resolved = this._table._db._tables[tableRef];
477
+ if (!resolved) {
478
+ throw createError('ER_NO_SUCH_TABLE', tableRef);
479
+ }
480
+ return resolved;
481
+ }
482
+ return tableRef;
483
+ }
484
+
485
+ /**
486
+ * 优化的 JOIN 执行
487
+ * 自动选择哈希 JOIN 或嵌套循环
488
+ */
489
+ _applyJoinOptimized(localRows, join) {
490
+ const foreignTable = this._resolveJoinTable(join.table);
491
+
492
+ // 如果强制或自动选择哈希 JOIN
493
+ const useHash = this._optimizerHints.useHashJoin !== false
494
+ && (this._optimizerHints.useHashJoin === true
495
+ || localRows.length > 100
496
+ || foreignTable._rows.length > 100);
497
+
498
+ if (useHash) {
499
+ return this._applyHashJoin(localRows, foreignTable, join);
500
+ }
501
+ return this._applyNestedLoopJoin(localRows, foreignTable, join);
502
+ }
503
+
504
+ /**
505
+ * 哈希 JOIN — O(n + m) 时间复杂度
506
+ * 构建外键表的哈希映射,然后一次遍历本地行完成 JOIN
507
+ */
508
+ _applyHashJoin(localRows, foreignTable, join) {
509
+ // 构建哈希表:foreignField -> [foreignRows]
510
+ const hashMap = new Map();
511
+ for (const fr of foreignTable._rows) {
512
+ const key = fr[join.foreignField];
513
+ if (key === undefined || key === null) continue;
514
+ if (!hashMap.has(key)) {
515
+ hashMap.set(key, []);
516
+ }
517
+ hashMap.get(key).push(fr);
518
+ }
519
+
520
+ const result = [];
521
+
522
+ if (join.type === 'left') {
523
+ for (const lr of localRows) {
524
+ const matches = hashMap.get(lr[join.localField]) || [];
525
+ if (matches.length === 0) {
526
+ result.push({ ...lr, ...this._nullRow(foreignTable) });
527
+ } else {
528
+ for (const fr of matches) {
529
+ result.push(this._mergeJoinRow(lr, fr, join));
530
+ }
531
+ }
532
+ }
533
+ } else if (join.type === 'right') {
534
+ for (const fr of foreignTable._rows) {
535
+ const key = fr[join.foreignField];
536
+ const matches = localRows.filter(lr => lr[join.localField] === key);
537
+ if (matches.length === 0) {
538
+ result.push({ ...fr, ...this._nullRow(join, true) });
539
+ } else {
540
+ for (const lr of matches) {
541
+ result.push(this._mergeJoinRow(lr, fr, join));
542
+ }
543
+ }
544
+ }
545
+ } else {
546
+ // INNER JOIN
547
+ for (const lr of localRows) {
548
+ const matches = hashMap.get(lr[join.localField]);
549
+ if (matches) {
550
+ for (const fr of matches) {
551
+ result.push(this._mergeJoinRow(lr, fr, join));
552
+ }
553
+ }
554
+ }
555
+ }
556
+
557
+ return result;
558
+ }
559
+
560
+ /**
561
+ * 嵌套循环 JOIN — O(n * m) 时间复杂度
562
+ * 用于小数据量场景
563
+ */
564
+ _applyNestedLoopJoin(localRows, foreignTable, join) {
565
+ const result = [];
566
+ const foreignRows = foreignTable._rows;
567
+
568
+ if (join.type === 'left') {
569
+ for (const lr of localRows) {
570
+ const matches = foreignRows.filter(fr => fr[join.foreignField] === lr[join.localField]);
571
+ if (matches.length === 0) {
572
+ result.push({ ...lr, ...this._nullRow(foreignTable) });
573
+ } else {
574
+ for (const fr of matches) {
575
+ result.push(this._mergeJoinRow(lr, fr, join));
576
+ }
577
+ }
578
+ }
579
+ } else if (join.type === 'right') {
580
+ for (const fr of foreignRows) {
581
+ const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
582
+ if (matches.length === 0) {
583
+ result.push({ ...fr, ...this._nullRow(join, true) });
584
+ } else {
585
+ for (const lr of matches) {
586
+ result.push(this._mergeJoinRow(lr, fr, join));
587
+ }
588
+ }
589
+ }
590
+ } else {
591
+ // INNER JOIN
592
+ for (const lr of localRows) {
593
+ for (const fr of foreignRows) {
594
+ if (fr[join.foreignField] === lr[join.localField]) {
595
+ result.push(this._mergeJoinRow(lr, fr, join));
596
+ }
597
+ }
598
+ }
599
+ }
600
+ return result;
601
+ }
602
+
603
+ _mergeJoinRow(lr, fr, join) {
604
+ const merged = { ...lr };
605
+ const prefix = join.as ? join.as + '_' : '';
606
+ for (const [key, value] of Object.entries(fr)) {
607
+ merged[prefix + key] = value;
608
+ }
609
+ return merged;
429
610
  }
430
611
 
612
+ _nullRow(tableRef) {
613
+ // tableRef 可能是 join 对象(用于 right join 的本地表)或 Table 对象
614
+ const schema = tableRef._schema || tableRef.table?._schema || {};
615
+ const nulls = {};
616
+ for (const field of Object.keys(schema)) {
617
+ if (field !== '_softDelete') nulls[field] = null;
618
+ }
619
+ return nulls;
620
+ }
621
+
622
+ // ============================================================
623
+ // 内部: 排序
624
+ // ============================================================
625
+
431
626
  _applyOrderBy(rows) {
432
627
  return [...rows].sort((a, b) => {
433
628
  for (const { field, dir } of this._orderBy) {
@@ -440,11 +635,14 @@ class Query {
440
635
  });
441
636
  }
442
637
 
638
+ // ============================================================
639
+ // 内部: 窗口函数
640
+ // ============================================================
641
+
443
642
  _applyWindow(rows) {
444
643
  const { fn, field, partitionBy, dir } = this._window;
445
644
 
446
645
  if (partitionBy) {
447
- // 分组
448
646
  const groups = {};
449
647
  for (const row of rows) {
450
648
  const key = String(row[partitionBy] ?? '__null__');
@@ -465,7 +663,6 @@ class Query {
465
663
  }
466
664
 
467
665
  _applyWindowToGroup(rows, fn, field, dir) {
468
- // 按字段排序
469
666
  if (field) {
470
667
  rows.sort((a, b) => {
471
668
  const va = a[field];
@@ -502,60 +699,9 @@ class Query {
502
699
  }
503
700
  }
504
701
 
505
- _applyJoin(localRows, join) {
506
- const result = [];
507
- const foreignRows = join.table._rows;
508
-
509
- if (join.type === 'left') {
510
- for (const lr of localRows) {
511
- const matches = foreignRows.filter(fr => fr[join.foreignField] === lr[join.localField]);
512
- if (matches.length === 0) {
513
- result.push({ ...lr, ...this._nullRow(join.table) });
514
- } else {
515
- for (const fr of matches) {
516
- result.push(this._mergeJoinRow(lr, fr, join));
517
- }
518
- }
519
- }
520
- } else if (join.type === 'right') {
521
- for (const fr of foreignRows) {
522
- const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
523
- if (matches.length === 0) {
524
- result.push({ ...fr, ...this._nullRow(join.table, true) });
525
- } else {
526
- for (const lr of matches) {
527
- result.push(this._mergeJoinRow(lr, fr, join));
528
- }
529
- }
530
- }
531
- } else {
532
- for (const lr of localRows) {
533
- for (const fr of foreignRows) {
534
- if (fr[join.foreignField] === lr[join.localField]) {
535
- result.push(this._mergeJoinRow(lr, fr, join));
536
- }
537
- }
538
- }
539
- }
540
- return result;
541
- }
542
-
543
- _mergeJoinRow(lr, fr, join) {
544
- const merged = { ...lr };
545
- const prefix = join.as ? join.as + '_' : '';
546
- for (const [key, value] of Object.entries(fr)) {
547
- merged[prefix + key] = value;
548
- }
549
- return merged;
550
- }
551
-
552
- _nullRow(table) {
553
- const nulls = {};
554
- for (const field of Object.keys(table._schema)) {
555
- nulls[field] = null;
556
- }
557
- return nulls;
558
- }
702
+ // ============================================================
703
+ // 内部: GROUP BY
704
+ // ============================================================
559
705
 
560
706
  _applyGroupBy(rows) {
561
707
  const groups = {};
@@ -578,36 +724,71 @@ class Query {
578
724
  return result;
579
725
  }
580
726
 
727
+ // ============================================================
728
+ // 内部: EXPLAIN(增强版)
729
+ // ============================================================
730
+
581
731
  _getExplain() {
582
732
  const totalRows = this._table._rows.length;
583
- const filtered = this._table._applyFilter(this._table._rows, this._conditions);
733
+ const filtered = this._table._applyFilterOptimized(this._table._rows, this._conditions);
584
734
  const plan = {
585
735
  table: this._table._name,
586
736
  totalRows,
587
737
  conditions: this._conditions,
588
738
  filteredRows: filtered.length,
589
739
  selectivity: totalRows > 0 ? (filtered.length / totalRows * 100).toFixed(1) + '%' : '0%',
590
- hasIndex: false,
740
+ hasBTree: false,
741
+ hasHashIndex: false,
591
742
  indexUsed: null,
592
- joins: this._joins.length,
743
+ btreeIndexes: Object.keys(this._table._btrees),
744
+ hashIndexes: Object.keys(this._table._indexes),
745
+ joins: [],
593
746
  groupBy: this._groupBy,
594
747
  having: this._having,
595
748
  window: this._window ? this._window.fn : null,
596
749
  orderBy: this._orderBy.map(o => `${o.field} ${o.dir}`),
597
750
  limit: this._limitCount,
598
751
  offset: this._offsetCount,
599
- estimatedCost: filtered.length * (1 + this._joins.length)
752
+ estimatedCost: filtered.length * (1 + this._joins.length),
753
+ optimizerHints: this._optimizerHints
600
754
  };
601
755
 
756
+ // 检查 B-Tree 使用
602
757
  for (const [field] of Object.entries(this._conditions)) {
758
+ if (this._table._btrees[field]) {
759
+ plan.hasBTree = true;
760
+ plan.indexUsed = field;
761
+ plan.estimatedCost = Math.floor(plan.estimatedCost * 0.05);
762
+ break;
763
+ }
603
764
  if (this._table._indexes[field]) {
604
- plan.hasIndex = true;
765
+ plan.hasHashIndex = true;
605
766
  plan.indexUsed = field;
606
767
  plan.estimatedCost = Math.floor(plan.estimatedCost * 0.1);
607
768
  break;
608
769
  }
609
770
  }
610
771
 
772
+ // JOIN 分析
773
+ for (const join of this._joins) {
774
+ const foreignTable = this._resolveJoinTable(join.table);
775
+ const joinInfo = {
776
+ type: join.type,
777
+ table: foreignTable._name,
778
+ localField: join.localField,
779
+ foreignField: join.foreignField,
780
+ foreignRows: foreignTable._rows.length,
781
+ strategy: (this._optimizerHints.useHashJoin !== false
782
+ && (filtered.length > 100 || foreignTable._rows.length > 100))
783
+ ? 'hash' : 'nested_loop'
784
+ };
785
+ // 检查外键表是否有索引
786
+ if (foreignTable._btrees[join.foreignField]) {
787
+ joinInfo.hasBTree = true;
788
+ }
789
+ plan.joins.push(joinInfo);
790
+ }
791
+
611
792
  return plan;
612
793
  }
613
794
  }