@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.
Files changed (48) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/tsmdb/engine/IndexEngine.d.ts +23 -3
  3. package/dist_ts/tsmdb/engine/IndexEngine.js +357 -55
  4. package/dist_ts/tsmdb/engine/QueryPlanner.d.ts +64 -0
  5. package/dist_ts/tsmdb/engine/QueryPlanner.js +308 -0
  6. package/dist_ts/tsmdb/engine/SessionEngine.d.ts +117 -0
  7. package/dist_ts/tsmdb/engine/SessionEngine.js +232 -0
  8. package/dist_ts/tsmdb/index.d.ts +7 -0
  9. package/dist_ts/tsmdb/index.js +6 -1
  10. package/dist_ts/tsmdb/server/CommandRouter.d.ts +36 -0
  11. package/dist_ts/tsmdb/server/CommandRouter.js +91 -1
  12. package/dist_ts/tsmdb/server/TsmdbServer.js +3 -1
  13. package/dist_ts/tsmdb/server/handlers/AdminHandler.js +106 -6
  14. package/dist_ts/tsmdb/server/handlers/DeleteHandler.js +15 -3
  15. package/dist_ts/tsmdb/server/handlers/FindHandler.js +44 -14
  16. package/dist_ts/tsmdb/server/handlers/InsertHandler.js +4 -1
  17. package/dist_ts/tsmdb/server/handlers/UpdateHandler.js +31 -5
  18. package/dist_ts/tsmdb/storage/FileStorageAdapter.d.ts +25 -1
  19. package/dist_ts/tsmdb/storage/FileStorageAdapter.js +75 -6
  20. package/dist_ts/tsmdb/storage/IStorageAdapter.d.ts +5 -0
  21. package/dist_ts/tsmdb/storage/MemoryStorageAdapter.d.ts +1 -0
  22. package/dist_ts/tsmdb/storage/MemoryStorageAdapter.js +12 -1
  23. package/dist_ts/tsmdb/storage/WAL.d.ts +117 -0
  24. package/dist_ts/tsmdb/storage/WAL.js +286 -0
  25. package/dist_ts/tsmdb/utils/checksum.d.ts +30 -0
  26. package/dist_ts/tsmdb/utils/checksum.js +77 -0
  27. package/dist_ts/tsmdb/utils/index.d.ts +1 -0
  28. package/dist_ts/tsmdb/utils/index.js +2 -0
  29. package/package.json +2 -2
  30. package/readme.md +140 -17
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/tsmdb/engine/IndexEngine.ts +375 -56
  33. package/ts/tsmdb/engine/QueryPlanner.ts +393 -0
  34. package/ts/tsmdb/engine/SessionEngine.ts +292 -0
  35. package/ts/tsmdb/index.ts +9 -0
  36. package/ts/tsmdb/server/CommandRouter.ts +109 -0
  37. package/ts/tsmdb/server/TsmdbServer.ts +3 -0
  38. package/ts/tsmdb/server/handlers/AdminHandler.ts +110 -5
  39. package/ts/tsmdb/server/handlers/DeleteHandler.ts +17 -2
  40. package/ts/tsmdb/server/handlers/FindHandler.ts +42 -13
  41. package/ts/tsmdb/server/handlers/InsertHandler.ts +6 -0
  42. package/ts/tsmdb/server/handlers/UpdateHandler.ts +33 -4
  43. package/ts/tsmdb/storage/FileStorageAdapter.ts +88 -5
  44. package/ts/tsmdb/storage/IStorageAdapter.ts +6 -0
  45. package/ts/tsmdb/storage/MemoryStorageAdapter.ts +12 -0
  46. package/ts/tsmdb/storage/WAL.ts +375 -0
  47. package/ts/tsmdb/utils/checksum.ts +88 -0
  48. 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
- * Index data structure for fast lookups
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
- // Map from index key value to document _id(s)
23
- entries: Map<string, Set<string>>;
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
- entries: new Map(),
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
- if (!indexData.entries.has(keyStr)) {
67
- indexData.entries.set(keyStr, new Set());
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
- entries: new Map(),
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.entries.has(keyStr)) {
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
- if (!indexData.entries.has(keyStr)) {
127
- indexData.entries.set(keyStr, new Set());
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.entries.get(keyStr);
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
- if (!indexData.entries.has(keyStr)) {
227
- indexData.entries.set(keyStr, new Set());
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
- const oldSet = indexData.entries.get(oldKeyStr);
249
- if (oldSet) {
250
- oldSet.delete(oldDoc._id.toHexString());
251
- if (oldSet.size === 0) {
252
- indexData.entries.delete(oldKeyStr);
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.entries.get(newKeyStr);
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
- if (!indexData.entries.has(newKeyStr)) {
272
- indexData.entries.set(newKeyStr, new Set());
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
- const set = indexData.entries.get(keyStr);
295
- if (set) {
296
- set.delete(doc._id.toHexString());
297
- if (set.size === 0) {
298
- indexData.entries.delete(keyStr);
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 filterFields = new Set(this.getFilterFields(filter));
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 are in the filter
523
+ // Count how many index fields can be used
324
524
  for (const field of indexFields) {
325
- if (filterFields.has(field)) {
326
- score++;
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; // Index fields must be contiguous
537
+ break;
329
538
  }
330
539
  }
331
540
 
@@ -344,7 +553,46 @@ export class IndexEngine {
344
553
  }
345
554
 
346
555
  /**
347
- * Use index to find candidate document IDs
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
- // Try to use the index for equality matches
603
+ const filterInfo = this.analyzeFilter(filter);
356
604
  const indexFields = Object.keys(index.data.key);
357
- const equalityValues: Record<string, any> = {};
358
605
 
359
- for (const field of indexFields) {
360
- const filterValue = this.getFilterValue(filter, field);
361
- if (filterValue === undefined) break;
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
- // Only use equality matches for index lookup
364
- if (typeof filterValue === 'object' && filterValue !== null) {
365
- if (filterValue.$eq !== undefined) {
366
- equalityValues[field] = filterValue.$eq;
367
- } else if (filterValue.$in !== undefined) {
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 filterValue.$in) {
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.entries.get(keyStr);
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
- } else {
385
- equalityValues[field] = filterValue;
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
- if (Object.keys(equalityValues).length === 0) {
390
- return null;
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
- const keyStr = JSON.stringify(this.buildKeyValue(equalityValues, index.data.key));
394
- return index.data.entries.get(keyStr) || new Set();
713
+ return results;
395
714
  }
396
715
 
397
716
  // ============================================================================