jsql-neo 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/index.js +6 -3
- package/lib/btree.js +326 -0
- package/lib/database.js +286 -43
- package/lib/date-types.js +183 -0
- package/lib/errors.js +84 -0
- package/lib/query.js +251 -70
- package/lib/table.js +281 -175
- package/package.json +7 -3
|
@@ -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
|
-
*
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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
|
|
335
|
-
const rows = this._table.
|
|
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.
|
|
460
|
+
return this._table._applyFilterOptimized(this._table._rows, this._conditions);
|
|
425
461
|
}
|
|
426
462
|
|
|
427
463
|
_countRaw() {
|
|
428
|
-
return this._table.
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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.
|
|
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
|
-
|
|
740
|
+
hasBTree: false,
|
|
741
|
+
hasHashIndex: false,
|
|
591
742
|
indexUsed: null,
|
|
592
|
-
|
|
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.
|
|
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
|
}
|