sehawq.db 3.0.0 → 4.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.
@@ -0,0 +1,814 @@
1
+ /**
2
+ * IndexManager - Makes queries lightning fast ⚡
3
+ *
4
+ * From O(n) to O(1) with the magic of indexing
5
+ * Because scanning millions of records should be illegal 😅
6
+ */
7
+
8
+ class IndexManager {
9
+ constructor(database, options = {}) {
10
+ this.db = database;
11
+ this.options = {
12
+ autoIndex: true,
13
+ backgroundIndexing: true,
14
+ maxIndexes: 10,
15
+ indexUpdateBatchSize: 1000,
16
+ ...options
17
+ };
18
+
19
+ // Index storage
20
+ this.indexes = new Map(); // indexName -> Index instance
21
+ this.fieldIndexes = new Map(); // fieldName -> Set of indexNames
22
+
23
+ // Performance tracking
24
+ this.stats = {
25
+ indexesCreated: 0,
26
+ indexesDropped: 0,
27
+ queriesWithIndex: 0,
28
+ queriesWithoutIndex: 0,
29
+ indexUpdates: 0,
30
+ backgroundJobs: 0
31
+ };
32
+
33
+ // Background indexing queue
34
+ this.indexQueue = [];
35
+ this.isProcessingQueue = false;
36
+
37
+ console.log('📊 IndexManager initialized - Ready to speed things up!');
38
+ }
39
+
40
+ /**
41
+ * Create a new index on a field
42
+ */
43
+ async createIndex(fieldName, indexType = 'hash', options = {}) {
44
+ if (this.indexes.size >= this.options.maxIndexes) {
45
+ throw new Error(`Maximum index limit (${this.options.maxIndexes}) reached`);
46
+ }
47
+
48
+ const indexName = this._getIndexName(fieldName, indexType);
49
+
50
+ if (this.indexes.has(indexName)) {
51
+ throw new Error(`Index '${indexName}' already exists`);
52
+ }
53
+
54
+ console.log(`🔄 Creating ${indexType} index on field: ${fieldName}`);
55
+
56
+ let index;
57
+ switch (indexType) {
58
+ case 'hash':
59
+ index = new HashIndex(fieldName, options);
60
+ break;
61
+ case 'range':
62
+ index = new RangeIndex(fieldName, options);
63
+ break;
64
+ case 'text':
65
+ index = new TextIndex(fieldName, options);
66
+ break;
67
+ default:
68
+ throw new Error(`Unsupported index type: ${indexType}`);
69
+ }
70
+
71
+ // Build the index
72
+ await this._buildIndex(index, fieldName);
73
+
74
+ // Store the index
75
+ this.indexes.set(indexName, index);
76
+
77
+ // Track field indexes
78
+ if (!this.fieldIndexes.has(fieldName)) {
79
+ this.fieldIndexes.set(fieldName, new Set());
80
+ }
81
+ this.fieldIndexes.get(fieldName).add(indexName);
82
+
83
+ this.stats.indexesCreated++;
84
+
85
+ console.log(`✅ Index created: ${indexName} (${index.getStats().entries} entries)`);
86
+
87
+ return indexName;
88
+ }
89
+
90
+ /**
91
+ * Build index from existing data
92
+ */
93
+ async _buildIndex(index, fieldName) {
94
+ const startTime = Date.now();
95
+ let entries = 0;
96
+
97
+ // Process in batches for large datasets
98
+ const batchSize = this.options.indexUpdateBatchSize;
99
+ const keys = Array.from(this.db.data.keys());
100
+
101
+ for (let i = 0; i < keys.length; i += batchSize) {
102
+ const batchKeys = keys.slice(i, i + batchSize);
103
+
104
+ for (const key of batchKeys) {
105
+ const value = this.db.data.get(key);
106
+ const fieldValue = this._getFieldValue(value, fieldName);
107
+
108
+ if (fieldValue !== undefined) {
109
+ index.add(fieldValue, key);
110
+ entries++;
111
+ }
112
+ }
113
+
114
+ // Yield to event loop for large datasets
115
+ if (this.options.backgroundIndexing) {
116
+ await new Promise(resolve => setImmediate(resolve));
117
+ }
118
+ }
119
+
120
+ const buildTime = Date.now() - startTime;
121
+ console.log(`🔨 Built index in ${buildTime}ms: ${entries} entries`);
122
+
123
+ return entries;
124
+ }
125
+
126
+ /**
127
+ * Drop an index
128
+ */
129
+ dropIndex(indexName) {
130
+ if (!this.indexes.has(indexName)) {
131
+ throw new Error(`Index '${indexName}' does not exist`);
132
+ }
133
+
134
+ const index = this.indexes.get(indexName);
135
+ const fieldName = index.fieldName;
136
+
137
+ // Remove from indexes
138
+ this.indexes.delete(indexName);
139
+
140
+ // Remove from field indexes tracking
141
+ if (this.fieldIndexes.has(fieldName)) {
142
+ this.fieldIndexes.get(fieldName).delete(indexName);
143
+ if (this.fieldIndexes.get(fieldName).size === 0) {
144
+ this.fieldIndexes.delete(fieldName);
145
+ }
146
+ }
147
+
148
+ this.stats.indexesDropped++;
149
+
150
+ console.log(`🗑️ Dropped index: ${indexName}`);
151
+ }
152
+
153
+ /**
154
+ * Get all indexes
155
+ */
156
+ getIndexes() {
157
+ const result = {};
158
+
159
+ for (const [name, index] of this.indexes) {
160
+ result[name] = index.getStats();
161
+ }
162
+
163
+ return result;
164
+ }
165
+
166
+ /**
167
+ * Check if a field has indexes
168
+ */
169
+ hasIndex(fieldName) {
170
+ return this.fieldIndexes.has(fieldName) && this.fieldIndexes.get(fieldName).size > 0;
171
+ }
172
+
173
+ /**
174
+ * Find records using indexes
175
+ */
176
+ find(fieldName, operator, value) {
177
+ const indexes = this.fieldIndexes.get(fieldName);
178
+
179
+ if (!indexes || indexes.size === 0) {
180
+ this.stats.queriesWithoutIndex++;
181
+ return null; // No index available
182
+ }
183
+
184
+ // Try to use the most appropriate index
185
+ for (const indexName of indexes) {
186
+ const index = this.indexes.get(indexName);
187
+
188
+ if (index.supportsOperator(operator)) {
189
+ const results = index.find(operator, value);
190
+
191
+ if (results !== null) {
192
+ this.stats.queriesWithIndex++;
193
+
194
+ if (this.options.debug) {
195
+ console.log(`⚡ Used index ${indexName} for ${fieldName} ${operator} ${value}`);
196
+ }
197
+
198
+ return results;
199
+ }
200
+ }
201
+ }
202
+
203
+ this.stats.queriesWithoutIndex++;
204
+ return null;
205
+ }
206
+
207
+ /**
208
+ * Update index when data changes
209
+ */
210
+ updateIndex(key, newValue, oldValue = null) {
211
+ this.stats.indexUpdates++;
212
+
213
+ // Queue the update for background processing
214
+ this.indexQueue.push({ key, newValue, oldValue });
215
+
216
+ if (!this.isProcessingQueue && this.options.backgroundIndexing) {
217
+ this._processIndexQueue();
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Process index update queue in background
223
+ */
224
+ async _processIndexQueue() {
225
+ if (this.isProcessingQueue || this.indexQueue.length === 0) {
226
+ return;
227
+ }
228
+
229
+ this.isProcessingQueue = true;
230
+ this.stats.backgroundJobs++;
231
+
232
+ while (this.indexQueue.length > 0) {
233
+ const update = this.indexQueue.shift();
234
+
235
+ try {
236
+ await this._processSingleUpdate(update);
237
+ } catch (error) {
238
+ console.error('🚨 Index update failed:', error);
239
+ }
240
+
241
+ // Yield to event loop every 100 updates
242
+ if (this.indexQueue.length % 100 === 0) {
243
+ await new Promise(resolve => setImmediate(resolve));
244
+ }
245
+ }
246
+
247
+ this.isProcessingQueue = false;
248
+ }
249
+
250
+ /**
251
+ * Process single index update
252
+ */
253
+ async _processSingleUpdate(update) {
254
+ const { key, newValue, oldValue } = update;
255
+
256
+ for (const [indexName, index] of this.indexes) {
257
+ const fieldName = index.fieldName;
258
+
259
+ const oldFieldValue = oldValue ? this._getFieldValue(oldValue, fieldName) : undefined;
260
+ const newFieldValue = newValue ? this._getFieldValue(newValue, fieldName) : undefined;
261
+
262
+ // Remove old value from index
263
+ if (oldFieldValue !== undefined) {
264
+ index.remove(oldFieldValue, key);
265
+ }
266
+
267
+ // Add new value to index
268
+ if (newFieldValue !== undefined) {
269
+ index.add(newFieldValue, key);
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Get nested field value using dot notation
276
+ */
277
+ _getFieldValue(obj, fieldPath) {
278
+ if (!fieldPath.includes('.')) {
279
+ return obj[fieldPath];
280
+ }
281
+
282
+ const parts = fieldPath.split('.');
283
+ let value = obj;
284
+
285
+ for (const part of parts) {
286
+ value = value?.[part];
287
+ if (value === undefined) break;
288
+ }
289
+
290
+ return value;
291
+ }
292
+
293
+ /**
294
+ * Generate index name from field and type
295
+ */
296
+ _getIndexName(fieldName, indexType) {
297
+ return `${fieldName}_${indexType}_index`;
298
+ }
299
+
300
+ /**
301
+ * Get performance statistics
302
+ */
303
+ getStats() {
304
+ const totalQueries = this.stats.queriesWithIndex + this.stats.queriesWithoutIndex;
305
+ const indexUsage = totalQueries > 0
306
+ ? (this.stats.queriesWithIndex / totalQueries * 100).toFixed(2)
307
+ : 0;
308
+
309
+ return {
310
+ ...this.stats,
311
+ totalIndexes: this.indexes.size,
312
+ indexUsage: `${indexUsage}%`,
313
+ queuedUpdates: this.indexQueue.length,
314
+ isProcessing: this.isProcessingQueue,
315
+ fieldsWithIndexes: Array.from(this.fieldIndexes.keys())
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Optimize indexes (reclaim memory, rebuild fragmented indexes)
321
+ */
322
+ async optimize() {
323
+ console.log('🔧 Optimizing indexes...');
324
+
325
+ for (const [name, index] of this.indexes) {
326
+ await index.optimize();
327
+ }
328
+
329
+ console.log('✅ Index optimization complete');
330
+ }
331
+
332
+ /**
333
+ * Clear all indexes
334
+ */
335
+ clear() {
336
+ this.indexes.clear();
337
+ this.fieldIndexes.clear();
338
+ this.indexQueue = [];
339
+
340
+ console.log('🧹 Cleared all indexes');
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Base Index class
346
+ */
347
+ class Index {
348
+ constructor(fieldName, options = {}) {
349
+ this.fieldName = fieldName;
350
+ this.options = options;
351
+ this.entries = 0;
352
+ }
353
+
354
+ add(value, key) {
355
+ throw new Error('Method not implemented');
356
+ }
357
+
358
+ remove(value, key) {
359
+ throw new Error('Method not implemented');
360
+ }
361
+
362
+ find(operator, value) {
363
+ throw new Error('Method not implemented');
364
+ }
365
+
366
+ supportsOperator(operator) {
367
+ throw new Error('Method not implemented');
368
+ }
369
+
370
+ optimize() {
371
+ // Default implementation - override if needed
372
+ return Promise.resolve();
373
+ }
374
+
375
+ getStats() {
376
+ return {
377
+ fieldName: this.fieldName,
378
+ entries: this.entries,
379
+ type: this.constructor.name
380
+ };
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Hash Index - for equality queries (=, !=)
386
+ */
387
+ class HashIndex extends Index {
388
+ constructor(fieldName, options = {}) {
389
+ super(fieldName, options);
390
+ this.index = new Map(); // value -> Set of keys
391
+ this.nullKeys = new Set();
392
+ this.undefinedKeys = new Set();
393
+ }
394
+
395
+ add(value, key) {
396
+ if (value === null) {
397
+ this.nullKeys.add(key);
398
+ } else if (value === undefined) {
399
+ this.undefinedKeys.add(key);
400
+ } else {
401
+ if (!this.index.has(value)) {
402
+ this.index.set(value, new Set());
403
+ }
404
+ this.index.get(value).add(key);
405
+ }
406
+ this.entries++;
407
+ }
408
+
409
+ remove(value, key) {
410
+ if (value === null) {
411
+ this.nullKeys.delete(key);
412
+ } else if (value === undefined) {
413
+ this.undefinedKeys.delete(key);
414
+ } else {
415
+ const keys = this.index.get(value);
416
+ if (keys) {
417
+ keys.delete(key);
418
+ if (keys.size === 0) {
419
+ this.index.delete(value);
420
+ }
421
+ }
422
+ }
423
+ this.entries--;
424
+ }
425
+
426
+ find(operator, value) {
427
+ switch (operator) {
428
+ case '=':
429
+ return this._findEquals(value);
430
+ case '!=':
431
+ return this._findNotEquals(value);
432
+ case 'in':
433
+ return this._findIn(value);
434
+ default:
435
+ return null;
436
+ }
437
+ }
438
+
439
+ supportsOperator(operator) {
440
+ return ['=', '!=', 'in'].includes(operator);
441
+ }
442
+
443
+ _findEquals(value) {
444
+ if (value === null) {
445
+ return Array.from(this.nullKeys);
446
+ }
447
+
448
+ const keys = this.index.get(value);
449
+ return keys ? Array.from(keys) : [];
450
+ }
451
+
452
+ _findNotEquals(value) {
453
+ const allKeys = new Set();
454
+
455
+ // Add all keys from other values
456
+ for (const [val, keys] of this.index) {
457
+ if (val !== value) {
458
+ for (const key of keys) {
459
+ allKeys.add(key);
460
+ }
461
+ }
462
+ }
463
+
464
+ // Add null/undefined keys if not searching for them
465
+ if (value !== null) {
466
+ for (const key of this.nullKeys) {
467
+ allKeys.add(key);
468
+ }
469
+ }
470
+ if (value !== undefined) {
471
+ for (const key of this.undefinedKeys) {
472
+ allKeys.add(key);
473
+ }
474
+ }
475
+
476
+ return Array.from(allKeys);
477
+ }
478
+
479
+ _findIn(values) {
480
+ if (!Array.isArray(values)) {
481
+ return null;
482
+ }
483
+
484
+ const result = new Set();
485
+
486
+ for (const value of values) {
487
+ const keys = this._findEquals(value);
488
+ for (const key of keys) {
489
+ result.add(key);
490
+ }
491
+ }
492
+
493
+ return Array.from(result);
494
+ }
495
+
496
+ getStats() {
497
+ return {
498
+ ...super.getStats(),
499
+ uniqueValues: this.index.size,
500
+ nullEntries: this.nullKeys.size,
501
+ undefinedEntries: this.undefinedKeys.size
502
+ };
503
+ }
504
+ }
505
+
506
+ /**
507
+ * Range Index - for comparison queries (>, <, >=, <=)
508
+ */
509
+ class RangeIndex extends Index {
510
+ constructor(fieldName, options = {}) {
511
+ super(fieldName, options);
512
+ this.sortedValues = []; // Array of {value, key} sorted by value
513
+ this.valueMap = new Map(); // value -> Array of keys
514
+ }
515
+
516
+ add(value, key) {
517
+ if (typeof value !== 'number' && typeof value !== 'string') {
518
+ return; // Only support numbers and strings for range queries
519
+ }
520
+
521
+ if (!this.valueMap.has(value)) {
522
+ this.valueMap.set(value, []);
523
+
524
+ // Insert into sorted array (maintain sorted order)
525
+ const index = this._findInsertionIndex(value);
526
+ this.sortedValues.splice(index, 0, { value, key });
527
+ }
528
+
529
+ this.valueMap.get(value).push(key);
530
+ this.entries++;
531
+ }
532
+
533
+ remove(value, key) {
534
+ const keys = this.valueMap.get(value);
535
+ if (keys) {
536
+ const keyIndex = keys.indexOf(key);
537
+ if (keyIndex > -1) {
538
+ keys.splice(keyIndex, 1);
539
+
540
+ if (keys.length === 0) {
541
+ this.valueMap.delete(value);
542
+
543
+ // Remove from sorted array
544
+ const index = this._findValueIndex(value);
545
+ if (index > -1) {
546
+ this.sortedValues.splice(index, 1);
547
+ }
548
+ }
549
+ }
550
+ }
551
+ this.entries--;
552
+ }
553
+
554
+ find(operator, value) {
555
+ switch (operator) {
556
+ case '>':
557
+ return this._findGreaterThan(value, false);
558
+ case '>=':
559
+ return this._findGreaterThan(value, true);
560
+ case '<':
561
+ return this._findLessThan(value, false);
562
+ case '<=':
563
+ return this._findLessThan(value, true);
564
+ default:
565
+ return null;
566
+ }
567
+ }
568
+
569
+ supportsOperator(operator) {
570
+ return ['>', '>=', '<', '<='].includes(operator);
571
+ }
572
+
573
+ _findGreaterThan(value, inclusive) {
574
+ const startIndex = this._findFirstIndexGreaterThan(value, inclusive);
575
+ if (startIndex === -1) return [];
576
+
577
+ const result = [];
578
+ for (let i = startIndex; i < this.sortedValues.length; i++) {
579
+ const { value: val, key } = this.sortedValues[i];
580
+ const keys = this.valueMap.get(val);
581
+ result.push(...keys);
582
+ }
583
+
584
+ return result;
585
+ }
586
+
587
+ _findLessThan(value, inclusive) {
588
+ const endIndex = this._findLastIndexLessThan(value, inclusive);
589
+ if (endIndex === -1) return [];
590
+
591
+ const result = [];
592
+ for (let i = 0; i <= endIndex; i++) {
593
+ const { value: val, key } = this.sortedValues[i];
594
+ const keys = this.valueMap.get(val);
595
+ result.push(...keys);
596
+ }
597
+
598
+ return result;
599
+ }
600
+
601
+ _findFirstIndexGreaterThan(value, inclusive) {
602
+ let low = 0;
603
+ let high = this.sortedValues.length - 1;
604
+ let result = -1;
605
+
606
+ while (low <= high) {
607
+ const mid = Math.floor((low + high) / 2);
608
+ const midValue = this.sortedValues[mid].value;
609
+
610
+ if (inclusive ? midValue >= value : midValue > value) {
611
+ result = mid;
612
+ high = mid - 1;
613
+ } else {
614
+ low = mid + 1;
615
+ }
616
+ }
617
+
618
+ return result;
619
+ }
620
+
621
+ _findLastIndexLessThan(value, inclusive) {
622
+ let low = 0;
623
+ let high = this.sortedValues.length - 1;
624
+ let result = -1;
625
+
626
+ while (low <= high) {
627
+ const mid = Math.floor((low + high) / 2);
628
+ const midValue = this.sortedValues[mid].value;
629
+
630
+ if (inclusive ? midValue <= value : midValue < value) {
631
+ result = mid;
632
+ low = mid + 1;
633
+ } else {
634
+ high = mid - 1;
635
+ }
636
+ }
637
+
638
+ return result;
639
+ }
640
+
641
+ _findInsertionIndex(value) {
642
+ let low = 0;
643
+ let high = this.sortedValues.length;
644
+
645
+ while (low < high) {
646
+ const mid = Math.floor((low + high) / 2);
647
+ if (this.sortedValues[mid].value < value) {
648
+ low = mid + 1;
649
+ } else {
650
+ high = mid;
651
+ }
652
+ }
653
+
654
+ return low;
655
+ }
656
+
657
+ _findValueIndex(value) {
658
+ let low = 0;
659
+ let high = this.sortedValues.length - 1;
660
+
661
+ while (low <= high) {
662
+ const mid = Math.floor((low + high) / 2);
663
+ const midValue = this.sortedValues[mid].value;
664
+
665
+ if (midValue === value) {
666
+ return mid;
667
+ } else if (midValue < value) {
668
+ low = mid + 1;
669
+ } else {
670
+ high = mid - 1;
671
+ }
672
+ }
673
+
674
+ return -1;
675
+ }
676
+
677
+ optimize() {
678
+ // Re-sort the array (should already be sorted, but just in case)
679
+ this.sortedValues.sort((a, b) => {
680
+ if (a.value < b.value) return -1;
681
+ if (a.value > b.value) return 1;
682
+ return 0;
683
+ });
684
+ }
685
+
686
+ getStats() {
687
+ return {
688
+ ...super.getStats(),
689
+ valueRange: this.sortedValues.length > 0
690
+ ? [this.sortedValues[0].value, this.sortedValues[this.sortedValues.length - 1].value]
691
+ : null
692
+ };
693
+ }
694
+ }
695
+
696
+ /**
697
+ * Text Index - for text search queries (contains, startsWith, endsWith)
698
+ */
699
+ class TextIndex extends Index {
700
+ constructor(fieldName, options = {}) {
701
+ super(fieldName, options);
702
+ this.trie = new Map(); // Simple prefix tree implementation
703
+ this.keys = new Set();
704
+ }
705
+
706
+ add(value, key) {
707
+ if (typeof value !== 'string') return;
708
+
709
+ const words = value.toLowerCase().split(/\W+/).filter(word => word.length > 0);
710
+
711
+ for (const word of words) {
712
+ if (!this.trie.has(word)) {
713
+ this.trie.set(word, new Set());
714
+ }
715
+ this.trie.get(word).add(key);
716
+ }
717
+
718
+ this.keys.add(key);
719
+ this.entries++;
720
+ }
721
+
722
+ remove(value, key) {
723
+ if (typeof value !== 'string') return;
724
+
725
+ const words = value.toLowerCase().split(/\W+/).filter(word => word.length > 0);
726
+
727
+ for (const word of words) {
728
+ const keys = this.trie.get(word);
729
+ if (keys) {
730
+ keys.delete(key);
731
+ if (keys.size === 0) {
732
+ this.trie.delete(word);
733
+ }
734
+ }
735
+ }
736
+
737
+ this.keys.delete(key);
738
+ this.entries--;
739
+ }
740
+
741
+ find(operator, value) {
742
+ if (typeof value !== 'string') return null;
743
+
744
+ switch (operator) {
745
+ case 'contains':
746
+ return this._findContains(value);
747
+ case 'startsWith':
748
+ return this._findStartsWith(value);
749
+ case 'endsWith':
750
+ return this._findEndsWith(value);
751
+ default:
752
+ return null;
753
+ }
754
+ }
755
+
756
+ supportsOperator(operator) {
757
+ return ['contains', 'startsWith', 'endsWith'].includes(operator);
758
+ }
759
+
760
+ _findContains(searchTerm) {
761
+ const term = searchTerm.toLowerCase();
762
+ const result = new Set();
763
+
764
+ for (const [word, keys] of this.trie) {
765
+ if (word.includes(term)) {
766
+ for (const key of keys) {
767
+ result.add(key);
768
+ }
769
+ }
770
+ }
771
+
772
+ return Array.from(result);
773
+ }
774
+
775
+ _findStartsWith(prefix) {
776
+ const prefixLower = prefix.toLowerCase();
777
+ const result = new Set();
778
+
779
+ for (const [word, keys] of this.trie) {
780
+ if (word.startsWith(prefixLower)) {
781
+ for (const key of keys) {
782
+ result.add(key);
783
+ }
784
+ }
785
+ }
786
+
787
+ return Array.from(result);
788
+ }
789
+
790
+ _findEndsWith(suffix) {
791
+ const suffixLower = suffix.toLowerCase();
792
+ const result = new Set();
793
+
794
+ for (const [word, keys] of this.trie) {
795
+ if (word.endsWith(suffixLower)) {
796
+ for (const key of keys) {
797
+ result.add(key);
798
+ }
799
+ }
800
+ }
801
+
802
+ return Array.from(result);
803
+ }
804
+
805
+ getStats() {
806
+ return {
807
+ ...super.getStats(),
808
+ uniqueWords: this.trie.size,
809
+ indexedKeys: this.keys.size
810
+ };
811
+ }
812
+ }
813
+
814
+ module.exports = IndexManager;