jsql-neo 1.2.1 → 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 +7 -15
- package/lib/btree.js +326 -0
- package/lib/database.js +572 -22
- package/lib/date-types.js +183 -0
- package/lib/errors.js +84 -0
- package/lib/query.js +412 -51
- package/lib/table.js +320 -142
- package/package.json +7 -3
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;
|
|
@@ -19,6 +21,10 @@ class Query {
|
|
|
19
21
|
this._joins = [];
|
|
20
22
|
this._union = null;
|
|
21
23
|
this._explain = false;
|
|
24
|
+
this._window = null;
|
|
25
|
+
this._cache = null; // { ttl, result, timestamp }
|
|
26
|
+
this._caseExpr = null; // [{ when, then }, ...] + else
|
|
27
|
+
this._optimizerHints = {}; // { useHashJoin: true }
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
// ============================================================
|
|
@@ -57,24 +63,12 @@ class Query {
|
|
|
57
63
|
return this;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
/**
|
|
61
|
-
* 便捷分页
|
|
62
|
-
* @param {number} page - 页码(从 1 开始)
|
|
63
|
-
* @param {number} perPage - 每页数量
|
|
64
|
-
* @returns {{ rows, total, page, perPage, totalPages }}
|
|
65
|
-
*/
|
|
66
66
|
paginate(page = 1, perPage = 20) {
|
|
67
67
|
this._limitCount = perPage;
|
|
68
68
|
this._offsetCount = (page - 1) * perPage;
|
|
69
69
|
const total = this._countRaw();
|
|
70
70
|
const rows = this.get();
|
|
71
|
-
return {
|
|
72
|
-
rows,
|
|
73
|
-
total,
|
|
74
|
-
page,
|
|
75
|
-
perPage,
|
|
76
|
-
totalPages: Math.ceil(total / perPage)
|
|
77
|
-
};
|
|
71
|
+
return { rows, total, page, perPage, totalPages: Math.ceil(total / perPage) };
|
|
78
72
|
}
|
|
79
73
|
|
|
80
74
|
// ============================================================
|
|
@@ -92,6 +86,31 @@ class Query {
|
|
|
92
86
|
return this;
|
|
93
87
|
}
|
|
94
88
|
|
|
89
|
+
// ============================================================
|
|
90
|
+
// CASE WHEN
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* CASE WHEN 表达式
|
|
95
|
+
* @example
|
|
96
|
+
* db.users.where({}).case([
|
|
97
|
+
* { when: { age: { $lt: 18 } }, then: 'minor' },
|
|
98
|
+
* { when: { age: { $lt: 60 } }, then: 'adult' }
|
|
99
|
+
* ], 'senior').as('ageGroup').get()
|
|
100
|
+
*/
|
|
101
|
+
case(whenClauses, elseExpr) {
|
|
102
|
+
this._caseExpr = { clauses: whenClauses, else: elseExpr };
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 给 CASE WHEN 结果命名
|
|
108
|
+
*/
|
|
109
|
+
as(fieldName) {
|
|
110
|
+
this._caseAs = fieldName;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
95
114
|
// ============================================================
|
|
96
115
|
// 分组 & 聚合后过滤
|
|
97
116
|
// ============================================================
|
|
@@ -107,15 +126,51 @@ class Query {
|
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
// ============================================================
|
|
110
|
-
//
|
|
129
|
+
// 窗口函数
|
|
130
|
+
// ============================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 窗口函数
|
|
134
|
+
* @param {string} fn - 'rowNumber' | 'rank' | 'denseRank'
|
|
135
|
+
* @param {string} field - 排序字段
|
|
136
|
+
* @param {string} partitionBy - 分区字段
|
|
137
|
+
* @param {string} dir - 'asc' | 'desc'
|
|
138
|
+
*/
|
|
139
|
+
window(fn, field, partitionBy, dir = 'asc') {
|
|
140
|
+
this._window = { fn, field, partitionBy, dir };
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================
|
|
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 优化)
|
|
111
166
|
// ============================================================
|
|
112
167
|
|
|
113
168
|
/**
|
|
114
169
|
* INNER JOIN
|
|
115
|
-
* @param {Table} otherTable -
|
|
170
|
+
* @param {string|Table} otherTable - 表名或 Table 对象
|
|
116
171
|
* @param {string} localField - 本地字段
|
|
117
|
-
* @param {string} foreignField -
|
|
118
|
-
* @param {string} as -
|
|
172
|
+
* @param {string} foreignField - 外键字段
|
|
173
|
+
* @param {string} as - 别名前缀
|
|
119
174
|
*/
|
|
120
175
|
join(otherTable, localField, foreignField, as) {
|
|
121
176
|
this._joins.push({ type: 'inner', table: otherTable, localField, foreignField, as });
|
|
@@ -150,6 +205,19 @@ class Query {
|
|
|
150
205
|
return this;
|
|
151
206
|
}
|
|
152
207
|
|
|
208
|
+
// ============================================================
|
|
209
|
+
// 缓存
|
|
210
|
+
// ============================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 缓存查询结果
|
|
214
|
+
* @param {number} ttlMs - 缓存有效期(毫秒),默认 60000
|
|
215
|
+
*/
|
|
216
|
+
cache(ttlMs = 60000) {
|
|
217
|
+
this._cache = { ttl: ttlMs, result: null, timestamp: 0 };
|
|
218
|
+
return this;
|
|
219
|
+
}
|
|
220
|
+
|
|
153
221
|
// ============================================================
|
|
154
222
|
// 执行
|
|
155
223
|
// ============================================================
|
|
@@ -157,11 +225,16 @@ class Query {
|
|
|
157
225
|
get() {
|
|
158
226
|
if (this._explain) return this._getExplain();
|
|
159
227
|
|
|
160
|
-
|
|
228
|
+
// 缓存命中
|
|
229
|
+
if (this._cache && this._cache.result && (Date.now() - this._cache.timestamp < this._cache.ttl)) {
|
|
230
|
+
return this._cache.result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let rows = this._table._applyFilterOptimized(this._table._rows, this._conditions);
|
|
161
234
|
|
|
162
235
|
// JOIN
|
|
163
236
|
for (const join of this._joins) {
|
|
164
|
-
rows = this.
|
|
237
|
+
rows = this._applyJoinOptimized(rows, join);
|
|
165
238
|
}
|
|
166
239
|
|
|
167
240
|
// UNION
|
|
@@ -180,12 +253,17 @@ class Query {
|
|
|
180
253
|
rows = this._table._applyFilter(rows, this._having);
|
|
181
254
|
}
|
|
182
255
|
|
|
256
|
+
// 窗口函数
|
|
257
|
+
if (this._window) {
|
|
258
|
+
rows = this._applyWindow(rows);
|
|
259
|
+
}
|
|
260
|
+
|
|
183
261
|
// DISTINCT
|
|
184
262
|
if (this._distinct && this._distinctField) {
|
|
185
263
|
const seen = new Set();
|
|
186
264
|
rows = rows.filter(r => {
|
|
187
265
|
const val = r[this._distinctField];
|
|
188
|
-
if (val === undefined) return false;
|
|
266
|
+
if (val === undefined) return false;
|
|
189
267
|
const key = String(val);
|
|
190
268
|
if (seen.has(key)) return false;
|
|
191
269
|
seen.add(key);
|
|
@@ -206,6 +284,23 @@ class Query {
|
|
|
206
284
|
rows = rows.slice(0, this._limitCount);
|
|
207
285
|
}
|
|
208
286
|
|
|
287
|
+
// CASE WHEN
|
|
288
|
+
if (this._caseExpr) {
|
|
289
|
+
rows = rows.map(r => {
|
|
290
|
+
const row = { ...r };
|
|
291
|
+
let val = this._caseExpr.else;
|
|
292
|
+
for (const clause of this._caseExpr.clauses) {
|
|
293
|
+
if (this._table._matchRow(row, clause.when)) {
|
|
294
|
+
val = clause.then;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const asField = this._caseAs || '_case';
|
|
299
|
+
row[asField] = val;
|
|
300
|
+
return row;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
209
304
|
// 字段选择
|
|
210
305
|
rows = rows.map(r => ({ ...r }));
|
|
211
306
|
if (this._selectFields) {
|
|
@@ -218,6 +313,12 @@ class Query {
|
|
|
218
313
|
});
|
|
219
314
|
}
|
|
220
315
|
|
|
316
|
+
// 缓存存储
|
|
317
|
+
if (this._cache) {
|
|
318
|
+
this._cache.result = rows;
|
|
319
|
+
this._cache.timestamp = Date.now();
|
|
320
|
+
}
|
|
321
|
+
|
|
221
322
|
return rows;
|
|
222
323
|
}
|
|
223
324
|
|
|
@@ -260,8 +361,8 @@ class Query {
|
|
|
260
361
|
}
|
|
261
362
|
|
|
262
363
|
groupStats(field) {
|
|
263
|
-
if (!this._groupBy) throw
|
|
264
|
-
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);
|
|
265
366
|
const groups = {};
|
|
266
367
|
|
|
267
368
|
for (const row of rows) {
|
|
@@ -283,7 +384,6 @@ class Query {
|
|
|
283
384
|
g._avg = g._count > 0 ? g._sum / g._count : 0;
|
|
284
385
|
delete g._rows;
|
|
285
386
|
});
|
|
286
|
-
|
|
287
387
|
return result;
|
|
288
388
|
}
|
|
289
389
|
|
|
@@ -295,39 +395,181 @@ class Query {
|
|
|
295
395
|
return this._table.remove(this._conditions);
|
|
296
396
|
}
|
|
297
397
|
|
|
398
|
+
/**
|
|
399
|
+
* 无效缓存
|
|
400
|
+
*/
|
|
401
|
+
invalidateCache() {
|
|
402
|
+
if (this._cache) {
|
|
403
|
+
this._cache.result = null;
|
|
404
|
+
this._cache.timestamp = 0;
|
|
405
|
+
}
|
|
406
|
+
return this;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============================================================
|
|
410
|
+
// toSQL
|
|
411
|
+
// ============================================================
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* 导出为类 SQL 字符串(调试用)
|
|
415
|
+
*/
|
|
416
|
+
toSQL() {
|
|
417
|
+
const parts = ['SELECT'];
|
|
418
|
+
if (this._selectFields) {
|
|
419
|
+
parts.push(this._selectFields.join(', '));
|
|
420
|
+
} else {
|
|
421
|
+
parts.push('*');
|
|
422
|
+
}
|
|
423
|
+
parts.push('FROM', this._table._name);
|
|
424
|
+
|
|
425
|
+
if (Object.keys(this._conditions).length > 0) {
|
|
426
|
+
parts.push('WHERE', JSON.stringify(this._conditions));
|
|
427
|
+
}
|
|
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
|
+
|
|
435
|
+
if (this._groupBy) {
|
|
436
|
+
parts.push('GROUP BY', this._groupBy);
|
|
437
|
+
if (this._having) parts.push('HAVING', JSON.stringify(this._having));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (this._orderBy.length > 0) {
|
|
441
|
+
parts.push('ORDER BY', this._orderBy.map(o => `${o.field} ${o.dir}`).join(', '));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (this._limitCount !== null) {
|
|
445
|
+
parts.push('LIMIT', String(this._limitCount));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (this._offsetCount > 0) {
|
|
449
|
+
parts.push('OFFSET', String(this._offsetCount));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return parts.join(' ');
|
|
453
|
+
}
|
|
454
|
+
|
|
298
455
|
// ============================================================
|
|
299
456
|
// 内部
|
|
300
457
|
// ============================================================
|
|
301
458
|
|
|
302
459
|
_getFilteredRows() {
|
|
303
|
-
return this._table.
|
|
460
|
+
return this._table._applyFilterOptimized(this._table._rows, this._conditions);
|
|
304
461
|
}
|
|
305
462
|
|
|
306
463
|
_countRaw() {
|
|
307
|
-
return this._table.
|
|
464
|
+
return this._table._applyFilterOptimized(this._table._rows, this._conditions).length;
|
|
308
465
|
}
|
|
309
466
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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);
|
|
317
479
|
}
|
|
318
|
-
return
|
|
319
|
-
}
|
|
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;
|
|
320
558
|
}
|
|
321
559
|
|
|
322
|
-
|
|
560
|
+
/**
|
|
561
|
+
* 嵌套循环 JOIN — O(n * m) 时间复杂度
|
|
562
|
+
* 用于小数据量场景
|
|
563
|
+
*/
|
|
564
|
+
_applyNestedLoopJoin(localRows, foreignTable, join) {
|
|
323
565
|
const result = [];
|
|
324
|
-
const foreignRows =
|
|
566
|
+
const foreignRows = foreignTable._rows;
|
|
325
567
|
|
|
326
568
|
if (join.type === 'left') {
|
|
327
569
|
for (const lr of localRows) {
|
|
328
570
|
const matches = foreignRows.filter(fr => fr[join.foreignField] === lr[join.localField]);
|
|
329
571
|
if (matches.length === 0) {
|
|
330
|
-
result.push({ ...lr, ...this._nullRow(
|
|
572
|
+
result.push({ ...lr, ...this._nullRow(foreignTable) });
|
|
331
573
|
} else {
|
|
332
574
|
for (const fr of matches) {
|
|
333
575
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -338,7 +580,7 @@ class Query {
|
|
|
338
580
|
for (const fr of foreignRows) {
|
|
339
581
|
const matches = localRows.filter(lr => lr[join.localField] === fr[join.foreignField]);
|
|
340
582
|
if (matches.length === 0) {
|
|
341
|
-
result.push({ ...fr, ...this._nullRow(join
|
|
583
|
+
result.push({ ...fr, ...this._nullRow(join, true) });
|
|
342
584
|
} else {
|
|
343
585
|
for (const lr of matches) {
|
|
344
586
|
result.push(this._mergeJoinRow(lr, fr, join));
|
|
@@ -346,7 +588,7 @@ class Query {
|
|
|
346
588
|
}
|
|
347
589
|
}
|
|
348
590
|
} else {
|
|
349
|
-
//
|
|
591
|
+
// INNER JOIN
|
|
350
592
|
for (const lr of localRows) {
|
|
351
593
|
for (const fr of foreignRows) {
|
|
352
594
|
if (fr[join.foreignField] === lr[join.localField]) {
|
|
@@ -367,15 +609,100 @@ class Query {
|
|
|
367
609
|
return merged;
|
|
368
610
|
}
|
|
369
611
|
|
|
370
|
-
_nullRow(
|
|
612
|
+
_nullRow(tableRef) {
|
|
613
|
+
// tableRef 可能是 join 对象(用于 right join 的本地表)或 Table 对象
|
|
614
|
+
const schema = tableRef._schema || tableRef.table?._schema || {};
|
|
371
615
|
const nulls = {};
|
|
372
|
-
const schema = table._schema;
|
|
373
616
|
for (const field of Object.keys(schema)) {
|
|
374
|
-
nulls[field] = null;
|
|
617
|
+
if (field !== '_softDelete') nulls[field] = null;
|
|
375
618
|
}
|
|
376
619
|
return nulls;
|
|
377
620
|
}
|
|
378
621
|
|
|
622
|
+
// ============================================================
|
|
623
|
+
// 内部: 排序
|
|
624
|
+
// ============================================================
|
|
625
|
+
|
|
626
|
+
_applyOrderBy(rows) {
|
|
627
|
+
return [...rows].sort((a, b) => {
|
|
628
|
+
for (const { field, dir } of this._orderBy) {
|
|
629
|
+
const va = a[field];
|
|
630
|
+
const vb = b[field];
|
|
631
|
+
if (va < vb) return dir === 'asc' ? -1 : 1;
|
|
632
|
+
if (va > vb) return dir === 'asc' ? 1 : -1;
|
|
633
|
+
}
|
|
634
|
+
return 0;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ============================================================
|
|
639
|
+
// 内部: 窗口函数
|
|
640
|
+
// ============================================================
|
|
641
|
+
|
|
642
|
+
_applyWindow(rows) {
|
|
643
|
+
const { fn, field, partitionBy, dir } = this._window;
|
|
644
|
+
|
|
645
|
+
if (partitionBy) {
|
|
646
|
+
const groups = {};
|
|
647
|
+
for (const row of rows) {
|
|
648
|
+
const key = String(row[partitionBy] ?? '__null__');
|
|
649
|
+
if (!groups[key]) groups[key] = [];
|
|
650
|
+
groups[key].push(row);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const result = [];
|
|
654
|
+
for (const group of Object.values(groups)) {
|
|
655
|
+
this._applyWindowToGroup(group, fn, field, dir);
|
|
656
|
+
result.push(...group);
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
this._applyWindowToGroup(rows, fn, field, dir);
|
|
662
|
+
return rows;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
_applyWindowToGroup(rows, fn, field, dir) {
|
|
666
|
+
if (field) {
|
|
667
|
+
rows.sort((a, b) => {
|
|
668
|
+
const va = a[field];
|
|
669
|
+
const vb = b[field];
|
|
670
|
+
if (va < vb) return dir === 'asc' ? -1 : 1;
|
|
671
|
+
if (va > vb) return dir === 'asc' ? 1 : -1;
|
|
672
|
+
return 0;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const colName = fn === 'rowNumber' ? '_row_number' : fn === 'rank' ? '_rank' : '_dense_rank';
|
|
677
|
+
let rank = 1;
|
|
678
|
+
let denseRank = 1;
|
|
679
|
+
let prevVal = null;
|
|
680
|
+
|
|
681
|
+
for (let i = 0; i < rows.length; i++) {
|
|
682
|
+
const curVal = field ? rows[i][field] : null;
|
|
683
|
+
|
|
684
|
+
if (fn === 'rowNumber') {
|
|
685
|
+
rows[i][colName] = i + 1;
|
|
686
|
+
} else if (fn === 'rank') {
|
|
687
|
+
if (i > 0 && curVal !== prevVal) {
|
|
688
|
+
rank = i + 1;
|
|
689
|
+
}
|
|
690
|
+
rows[i][colName] = rank;
|
|
691
|
+
} else if (fn === 'denseRank') {
|
|
692
|
+
if (i > 0 && curVal !== prevVal) {
|
|
693
|
+
denseRank++;
|
|
694
|
+
}
|
|
695
|
+
rows[i][colName] = denseRank;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
prevVal = curVal;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ============================================================
|
|
703
|
+
// 内部: GROUP BY
|
|
704
|
+
// ============================================================
|
|
705
|
+
|
|
379
706
|
_applyGroupBy(rows) {
|
|
380
707
|
const groups = {};
|
|
381
708
|
for (const row of rows) {
|
|
@@ -389,7 +716,6 @@ class Query {
|
|
|
389
716
|
|
|
390
717
|
const result = [];
|
|
391
718
|
for (const g of Object.values(groups)) {
|
|
392
|
-
// 使用第一行作为基础,并添加 _group 和 _count
|
|
393
719
|
const base = { ...g._rows[0] };
|
|
394
720
|
base._group = g._group;
|
|
395
721
|
base._count = g._count;
|
|
@@ -398,36 +724,71 @@ class Query {
|
|
|
398
724
|
return result;
|
|
399
725
|
}
|
|
400
726
|
|
|
727
|
+
// ============================================================
|
|
728
|
+
// 内部: EXPLAIN(增强版)
|
|
729
|
+
// ============================================================
|
|
730
|
+
|
|
401
731
|
_getExplain() {
|
|
402
732
|
const totalRows = this._table._rows.length;
|
|
403
|
-
const filtered = this._table.
|
|
733
|
+
const filtered = this._table._applyFilterOptimized(this._table._rows, this._conditions);
|
|
404
734
|
const plan = {
|
|
405
735
|
table: this._table._name,
|
|
406
736
|
totalRows,
|
|
407
737
|
conditions: this._conditions,
|
|
408
738
|
filteredRows: filtered.length,
|
|
409
739
|
selectivity: totalRows > 0 ? (filtered.length / totalRows * 100).toFixed(1) + '%' : '0%',
|
|
410
|
-
|
|
740
|
+
hasBTree: false,
|
|
741
|
+
hasHashIndex: false,
|
|
411
742
|
indexUsed: null,
|
|
412
|
-
|
|
743
|
+
btreeIndexes: Object.keys(this._table._btrees),
|
|
744
|
+
hashIndexes: Object.keys(this._table._indexes),
|
|
745
|
+
joins: [],
|
|
413
746
|
groupBy: this._groupBy,
|
|
414
747
|
having: this._having,
|
|
748
|
+
window: this._window ? this._window.fn : null,
|
|
415
749
|
orderBy: this._orderBy.map(o => `${o.field} ${o.dir}`),
|
|
416
750
|
limit: this._limitCount,
|
|
417
751
|
offset: this._offsetCount,
|
|
418
|
-
estimatedCost: filtered.length * (1 + this._joins.length)
|
|
752
|
+
estimatedCost: filtered.length * (1 + this._joins.length),
|
|
753
|
+
optimizerHints: this._optimizerHints
|
|
419
754
|
};
|
|
420
755
|
|
|
421
|
-
//
|
|
756
|
+
// 检查 B-Tree 使用
|
|
422
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
|
+
}
|
|
423
764
|
if (this._table._indexes[field]) {
|
|
424
|
-
plan.
|
|
765
|
+
plan.hasHashIndex = true;
|
|
425
766
|
plan.indexUsed = field;
|
|
426
767
|
plan.estimatedCost = Math.floor(plan.estimatedCost * 0.1);
|
|
427
768
|
break;
|
|
428
769
|
}
|
|
429
770
|
}
|
|
430
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
|
+
|
|
431
792
|
return plan;
|
|
432
793
|
}
|
|
433
794
|
}
|