@push.rocks/smartmongo 3.0.0 → 4.1.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/tsmdb/engine/IndexEngine.d.ts +23 -3
- package/dist_ts/tsmdb/engine/IndexEngine.js +357 -55
- package/dist_ts/tsmdb/engine/QueryPlanner.d.ts +64 -0
- package/dist_ts/tsmdb/engine/QueryPlanner.js +308 -0
- package/dist_ts/tsmdb/engine/SessionEngine.d.ts +117 -0
- package/dist_ts/tsmdb/engine/SessionEngine.js +232 -0
- package/dist_ts/tsmdb/index.d.ts +7 -0
- package/dist_ts/tsmdb/index.js +6 -1
- package/dist_ts/tsmdb/server/CommandRouter.d.ts +36 -0
- package/dist_ts/tsmdb/server/CommandRouter.js +91 -1
- package/dist_ts/tsmdb/server/TsmdbServer.js +3 -1
- package/dist_ts/tsmdb/server/handlers/AdminHandler.js +106 -6
- package/dist_ts/tsmdb/server/handlers/DeleteHandler.js +15 -3
- package/dist_ts/tsmdb/server/handlers/FindHandler.js +44 -14
- package/dist_ts/tsmdb/server/handlers/InsertHandler.js +4 -1
- package/dist_ts/tsmdb/server/handlers/UpdateHandler.js +31 -5
- package/dist_ts/tsmdb/storage/FileStorageAdapter.d.ts +25 -1
- package/dist_ts/tsmdb/storage/FileStorageAdapter.js +75 -6
- package/dist_ts/tsmdb/storage/IStorageAdapter.d.ts +5 -0
- package/dist_ts/tsmdb/storage/MemoryStorageAdapter.d.ts +1 -0
- package/dist_ts/tsmdb/storage/MemoryStorageAdapter.js +12 -1
- package/dist_ts/tsmdb/storage/WAL.d.ts +117 -0
- package/dist_ts/tsmdb/storage/WAL.js +286 -0
- package/dist_ts/tsmdb/utils/checksum.d.ts +30 -0
- package/dist_ts/tsmdb/utils/checksum.js +77 -0
- package/dist_ts/tsmdb/utils/index.d.ts +1 -0
- package/dist_ts/tsmdb/utils/index.js +2 -0
- package/package.json +2 -2
- package/readme.md +140 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/tsmdb/engine/IndexEngine.ts +375 -56
- package/ts/tsmdb/engine/QueryPlanner.ts +393 -0
- package/ts/tsmdb/engine/SessionEngine.ts +292 -0
- package/ts/tsmdb/index.ts +9 -0
- package/ts/tsmdb/server/CommandRouter.ts +109 -0
- package/ts/tsmdb/server/TsmdbServer.ts +3 -0
- package/ts/tsmdb/server/handlers/AdminHandler.ts +110 -5
- package/ts/tsmdb/server/handlers/DeleteHandler.ts +17 -2
- package/ts/tsmdb/server/handlers/FindHandler.ts +42 -13
- package/ts/tsmdb/server/handlers/InsertHandler.ts +6 -0
- package/ts/tsmdb/server/handlers/UpdateHandler.ts +33 -4
- package/ts/tsmdb/storage/FileStorageAdapter.ts +88 -5
- package/ts/tsmdb/storage/IStorageAdapter.ts +6 -0
- package/ts/tsmdb/storage/MemoryStorageAdapter.ts +12 -0
- package/ts/tsmdb/storage/WAL.ts +375 -0
- package/ts/tsmdb/utils/checksum.ts +88 -0
- package/ts/tsmdb/utils/index.ts +1 -0
|
@@ -1,5 +1,89 @@
|
|
|
1
1
|
import * as plugins from '../tsmdb.plugins.js';
|
|
2
2
|
import type { IStorageAdapter } from '../storage/IStorageAdapter.js';
|
|
3
|
+
|
|
4
|
+
// Simple B-Tree implementation for range queries
|
|
5
|
+
// Since sorted-btree has ESM/CJS interop issues, we use a simple custom implementation
|
|
6
|
+
class SimpleBTree<K, V> {
|
|
7
|
+
private entries: Map<string, { key: K; value: V }> = new Map();
|
|
8
|
+
private sortedKeys: K[] = [];
|
|
9
|
+
private comparator: (a: K, b: K) => number;
|
|
10
|
+
|
|
11
|
+
constructor(_unused?: undefined, comparator?: (a: K, b: K) => number) {
|
|
12
|
+
this.comparator = comparator || ((a: K, b: K) => {
|
|
13
|
+
if (a < b) return -1;
|
|
14
|
+
if (a > b) return 1;
|
|
15
|
+
return 0;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private keyToString(key: K): string {
|
|
20
|
+
return JSON.stringify(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(key: K, value: V): boolean {
|
|
24
|
+
const keyStr = this.keyToString(key);
|
|
25
|
+
const existed = this.entries.has(keyStr);
|
|
26
|
+
this.entries.set(keyStr, { key, value });
|
|
27
|
+
|
|
28
|
+
if (!existed) {
|
|
29
|
+
// Insert in sorted order
|
|
30
|
+
const idx = this.sortedKeys.findIndex(k => this.comparator(k, key) > 0);
|
|
31
|
+
if (idx === -1) {
|
|
32
|
+
this.sortedKeys.push(key);
|
|
33
|
+
} else {
|
|
34
|
+
this.sortedKeys.splice(idx, 0, key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return !existed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get(key: K): V | undefined {
|
|
41
|
+
const entry = this.entries.get(this.keyToString(key));
|
|
42
|
+
return entry?.value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
delete(key: K): boolean {
|
|
46
|
+
const keyStr = this.keyToString(key);
|
|
47
|
+
if (this.entries.has(keyStr)) {
|
|
48
|
+
this.entries.delete(keyStr);
|
|
49
|
+
const idx = this.sortedKeys.findIndex(k => this.comparator(k, key) === 0);
|
|
50
|
+
if (idx !== -1) {
|
|
51
|
+
this.sortedKeys.splice(idx, 1);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
forRange(
|
|
59
|
+
lowKey: K | undefined,
|
|
60
|
+
highKey: K | undefined,
|
|
61
|
+
lowInclusive: boolean,
|
|
62
|
+
highInclusive: boolean,
|
|
63
|
+
callback: (value: V, key: K) => void
|
|
64
|
+
): void {
|
|
65
|
+
for (const key of this.sortedKeys) {
|
|
66
|
+
// Check low bound
|
|
67
|
+
if (lowKey !== undefined) {
|
|
68
|
+
const cmp = this.comparator(key, lowKey);
|
|
69
|
+
if (cmp < 0) continue;
|
|
70
|
+
if (cmp === 0 && !lowInclusive) continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check high bound
|
|
74
|
+
if (highKey !== undefined) {
|
|
75
|
+
const cmp = this.comparator(key, highKey);
|
|
76
|
+
if (cmp > 0) break;
|
|
77
|
+
if (cmp === 0 && !highInclusive) break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const entry = this.entries.get(this.keyToString(key));
|
|
81
|
+
if (entry) {
|
|
82
|
+
callback(entry.value, key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
3
87
|
import type {
|
|
4
88
|
Document,
|
|
5
89
|
IStoredDocument,
|
|
@@ -11,7 +95,61 @@ import { TsmdbDuplicateKeyError, TsmdbIndexError } from '../errors/TsmdbErrors.j
|
|
|
11
95
|
import { QueryEngine } from './QueryEngine.js';
|
|
12
96
|
|
|
13
97
|
/**
|
|
14
|
-
*
|
|
98
|
+
* Comparator for B-Tree that handles mixed types consistently
|
|
99
|
+
*/
|
|
100
|
+
function indexKeyComparator(a: any, b: any): number {
|
|
101
|
+
// Handle null/undefined
|
|
102
|
+
if (a === null || a === undefined) {
|
|
103
|
+
if (b === null || b === undefined) return 0;
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
|
106
|
+
if (b === null || b === undefined) return 1;
|
|
107
|
+
|
|
108
|
+
// Handle arrays (compound keys)
|
|
109
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
110
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
111
|
+
const cmp = indexKeyComparator(a[i], b[i]);
|
|
112
|
+
if (cmp !== 0) return cmp;
|
|
113
|
+
}
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Handle ObjectId
|
|
118
|
+
if (a instanceof plugins.bson.ObjectId && b instanceof plugins.bson.ObjectId) {
|
|
119
|
+
return a.toHexString().localeCompare(b.toHexString());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle Date
|
|
123
|
+
if (a instanceof Date && b instanceof Date) {
|
|
124
|
+
return a.getTime() - b.getTime();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle different types - use type ordering (null < number < string < object)
|
|
128
|
+
const typeOrder = (v: any): number => {
|
|
129
|
+
if (v === null || v === undefined) return 0;
|
|
130
|
+
if (typeof v === 'number') return 1;
|
|
131
|
+
if (typeof v === 'string') return 2;
|
|
132
|
+
if (typeof v === 'boolean') return 3;
|
|
133
|
+
if (v instanceof Date) return 4;
|
|
134
|
+
if (v instanceof plugins.bson.ObjectId) return 5;
|
|
135
|
+
return 6;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const typeA = typeOrder(a);
|
|
139
|
+
const typeB = typeOrder(b);
|
|
140
|
+
if (typeA !== typeB) return typeA - typeB;
|
|
141
|
+
|
|
142
|
+
// Same type comparison
|
|
143
|
+
if (typeof a === 'number') return a - b;
|
|
144
|
+
if (typeof a === 'string') return a.localeCompare(b);
|
|
145
|
+
if (typeof a === 'boolean') return (a ? 1 : 0) - (b ? 1 : 0);
|
|
146
|
+
|
|
147
|
+
// Fallback to string comparison
|
|
148
|
+
return String(a).localeCompare(String(b));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Index data structure using B-Tree for range queries
|
|
15
153
|
*/
|
|
16
154
|
interface IIndexData {
|
|
17
155
|
name: string;
|
|
@@ -19,8 +157,10 @@ interface IIndexData {
|
|
|
19
157
|
unique: boolean;
|
|
20
158
|
sparse: boolean;
|
|
21
159
|
expireAfterSeconds?: number;
|
|
22
|
-
//
|
|
23
|
-
|
|
160
|
+
// B-Tree for ordered index lookups (supports range queries)
|
|
161
|
+
btree: SimpleBTree<any, Set<string>>;
|
|
162
|
+
// Hash map for fast equality lookups
|
|
163
|
+
hashMap: Map<string, Set<string>>;
|
|
24
164
|
}
|
|
25
165
|
|
|
26
166
|
/**
|
|
@@ -55,7 +195,8 @@ export class IndexEngine {
|
|
|
55
195
|
unique: indexSpec.unique || false,
|
|
56
196
|
sparse: indexSpec.sparse || false,
|
|
57
197
|
expireAfterSeconds: indexSpec.expireAfterSeconds,
|
|
58
|
-
|
|
198
|
+
btree: new SimpleBTree<any, Set<string>>(undefined, indexKeyComparator),
|
|
199
|
+
hashMap: new Map(),
|
|
59
200
|
};
|
|
60
201
|
|
|
61
202
|
// Build index entries
|
|
@@ -63,10 +204,20 @@ export class IndexEngine {
|
|
|
63
204
|
const keyValue = this.extractKeyValue(doc, indexSpec.key);
|
|
64
205
|
if (keyValue !== null || !indexData.sparse) {
|
|
65
206
|
const keyStr = JSON.stringify(keyValue);
|
|
66
|
-
|
|
67
|
-
|
|
207
|
+
|
|
208
|
+
// Add to hash map
|
|
209
|
+
if (!indexData.hashMap.has(keyStr)) {
|
|
210
|
+
indexData.hashMap.set(keyStr, new Set());
|
|
211
|
+
}
|
|
212
|
+
indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
|
|
213
|
+
|
|
214
|
+
// Add to B-tree
|
|
215
|
+
const existing = indexData.btree.get(keyValue);
|
|
216
|
+
if (existing) {
|
|
217
|
+
existing.add(doc._id.toHexString());
|
|
218
|
+
} else {
|
|
219
|
+
indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
|
|
68
220
|
}
|
|
69
|
-
indexData.entries.get(keyStr)!.add(doc._id.toHexString());
|
|
70
221
|
}
|
|
71
222
|
}
|
|
72
223
|
|
|
@@ -100,7 +251,8 @@ export class IndexEngine {
|
|
|
100
251
|
unique: options?.unique || false,
|
|
101
252
|
sparse: options?.sparse || false,
|
|
102
253
|
expireAfterSeconds: options?.expireAfterSeconds,
|
|
103
|
-
|
|
254
|
+
btree: new SimpleBTree<any, Set<string>>(undefined, indexKeyComparator),
|
|
255
|
+
hashMap: new Map(),
|
|
104
256
|
};
|
|
105
257
|
|
|
106
258
|
// Build index from existing documents
|
|
@@ -115,7 +267,7 @@ export class IndexEngine {
|
|
|
115
267
|
|
|
116
268
|
const keyStr = JSON.stringify(keyValue);
|
|
117
269
|
|
|
118
|
-
if (indexData.unique && indexData.
|
|
270
|
+
if (indexData.unique && indexData.hashMap.has(keyStr)) {
|
|
119
271
|
throw new TsmdbDuplicateKeyError(
|
|
120
272
|
`E11000 duplicate key error index: ${this.dbName}.${this.collName}.$${name}`,
|
|
121
273
|
key as Record<string, 1>,
|
|
@@ -123,10 +275,19 @@ export class IndexEngine {
|
|
|
123
275
|
);
|
|
124
276
|
}
|
|
125
277
|
|
|
126
|
-
|
|
127
|
-
|
|
278
|
+
// Add to hash map
|
|
279
|
+
if (!indexData.hashMap.has(keyStr)) {
|
|
280
|
+
indexData.hashMap.set(keyStr, new Set());
|
|
281
|
+
}
|
|
282
|
+
indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
|
|
283
|
+
|
|
284
|
+
// Add to B-tree
|
|
285
|
+
const existing = indexData.btree.get(keyValue);
|
|
286
|
+
if (existing) {
|
|
287
|
+
existing.add(doc._id.toHexString());
|
|
288
|
+
} else {
|
|
289
|
+
indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
|
|
128
290
|
}
|
|
129
|
-
indexData.entries.get(keyStr)!.add(doc._id.toHexString());
|
|
130
291
|
}
|
|
131
292
|
|
|
132
293
|
// Store index
|
|
@@ -213,7 +374,7 @@ export class IndexEngine {
|
|
|
213
374
|
|
|
214
375
|
// Check unique constraint
|
|
215
376
|
if (indexData.unique) {
|
|
216
|
-
const existing = indexData.
|
|
377
|
+
const existing = indexData.hashMap.get(keyStr);
|
|
217
378
|
if (existing && existing.size > 0) {
|
|
218
379
|
throw new TsmdbDuplicateKeyError(
|
|
219
380
|
`E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
|
|
@@ -223,10 +384,19 @@ export class IndexEngine {
|
|
|
223
384
|
}
|
|
224
385
|
}
|
|
225
386
|
|
|
226
|
-
|
|
227
|
-
|
|
387
|
+
// Add to hash map
|
|
388
|
+
if (!indexData.hashMap.has(keyStr)) {
|
|
389
|
+
indexData.hashMap.set(keyStr, new Set());
|
|
390
|
+
}
|
|
391
|
+
indexData.hashMap.get(keyStr)!.add(doc._id.toHexString());
|
|
392
|
+
|
|
393
|
+
// Add to B-tree
|
|
394
|
+
const btreeSet = indexData.btree.get(keyValue);
|
|
395
|
+
if (btreeSet) {
|
|
396
|
+
btreeSet.add(doc._id.toHexString());
|
|
397
|
+
} else {
|
|
398
|
+
indexData.btree.set(keyValue, new Set([doc._id.toHexString()]));
|
|
228
399
|
}
|
|
229
|
-
indexData.entries.get(keyStr)!.add(doc._id.toHexString());
|
|
230
400
|
}
|
|
231
401
|
}
|
|
232
402
|
|
|
@@ -245,11 +415,21 @@ export class IndexEngine {
|
|
|
245
415
|
// Remove old entry if key changed
|
|
246
416
|
if (oldKeyStr !== newKeyStr) {
|
|
247
417
|
if (oldKeyValue !== null || !indexData.sparse) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
418
|
+
// Remove from hash map
|
|
419
|
+
const oldHashSet = indexData.hashMap.get(oldKeyStr);
|
|
420
|
+
if (oldHashSet) {
|
|
421
|
+
oldHashSet.delete(oldDoc._id.toHexString());
|
|
422
|
+
if (oldHashSet.size === 0) {
|
|
423
|
+
indexData.hashMap.delete(oldKeyStr);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Remove from B-tree
|
|
428
|
+
const oldBtreeSet = indexData.btree.get(oldKeyValue);
|
|
429
|
+
if (oldBtreeSet) {
|
|
430
|
+
oldBtreeSet.delete(oldDoc._id.toHexString());
|
|
431
|
+
if (oldBtreeSet.size === 0) {
|
|
432
|
+
indexData.btree.delete(oldKeyValue);
|
|
253
433
|
}
|
|
254
434
|
}
|
|
255
435
|
}
|
|
@@ -258,7 +438,7 @@ export class IndexEngine {
|
|
|
258
438
|
if (newKeyValue !== null || !indexData.sparse) {
|
|
259
439
|
// Check unique constraint
|
|
260
440
|
if (indexData.unique) {
|
|
261
|
-
const existing = indexData.
|
|
441
|
+
const existing = indexData.hashMap.get(newKeyStr);
|
|
262
442
|
if (existing && existing.size > 0) {
|
|
263
443
|
throw new TsmdbDuplicateKeyError(
|
|
264
444
|
`E11000 duplicate key error collection: ${this.dbName}.${this.collName} index: ${name}`,
|
|
@@ -268,10 +448,19 @@ export class IndexEngine {
|
|
|
268
448
|
}
|
|
269
449
|
}
|
|
270
450
|
|
|
271
|
-
|
|
272
|
-
|
|
451
|
+
// Add to hash map
|
|
452
|
+
if (!indexData.hashMap.has(newKeyStr)) {
|
|
453
|
+
indexData.hashMap.set(newKeyStr, new Set());
|
|
454
|
+
}
|
|
455
|
+
indexData.hashMap.get(newKeyStr)!.add(newDoc._id.toHexString());
|
|
456
|
+
|
|
457
|
+
// Add to B-tree
|
|
458
|
+
const newBtreeSet = indexData.btree.get(newKeyValue);
|
|
459
|
+
if (newBtreeSet) {
|
|
460
|
+
newBtreeSet.add(newDoc._id.toHexString());
|
|
461
|
+
} else {
|
|
462
|
+
indexData.btree.set(newKeyValue, new Set([newDoc._id.toHexString()]));
|
|
273
463
|
}
|
|
274
|
-
indexData.entries.get(newKeyStr)!.add(newDoc._id.toHexString());
|
|
275
464
|
}
|
|
276
465
|
}
|
|
277
466
|
}
|
|
@@ -291,11 +480,22 @@ export class IndexEngine {
|
|
|
291
480
|
}
|
|
292
481
|
|
|
293
482
|
const keyStr = JSON.stringify(keyValue);
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
483
|
+
|
|
484
|
+
// Remove from hash map
|
|
485
|
+
const hashSet = indexData.hashMap.get(keyStr);
|
|
486
|
+
if (hashSet) {
|
|
487
|
+
hashSet.delete(doc._id.toHexString());
|
|
488
|
+
if (hashSet.size === 0) {
|
|
489
|
+
indexData.hashMap.delete(keyStr);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Remove from B-tree
|
|
494
|
+
const btreeSet = indexData.btree.get(keyValue);
|
|
495
|
+
if (btreeSet) {
|
|
496
|
+
btreeSet.delete(doc._id.toHexString());
|
|
497
|
+
if (btreeSet.size === 0) {
|
|
498
|
+
indexData.btree.delete(keyValue);
|
|
299
499
|
}
|
|
300
500
|
}
|
|
301
501
|
}
|
|
@@ -309,8 +509,8 @@ export class IndexEngine {
|
|
|
309
509
|
return null;
|
|
310
510
|
}
|
|
311
511
|
|
|
312
|
-
// Get filter fields
|
|
313
|
-
const
|
|
512
|
+
// Get filter fields and operators
|
|
513
|
+
const filterInfo = this.analyzeFilter(filter);
|
|
314
514
|
|
|
315
515
|
// Score each index
|
|
316
516
|
let bestIndex: { name: string; data: IIndexData } | null = null;
|
|
@@ -320,12 +520,21 @@ export class IndexEngine {
|
|
|
320
520
|
const indexFields = Object.keys(indexData.key);
|
|
321
521
|
let score = 0;
|
|
322
522
|
|
|
323
|
-
// Count how many index fields
|
|
523
|
+
// Count how many index fields can be used
|
|
324
524
|
for (const field of indexFields) {
|
|
325
|
-
|
|
326
|
-
|
|
525
|
+
const info = filterInfo.get(field);
|
|
526
|
+
if (!info) break;
|
|
527
|
+
|
|
528
|
+
// Equality is best
|
|
529
|
+
if (info.equality) {
|
|
530
|
+
score += 2;
|
|
531
|
+
} else if (info.range) {
|
|
532
|
+
// Range queries can use B-tree
|
|
533
|
+
score += 1;
|
|
534
|
+
} else if (info.in) {
|
|
535
|
+
score += 1.5;
|
|
327
536
|
} else {
|
|
328
|
-
break;
|
|
537
|
+
break;
|
|
329
538
|
}
|
|
330
539
|
}
|
|
331
540
|
|
|
@@ -344,7 +553,46 @@ export class IndexEngine {
|
|
|
344
553
|
}
|
|
345
554
|
|
|
346
555
|
/**
|
|
347
|
-
*
|
|
556
|
+
* Analyze filter to extract field operators
|
|
557
|
+
*/
|
|
558
|
+
private analyzeFilter(filter: Document): Map<string, { equality: boolean; range: boolean; in: boolean; ops: Record<string, any> }> {
|
|
559
|
+
const result = new Map<string, { equality: boolean; range: boolean; in: boolean; ops: Record<string, any> }>();
|
|
560
|
+
|
|
561
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
562
|
+
if (key.startsWith('$')) continue;
|
|
563
|
+
|
|
564
|
+
const info = { equality: false, range: false, in: false, ops: {} as Record<string, any> };
|
|
565
|
+
|
|
566
|
+
if (typeof value !== 'object' || value === null || value instanceof plugins.bson.ObjectId || value instanceof Date) {
|
|
567
|
+
info.equality = true;
|
|
568
|
+
info.ops['$eq'] = value;
|
|
569
|
+
} else {
|
|
570
|
+
const ops = value as Record<string, any>;
|
|
571
|
+
if (ops.$eq !== undefined) {
|
|
572
|
+
info.equality = true;
|
|
573
|
+
info.ops['$eq'] = ops.$eq;
|
|
574
|
+
}
|
|
575
|
+
if (ops.$in !== undefined) {
|
|
576
|
+
info.in = true;
|
|
577
|
+
info.ops['$in'] = ops.$in;
|
|
578
|
+
}
|
|
579
|
+
if (ops.$gt !== undefined || ops.$gte !== undefined || ops.$lt !== undefined || ops.$lte !== undefined) {
|
|
580
|
+
info.range = true;
|
|
581
|
+
if (ops.$gt !== undefined) info.ops['$gt'] = ops.$gt;
|
|
582
|
+
if (ops.$gte !== undefined) info.ops['$gte'] = ops.$gte;
|
|
583
|
+
if (ops.$lt !== undefined) info.ops['$lt'] = ops.$lt;
|
|
584
|
+
if (ops.$lte !== undefined) info.ops['$lte'] = ops.$lte;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
result.set(key, info);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Use index to find candidate document IDs (supports range queries with B-tree)
|
|
348
596
|
*/
|
|
349
597
|
async findCandidateIds(filter: Document): Promise<Set<string> | null> {
|
|
350
598
|
await this.initialize();
|
|
@@ -352,25 +600,58 @@ export class IndexEngine {
|
|
|
352
600
|
const index = this.selectIndex(filter);
|
|
353
601
|
if (!index) return null;
|
|
354
602
|
|
|
355
|
-
|
|
603
|
+
const filterInfo = this.analyzeFilter(filter);
|
|
356
604
|
const indexFields = Object.keys(index.data.key);
|
|
357
|
-
const equalityValues: Record<string, any> = {};
|
|
358
605
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
606
|
+
// For single-field indexes with range queries, use B-tree
|
|
607
|
+
if (indexFields.length === 1) {
|
|
608
|
+
const field = indexFields[0];
|
|
609
|
+
const info = filterInfo.get(field);
|
|
610
|
+
|
|
611
|
+
if (info) {
|
|
612
|
+
// Handle equality using hash map (faster)
|
|
613
|
+
if (info.equality) {
|
|
614
|
+
const keyStr = JSON.stringify(info.ops['$eq']);
|
|
615
|
+
return index.data.hashMap.get(keyStr) || new Set();
|
|
616
|
+
}
|
|
362
617
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
618
|
+
// Handle $in using hash map
|
|
619
|
+
if (info.in) {
|
|
620
|
+
const results = new Set<string>();
|
|
621
|
+
for (const val of info.ops['$in']) {
|
|
622
|
+
const keyStr = JSON.stringify(val);
|
|
623
|
+
const ids = index.data.hashMap.get(keyStr);
|
|
624
|
+
if (ids) {
|
|
625
|
+
for (const id of ids) {
|
|
626
|
+
results.add(id);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return results;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Handle range queries using B-tree
|
|
634
|
+
if (info.range) {
|
|
635
|
+
return this.findRangeCandidates(index.data, info.ops);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
// For compound indexes, use hash map with partial key matching
|
|
640
|
+
const equalityValues: Record<string, any> = {};
|
|
641
|
+
|
|
642
|
+
for (const field of indexFields) {
|
|
643
|
+
const info = filterInfo.get(field);
|
|
644
|
+
if (!info) break;
|
|
645
|
+
|
|
646
|
+
if (info.equality) {
|
|
647
|
+
equalityValues[field] = info.ops['$eq'];
|
|
648
|
+
} else if (info.in) {
|
|
368
649
|
// Handle $in with multiple lookups
|
|
369
650
|
const results = new Set<string>();
|
|
370
|
-
for (const val of
|
|
651
|
+
for (const val of info.ops['$in']) {
|
|
371
652
|
equalityValues[field] = val;
|
|
372
653
|
const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
|
|
373
|
-
const ids = index.data.
|
|
654
|
+
const ids = index.data.hashMap.get(keyStr);
|
|
374
655
|
if (ids) {
|
|
375
656
|
for (const id of ids) {
|
|
376
657
|
results.add(id);
|
|
@@ -379,19 +660,57 @@ export class IndexEngine {
|
|
|
379
660
|
}
|
|
380
661
|
return results;
|
|
381
662
|
} else {
|
|
382
|
-
break; // Non-equality operator, stop here
|
|
663
|
+
break; // Non-equality/in operator, stop here
|
|
383
664
|
}
|
|
384
|
-
}
|
|
385
|
-
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (Object.keys(equalityValues).length > 0) {
|
|
668
|
+
const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
|
|
669
|
+
return index.data.hashMap.get(keyStr) || new Set();
|
|
386
670
|
}
|
|
387
671
|
}
|
|
388
672
|
|
|
389
|
-
|
|
390
|
-
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Find candidates using B-tree range scan
|
|
678
|
+
*/
|
|
679
|
+
private findRangeCandidates(indexData: IIndexData, ops: Record<string, any>): Set<string> {
|
|
680
|
+
const results = new Set<string>();
|
|
681
|
+
|
|
682
|
+
let lowKey: any = undefined;
|
|
683
|
+
let highKey: any = undefined;
|
|
684
|
+
let lowInclusive = true;
|
|
685
|
+
let highInclusive = true;
|
|
686
|
+
|
|
687
|
+
if (ops['$gt'] !== undefined) {
|
|
688
|
+
lowKey = ops['$gt'];
|
|
689
|
+
lowInclusive = false;
|
|
690
|
+
}
|
|
691
|
+
if (ops['$gte'] !== undefined) {
|
|
692
|
+
lowKey = ops['$gte'];
|
|
693
|
+
lowInclusive = true;
|
|
391
694
|
}
|
|
695
|
+
if (ops['$lt'] !== undefined) {
|
|
696
|
+
highKey = ops['$lt'];
|
|
697
|
+
highInclusive = false;
|
|
698
|
+
}
|
|
699
|
+
if (ops['$lte'] !== undefined) {
|
|
700
|
+
highKey = ops['$lte'];
|
|
701
|
+
highInclusive = true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Use B-tree range iteration
|
|
705
|
+
indexData.btree.forRange(lowKey, highKey, lowInclusive, highInclusive, (value, key) => {
|
|
706
|
+
if (value) {
|
|
707
|
+
for (const id of value) {
|
|
708
|
+
results.add(id);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
});
|
|
392
712
|
|
|
393
|
-
|
|
394
|
-
return index.data.entries.get(keyStr) || new Set();
|
|
713
|
+
return results;
|
|
395
714
|
}
|
|
396
715
|
|
|
397
716
|
// ============================================================================
|