jexidb 2.1.9 → 2.2.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/LICENSE +21 -21
- package/dist/Database.cjs +964 -773
- package/package.json +82 -82
- package/src/Database.mjs +6570 -6491
- package/src/FileHandler.mjs +1439 -1439
- package/src/SchemaManager.mjs +297 -297
- package/src/Serializer.mjs +777 -777
- package/src/managers/IndexManager.mjs +2435 -2371
- package/src/managers/QueryManager.mjs +1619 -1552
- package/src/managers/TermManager.mjs +285 -285
package/dist/Database.cjs
CHANGED
|
@@ -418,6 +418,10 @@ class IndexManager {
|
|
|
418
418
|
this.rangeThreshold = 10; // Sensible threshold: 10+ consecutive numbers justify ranges
|
|
419
419
|
this.binarySearchThreshold = 32; // Much higher for better performance
|
|
420
420
|
this.database = database; // Reference to database for term manager access
|
|
421
|
+
this.indexLoaded = false;
|
|
422
|
+
this.indexIdleUnloadMs = typeof this.opts.indexIdleUnloadMs === 'number' ? this.opts.indexIdleUnloadMs : 30000;
|
|
423
|
+
this._idleUnloadTimer = null;
|
|
424
|
+
this._indexLoadPromise = null;
|
|
421
425
|
|
|
422
426
|
// CRITICAL: Use database mutex to prevent deadlocks
|
|
423
427
|
// If no database mutex provided, create a local one (for backward compatibility)
|
|
@@ -429,9 +433,9 @@ class IndexManager {
|
|
|
429
433
|
this.totalLines = total;
|
|
430
434
|
}
|
|
431
435
|
|
|
432
|
-
/**
|
|
433
|
-
* Update indexes configuration and ensure internal structures stay in sync
|
|
434
|
-
* @param {Object|Array<string>} indexes
|
|
436
|
+
/**
|
|
437
|
+
* Update indexes configuration and ensure internal structures stay in sync
|
|
438
|
+
* @param {Object|Array<string>} indexes
|
|
435
439
|
*/
|
|
436
440
|
setIndexesConfig(indexes) {
|
|
437
441
|
if (!indexes) {
|
|
@@ -463,11 +467,57 @@ class IndexManager {
|
|
|
463
467
|
}
|
|
464
468
|
}
|
|
465
469
|
}
|
|
470
|
+
_clearIdleUnloadTimer() {
|
|
471
|
+
if (this._idleUnloadTimer) {
|
|
472
|
+
clearTimeout(this._idleUnloadTimer);
|
|
473
|
+
this._idleUnloadTimer = null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
cancelIdleUnloadTimer() {
|
|
477
|
+
this._clearIdleUnloadTimer();
|
|
478
|
+
}
|
|
479
|
+
markIndexUsed() {
|
|
480
|
+
if (!this.indexLoaded) return;
|
|
481
|
+
this._clearIdleUnloadTimer();
|
|
482
|
+
if (this.indexIdleUnloadMs > 0) {
|
|
483
|
+
const timer = setTimeout(() => {
|
|
484
|
+
try {
|
|
485
|
+
this.unload();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
if (this.opts.debugMode) {
|
|
488
|
+
console.error('⚠️ IndexManager.markIndexUsed: Failed to unload index', error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}, this.indexIdleUnloadMs);
|
|
492
|
+
if (timer && typeof timer.unref === 'function') {
|
|
493
|
+
timer.unref();
|
|
494
|
+
}
|
|
495
|
+
this._idleUnloadTimer = timer;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
unload() {
|
|
499
|
+
if (!this.indexLoaded) return;
|
|
500
|
+
if (this.opts.debugMode) {
|
|
501
|
+
console.log(`🧹 IndexManager.unload: Unloading index data after ${this.indexIdleUnloadMs}ms idle`);
|
|
502
|
+
}
|
|
466
503
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
504
|
+
// Mark rebuild as needed so score() can trigger lazy reload
|
|
505
|
+
if (this.database) {
|
|
506
|
+
this.database._indexRebuildNeeded = true;
|
|
507
|
+
}
|
|
508
|
+
this._clearIdleUnloadTimer();
|
|
509
|
+
const newData = {};
|
|
510
|
+
for (const field of this.indexedFields) {
|
|
511
|
+
newData[field] = {};
|
|
512
|
+
}
|
|
513
|
+
this.index.data = newData;
|
|
514
|
+
this.indexLoaded = false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Check if a field is configured as an index
|
|
519
|
+
* @param {string} field - Field name
|
|
520
|
+
* @returns {boolean}
|
|
471
521
|
*/
|
|
472
522
|
isFieldIndexed(field) {
|
|
473
523
|
if (!field) return false;
|
|
@@ -477,10 +527,10 @@ class IndexManager {
|
|
|
477
527
|
return this.indexedFields.includes(field);
|
|
478
528
|
}
|
|
479
529
|
|
|
480
|
-
/**
|
|
481
|
-
* Determine whether the index has usable data for a given field
|
|
482
|
-
* @param {string} field - Field name
|
|
483
|
-
* @returns {boolean}
|
|
530
|
+
/**
|
|
531
|
+
* Determine whether the index has usable data for a given field
|
|
532
|
+
* @param {string} field - Field name
|
|
533
|
+
* @returns {boolean}
|
|
484
534
|
*/
|
|
485
535
|
hasUsableIndexData(field) {
|
|
486
536
|
if (!field) return false;
|
|
@@ -875,6 +925,7 @@ class IndexManager {
|
|
|
875
925
|
|
|
876
926
|
// OPTIMIZATION 6: Ultra-fast add operation with incremental index updates
|
|
877
927
|
async add(row, lineNumber) {
|
|
928
|
+
this.markIndexUsed();
|
|
878
929
|
if (typeof row !== 'object' || !row) {
|
|
879
930
|
throw new Error('Invalid \'row\' parameter, it must be an object');
|
|
880
931
|
}
|
|
@@ -950,12 +1001,13 @@ class IndexManager {
|
|
|
950
1001
|
}
|
|
951
1002
|
}
|
|
952
1003
|
|
|
953
|
-
/**
|
|
954
|
-
* OPTIMIZATION 6: Add multiple records to the index in batch with optimized operations
|
|
955
|
-
* @param {Array} records - Records to add
|
|
956
|
-
* @param {number} startLineNumber - Starting line number
|
|
1004
|
+
/**
|
|
1005
|
+
* OPTIMIZATION 6: Add multiple records to the index in batch with optimized operations
|
|
1006
|
+
* @param {Array} records - Records to add
|
|
1007
|
+
* @param {number} startLineNumber - Starting line number
|
|
957
1008
|
*/
|
|
958
1009
|
async addBatch(records, startLineNumber) {
|
|
1010
|
+
this.markIndexUsed();
|
|
959
1011
|
if (!records || !records.length) return;
|
|
960
1012
|
|
|
961
1013
|
// OPTIMIZATION 6: Pre-allocate index structures for better performance
|
|
@@ -1072,6 +1124,7 @@ class IndexManager {
|
|
|
1072
1124
|
|
|
1073
1125
|
// Cleanup method to free memory
|
|
1074
1126
|
cleanup() {
|
|
1127
|
+
this._clearIdleUnloadTimer();
|
|
1075
1128
|
const data = this.index.data;
|
|
1076
1129
|
for (const field in data) {
|
|
1077
1130
|
for (const value in data[field]) {
|
|
@@ -1096,12 +1149,14 @@ class IndexManager {
|
|
|
1096
1149
|
|
|
1097
1150
|
// Clear all indexes
|
|
1098
1151
|
clear() {
|
|
1152
|
+
this._clearIdleUnloadTimer();
|
|
1099
1153
|
this.index.data = {};
|
|
1100
1154
|
this.totalLines = 0;
|
|
1101
1155
|
}
|
|
1102
1156
|
|
|
1103
1157
|
// Update a record in the index
|
|
1104
1158
|
async update(oldRecord, newRecord, lineNumber = null) {
|
|
1159
|
+
this.markIndexUsed();
|
|
1105
1160
|
if (!oldRecord || !newRecord) return;
|
|
1106
1161
|
|
|
1107
1162
|
// Remove old record by ID
|
|
@@ -1130,6 +1185,7 @@ class IndexManager {
|
|
|
1130
1185
|
|
|
1131
1186
|
// Remove a record from the index
|
|
1132
1187
|
async remove(record) {
|
|
1188
|
+
this.markIndexUsed();
|
|
1133
1189
|
if (!record) return;
|
|
1134
1190
|
|
|
1135
1191
|
// If record is an array of line numbers, use the original method
|
|
@@ -1307,6 +1363,7 @@ class IndexManager {
|
|
|
1307
1363
|
|
|
1308
1364
|
// Ultra-fast query with early exit and smart processing
|
|
1309
1365
|
query(criteria, options = {}) {
|
|
1366
|
+
this.markIndexUsed();
|
|
1310
1367
|
if (typeof options === 'boolean') {
|
|
1311
1368
|
options = {
|
|
1312
1369
|
matchAny: options
|
|
@@ -1772,36 +1829,37 @@ class IndexManager {
|
|
|
1772
1829
|
return matchingLines || new Set();
|
|
1773
1830
|
}
|
|
1774
1831
|
|
|
1775
|
-
/**
|
|
1776
|
-
* Check if any records exist for given field and terms (index-only, ultra-fast)
|
|
1777
|
-
* Stops at first match for maximum performance - no disk I/O required
|
|
1778
|
-
*
|
|
1779
|
-
* @param {string} fieldName - Indexed field name (e.g., 'nameTerms', 'groupTerms')
|
|
1780
|
-
* @param {string|Array<string>} terms - Single term or array of terms to check
|
|
1781
|
-
* @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
|
|
1782
|
-
* @returns {boolean} - True if at least one match exists
|
|
1783
|
-
*
|
|
1784
|
-
* @example
|
|
1785
|
-
* // Check if any record has 'channel' in nameTerms
|
|
1786
|
-
* indexManager.exists('nameTerms', 'channel')
|
|
1787
|
-
*
|
|
1788
|
-
* @example
|
|
1789
|
-
* // Check if any record has ALL terms ['a', 'e'] in nameTerms ($all)
|
|
1790
|
-
* indexManager.exists('nameTerms', ['a', 'e'], { $all: true })
|
|
1791
|
-
*
|
|
1792
|
-
* @example
|
|
1793
|
-
* // Check if any record has ANY of the terms ['channel', 'tv'] in nameTerms
|
|
1794
|
-
* indexManager.exists('nameTerms', ['channel', 'tv'], { $all: false })
|
|
1795
|
-
*
|
|
1796
|
-
* @example
|
|
1797
|
-
* // Check if any record has 'tv' but NOT 'globo' in nameTerms
|
|
1798
|
-
* indexManager.exists('nameTerms', 'tv', { excludes: ['globo'] })
|
|
1799
|
-
*
|
|
1800
|
-
* @example
|
|
1801
|
-
* // Check if any record has ['tv', 'news'] but NOT 'sports' in nameTerms
|
|
1802
|
-
* indexManager.exists('nameTerms', ['tv', 'news'], { $all: true, excludes: ['sports'] })
|
|
1832
|
+
/**
|
|
1833
|
+
* Check if any records exist for given field and terms (index-only, ultra-fast)
|
|
1834
|
+
* Stops at first match for maximum performance - no disk I/O required
|
|
1835
|
+
*
|
|
1836
|
+
* @param {string} fieldName - Indexed field name (e.g., 'nameTerms', 'groupTerms')
|
|
1837
|
+
* @param {string|Array<string>} terms - Single term or array of terms to check
|
|
1838
|
+
* @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
|
|
1839
|
+
* @returns {boolean} - True if at least one match exists
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* // Check if any record has 'channel' in nameTerms
|
|
1843
|
+
* indexManager.exists('nameTerms', 'channel')
|
|
1844
|
+
*
|
|
1845
|
+
* @example
|
|
1846
|
+
* // Check if any record has ALL terms ['a', 'e'] in nameTerms ($all)
|
|
1847
|
+
* indexManager.exists('nameTerms', ['a', 'e'], { $all: true })
|
|
1848
|
+
*
|
|
1849
|
+
* @example
|
|
1850
|
+
* // Check if any record has ANY of the terms ['channel', 'tv'] in nameTerms
|
|
1851
|
+
* indexManager.exists('nameTerms', ['channel', 'tv'], { $all: false })
|
|
1852
|
+
*
|
|
1853
|
+
* @example
|
|
1854
|
+
* // Check if any record has 'tv' but NOT 'globo' in nameTerms
|
|
1855
|
+
* indexManager.exists('nameTerms', 'tv', { excludes: ['globo'] })
|
|
1856
|
+
*
|
|
1857
|
+
* @example
|
|
1858
|
+
* // Check if any record has ['tv', 'news'] but NOT 'sports' in nameTerms
|
|
1859
|
+
* indexManager.exists('nameTerms', ['tv', 'news'], { $all: true, excludes: ['sports'] })
|
|
1803
1860
|
*/
|
|
1804
1861
|
exists(fieldName, terms, options = {}) {
|
|
1862
|
+
this.markIndexUsed();
|
|
1805
1863
|
// Early exit: validate fieldName
|
|
1806
1864
|
if (!fieldName || typeof fieldName !== 'string') {
|
|
1807
1865
|
return false;
|
|
@@ -2154,16 +2212,17 @@ class IndexManager {
|
|
|
2154
2212
|
return candidateLines.size > 0;
|
|
2155
2213
|
}
|
|
2156
2214
|
|
|
2157
|
-
/**
|
|
2158
|
-
* Evaluate many index-only existence checks while sharing the same field data.
|
|
2159
|
-
* Returns a map keyed by the criteria id so callers can reuse a single scan per field.
|
|
2160
|
-
*
|
|
2161
|
-
* @param {string} fieldName - Indexed field name
|
|
2162
|
-
* @param {Array<Object>} criteriaArray - Array of { id, terms, options }
|
|
2163
|
-
* @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
|
|
2164
|
-
* @returns {Object<string, boolean>} - Map of criteria id to boolean existence
|
|
2215
|
+
/**
|
|
2216
|
+
* Evaluate many index-only existence checks while sharing the same field data.
|
|
2217
|
+
* Returns a map keyed by the criteria id so callers can reuse a single scan per field.
|
|
2218
|
+
*
|
|
2219
|
+
* @param {string} fieldName - Indexed field name
|
|
2220
|
+
* @param {Array<Object>} criteriaArray - Array of { id, terms, options }
|
|
2221
|
+
* @param {Object} opts - { limit, allowPartial } (limit only applies when allowPartial is true)
|
|
2222
|
+
* @returns {Object<string, boolean>} - Map of criteria id to boolean existence
|
|
2165
2223
|
*/
|
|
2166
2224
|
multiExists(fieldName, criteriaArray, opts = {}) {
|
|
2225
|
+
this.markIndexUsed();
|
|
2167
2226
|
const results = {};
|
|
2168
2227
|
if (!Array.isArray(criteriaArray) || criteriaArray.length === 0) {
|
|
2169
2228
|
return results;
|
|
@@ -2549,6 +2608,8 @@ class IndexManager {
|
|
|
2549
2608
|
console.log(`🔍 IndexManager.load: No data loaded, preserving initialized fields: ${Object.keys(this.index.data).join(', ')}`);
|
|
2550
2609
|
}
|
|
2551
2610
|
// Keep the current index with initialized fields
|
|
2611
|
+
this.indexLoaded = true;
|
|
2612
|
+
this.markIndexUsed();
|
|
2552
2613
|
return;
|
|
2553
2614
|
}
|
|
2554
2615
|
|
|
@@ -2560,11 +2621,13 @@ class IndexManager {
|
|
|
2560
2621
|
}
|
|
2561
2622
|
}
|
|
2562
2623
|
this.index = processedIndex;
|
|
2624
|
+
this.indexLoaded = true;
|
|
2625
|
+
this.markIndexUsed();
|
|
2563
2626
|
}
|
|
2564
2627
|
|
|
2565
|
-
/**
|
|
2566
|
-
* CRITICAL FIX: Initialize default index structure when invalid data is provided
|
|
2567
|
-
* This prevents TypeError when Object.keys() is called on null/undefined
|
|
2628
|
+
/**
|
|
2629
|
+
* CRITICAL FIX: Initialize default index structure when invalid data is provided
|
|
2630
|
+
* This prevents TypeError when Object.keys() is called on null/undefined
|
|
2568
2631
|
*/
|
|
2569
2632
|
_initializeDefaults() {
|
|
2570
2633
|
if (this.opts.debugMode) {
|
|
@@ -2585,17 +2648,13 @@ class IndexManager {
|
|
|
2585
2648
|
}
|
|
2586
2649
|
}
|
|
2587
2650
|
}
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
}
|
|
2591
|
-
}
|
|
2592
|
-
readColumnIndex(column) {
|
|
2593
|
-
return new Set(this.index.data && this.index.data[column] ? Object.keys(this.index.data[column]) : []);
|
|
2651
|
+
this.indexLoaded = true;
|
|
2652
|
+
this.markIndexUsed();
|
|
2594
2653
|
}
|
|
2595
2654
|
|
|
2596
|
-
/**
|
|
2597
|
-
* Convert index to JSON-serializable format for debugging and export
|
|
2598
|
-
* This resolves the issue where Sets appear as empty objects in JSON.stringify
|
|
2655
|
+
/**
|
|
2656
|
+
* Convert index to JSON-serializable format for debugging and export
|
|
2657
|
+
* This resolves the issue where Sets appear as empty objects in JSON.stringify
|
|
2599
2658
|
*/
|
|
2600
2659
|
toJSON() {
|
|
2601
2660
|
const serializable = {
|
|
@@ -2661,9 +2720,9 @@ class IndexManager {
|
|
|
2661
2720
|
return serializable;
|
|
2662
2721
|
}
|
|
2663
2722
|
|
|
2664
|
-
/**
|
|
2665
|
-
* Get a JSON string representation of the index
|
|
2666
|
-
* This properly handles Sets unlike the default JSON.stringify
|
|
2723
|
+
/**
|
|
2724
|
+
* Get a JSON string representation of the index
|
|
2725
|
+
* This properly handles Sets unlike the default JSON.stringify
|
|
2667
2726
|
*/
|
|
2668
2727
|
toString() {
|
|
2669
2728
|
return JSON.stringify(this.toJSON(), null, 2);
|
|
@@ -2671,8 +2730,8 @@ class IndexManager {
|
|
|
2671
2730
|
|
|
2672
2731
|
// Simplified term mapping methods - just basic functionality
|
|
2673
2732
|
|
|
2674
|
-
/**
|
|
2675
|
-
* Rebuild index (stub for compatibility)
|
|
2733
|
+
/**
|
|
2734
|
+
* Rebuild index (stub for compatibility)
|
|
2676
2735
|
*/
|
|
2677
2736
|
async rebuild() {
|
|
2678
2737
|
// Stub implementation for compatibility
|
|
@@ -2680,9 +2739,9 @@ class IndexManager {
|
|
|
2680
2739
|
}
|
|
2681
2740
|
}
|
|
2682
2741
|
|
|
2683
|
-
/**
|
|
2684
|
-
* SchemaManager - Manages field schemas for optimized array-based serialization
|
|
2685
|
-
* This replaces the need for repeating field names in JSON objects
|
|
2742
|
+
/**
|
|
2743
|
+
* SchemaManager - Manages field schemas for optimized array-based serialization
|
|
2744
|
+
* This replaces the need for repeating field names in JSON objects
|
|
2686
2745
|
*/
|
|
2687
2746
|
class SchemaManager {
|
|
2688
2747
|
constructor(opts = {}) {
|
|
@@ -2700,8 +2759,8 @@ class SchemaManager {
|
|
|
2700
2759
|
this.isInitialized = false;
|
|
2701
2760
|
}
|
|
2702
2761
|
|
|
2703
|
-
/**
|
|
2704
|
-
* Initialize schema from options or auto-detect from data
|
|
2762
|
+
/**
|
|
2763
|
+
* Initialize schema from options or auto-detect from data
|
|
2705
2764
|
*/
|
|
2706
2765
|
initializeSchema(schemaOrData, autoDetect = false) {
|
|
2707
2766
|
if (this.isInitialized && this.opts.strictSchema) {
|
|
@@ -2726,8 +2785,8 @@ class SchemaManager {
|
|
|
2726
2785
|
}
|
|
2727
2786
|
}
|
|
2728
2787
|
|
|
2729
|
-
/**
|
|
2730
|
-
* Set explicit schema
|
|
2788
|
+
/**
|
|
2789
|
+
* Set explicit schema
|
|
2731
2790
|
*/
|
|
2732
2791
|
setSchema(fieldNames) {
|
|
2733
2792
|
this.schema = [...fieldNames]; // Create copy
|
|
@@ -2742,8 +2801,8 @@ class SchemaManager {
|
|
|
2742
2801
|
}
|
|
2743
2802
|
}
|
|
2744
2803
|
|
|
2745
|
-
/**
|
|
2746
|
-
* Auto-detect schema from sample data
|
|
2804
|
+
/**
|
|
2805
|
+
* Auto-detect schema from sample data
|
|
2747
2806
|
*/
|
|
2748
2807
|
autoDetectSchema(sampleData) {
|
|
2749
2808
|
if (Array.isArray(sampleData)) {
|
|
@@ -2764,9 +2823,9 @@ class SchemaManager {
|
|
|
2764
2823
|
}
|
|
2765
2824
|
}
|
|
2766
2825
|
|
|
2767
|
-
/**
|
|
2768
|
-
* Initialize schema from database options
|
|
2769
|
-
* Note: schema option is no longer supported, use fields instead
|
|
2826
|
+
/**
|
|
2827
|
+
* Initialize schema from database options
|
|
2828
|
+
* Note: schema option is no longer supported, use fields instead
|
|
2770
2829
|
*/
|
|
2771
2830
|
initializeFromOptions(opts) {
|
|
2772
2831
|
// Schema option is no longer supported - fields should be used instead
|
|
@@ -2774,8 +2833,8 @@ class SchemaManager {
|
|
|
2774
2833
|
// Schema initialization is handled by Database.initializeSchema() using fields
|
|
2775
2834
|
}
|
|
2776
2835
|
|
|
2777
|
-
/**
|
|
2778
|
-
* Add new field to schema (for schema evolution)
|
|
2836
|
+
/**
|
|
2837
|
+
* Add new field to schema (for schema evolution)
|
|
2779
2838
|
*/
|
|
2780
2839
|
addField(fieldName) {
|
|
2781
2840
|
if (this.fieldToIndex.has(fieldName)) {
|
|
@@ -2791,8 +2850,8 @@ class SchemaManager {
|
|
|
2791
2850
|
return newIndex;
|
|
2792
2851
|
}
|
|
2793
2852
|
|
|
2794
|
-
/**
|
|
2795
|
-
* Convert object to array using schema with strict field enforcement
|
|
2853
|
+
/**
|
|
2854
|
+
* Convert object to array using schema with strict field enforcement
|
|
2796
2855
|
*/
|
|
2797
2856
|
objectToArray(obj) {
|
|
2798
2857
|
if (!this.isInitialized || !this.opts.enableArraySerialization) {
|
|
@@ -2818,8 +2877,8 @@ class SchemaManager {
|
|
|
2818
2877
|
return result;
|
|
2819
2878
|
}
|
|
2820
2879
|
|
|
2821
|
-
/**
|
|
2822
|
-
* Convert array back to object using schema
|
|
2880
|
+
/**
|
|
2881
|
+
* Convert array back to object using schema
|
|
2823
2882
|
*/
|
|
2824
2883
|
arrayToObject(arr) {
|
|
2825
2884
|
if (!this.isInitialized || !this.opts.enableArraySerialization) {
|
|
@@ -2867,43 +2926,43 @@ class SchemaManager {
|
|
|
2867
2926
|
return obj;
|
|
2868
2927
|
}
|
|
2869
2928
|
|
|
2870
|
-
/**
|
|
2871
|
-
* Get field index by name
|
|
2929
|
+
/**
|
|
2930
|
+
* Get field index by name
|
|
2872
2931
|
*/
|
|
2873
2932
|
getFieldIndex(fieldName) {
|
|
2874
2933
|
return this.fieldToIndex.get(fieldName);
|
|
2875
2934
|
}
|
|
2876
2935
|
|
|
2877
|
-
/**
|
|
2878
|
-
* Get field name by index
|
|
2936
|
+
/**
|
|
2937
|
+
* Get field name by index
|
|
2879
2938
|
*/
|
|
2880
2939
|
getFieldName(index) {
|
|
2881
2940
|
return this.indexToField.get(index);
|
|
2882
2941
|
}
|
|
2883
2942
|
|
|
2884
|
-
/**
|
|
2885
|
-
* Check if field exists in schema
|
|
2943
|
+
/**
|
|
2944
|
+
* Check if field exists in schema
|
|
2886
2945
|
*/
|
|
2887
2946
|
hasField(fieldName) {
|
|
2888
2947
|
return this.fieldToIndex.has(fieldName);
|
|
2889
2948
|
}
|
|
2890
2949
|
|
|
2891
|
-
/**
|
|
2892
|
-
* Get schema as array of field names
|
|
2950
|
+
/**
|
|
2951
|
+
* Get schema as array of field names
|
|
2893
2952
|
*/
|
|
2894
2953
|
getSchema() {
|
|
2895
2954
|
return [...this.schema]; // Return copy
|
|
2896
2955
|
}
|
|
2897
2956
|
|
|
2898
|
-
/**
|
|
2899
|
-
* Get schema size
|
|
2957
|
+
/**
|
|
2958
|
+
* Get schema size
|
|
2900
2959
|
*/
|
|
2901
2960
|
getSchemaSize() {
|
|
2902
2961
|
return this.schema.length;
|
|
2903
2962
|
}
|
|
2904
2963
|
|
|
2905
|
-
/**
|
|
2906
|
-
* Validate that object conforms to schema
|
|
2964
|
+
/**
|
|
2965
|
+
* Validate that object conforms to schema
|
|
2907
2966
|
*/
|
|
2908
2967
|
validateObject(obj) {
|
|
2909
2968
|
if (!this.isInitialized || !this.opts.strictSchema) {
|
|
@@ -2925,8 +2984,8 @@ class SchemaManager {
|
|
|
2925
2984
|
return true;
|
|
2926
2985
|
}
|
|
2927
2986
|
|
|
2928
|
-
/**
|
|
2929
|
-
* Get schema metadata for serialization
|
|
2987
|
+
/**
|
|
2988
|
+
* Get schema metadata for serialization
|
|
2930
2989
|
*/
|
|
2931
2990
|
getSchemaMetadata() {
|
|
2932
2991
|
return {
|
|
@@ -2937,8 +2996,8 @@ class SchemaManager {
|
|
|
2937
2996
|
};
|
|
2938
2997
|
}
|
|
2939
2998
|
|
|
2940
|
-
/**
|
|
2941
|
-
* Reset schema
|
|
2999
|
+
/**
|
|
3000
|
+
* Reset schema
|
|
2942
3001
|
*/
|
|
2943
3002
|
reset() {
|
|
2944
3003
|
this.schema = [];
|
|
@@ -2948,8 +3007,8 @@ class SchemaManager {
|
|
|
2948
3007
|
this.schemaVersion++;
|
|
2949
3008
|
}
|
|
2950
3009
|
|
|
2951
|
-
/**
|
|
2952
|
-
* Get performance statistics
|
|
3010
|
+
/**
|
|
3011
|
+
* Get performance statistics
|
|
2953
3012
|
*/
|
|
2954
3013
|
getStats() {
|
|
2955
3014
|
return {
|
|
@@ -2992,22 +3051,22 @@ class Serializer {
|
|
|
2992
3051
|
};
|
|
2993
3052
|
}
|
|
2994
3053
|
|
|
2995
|
-
/**
|
|
2996
|
-
* Initialize schema for array-based serialization
|
|
3054
|
+
/**
|
|
3055
|
+
* Initialize schema for array-based serialization
|
|
2997
3056
|
*/
|
|
2998
3057
|
initializeSchema(schemaOrData, autoDetect = false) {
|
|
2999
3058
|
this.schemaManager.initializeSchema(schemaOrData, autoDetect);
|
|
3000
3059
|
}
|
|
3001
3060
|
|
|
3002
|
-
/**
|
|
3003
|
-
* Get current schema
|
|
3061
|
+
/**
|
|
3062
|
+
* Get current schema
|
|
3004
3063
|
*/
|
|
3005
3064
|
getSchema() {
|
|
3006
3065
|
return this.schemaManager.getSchema();
|
|
3007
3066
|
}
|
|
3008
3067
|
|
|
3009
|
-
/**
|
|
3010
|
-
* Convert object to array format for optimized serialization
|
|
3068
|
+
/**
|
|
3069
|
+
* Convert object to array format for optimized serialization
|
|
3011
3070
|
*/
|
|
3012
3071
|
convertToArrayFormat(obj) {
|
|
3013
3072
|
if (!this.opts.enableArraySerialization) {
|
|
@@ -3016,8 +3075,8 @@ class Serializer {
|
|
|
3016
3075
|
return this.schemaManager.objectToArray(obj);
|
|
3017
3076
|
}
|
|
3018
3077
|
|
|
3019
|
-
/**
|
|
3020
|
-
* Convert array format back to object
|
|
3078
|
+
/**
|
|
3079
|
+
* Convert array format back to object
|
|
3021
3080
|
*/
|
|
3022
3081
|
convertFromArrayFormat(arr) {
|
|
3023
3082
|
if (!this.opts.enableArraySerialization) {
|
|
@@ -3044,8 +3103,8 @@ class Serializer {
|
|
|
3044
3103
|
return obj;
|
|
3045
3104
|
}
|
|
3046
3105
|
|
|
3047
|
-
/**
|
|
3048
|
-
* Advanced serialization with optimized JSON and buffer pooling
|
|
3106
|
+
/**
|
|
3107
|
+
* Advanced serialization with optimized JSON and buffer pooling
|
|
3049
3108
|
*/
|
|
3050
3109
|
serialize(data, opts = {}) {
|
|
3051
3110
|
this.serializationStats.totalSerializations++;
|
|
@@ -3072,8 +3131,8 @@ class Serializer {
|
|
|
3072
3131
|
return this.serializeStandard(serializationData, addLinebreak);
|
|
3073
3132
|
}
|
|
3074
3133
|
|
|
3075
|
-
/**
|
|
3076
|
-
* Advanced serialization with optimized JSON.stringify and buffer pooling
|
|
3134
|
+
/**
|
|
3135
|
+
* Advanced serialization with optimized JSON.stringify and buffer pooling
|
|
3077
3136
|
*/
|
|
3078
3137
|
serializeAdvanced(data, addLinebreak) {
|
|
3079
3138
|
// CRITICAL FIX: Sanitize data to remove problematic characters before serialization
|
|
@@ -3098,9 +3157,9 @@ class Serializer {
|
|
|
3098
3157
|
return result;
|
|
3099
3158
|
}
|
|
3100
3159
|
|
|
3101
|
-
/**
|
|
3102
|
-
* Proper encoding normalization with UTF-8 validation
|
|
3103
|
-
* Fixed to prevent double-encoding and data corruption
|
|
3160
|
+
/**
|
|
3161
|
+
* Proper encoding normalization with UTF-8 validation
|
|
3162
|
+
* Fixed to prevent double-encoding and data corruption
|
|
3104
3163
|
*/
|
|
3105
3164
|
normalizeEncoding(str) {
|
|
3106
3165
|
if (typeof str !== 'string') return str;
|
|
@@ -3112,8 +3171,8 @@ class Serializer {
|
|
|
3112
3171
|
return this.safeConvertToUTF8(str);
|
|
3113
3172
|
}
|
|
3114
3173
|
|
|
3115
|
-
/**
|
|
3116
|
-
* Check if string is valid UTF-8
|
|
3174
|
+
/**
|
|
3175
|
+
* Check if string is valid UTF-8
|
|
3117
3176
|
*/
|
|
3118
3177
|
isValidUTF8(str) {
|
|
3119
3178
|
try {
|
|
@@ -3126,8 +3185,8 @@ class Serializer {
|
|
|
3126
3185
|
}
|
|
3127
3186
|
}
|
|
3128
3187
|
|
|
3129
|
-
/**
|
|
3130
|
-
* Safe conversion to UTF-8 with proper encoding detection
|
|
3188
|
+
/**
|
|
3189
|
+
* Safe conversion to UTF-8 with proper encoding detection
|
|
3131
3190
|
*/
|
|
3132
3191
|
safeConvertToUTF8(str) {
|
|
3133
3192
|
// Try common encodings in order of likelihood
|
|
@@ -3151,9 +3210,9 @@ class Serializer {
|
|
|
3151
3210
|
return str;
|
|
3152
3211
|
}
|
|
3153
3212
|
|
|
3154
|
-
/**
|
|
3155
|
-
* Enhanced deep encoding normalization with UTF-8 validation
|
|
3156
|
-
* Fixed to prevent double-encoding and data corruption
|
|
3213
|
+
/**
|
|
3214
|
+
* Enhanced deep encoding normalization with UTF-8 validation
|
|
3215
|
+
* Fixed to prevent double-encoding and data corruption
|
|
3157
3216
|
*/
|
|
3158
3217
|
deepNormalizeEncoding(obj) {
|
|
3159
3218
|
if (obj === null || obj === undefined) return obj;
|
|
@@ -3179,13 +3238,13 @@ class Serializer {
|
|
|
3179
3238
|
return obj;
|
|
3180
3239
|
}
|
|
3181
3240
|
|
|
3182
|
-
/**
|
|
3183
|
-
* Validate encoding before serialization
|
|
3241
|
+
/**
|
|
3242
|
+
* Validate encoding before serialization
|
|
3184
3243
|
*/
|
|
3185
|
-
/**
|
|
3186
|
-
* Sanitize data to remove problematic characters that break JSON parsing
|
|
3187
|
-
* CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
|
|
3188
|
-
* by removing control characters that cannot be safely represented in JSON
|
|
3244
|
+
/**
|
|
3245
|
+
* Sanitize data to remove problematic characters that break JSON parsing
|
|
3246
|
+
* CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
|
|
3247
|
+
* by removing control characters that cannot be safely represented in JSON
|
|
3189
3248
|
*/
|
|
3190
3249
|
sanitizeDataForJSON(data) {
|
|
3191
3250
|
const sanitizeString = str => {
|
|
@@ -3247,9 +3306,9 @@ class Serializer {
|
|
|
3247
3306
|
return issues.length === 0;
|
|
3248
3307
|
}
|
|
3249
3308
|
|
|
3250
|
-
/**
|
|
3251
|
-
* Optimized JSON.stringify with fast paths for common data structures
|
|
3252
|
-
* Now includes deep encoding normalization for all string fields
|
|
3309
|
+
/**
|
|
3310
|
+
* Optimized JSON.stringify with fast paths for common data structures
|
|
3311
|
+
* Now includes deep encoding normalization for all string fields
|
|
3253
3312
|
*/
|
|
3254
3313
|
optimizedStringify(obj) {
|
|
3255
3314
|
// CRITICAL: Normalize encoding for all string fields before stringify
|
|
@@ -3310,8 +3369,8 @@ class Serializer {
|
|
|
3310
3369
|
return result;
|
|
3311
3370
|
}
|
|
3312
3371
|
|
|
3313
|
-
/**
|
|
3314
|
-
* Standard serialization (fallback)
|
|
3372
|
+
/**
|
|
3373
|
+
* Standard serialization (fallback)
|
|
3315
3374
|
*/
|
|
3316
3375
|
serializeStandard(data, addLinebreak) {
|
|
3317
3376
|
// CRITICAL FIX: Sanitize data to remove problematic characters before serialization
|
|
@@ -3337,8 +3396,8 @@ class Serializer {
|
|
|
3337
3396
|
return result;
|
|
3338
3397
|
}
|
|
3339
3398
|
|
|
3340
|
-
/**
|
|
3341
|
-
* Advanced deserialization with fast paths
|
|
3399
|
+
/**
|
|
3400
|
+
* Advanced deserialization with fast paths
|
|
3342
3401
|
*/
|
|
3343
3402
|
deserialize(data) {
|
|
3344
3403
|
this.serializationStats.totalDeserializations++;
|
|
@@ -3522,8 +3581,8 @@ class Serializer {
|
|
|
3522
3581
|
}
|
|
3523
3582
|
}
|
|
3524
3583
|
|
|
3525
|
-
/**
|
|
3526
|
-
* Batch serialization for multiple records
|
|
3584
|
+
/**
|
|
3585
|
+
* Batch serialization for multiple records
|
|
3527
3586
|
*/
|
|
3528
3587
|
serializeBatch(dataArray, opts = {}) {
|
|
3529
3588
|
// CRITICAL FIX: Sanitize data to remove problematic characters before serialization
|
|
@@ -3574,8 +3633,8 @@ class Serializer {
|
|
|
3574
3633
|
}
|
|
3575
3634
|
}
|
|
3576
3635
|
|
|
3577
|
-
/**
|
|
3578
|
-
* Batch deserialization for multiple records
|
|
3636
|
+
/**
|
|
3637
|
+
* Batch deserialization for multiple records
|
|
3579
3638
|
*/
|
|
3580
3639
|
deserializeBatch(dataArray) {
|
|
3581
3640
|
// Optimization: try to parse all entries as a single JSON array first
|
|
@@ -3617,16 +3676,16 @@ class Serializer {
|
|
|
3617
3676
|
}
|
|
3618
3677
|
}
|
|
3619
3678
|
|
|
3620
|
-
/**
|
|
3621
|
-
* Check if data appears to be binary (always false since we only use JSON now)
|
|
3679
|
+
/**
|
|
3680
|
+
* Check if data appears to be binary (always false since we only use JSON now)
|
|
3622
3681
|
*/
|
|
3623
3682
|
isBinaryData(data) {
|
|
3624
3683
|
// All data is now JSON format
|
|
3625
3684
|
return false;
|
|
3626
3685
|
}
|
|
3627
3686
|
|
|
3628
|
-
/**
|
|
3629
|
-
* Get comprehensive performance statistics
|
|
3687
|
+
/**
|
|
3688
|
+
* Get comprehensive performance statistics
|
|
3630
3689
|
*/
|
|
3631
3690
|
getStats() {
|
|
3632
3691
|
// NOTE: Buffer pool stats removed - buffer pool was causing more problems than benefits
|
|
@@ -3645,8 +3704,8 @@ class Serializer {
|
|
|
3645
3704
|
};
|
|
3646
3705
|
}
|
|
3647
3706
|
|
|
3648
|
-
/**
|
|
3649
|
-
* Cleanup resources
|
|
3707
|
+
/**
|
|
3708
|
+
* Cleanup resources
|
|
3650
3709
|
*/
|
|
3651
3710
|
cleanup() {
|
|
3652
3711
|
// NOTE: Buffer pool cleanup removed - buffer pool was causing more problems than benefits
|
|
@@ -4626,12 +4685,12 @@ class FileHandler {
|
|
|
4626
4685
|
return groupedRanges;
|
|
4627
4686
|
}
|
|
4628
4687
|
|
|
4629
|
-
/**
|
|
4630
|
-
* Ensure a line is complete by reading until newline if JSON appears truncated
|
|
4631
|
-
* @param {string} line - The potentially incomplete line
|
|
4632
|
-
* @param {number} fd - File descriptor
|
|
4633
|
-
* @param {number} currentOffset - Current read offset
|
|
4634
|
-
* @returns {string} Complete line
|
|
4688
|
+
/**
|
|
4689
|
+
* Ensure a line is complete by reading until newline if JSON appears truncated
|
|
4690
|
+
* @param {string} line - The potentially incomplete line
|
|
4691
|
+
* @param {number} fd - File descriptor
|
|
4692
|
+
* @param {number} currentOffset - Current read offset
|
|
4693
|
+
* @returns {string} Complete line
|
|
4635
4694
|
*/
|
|
4636
4695
|
async ensureCompleteLine(line, fd, currentOffset) {
|
|
4637
4696
|
// Fast check: if line already ends with newline, it's likely complete
|
|
@@ -4707,12 +4766,12 @@ class FileHandler {
|
|
|
4707
4766
|
}
|
|
4708
4767
|
}
|
|
4709
4768
|
|
|
4710
|
-
/**
|
|
4711
|
-
* Split content into complete JSON lines, handling special characters and escaped quotes
|
|
4712
|
-
* CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors by ensuring
|
|
4713
|
-
* each line is a complete, valid JSON object/array, even when containing special characters
|
|
4714
|
-
* @param {string} content - Raw content containing multiple JSON lines
|
|
4715
|
-
* @returns {string[]} Array of complete JSON lines
|
|
4769
|
+
/**
|
|
4770
|
+
* Split content into complete JSON lines, handling special characters and escaped quotes
|
|
4771
|
+
* CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors by ensuring
|
|
4772
|
+
* each line is a complete, valid JSON object/array, even when containing special characters
|
|
4773
|
+
* @param {string} content - Raw content containing multiple JSON lines
|
|
4774
|
+
* @returns {string[]} Array of complete JSON lines
|
|
4716
4775
|
*/
|
|
4717
4776
|
splitJsonLines(content) {
|
|
4718
4777
|
const lines = [];
|
|
@@ -5149,16 +5208,16 @@ class FileHandler {
|
|
|
5149
5208
|
await fs.promises.appendFile(this.file, data);
|
|
5150
5209
|
}
|
|
5151
5210
|
|
|
5152
|
-
/**
|
|
5153
|
-
* Check if data appears to be binary (always false since we only use JSON now)
|
|
5211
|
+
/**
|
|
5212
|
+
* Check if data appears to be binary (always false since we only use JSON now)
|
|
5154
5213
|
*/
|
|
5155
5214
|
isBinaryData(data) {
|
|
5156
5215
|
// All data is now JSON format
|
|
5157
5216
|
return false;
|
|
5158
5217
|
}
|
|
5159
5218
|
|
|
5160
|
-
/**
|
|
5161
|
-
* Check if file is binary (always false since we only use JSON now)
|
|
5219
|
+
/**
|
|
5220
|
+
* Check if file is binary (always false since we only use JSON now)
|
|
5162
5221
|
*/
|
|
5163
5222
|
async isBinaryFile() {
|
|
5164
5223
|
// All files are now JSON format
|
|
@@ -5215,12 +5274,12 @@ class FileHandler {
|
|
|
5215
5274
|
});
|
|
5216
5275
|
}
|
|
5217
5276
|
|
|
5218
|
-
/**
|
|
5219
|
-
* Read records with streaming using readline
|
|
5220
|
-
* @param {Object} criteria - Filter criteria
|
|
5221
|
-
* @param {Object} options - Options (limit, skip)
|
|
5222
|
-
* @param {Function} matchesCriteria - Function to check if record matches criteria
|
|
5223
|
-
* @returns {Promise<Array>} - Array of records
|
|
5277
|
+
/**
|
|
5278
|
+
* Read records with streaming using readline
|
|
5279
|
+
* @param {Object} criteria - Filter criteria
|
|
5280
|
+
* @param {Object} options - Options (limit, skip)
|
|
5281
|
+
* @param {Function} matchesCriteria - Function to check if record matches criteria
|
|
5282
|
+
* @returns {Promise<Array>} - Array of records
|
|
5224
5283
|
*/
|
|
5225
5284
|
async readWithStreaming(criteria, options = {}, matchesCriteria, serializer = null) {
|
|
5226
5285
|
// CRITICAL: Always use file mutex to prevent concurrent file operations
|
|
@@ -5389,12 +5448,12 @@ class FileHandler {
|
|
|
5389
5448
|
}
|
|
5390
5449
|
}
|
|
5391
5450
|
|
|
5392
|
-
/**
|
|
5393
|
-
* Count records with streaming
|
|
5394
|
-
* @param {Object} criteria - Filter criteria
|
|
5395
|
-
* @param {Object} options - Options (limit)
|
|
5396
|
-
* @param {Function} matchesCriteria - Function to check if record matches criteria
|
|
5397
|
-
* @returns {Promise<number>} - Number of records
|
|
5451
|
+
/**
|
|
5452
|
+
* Count records with streaming
|
|
5453
|
+
* @param {Object} criteria - Filter criteria
|
|
5454
|
+
* @param {Object} options - Options (limit)
|
|
5455
|
+
* @param {Function} matchesCriteria - Function to check if record matches criteria
|
|
5456
|
+
* @returns {Promise<number>} - Number of records
|
|
5398
5457
|
*/
|
|
5399
5458
|
async countWithStreaming(criteria, options = {}, matchesCriteria, serializer = null) {
|
|
5400
5459
|
const {
|
|
@@ -5459,9 +5518,9 @@ class FileHandler {
|
|
|
5459
5518
|
}
|
|
5460
5519
|
}
|
|
5461
5520
|
|
|
5462
|
-
/**
|
|
5463
|
-
* Get file statistics
|
|
5464
|
-
* @returns {Promise<Object>} - File statistics
|
|
5521
|
+
/**
|
|
5522
|
+
* Get file statistics
|
|
5523
|
+
* @returns {Promise<Object>} - File statistics
|
|
5465
5524
|
*/
|
|
5466
5525
|
async getFileStats() {
|
|
5467
5526
|
try {
|
|
@@ -5478,9 +5537,9 @@ class FileHandler {
|
|
|
5478
5537
|
}
|
|
5479
5538
|
}
|
|
5480
5539
|
|
|
5481
|
-
/**
|
|
5482
|
-
* Count lines in file
|
|
5483
|
-
* @returns {Promise<number>} - Number of lines
|
|
5540
|
+
/**
|
|
5541
|
+
* Count lines in file
|
|
5542
|
+
* @returns {Promise<number>} - Number of lines
|
|
5484
5543
|
*/
|
|
5485
5544
|
async countLines() {
|
|
5486
5545
|
let lineCount = 0;
|
|
@@ -5568,10 +5627,10 @@ class FileHandler {
|
|
|
5568
5627
|
}
|
|
5569
5628
|
}
|
|
5570
5629
|
|
|
5571
|
-
/**
|
|
5572
|
-
* Optimized batch write operation (OPTIMIZATION)
|
|
5573
|
-
* @param {Array} dataChunks - Array of data chunks to write
|
|
5574
|
-
* @param {boolean} append - Whether to append or overwrite
|
|
5630
|
+
/**
|
|
5631
|
+
* Optimized batch write operation (OPTIMIZATION)
|
|
5632
|
+
* @param {Array} dataChunks - Array of data chunks to write
|
|
5633
|
+
* @param {boolean} append - Whether to append or overwrite
|
|
5575
5634
|
*/
|
|
5576
5635
|
async writeBatch(dataChunks, append = false) {
|
|
5577
5636
|
if (!dataChunks || !dataChunks.length) return;
|
|
@@ -5594,10 +5653,10 @@ class FileHandler {
|
|
|
5594
5653
|
}
|
|
5595
5654
|
}
|
|
5596
5655
|
|
|
5597
|
-
/**
|
|
5598
|
-
* OPTIMIZATION: Streaming write for multiple chunks
|
|
5599
|
-
* @param {Array} dataChunks - Array of data chunks to write
|
|
5600
|
-
* @param {boolean} append - Whether to append or overwrite
|
|
5656
|
+
/**
|
|
5657
|
+
* OPTIMIZATION: Streaming write for multiple chunks
|
|
5658
|
+
* @param {Array} dataChunks - Array of data chunks to write
|
|
5659
|
+
* @param {boolean} append - Whether to append or overwrite
|
|
5601
5660
|
*/
|
|
5602
5661
|
async _writeBatchStreaming(dataChunks, append = false) {
|
|
5603
5662
|
// OPTIMIZATION: Use createWriteStream for better performance
|
|
@@ -5628,9 +5687,9 @@ class FileHandler {
|
|
|
5628
5687
|
});
|
|
5629
5688
|
}
|
|
5630
5689
|
|
|
5631
|
-
/**
|
|
5632
|
-
* Optimized append operation for single data chunk (OPTIMIZATION)
|
|
5633
|
-
* @param {string|Buffer} data - Data to append
|
|
5690
|
+
/**
|
|
5691
|
+
* Optimized append operation for single data chunk (OPTIMIZATION)
|
|
5692
|
+
* @param {string|Buffer} data - Data to append
|
|
5634
5693
|
*/
|
|
5635
5694
|
async appendOptimized(data) {
|
|
5636
5695
|
const release = this.fileMutex ? await this.fileMutex.acquire() : () => {};
|
|
@@ -5642,9 +5701,9 @@ class FileHandler {
|
|
|
5642
5701
|
}
|
|
5643
5702
|
}
|
|
5644
5703
|
|
|
5645
|
-
/**
|
|
5646
|
-
* Windows-specific retry logic for fs.promises.writeFile operations
|
|
5647
|
-
* Based on node-graceful-fs workarounds for EPERM issues
|
|
5704
|
+
/**
|
|
5705
|
+
* Windows-specific retry logic for fs.promises.writeFile operations
|
|
5706
|
+
* Based on node-graceful-fs workarounds for EPERM issues
|
|
5648
5707
|
*/
|
|
5649
5708
|
async _writeFileWithRetry(filePath, data, maxRetries = 3) {
|
|
5650
5709
|
const isWindows = process.platform === 'win32';
|
|
@@ -5680,9 +5739,9 @@ class FileHandler {
|
|
|
5680
5739
|
}
|
|
5681
5740
|
}
|
|
5682
5741
|
|
|
5683
|
-
/**
|
|
5684
|
-
* Windows-specific retry logic for file operations
|
|
5685
|
-
* Based on node-graceful-fs workarounds for EPERM issues
|
|
5742
|
+
/**
|
|
5743
|
+
* Windows-specific retry logic for file operations
|
|
5744
|
+
* Based on node-graceful-fs workarounds for EPERM issues
|
|
5686
5745
|
*/
|
|
5687
5746
|
async _writeWithRetry(data, maxRetries = 3) {
|
|
5688
5747
|
const isWindows = process.platform === 'win32';
|
|
@@ -5748,11 +5807,11 @@ class FileHandler {
|
|
|
5748
5807
|
}
|
|
5749
5808
|
}
|
|
5750
5809
|
|
|
5751
|
-
/**
|
|
5752
|
-
* Read specific lines from the file using line numbers
|
|
5753
|
-
* This is optimized for partial reads when using indexed queries
|
|
5754
|
-
* @param {number[]} lineNumbers - Array of line numbers to read (1-based)
|
|
5755
|
-
* @returns {Promise<string>} - Content of the specified lines
|
|
5810
|
+
/**
|
|
5811
|
+
* Read specific lines from the file using line numbers
|
|
5812
|
+
* This is optimized for partial reads when using indexed queries
|
|
5813
|
+
* @param {number[]} lineNumbers - Array of line numbers to read (1-based)
|
|
5814
|
+
* @returns {Promise<string>} - Content of the specified lines
|
|
5756
5815
|
*/
|
|
5757
5816
|
async readSpecificLines(lineNumbers) {
|
|
5758
5817
|
if (!lineNumbers || lineNumbers.length === 0) {
|
|
@@ -5822,15 +5881,15 @@ class FileHandler {
|
|
|
5822
5881
|
}
|
|
5823
5882
|
}
|
|
5824
5883
|
|
|
5825
|
-
/**
|
|
5826
|
-
* QueryManager - Handles all query operations and strategies
|
|
5827
|
-
*
|
|
5828
|
-
* Responsibilities:
|
|
5829
|
-
* - find(), findOne(), count(), query()
|
|
5830
|
-
* - findWithStreaming(), findWithIndexed()
|
|
5831
|
-
* - matchesCriteria(), extractQueryFields()
|
|
5832
|
-
* - Query strategies (INDEXED vs STREAMING)
|
|
5833
|
-
* - Result estimation
|
|
5884
|
+
/**
|
|
5885
|
+
* QueryManager - Handles all query operations and strategies
|
|
5886
|
+
*
|
|
5887
|
+
* Responsibilities:
|
|
5888
|
+
* - find(), findOne(), count(), query()
|
|
5889
|
+
* - findWithStreaming(), findWithIndexed()
|
|
5890
|
+
* - matchesCriteria(), extractQueryFields()
|
|
5891
|
+
* - Query strategies (INDEXED vs STREAMING)
|
|
5892
|
+
* - Result estimation
|
|
5834
5893
|
*/
|
|
5835
5894
|
|
|
5836
5895
|
class QueryManager {
|
|
@@ -5849,11 +5908,11 @@ class QueryManager {
|
|
|
5849
5908
|
};
|
|
5850
5909
|
}
|
|
5851
5910
|
|
|
5852
|
-
/**
|
|
5853
|
-
* Main find method with strategy selection
|
|
5854
|
-
* @param {Object} criteria - Query criteria
|
|
5855
|
-
* @param {Object} options - Query options
|
|
5856
|
-
* @returns {Promise<Array>} - Query results
|
|
5911
|
+
/**
|
|
5912
|
+
* Main find method with strategy selection
|
|
5913
|
+
* @param {Object} criteria - Query criteria
|
|
5914
|
+
* @param {Object} options - Query options
|
|
5915
|
+
* @returns {Promise<Array>} - Query results
|
|
5857
5916
|
*/
|
|
5858
5917
|
async find(criteria, options = {}) {
|
|
5859
5918
|
if (this.database.destroyed) throw new Error('Database is destroyed');
|
|
@@ -5902,11 +5961,11 @@ class QueryManager {
|
|
|
5902
5961
|
}
|
|
5903
5962
|
}
|
|
5904
5963
|
|
|
5905
|
-
/**
|
|
5906
|
-
* Find one record
|
|
5907
|
-
* @param {Object} criteria - Query criteria
|
|
5908
|
-
* @param {Object} options - Query options
|
|
5909
|
-
* @returns {Promise<Object|null>} - First matching record or null
|
|
5964
|
+
/**
|
|
5965
|
+
* Find one record
|
|
5966
|
+
* @param {Object} criteria - Query criteria
|
|
5967
|
+
* @param {Object} options - Query options
|
|
5968
|
+
* @returns {Promise<Object|null>} - First matching record or null
|
|
5910
5969
|
*/
|
|
5911
5970
|
async findOne(criteria, options = {}) {
|
|
5912
5971
|
if (this.database.destroyed) throw new Error('Database is destroyed');
|
|
@@ -5957,11 +6016,11 @@ class QueryManager {
|
|
|
5957
6016
|
}
|
|
5958
6017
|
}
|
|
5959
6018
|
|
|
5960
|
-
/**
|
|
5961
|
-
* Count records matching criteria
|
|
5962
|
-
* @param {Object} criteria - Query criteria
|
|
5963
|
-
* @param {Object} options - Query options
|
|
5964
|
-
* @returns {Promise<number>} - Count of matching records
|
|
6019
|
+
/**
|
|
6020
|
+
* Count records matching criteria
|
|
6021
|
+
* @param {Object} criteria - Query criteria
|
|
6022
|
+
* @param {Object} options - Query options
|
|
6023
|
+
* @returns {Promise<number>} - Count of matching records
|
|
5965
6024
|
*/
|
|
5966
6025
|
async count(criteria, options = {}) {
|
|
5967
6026
|
if (this.database.destroyed) throw new Error('Database is destroyed');
|
|
@@ -5985,7 +6044,18 @@ class QueryManager {
|
|
|
5985
6044
|
const results = await this.findWithStreaming(criteria, options);
|
|
5986
6045
|
count = results.length;
|
|
5987
6046
|
} else {
|
|
5988
|
-
|
|
6047
|
+
try {
|
|
6048
|
+
await this.database._ensureLazyIndexLoaded();
|
|
6049
|
+
} catch (error) {
|
|
6050
|
+
if (this.opts.debugMode) {
|
|
6051
|
+
console.log('⚠️ _ensureLazyIndexLoaded failed in count, falling back to streaming', error.message || error);
|
|
6052
|
+
}
|
|
6053
|
+
const streamingResults = await this.findWithStreaming(criteria, {
|
|
6054
|
+
...options,
|
|
6055
|
+
forceFullScan: true
|
|
6056
|
+
});
|
|
6057
|
+
return streamingResults.length;
|
|
6058
|
+
}
|
|
5989
6059
|
// This avoids reading actual records from the file - much faster!
|
|
5990
6060
|
const lineNumbers = this.indexManager.query(criteria, options);
|
|
5991
6061
|
if (lineNumbers.size === 0) {
|
|
@@ -6038,21 +6108,21 @@ class QueryManager {
|
|
|
6038
6108
|
return count;
|
|
6039
6109
|
}
|
|
6040
6110
|
|
|
6041
|
-
/**
|
|
6042
|
-
* Compatibility method that redirects to find
|
|
6043
|
-
* @param {Object} criteria - Query criteria
|
|
6044
|
-
* @param {Object} options - Query options
|
|
6045
|
-
* @returns {Promise<Array>} - Query results
|
|
6111
|
+
/**
|
|
6112
|
+
* Compatibility method that redirects to find
|
|
6113
|
+
* @param {Object} criteria - Query criteria
|
|
6114
|
+
* @param {Object} options - Query options
|
|
6115
|
+
* @returns {Promise<Array>} - Query results
|
|
6046
6116
|
*/
|
|
6047
6117
|
async query(criteria, options = {}) {
|
|
6048
6118
|
return this.find(criteria, options);
|
|
6049
6119
|
}
|
|
6050
6120
|
|
|
6051
|
-
/**
|
|
6052
|
-
* Find using streaming strategy with pre-filtering optimization
|
|
6053
|
-
* @param {Object} criteria - Query criteria
|
|
6054
|
-
* @param {Object} options - Query options
|
|
6055
|
-
* @returns {Promise<Array>} - Query results
|
|
6121
|
+
/**
|
|
6122
|
+
* Find using streaming strategy with pre-filtering optimization
|
|
6123
|
+
* @param {Object} criteria - Query criteria
|
|
6124
|
+
* @param {Object} options - Query options
|
|
6125
|
+
* @returns {Promise<Array>} - Query results
|
|
6056
6126
|
*/
|
|
6057
6127
|
async findWithStreaming(criteria, options = {}) {
|
|
6058
6128
|
const streamingOptions = {
|
|
@@ -6071,6 +6141,21 @@ class QueryManager {
|
|
|
6071
6141
|
// OPTIMIZATION: Try to use indices for pre-filtering when possible
|
|
6072
6142
|
const indexableFields = this._getIndexableFields(criteria);
|
|
6073
6143
|
if (indexableFields.length > 0) {
|
|
6144
|
+
try {
|
|
6145
|
+
await this.database._ensureLazyIndexLoaded();
|
|
6146
|
+
} catch (error) {
|
|
6147
|
+
if (this.opts.debugMode) {
|
|
6148
|
+
console.log('⚠️ _ensureLazyIndexLoaded failed, falling back to full streaming', error.message || error);
|
|
6149
|
+
}
|
|
6150
|
+
return this._streamAllRecords(criteria, streamingOptions);
|
|
6151
|
+
}
|
|
6152
|
+
const usableIndexableFields = indexableFields.filter(field => this.indexManager.hasUsableIndexData(field));
|
|
6153
|
+
if (usableIndexableFields.length !== indexableFields.length) {
|
|
6154
|
+
if (this.opts.debugMode) {
|
|
6155
|
+
console.log('🌊 Falling back to full streaming because some indexed fields lack usable index data');
|
|
6156
|
+
}
|
|
6157
|
+
return this._streamAllRecords(criteria, streamingOptions);
|
|
6158
|
+
}
|
|
6074
6159
|
if (this.opts.debugMode) {
|
|
6075
6160
|
console.log(`🌊 Using pre-filtered streaming with ${indexableFields.length} indexable fields`);
|
|
6076
6161
|
}
|
|
@@ -6090,10 +6175,10 @@ class QueryManager {
|
|
|
6090
6175
|
return this._streamAllRecords(criteria, streamingOptions);
|
|
6091
6176
|
}
|
|
6092
6177
|
|
|
6093
|
-
/**
|
|
6094
|
-
* Get indexable fields from criteria
|
|
6095
|
-
* @param {Object} criteria - Query criteria
|
|
6096
|
-
* @returns {Array} - Array of indexable field names
|
|
6178
|
+
/**
|
|
6179
|
+
* Get indexable fields from criteria
|
|
6180
|
+
* @param {Object} criteria - Query criteria
|
|
6181
|
+
* @returns {Array} - Array of indexable field names
|
|
6097
6182
|
*/
|
|
6098
6183
|
_getIndexableFields(criteria) {
|
|
6099
6184
|
const indexableFields = [];
|
|
@@ -6123,10 +6208,10 @@ class QueryManager {
|
|
|
6123
6208
|
return [...new Set(indexableFields)]; // Remove duplicates
|
|
6124
6209
|
}
|
|
6125
6210
|
|
|
6126
|
-
/**
|
|
6127
|
-
* Extract indexable criteria for pre-filtering
|
|
6128
|
-
* @param {Object} criteria - Full query criteria
|
|
6129
|
-
* @returns {Object} - Criteria with only indexable fields
|
|
6211
|
+
/**
|
|
6212
|
+
* Extract indexable criteria for pre-filtering
|
|
6213
|
+
* @param {Object} criteria - Full query criteria
|
|
6214
|
+
* @returns {Object} - Criteria with only indexable fields
|
|
6130
6215
|
*/
|
|
6131
6216
|
_extractIndexableCriteria(criteria) {
|
|
6132
6217
|
if (!criteria || typeof criteria !== 'object') {
|
|
@@ -6171,9 +6256,9 @@ class QueryManager {
|
|
|
6171
6256
|
return indexableCriteria;
|
|
6172
6257
|
}
|
|
6173
6258
|
|
|
6174
|
-
/**
|
|
6175
|
-
* Determine whether the database currently has any records (persisted or pending)
|
|
6176
|
-
* @returns {boolean}
|
|
6259
|
+
/**
|
|
6260
|
+
* Determine whether the database currently has any records (persisted or pending)
|
|
6261
|
+
* @returns {boolean}
|
|
6177
6262
|
*/
|
|
6178
6263
|
_hasAnyRecords() {
|
|
6179
6264
|
if (!this.database) {
|
|
@@ -6191,11 +6276,11 @@ class QueryManager {
|
|
|
6191
6276
|
return false;
|
|
6192
6277
|
}
|
|
6193
6278
|
|
|
6194
|
-
/**
|
|
6195
|
-
* Extract all indexed fields referenced in the criteria
|
|
6196
|
-
* @param {Object} criteria
|
|
6197
|
-
* @param {Set<string>} accumulator
|
|
6198
|
-
* @returns {Array<string>}
|
|
6279
|
+
/**
|
|
6280
|
+
* Extract all indexed fields referenced in the criteria
|
|
6281
|
+
* @param {Object} criteria
|
|
6282
|
+
* @param {Set<string>} accumulator
|
|
6283
|
+
* @returns {Array<string>}
|
|
6199
6284
|
*/
|
|
6200
6285
|
_extractIndexedFields(criteria, accumulator = new Set()) {
|
|
6201
6286
|
if (!criteria) {
|
|
@@ -6225,10 +6310,10 @@ class QueryManager {
|
|
|
6225
6310
|
return Array.from(accumulator);
|
|
6226
6311
|
}
|
|
6227
6312
|
|
|
6228
|
-
/**
|
|
6229
|
-
* Identify indexed fields present in criteria whose index data is missing
|
|
6230
|
-
* @param {Object} criteria
|
|
6231
|
-
* @returns {Array<string>}
|
|
6313
|
+
/**
|
|
6314
|
+
* Identify indexed fields present in criteria whose index data is missing
|
|
6315
|
+
* @param {Object} criteria
|
|
6316
|
+
* @returns {Array<string>}
|
|
6232
6317
|
*/
|
|
6233
6318
|
_getIndexedFieldsWithMissingData(criteria) {
|
|
6234
6319
|
if (!this.indexManager || !criteria) {
|
|
@@ -6247,12 +6332,12 @@ class QueryManager {
|
|
|
6247
6332
|
return missing;
|
|
6248
6333
|
}
|
|
6249
6334
|
|
|
6250
|
-
/**
|
|
6251
|
-
* OPTIMIZATION 4: Stream pre-filtered records using line numbers from indices with partial index optimization
|
|
6252
|
-
* @param {Set} preFilteredLines - Line numbers from index query
|
|
6253
|
-
* @param {Object} criteria - Full query criteria
|
|
6254
|
-
* @param {Object} options - Query options
|
|
6255
|
-
* @returns {Promise<Array>} - Query results
|
|
6335
|
+
/**
|
|
6336
|
+
* OPTIMIZATION 4: Stream pre-filtered records using line numbers from indices with partial index optimization
|
|
6337
|
+
* @param {Set} preFilteredLines - Line numbers from index query
|
|
6338
|
+
* @param {Object} criteria - Full query criteria
|
|
6339
|
+
* @param {Object} options - Query options
|
|
6340
|
+
* @returns {Promise<Array>} - Query results
|
|
6256
6341
|
*/
|
|
6257
6342
|
async _streamPreFilteredRecords(preFilteredLines, criteria, options = {}) {
|
|
6258
6343
|
if (preFilteredLines.size === 0) {
|
|
@@ -6270,71 +6355,95 @@ class QueryManager {
|
|
|
6270
6355
|
for (let i = 0; i < lineNumbers.length; i += batchSize) {
|
|
6271
6356
|
batches.push(lineNumbers.slice(i, i + batchSize));
|
|
6272
6357
|
}
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6358
|
+
const persistedCount = Array.isArray(this.database.offsets) ? this.database.offsets.length : 0;
|
|
6359
|
+
const fileLineNumbers = [];
|
|
6360
|
+
const writeBufferLineNumbers = [];
|
|
6361
|
+
for (const lineNumber of lineNumbers) {
|
|
6362
|
+
if (lineNumber >= persistedCount) {
|
|
6363
|
+
writeBufferLineNumbers.push(lineNumber);
|
|
6364
|
+
} else {
|
|
6365
|
+
fileLineNumbers.push(lineNumber);
|
|
6366
|
+
}
|
|
6367
|
+
}
|
|
6368
|
+
if (fileLineNumbers.length > 0) {
|
|
6369
|
+
const fileBatches = [];
|
|
6370
|
+
for (let i = 0; i < fileLineNumbers.length; i += batchSize) {
|
|
6371
|
+
fileBatches.push(fileLineNumbers.slice(i, i + batchSize));
|
|
6372
|
+
}
|
|
6373
|
+
for (const batch of fileBatches) {
|
|
6374
|
+
// OPTIMIZATION: Use ranges instead of reading entire file
|
|
6375
|
+
const ranges = this.database.getRanges(batch);
|
|
6376
|
+
const groupedRanges = await this.fileHandler.groupedRanges(ranges);
|
|
6377
|
+
const fd = await fs.promises.open(this.fileHandler.file, 'r');
|
|
6378
|
+
try {
|
|
6379
|
+
for (const groupedRange of groupedRanges) {
|
|
6380
|
+
var _iteratorAbruptCompletion = false;
|
|
6381
|
+
var _didIteratorError = false;
|
|
6382
|
+
var _iteratorError;
|
|
6383
|
+
try {
|
|
6384
|
+
for (var _iterator = _asyncIterator(this.fileHandler.readGroupedRange(groupedRange, fd)), _step; _iteratorAbruptCompletion = !(_step = await _iterator.next()).done; _iteratorAbruptCompletion = false) {
|
|
6385
|
+
const row = _step.value;
|
|
6386
|
+
{
|
|
6387
|
+
if (row.line && row.line.trim()) {
|
|
6388
|
+
try {
|
|
6389
|
+
const record = this.database.serializer.deserialize(row.line);
|
|
6390
|
+
if (this._matchesCriteriaOptimized(record, criteria, options)) {
|
|
6391
|
+
const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
|
|
6392
|
+
results.push(recordWithTerms);
|
|
6393
|
+
if (options.limit && results.length >= options.limit) {
|
|
6394
|
+
return this._applyOrdering(results, options);
|
|
6395
|
+
}
|
|
6301
6396
|
}
|
|
6397
|
+
} catch (error) {
|
|
6398
|
+
continue;
|
|
6302
6399
|
}
|
|
6303
|
-
} catch (error) {
|
|
6304
|
-
// Skip invalid lines
|
|
6305
|
-
continue;
|
|
6306
6400
|
}
|
|
6307
6401
|
}
|
|
6308
6402
|
}
|
|
6309
|
-
}
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
_iteratorError = err;
|
|
6313
|
-
} finally {
|
|
6314
|
-
try {
|
|
6315
|
-
if (_iteratorAbruptCompletion && _iterator.return != null) {
|
|
6316
|
-
await _iterator.return();
|
|
6317
|
-
}
|
|
6403
|
+
} catch (err) {
|
|
6404
|
+
_didIteratorError = true;
|
|
6405
|
+
_iteratorError = err;
|
|
6318
6406
|
} finally {
|
|
6319
|
-
|
|
6320
|
-
|
|
6407
|
+
try {
|
|
6408
|
+
if (_iteratorAbruptCompletion && _iterator.return != null) {
|
|
6409
|
+
await _iterator.return();
|
|
6410
|
+
}
|
|
6411
|
+
} finally {
|
|
6412
|
+
if (_didIteratorError) {
|
|
6413
|
+
throw _iteratorError;
|
|
6414
|
+
}
|
|
6321
6415
|
}
|
|
6322
6416
|
}
|
|
6323
6417
|
}
|
|
6418
|
+
} finally {
|
|
6419
|
+
await fd.close();
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
if (writeBufferLineNumbers.length > 0) {
|
|
6424
|
+
writeBufferLineNumbers.sort((a, b) => a - b);
|
|
6425
|
+
for (const lineNumber of writeBufferLineNumbers) {
|
|
6426
|
+
const writeBufferIndex = lineNumber - persistedCount;
|
|
6427
|
+
const record = this.database.writeBuffer[writeBufferIndex];
|
|
6428
|
+
if (!record) continue;
|
|
6429
|
+
if (this._matchesCriteriaOptimized(record, criteria, options)) {
|
|
6430
|
+
const recordWithTerms = options.restoreTerms !== false ? this.database.restoreTermIdsAfterDeserialization(record) : record;
|
|
6431
|
+
results.push(recordWithTerms);
|
|
6432
|
+
if (options.limit && results.length >= options.limit) {
|
|
6433
|
+
return this._applyOrdering(results, options);
|
|
6434
|
+
}
|
|
6324
6435
|
}
|
|
6325
|
-
} finally {
|
|
6326
|
-
await fd.close();
|
|
6327
6436
|
}
|
|
6328
6437
|
}
|
|
6329
6438
|
return this._applyOrdering(results, options);
|
|
6330
6439
|
}
|
|
6331
6440
|
|
|
6332
|
-
/**
|
|
6333
|
-
* OPTIMIZATION 4: Optimized criteria matching for pre-filtered records
|
|
6334
|
-
* @param {Object} record - Record to check
|
|
6335
|
-
* @param {Object} criteria - Filter criteria
|
|
6336
|
-
* @param {Object} options - Query options
|
|
6337
|
-
* @returns {boolean} - True if matches
|
|
6441
|
+
/**
|
|
6442
|
+
* OPTIMIZATION 4: Optimized criteria matching for pre-filtered records
|
|
6443
|
+
* @param {Object} record - Record to check
|
|
6444
|
+
* @param {Object} criteria - Filter criteria
|
|
6445
|
+
* @param {Object} options - Query options
|
|
6446
|
+
* @returns {boolean} - True if matches
|
|
6338
6447
|
*/
|
|
6339
6448
|
_matchesCriteriaOptimized(record, criteria, options = {}) {
|
|
6340
6449
|
if (!criteria || Object.keys(criteria).length === 0) {
|
|
@@ -6393,11 +6502,11 @@ class QueryManager {
|
|
|
6393
6502
|
return true;
|
|
6394
6503
|
}
|
|
6395
6504
|
|
|
6396
|
-
/**
|
|
6397
|
-
* OPTIMIZATION 4: Apply ordering to results
|
|
6398
|
-
* @param {Array} results - Results to order
|
|
6399
|
-
* @param {Object} options - Query options
|
|
6400
|
-
* @returns {Array} - Ordered results
|
|
6505
|
+
/**
|
|
6506
|
+
* OPTIMIZATION 4: Apply ordering to results
|
|
6507
|
+
* @param {Array} results - Results to order
|
|
6508
|
+
* @param {Object} options - Query options
|
|
6509
|
+
* @returns {Array} - Ordered results
|
|
6401
6510
|
*/
|
|
6402
6511
|
_applyOrdering(results, options) {
|
|
6403
6512
|
if (options.orderBy) {
|
|
@@ -6411,11 +6520,11 @@ class QueryManager {
|
|
|
6411
6520
|
return results;
|
|
6412
6521
|
}
|
|
6413
6522
|
|
|
6414
|
-
/**
|
|
6415
|
-
* Stream all records (fallback method)
|
|
6416
|
-
* @param {Object} criteria - Query criteria
|
|
6417
|
-
* @param {Object} options - Query options
|
|
6418
|
-
* @returns {Promise<Array>} - Query results
|
|
6523
|
+
/**
|
|
6524
|
+
* Stream all records (fallback method)
|
|
6525
|
+
* @param {Object} criteria - Query criteria
|
|
6526
|
+
* @param {Object} options - Query options
|
|
6527
|
+
* @returns {Promise<Array>} - Query results
|
|
6419
6528
|
*/
|
|
6420
6529
|
async _streamAllRecords(criteria, options = {}) {
|
|
6421
6530
|
const memoryLimit = options.limit || undefined;
|
|
@@ -6442,16 +6551,27 @@ class QueryManager {
|
|
|
6442
6551
|
return resultsWithTerms;
|
|
6443
6552
|
}
|
|
6444
6553
|
|
|
6445
|
-
/**
|
|
6446
|
-
* Find using indexed search strategy with real streaming
|
|
6447
|
-
* @param {Object} criteria - Query criteria
|
|
6448
|
-
* @param {Object} options - Query options
|
|
6449
|
-
* @returns {Promise<Array>} - Query results
|
|
6554
|
+
/**
|
|
6555
|
+
* Find using indexed search strategy with real streaming
|
|
6556
|
+
* @param {Object} criteria - Query criteria
|
|
6557
|
+
* @param {Object} options - Query options
|
|
6558
|
+
* @returns {Promise<Array>} - Query results
|
|
6450
6559
|
*/
|
|
6451
6560
|
async findWithIndexed(criteria, options = {}) {
|
|
6452
6561
|
if (this.opts.debugMode) {
|
|
6453
6562
|
console.log('📊 Using indexed strategy with real streaming');
|
|
6454
6563
|
}
|
|
6564
|
+
try {
|
|
6565
|
+
await this.database._ensureLazyIndexLoaded();
|
|
6566
|
+
} catch (error) {
|
|
6567
|
+
if (this.opts.debugMode) {
|
|
6568
|
+
console.log('⚠️ _ensureLazyIndexLoaded failed, falling back to full streaming', error.message || error);
|
|
6569
|
+
}
|
|
6570
|
+
return this.findWithStreaming(criteria, {
|
|
6571
|
+
...options,
|
|
6572
|
+
forceFullScan: true
|
|
6573
|
+
});
|
|
6574
|
+
}
|
|
6455
6575
|
let results = [];
|
|
6456
6576
|
const limit = options.limit; // No default limit - return all results unless explicitly limited
|
|
6457
6577
|
|
|
@@ -6598,12 +6718,12 @@ class QueryManager {
|
|
|
6598
6718
|
return results;
|
|
6599
6719
|
}
|
|
6600
6720
|
|
|
6601
|
-
/**
|
|
6602
|
-
* Check if a record matches criteria
|
|
6603
|
-
* @param {Object} record - Record to check
|
|
6604
|
-
* @param {Object} criteria - Filter criteria
|
|
6605
|
-
* @param {Object} options - Query options (for caseInsensitive, etc.)
|
|
6606
|
-
* @returns {boolean} - True if matches
|
|
6721
|
+
/**
|
|
6722
|
+
* Check if a record matches criteria
|
|
6723
|
+
* @param {Object} record - Record to check
|
|
6724
|
+
* @param {Object} criteria - Filter criteria
|
|
6725
|
+
* @param {Object} options - Query options (for caseInsensitive, etc.)
|
|
6726
|
+
* @returns {boolean} - True if matches
|
|
6607
6727
|
*/
|
|
6608
6728
|
matchesCriteria(record, criteria, options = {}) {
|
|
6609
6729
|
if (!criteria || Object.keys(criteria).length === 0) {
|
|
@@ -6672,13 +6792,13 @@ class QueryManager {
|
|
|
6672
6792
|
return true;
|
|
6673
6793
|
}
|
|
6674
6794
|
|
|
6675
|
-
/**
|
|
6676
|
-
* Check if a field matches a condition
|
|
6677
|
-
* @param {Object} record - Record to check
|
|
6678
|
-
* @param {string} field - Field name
|
|
6679
|
-
* @param {*} condition - Condition to match
|
|
6680
|
-
* @param {Object} options - Query options
|
|
6681
|
-
* @returns {boolean} - True if matches
|
|
6795
|
+
/**
|
|
6796
|
+
* Check if a field matches a condition
|
|
6797
|
+
* @param {Object} record - Record to check
|
|
6798
|
+
* @param {string} field - Field name
|
|
6799
|
+
* @param {*} condition - Condition to match
|
|
6800
|
+
* @param {Object} options - Query options
|
|
6801
|
+
* @returns {boolean} - True if matches
|
|
6682
6802
|
*/
|
|
6683
6803
|
matchesFieldCondition(record, field, condition, options = {}) {
|
|
6684
6804
|
const value = record[field];
|
|
@@ -6753,13 +6873,13 @@ class QueryManager {
|
|
|
6753
6873
|
return value === condition;
|
|
6754
6874
|
}
|
|
6755
6875
|
|
|
6756
|
-
/**
|
|
6757
|
-
* Check if a value matches an operator condition
|
|
6758
|
-
* @param {*} value - Value to check
|
|
6759
|
-
* @param {string} operator - Operator
|
|
6760
|
-
* @param {*} operatorValue - Operator value
|
|
6761
|
-
* @param {Object} options - Query options
|
|
6762
|
-
* @returns {boolean} - True if matches
|
|
6876
|
+
/**
|
|
6877
|
+
* Check if a value matches an operator condition
|
|
6878
|
+
* @param {*} value - Value to check
|
|
6879
|
+
* @param {string} operator - Operator
|
|
6880
|
+
* @param {*} operatorValue - Operator value
|
|
6881
|
+
* @param {Object} options - Query options
|
|
6882
|
+
* @returns {boolean} - True if matches
|
|
6763
6883
|
*/
|
|
6764
6884
|
matchesOperator(value, operator, operatorValue, options = {}) {
|
|
6765
6885
|
switch (operator) {
|
|
@@ -6825,10 +6945,10 @@ class QueryManager {
|
|
|
6825
6945
|
}
|
|
6826
6946
|
}
|
|
6827
6947
|
|
|
6828
|
-
/**
|
|
6829
|
-
* Preprocess query to handle array field syntax automatically
|
|
6830
|
-
* @param {Object} criteria - Query criteria
|
|
6831
|
-
* @returns {Object} - Processed criteria
|
|
6948
|
+
/**
|
|
6949
|
+
* Preprocess query to handle array field syntax automatically
|
|
6950
|
+
* @param {Object} criteria - Query criteria
|
|
6951
|
+
* @returns {Object} - Processed criteria
|
|
6832
6952
|
*/
|
|
6833
6953
|
preprocessQuery(criteria) {
|
|
6834
6954
|
if (!criteria || typeof criteria !== 'object') {
|
|
@@ -6903,11 +7023,11 @@ class QueryManager {
|
|
|
6903
7023
|
return processed;
|
|
6904
7024
|
}
|
|
6905
7025
|
|
|
6906
|
-
/**
|
|
6907
|
-
* Determine which query strategy to use
|
|
6908
|
-
* @param {Object} criteria - Query criteria
|
|
6909
|
-
* @param {Object} options - Query options
|
|
6910
|
-
* @returns {string} - 'streaming' or 'indexed'
|
|
7026
|
+
/**
|
|
7027
|
+
* Determine which query strategy to use
|
|
7028
|
+
* @param {Object} criteria - Query criteria
|
|
7029
|
+
* @param {Object} options - Query options
|
|
7030
|
+
* @returns {string} - 'streaming' or 'indexed'
|
|
6911
7031
|
*/
|
|
6912
7032
|
shouldUseStreaming(criteria, options = {}) {
|
|
6913
7033
|
const {
|
|
@@ -7062,7 +7182,7 @@ class QueryManager {
|
|
|
7062
7182
|
|
|
7063
7183
|
// OPTIMIZATION 2: Hybrid strategy - use pre-filtered streaming when index is empty
|
|
7064
7184
|
const indexData = this.indexManager.index.data || {};
|
|
7065
|
-
const hasIndexData = Object.keys(indexData).
|
|
7185
|
+
const hasIndexData = Object.keys(indexData).some(field => this.indexManager.hasUsableIndexData(field));
|
|
7066
7186
|
if (!hasIndexData) {
|
|
7067
7187
|
// Check if we can use pre-filtered streaming with term mapping
|
|
7068
7188
|
if (this.opts.termMapping && this._canUsePreFilteredStreaming(criteria)) {
|
|
@@ -7092,11 +7212,11 @@ class QueryManager {
|
|
|
7092
7212
|
return 'indexed';
|
|
7093
7213
|
}
|
|
7094
7214
|
|
|
7095
|
-
/**
|
|
7096
|
-
* Estimate number of results for a query
|
|
7097
|
-
* @param {Object} criteria - Query criteria
|
|
7098
|
-
* @param {number} totalRecords - Total records in database
|
|
7099
|
-
* @returns {number} - Estimated results
|
|
7215
|
+
/**
|
|
7216
|
+
* Estimate number of results for a query
|
|
7217
|
+
* @param {Object} criteria - Query criteria
|
|
7218
|
+
* @param {number} totalRecords - Total records in database
|
|
7219
|
+
* @returns {number} - Estimated results
|
|
7100
7220
|
*/
|
|
7101
7221
|
estimateQueryResults(criteria, totalRecords) {
|
|
7102
7222
|
// If database is empty, return 0
|
|
@@ -7152,11 +7272,11 @@ class QueryManager {
|
|
|
7152
7272
|
return minResults === Infinity ? 0 : minResults;
|
|
7153
7273
|
}
|
|
7154
7274
|
|
|
7155
|
-
/**
|
|
7156
|
-
* Estimate results for $all operator
|
|
7157
|
-
* @param {Object} fieldIndex - Field index
|
|
7158
|
-
* @param {Array} values - Values to match
|
|
7159
|
-
* @returns {number} - Estimated results
|
|
7275
|
+
/**
|
|
7276
|
+
* Estimate results for $all operator
|
|
7277
|
+
* @param {Object} fieldIndex - Field index
|
|
7278
|
+
* @param {Array} values - Values to match
|
|
7279
|
+
* @returns {number} - Estimated results
|
|
7160
7280
|
*/
|
|
7161
7281
|
estimateAllOperator(fieldIndex, values) {
|
|
7162
7282
|
if (!Array.isArray(values) || values.length === 0) {
|
|
@@ -7171,13 +7291,13 @@ class QueryManager {
|
|
|
7171
7291
|
return minCount === Infinity ? 0 : minCount;
|
|
7172
7292
|
}
|
|
7173
7293
|
|
|
7174
|
-
/**
|
|
7175
|
-
* Estimate results for operators
|
|
7176
|
-
* @param {Object} fieldIndex - Field index
|
|
7177
|
-
* @param {string} operator - Operator
|
|
7178
|
-
* @param {*} value - Value
|
|
7179
|
-
* @param {number} totalRecords - Total records
|
|
7180
|
-
* @returns {number} - Estimated results
|
|
7294
|
+
/**
|
|
7295
|
+
* Estimate results for operators
|
|
7296
|
+
* @param {Object} fieldIndex - Field index
|
|
7297
|
+
* @param {string} operator - Operator
|
|
7298
|
+
* @param {*} value - Value
|
|
7299
|
+
* @param {number} totalRecords - Total records
|
|
7300
|
+
* @returns {number} - Estimated results
|
|
7181
7301
|
*/
|
|
7182
7302
|
estimateOperatorResults(fieldIndex, operator, value, totalRecords) {
|
|
7183
7303
|
// This is a simplified estimation - in practice, you might want more sophisticated logic
|
|
@@ -7208,10 +7328,10 @@ class QueryManager {
|
|
|
7208
7328
|
return 0;
|
|
7209
7329
|
}
|
|
7210
7330
|
|
|
7211
|
-
/**
|
|
7212
|
-
* Validate strict query mode
|
|
7213
|
-
* @param {Object} criteria - Query criteria
|
|
7214
|
-
* @param {Object} options - Query options
|
|
7331
|
+
/**
|
|
7332
|
+
* Validate strict query mode
|
|
7333
|
+
* @param {Object} criteria - Query criteria
|
|
7334
|
+
* @param {Object} options - Query options
|
|
7215
7335
|
*/
|
|
7216
7336
|
validateStrictQuery(criteria, options = {}) {
|
|
7217
7337
|
// Allow bypassing strict mode validation with allowNonIndexed option
|
|
@@ -7278,10 +7398,10 @@ class QueryManager {
|
|
|
7278
7398
|
}
|
|
7279
7399
|
}
|
|
7280
7400
|
|
|
7281
|
-
/**
|
|
7282
|
-
* Update average time for performance tracking
|
|
7283
|
-
* @param {string} type - Type of operation ('streaming' or 'indexed')
|
|
7284
|
-
* @param {number} time - Time taken
|
|
7401
|
+
/**
|
|
7402
|
+
* Update average time for performance tracking
|
|
7403
|
+
* @param {string} type - Type of operation ('streaming' or 'indexed')
|
|
7404
|
+
* @param {number} time - Time taken
|
|
7285
7405
|
*/
|
|
7286
7406
|
updateAverageTime(type, time) {
|
|
7287
7407
|
if (!this.usageStats[`${type}AverageTime`]) {
|
|
@@ -7294,10 +7414,10 @@ class QueryManager {
|
|
|
7294
7414
|
this.usageStats[`${type}AverageTime`] = (currentAverage * (count - 1) + time) / count;
|
|
7295
7415
|
}
|
|
7296
7416
|
|
|
7297
|
-
/**
|
|
7298
|
-
* OPTIMIZATION 2: Check if we can use pre-filtered streaming with term mapping
|
|
7299
|
-
* @param {Object} criteria - Query criteria
|
|
7300
|
-
* @returns {boolean} - True if pre-filtered streaming can be used
|
|
7417
|
+
/**
|
|
7418
|
+
* OPTIMIZATION 2: Check if we can use pre-filtered streaming with term mapping
|
|
7419
|
+
* @param {Object} criteria - Query criteria
|
|
7420
|
+
* @returns {boolean} - True if pre-filtered streaming can be used
|
|
7301
7421
|
*/
|
|
7302
7422
|
_canUsePreFilteredStreaming(criteria) {
|
|
7303
7423
|
if (!criteria || typeof criteria !== 'object') {
|
|
@@ -8213,14 +8333,14 @@ class StreamingProcessor extends events.EventEmitter {
|
|
|
8213
8333
|
}
|
|
8214
8334
|
}
|
|
8215
8335
|
|
|
8216
|
-
/**
|
|
8217
|
-
* TermManager - Manages term-to-ID mapping for efficient storage
|
|
8218
|
-
*
|
|
8219
|
-
* Responsibilities:
|
|
8220
|
-
* - Map terms to numeric IDs for space efficiency
|
|
8221
|
-
* - Track term usage counts for cleanup
|
|
8222
|
-
* - Load/save terms from/to index file
|
|
8223
|
-
* - Clean up orphaned terms
|
|
8336
|
+
/**
|
|
8337
|
+
* TermManager - Manages term-to-ID mapping for efficient storage
|
|
8338
|
+
*
|
|
8339
|
+
* Responsibilities:
|
|
8340
|
+
* - Map terms to numeric IDs for space efficiency
|
|
8341
|
+
* - Track term usage counts for cleanup
|
|
8342
|
+
* - Load/save terms from/to index file
|
|
8343
|
+
* - Clean up orphaned terms
|
|
8224
8344
|
*/
|
|
8225
8345
|
class TermManager {
|
|
8226
8346
|
constructor() {
|
|
@@ -8230,10 +8350,10 @@ class TermManager {
|
|
|
8230
8350
|
this.nextId = 1;
|
|
8231
8351
|
}
|
|
8232
8352
|
|
|
8233
|
-
/**
|
|
8234
|
-
* Get ID for a term (create if doesn't exist)
|
|
8235
|
-
* @param {string} term - Term to get ID for
|
|
8236
|
-
* @returns {number} - Numeric ID for the term
|
|
8353
|
+
/**
|
|
8354
|
+
* Get ID for a term (create if doesn't exist)
|
|
8355
|
+
* @param {string} term - Term to get ID for
|
|
8356
|
+
* @returns {number} - Numeric ID for the term
|
|
8237
8357
|
*/
|
|
8238
8358
|
getTermId(term) {
|
|
8239
8359
|
if (this.termToId.has(term)) {
|
|
@@ -8248,12 +8368,12 @@ class TermManager {
|
|
|
8248
8368
|
return id;
|
|
8249
8369
|
}
|
|
8250
8370
|
|
|
8251
|
-
/**
|
|
8252
|
-
* Get term ID without incrementing count (for IndexManager use)
|
|
8253
|
-
* @param {string} term - Term to get ID for
|
|
8254
|
-
* @returns {number|undefined} - Numeric ID for the term, or undefined if not found
|
|
8255
|
-
* CRITICAL: Does NOT create new IDs - only returns existing ones
|
|
8256
|
-
* This prevents creating invalid term IDs during queries when terms haven't been loaded yet
|
|
8371
|
+
/**
|
|
8372
|
+
* Get term ID without incrementing count (for IndexManager use)
|
|
8373
|
+
* @param {string} term - Term to get ID for
|
|
8374
|
+
* @returns {number|undefined} - Numeric ID for the term, or undefined if not found
|
|
8375
|
+
* CRITICAL: Does NOT create new IDs - only returns existing ones
|
|
8376
|
+
* This prevents creating invalid term IDs during queries when terms haven't been loaded yet
|
|
8257
8377
|
*/
|
|
8258
8378
|
getTermIdWithoutIncrement(term) {
|
|
8259
8379
|
if (this.termToId.has(term)) {
|
|
@@ -8266,19 +8386,19 @@ class TermManager {
|
|
|
8266
8386
|
return undefined;
|
|
8267
8387
|
}
|
|
8268
8388
|
|
|
8269
|
-
/**
|
|
8270
|
-
* Get term by ID
|
|
8271
|
-
* @param {number} id - Numeric ID
|
|
8272
|
-
* @returns {string|null} - Term or null if not found
|
|
8389
|
+
/**
|
|
8390
|
+
* Get term by ID
|
|
8391
|
+
* @param {number} id - Numeric ID
|
|
8392
|
+
* @returns {string|null} - Term or null if not found
|
|
8273
8393
|
*/
|
|
8274
8394
|
getTerm(id) {
|
|
8275
8395
|
return this.idToTerm.get(id) || null;
|
|
8276
8396
|
}
|
|
8277
8397
|
|
|
8278
|
-
/**
|
|
8279
|
-
* Bulk get term IDs for multiple terms (optimized for performance)
|
|
8280
|
-
* @param {string[]} terms - Array of terms to get IDs for
|
|
8281
|
-
* @returns {number[]} - Array of term IDs in the same order
|
|
8398
|
+
/**
|
|
8399
|
+
* Bulk get term IDs for multiple terms (optimized for performance)
|
|
8400
|
+
* @param {string[]} terms - Array of terms to get IDs for
|
|
8401
|
+
* @returns {number[]} - Array of term IDs in the same order
|
|
8282
8402
|
*/
|
|
8283
8403
|
bulkGetTermIds(terms) {
|
|
8284
8404
|
if (!Array.isArray(terms) || terms.length === 0) {
|
|
@@ -8304,9 +8424,9 @@ class TermManager {
|
|
|
8304
8424
|
return termIds;
|
|
8305
8425
|
}
|
|
8306
8426
|
|
|
8307
|
-
/**
|
|
8308
|
-
* Load terms from file data
|
|
8309
|
-
* @param {Object} termsData - Terms data from file
|
|
8427
|
+
/**
|
|
8428
|
+
* Load terms from file data
|
|
8429
|
+
* @param {Object} termsData - Terms data from file
|
|
8310
8430
|
*/
|
|
8311
8431
|
loadTerms(termsData) {
|
|
8312
8432
|
if (!termsData || typeof termsData !== 'object') {
|
|
@@ -8324,9 +8444,9 @@ class TermManager {
|
|
|
8324
8444
|
}
|
|
8325
8445
|
}
|
|
8326
8446
|
|
|
8327
|
-
/**
|
|
8328
|
-
* Save terms to file format
|
|
8329
|
-
* @returns {Object} - Terms data for file
|
|
8447
|
+
/**
|
|
8448
|
+
* Save terms to file format
|
|
8449
|
+
* @returns {Object} - Terms data for file
|
|
8330
8450
|
*/
|
|
8331
8451
|
saveTerms() {
|
|
8332
8452
|
const termsData = {};
|
|
@@ -8336,11 +8456,11 @@ class TermManager {
|
|
|
8336
8456
|
return termsData;
|
|
8337
8457
|
}
|
|
8338
8458
|
|
|
8339
|
-
/**
|
|
8340
|
-
* Clean up orphaned terms (terms with count 0)
|
|
8341
|
-
* @param {boolean} forceCleanup - Force cleanup even if conditions not met
|
|
8342
|
-
* @param {Object} options - Cleanup options
|
|
8343
|
-
* @returns {number} - Number of orphaned terms removed
|
|
8459
|
+
/**
|
|
8460
|
+
* Clean up orphaned terms (terms with count 0)
|
|
8461
|
+
* @param {boolean} forceCleanup - Force cleanup even if conditions not met
|
|
8462
|
+
* @param {Object} options - Cleanup options
|
|
8463
|
+
* @returns {number} - Number of orphaned terms removed
|
|
8344
8464
|
*/
|
|
8345
8465
|
cleanupOrphanedTerms(forceCleanup = false, options = {}) {
|
|
8346
8466
|
const {
|
|
@@ -8398,9 +8518,9 @@ class TermManager {
|
|
|
8398
8518
|
return orphanedIds.length;
|
|
8399
8519
|
}
|
|
8400
8520
|
|
|
8401
|
-
/**
|
|
8402
|
-
* Check if system is safe for cleanup operations
|
|
8403
|
-
* @returns {boolean} - True if system is safe for cleanup
|
|
8521
|
+
/**
|
|
8522
|
+
* Check if system is safe for cleanup operations
|
|
8523
|
+
* @returns {boolean} - True if system is safe for cleanup
|
|
8404
8524
|
*/
|
|
8405
8525
|
isSystemSafe() {
|
|
8406
8526
|
// This method should be overridden by the database instance
|
|
@@ -8408,10 +8528,10 @@ class TermManager {
|
|
|
8408
8528
|
return true; // Default to safe for backward compatibility
|
|
8409
8529
|
}
|
|
8410
8530
|
|
|
8411
|
-
/**
|
|
8412
|
-
* Perform intelligent automatic cleanup
|
|
8413
|
-
* @param {Object} options - Cleanup options
|
|
8414
|
-
* @returns {number} - Number of orphaned terms removed
|
|
8531
|
+
/**
|
|
8532
|
+
* Perform intelligent automatic cleanup
|
|
8533
|
+
* @param {Object} options - Cleanup options
|
|
8534
|
+
* @returns {number} - Number of orphaned terms removed
|
|
8415
8535
|
*/
|
|
8416
8536
|
performIntelligentCleanup(options = {}) {
|
|
8417
8537
|
return this.cleanupOrphanedTerms(false, {
|
|
@@ -8425,27 +8545,27 @@ class TermManager {
|
|
|
8425
8545
|
});
|
|
8426
8546
|
}
|
|
8427
8547
|
|
|
8428
|
-
/**
|
|
8429
|
-
* Decrement term count (when term is removed from index)
|
|
8430
|
-
* @param {number} termId - Term ID to decrement
|
|
8548
|
+
/**
|
|
8549
|
+
* Decrement term count (when term is removed from index)
|
|
8550
|
+
* @param {number} termId - Term ID to decrement
|
|
8431
8551
|
*/
|
|
8432
8552
|
decrementTermCount(termId) {
|
|
8433
8553
|
const count = this.termCounts.get(termId) || 0;
|
|
8434
8554
|
this.termCounts.set(termId, Math.max(0, count - 1));
|
|
8435
8555
|
}
|
|
8436
8556
|
|
|
8437
|
-
/**
|
|
8438
|
-
* Increment term count (when term is added to index)
|
|
8439
|
-
* @param {number} termId - Term ID to increment
|
|
8557
|
+
/**
|
|
8558
|
+
* Increment term count (when term is added to index)
|
|
8559
|
+
* @param {number} termId - Term ID to increment
|
|
8440
8560
|
*/
|
|
8441
8561
|
incrementTermCount(termId) {
|
|
8442
8562
|
const count = this.termCounts.get(termId) || 0;
|
|
8443
8563
|
this.termCounts.set(termId, count + 1);
|
|
8444
8564
|
}
|
|
8445
8565
|
|
|
8446
|
-
/**
|
|
8447
|
-
* Get statistics about terms
|
|
8448
|
-
* @returns {Object} - Term statistics
|
|
8566
|
+
/**
|
|
8567
|
+
* Get statistics about terms
|
|
8568
|
+
* @returns {Object} - Term statistics
|
|
8449
8569
|
*/
|
|
8450
8570
|
getStats() {
|
|
8451
8571
|
return {
|
|
@@ -8455,34 +8575,34 @@ class TermManager {
|
|
|
8455
8575
|
};
|
|
8456
8576
|
}
|
|
8457
8577
|
|
|
8458
|
-
/**
|
|
8459
|
-
* Check if a term exists
|
|
8460
|
-
* @param {string} term - Term to check
|
|
8461
|
-
* @returns {boolean} - True if term exists
|
|
8578
|
+
/**
|
|
8579
|
+
* Check if a term exists
|
|
8580
|
+
* @param {string} term - Term to check
|
|
8581
|
+
* @returns {boolean} - True if term exists
|
|
8462
8582
|
*/
|
|
8463
8583
|
hasTerm(term) {
|
|
8464
8584
|
return this.termToId.has(term);
|
|
8465
8585
|
}
|
|
8466
8586
|
|
|
8467
|
-
/**
|
|
8468
|
-
* Get all terms
|
|
8469
|
-
* @returns {Array} - Array of all terms
|
|
8587
|
+
/**
|
|
8588
|
+
* Get all terms
|
|
8589
|
+
* @returns {Array} - Array of all terms
|
|
8470
8590
|
*/
|
|
8471
8591
|
getAllTerms() {
|
|
8472
8592
|
return Array.from(this.termToId.keys());
|
|
8473
8593
|
}
|
|
8474
8594
|
|
|
8475
|
-
/**
|
|
8476
|
-
* Get all term IDs
|
|
8477
|
-
* @returns {Array} - Array of all term IDs
|
|
8595
|
+
/**
|
|
8596
|
+
* Get all term IDs
|
|
8597
|
+
* @returns {Array} - Array of all term IDs
|
|
8478
8598
|
*/
|
|
8479
8599
|
getAllTermIds() {
|
|
8480
8600
|
return Array.from(this.idToTerm.keys());
|
|
8481
8601
|
}
|
|
8482
8602
|
|
|
8483
|
-
/**
|
|
8484
|
-
* Get statistics about term mapping
|
|
8485
|
-
* @returns {Object} - Statistics object
|
|
8603
|
+
/**
|
|
8604
|
+
* Get statistics about term mapping
|
|
8605
|
+
* @returns {Object} - Statistics object
|
|
8486
8606
|
*/
|
|
8487
8607
|
getStatistics() {
|
|
8488
8608
|
return {
|
|
@@ -8494,9 +8614,9 @@ class TermManager {
|
|
|
8494
8614
|
}
|
|
8495
8615
|
}
|
|
8496
8616
|
|
|
8497
|
-
/**
|
|
8498
|
-
* IterateEntry class for intuitive API with automatic change detection
|
|
8499
|
-
* Uses native JavaScript setters for maximum performance
|
|
8617
|
+
/**
|
|
8618
|
+
* IterateEntry class for intuitive API with automatic change detection
|
|
8619
|
+
* Uses native JavaScript setters for maximum performance
|
|
8500
8620
|
*/
|
|
8501
8621
|
class IterateEntry {
|
|
8502
8622
|
constructor(entry, originalRecord) {
|
|
@@ -8549,8 +8669,8 @@ class IterateEntry {
|
|
|
8549
8669
|
}
|
|
8550
8670
|
}
|
|
8551
8671
|
|
|
8552
|
-
/**
|
|
8553
|
-
* InsertSession - Simple batch insertion without memory duplication
|
|
8672
|
+
/**
|
|
8673
|
+
* InsertSession - Simple batch insertion without memory duplication
|
|
8554
8674
|
*/
|
|
8555
8675
|
class InsertSession {
|
|
8556
8676
|
constructor(database, sessionOptions = {}) {
|
|
@@ -8731,8 +8851,8 @@ class InsertSession {
|
|
|
8731
8851
|
return insertedCount;
|
|
8732
8852
|
}
|
|
8733
8853
|
|
|
8734
|
-
/**
|
|
8735
|
-
* Wait for this session's operations to complete
|
|
8854
|
+
/**
|
|
8855
|
+
* Wait for this session's operations to complete
|
|
8736
8856
|
*/
|
|
8737
8857
|
async waitForOperations(maxWaitTime = null) {
|
|
8738
8858
|
const startTime = Date.now();
|
|
@@ -8750,15 +8870,15 @@ class InsertSession {
|
|
|
8750
8870
|
return true;
|
|
8751
8871
|
}
|
|
8752
8872
|
|
|
8753
|
-
/**
|
|
8754
|
-
* Check if this session has pending operations
|
|
8873
|
+
/**
|
|
8874
|
+
* Check if this session has pending operations
|
|
8755
8875
|
*/
|
|
8756
8876
|
hasPendingOperations() {
|
|
8757
8877
|
return this.pendingAutoFlushes.size > 0 || this.flushing || this.batches.length > 0 || this.currentBatch.length > 0;
|
|
8758
8878
|
}
|
|
8759
8879
|
|
|
8760
|
-
/**
|
|
8761
|
-
* Destroy this session and unregister it
|
|
8880
|
+
/**
|
|
8881
|
+
* Destroy this session and unregister it
|
|
8762
8882
|
*/
|
|
8763
8883
|
destroy() {
|
|
8764
8884
|
// Unregister from database
|
|
@@ -8773,16 +8893,16 @@ class InsertSession {
|
|
|
8773
8893
|
}
|
|
8774
8894
|
}
|
|
8775
8895
|
|
|
8776
|
-
/**
|
|
8777
|
-
* JexiDB - A high-performance, in-memory database with persistence
|
|
8778
|
-
*
|
|
8779
|
-
* Features:
|
|
8780
|
-
* - In-memory storage with optional persistence
|
|
8781
|
-
* - Advanced indexing and querying
|
|
8782
|
-
* - Transaction support
|
|
8783
|
-
* - Manual save functionality
|
|
8784
|
-
* - Recovery mechanisms
|
|
8785
|
-
* - Performance optimizations
|
|
8896
|
+
/**
|
|
8897
|
+
* JexiDB - A high-performance, in-memory database with persistence
|
|
8898
|
+
*
|
|
8899
|
+
* Features:
|
|
8900
|
+
* - In-memory storage with optional persistence
|
|
8901
|
+
* - Advanced indexing and querying
|
|
8902
|
+
* - Transaction support
|
|
8903
|
+
* - Manual save functionality
|
|
8904
|
+
* - Recovery mechanisms
|
|
8905
|
+
* - Performance optimizations
|
|
8786
8906
|
*/
|
|
8787
8907
|
class Database extends events.EventEmitter {
|
|
8788
8908
|
constructor(file, opts = {}) {
|
|
@@ -8906,8 +9026,8 @@ class Database extends events.EventEmitter {
|
|
|
8906
9026
|
// Note: Validation will be done after configuration conversion in initializeManagers()
|
|
8907
9027
|
}
|
|
8908
9028
|
|
|
8909
|
-
/**
|
|
8910
|
-
* Validate field and index configuration
|
|
9029
|
+
/**
|
|
9030
|
+
* Validate field and index configuration
|
|
8911
9031
|
*/
|
|
8912
9032
|
validateIndexConfiguration() {
|
|
8913
9033
|
// Validate fields configuration
|
|
@@ -8937,8 +9057,8 @@ class Database extends events.EventEmitter {
|
|
|
8937
9057
|
}
|
|
8938
9058
|
}
|
|
8939
9059
|
|
|
8940
|
-
/**
|
|
8941
|
-
* Validate field types
|
|
9060
|
+
/**
|
|
9061
|
+
* Validate field types
|
|
8942
9062
|
*/
|
|
8943
9063
|
validateFieldTypes(fields, configType) {
|
|
8944
9064
|
const supportedTypes = ['string', 'number', 'boolean', 'array:string', 'array:number', 'array:boolean', 'array', 'object', 'auto'];
|
|
@@ -8970,8 +9090,8 @@ class Database extends events.EventEmitter {
|
|
|
8970
9090
|
}
|
|
8971
9091
|
}
|
|
8972
9092
|
|
|
8973
|
-
/**
|
|
8974
|
-
* Validate index fields array
|
|
9093
|
+
/**
|
|
9094
|
+
* Validate index fields array
|
|
8975
9095
|
*/
|
|
8976
9096
|
validateIndexFields(indexFields) {
|
|
8977
9097
|
if (!this.opts.fields) {
|
|
@@ -8989,8 +9109,8 @@ class Database extends events.EventEmitter {
|
|
|
8989
9109
|
}
|
|
8990
9110
|
}
|
|
8991
9111
|
|
|
8992
|
-
/**
|
|
8993
|
-
* Prepare index configuration for IndexManager
|
|
9112
|
+
/**
|
|
9113
|
+
* Prepare index configuration for IndexManager
|
|
8994
9114
|
*/
|
|
8995
9115
|
prepareIndexConfiguration() {
|
|
8996
9116
|
if (Array.isArray(this.opts.indexes)) {
|
|
@@ -9013,8 +9133,8 @@ class Database extends events.EventEmitter {
|
|
|
9013
9133
|
// Legacy format (indexes as object) is already compatible
|
|
9014
9134
|
}
|
|
9015
9135
|
|
|
9016
|
-
/**
|
|
9017
|
-
* Initialize all managers
|
|
9136
|
+
/**
|
|
9137
|
+
* Initialize all managers
|
|
9018
9138
|
*/
|
|
9019
9139
|
initializeManagers() {
|
|
9020
9140
|
// CRITICAL FIX: Prevent double initialization which corrupts term mappings
|
|
@@ -9120,9 +9240,9 @@ class Database extends events.EventEmitter {
|
|
|
9120
9240
|
this.streamingProcessor = new StreamingProcessor(this.opts);
|
|
9121
9241
|
}
|
|
9122
9242
|
|
|
9123
|
-
/**
|
|
9124
|
-
* Get term mapping fields from configuration or indexes (auto-detected)
|
|
9125
|
-
* @returns {string[]} Array of field names that use term mapping
|
|
9243
|
+
/**
|
|
9244
|
+
* Get term mapping fields from configuration or indexes (auto-detected)
|
|
9245
|
+
* @returns {string[]} Array of field names that use term mapping
|
|
9126
9246
|
*/
|
|
9127
9247
|
getTermMappingFields() {
|
|
9128
9248
|
// If termMappingFields is explicitly configured, use it
|
|
@@ -9142,9 +9262,9 @@ class Database extends events.EventEmitter {
|
|
|
9142
9262
|
return termMappingFields;
|
|
9143
9263
|
}
|
|
9144
9264
|
|
|
9145
|
-
/**
|
|
9146
|
-
* CRITICAL FIX: Validate database state before critical operations
|
|
9147
|
-
* Prevents crashes from undefined methods and invalid states
|
|
9265
|
+
/**
|
|
9266
|
+
* CRITICAL FIX: Validate database state before critical operations
|
|
9267
|
+
* Prevents crashes from undefined methods and invalid states
|
|
9148
9268
|
*/
|
|
9149
9269
|
validateState() {
|
|
9150
9270
|
if (this.destroyed) {
|
|
@@ -9171,9 +9291,9 @@ class Database extends events.EventEmitter {
|
|
|
9171
9291
|
return true;
|
|
9172
9292
|
}
|
|
9173
9293
|
|
|
9174
|
-
/**
|
|
9175
|
-
* CRITICAL FIX: Ensure file path is valid and accessible
|
|
9176
|
-
* Prevents file path loss issues mentioned in crash report
|
|
9294
|
+
/**
|
|
9295
|
+
* CRITICAL FIX: Ensure file path is valid and accessible
|
|
9296
|
+
* Prevents file path loss issues mentioned in crash report
|
|
9177
9297
|
*/
|
|
9178
9298
|
ensureFilePath() {
|
|
9179
9299
|
if (!this.normalizedFile) {
|
|
@@ -9182,16 +9302,16 @@ class Database extends events.EventEmitter {
|
|
|
9182
9302
|
return this.normalizedFile;
|
|
9183
9303
|
}
|
|
9184
9304
|
|
|
9185
|
-
/**
|
|
9186
|
-
* Normalize file path to ensure it ends with .jdb
|
|
9305
|
+
/**
|
|
9306
|
+
* Normalize file path to ensure it ends with .jdb
|
|
9187
9307
|
*/
|
|
9188
9308
|
normalizeFilePath(file) {
|
|
9189
9309
|
if (!file) return null;
|
|
9190
9310
|
return file.endsWith('.jdb') ? file : `${file}.jdb`;
|
|
9191
9311
|
}
|
|
9192
9312
|
|
|
9193
|
-
/**
|
|
9194
|
-
* Initialize the database
|
|
9313
|
+
/**
|
|
9314
|
+
* Initialize the database
|
|
9195
9315
|
*/
|
|
9196
9316
|
async initialize() {
|
|
9197
9317
|
// Check if database is destroyed first (before checking initialized)
|
|
@@ -9281,10 +9401,10 @@ class Database extends events.EventEmitter {
|
|
|
9281
9401
|
}
|
|
9282
9402
|
}
|
|
9283
9403
|
|
|
9284
|
-
/**
|
|
9285
|
-
* Validate that the database is initialized before performing operations
|
|
9286
|
-
* @param {string} operation - The operation being attempted
|
|
9287
|
-
* @throws {Error} If database is not initialized
|
|
9404
|
+
/**
|
|
9405
|
+
* Validate that the database is initialized before performing operations
|
|
9406
|
+
* @param {string} operation - The operation being attempted
|
|
9407
|
+
* @throws {Error} If database is not initialized
|
|
9288
9408
|
*/
|
|
9289
9409
|
_validateInitialization(operation) {
|
|
9290
9410
|
if (this.destroyed) {
|
|
@@ -9299,8 +9419,8 @@ class Database extends events.EventEmitter {
|
|
|
9299
9419
|
}
|
|
9300
9420
|
}
|
|
9301
9421
|
|
|
9302
|
-
/**
|
|
9303
|
-
* Clear existing database files (.jdb and .idx.jdb)
|
|
9422
|
+
/**
|
|
9423
|
+
* Clear existing database files (.jdb and .idx.jdb)
|
|
9304
9424
|
*/
|
|
9305
9425
|
async clearExistingFiles() {
|
|
9306
9426
|
if (!this.normalizedFile) return;
|
|
@@ -9341,8 +9461,8 @@ class Database extends events.EventEmitter {
|
|
|
9341
9461
|
}
|
|
9342
9462
|
}
|
|
9343
9463
|
|
|
9344
|
-
/**
|
|
9345
|
-
* Load data from file
|
|
9464
|
+
/**
|
|
9465
|
+
* Load data from file
|
|
9346
9466
|
*/
|
|
9347
9467
|
async load() {
|
|
9348
9468
|
if (!this.normalizedFile) return;
|
|
@@ -9627,9 +9747,9 @@ class Database extends events.EventEmitter {
|
|
|
9627
9747
|
}
|
|
9628
9748
|
}
|
|
9629
9749
|
|
|
9630
|
-
/**
|
|
9631
|
-
* Save data to file
|
|
9632
|
-
* @param {boolean} inQueue - Whether to execute within the operation queue (default: false)
|
|
9750
|
+
/**
|
|
9751
|
+
* Save data to file
|
|
9752
|
+
* @param {boolean} inQueue - Whether to execute within the operation queue (default: false)
|
|
9633
9753
|
*/
|
|
9634
9754
|
async save(inQueue = false) {
|
|
9635
9755
|
this._validateInitialization('save');
|
|
@@ -9706,8 +9826,8 @@ class Database extends events.EventEmitter {
|
|
|
9706
9826
|
}
|
|
9707
9827
|
}
|
|
9708
9828
|
|
|
9709
|
-
/**
|
|
9710
|
-
* Internal save implementation (without queue)
|
|
9829
|
+
/**
|
|
9830
|
+
* Internal save implementation (without queue)
|
|
9711
9831
|
*/
|
|
9712
9832
|
async _doSave() {
|
|
9713
9833
|
// CRITICAL FIX: Check if database is destroyed
|
|
@@ -9758,6 +9878,7 @@ class Database extends events.EventEmitter {
|
|
|
9758
9878
|
const startLineNumber = this.pendingIndexUpdates[0].lineNumber;
|
|
9759
9879
|
|
|
9760
9880
|
// Process index updates in batch
|
|
9881
|
+
await this._ensureLazyIndexLoaded();
|
|
9761
9882
|
await this.indexManager.addBatch(records, startLineNumber);
|
|
9762
9883
|
|
|
9763
9884
|
// Clear pending updates
|
|
@@ -10282,11 +10403,11 @@ class Database extends events.EventEmitter {
|
|
|
10282
10403
|
}
|
|
10283
10404
|
}
|
|
10284
10405
|
|
|
10285
|
-
/**
|
|
10286
|
-
* Process term mapping for a record
|
|
10287
|
-
* @param {Object} record - Record to process
|
|
10288
|
-
* @param {boolean} isUpdate - Whether this is an update operation
|
|
10289
|
-
* @param {Object} oldRecord - Original record (for updates)
|
|
10406
|
+
/**
|
|
10407
|
+
* Process term mapping for a record
|
|
10408
|
+
* @param {Object} record - Record to process
|
|
10409
|
+
* @param {boolean} isUpdate - Whether this is an update operation
|
|
10410
|
+
* @param {Object} oldRecord - Original record (for updates)
|
|
10290
10411
|
*/
|
|
10291
10412
|
processTermMapping(record, isUpdate = false, oldRecord = null) {
|
|
10292
10413
|
const termMappingFields = this.getTermMappingFields();
|
|
@@ -10347,10 +10468,10 @@ class Database extends events.EventEmitter {
|
|
|
10347
10468
|
}
|
|
10348
10469
|
}
|
|
10349
10470
|
|
|
10350
|
-
/**
|
|
10351
|
-
* Convert terms to term IDs for serialization (SPACE OPTIMIZATION)
|
|
10352
|
-
* @param {Object} record - Record to process
|
|
10353
|
-
* @returns {Object} - Record with terms converted to term IDs
|
|
10471
|
+
/**
|
|
10472
|
+
* Convert terms to term IDs for serialization (SPACE OPTIMIZATION)
|
|
10473
|
+
* @param {Object} record - Record to process
|
|
10474
|
+
* @returns {Object} - Record with terms converted to term IDs
|
|
10354
10475
|
*/
|
|
10355
10476
|
removeTermIdsForSerialization(record) {
|
|
10356
10477
|
const termMappingFields = this.getTermMappingFields();
|
|
@@ -10376,10 +10497,10 @@ class Database extends events.EventEmitter {
|
|
|
10376
10497
|
return optimizedRecord;
|
|
10377
10498
|
}
|
|
10378
10499
|
|
|
10379
|
-
/**
|
|
10380
|
-
* Convert term IDs back to terms after deserialization (SPACE OPTIMIZATION)
|
|
10381
|
-
* @param {Object} record - Record with term IDs
|
|
10382
|
-
* @returns {Object} - Record with terms restored
|
|
10500
|
+
/**
|
|
10501
|
+
* Convert term IDs back to terms after deserialization (SPACE OPTIMIZATION)
|
|
10502
|
+
* @param {Object} record - Record with term IDs
|
|
10503
|
+
* @returns {Object} - Record with terms restored
|
|
10383
10504
|
*/
|
|
10384
10505
|
restoreTermIdsAfterDeserialization(record) {
|
|
10385
10506
|
const termMappingFields = this.getTermMappingFields();
|
|
@@ -10409,9 +10530,9 @@ class Database extends events.EventEmitter {
|
|
|
10409
10530
|
return restoredRecord;
|
|
10410
10531
|
}
|
|
10411
10532
|
|
|
10412
|
-
/**
|
|
10413
|
-
* Remove term mapping for a record
|
|
10414
|
-
* @param {Object} record - Record to process
|
|
10533
|
+
/**
|
|
10534
|
+
* Remove term mapping for a record
|
|
10535
|
+
* @param {Object} record - Record to process
|
|
10415
10536
|
*/
|
|
10416
10537
|
removeTermMapping(record) {
|
|
10417
10538
|
const termMappingFields = this.getTermMappingFields();
|
|
@@ -10433,10 +10554,10 @@ class Database extends events.EventEmitter {
|
|
|
10433
10554
|
}
|
|
10434
10555
|
}
|
|
10435
10556
|
|
|
10436
|
-
/**
|
|
10437
|
-
* Process term mapping for multiple records in batch (OPTIMIZATION)
|
|
10438
|
-
* @param {Array} records - Records to process
|
|
10439
|
-
* @returns {Array} - Processed records with term mappings
|
|
10557
|
+
/**
|
|
10558
|
+
* Process term mapping for multiple records in batch (OPTIMIZATION)
|
|
10559
|
+
* @param {Array} records - Records to process
|
|
10560
|
+
* @returns {Array} - Processed records with term mappings
|
|
10440
10561
|
*/
|
|
10441
10562
|
processTermMappingBatch(records) {
|
|
10442
10563
|
const termMappingFields = this.getTermMappingFields();
|
|
@@ -10488,10 +10609,10 @@ class Database extends events.EventEmitter {
|
|
|
10488
10609
|
return records;
|
|
10489
10610
|
}
|
|
10490
10611
|
|
|
10491
|
-
/**
|
|
10492
|
-
* Calculate total size of serialized records (OPTIMIZATION)
|
|
10493
|
-
* @param {Array} records - Records to calculate size for
|
|
10494
|
-
* @returns {number} - Total size in bytes
|
|
10612
|
+
/**
|
|
10613
|
+
* Calculate total size of serialized records (OPTIMIZATION)
|
|
10614
|
+
* @param {Array} records - Records to calculate size for
|
|
10615
|
+
* @returns {number} - Total size in bytes
|
|
10495
10616
|
*/
|
|
10496
10617
|
calculateBatchSize(records) {
|
|
10497
10618
|
if (!records || !records.length) return 0;
|
|
@@ -10506,10 +10627,10 @@ class Database extends events.EventEmitter {
|
|
|
10506
10627
|
return totalSize;
|
|
10507
10628
|
}
|
|
10508
10629
|
|
|
10509
|
-
/**
|
|
10510
|
-
* Begin an insert session for batch operations
|
|
10511
|
-
* @param {Object} sessionOptions - Options for the insert session
|
|
10512
|
-
* @returns {InsertSession} - The insert session instance
|
|
10630
|
+
/**
|
|
10631
|
+
* Begin an insert session for batch operations
|
|
10632
|
+
* @param {Object} sessionOptions - Options for the insert session
|
|
10633
|
+
* @returns {InsertSession} - The insert session instance
|
|
10513
10634
|
*/
|
|
10514
10635
|
beginInsertSession(sessionOptions = {}) {
|
|
10515
10636
|
if (this.destroyed) {
|
|
@@ -10521,8 +10642,8 @@ class Database extends events.EventEmitter {
|
|
|
10521
10642
|
return new InsertSession(this, sessionOptions);
|
|
10522
10643
|
}
|
|
10523
10644
|
|
|
10524
|
-
/**
|
|
10525
|
-
* Insert a new record
|
|
10645
|
+
/**
|
|
10646
|
+
* Insert a new record
|
|
10526
10647
|
*/
|
|
10527
10648
|
async insert(data) {
|
|
10528
10649
|
this._validateInitialization('insert');
|
|
@@ -10615,8 +10736,8 @@ class Database extends events.EventEmitter {
|
|
|
10615
10736
|
});
|
|
10616
10737
|
}
|
|
10617
10738
|
|
|
10618
|
-
/**
|
|
10619
|
-
* Insert multiple records in batch (OPTIMIZATION)
|
|
10739
|
+
/**
|
|
10740
|
+
* Insert multiple records in batch (OPTIMIZATION)
|
|
10620
10741
|
*/
|
|
10621
10742
|
async insertBatch(dataArray) {
|
|
10622
10743
|
this._validateInitialization('insertBatch');
|
|
@@ -10641,8 +10762,8 @@ class Database extends events.EventEmitter {
|
|
|
10641
10762
|
});
|
|
10642
10763
|
}
|
|
10643
10764
|
|
|
10644
|
-
/**
|
|
10645
|
-
* Internal implementation for insertBatch to allow inline execution when already inside the queue
|
|
10765
|
+
/**
|
|
10766
|
+
* Internal implementation for insertBatch to allow inline execution when already inside the queue
|
|
10646
10767
|
*/
|
|
10647
10768
|
async _insertBatchInternal(dataArray) {
|
|
10648
10769
|
// CRITICAL FIX: Validate state before insert operation
|
|
@@ -10733,8 +10854,8 @@ class Database extends events.EventEmitter {
|
|
|
10733
10854
|
return processedRecords;
|
|
10734
10855
|
}
|
|
10735
10856
|
|
|
10736
|
-
/**
|
|
10737
|
-
* Find records matching criteria
|
|
10857
|
+
/**
|
|
10858
|
+
* Find records matching criteria
|
|
10738
10859
|
*/
|
|
10739
10860
|
async find(criteria = {}, options = {}) {
|
|
10740
10861
|
this._validateInitialization('find');
|
|
@@ -10842,11 +10963,11 @@ class Database extends events.EventEmitter {
|
|
|
10842
10963
|
}
|
|
10843
10964
|
}
|
|
10844
10965
|
|
|
10845
|
-
/**
|
|
10846
|
-
* Validate indexed query mode for strict mode
|
|
10847
|
-
* @private
|
|
10848
|
-
* @param {Object} criteria - Query criteria
|
|
10849
|
-
* @param {Object} options - Query options
|
|
10966
|
+
/**
|
|
10967
|
+
* Validate indexed query mode for strict mode
|
|
10968
|
+
* @private
|
|
10969
|
+
* @param {Object} criteria - Query criteria
|
|
10970
|
+
* @param {Object} options - Query options
|
|
10850
10971
|
*/
|
|
10851
10972
|
_validateIndexedQuery(criteria, options = {}) {
|
|
10852
10973
|
// Allow bypassing strict mode validation with allowNonIndexed option
|
|
@@ -10872,10 +10993,10 @@ class Database extends events.EventEmitter {
|
|
|
10872
10993
|
}
|
|
10873
10994
|
}
|
|
10874
10995
|
|
|
10875
|
-
/**
|
|
10876
|
-
* Create a shallow copy of a record for change detection
|
|
10877
|
-
* Optimized for known field types: number, string, null, or single-level arrays
|
|
10878
|
-
* @private
|
|
10996
|
+
/**
|
|
10997
|
+
* Create a shallow copy of a record for change detection
|
|
10998
|
+
* Optimized for known field types: number, string, null, or single-level arrays
|
|
10999
|
+
* @private
|
|
10879
11000
|
*/
|
|
10880
11001
|
_createShallowCopy(record) {
|
|
10881
11002
|
const copy = {};
|
|
@@ -10900,10 +11021,10 @@ class Database extends events.EventEmitter {
|
|
|
10900
11021
|
return copy;
|
|
10901
11022
|
}
|
|
10902
11023
|
|
|
10903
|
-
/**
|
|
10904
|
-
* Create an intuitive API wrapper using a class with Proxy
|
|
10905
|
-
* Combines the benefits of classes with the flexibility of Proxy
|
|
10906
|
-
* @private
|
|
11024
|
+
/**
|
|
11025
|
+
* Create an intuitive API wrapper using a class with Proxy
|
|
11026
|
+
* Combines the benefits of classes with the flexibility of Proxy
|
|
11027
|
+
* @private
|
|
10907
11028
|
*/
|
|
10908
11029
|
_createEntryProxy(entry, originalRecord) {
|
|
10909
11030
|
// Create a class instance that wraps the entry
|
|
@@ -10938,9 +11059,9 @@ class Database extends events.EventEmitter {
|
|
|
10938
11059
|
});
|
|
10939
11060
|
}
|
|
10940
11061
|
|
|
10941
|
-
/**
|
|
10942
|
-
* Create a high-performance wrapper for maximum speed
|
|
10943
|
-
* @private
|
|
11062
|
+
/**
|
|
11063
|
+
* Create a high-performance wrapper for maximum speed
|
|
11064
|
+
* @private
|
|
10944
11065
|
*/
|
|
10945
11066
|
_createHighPerformanceWrapper(entry, originalRecord) {
|
|
10946
11067
|
// Create a simple wrapper object for high performance
|
|
@@ -10958,10 +11079,10 @@ class Database extends events.EventEmitter {
|
|
|
10958
11079
|
return wrapper;
|
|
10959
11080
|
}
|
|
10960
11081
|
|
|
10961
|
-
/**
|
|
10962
|
-
* Check if a record has changed using optimized comparison
|
|
10963
|
-
* Optimized for known field types: number, string, null, or single-level arrays
|
|
10964
|
-
* @private
|
|
11082
|
+
/**
|
|
11083
|
+
* Check if a record has changed using optimized comparison
|
|
11084
|
+
* Optimized for known field types: number, string, null, or single-level arrays
|
|
11085
|
+
* @private
|
|
10965
11086
|
*/
|
|
10966
11087
|
_hasRecordChanged(current, original) {
|
|
10967
11088
|
// Quick reference check first
|
|
@@ -11025,9 +11146,9 @@ class Database extends events.EventEmitter {
|
|
|
11025
11146
|
return false;
|
|
11026
11147
|
}
|
|
11027
11148
|
|
|
11028
|
-
/**
|
|
11029
|
-
* Extract field names from query criteria
|
|
11030
|
-
* @private
|
|
11149
|
+
/**
|
|
11150
|
+
* Extract field names from query criteria
|
|
11151
|
+
* @private
|
|
11031
11152
|
*/
|
|
11032
11153
|
_extractQueryFields(criteria) {
|
|
11033
11154
|
const fields = new Set();
|
|
@@ -11054,8 +11175,8 @@ class Database extends events.EventEmitter {
|
|
|
11054
11175
|
return Array.from(fields);
|
|
11055
11176
|
}
|
|
11056
11177
|
|
|
11057
|
-
/**
|
|
11058
|
-
* Update records matching criteria
|
|
11178
|
+
/**
|
|
11179
|
+
* Update records matching criteria
|
|
11059
11180
|
*/
|
|
11060
11181
|
async update(criteria, updateData) {
|
|
11061
11182
|
this._validateInitialization('update');
|
|
@@ -11116,6 +11237,7 @@ class Database extends events.EventEmitter {
|
|
|
11116
11237
|
|
|
11117
11238
|
// CRITICAL FIX: Remove old terms from index before adding new ones
|
|
11118
11239
|
if (this.indexManager) {
|
|
11240
|
+
await this._ensureLazyIndexLoaded();
|
|
11119
11241
|
await this.indexManager.remove(record);
|
|
11120
11242
|
if (this.opts.debugMode) {
|
|
11121
11243
|
console.log(`🔄 UPDATE: Removed old terms from index for record ${record.id}`);
|
|
@@ -11151,6 +11273,7 @@ class Database extends events.EventEmitter {
|
|
|
11151
11273
|
}
|
|
11152
11274
|
}
|
|
11153
11275
|
const indexUpdateStart = Date.now();
|
|
11276
|
+
await this._ensureLazyIndexLoaded();
|
|
11154
11277
|
await this.indexManager.update(record, updated, lineNumber);
|
|
11155
11278
|
if (this.opts.debugMode) {
|
|
11156
11279
|
console.log(`🔄 UPDATE: Index update completed in ${Date.now() - indexUpdateStart}ms`);
|
|
@@ -11173,8 +11296,8 @@ class Database extends events.EventEmitter {
|
|
|
11173
11296
|
});
|
|
11174
11297
|
}
|
|
11175
11298
|
|
|
11176
|
-
/**
|
|
11177
|
-
* Delete records matching criteria
|
|
11299
|
+
/**
|
|
11300
|
+
* Delete records matching criteria
|
|
11178
11301
|
*/
|
|
11179
11302
|
async delete(criteria) {
|
|
11180
11303
|
this._validateInitialization('delete');
|
|
@@ -11217,6 +11340,7 @@ class Database extends events.EventEmitter {
|
|
|
11217
11340
|
for (const record of records) {
|
|
11218
11341
|
// Remove term mapping
|
|
11219
11342
|
this.removeTermMapping(record);
|
|
11343
|
+
await this._ensureLazyIndexLoaded();
|
|
11220
11344
|
await this.indexManager.remove(record);
|
|
11221
11345
|
|
|
11222
11346
|
// Remove record from writeBuffer or mark as deleted
|
|
@@ -11248,16 +11372,16 @@ class Database extends events.EventEmitter {
|
|
|
11248
11372
|
});
|
|
11249
11373
|
}
|
|
11250
11374
|
|
|
11251
|
-
/**
|
|
11252
|
-
* Generate a unique ID
|
|
11375
|
+
/**
|
|
11376
|
+
* Generate a unique ID
|
|
11253
11377
|
*/
|
|
11254
11378
|
generateId() {
|
|
11255
11379
|
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
|
11256
11380
|
}
|
|
11257
11381
|
|
|
11258
|
-
/**
|
|
11259
|
-
* Apply schema enforcement to a record
|
|
11260
|
-
* Converts object to array and back to enforce schema (remove extra fields, add undefined for missing fields)
|
|
11382
|
+
/**
|
|
11383
|
+
* Apply schema enforcement to a record
|
|
11384
|
+
* Converts object to array and back to enforce schema (remove extra fields, add undefined for missing fields)
|
|
11261
11385
|
*/
|
|
11262
11386
|
applySchemaEnforcement(record) {
|
|
11263
11387
|
// Only apply schema enforcement if fields configuration is explicitly provided
|
|
@@ -11281,8 +11405,8 @@ class Database extends events.EventEmitter {
|
|
|
11281
11405
|
return enforcedRecord;
|
|
11282
11406
|
}
|
|
11283
11407
|
|
|
11284
|
-
/**
|
|
11285
|
-
* Initialize schema for array-based serialization
|
|
11408
|
+
/**
|
|
11409
|
+
* Initialize schema for array-based serialization
|
|
11286
11410
|
*/
|
|
11287
11411
|
initializeSchema() {
|
|
11288
11412
|
if (!this.serializer || !this.serializer.schemaManager) {
|
|
@@ -11314,8 +11438,8 @@ class Database extends events.EventEmitter {
|
|
|
11314
11438
|
}
|
|
11315
11439
|
}
|
|
11316
11440
|
|
|
11317
|
-
/**
|
|
11318
|
-
* Get database length (number of records)
|
|
11441
|
+
/**
|
|
11442
|
+
* Get database length (number of records)
|
|
11319
11443
|
*/
|
|
11320
11444
|
get length() {
|
|
11321
11445
|
// Return total records: writeBuffer + saved records
|
|
@@ -11378,15 +11502,15 @@ class Database extends events.EventEmitter {
|
|
|
11378
11502
|
return writeBufferRecords + savedRecords;
|
|
11379
11503
|
}
|
|
11380
11504
|
|
|
11381
|
-
/**
|
|
11382
|
-
* Calculate current writeBuffer size in bytes (similar to published v1.1.0)
|
|
11505
|
+
/**
|
|
11506
|
+
* Calculate current writeBuffer size in bytes (similar to published v1.1.0)
|
|
11383
11507
|
*/
|
|
11384
11508
|
currentWriteBufferSize() {
|
|
11385
11509
|
return this.writeBufferTotalSize || 0;
|
|
11386
11510
|
}
|
|
11387
11511
|
|
|
11388
|
-
/**
|
|
11389
|
-
* Get database statistics
|
|
11512
|
+
/**
|
|
11513
|
+
* Get database statistics
|
|
11390
11514
|
*/
|
|
11391
11515
|
getStats() {
|
|
11392
11516
|
const stats = {
|
|
@@ -11406,16 +11530,16 @@ class Database extends events.EventEmitter {
|
|
|
11406
11530
|
return stats;
|
|
11407
11531
|
}
|
|
11408
11532
|
|
|
11409
|
-
/**
|
|
11410
|
-
* Initialize database (alias for initialize for backward compatibility)
|
|
11533
|
+
/**
|
|
11534
|
+
* Initialize database (alias for initialize for backward compatibility)
|
|
11411
11535
|
*/
|
|
11412
11536
|
async init() {
|
|
11413
11537
|
return this.initialize();
|
|
11414
11538
|
}
|
|
11415
11539
|
|
|
11416
|
-
/**
|
|
11417
|
-
* Schedule index rebuild when index data is missing or corrupted
|
|
11418
|
-
* @private
|
|
11540
|
+
/**
|
|
11541
|
+
* Schedule index rebuild when index data is missing or corrupted
|
|
11542
|
+
* @private
|
|
11419
11543
|
*/
|
|
11420
11544
|
_scheduleIndexRebuild() {
|
|
11421
11545
|
// Mark that rebuild is needed
|
|
@@ -11425,9 +11549,9 @@ class Database extends events.EventEmitter {
|
|
|
11425
11549
|
// This avoids blocking init() but ensures index is available when needed
|
|
11426
11550
|
}
|
|
11427
11551
|
|
|
11428
|
-
/**
|
|
11429
|
-
* Rebuild indexes from data file if needed
|
|
11430
|
-
* @private
|
|
11552
|
+
/**
|
|
11553
|
+
* Rebuild indexes from data file if needed
|
|
11554
|
+
* @private
|
|
11431
11555
|
*/
|
|
11432
11556
|
async _rebuildIndexesIfNeeded() {
|
|
11433
11557
|
if (this.opts.debugMode) {
|
|
@@ -11489,9 +11613,9 @@ class Database extends events.EventEmitter {
|
|
|
11489
11613
|
}
|
|
11490
11614
|
}
|
|
11491
11615
|
|
|
11492
|
-
/**
|
|
11493
|
-
* Rebuild indexes with retry logic to handle I/O hangs
|
|
11494
|
-
* @private
|
|
11616
|
+
/**
|
|
11617
|
+
* Rebuild indexes with retry logic to handle I/O hangs
|
|
11618
|
+
* @private
|
|
11495
11619
|
*/
|
|
11496
11620
|
async _rebuildIndexesWithRetry() {
|
|
11497
11621
|
// If no timeout configured, use original implementation without retry
|
|
@@ -11671,10 +11795,10 @@ class Database extends events.EventEmitter {
|
|
|
11671
11795
|
return count;
|
|
11672
11796
|
}
|
|
11673
11797
|
|
|
11674
|
-
/**
|
|
11675
|
-
* Destroy database - DESTRUCTIVE MODE
|
|
11676
|
-
* Assumes save() has already been called by user
|
|
11677
|
-
* If anything is still active, it indicates a bug - log error and force cleanup
|
|
11798
|
+
/**
|
|
11799
|
+
* Destroy database - DESTRUCTIVE MODE
|
|
11800
|
+
* Assumes save() has already been called by user
|
|
11801
|
+
* If anything is still active, it indicates a bug - log error and force cleanup
|
|
11678
11802
|
*/
|
|
11679
11803
|
async destroy() {
|
|
11680
11804
|
if (this.destroyed) return;
|
|
@@ -11732,8 +11856,8 @@ class Database extends events.EventEmitter {
|
|
|
11732
11856
|
}
|
|
11733
11857
|
}
|
|
11734
11858
|
|
|
11735
|
-
/**
|
|
11736
|
-
* Internal destroy implementation
|
|
11859
|
+
/**
|
|
11860
|
+
* Internal destroy implementation
|
|
11737
11861
|
*/
|
|
11738
11862
|
async _performDestroy() {
|
|
11739
11863
|
try {
|
|
@@ -11833,8 +11957,8 @@ class Database extends events.EventEmitter {
|
|
|
11833
11957
|
}
|
|
11834
11958
|
}
|
|
11835
11959
|
|
|
11836
|
-
/**
|
|
11837
|
-
* Find one record
|
|
11960
|
+
/**
|
|
11961
|
+
* Find one record
|
|
11838
11962
|
*/
|
|
11839
11963
|
async findOne(criteria, options = {}) {
|
|
11840
11964
|
this._validateInitialization('findOne');
|
|
@@ -11845,8 +11969,8 @@ class Database extends events.EventEmitter {
|
|
|
11845
11969
|
return results.length > 0 ? results[0] : null;
|
|
11846
11970
|
}
|
|
11847
11971
|
|
|
11848
|
-
/**
|
|
11849
|
-
* Count records
|
|
11972
|
+
/**
|
|
11973
|
+
* Count records
|
|
11850
11974
|
*/
|
|
11851
11975
|
async count(criteria = {}, options = {}) {
|
|
11852
11976
|
this._validateInitialization('count');
|
|
@@ -11860,23 +11984,23 @@ class Database extends events.EventEmitter {
|
|
|
11860
11984
|
return fileCount + writeBufferCount;
|
|
11861
11985
|
}
|
|
11862
11986
|
|
|
11863
|
-
/**
|
|
11864
|
-
* Check if any records exist matching the given criteria (ultra-fast when using indexed fields)
|
|
11865
|
-
*
|
|
11866
|
-
* @param {string|object} fieldName - Indexed field name (legacy) OR query criteria object (new)
|
|
11867
|
-
* @param {string|Array<string>} terms - Single term or array of terms (when using legacy syntax)
|
|
11868
|
-
* @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
|
|
11869
|
-
* @returns {Promise<boolean>} - True if at least one match exists
|
|
11870
|
-
*
|
|
11871
|
-
* @example
|
|
11872
|
-
* // Legacy syntax - ultra-fast index-only check
|
|
11873
|
-
* const exists = await db.exists('nameTerms', 'tv');
|
|
11874
|
-
* const existsAll = await db.exists('nameTerms', ['tv', 'globo'], { $all: true });
|
|
11875
|
-
*
|
|
11876
|
-
* @example
|
|
11877
|
-
* // New syntax - full query criteria support
|
|
11878
|
-
* const exists = await db.exists({ mediaType: 'live', status: 'active' });
|
|
11879
|
-
* const existsOr = await db.exists({ mediaType: ['live', 'vod'] });
|
|
11987
|
+
/**
|
|
11988
|
+
* Check if any records exist matching the given criteria (ultra-fast when using indexed fields)
|
|
11989
|
+
*
|
|
11990
|
+
* @param {string|object} fieldName - Indexed field name (legacy) OR query criteria object (new)
|
|
11991
|
+
* @param {string|Array<string>} terms - Single term or array of terms (when using legacy syntax)
|
|
11992
|
+
* @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
|
|
11993
|
+
* @returns {Promise<boolean>} - True if at least one match exists
|
|
11994
|
+
*
|
|
11995
|
+
* @example
|
|
11996
|
+
* // Legacy syntax - ultra-fast index-only check
|
|
11997
|
+
* const exists = await db.exists('nameTerms', 'tv');
|
|
11998
|
+
* const existsAll = await db.exists('nameTerms', ['tv', 'globo'], { $all: true });
|
|
11999
|
+
*
|
|
12000
|
+
* @example
|
|
12001
|
+
* // New syntax - full query criteria support
|
|
12002
|
+
* const exists = await db.exists({ mediaType: 'live', status: 'active' });
|
|
12003
|
+
* const existsOr = await db.exists({ mediaType: ['live', 'vod'] });
|
|
11880
12004
|
*/
|
|
11881
12005
|
async exists(fieldNameOrCriteria, terms, options = {}) {
|
|
11882
12006
|
this._validateInitialization('exists');
|
|
@@ -11890,6 +12014,7 @@ class Database extends events.EventEmitter {
|
|
|
11890
12014
|
// Legacy syntax: exists(fieldName, terms, options)
|
|
11891
12015
|
// Also handle invalid inputs (null, array) for backward compatibility
|
|
11892
12016
|
const fieldName = fieldNameOrCriteria;
|
|
12017
|
+
await this._ensureLazyIndexLoaded();
|
|
11893
12018
|
return this.indexManager.exists(fieldName, terms, options);
|
|
11894
12019
|
} else {
|
|
11895
12020
|
// Invalid input type
|
|
@@ -11897,13 +12022,13 @@ class Database extends events.EventEmitter {
|
|
|
11897
12022
|
}
|
|
11898
12023
|
}
|
|
11899
12024
|
|
|
11900
|
-
/**
|
|
11901
|
-
* Run batched index-only existence checks using a single connection per field.
|
|
11902
|
-
* Each entry must specify { field, terms, options?, id? } so the result map stays stable.
|
|
11903
|
-
*
|
|
11904
|
-
* @param {Array<Object>} criteriaArray
|
|
11905
|
-
* @param {Object} opts
|
|
11906
|
-
* @returns {Promise<Object<string, boolean>>}
|
|
12025
|
+
/**
|
|
12026
|
+
* Run batched index-only existence checks using a single connection per field.
|
|
12027
|
+
* Each entry must specify { field, terms, options?, id? } so the result map stays stable.
|
|
12028
|
+
*
|
|
12029
|
+
* @param {Array<Object>} criteriaArray
|
|
12030
|
+
* @param {Object} opts
|
|
12031
|
+
* @returns {Promise<Object<string, boolean>>}
|
|
11907
12032
|
*/
|
|
11908
12033
|
async multiExists(criteriaArray, opts = {}) {
|
|
11909
12034
|
this._validateInitialization('multiExists');
|
|
@@ -11913,6 +12038,7 @@ class Database extends events.EventEmitter {
|
|
|
11913
12038
|
if (!this.indexManager) {
|
|
11914
12039
|
return {};
|
|
11915
12040
|
}
|
|
12041
|
+
await this._ensureLazyIndexLoaded();
|
|
11916
12042
|
const results = {};
|
|
11917
12043
|
const perField = new Map();
|
|
11918
12044
|
for (let i = 0; i < criteriaArray.length; i++) {
|
|
@@ -11942,12 +12068,12 @@ class Database extends events.EventEmitter {
|
|
|
11942
12068
|
return results;
|
|
11943
12069
|
}
|
|
11944
12070
|
|
|
11945
|
-
/**
|
|
11946
|
-
* Check if any records exist using full query criteria
|
|
11947
|
-
* Uses index intersection when possible for maximum performance
|
|
11948
|
-
* @private
|
|
11949
|
-
* @param {object} criteria - Query criteria object
|
|
11950
|
-
* @returns {Promise<boolean>} - True if at least one match exists
|
|
12071
|
+
/**
|
|
12072
|
+
* Check if any records exist using full query criteria
|
|
12073
|
+
* Uses index intersection when possible for maximum performance
|
|
12074
|
+
* @private
|
|
12075
|
+
* @param {object} criteria - Query criteria object
|
|
12076
|
+
* @returns {Promise<boolean>} - True if at least one match exists
|
|
11951
12077
|
*/
|
|
11952
12078
|
|
|
11953
12079
|
async _existsWithCriteria(criteria) {
|
|
@@ -11974,7 +12100,7 @@ class Database extends events.EventEmitter {
|
|
|
11974
12100
|
if (this.opts.debugMode) {
|
|
11975
12101
|
console.log(`⚡ exists() using INDEX-ONLY optimization for: ${JSON.stringify(criteria)}`);
|
|
11976
12102
|
}
|
|
11977
|
-
return this._existsIndexOnly(criteria);
|
|
12103
|
+
return await this._existsIndexOnly(criteria);
|
|
11978
12104
|
}
|
|
11979
12105
|
|
|
11980
12106
|
// 🎯 FALLBACK: Use the same find() logic for complex criteria or non-indexed fields
|
|
@@ -11990,11 +12116,11 @@ class Database extends events.EventEmitter {
|
|
|
11990
12116
|
}
|
|
11991
12117
|
}
|
|
11992
12118
|
|
|
11993
|
-
/**
|
|
11994
|
-
* Check if criteria can use index-only existence check
|
|
11995
|
-
* @private
|
|
11996
|
-
* @param {object} criteria - Query criteria
|
|
11997
|
-
* @returns {boolean} - True if can use index-only
|
|
12119
|
+
/**
|
|
12120
|
+
* Check if criteria can use index-only existence check
|
|
12121
|
+
* @private
|
|
12122
|
+
* @param {object} criteria - Query criteria
|
|
12123
|
+
* @returns {boolean} - True if can use index-only
|
|
11998
12124
|
*/
|
|
11999
12125
|
_canUseIndexOnlyForExists(criteria) {
|
|
12000
12126
|
// Must have indexes configured
|
|
@@ -12025,13 +12151,14 @@ class Database extends events.EventEmitter {
|
|
|
12025
12151
|
return true;
|
|
12026
12152
|
}
|
|
12027
12153
|
|
|
12028
|
-
/**
|
|
12029
|
-
* Perform index-only existence check (ultra-fast, no disk I/O)
|
|
12030
|
-
* @private
|
|
12031
|
-
* @param {object} criteria - Simple criteria with only indexed fields
|
|
12032
|
-
* @returns {boolean} - True if any records match the criteria
|
|
12154
|
+
/**
|
|
12155
|
+
* Perform index-only existence check (ultra-fast, no disk I/O)
|
|
12156
|
+
* @private
|
|
12157
|
+
* @param {object} criteria - Simple criteria with only indexed fields
|
|
12158
|
+
* @returns {boolean} - True if any records match the criteria
|
|
12033
12159
|
*/
|
|
12034
|
-
_existsIndexOnly(criteria) {
|
|
12160
|
+
async _existsIndexOnly(criteria) {
|
|
12161
|
+
await this._ensureLazyIndexLoaded();
|
|
12035
12162
|
const criteriaEntries = Object.entries(criteria);
|
|
12036
12163
|
|
|
12037
12164
|
// For single field criteria, use direct indexManager.exists()
|
|
@@ -12066,12 +12193,12 @@ class Database extends events.EventEmitter {
|
|
|
12066
12193
|
return intersection.size > 0;
|
|
12067
12194
|
}
|
|
12068
12195
|
|
|
12069
|
-
/**
|
|
12070
|
-
* Get line numbers for a field/value combination using index-only lookup
|
|
12071
|
-
* @private
|
|
12072
|
-
* @param {string} field - Field name
|
|
12073
|
-
* @param {*} value - Field value (string, number, or array)
|
|
12074
|
-
* @returns {Array<number>} Array of line numbers
|
|
12196
|
+
/**
|
|
12197
|
+
* Get line numbers for a field/value combination using index-only lookup
|
|
12198
|
+
* @private
|
|
12199
|
+
* @param {string} field - Field name
|
|
12200
|
+
* @param {*} value - Field value (string, number, or array)
|
|
12201
|
+
* @returns {Array<number>} Array of line numbers
|
|
12075
12202
|
*/
|
|
12076
12203
|
_getIndexLinesForFieldValue(field, value) {
|
|
12077
12204
|
// For arrays (OR logic), check each value
|
|
@@ -12088,12 +12215,12 @@ class Database extends events.EventEmitter {
|
|
|
12088
12215
|
return this._getSingleFieldLines(field, value);
|
|
12089
12216
|
}
|
|
12090
12217
|
|
|
12091
|
-
/**
|
|
12092
|
-
* Get line numbers for a single field/value using index lookup
|
|
12093
|
-
* @private
|
|
12094
|
-
* @param {string} field - Field name
|
|
12095
|
-
* @param {*} value - Single field value
|
|
12096
|
-
* @returns {Array<number>} Array of line numbers
|
|
12218
|
+
/**
|
|
12219
|
+
* Get line numbers for a single field/value using index lookup
|
|
12220
|
+
* @private
|
|
12221
|
+
* @param {string} field - Field name
|
|
12222
|
+
* @param {*} value - Single field value
|
|
12223
|
+
* @returns {Array<number>} Array of line numbers
|
|
12097
12224
|
*/
|
|
12098
12225
|
_getSingleFieldLines(field, value) {
|
|
12099
12226
|
// Use indexManager.exists() logic but return line numbers instead of boolean
|
|
@@ -12136,13 +12263,13 @@ class Database extends events.EventEmitter {
|
|
|
12136
12263
|
return Array.from(lines);
|
|
12137
12264
|
}
|
|
12138
12265
|
|
|
12139
|
-
/**
|
|
12140
|
-
* Calculate coverage for grouped include/exclude term sets
|
|
12141
|
-
* @param {string} fieldName - Name of the indexed field
|
|
12142
|
-
* @param {Array<object>} groups - Array of { terms, excludes } objects
|
|
12143
|
-
* @param {object} filterCriteria - Optional filter criteria
|
|
12144
|
-
* @param {object} options - Optional settings
|
|
12145
|
-
* @returns {Promise<number>} Coverage percentage between 0 and 100
|
|
12266
|
+
/**
|
|
12267
|
+
* Calculate coverage for grouped include/exclude term sets
|
|
12268
|
+
* @param {string} fieldName - Name of the indexed field
|
|
12269
|
+
* @param {Array<object>} groups - Array of { terms, excludes } objects
|
|
12270
|
+
* @param {object} filterCriteria - Optional filter criteria
|
|
12271
|
+
* @param {object} options - Optional settings
|
|
12272
|
+
* @returns {Promise<number>} Coverage percentage between 0 and 100
|
|
12146
12273
|
*/
|
|
12147
12274
|
async coverage(fieldName, groups, filterCriteria = null, options = {}) {
|
|
12148
12275
|
this._validateInitialization('coverage');
|
|
@@ -12308,12 +12435,12 @@ class Database extends events.EventEmitter {
|
|
|
12308
12435
|
return Number(coverageValue.toFixed(precision));
|
|
12309
12436
|
}
|
|
12310
12437
|
|
|
12311
|
-
/**
|
|
12312
|
-
* Score records based on weighted terms in an indexed array:string field
|
|
12313
|
-
* @param {string} fieldName - Name of indexed array:string field
|
|
12314
|
-
* @param {object} scores - Map of terms to numeric weights
|
|
12315
|
-
* @param {object} options - Query options
|
|
12316
|
-
* @returns {Promise<Array>} Records with scores, sorted by score
|
|
12438
|
+
/**
|
|
12439
|
+
* Score records based on weighted terms in an indexed array:string field
|
|
12440
|
+
* @param {string} fieldName - Name of indexed array:string field
|
|
12441
|
+
* @param {object} scores - Map of terms to numeric weights
|
|
12442
|
+
* @param {object} options - Query options
|
|
12443
|
+
* @returns {Promise<Array>} Records with scores, sorted by score
|
|
12317
12444
|
*/
|
|
12318
12445
|
async score(fieldName, scores, options = {}) {
|
|
12319
12446
|
// Validate initialization
|
|
@@ -12367,12 +12494,23 @@ class Database extends events.EventEmitter {
|
|
|
12367
12494
|
// Check if this is a term-mapped field
|
|
12368
12495
|
const isTermMapped = this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.includes(fieldName);
|
|
12369
12496
|
|
|
12497
|
+
// CRITICAL FIX: Ensure index is loaded before accessing data
|
|
12498
|
+
// After idle unload, index.data[fieldName] becomes {} (truthy but empty)
|
|
12499
|
+
// Use _ensureLazyIndexLoaded() to reload from disk instead of _rebuildIndexesIfNeeded()
|
|
12500
|
+
// which requires allowIndexRebuild=true and rebuilds from scratch
|
|
12501
|
+
if (!this.indexManager.indexLoaded) {
|
|
12502
|
+
await this._ensureLazyIndexLoaded();
|
|
12503
|
+
}
|
|
12504
|
+
|
|
12370
12505
|
// Access the index for this field
|
|
12371
12506
|
const fieldIndex = this.indexManager.index.data[fieldName];
|
|
12372
|
-
if (!fieldIndex) {
|
|
12507
|
+
if (!fieldIndex || Object.keys(fieldIndex).length === 0) {
|
|
12373
12508
|
return [];
|
|
12374
12509
|
}
|
|
12375
12510
|
|
|
12511
|
+
// Reset idle unload timer since we're using the index
|
|
12512
|
+
this.indexManager.markIndexUsed();
|
|
12513
|
+
|
|
12376
12514
|
// Accumulate scores for each line number
|
|
12377
12515
|
const scoreMap = new Map();
|
|
12378
12516
|
const countMap = opts.mode === 'avg' ? new Map() : null;
|
|
@@ -12538,9 +12676,9 @@ class Database extends events.EventEmitter {
|
|
|
12538
12676
|
return results;
|
|
12539
12677
|
}
|
|
12540
12678
|
|
|
12541
|
-
/**
|
|
12542
|
-
* Read a grouped range with retry logic to handle I/O hangs
|
|
12543
|
-
* @private
|
|
12679
|
+
/**
|
|
12680
|
+
* Read a grouped range with retry logic to handle I/O hangs
|
|
12681
|
+
* @private
|
|
12544
12682
|
*/
|
|
12545
12683
|
async _readGroupedRangeWithRetry(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts) {
|
|
12546
12684
|
// If no timeout configured, use original implementation without retry
|
|
@@ -12635,9 +12773,9 @@ class Database extends events.EventEmitter {
|
|
|
12635
12773
|
return results;
|
|
12636
12774
|
}
|
|
12637
12775
|
|
|
12638
|
-
/**
|
|
12639
|
-
* Original read grouped range logic without retry (for backward compatibility)
|
|
12640
|
-
* @private
|
|
12776
|
+
/**
|
|
12777
|
+
* Original read grouped range logic without retry (for backward compatibility)
|
|
12778
|
+
* @private
|
|
12641
12779
|
*/
|
|
12642
12780
|
async _readGroupedRangeOriginal(groupedRange, fd, startToLineNumber, scoresByLineNumber, opts) {
|
|
12643
12781
|
const results = [];
|
|
@@ -12695,9 +12833,9 @@ class Database extends events.EventEmitter {
|
|
|
12695
12833
|
return results;
|
|
12696
12834
|
}
|
|
12697
12835
|
|
|
12698
|
-
/**
|
|
12699
|
-
* Original rebuild indexes logic without retry (for backward compatibility)
|
|
12700
|
-
* @private
|
|
12836
|
+
/**
|
|
12837
|
+
* Original rebuild indexes logic without retry (for backward compatibility)
|
|
12838
|
+
* @private
|
|
12701
12839
|
*/
|
|
12702
12840
|
async _rebuildIndexesOriginal() {
|
|
12703
12841
|
let count = 0;
|
|
@@ -12832,8 +12970,8 @@ class Database extends events.EventEmitter {
|
|
|
12832
12970
|
return count;
|
|
12833
12971
|
}
|
|
12834
12972
|
|
|
12835
|
-
/**
|
|
12836
|
-
* Wait for all pending operations to complete
|
|
12973
|
+
/**
|
|
12974
|
+
* Wait for all pending operations to complete
|
|
12837
12975
|
*/
|
|
12838
12976
|
async _waitForPendingOperations() {
|
|
12839
12977
|
if (this.operationQueue && this.operationQueue.getQueueLength() > 0) {
|
|
@@ -12851,8 +12989,8 @@ class Database extends events.EventEmitter {
|
|
|
12851
12989
|
}
|
|
12852
12990
|
}
|
|
12853
12991
|
|
|
12854
|
-
/**
|
|
12855
|
-
* Flush write buffer completely with smart detection of ongoing insertions
|
|
12992
|
+
/**
|
|
12993
|
+
* Flush write buffer completely with smart detection of ongoing insertions
|
|
12856
12994
|
*/
|
|
12857
12995
|
async _flushWriteBufferCompletely() {
|
|
12858
12996
|
// Force complete flush of write buffer with intelligent detection
|
|
@@ -12912,9 +13050,9 @@ class Database extends events.EventEmitter {
|
|
|
12912
13050
|
}
|
|
12913
13051
|
}
|
|
12914
13052
|
|
|
12915
|
-
/**
|
|
12916
|
-
* Get a lightweight sample of the write buffer for comparison
|
|
12917
|
-
* @returns {Object} - Sample data for comparison
|
|
13053
|
+
/**
|
|
13054
|
+
* Get a lightweight sample of the write buffer for comparison
|
|
13055
|
+
* @returns {Object} - Sample data for comparison
|
|
12918
13056
|
*/
|
|
12919
13057
|
_getBufferSample() {
|
|
12920
13058
|
if (!this.writeBuffer || this.writeBuffer.length === 0) {
|
|
@@ -12953,11 +13091,11 @@ class Database extends events.EventEmitter {
|
|
|
12953
13091
|
return sample;
|
|
12954
13092
|
}
|
|
12955
13093
|
|
|
12956
|
-
/**
|
|
12957
|
-
* Check if two buffer samples are identical (indicating stuck flush)
|
|
12958
|
-
* @param {Object} sample1 - First sample
|
|
12959
|
-
* @param {Object} sample2 - Second sample
|
|
12960
|
-
* @returns {boolean} - True if samples are identical
|
|
13094
|
+
/**
|
|
13095
|
+
* Check if two buffer samples are identical (indicating stuck flush)
|
|
13096
|
+
* @param {Object} sample1 - First sample
|
|
13097
|
+
* @param {Object} sample2 - Second sample
|
|
13098
|
+
* @returns {boolean} - True if samples are identical
|
|
12961
13099
|
*/
|
|
12962
13100
|
_isBufferSampleIdentical(sample1, sample2) {
|
|
12963
13101
|
if (!sample1 || !sample2) {
|
|
@@ -12991,8 +13129,8 @@ class Database extends events.EventEmitter {
|
|
|
12991
13129
|
return true;
|
|
12992
13130
|
}
|
|
12993
13131
|
|
|
12994
|
-
/**
|
|
12995
|
-
* Process write buffer items
|
|
13132
|
+
/**
|
|
13133
|
+
* Process write buffer items
|
|
12996
13134
|
*/
|
|
12997
13135
|
async _processWriteBuffer() {
|
|
12998
13136
|
// Process write buffer items without loading entire file
|
|
@@ -13070,8 +13208,8 @@ class Database extends events.EventEmitter {
|
|
|
13070
13208
|
}
|
|
13071
13209
|
}
|
|
13072
13210
|
|
|
13073
|
-
/**
|
|
13074
|
-
* Process individual buffer item
|
|
13211
|
+
/**
|
|
13212
|
+
* Process individual buffer item
|
|
13075
13213
|
*/
|
|
13076
13214
|
async _processBufferItem(buffer) {
|
|
13077
13215
|
// Process buffer item without loading entire file
|
|
@@ -13082,8 +13220,8 @@ class Database extends events.EventEmitter {
|
|
|
13082
13220
|
}
|
|
13083
13221
|
}
|
|
13084
13222
|
|
|
13085
|
-
/**
|
|
13086
|
-
* Process individual object item
|
|
13223
|
+
/**
|
|
13224
|
+
* Process individual object item
|
|
13087
13225
|
*/
|
|
13088
13226
|
async _processObjectItem(obj) {
|
|
13089
13227
|
// Process object item without loading entire file
|
|
@@ -13096,8 +13234,8 @@ class Database extends events.EventEmitter {
|
|
|
13096
13234
|
}
|
|
13097
13235
|
}
|
|
13098
13236
|
|
|
13099
|
-
/**
|
|
13100
|
-
* Process multiple object items in a single batch write operation
|
|
13237
|
+
/**
|
|
13238
|
+
* Process multiple object items in a single batch write operation
|
|
13101
13239
|
*/
|
|
13102
13240
|
async _processObjectItemsBatch(objects) {
|
|
13103
13241
|
if (!this.fileHandler || objects.length === 0) return;
|
|
@@ -13115,8 +13253,8 @@ class Database extends events.EventEmitter {
|
|
|
13115
13253
|
await this.fileHandler.writeDataAsync(buffer);
|
|
13116
13254
|
}
|
|
13117
13255
|
|
|
13118
|
-
/**
|
|
13119
|
-
* Wait for all I/O operations to complete
|
|
13256
|
+
/**
|
|
13257
|
+
* Wait for all I/O operations to complete
|
|
13120
13258
|
*/
|
|
13121
13259
|
async _waitForIOCompletion() {
|
|
13122
13260
|
// Wait for all file operations to complete
|
|
@@ -13128,9 +13266,9 @@ class Database extends events.EventEmitter {
|
|
|
13128
13266
|
}
|
|
13129
13267
|
}
|
|
13130
13268
|
|
|
13131
|
-
/**
|
|
13132
|
-
* CRITICAL FIX: Safe fallback method to load existing records when _streamExistingRecords fails
|
|
13133
|
-
* This prevents data loss by attempting alternative methods to preserve existing data
|
|
13269
|
+
/**
|
|
13270
|
+
* CRITICAL FIX: Safe fallback method to load existing records when _streamExistingRecords fails
|
|
13271
|
+
* This prevents data loss by attempting alternative methods to preserve existing data
|
|
13134
13272
|
*/
|
|
13135
13273
|
async _loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot) {
|
|
13136
13274
|
const existingRecords = [];
|
|
@@ -13176,9 +13314,9 @@ class Database extends events.EventEmitter {
|
|
|
13176
13314
|
}
|
|
13177
13315
|
}
|
|
13178
13316
|
|
|
13179
|
-
/**
|
|
13180
|
-
* Stream existing records without loading entire file into memory
|
|
13181
|
-
* Optimized with group ranging and reduced JSON parsing
|
|
13317
|
+
/**
|
|
13318
|
+
* Stream existing records without loading entire file into memory
|
|
13319
|
+
* Optimized with group ranging and reduced JSON parsing
|
|
13182
13320
|
*/
|
|
13183
13321
|
async _streamExistingRecords(deletedIdsSnapshot, writeBufferSnapshot) {
|
|
13184
13322
|
const existingRecords = [];
|
|
@@ -13650,8 +13788,8 @@ class Database extends events.EventEmitter {
|
|
|
13650
13788
|
return existingRecords;
|
|
13651
13789
|
}
|
|
13652
13790
|
|
|
13653
|
-
/**
|
|
13654
|
-
* Flush write buffer
|
|
13791
|
+
/**
|
|
13792
|
+
* Flush write buffer
|
|
13655
13793
|
*/
|
|
13656
13794
|
async flush() {
|
|
13657
13795
|
return this.operationQueue.enqueue(async () => {
|
|
@@ -13668,8 +13806,8 @@ class Database extends events.EventEmitter {
|
|
|
13668
13806
|
});
|
|
13669
13807
|
}
|
|
13670
13808
|
|
|
13671
|
-
/**
|
|
13672
|
-
* Flush insertion buffer (backward compatibility)
|
|
13809
|
+
/**
|
|
13810
|
+
* Flush insertion buffer (backward compatibility)
|
|
13673
13811
|
*/
|
|
13674
13812
|
async flushInsertionBuffer() {
|
|
13675
13813
|
// Flush insertion buffer implementation - save any pending data
|
|
@@ -13677,8 +13815,8 @@ class Database extends events.EventEmitter {
|
|
|
13677
13815
|
return this.flush();
|
|
13678
13816
|
}
|
|
13679
13817
|
|
|
13680
|
-
/**
|
|
13681
|
-
* Get memory usage
|
|
13818
|
+
/**
|
|
13819
|
+
* Get memory usage
|
|
13682
13820
|
*/
|
|
13683
13821
|
getMemoryUsage() {
|
|
13684
13822
|
return {
|
|
@@ -13690,7 +13828,7 @@ class Database extends events.EventEmitter {
|
|
|
13690
13828
|
};
|
|
13691
13829
|
}
|
|
13692
13830
|
_hasActualIndexData() {
|
|
13693
|
-
if (!this.indexManager) return false;
|
|
13831
|
+
if (!this.indexManager || this.indexManager.indexLoaded === false) return false;
|
|
13694
13832
|
const data = this.indexManager.index.data;
|
|
13695
13833
|
for (const field in data) {
|
|
13696
13834
|
const fieldData = data[field];
|
|
@@ -13703,11 +13841,53 @@ class Database extends events.EventEmitter {
|
|
|
13703
13841
|
}
|
|
13704
13842
|
return false;
|
|
13705
13843
|
}
|
|
13844
|
+
async _loadIndexDataFromFile() {
|
|
13845
|
+
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
13846
|
+
try {
|
|
13847
|
+
await fs.promises.access(idxPath);
|
|
13848
|
+
} catch (error) {
|
|
13849
|
+
return null;
|
|
13850
|
+
}
|
|
13851
|
+
try {
|
|
13852
|
+
const raw = await fs.promises.readFile(idxPath, 'utf8');
|
|
13853
|
+
const parsed = JSON.parse(raw);
|
|
13854
|
+
return parsed?.index ?? null;
|
|
13855
|
+
} catch (error) {
|
|
13856
|
+
if (this.opts.debugMode) {
|
|
13857
|
+
console.error(`⚠️ _loadIndexDataFromFile: Failed to read or parse ${idxPath}`, error);
|
|
13858
|
+
}
|
|
13859
|
+
return null;
|
|
13860
|
+
}
|
|
13861
|
+
}
|
|
13862
|
+
async _ensureLazyIndexLoaded() {
|
|
13863
|
+
if (!this.indexManager || this.indexManager.indexLoaded) return;
|
|
13864
|
+
if (!this.indexManager.indexedFields || this.indexManager.indexedFields.length === 0) {
|
|
13865
|
+
this.indexManager.indexLoaded = true;
|
|
13866
|
+
return;
|
|
13867
|
+
}
|
|
13868
|
+
if (this._lazyIndexLoadPromise) {
|
|
13869
|
+
return this._lazyIndexLoadPromise;
|
|
13870
|
+
}
|
|
13871
|
+
this._lazyIndexLoadPromise = (async () => {
|
|
13872
|
+
const indexData = await this._loadIndexDataFromFile();
|
|
13873
|
+
if (indexData) {
|
|
13874
|
+
this.indexManager.load(indexData);
|
|
13875
|
+
} else {
|
|
13876
|
+
if (this.opts.debugMode) {
|
|
13877
|
+
console.log('🔍 _ensureLazyIndexLoaded: No index file found or invalid index data, continuing with empty index');
|
|
13878
|
+
}
|
|
13879
|
+
this.indexManager.indexLoaded = true;
|
|
13880
|
+
this.indexManager.markIndexUsed();
|
|
13881
|
+
}
|
|
13882
|
+
this._lazyIndexLoadPromise = null;
|
|
13883
|
+
})();
|
|
13884
|
+
return this._lazyIndexLoadPromise;
|
|
13885
|
+
}
|
|
13706
13886
|
|
|
13707
|
-
/**
|
|
13708
|
-
* Locate a record by line number and return its byte range
|
|
13709
|
-
* @param {number} n - Line number
|
|
13710
|
-
* @returns {Array} - [start, end] byte range or undefined
|
|
13887
|
+
/**
|
|
13888
|
+
* Locate a record by line number and return its byte range
|
|
13889
|
+
* @param {number} n - Line number
|
|
13890
|
+
* @returns {Array} - [start, end] byte range or undefined
|
|
13711
13891
|
*/
|
|
13712
13892
|
locate(n) {
|
|
13713
13893
|
if (this.offsets[n] === undefined) {
|
|
@@ -13726,10 +13906,10 @@ class Database extends events.EventEmitter {
|
|
|
13726
13906
|
return [this.offsets[n], end];
|
|
13727
13907
|
}
|
|
13728
13908
|
|
|
13729
|
-
/**
|
|
13730
|
-
* Get ranges for streaming based on line numbers
|
|
13731
|
-
* @param {Array|Set} map - Line numbers to get ranges for
|
|
13732
|
-
* @returns {Array} - Array of range objects {start, end, index}
|
|
13909
|
+
/**
|
|
13910
|
+
* Get ranges for streaming based on line numbers
|
|
13911
|
+
* @param {Array|Set} map - Line numbers to get ranges for
|
|
13912
|
+
* @returns {Array} - Array of range objects {start, end, index}
|
|
13733
13913
|
*/
|
|
13734
13914
|
getRanges(map) {
|
|
13735
13915
|
return (map || Array.from(this.offsets.keys())).map(n => {
|
|
@@ -13742,19 +13922,19 @@ class Database extends events.EventEmitter {
|
|
|
13742
13922
|
}).filter(n => n !== undefined);
|
|
13743
13923
|
}
|
|
13744
13924
|
|
|
13745
|
-
/**
|
|
13746
|
-
* Get the base line number for writeBuffer entries (number of persisted records)
|
|
13747
|
-
* @private
|
|
13925
|
+
/**
|
|
13926
|
+
* Get the base line number for writeBuffer entries (number of persisted records)
|
|
13927
|
+
* @private
|
|
13748
13928
|
*/
|
|
13749
13929
|
_getWriteBufferBaseLineNumber() {
|
|
13750
13930
|
return Array.isArray(this.offsets) ? this.offsets.length : 0;
|
|
13751
13931
|
}
|
|
13752
13932
|
|
|
13753
|
-
/**
|
|
13754
|
-
* Convert a writeBuffer index into an absolute line number
|
|
13755
|
-
* @param {number} writeBufferIndex - Index inside writeBuffer (0-based)
|
|
13756
|
-
* @returns {number} Absolute line number (0-based)
|
|
13757
|
-
* @private
|
|
13933
|
+
/**
|
|
13934
|
+
* Convert a writeBuffer index into an absolute line number
|
|
13935
|
+
* @param {number} writeBufferIndex - Index inside writeBuffer (0-based)
|
|
13936
|
+
* @returns {number} Absolute line number (0-based)
|
|
13937
|
+
* @private
|
|
13758
13938
|
*/
|
|
13759
13939
|
_getAbsoluteLineNumber(writeBufferIndex) {
|
|
13760
13940
|
if (typeof writeBufferIndex !== 'number' || writeBufferIndex < 0) {
|
|
@@ -13763,10 +13943,10 @@ class Database extends events.EventEmitter {
|
|
|
13763
13943
|
return this._getWriteBufferBaseLineNumber() + writeBufferIndex;
|
|
13764
13944
|
}
|
|
13765
13945
|
|
|
13766
|
-
/**
|
|
13767
|
-
* Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
|
|
13768
|
-
* @param {string} line - The corrupted line to recover
|
|
13769
|
-
* @returns {string|null} - The recovered line or null if recovery is not possible
|
|
13946
|
+
/**
|
|
13947
|
+
* Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
|
|
13948
|
+
* @param {string} line - The corrupted line to recover
|
|
13949
|
+
* @returns {string|null} - The recovered line or null if recovery is not possible
|
|
13770
13950
|
*/
|
|
13771
13951
|
_tryRecoverLine(line) {
|
|
13772
13952
|
if (!line || typeof line !== 'string') {
|
|
@@ -14016,8 +14196,8 @@ class Database extends events.EventEmitter {
|
|
|
14016
14196
|
}).apply(this, arguments);
|
|
14017
14197
|
}
|
|
14018
14198
|
|
|
14019
|
-
/**
|
|
14020
|
-
* Walk through records using streaming (real implementation)
|
|
14199
|
+
/**
|
|
14200
|
+
* Walk through records using streaming (real implementation)
|
|
14021
14201
|
*/
|
|
14022
14202
|
walk(_x3) {
|
|
14023
14203
|
var _this2 = this;
|
|
@@ -14474,18 +14654,18 @@ class Database extends events.EventEmitter {
|
|
|
14474
14654
|
}).apply(this, arguments);
|
|
14475
14655
|
}
|
|
14476
14656
|
|
|
14477
|
-
/**
|
|
14478
|
-
* Iterate through records with bulk update capabilities
|
|
14479
|
-
* Allows in-place modifications and deletions with optimized performance
|
|
14480
|
-
*
|
|
14481
|
-
* @param {Object} criteria - Query criteria
|
|
14482
|
-
* @param {Object} options - Iteration options
|
|
14483
|
-
* @param {number} options.chunkSize - Batch size for processing (default: 1000)
|
|
14484
|
-
* @param {string} options.strategy - Processing strategy: 'streaming' (always uses walk() method)
|
|
14485
|
-
* @param {boolean} options.autoSave - Auto-save after each chunk (default: false)
|
|
14486
|
-
* @param {Function} options.progressCallback - Progress callback function
|
|
14487
|
-
* @param {boolean} options.detectChanges - Auto-detect changes (default: true)
|
|
14488
|
-
* @returns {AsyncGenerator} Generator yielding records for modification
|
|
14657
|
+
/**
|
|
14658
|
+
* Iterate through records with bulk update capabilities
|
|
14659
|
+
* Allows in-place modifications and deletions with optimized performance
|
|
14660
|
+
*
|
|
14661
|
+
* @param {Object} criteria - Query criteria
|
|
14662
|
+
* @param {Object} options - Iteration options
|
|
14663
|
+
* @param {number} options.chunkSize - Batch size for processing (default: 1000)
|
|
14664
|
+
* @param {string} options.strategy - Processing strategy: 'streaming' (always uses walk() method)
|
|
14665
|
+
* @param {boolean} options.autoSave - Auto-save after each chunk (default: false)
|
|
14666
|
+
* @param {Function} options.progressCallback - Progress callback function
|
|
14667
|
+
* @param {boolean} options.detectChanges - Auto-detect changes (default: true)
|
|
14668
|
+
* @returns {AsyncGenerator} Generator yielding records for modification
|
|
14489
14669
|
*/
|
|
14490
14670
|
iterate(_x4) {
|
|
14491
14671
|
var _this3 = this;
|
|
@@ -14620,9 +14800,9 @@ class Database extends events.EventEmitter {
|
|
|
14620
14800
|
}).apply(this, arguments);
|
|
14621
14801
|
}
|
|
14622
14802
|
|
|
14623
|
-
/**
|
|
14624
|
-
* Process a batch of updates and deletes from iterate operation
|
|
14625
|
-
* @private
|
|
14803
|
+
/**
|
|
14804
|
+
* Process a batch of updates and deletes from iterate operation
|
|
14805
|
+
* @private
|
|
14626
14806
|
*/
|
|
14627
14807
|
async _processIterateBatch(updateBuffer, deleteBuffer, options) {
|
|
14628
14808
|
if (updateBuffer.length === 0 && deleteBuffer.size === 0) return;
|
|
@@ -14649,6 +14829,7 @@ class Database extends events.EventEmitter {
|
|
|
14649
14829
|
|
|
14650
14830
|
// Update index
|
|
14651
14831
|
const absoluteLineNumber = this._getAbsoluteLineNumber(targetIndex);
|
|
14832
|
+
await this._ensureLazyIndexLoaded();
|
|
14652
14833
|
await this.indexManager.update(record, record, absoluteLineNumber);
|
|
14653
14834
|
}
|
|
14654
14835
|
if (this.opts.debugMode) {
|
|
@@ -14668,6 +14849,7 @@ class Database extends events.EventEmitter {
|
|
|
14668
14849
|
this.removeTermMapping(record);
|
|
14669
14850
|
|
|
14670
14851
|
// Remove from index
|
|
14852
|
+
await this._ensureLazyIndexLoaded();
|
|
14671
14853
|
await this.indexManager.remove(record);
|
|
14672
14854
|
|
|
14673
14855
|
// Remove from writeBuffer or mark as deleted
|
|
@@ -14697,8 +14879,8 @@ class Database extends events.EventEmitter {
|
|
|
14697
14879
|
}
|
|
14698
14880
|
}
|
|
14699
14881
|
|
|
14700
|
-
/**
|
|
14701
|
-
* Close the database
|
|
14882
|
+
/**
|
|
14883
|
+
* Close the database
|
|
14702
14884
|
*/
|
|
14703
14885
|
async close() {
|
|
14704
14886
|
if (this.destroyed || this.closed) return;
|
|
@@ -14742,11 +14924,14 @@ class Database extends events.EventEmitter {
|
|
|
14742
14924
|
}
|
|
14743
14925
|
}
|
|
14744
14926
|
|
|
14745
|
-
// 2.
|
|
14927
|
+
// 2. Cancel any pending idle unload timers before shutdown
|
|
14928
|
+
this.indexManager?.cancelIdleUnloadTimer?.();
|
|
14929
|
+
|
|
14930
|
+
// 3. Mark as closed (but not destroyed) to allow reopening
|
|
14746
14931
|
this.closed = true;
|
|
14747
14932
|
this.initialized = false;
|
|
14748
14933
|
|
|
14749
|
-
//
|
|
14934
|
+
// 4. Clear any remaining state for clean reopening
|
|
14750
14935
|
this.writeBuffer = [];
|
|
14751
14936
|
this.writeBufferOffsets = [];
|
|
14752
14937
|
this.writeBufferSizes = [];
|
|
@@ -14765,14 +14950,20 @@ class Database extends events.EventEmitter {
|
|
|
14765
14950
|
}
|
|
14766
14951
|
}
|
|
14767
14952
|
|
|
14768
|
-
/**
|
|
14769
|
-
* Save index data to .idx.jdb file
|
|
14770
|
-
* @private
|
|
14953
|
+
/**
|
|
14954
|
+
* Save index data to .idx.jdb file
|
|
14955
|
+
* @private
|
|
14771
14956
|
*/
|
|
14772
14957
|
async _saveIndexDataToFile() {
|
|
14773
14958
|
if (this.indexManager) {
|
|
14774
14959
|
try {
|
|
14775
14960
|
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
14961
|
+
if (this.indexManager.indexLoaded === false && fs.existsSync(idxPath)) {
|
|
14962
|
+
if (this.opts.debugMode) {
|
|
14963
|
+
console.log(`⚠️ _saveIndexDataToFile: Index unloaded in memory, preserving existing index file ${idxPath}`);
|
|
14964
|
+
}
|
|
14965
|
+
return;
|
|
14966
|
+
}
|
|
14776
14967
|
const indexJSON = this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {};
|
|
14777
14968
|
|
|
14778
14969
|
// Check if index is empty
|
|
@@ -14843,8 +15034,8 @@ class Database extends events.EventEmitter {
|
|
|
14843
15034
|
}
|
|
14844
15035
|
}
|
|
14845
15036
|
|
|
14846
|
-
/**
|
|
14847
|
-
* Get operation queue statistics
|
|
15037
|
+
/**
|
|
15038
|
+
* Get operation queue statistics
|
|
14848
15039
|
*/
|
|
14849
15040
|
getQueueStats() {
|
|
14850
15041
|
if (!this.operationQueue) {
|
|
@@ -14862,11 +15053,11 @@ class Database extends events.EventEmitter {
|
|
|
14862
15053
|
return this.operationQueue.getStats();
|
|
14863
15054
|
}
|
|
14864
15055
|
|
|
14865
|
-
/**
|
|
14866
|
-
* Wait for all pending operations to complete
|
|
14867
|
-
* This includes operation queue AND active insert sessions
|
|
14868
|
-
* If called with no arguments, interpret as waitForOperations(null).
|
|
14869
|
-
* If argument provided (maxWaitTime), pass that on.
|
|
15056
|
+
/**
|
|
15057
|
+
* Wait for all pending operations to complete
|
|
15058
|
+
* This includes operation queue AND active insert sessions
|
|
15059
|
+
* If called with no arguments, interpret as waitForOperations(null).
|
|
15060
|
+
* If argument provided (maxWaitTime), pass that on.
|
|
14870
15061
|
*/
|
|
14871
15062
|
async waitForOperations(maxWaitTime = null) {
|
|
14872
15063
|
// Accept any falsy/undefined/empty call as "wait for all"
|