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/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、多字段排序、高级操作符、分页、EXPLAIN
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
- // JOIN
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 - JOIN 的表
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
- let rows = this._table._applyFilter(this._table._rows, this._conditions);
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._applyJoin(rows, join);
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; // 跳过 undefined
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 new Error('groupStats() requires groupBy() to be called first');
264
- 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);
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._applyFilter(this._table._rows, this._conditions);
460
+ return this._table._applyFilterOptimized(this._table._rows, this._conditions);
304
461
  }
305
462
 
306
463
  _countRaw() {
307
- return this._table._applyFilter(this._table._rows, this._conditions).length;
464
+ return this._table._applyFilterOptimized(this._table._rows, this._conditions).length;
308
465
  }
309
466
 
310
- _applyOrderBy(rows) {
311
- return [...rows].sort((a, b) => {
312
- for (const { field, dir } of this._orderBy) {
313
- const va = a[field];
314
- const vb = b[field];
315
- if (va < vb) return dir === 'asc' ? -1 : 1;
316
- if (va > vb) return dir === 'asc' ? 1 : -1;
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 0;
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
- _applyJoin(localRows, join) {
560
+ /**
561
+ * 嵌套循环 JOIN — O(n * m) 时间复杂度
562
+ * 用于小数据量场景
563
+ */
564
+ _applyNestedLoopJoin(localRows, foreignTable, join) {
323
565
  const result = [];
324
- const foreignRows = join.table._rows;
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(join.table) });
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.table, true) });
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
- // inner join
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(table, isLocal) {
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._applyFilter(this._table._rows, this._conditions);
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
- hasIndex: false,
740
+ hasBTree: false,
741
+ hasHashIndex: false,
411
742
  indexUsed: null,
412
- joins: this._joins.length,
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.hasIndex = true;
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
  }