jexidb 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Database.cjs +1642 -334
- package/docs/API.md +1057 -1051
- package/package.json +1 -1
- package/scripts/benchmark-array-serialization.js +108 -0
- package/scripts/score-mode-demo.js +45 -0
- package/src/Database.mjs +1362 -167
- package/src/FileHandler.mjs +83 -44
- package/src/OperationQueue.mjs +23 -23
- package/src/Serializer.mjs +214 -23
- package/src/managers/IndexManager.mjs +778 -87
- package/src/managers/QueryManager.mjs +266 -49
- package/src/managers/TermManager.mjs +7 -7
- package/src/utils/operatorNormalizer.mjs +116 -0
- package/test/coverage-method.test.js +93 -0
- package/test/deserialize-corruption-fixes.test.js +296 -0
- package/test/exists-method.test.js +318 -0
- package/test/explicit-indexes-comparison.test.js +219 -0
- package/test/filehandler-non-adjacent-ranges-bug.test.js +175 -0
- package/test/index-line-number-regression.test.js +100 -0
- package/test/index-missing-index-data.test.js +91 -0
- package/test/index-persistence.test.js +205 -20
- package/test/insert-session-auto-flush.test.js +353 -0
- package/test/legacy-operator-compat.test.js +154 -0
- package/test/score-method.test.js +60 -0
package/dist/Database.cjs
CHANGED
|
@@ -90,12 +90,16 @@ class InsertSession {
|
|
|
90
90
|
constructor(database, sessionOptions = {}) {
|
|
91
91
|
this.database = database;
|
|
92
92
|
this.batchSize = sessionOptions.batchSize || 100;
|
|
93
|
+
this.enableAutoSave = sessionOptions.enableAutoSave !== undefined ? sessionOptions.enableAutoSave : true;
|
|
93
94
|
this.totalInserted = 0;
|
|
94
95
|
this.flushing = false;
|
|
95
96
|
this.batches = []; // Array of batches to avoid slice() in flush()
|
|
96
97
|
this.currentBatch = []; // Current batch being filled
|
|
97
98
|
this.sessionId = Math.random().toString(36).substr(2, 9);
|
|
98
99
|
|
|
100
|
+
// Track pending auto-flush operations
|
|
101
|
+
this.pendingAutoFlushes = new Set();
|
|
102
|
+
|
|
99
103
|
// Register this session as active
|
|
100
104
|
this.database.activeInsertSessions.add(this);
|
|
101
105
|
}
|
|
@@ -118,42 +122,141 @@ class InsertSession {
|
|
|
118
122
|
this.currentBatch.push(finalRecord);
|
|
119
123
|
this.totalInserted++;
|
|
120
124
|
|
|
121
|
-
// If batch is full, move it to batches array
|
|
125
|
+
// If batch is full, move it to batches array and trigger auto-flush
|
|
122
126
|
if (this.currentBatch.length >= this.batchSize) {
|
|
123
127
|
this.batches.push(this.currentBatch);
|
|
124
128
|
this.currentBatch = [];
|
|
129
|
+
|
|
130
|
+
// Auto-flush in background (non-blocking)
|
|
131
|
+
// This ensures batches are flushed automatically without blocking add()
|
|
132
|
+
this.autoFlush().catch(err => {
|
|
133
|
+
// Log error but don't throw - we don't want to break the add() flow
|
|
134
|
+
console.error('Auto-flush error in InsertSession:', err);
|
|
135
|
+
});
|
|
125
136
|
}
|
|
126
137
|
return finalRecord;
|
|
127
138
|
}
|
|
128
|
-
async
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// Prevent concurrent flushes
|
|
139
|
+
async autoFlush() {
|
|
140
|
+
// Only flush if not already flushing
|
|
141
|
+
// This method will process all pending batches
|
|
133
142
|
if (this.flushing) return;
|
|
134
|
-
this.flushing = true;
|
|
135
|
-
try {
|
|
136
|
-
// Process all complete batches
|
|
137
|
-
for (const batch of this.batches) {
|
|
138
|
-
await this.database.insertBatch(batch);
|
|
139
|
-
}
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
144
|
+
// Create a promise for this auto-flush operation
|
|
145
|
+
const flushPromise = this._doFlush();
|
|
146
|
+
this.pendingAutoFlushes.add(flushPromise);
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
// Remove from pending set when complete (success or error)
|
|
149
|
+
flushPromise.then(() => {
|
|
150
|
+
this.pendingAutoFlushes.delete(flushPromise);
|
|
151
|
+
}).catch(err => {
|
|
152
|
+
this.pendingAutoFlushes.delete(flushPromise);
|
|
153
|
+
throw err;
|
|
154
|
+
});
|
|
155
|
+
return flushPromise;
|
|
156
|
+
}
|
|
157
|
+
async _doFlush() {
|
|
158
|
+
// Check if database is destroyed or closed before starting
|
|
159
|
+
if (this.database.destroyed || this.database.closed) {
|
|
160
|
+
// Clear batches if database is closed/destroyed
|
|
147
161
|
this.batches = [];
|
|
148
162
|
this.currentBatch = [];
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Prevent concurrent flushes - if already flushing, wait for it
|
|
167
|
+
if (this.flushing) {
|
|
168
|
+
// Wait for the current flush to complete
|
|
169
|
+
while (this.flushing) {
|
|
170
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
171
|
+
}
|
|
172
|
+
// After waiting, check if there's anything left to flush
|
|
173
|
+
// If another flush completed everything, we're done
|
|
174
|
+
if (this.batches.length === 0 && this.currentBatch.length === 0) return;
|
|
175
|
+
|
|
176
|
+
// Check again if database was closed during wait
|
|
177
|
+
if (this.database.destroyed || this.database.closed) {
|
|
178
|
+
this.batches = [];
|
|
179
|
+
this.currentBatch = [];
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.flushing = true;
|
|
184
|
+
try {
|
|
185
|
+
// Process continuously until queue is completely empty
|
|
186
|
+
// This handles the case where new data is added during the flush
|
|
187
|
+
while (this.batches.length > 0 || this.currentBatch.length > 0) {
|
|
188
|
+
// Check if database was closed during processing
|
|
189
|
+
if (this.database.destroyed || this.database.closed) {
|
|
190
|
+
// Clear remaining batches
|
|
191
|
+
this.batches = [];
|
|
192
|
+
this.currentBatch = [];
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Process all complete batches that exist at this moment
|
|
197
|
+
// Note: new batches may be added to this.batches during this loop
|
|
198
|
+
const batchesToProcess = this.batches.length;
|
|
199
|
+
for (let i = 0; i < batchesToProcess; i++) {
|
|
200
|
+
// Check again before each batch
|
|
201
|
+
if (this.database.destroyed || this.database.closed) {
|
|
202
|
+
this.batches = [];
|
|
203
|
+
this.currentBatch = [];
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const batch = this.batches.shift(); // Remove from front
|
|
207
|
+
await this.database.insertBatch(batch);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Process current batch if it has data
|
|
211
|
+
// Note: new records may be added to currentBatch during processing
|
|
212
|
+
if (this.currentBatch.length > 0) {
|
|
213
|
+
// Check if database was closed
|
|
214
|
+
if (this.database.destroyed || this.database.closed) {
|
|
215
|
+
this.batches = [];
|
|
216
|
+
this.currentBatch = [];
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if currentBatch reached batchSize during processing
|
|
221
|
+
if (this.currentBatch.length >= this.batchSize) {
|
|
222
|
+
// Move it to batches array and process in next iteration
|
|
223
|
+
this.batches.push(this.currentBatch);
|
|
224
|
+
this.currentBatch = [];
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Process the current batch
|
|
229
|
+
const batchToProcess = this.currentBatch;
|
|
230
|
+
this.currentBatch = []; // Clear before processing to allow new adds
|
|
231
|
+
await this.database.insertBatch(batchToProcess);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
149
234
|
} finally {
|
|
150
235
|
this.flushing = false;
|
|
151
236
|
}
|
|
152
237
|
}
|
|
238
|
+
async flush() {
|
|
239
|
+
// Wait for any pending auto-flushes to complete first
|
|
240
|
+
await this.waitForAutoFlushes();
|
|
241
|
+
|
|
242
|
+
// Then do a final flush to ensure everything is processed
|
|
243
|
+
await this._doFlush();
|
|
244
|
+
}
|
|
245
|
+
async waitForAutoFlushes() {
|
|
246
|
+
// Wait for all pending auto-flush operations to complete
|
|
247
|
+
if (this.pendingAutoFlushes.size > 0) {
|
|
248
|
+
await Promise.all(Array.from(this.pendingAutoFlushes));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
153
251
|
async commit() {
|
|
154
252
|
// CRITICAL FIX: Make session auto-reusable by removing committed state
|
|
155
253
|
// Allow multiple commits on the same session
|
|
156
254
|
|
|
255
|
+
// First, wait for all pending auto-flushes to complete
|
|
256
|
+
await this.waitForAutoFlushes();
|
|
257
|
+
|
|
258
|
+
// Then flush any remaining data (including currentBatch)
|
|
259
|
+
// This ensures everything is inserted before commit returns
|
|
157
260
|
await this.flush();
|
|
158
261
|
|
|
159
262
|
// Reset session state for next commit cycle
|
|
@@ -168,6 +271,9 @@ class InsertSession {
|
|
|
168
271
|
async waitForOperations(maxWaitTime = null) {
|
|
169
272
|
const startTime = Date.now();
|
|
170
273
|
const hasTimeout = maxWaitTime !== null && maxWaitTime !== undefined;
|
|
274
|
+
|
|
275
|
+
// Wait for auto-flushes first
|
|
276
|
+
await this.waitForAutoFlushes();
|
|
171
277
|
while (this.flushing || this.batches.length > 0 || this.currentBatch.length > 0) {
|
|
172
278
|
// Check timeout only if we have one
|
|
173
279
|
if (hasTimeout && Date.now() - startTime >= maxWaitTime) {
|
|
@@ -182,7 +288,7 @@ class InsertSession {
|
|
|
182
288
|
* Check if this session has pending operations
|
|
183
289
|
*/
|
|
184
290
|
hasPendingOperations() {
|
|
185
|
-
return this.flushing || this.batches.length > 0 || this.currentBatch.length > 0;
|
|
291
|
+
return this.pendingAutoFlushes.size > 0 || this.flushing || this.batches.length > 0 || this.currentBatch.length > 0;
|
|
186
292
|
}
|
|
187
293
|
|
|
188
294
|
/**
|
|
@@ -197,6 +303,7 @@ class InsertSession {
|
|
|
197
303
|
this.currentBatch = [];
|
|
198
304
|
this.totalInserted = 0;
|
|
199
305
|
this.flushing = false;
|
|
306
|
+
this.pendingAutoFlushes.clear();
|
|
200
307
|
}
|
|
201
308
|
}
|
|
202
309
|
|
|
@@ -255,7 +362,10 @@ class Database extends _events.EventEmitter {
|
|
|
255
362
|
streamingThreshold: opts.streamingThreshold || 0.8,
|
|
256
363
|
// Use streaming when limit > 80% of total records
|
|
257
364
|
// Serialization options
|
|
258
|
-
enableArraySerialization: opts.enableArraySerialization !== false
|
|
365
|
+
enableArraySerialization: opts.enableArraySerialization !== false,
|
|
366
|
+
// Enable array serialization by default
|
|
367
|
+
// Index rebuild options
|
|
368
|
+
allowIndexRebuild: opts.allowIndexRebuild === true // Allow automatic index rebuild when corrupted (default false - throws error)
|
|
259
369
|
}, opts);
|
|
260
370
|
|
|
261
371
|
// CRITICAL FIX: Initialize AbortController for lifecycle management
|
|
@@ -282,6 +392,8 @@ class Database extends _events.EventEmitter {
|
|
|
282
392
|
this.isSaving = false;
|
|
283
393
|
this.lastSaveTime = null;
|
|
284
394
|
this.initialized = false;
|
|
395
|
+
this._offsetRecoveryInProgress = false;
|
|
396
|
+
this.writeBufferTotalSize = 0;
|
|
285
397
|
|
|
286
398
|
// Initialize managers
|
|
287
399
|
this.initializeManagers();
|
|
@@ -327,10 +439,11 @@ class Database extends _events.EventEmitter {
|
|
|
327
439
|
|
|
328
440
|
// Validate indexes array (new format) - but only if we have fields
|
|
329
441
|
if (this.opts.originalIndexes && Array.isArray(this.opts.originalIndexes)) {
|
|
330
|
-
if (
|
|
331
|
-
|
|
442
|
+
if (this.opts.fields) {
|
|
443
|
+
this.validateIndexFields(this.opts.originalIndexes);
|
|
444
|
+
} else if (this.opts.debugMode) {
|
|
445
|
+
console.log('⚠️ Skipping index field validation because no fields configuration was provided');
|
|
332
446
|
}
|
|
333
|
-
this.validateIndexFields(this.opts.originalIndexes);
|
|
334
447
|
}
|
|
335
448
|
if (this.opts.debugMode) {
|
|
336
449
|
const fieldCount = this.opts.fields ? Object.keys(this.opts.fields).length : 0;
|
|
@@ -345,9 +458,13 @@ class Database extends _events.EventEmitter {
|
|
|
345
458
|
* Validate field types
|
|
346
459
|
*/
|
|
347
460
|
validateFieldTypes(fields, configType) {
|
|
348
|
-
const supportedTypes = ['string', 'number', 'boolean', 'array:string', 'array:number', 'array:boolean', 'array', 'object'];
|
|
461
|
+
const supportedTypes = ['string', 'number', 'boolean', 'array:string', 'array:number', 'array:boolean', 'array', 'object', 'auto'];
|
|
349
462
|
const errors = [];
|
|
350
463
|
for (const [fieldName, fieldType] of Object.entries(fields)) {
|
|
464
|
+
if (fieldType === 'auto') {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
351
468
|
// Check if type is supported
|
|
352
469
|
if (!supportedTypes.includes(fieldType)) {
|
|
353
470
|
errors.push(`Unsupported ${configType} type '${fieldType}' for field '${fieldName}'. Supported types: ${supportedTypes.join(', ')}`);
|
|
@@ -393,25 +510,21 @@ class Database extends _events.EventEmitter {
|
|
|
393
510
|
* Prepare index configuration for IndexManager
|
|
394
511
|
*/
|
|
395
512
|
prepareIndexConfiguration() {
|
|
396
|
-
|
|
397
|
-
if (this.opts.fields && Array.isArray(this.opts.indexes)) {
|
|
398
|
-
// New format: { fields: {...}, indexes: [...] }
|
|
513
|
+
if (Array.isArray(this.opts.indexes)) {
|
|
399
514
|
const indexedFields = {};
|
|
400
|
-
const originalIndexes = [...this.opts.indexes];
|
|
401
|
-
|
|
515
|
+
const originalIndexes = [...this.opts.indexes];
|
|
516
|
+
const hasFieldConfig = this.opts.fields && typeof this.opts.fields === 'object';
|
|
402
517
|
for (const fieldName of this.opts.indexes) {
|
|
403
|
-
if (this.opts.fields[fieldName]) {
|
|
518
|
+
if (hasFieldConfig && this.opts.fields[fieldName]) {
|
|
404
519
|
indexedFields[fieldName] = this.opts.fields[fieldName];
|
|
520
|
+
} else {
|
|
521
|
+
indexedFields[fieldName] = 'auto';
|
|
405
522
|
}
|
|
406
523
|
}
|
|
407
|
-
|
|
408
|
-
// Store original indexes for validation
|
|
409
524
|
this.opts.originalIndexes = originalIndexes;
|
|
410
|
-
|
|
411
|
-
// Replace indexes array with object for IndexManager
|
|
412
525
|
this.opts.indexes = indexedFields;
|
|
413
526
|
if (this.opts.debugMode) {
|
|
414
|
-
console.log(`🔍
|
|
527
|
+
console.log(`🔍 Normalized indexes array to object: ${Object.keys(indexedFields).join(', ')} [${this.instanceId}]`);
|
|
415
528
|
}
|
|
416
529
|
}
|
|
417
530
|
// Legacy format (indexes as object) is already compatible
|
|
@@ -445,6 +558,18 @@ class Database extends _events.EventEmitter {
|
|
|
445
558
|
this.termManager.termMappingFields = termMappingFields;
|
|
446
559
|
this.opts.termMapping = true; // Always enable term mapping for optimal performance
|
|
447
560
|
|
|
561
|
+
// Validation: Ensure all array:string indexed fields are in term mapping fields
|
|
562
|
+
if (this.opts.indexes) {
|
|
563
|
+
const arrayStringFields = [];
|
|
564
|
+
for (const [field, type] of Object.entries(this.opts.indexes)) {
|
|
565
|
+
if (type === 'array:string' && !termMappingFields.includes(field)) {
|
|
566
|
+
arrayStringFields.push(field);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (arrayStringFields.length > 0) {
|
|
570
|
+
console.warn(`⚠️ Warning: The following array:string indexed fields were not added to term mapping: ${arrayStringFields.join(', ')}. This may impact performance.`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
448
573
|
if (this.opts.debugMode) {
|
|
449
574
|
if (termMappingFields.length > 0) {
|
|
450
575
|
console.log(`🔍 TermManager initialized for fields: ${termMappingFields.join(', ')} [${this.instanceId}]`);
|
|
@@ -471,6 +596,7 @@ class Database extends _events.EventEmitter {
|
|
|
471
596
|
this.writeBuffer = [];
|
|
472
597
|
this.writeBufferOffsets = []; // Track offsets for writeBuffer records
|
|
473
598
|
this.writeBufferSizes = []; // Track sizes for writeBuffer records
|
|
599
|
+
this.writeBufferTotalSize = 0;
|
|
474
600
|
this.isInsideOperationQueue = false; // Flag to prevent deadlock in save() calls
|
|
475
601
|
|
|
476
602
|
// Initialize other managers
|
|
@@ -491,8 +617,8 @@ class Database extends _events.EventEmitter {
|
|
|
491
617
|
// Auto-detect fields that benefit from term mapping
|
|
492
618
|
const termMappingFields = [];
|
|
493
619
|
for (const [field, type] of Object.entries(this.opts.indexes)) {
|
|
494
|
-
// Fields that should use term mapping
|
|
495
|
-
if (type === 'array:string'
|
|
620
|
+
// Fields that should use term mapping (only array fields)
|
|
621
|
+
if (type === 'array:string') {
|
|
496
622
|
termMappingFields.push(field);
|
|
497
623
|
}
|
|
498
624
|
}
|
|
@@ -690,6 +816,9 @@ class Database extends _events.EventEmitter {
|
|
|
690
816
|
// Don't load the entire file - just initialize empty state
|
|
691
817
|
// The actual record count will come from loaded offsets
|
|
692
818
|
this.writeBuffer = []; // writeBuffer is only for new unsaved records
|
|
819
|
+
this.writeBufferOffsets = [];
|
|
820
|
+
this.writeBufferSizes = [];
|
|
821
|
+
this.writeBufferTotalSize = 0;
|
|
693
822
|
|
|
694
823
|
// recordCount will be determined from loaded offsets
|
|
695
824
|
// If no offsets were loaded, we'll count records only if needed
|
|
@@ -699,13 +828,49 @@ class Database extends _events.EventEmitter {
|
|
|
699
828
|
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
700
829
|
try {
|
|
701
830
|
const idxFileHandler = new _FileHandler.default(idxPath, this.fileMutex, this.opts);
|
|
831
|
+
|
|
832
|
+
// Check if file exists BEFORE trying to read it
|
|
833
|
+
const fileExists = await idxFileHandler.exists();
|
|
834
|
+
if (!fileExists) {
|
|
835
|
+
// File doesn't exist - this will be handled by catch block
|
|
836
|
+
throw new Error('Index file does not exist');
|
|
837
|
+
}
|
|
702
838
|
const idxData = await idxFileHandler.readAll();
|
|
703
|
-
|
|
839
|
+
|
|
840
|
+
// If file exists but is empty or has no content, treat as corrupted
|
|
841
|
+
if (!idxData || !idxData.trim()) {
|
|
842
|
+
// File exists but is empty - treat as corrupted
|
|
843
|
+
const fileExists = await this.fileHandler.exists();
|
|
844
|
+
if (fileExists) {
|
|
845
|
+
const stats = await this.fileHandler.getFileStats();
|
|
846
|
+
if (stats && stats.size > 0) {
|
|
847
|
+
// Data file has content but index is empty - corrupted
|
|
848
|
+
if (!this.opts.allowIndexRebuild) {
|
|
849
|
+
throw new Error(`Index file is corrupted: ${idxPath} exists but contains no index data, ` + `while the data file has ${stats.size} bytes. ` + `Set allowIndexRebuild: true to automatically rebuild the index, ` + `or manually fix/delete the corrupted index file.`);
|
|
850
|
+
}
|
|
851
|
+
// Schedule rebuild if allowed
|
|
852
|
+
if (this.opts.debugMode) {
|
|
853
|
+
console.log(`⚠️ Index file exists but is empty while data file has ${stats.size} bytes - scheduling rebuild`);
|
|
854
|
+
}
|
|
855
|
+
this._scheduleIndexRebuild();
|
|
856
|
+
// Continue execution - rebuild will happen on first query
|
|
857
|
+
// Don't return - let the code continue to load other things if needed
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// If data file is also empty, just continue (no error needed)
|
|
861
|
+
// Don't return - let the code continue to load other things if needed
|
|
862
|
+
} else {
|
|
863
|
+
// File has content - parse and load it
|
|
704
864
|
const parsedIdxData = JSON.parse(idxData);
|
|
705
865
|
|
|
706
866
|
// Always load offsets if available (even without indexed fields)
|
|
707
867
|
if (parsedIdxData.offsets && Array.isArray(parsedIdxData.offsets)) {
|
|
708
868
|
this.offsets = parsedIdxData.offsets;
|
|
869
|
+
// CRITICAL FIX: Update IndexManager totalLines to match offsets length
|
|
870
|
+
// This ensures queries and length property work correctly even if offsets are reset later
|
|
871
|
+
if (this.indexManager && this.offsets.length > 0) {
|
|
872
|
+
this.indexManager.setTotalLines(this.offsets.length);
|
|
873
|
+
}
|
|
709
874
|
if (this.opts.debugMode) {
|
|
710
875
|
console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}`);
|
|
711
876
|
}
|
|
@@ -719,23 +884,8 @@ class Database extends _events.EventEmitter {
|
|
|
719
884
|
}
|
|
720
885
|
}
|
|
721
886
|
|
|
722
|
-
// Load index data only if available and we have indexed fields
|
|
723
|
-
if (parsedIdxData && parsedIdxData.index && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
724
|
-
this.indexManager.load(parsedIdxData.index);
|
|
725
|
-
|
|
726
|
-
// Load term mapping data from .idx file if it exists
|
|
727
|
-
if (parsedIdxData.termMapping && this.termManager) {
|
|
728
|
-
await this.termManager.loadTerms(parsedIdxData.termMapping);
|
|
729
|
-
if (this.opts.debugMode) {
|
|
730
|
-
console.log(`📂 Loaded term mapping from ${idxPath}`);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
if (this.opts.debugMode) {
|
|
734
|
-
console.log(`📂 Loaded index data from ${idxPath}`);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
887
|
// Load configuration from .idx file if database exists
|
|
888
|
+
// CRITICAL: Load config FIRST so indexes are available for term mapping detection
|
|
739
889
|
if (parsedIdxData.config) {
|
|
740
890
|
const config = parsedIdxData.config;
|
|
741
891
|
|
|
@@ -748,10 +898,86 @@ class Database extends _events.EventEmitter {
|
|
|
748
898
|
}
|
|
749
899
|
if (config.indexes) {
|
|
750
900
|
this.opts.indexes = config.indexes;
|
|
901
|
+
if (this.indexManager) {
|
|
902
|
+
this.indexManager.setIndexesConfig(config.indexes);
|
|
903
|
+
}
|
|
751
904
|
if (this.opts.debugMode) {
|
|
752
905
|
console.log(`📂 Loaded indexes config from ${idxPath}:`, Object.keys(config.indexes));
|
|
753
906
|
}
|
|
754
907
|
}
|
|
908
|
+
|
|
909
|
+
// CRITICAL FIX: Update term mapping fields AFTER loading indexes from config
|
|
910
|
+
// This ensures termManager knows which fields use term mapping
|
|
911
|
+
// (getTermMappingFields() was called during init() before indexes were loaded)
|
|
912
|
+
if (this.termManager && config.indexes) {
|
|
913
|
+
const termMappingFields = this.getTermMappingFields();
|
|
914
|
+
this.termManager.termMappingFields = termMappingFields;
|
|
915
|
+
if (this.opts.debugMode && termMappingFields.length > 0) {
|
|
916
|
+
console.log(`🔍 Updated term mapping fields after loading indexes: ${termMappingFields.join(', ')}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Load term mapping data from .idx file if it exists
|
|
922
|
+
// CRITICAL: Load termMapping even if index is empty (terms are needed for queries)
|
|
923
|
+
// NOTE: termMappingFields should already be set above from config.indexes
|
|
924
|
+
if (parsedIdxData.termMapping && this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.length > 0) {
|
|
925
|
+
await this.termManager.loadTerms(parsedIdxData.termMapping);
|
|
926
|
+
if (this.opts.debugMode) {
|
|
927
|
+
console.log(`📂 Loaded term mapping from ${idxPath} (${Object.keys(parsedIdxData.termMapping).length} terms)`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Load index data only if available and we have indexed fields
|
|
932
|
+
if (parsedIdxData && parsedIdxData.index && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
933
|
+
this.indexManager.load(parsedIdxData.index);
|
|
934
|
+
if (this.opts.debugMode) {
|
|
935
|
+
console.log(`📂 Loaded index data from ${idxPath}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Check if loaded index is actually empty (corrupted)
|
|
939
|
+
let hasAnyIndexData = false;
|
|
940
|
+
for (const field of this.indexManager.indexedFields) {
|
|
941
|
+
if (this.indexManager.hasUsableIndexData(field)) {
|
|
942
|
+
hasAnyIndexData = true;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
if (this.opts.debugMode) {
|
|
947
|
+
console.log(`📊 Index check: hasAnyIndexData=${hasAnyIndexData}, indexedFields=${this.indexManager.indexedFields.join(',')}`);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Schedule rebuild if index is empty AND file exists with data
|
|
951
|
+
if (!hasAnyIndexData) {
|
|
952
|
+
// Check if the actual .jdb file has data
|
|
953
|
+
const fileExists = await this.fileHandler.exists();
|
|
954
|
+
if (this.opts.debugMode) {
|
|
955
|
+
console.log(`📊 File check: exists=${fileExists}`);
|
|
956
|
+
}
|
|
957
|
+
if (fileExists) {
|
|
958
|
+
const stats = await this.fileHandler.getFileStats();
|
|
959
|
+
if (this.opts.debugMode) {
|
|
960
|
+
console.log(`📊 File stats: size=${stats?.size}`);
|
|
961
|
+
}
|
|
962
|
+
if (stats && stats.size > 0) {
|
|
963
|
+
// File has data but index is empty - corrupted index detected
|
|
964
|
+
if (!this.opts.allowIndexRebuild) {
|
|
965
|
+
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
966
|
+
throw new Error(`Index file is corrupted: ${idxPath} exists but contains no index data, ` + `while the data file has ${stats.size} bytes. ` + `Set allowIndexRebuild: true to automatically rebuild the index, ` + `or manually fix/delete the corrupted index file.`);
|
|
967
|
+
}
|
|
968
|
+
// Schedule rebuild if allowed
|
|
969
|
+
if (this.opts.debugMode) {
|
|
970
|
+
console.log(`⚠️ Index loaded but empty while file has ${stats.size} bytes - scheduling rebuild`);
|
|
971
|
+
}
|
|
972
|
+
this._scheduleIndexRebuild();
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Continue with remaining config loading
|
|
979
|
+
if (parsedIdxData.config) {
|
|
980
|
+
const config = parsedIdxData.config;
|
|
755
981
|
if (config.originalIndexes) {
|
|
756
982
|
this.opts.originalIndexes = config.originalIndexes;
|
|
757
983
|
if (this.opts.debugMode) {
|
|
@@ -770,11 +996,75 @@ class Database extends _events.EventEmitter {
|
|
|
770
996
|
}
|
|
771
997
|
} catch (idxError) {
|
|
772
998
|
// Index file doesn't exist or is corrupted, rebuild from data
|
|
999
|
+
// BUT: if error is about rebuild being disabled, re-throw it immediately
|
|
1000
|
+
if (idxError.message && (idxError.message.includes('allowIndexRebuild') || idxError.message.includes('corrupted'))) {
|
|
1001
|
+
throw idxError;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// If error is "Index file does not exist", check if we should throw or rebuild
|
|
1005
|
+
if (idxError.message && idxError.message.includes('does not exist')) {
|
|
1006
|
+
// Check if the actual .jdb file has data that needs indexing
|
|
1007
|
+
try {
|
|
1008
|
+
const fileExists = await this.fileHandler.exists();
|
|
1009
|
+
if (fileExists) {
|
|
1010
|
+
const stats = await this.fileHandler.getFileStats();
|
|
1011
|
+
if (stats && stats.size > 0) {
|
|
1012
|
+
// File has data but index is missing
|
|
1013
|
+
if (!this.opts.allowIndexRebuild) {
|
|
1014
|
+
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
1015
|
+
throw new Error(`Index file is missing or corrupted: ${idxPath} does not exist or is invalid, ` + `while the data file has ${stats.size} bytes. ` + `Set allowIndexRebuild: true to automatically rebuild the index, ` + `or manually create/fix the index file.`);
|
|
1016
|
+
}
|
|
1017
|
+
// Schedule rebuild if allowed
|
|
1018
|
+
if (this.opts.debugMode) {
|
|
1019
|
+
console.log(`⚠️ .jdb file has ${stats.size} bytes but index missing - scheduling rebuild`);
|
|
1020
|
+
}
|
|
1021
|
+
this._scheduleIndexRebuild();
|
|
1022
|
+
return; // Exit early
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch (statsError) {
|
|
1026
|
+
if (this.opts.debugMode) {
|
|
1027
|
+
console.log('⚠️ Could not check file stats:', statsError.message);
|
|
1028
|
+
}
|
|
1029
|
+
// Re-throw if it's our error about rebuild being disabled
|
|
1030
|
+
if (statsError.message && statsError.message.includes('allowIndexRebuild')) {
|
|
1031
|
+
throw statsError;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
// If no data file or empty, just continue (no error needed)
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
773
1037
|
if (this.opts.debugMode) {
|
|
774
|
-
console.log('📂 No index file found,
|
|
1038
|
+
console.log('📂 No index file found or corrupted, checking if rebuild is needed...');
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Check if the actual .jdb file has data that needs indexing
|
|
1042
|
+
try {
|
|
1043
|
+
const fileExists = await this.fileHandler.exists();
|
|
1044
|
+
if (fileExists) {
|
|
1045
|
+
const stats = await this.fileHandler.getFileStats();
|
|
1046
|
+
if (stats && stats.size > 0) {
|
|
1047
|
+
// File has data but index is missing or corrupted
|
|
1048
|
+
if (!this.opts.allowIndexRebuild) {
|
|
1049
|
+
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
1050
|
+
throw new Error(`Index file is missing or corrupted: ${idxPath} does not exist or is invalid, ` + `while the data file has ${stats.size} bytes. ` + `Set allowIndexRebuild: true to automatically rebuild the index, ` + `or manually create/fix the index file.`);
|
|
1051
|
+
}
|
|
1052
|
+
// Schedule rebuild if allowed
|
|
1053
|
+
if (this.opts.debugMode) {
|
|
1054
|
+
console.log(`⚠️ .jdb file has ${stats.size} bytes but index missing - scheduling rebuild`);
|
|
1055
|
+
}
|
|
1056
|
+
this._scheduleIndexRebuild();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
} catch (statsError) {
|
|
1060
|
+
if (this.opts.debugMode) {
|
|
1061
|
+
console.log('⚠️ Could not check file stats:', statsError.message);
|
|
1062
|
+
}
|
|
1063
|
+
// Re-throw if it's our error about rebuild being disabled
|
|
1064
|
+
if (statsError.message && statsError.message.includes('allowIndexRebuild')) {
|
|
1065
|
+
throw statsError;
|
|
1066
|
+
}
|
|
775
1067
|
}
|
|
776
|
-
// We can't rebuild index without violating no-memory-storage rule
|
|
777
|
-
// Index will be rebuilt as needed during queries
|
|
778
1068
|
}
|
|
779
1069
|
} else {
|
|
780
1070
|
// No indexed fields, no need to rebuild indexes
|
|
@@ -800,6 +1090,23 @@ class Database extends _events.EventEmitter {
|
|
|
800
1090
|
console.log(`💾 save() called: writeBuffer.length=${this.writeBuffer.length}, offsets.length=${this.offsets.length}`);
|
|
801
1091
|
}
|
|
802
1092
|
|
|
1093
|
+
// CRITICAL FIX: Wait for all active insert sessions to complete their auto-flushes
|
|
1094
|
+
// This prevents race conditions where save() writes data while auto-flushes are still adding to writeBuffer
|
|
1095
|
+
if (this.activeInsertSessions && this.activeInsertSessions.size > 0) {
|
|
1096
|
+
if (this.opts.debugMode) {
|
|
1097
|
+
console.log(`⏳ save(): Waiting for ${this.activeInsertSessions.size} active insert sessions to complete auto-flushes`);
|
|
1098
|
+
}
|
|
1099
|
+
const sessionPromises = Array.from(this.activeInsertSessions).map(session => session.waitForAutoFlushes().catch(err => {
|
|
1100
|
+
if (this.opts.debugMode) {
|
|
1101
|
+
console.warn(`⚠️ save(): Error waiting for insert session: ${err.message}`);
|
|
1102
|
+
}
|
|
1103
|
+
}));
|
|
1104
|
+
await Promise.all(sessionPromises);
|
|
1105
|
+
if (this.opts.debugMode) {
|
|
1106
|
+
console.log(`✅ save(): All insert sessions completed auto-flushes`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
803
1110
|
// Auto-save removed - no need to pause anything
|
|
804
1111
|
|
|
805
1112
|
try {
|
|
@@ -1165,8 +1472,33 @@ class Database extends _events.EventEmitter {
|
|
|
1165
1472
|
}
|
|
1166
1473
|
|
|
1167
1474
|
// Rebuild index from the saved records
|
|
1475
|
+
// CRITICAL: Process term mapping for records loaded from file to ensure ${field}Ids are available
|
|
1168
1476
|
for (let i = 0; i < allData.length; i++) {
|
|
1169
|
-
|
|
1477
|
+
let record = allData[i];
|
|
1478
|
+
|
|
1479
|
+
// CRITICAL FIX: Ensure records have ${field}Ids for term mapping fields
|
|
1480
|
+
// Records from writeBuffer already have ${field}Ids from processTermMapping
|
|
1481
|
+
// Records from file need to be processed to restore ${field}Ids
|
|
1482
|
+
const termMappingFields = this.getTermMappingFields();
|
|
1483
|
+
if (termMappingFields.length > 0 && this.termManager) {
|
|
1484
|
+
for (const field of termMappingFields) {
|
|
1485
|
+
if (record[field] && Array.isArray(record[field])) {
|
|
1486
|
+
// Check if field contains term IDs (numbers) or terms (strings)
|
|
1487
|
+
const firstValue = record[field][0];
|
|
1488
|
+
if (typeof firstValue === 'number') {
|
|
1489
|
+
// Already term IDs, create ${field}Ids
|
|
1490
|
+
record[`${field}Ids`] = record[field];
|
|
1491
|
+
} else if (typeof firstValue === 'string') {
|
|
1492
|
+
// Terms, need to convert to term IDs
|
|
1493
|
+
const termIds = record[field].map(term => {
|
|
1494
|
+
const termId = this.termManager.getTermIdWithoutIncrement(term);
|
|
1495
|
+
return termId !== undefined ? termId : this.termManager.getTermId(term);
|
|
1496
|
+
});
|
|
1497
|
+
record[`${field}Ids`] = termIds;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1170
1502
|
await this.indexManager.add(record, i);
|
|
1171
1503
|
}
|
|
1172
1504
|
}
|
|
@@ -1199,6 +1531,8 @@ class Database extends _events.EventEmitter {
|
|
|
1199
1531
|
this.writeBuffer = [];
|
|
1200
1532
|
this.writeBufferOffsets = [];
|
|
1201
1533
|
this.writeBufferSizes = [];
|
|
1534
|
+
this.writeBufferTotalSize = 0;
|
|
1535
|
+
this.writeBufferTotalSize = 0;
|
|
1202
1536
|
}
|
|
1203
1537
|
|
|
1204
1538
|
// indexOffset already set correctly to currentOffset (total file size) above
|
|
@@ -1404,18 +1738,15 @@ class Database extends _events.EventEmitter {
|
|
|
1404
1738
|
}
|
|
1405
1739
|
|
|
1406
1740
|
// OPTIMIZATION: Process records using pre-computed term IDs
|
|
1407
|
-
|
|
1408
|
-
const processedRecord = {
|
|
1409
|
-
...record
|
|
1410
|
-
};
|
|
1741
|
+
for (const record of records) {
|
|
1411
1742
|
for (const field of termMappingFields) {
|
|
1412
1743
|
if (record[field] && Array.isArray(record[field])) {
|
|
1413
1744
|
const termIds = record[field].map(term => termIdMap.get(term));
|
|
1414
|
-
|
|
1745
|
+
record[`${field}Ids`] = termIds;
|
|
1415
1746
|
}
|
|
1416
1747
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1748
|
+
}
|
|
1749
|
+
return records;
|
|
1419
1750
|
}
|
|
1420
1751
|
|
|
1421
1752
|
/**
|
|
@@ -1507,17 +1838,18 @@ class Database extends _events.EventEmitter {
|
|
|
1507
1838
|
// OPTIMIZATION: Calculate and store offset and size for writeBuffer record
|
|
1508
1839
|
// SPACE OPTIMIZATION: Remove term IDs before serialization
|
|
1509
1840
|
const cleanRecord = this.removeTermIdsForSerialization(record);
|
|
1510
|
-
const
|
|
1511
|
-
const recordSize =
|
|
1841
|
+
const recordBuffer = this.serializer.serialize(cleanRecord);
|
|
1842
|
+
const recordSize = recordBuffer.length;
|
|
1512
1843
|
|
|
1513
1844
|
// Calculate offset based on end of file + previous writeBuffer sizes
|
|
1514
|
-
const previousWriteBufferSize = this.
|
|
1845
|
+
const previousWriteBufferSize = this.writeBufferTotalSize;
|
|
1515
1846
|
const recordOffset = this.indexOffset + previousWriteBufferSize;
|
|
1516
1847
|
this.writeBufferOffsets.push(recordOffset);
|
|
1517
1848
|
this.writeBufferSizes.push(recordSize);
|
|
1849
|
+
this.writeBufferTotalSize += recordSize;
|
|
1518
1850
|
|
|
1519
|
-
// OPTIMIZATION: Use the
|
|
1520
|
-
const lineNumber = this.writeBuffer.length - 1;
|
|
1851
|
+
// OPTIMIZATION: Use the absolute line number (persisted records + writeBuffer index)
|
|
1852
|
+
const lineNumber = this._getAbsoluteLineNumber(this.writeBuffer.length - 1);
|
|
1521
1853
|
|
|
1522
1854
|
// OPTIMIZATION: Defer index updates to batch processing
|
|
1523
1855
|
// Store the record for batch index processing
|
|
@@ -1587,7 +1919,7 @@ class Database extends _events.EventEmitter {
|
|
|
1587
1919
|
console.log(`💾 _insertBatchInternal: processing size=${dataArray.length}, startWriteBuffer=${this.writeBuffer.length}`);
|
|
1588
1920
|
}
|
|
1589
1921
|
const records = [];
|
|
1590
|
-
const
|
|
1922
|
+
const existingWriteBufferLength = this.writeBuffer.length;
|
|
1591
1923
|
|
|
1592
1924
|
// Initialize schema if not already done (auto-detect from first record)
|
|
1593
1925
|
if (this.serializer && !this.serializer.schemaManager.isInitialized && dataArray.length > 0) {
|
|
@@ -1621,25 +1953,26 @@ class Database extends _events.EventEmitter {
|
|
|
1621
1953
|
this.writeBuffer.push(...schemaEnforcedRecords);
|
|
1622
1954
|
|
|
1623
1955
|
// OPTIMIZATION: Calculate offsets and sizes in batch (O(n))
|
|
1624
|
-
let runningTotalSize = this.
|
|
1956
|
+
let runningTotalSize = this.writeBufferTotalSize;
|
|
1625
1957
|
for (let i = 0; i < processedRecords.length; i++) {
|
|
1626
1958
|
const record = processedRecords[i];
|
|
1627
1959
|
// SPACE OPTIMIZATION: Remove term IDs before serialization
|
|
1628
1960
|
const cleanRecord = this.removeTermIdsForSerialization(record);
|
|
1629
|
-
const
|
|
1630
|
-
const recordSize =
|
|
1961
|
+
const recordBuffer = this.serializer.serialize(cleanRecord);
|
|
1962
|
+
const recordSize = recordBuffer.length;
|
|
1631
1963
|
const recordOffset = this.indexOffset + runningTotalSize;
|
|
1632
1964
|
runningTotalSize += recordSize;
|
|
1633
1965
|
this.writeBufferOffsets.push(recordOffset);
|
|
1634
1966
|
this.writeBufferSizes.push(recordSize);
|
|
1635
1967
|
}
|
|
1968
|
+
this.writeBufferTotalSize = runningTotalSize;
|
|
1636
1969
|
|
|
1637
1970
|
// OPTIMIZATION: Batch process index updates
|
|
1638
1971
|
if (!this.pendingIndexUpdates) {
|
|
1639
1972
|
this.pendingIndexUpdates = [];
|
|
1640
1973
|
}
|
|
1641
1974
|
for (let i = 0; i < processedRecords.length; i++) {
|
|
1642
|
-
const lineNumber =
|
|
1975
|
+
const lineNumber = this._getAbsoluteLineNumber(existingWriteBufferLength + i);
|
|
1643
1976
|
this.pendingIndexUpdates.push({
|
|
1644
1977
|
record: processedRecords[i],
|
|
1645
1978
|
lineNumber
|
|
@@ -1678,7 +2011,7 @@ class Database extends _events.EventEmitter {
|
|
|
1678
2011
|
try {
|
|
1679
2012
|
// Validate indexed query mode if enabled
|
|
1680
2013
|
if (this.opts.indexedQueryMode === 'strict') {
|
|
1681
|
-
this._validateIndexedQuery(criteria);
|
|
2014
|
+
this._validateIndexedQuery(criteria, options);
|
|
1682
2015
|
}
|
|
1683
2016
|
|
|
1684
2017
|
// Get results from file (QueryManager already handles term ID restoration)
|
|
@@ -1740,8 +2073,14 @@ class Database extends _events.EventEmitter {
|
|
|
1740
2073
|
/**
|
|
1741
2074
|
* Validate indexed query mode for strict mode
|
|
1742
2075
|
* @private
|
|
2076
|
+
* @param {Object} criteria - Query criteria
|
|
2077
|
+
* @param {Object} options - Query options
|
|
1743
2078
|
*/
|
|
1744
|
-
_validateIndexedQuery(criteria) {
|
|
2079
|
+
_validateIndexedQuery(criteria, options = {}) {
|
|
2080
|
+
// Allow bypassing strict mode validation with allowNonIndexed option
|
|
2081
|
+
if (options.allowNonIndexed === true) {
|
|
2082
|
+
return; // Skip validation for this query
|
|
2083
|
+
}
|
|
1745
2084
|
if (!criteria || typeof criteria !== 'object') {
|
|
1746
2085
|
return; // Allow null/undefined criteria
|
|
1747
2086
|
}
|
|
@@ -2014,7 +2353,7 @@ class Database extends _events.EventEmitter {
|
|
|
2014
2353
|
if (index !== -1) {
|
|
2015
2354
|
// Record is already in writeBuffer, update it
|
|
2016
2355
|
this.writeBuffer[index] = updated;
|
|
2017
|
-
lineNumber = index;
|
|
2356
|
+
lineNumber = this._getAbsoluteLineNumber(index);
|
|
2018
2357
|
if (this.opts.debugMode) {
|
|
2019
2358
|
console.log(`🔄 UPDATE: Updated existing writeBuffer record at index ${index}`);
|
|
2020
2359
|
}
|
|
@@ -2022,7 +2361,7 @@ class Database extends _events.EventEmitter {
|
|
|
2022
2361
|
// Record is in file, add updated version to writeBuffer
|
|
2023
2362
|
// This will ensure the updated record is saved and replaces the file version
|
|
2024
2363
|
this.writeBuffer.push(updated);
|
|
2025
|
-
lineNumber = this.writeBuffer.length - 1;
|
|
2364
|
+
lineNumber = this._getAbsoluteLineNumber(this.writeBuffer.length - 1);
|
|
2026
2365
|
if (this.opts.debugMode) {
|
|
2027
2366
|
console.log(`🔄 UPDATE: Added new record to writeBuffer at index ${lineNumber}`);
|
|
2028
2367
|
}
|
|
@@ -2195,6 +2534,21 @@ class Database extends _events.EventEmitter {
|
|
|
2195
2534
|
const savedRecords = this.offsets.length;
|
|
2196
2535
|
const writeBufferRecords = this.writeBuffer.length;
|
|
2197
2536
|
|
|
2537
|
+
// CRITICAL FIX: If offsets are empty but indexOffset exists, use fallback calculation
|
|
2538
|
+
// This handles cases where offsets weren't loaded or were reset
|
|
2539
|
+
if (savedRecords === 0 && this.indexOffset > 0 && this.initialized) {
|
|
2540
|
+
// Try to use IndexManager totalLines if available
|
|
2541
|
+
if (this.indexManager && this.indexManager.totalLines > 0) {
|
|
2542
|
+
return this.indexManager.totalLines + writeBufferRecords;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// Fallback: estimate from indexOffset (less accurate but better than 0)
|
|
2546
|
+
// This is a defensive fix for cases where offsets are missing but file has data
|
|
2547
|
+
if (this.opts.debugMode) {
|
|
2548
|
+
console.log(`⚠️ LENGTH: offsets array is empty but indexOffset=${this.indexOffset}, using IndexManager.totalLines or estimation`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2198
2552
|
// CRITICAL FIX: Validate that offsets array is consistent with actual data
|
|
2199
2553
|
// This prevents the bug where database reassignment causes desynchronization
|
|
2200
2554
|
if (this.initialized && savedRecords > 0) {
|
|
@@ -2238,21 +2592,7 @@ class Database extends _events.EventEmitter {
|
|
|
2238
2592
|
* Calculate current writeBuffer size in bytes (similar to published v1.1.0)
|
|
2239
2593
|
*/
|
|
2240
2594
|
currentWriteBufferSize() {
|
|
2241
|
-
|
|
2242
|
-
return 0;
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
// Calculate total size of all records in writeBuffer
|
|
2246
|
-
let totalSize = 0;
|
|
2247
|
-
for (const record of this.writeBuffer) {
|
|
2248
|
-
if (record) {
|
|
2249
|
-
// SPACE OPTIMIZATION: Remove term IDs before size calculation
|
|
2250
|
-
const cleanRecord = this.removeTermIdsForSerialization(record);
|
|
2251
|
-
const recordJson = JSON.stringify(cleanRecord) + '\n';
|
|
2252
|
-
totalSize += Buffer.byteLength(recordJson, 'utf8');
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
return totalSize;
|
|
2595
|
+
return this.writeBufferTotalSize || 0;
|
|
2256
2596
|
}
|
|
2257
2597
|
|
|
2258
2598
|
/**
|
|
@@ -2284,91 +2624,291 @@ class Database extends _events.EventEmitter {
|
|
|
2284
2624
|
}
|
|
2285
2625
|
|
|
2286
2626
|
/**
|
|
2287
|
-
*
|
|
2288
|
-
*
|
|
2289
|
-
* If anything is still active, it indicates a bug - log error and force cleanup
|
|
2627
|
+
* Schedule index rebuild when index data is missing or corrupted
|
|
2628
|
+
* @private
|
|
2290
2629
|
*/
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
// Mark as destroying immediately to prevent new operations
|
|
2295
|
-
this.destroying = true;
|
|
2296
|
-
|
|
2297
|
-
// Wait for all active insert sessions to complete before destroying
|
|
2298
|
-
if (this.activeInsertSessions.size > 0) {
|
|
2299
|
-
if (this.opts.debugMode) {
|
|
2300
|
-
console.log(`⏳ destroy: Waiting for ${this.activeInsertSessions.size} active insert sessions`);
|
|
2301
|
-
}
|
|
2302
|
-
const sessionPromises = Array.from(this.activeInsertSessions).map(session => session.waitForOperations(null) // Wait indefinitely for sessions to complete
|
|
2303
|
-
);
|
|
2304
|
-
try {
|
|
2305
|
-
await Promise.all(sessionPromises);
|
|
2306
|
-
} catch (error) {
|
|
2307
|
-
if (this.opts.debugMode) {
|
|
2308
|
-
console.log(`⚠️ destroy: Error waiting for sessions: ${error.message}`);
|
|
2309
|
-
}
|
|
2310
|
-
// Continue with destruction even if sessions have issues
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
// Destroy all active sessions
|
|
2314
|
-
for (const session of this.activeInsertSessions) {
|
|
2315
|
-
session.destroy();
|
|
2316
|
-
}
|
|
2317
|
-
this.activeInsertSessions.clear();
|
|
2318
|
-
}
|
|
2630
|
+
_scheduleIndexRebuild() {
|
|
2631
|
+
// Mark that rebuild is needed
|
|
2632
|
+
this._indexRebuildNeeded = true;
|
|
2319
2633
|
|
|
2320
|
-
//
|
|
2321
|
-
|
|
2322
|
-
let timeoutHandle = null;
|
|
2323
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
2324
|
-
timeoutHandle = setTimeout(() => {
|
|
2325
|
-
reject(new Error('Destroy operation timed out after 5 seconds'));
|
|
2326
|
-
}, 5000);
|
|
2327
|
-
});
|
|
2328
|
-
try {
|
|
2329
|
-
await Promise.race([destroyPromise, timeoutPromise]);
|
|
2330
|
-
} catch (error) {
|
|
2331
|
-
if (error.message === 'Destroy operation timed out after 5 seconds') {
|
|
2332
|
-
console.error('🚨 DESTROY TIMEOUT: Force destroying database after timeout');
|
|
2333
|
-
// Force mark as destroyed even if cleanup failed
|
|
2334
|
-
this.destroyed = true;
|
|
2335
|
-
this.destroying = false;
|
|
2336
|
-
return;
|
|
2337
|
-
}
|
|
2338
|
-
throw error;
|
|
2339
|
-
} finally {
|
|
2340
|
-
// Clear the timeout to prevent Jest open handle warning
|
|
2341
|
-
if (timeoutHandle) {
|
|
2342
|
-
clearTimeout(timeoutHandle);
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2634
|
+
// Rebuild will happen lazily on first query if index is empty
|
|
2635
|
+
// This avoids blocking init() but ensures index is available when needed
|
|
2345
2636
|
}
|
|
2346
2637
|
|
|
2347
2638
|
/**
|
|
2348
|
-
*
|
|
2639
|
+
* Rebuild indexes from data file if needed
|
|
2640
|
+
* @private
|
|
2349
2641
|
*/
|
|
2350
|
-
async
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
if (this.writeBuffer.length > 0) {
|
|
2357
|
-
const bug = `BUG: writeBuffer has ${this.writeBuffer.length} records - save() should have cleared this`;
|
|
2358
|
-
bugs.push(bug);
|
|
2359
|
-
console.error(`🚨 ${bug}`);
|
|
2360
|
-
}
|
|
2642
|
+
async _rebuildIndexesIfNeeded() {
|
|
2643
|
+
if (this.opts.debugMode) {
|
|
2644
|
+
console.log(`🔍 _rebuildIndexesIfNeeded called: _indexRebuildNeeded=${this._indexRebuildNeeded}`);
|
|
2645
|
+
}
|
|
2646
|
+
if (!this._indexRebuildNeeded) return;
|
|
2647
|
+
if (!this.indexManager || !this.indexManager.indexedFields || this.indexManager.indexedFields.length === 0) return;
|
|
2361
2648
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2649
|
+
// Check if index actually needs rebuilding
|
|
2650
|
+
let needsRebuild = false;
|
|
2651
|
+
for (const field of this.indexManager.indexedFields) {
|
|
2652
|
+
if (!this.indexManager.hasUsableIndexData(field)) {
|
|
2653
|
+
needsRebuild = true;
|
|
2654
|
+
break;
|
|
2367
2655
|
}
|
|
2656
|
+
}
|
|
2657
|
+
if (!needsRebuild) {
|
|
2658
|
+
this._indexRebuildNeeded = false;
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2368
2661
|
|
|
2369
|
-
|
|
2662
|
+
// Check if rebuild is allowed
|
|
2663
|
+
if (!this.opts.allowIndexRebuild) {
|
|
2664
|
+
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
2665
|
+
throw new Error(`Index rebuild required but disabled: Index file ${idxPath} is corrupted or missing, ` + `and allowIndexRebuild is set to false. ` + `Set allowIndexRebuild: true to automatically rebuild the index, ` + `or manually fix/delete the corrupted index file.`);
|
|
2666
|
+
}
|
|
2667
|
+
if (this.opts.debugMode) {
|
|
2668
|
+
console.log('🔨 Rebuilding indexes from data file...');
|
|
2669
|
+
}
|
|
2670
|
+
try {
|
|
2671
|
+
// Read all records and rebuild index
|
|
2672
|
+
let count = 0;
|
|
2673
|
+
const startTime = Date.now();
|
|
2370
2674
|
|
|
2371
|
-
//
|
|
2675
|
+
// Auto-detect schema from first line if not initialized
|
|
2676
|
+
if (!this.serializer.schemaManager.isInitialized) {
|
|
2677
|
+
const fs = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
|
|
2678
|
+
const readline = await Promise.resolve().then(() => _interopRequireWildcard(require('readline')));
|
|
2679
|
+
const stream = fs.createReadStream(this.fileHandler.file, {
|
|
2680
|
+
highWaterMark: 64 * 1024,
|
|
2681
|
+
encoding: 'utf8'
|
|
2682
|
+
});
|
|
2683
|
+
const rl = readline.createInterface({
|
|
2684
|
+
input: stream,
|
|
2685
|
+
crlfDelay: Infinity
|
|
2686
|
+
});
|
|
2687
|
+
var _iteratorAbruptCompletion = false;
|
|
2688
|
+
var _didIteratorError = false;
|
|
2689
|
+
var _iteratorError;
|
|
2690
|
+
try {
|
|
2691
|
+
for (var _iterator = _asyncIterator(rl), _step; _iteratorAbruptCompletion = !(_step = await _iterator.next()).done; _iteratorAbruptCompletion = false) {
|
|
2692
|
+
const line = _step.value;
|
|
2693
|
+
{
|
|
2694
|
+
if (line && line.trim()) {
|
|
2695
|
+
try {
|
|
2696
|
+
const firstRecord = JSON.parse(line);
|
|
2697
|
+
if (Array.isArray(firstRecord)) {
|
|
2698
|
+
// Try to infer schema from opts.fields if available
|
|
2699
|
+
if (this.opts.fields && typeof this.opts.fields === 'object') {
|
|
2700
|
+
const fieldNames = Object.keys(this.opts.fields);
|
|
2701
|
+
if (fieldNames.length >= firstRecord.length) {
|
|
2702
|
+
// Use first N fields from opts.fields to match array length
|
|
2703
|
+
const schema = fieldNames.slice(0, firstRecord.length);
|
|
2704
|
+
this.serializer.initializeSchema(schema);
|
|
2705
|
+
if (this.opts.debugMode) {
|
|
2706
|
+
console.log(`🔍 Inferred schema from opts.fields: ${schema.join(', ')}`);
|
|
2707
|
+
}
|
|
2708
|
+
} else {
|
|
2709
|
+
throw new Error(`Cannot rebuild index: array has ${firstRecord.length} elements but opts.fields only defines ${fieldNames.length} fields. Schema must be explicitly provided.`);
|
|
2710
|
+
}
|
|
2711
|
+
} else {
|
|
2712
|
+
throw new Error('Cannot rebuild index: schema missing, file uses array format, and opts.fields not provided. The .idx.jdb file is corrupted.');
|
|
2713
|
+
}
|
|
2714
|
+
} else {
|
|
2715
|
+
// Object format, initialize from object keys
|
|
2716
|
+
this.serializer.initializeSchema(firstRecord, true);
|
|
2717
|
+
if (this.opts.debugMode) {
|
|
2718
|
+
console.log(`🔍 Auto-detected schema from object: ${Object.keys(firstRecord).join(', ')}`);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
break;
|
|
2722
|
+
} catch (error) {
|
|
2723
|
+
if (this.opts.debugMode) {
|
|
2724
|
+
console.error('❌ Failed to auto-detect schema:', error.message);
|
|
2725
|
+
}
|
|
2726
|
+
throw error;
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
} catch (err) {
|
|
2732
|
+
_didIteratorError = true;
|
|
2733
|
+
_iteratorError = err;
|
|
2734
|
+
} finally {
|
|
2735
|
+
try {
|
|
2736
|
+
if (_iteratorAbruptCompletion && _iterator.return != null) {
|
|
2737
|
+
await _iterator.return();
|
|
2738
|
+
}
|
|
2739
|
+
} finally {
|
|
2740
|
+
if (_didIteratorError) {
|
|
2741
|
+
throw _iteratorError;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
stream.destroy();
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
// Use streaming to read records without loading everything into memory
|
|
2749
|
+
// Also rebuild offsets while we're at it
|
|
2750
|
+
const fs = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
|
|
2751
|
+
const readline = await Promise.resolve().then(() => _interopRequireWildcard(require('readline')));
|
|
2752
|
+
this.offsets = [];
|
|
2753
|
+
let currentOffset = 0;
|
|
2754
|
+
const stream = fs.createReadStream(this.fileHandler.file, {
|
|
2755
|
+
highWaterMark: 64 * 1024,
|
|
2756
|
+
encoding: 'utf8'
|
|
2757
|
+
});
|
|
2758
|
+
const rl = readline.createInterface({
|
|
2759
|
+
input: stream,
|
|
2760
|
+
crlfDelay: Infinity
|
|
2761
|
+
});
|
|
2762
|
+
try {
|
|
2763
|
+
var _iteratorAbruptCompletion2 = false;
|
|
2764
|
+
var _didIteratorError2 = false;
|
|
2765
|
+
var _iteratorError2;
|
|
2766
|
+
try {
|
|
2767
|
+
for (var _iterator2 = _asyncIterator(rl), _step2; _iteratorAbruptCompletion2 = !(_step2 = await _iterator2.next()).done; _iteratorAbruptCompletion2 = false) {
|
|
2768
|
+
const line = _step2.value;
|
|
2769
|
+
{
|
|
2770
|
+
if (line && line.trim()) {
|
|
2771
|
+
try {
|
|
2772
|
+
// Record the offset for this line
|
|
2773
|
+
this.offsets.push(currentOffset);
|
|
2774
|
+
const record = this.serializer.deserialize(line);
|
|
2775
|
+
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
|
|
2776
|
+
await this.indexManager.add(recordWithTerms, count);
|
|
2777
|
+
count++;
|
|
2778
|
+
} catch (error) {
|
|
2779
|
+
// Skip invalid lines
|
|
2780
|
+
if (this.opts.debugMode) {
|
|
2781
|
+
console.log(`⚠️ Rebuild: Failed to deserialize line ${count}:`, error.message);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
// Update offset for next line (including newline character)
|
|
2786
|
+
currentOffset += Buffer.byteLength(line, 'utf8') + 1;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
} catch (err) {
|
|
2790
|
+
_didIteratorError2 = true;
|
|
2791
|
+
_iteratorError2 = err;
|
|
2792
|
+
} finally {
|
|
2793
|
+
try {
|
|
2794
|
+
if (_iteratorAbruptCompletion2 && _iterator2.return != null) {
|
|
2795
|
+
await _iterator2.return();
|
|
2796
|
+
}
|
|
2797
|
+
} finally {
|
|
2798
|
+
if (_didIteratorError2) {
|
|
2799
|
+
throw _iteratorError2;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
} finally {
|
|
2804
|
+
stream.destroy();
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
// Update indexManager totalLines
|
|
2808
|
+
if (this.indexManager) {
|
|
2809
|
+
this.indexManager.setTotalLines(this.offsets.length);
|
|
2810
|
+
}
|
|
2811
|
+
this._indexRebuildNeeded = false;
|
|
2812
|
+
if (this.opts.debugMode) {
|
|
2813
|
+
console.log(`✅ Index rebuilt from ${count} records in ${Date.now() - startTime}ms`);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
// Save the rebuilt index
|
|
2817
|
+
await this._saveIndexDataToFile();
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
if (this.opts.debugMode) {
|
|
2820
|
+
console.error('❌ Failed to rebuild indexes:', error.message);
|
|
2821
|
+
}
|
|
2822
|
+
// Don't throw - queries will fall back to streaming
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
/**
|
|
2827
|
+
* Destroy database - DESTRUCTIVE MODE
|
|
2828
|
+
* Assumes save() has already been called by user
|
|
2829
|
+
* If anything is still active, it indicates a bug - log error and force cleanup
|
|
2830
|
+
*/
|
|
2831
|
+
async destroy() {
|
|
2832
|
+
if (this.destroyed) return;
|
|
2833
|
+
|
|
2834
|
+
// Mark as destroying immediately to prevent new operations
|
|
2835
|
+
this.destroying = true;
|
|
2836
|
+
|
|
2837
|
+
// Wait for all active insert sessions to complete before destroying
|
|
2838
|
+
if (this.activeInsertSessions.size > 0) {
|
|
2839
|
+
if (this.opts.debugMode) {
|
|
2840
|
+
console.log(`⏳ destroy: Waiting for ${this.activeInsertSessions.size} active insert sessions`);
|
|
2841
|
+
}
|
|
2842
|
+
const sessionPromises = Array.from(this.activeInsertSessions).map(session => session.waitForOperations(null) // Wait indefinitely for sessions to complete
|
|
2843
|
+
);
|
|
2844
|
+
try {
|
|
2845
|
+
await Promise.all(sessionPromises);
|
|
2846
|
+
} catch (error) {
|
|
2847
|
+
if (this.opts.debugMode) {
|
|
2848
|
+
console.log(`⚠️ destroy: Error waiting for sessions: ${error.message}`);
|
|
2849
|
+
}
|
|
2850
|
+
// Continue with destruction even if sessions have issues
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// Destroy all active sessions
|
|
2854
|
+
for (const session of this.activeInsertSessions) {
|
|
2855
|
+
session.destroy();
|
|
2856
|
+
}
|
|
2857
|
+
this.activeInsertSessions.clear();
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
// CRITICAL FIX: Add timeout protection to prevent destroy() from hanging
|
|
2861
|
+
const destroyPromise = this._performDestroy();
|
|
2862
|
+
let timeoutHandle = null;
|
|
2863
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2864
|
+
timeoutHandle = setTimeout(() => {
|
|
2865
|
+
reject(new Error('Destroy operation timed out after 5 seconds'));
|
|
2866
|
+
}, 5000);
|
|
2867
|
+
});
|
|
2868
|
+
try {
|
|
2869
|
+
await Promise.race([destroyPromise, timeoutPromise]);
|
|
2870
|
+
} catch (error) {
|
|
2871
|
+
if (error.message === 'Destroy operation timed out after 5 seconds') {
|
|
2872
|
+
console.error('🚨 DESTROY TIMEOUT: Force destroying database after timeout');
|
|
2873
|
+
// Force mark as destroyed even if cleanup failed
|
|
2874
|
+
this.destroyed = true;
|
|
2875
|
+
this.destroying = false;
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
throw error;
|
|
2879
|
+
} finally {
|
|
2880
|
+
// Clear the timeout to prevent Jest open handle warning
|
|
2881
|
+
if (timeoutHandle) {
|
|
2882
|
+
clearTimeout(timeoutHandle);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
/**
|
|
2888
|
+
* Internal destroy implementation
|
|
2889
|
+
*/
|
|
2890
|
+
async _performDestroy() {
|
|
2891
|
+
try {
|
|
2892
|
+
// CRITICAL: Check for bugs - anything active indicates save() didn't work properly
|
|
2893
|
+
const bugs = [];
|
|
2894
|
+
|
|
2895
|
+
// Check for pending data that should have been saved
|
|
2896
|
+
if (this.writeBuffer.length > 0) {
|
|
2897
|
+
const bug = `BUG: writeBuffer has ${this.writeBuffer.length} records - save() should have cleared this`;
|
|
2898
|
+
bugs.push(bug);
|
|
2899
|
+
console.error(`🚨 ${bug}`);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
// Check for pending operations that should have completed
|
|
2903
|
+
if (this.pendingOperations.size > 0) {
|
|
2904
|
+
const bug = `BUG: ${this.pendingOperations.size} pending operations - save() should have completed these`;
|
|
2905
|
+
bugs.push(bug);
|
|
2906
|
+
console.error(`🚨 ${bug}`);
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
// Auto-save manager removed - no cleanup needed
|
|
2910
|
+
|
|
2911
|
+
// Check for active save operation
|
|
2372
2912
|
if (this.isSaving) {
|
|
2373
2913
|
const bug = `BUG: save operation still active - previous save() should have completed`;
|
|
2374
2914
|
bugs.push(bug);
|
|
@@ -2396,6 +2936,8 @@ class Database extends _events.EventEmitter {
|
|
|
2396
2936
|
this.writeBuffer = [];
|
|
2397
2937
|
this.writeBufferOffsets = [];
|
|
2398
2938
|
this.writeBufferSizes = [];
|
|
2939
|
+
this.writeBufferTotalSize = 0;
|
|
2940
|
+
this.writeBufferTotalSize = 0;
|
|
2399
2941
|
this.deletedIds.clear();
|
|
2400
2942
|
this.pendingOperations.clear();
|
|
2401
2943
|
this.pendingIndexUpdates = [];
|
|
@@ -2460,8 +3002,393 @@ class Database extends _events.EventEmitter {
|
|
|
2460
3002
|
*/
|
|
2461
3003
|
async count(criteria = {}, options = {}) {
|
|
2462
3004
|
this._validateInitialization('count');
|
|
2463
|
-
|
|
2464
|
-
|
|
3005
|
+
|
|
3006
|
+
// OPTIMIZATION: Use queryManager.count() instead of find() for better performance
|
|
3007
|
+
// This is especially faster for indexed queries which can use indexManager.query().size
|
|
3008
|
+
const fileCount = await this.queryManager.count(criteria, options);
|
|
3009
|
+
|
|
3010
|
+
// Count matching records in writeBuffer
|
|
3011
|
+
const writeBufferCount = this.writeBuffer.filter(record => this.queryManager.matchesCriteria(record, criteria, options)).length;
|
|
3012
|
+
return fileCount + writeBufferCount;
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/**
|
|
3016
|
+
* Check if any records exist for given field and terms (index-only, ultra-fast)
|
|
3017
|
+
* Delegates to IndexManager.exists() for maximum performance
|
|
3018
|
+
*
|
|
3019
|
+
* @param {string} fieldName - Indexed field name
|
|
3020
|
+
* @param {string|Array<string>} terms - Single term or array of terms
|
|
3021
|
+
* @param {Object} options - Options: { $all: true/false, caseInsensitive: true/false, excludes: Array<string> }
|
|
3022
|
+
* @returns {Promise<boolean>} - True if at least one match exists
|
|
3023
|
+
*
|
|
3024
|
+
* @example
|
|
3025
|
+
* // Check if channel exists
|
|
3026
|
+
* const exists = await db.exists('nameTerms', ['a', 'e'], { $all: true });
|
|
3027
|
+
*
|
|
3028
|
+
* @example
|
|
3029
|
+
* // Check if 'tv' exists but not 'globo'
|
|
3030
|
+
* const exists = await db.exists('nameTerms', 'tv', { excludes: ['globo'] });
|
|
3031
|
+
*/
|
|
3032
|
+
async exists(fieldName, terms, options = {}) {
|
|
3033
|
+
this._validateInitialization('exists');
|
|
3034
|
+
return this.indexManager.exists(fieldName, terms, options);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
/**
|
|
3038
|
+
* Calculate coverage for grouped include/exclude term sets
|
|
3039
|
+
* @param {string} fieldName - Name of the indexed field
|
|
3040
|
+
* @param {Array<object>} groups - Array of { terms, excludes } objects
|
|
3041
|
+
* @param {object} options - Optional settings
|
|
3042
|
+
* @returns {Promise<number>} Coverage percentage between 0 and 100
|
|
3043
|
+
*/
|
|
3044
|
+
async coverage(fieldName, groups, options = {}) {
|
|
3045
|
+
this._validateInitialization('coverage');
|
|
3046
|
+
if (typeof fieldName !== 'string' || !fieldName.trim()) {
|
|
3047
|
+
throw new Error('fieldName must be a non-empty string');
|
|
3048
|
+
}
|
|
3049
|
+
if (!Array.isArray(groups)) {
|
|
3050
|
+
throw new Error('groups must be an array');
|
|
3051
|
+
}
|
|
3052
|
+
if (groups.length === 0) {
|
|
3053
|
+
return 0;
|
|
3054
|
+
}
|
|
3055
|
+
if (!this.opts.indexes || !this.opts.indexes[fieldName]) {
|
|
3056
|
+
throw new Error(`Field "${fieldName}" is not indexed`);
|
|
3057
|
+
}
|
|
3058
|
+
const fieldType = this.opts.indexes[fieldName];
|
|
3059
|
+
const supportedTypes = ['array:string', 'string'];
|
|
3060
|
+
if (!supportedTypes.includes(fieldType)) {
|
|
3061
|
+
throw new Error(`coverage() only supports fields of type ${supportedTypes.join(', ')} (found: ${fieldType})`);
|
|
3062
|
+
}
|
|
3063
|
+
const fieldIndex = this.indexManager?.index?.data?.[fieldName];
|
|
3064
|
+
if (!fieldIndex) {
|
|
3065
|
+
return 0;
|
|
3066
|
+
}
|
|
3067
|
+
const isTermMapped = this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.includes(fieldName);
|
|
3068
|
+
const normalizeTerm = term => {
|
|
3069
|
+
if (term === undefined || term === null) {
|
|
3070
|
+
return '';
|
|
3071
|
+
}
|
|
3072
|
+
return String(term).trim();
|
|
3073
|
+
};
|
|
3074
|
+
const resolveKey = term => {
|
|
3075
|
+
if (isTermMapped) {
|
|
3076
|
+
const termId = this.termManager.getTermIdWithoutIncrement(term);
|
|
3077
|
+
if (termId === null || termId === undefined) {
|
|
3078
|
+
return null;
|
|
3079
|
+
}
|
|
3080
|
+
return String(termId);
|
|
3081
|
+
}
|
|
3082
|
+
return String(term);
|
|
3083
|
+
};
|
|
3084
|
+
let matchedGroups = 0;
|
|
3085
|
+
for (const group of groups) {
|
|
3086
|
+
if (!group || typeof group !== 'object') {
|
|
3087
|
+
throw new Error('Each coverage group must be an object');
|
|
3088
|
+
}
|
|
3089
|
+
const includeTermsRaw = Array.isArray(group.terms) ? group.terms : [];
|
|
3090
|
+
const excludeTermsRaw = Array.isArray(group.excludes) ? group.excludes : [];
|
|
3091
|
+
const includeTerms = Array.from(new Set(includeTermsRaw.map(normalizeTerm).filter(term => term.length > 0)));
|
|
3092
|
+
if (includeTerms.length === 0) {
|
|
3093
|
+
throw new Error('Each coverage group must define at least one term');
|
|
3094
|
+
}
|
|
3095
|
+
const excludeTerms = Array.from(new Set(excludeTermsRaw.map(normalizeTerm).filter(term => term.length > 0)));
|
|
3096
|
+
let candidateLines = null;
|
|
3097
|
+
let groupMatched = true;
|
|
3098
|
+
for (const term of includeTerms) {
|
|
3099
|
+
const key = resolveKey(term);
|
|
3100
|
+
if (key === null) {
|
|
3101
|
+
groupMatched = false;
|
|
3102
|
+
break;
|
|
3103
|
+
}
|
|
3104
|
+
const termData = fieldIndex[key];
|
|
3105
|
+
if (!termData) {
|
|
3106
|
+
groupMatched = false;
|
|
3107
|
+
break;
|
|
3108
|
+
}
|
|
3109
|
+
const lineNumbers = this.indexManager._getAllLineNumbers(termData);
|
|
3110
|
+
if (!lineNumbers || lineNumbers.length === 0) {
|
|
3111
|
+
groupMatched = false;
|
|
3112
|
+
break;
|
|
3113
|
+
}
|
|
3114
|
+
if (candidateLines === null) {
|
|
3115
|
+
candidateLines = new Set(lineNumbers);
|
|
3116
|
+
} else {
|
|
3117
|
+
const termSet = new Set(lineNumbers);
|
|
3118
|
+
for (const line of Array.from(candidateLines)) {
|
|
3119
|
+
if (!termSet.has(line)) {
|
|
3120
|
+
candidateLines.delete(line);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
if (!candidateLines || candidateLines.size === 0) {
|
|
3125
|
+
groupMatched = false;
|
|
3126
|
+
break;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
if (!groupMatched || !candidateLines || candidateLines.size === 0) {
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
for (const term of excludeTerms) {
|
|
3133
|
+
const key = resolveKey(term);
|
|
3134
|
+
if (key === null) {
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
const termData = fieldIndex[key];
|
|
3138
|
+
if (!termData) {
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3141
|
+
const excludeLines = this.indexManager._getAllLineNumbers(termData);
|
|
3142
|
+
if (!excludeLines || excludeLines.length === 0) {
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
for (const line of excludeLines) {
|
|
3146
|
+
if (!candidateLines.size) {
|
|
3147
|
+
break;
|
|
3148
|
+
}
|
|
3149
|
+
candidateLines.delete(line);
|
|
3150
|
+
}
|
|
3151
|
+
if (!candidateLines.size) {
|
|
3152
|
+
break;
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
if (candidateLines && candidateLines.size > 0) {
|
|
3156
|
+
matchedGroups++;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (matchedGroups === 0) {
|
|
3160
|
+
return 0;
|
|
3161
|
+
}
|
|
3162
|
+
const precision = typeof options.precision === 'number' && options.precision >= 0 ? options.precision : 2;
|
|
3163
|
+
const coverageValue = matchedGroups / groups.length * 100;
|
|
3164
|
+
return Number(coverageValue.toFixed(precision));
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
/**
|
|
3168
|
+
* Score records based on weighted terms in an indexed array:string field
|
|
3169
|
+
* @param {string} fieldName - Name of indexed array:string field
|
|
3170
|
+
* @param {object} scores - Map of terms to numeric weights
|
|
3171
|
+
* @param {object} options - Query options
|
|
3172
|
+
* @returns {Promise<Array>} Records with scores, sorted by score
|
|
3173
|
+
*/
|
|
3174
|
+
async score(fieldName, scores, options = {}) {
|
|
3175
|
+
// Validate initialization
|
|
3176
|
+
this._validateInitialization('score');
|
|
3177
|
+
|
|
3178
|
+
// Set default options
|
|
3179
|
+
const opts = {
|
|
3180
|
+
limit: options.limit ?? 100,
|
|
3181
|
+
sort: options.sort ?? 'desc',
|
|
3182
|
+
includeScore: options.includeScore !== false,
|
|
3183
|
+
mode: options.mode ?? 'sum'
|
|
3184
|
+
};
|
|
3185
|
+
|
|
3186
|
+
// Validate fieldName
|
|
3187
|
+
if (typeof fieldName !== 'string' || !fieldName) {
|
|
3188
|
+
throw new Error('fieldName must be a non-empty string');
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
// Validate scores object
|
|
3192
|
+
if (!scores || typeof scores !== 'object' || Array.isArray(scores)) {
|
|
3193
|
+
throw new Error('scores must be an object');
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// Handle empty scores - return empty array as specified
|
|
3197
|
+
if (Object.keys(scores).length === 0) {
|
|
3198
|
+
return [];
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// Validate scores values are numeric
|
|
3202
|
+
for (const [term, weight] of Object.entries(scores)) {
|
|
3203
|
+
if (typeof weight !== 'number' || isNaN(weight)) {
|
|
3204
|
+
throw new Error(`Score value for term "${term}" must be a number`);
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
// Validate mode
|
|
3209
|
+
const allowedModes = new Set(['sum', 'max', 'avg', 'first']);
|
|
3210
|
+
if (!allowedModes.has(opts.mode)) {
|
|
3211
|
+
throw new Error(`Invalid score mode "${opts.mode}". Must be one of: ${Array.from(allowedModes).join(', ')}`);
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
// Check if field is indexed and is array:string type
|
|
3215
|
+
if (!this.opts.indexes || !this.opts.indexes[fieldName]) {
|
|
3216
|
+
throw new Error(`Field "${fieldName}" is not indexed`);
|
|
3217
|
+
}
|
|
3218
|
+
const fieldType = this.opts.indexes[fieldName];
|
|
3219
|
+
if (fieldType !== 'array:string') {
|
|
3220
|
+
throw new Error(`Field "${fieldName}" must be of type "array:string" (found: ${fieldType})`);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// Check if this is a term-mapped field
|
|
3224
|
+
const isTermMapped = this.termManager && this.termManager.termMappingFields && this.termManager.termMappingFields.includes(fieldName);
|
|
3225
|
+
|
|
3226
|
+
// Access the index for this field
|
|
3227
|
+
const fieldIndex = this.indexManager.index.data[fieldName];
|
|
3228
|
+
if (!fieldIndex) {
|
|
3229
|
+
return [];
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
// Accumulate scores for each line number
|
|
3233
|
+
const scoreMap = new Map();
|
|
3234
|
+
const countMap = opts.mode === 'avg' ? new Map() : null;
|
|
3235
|
+
|
|
3236
|
+
// Iterate through each term in the scores object
|
|
3237
|
+
for (const [term, weight] of Object.entries(scores)) {
|
|
3238
|
+
// Get term ID if this is a term-mapped field
|
|
3239
|
+
let termKey;
|
|
3240
|
+
if (isTermMapped) {
|
|
3241
|
+
// For term-mapped fields, convert term to term ID
|
|
3242
|
+
const termId = this.termManager.getTermIdWithoutIncrement(term);
|
|
3243
|
+
if (termId === null || termId === undefined) {
|
|
3244
|
+
// Term doesn't exist, skip it
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
termKey = String(termId);
|
|
3248
|
+
} else {
|
|
3249
|
+
termKey = String(term);
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3252
|
+
// Look up line numbers for this term
|
|
3253
|
+
const termData = fieldIndex[termKey];
|
|
3254
|
+
if (!termData) {
|
|
3255
|
+
// Term doesn't exist in index, skip
|
|
3256
|
+
continue;
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
// Get all line numbers for this term
|
|
3260
|
+
const lineNumbers = this.indexManager._getAllLineNumbers(termData);
|
|
3261
|
+
|
|
3262
|
+
// Add weight to score for each line number
|
|
3263
|
+
for (const lineNumber of lineNumbers) {
|
|
3264
|
+
const currentScore = scoreMap.get(lineNumber);
|
|
3265
|
+
switch (opts.mode) {
|
|
3266
|
+
case 'sum':
|
|
3267
|
+
{
|
|
3268
|
+
const nextScore = (currentScore || 0) + weight;
|
|
3269
|
+
scoreMap.set(lineNumber, nextScore);
|
|
3270
|
+
break;
|
|
3271
|
+
}
|
|
3272
|
+
case 'max':
|
|
3273
|
+
{
|
|
3274
|
+
if (currentScore === undefined) {
|
|
3275
|
+
scoreMap.set(lineNumber, weight);
|
|
3276
|
+
} else {
|
|
3277
|
+
scoreMap.set(lineNumber, Math.max(currentScore, weight));
|
|
3278
|
+
}
|
|
3279
|
+
break;
|
|
3280
|
+
}
|
|
3281
|
+
case 'avg':
|
|
3282
|
+
{
|
|
3283
|
+
const previous = currentScore || 0;
|
|
3284
|
+
scoreMap.set(lineNumber, previous + weight);
|
|
3285
|
+
const count = (countMap.get(lineNumber) || 0) + 1;
|
|
3286
|
+
countMap.set(lineNumber, count);
|
|
3287
|
+
break;
|
|
3288
|
+
}
|
|
3289
|
+
case 'first':
|
|
3290
|
+
{
|
|
3291
|
+
if (currentScore === undefined) {
|
|
3292
|
+
scoreMap.set(lineNumber, weight);
|
|
3293
|
+
}
|
|
3294
|
+
break;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// For average mode, divide total by count
|
|
3301
|
+
if (opts.mode === 'avg') {
|
|
3302
|
+
for (const [lineNumber, totalScore] of scoreMap.entries()) {
|
|
3303
|
+
const count = countMap.get(lineNumber) || 1;
|
|
3304
|
+
scoreMap.set(lineNumber, totalScore / count);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// Filter out zero scores and sort by score
|
|
3309
|
+
const scoredEntries = Array.from(scoreMap.entries()).filter(([, score]) => score > 0);
|
|
3310
|
+
|
|
3311
|
+
// Sort by score
|
|
3312
|
+
scoredEntries.sort((a, b) => {
|
|
3313
|
+
return opts.sort === 'asc' ? a[1] - b[1] : b[1] - a[1];
|
|
3314
|
+
});
|
|
3315
|
+
|
|
3316
|
+
// Apply limit
|
|
3317
|
+
const limitedEntries = opts.limit > 0 ? scoredEntries.slice(0, opts.limit) : scoredEntries;
|
|
3318
|
+
if (limitedEntries.length === 0) {
|
|
3319
|
+
return [];
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3322
|
+
// Fetch actual records
|
|
3323
|
+
const lineNumbers = limitedEntries.map(([lineNumber]) => lineNumber);
|
|
3324
|
+
const scoresByLineNumber = new Map(limitedEntries);
|
|
3325
|
+
|
|
3326
|
+
// Use getRanges and fileHandler to read records
|
|
3327
|
+
const ranges = this.getRanges(lineNumbers);
|
|
3328
|
+
const groupedRanges = await this.fileHandler.groupedRanges(ranges);
|
|
3329
|
+
const fs = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
|
|
3330
|
+
const fd = await fs.promises.open(this.fileHandler.file, 'r');
|
|
3331
|
+
const results = [];
|
|
3332
|
+
try {
|
|
3333
|
+
for (const groupedRange of groupedRanges) {
|
|
3334
|
+
var _iteratorAbruptCompletion3 = false;
|
|
3335
|
+
var _didIteratorError3 = false;
|
|
3336
|
+
var _iteratorError3;
|
|
3337
|
+
try {
|
|
3338
|
+
for (var _iterator3 = _asyncIterator(this.fileHandler.readGroupedRange(groupedRange, fd)), _step3; _iteratorAbruptCompletion3 = !(_step3 = await _iterator3.next()).done; _iteratorAbruptCompletion3 = false) {
|
|
3339
|
+
const row = _step3.value;
|
|
3340
|
+
{
|
|
3341
|
+
try {
|
|
3342
|
+
const record = this.serializer.deserialize(row.line);
|
|
3343
|
+
|
|
3344
|
+
// Get line number from the row
|
|
3345
|
+
const lineNumber = row._ || 0;
|
|
3346
|
+
|
|
3347
|
+
// Restore term IDs to terms
|
|
3348
|
+
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record);
|
|
3349
|
+
|
|
3350
|
+
// Add line number
|
|
3351
|
+
recordWithTerms._ = lineNumber;
|
|
3352
|
+
|
|
3353
|
+
// Add score if includeScore is true
|
|
3354
|
+
if (opts.includeScore) {
|
|
3355
|
+
recordWithTerms.score = scoresByLineNumber.get(lineNumber) || 0;
|
|
3356
|
+
}
|
|
3357
|
+
results.push(recordWithTerms);
|
|
3358
|
+
} catch (error) {
|
|
3359
|
+
// Skip invalid lines
|
|
3360
|
+
if (this.opts.debugMode) {
|
|
3361
|
+
console.error('Error deserializing record in score():', error);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
} catch (err) {
|
|
3367
|
+
_didIteratorError3 = true;
|
|
3368
|
+
_iteratorError3 = err;
|
|
3369
|
+
} finally {
|
|
3370
|
+
try {
|
|
3371
|
+
if (_iteratorAbruptCompletion3 && _iterator3.return != null) {
|
|
3372
|
+
await _iterator3.return();
|
|
3373
|
+
}
|
|
3374
|
+
} finally {
|
|
3375
|
+
if (_didIteratorError3) {
|
|
3376
|
+
throw _iteratorError3;
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
} finally {
|
|
3382
|
+
await fd.close();
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// Re-sort results to maintain score order (since reads might be out of order)
|
|
3386
|
+
results.sort((a, b) => {
|
|
3387
|
+
const scoreA = scoresByLineNumber.get(a._) || 0;
|
|
3388
|
+
const scoreB = scoresByLineNumber.get(b._) || 0;
|
|
3389
|
+
return opts.sort === 'asc' ? scoreA - scoreB : scoreB - scoreA;
|
|
3390
|
+
});
|
|
3391
|
+
return results;
|
|
2465
3392
|
}
|
|
2466
3393
|
|
|
2467
3394
|
/**
|
|
@@ -2656,10 +3583,47 @@ class Database extends _events.EventEmitter {
|
|
|
2656
3583
|
}
|
|
2657
3584
|
|
|
2658
3585
|
// CRITICAL FIX: Only remove processed items from writeBuffer after all async operations complete
|
|
2659
|
-
// OPTIMIZATION: Use Set.has() for O(1) lookup - same Set used for processing
|
|
2660
3586
|
const beforeLength = this.writeBuffer.length;
|
|
2661
|
-
|
|
3587
|
+
if (beforeLength > 0) {
|
|
3588
|
+
const originalRecords = this.writeBuffer;
|
|
3589
|
+
const originalOffsets = this.writeBufferOffsets;
|
|
3590
|
+
const originalSizes = this.writeBufferSizes;
|
|
3591
|
+
const retainedRecords = [];
|
|
3592
|
+
const retainedOffsets = [];
|
|
3593
|
+
const retainedSizes = [];
|
|
3594
|
+
let retainedTotal = 0;
|
|
3595
|
+
let removedCount = 0;
|
|
3596
|
+
for (let i = 0; i < originalRecords.length; i++) {
|
|
3597
|
+
const record = originalRecords[i];
|
|
3598
|
+
if (itemsToProcess.has(record)) {
|
|
3599
|
+
removedCount++;
|
|
3600
|
+
continue;
|
|
3601
|
+
}
|
|
3602
|
+
retainedRecords.push(record);
|
|
3603
|
+
if (originalOffsets && i < originalOffsets.length) {
|
|
3604
|
+
retainedOffsets.push(originalOffsets[i]);
|
|
3605
|
+
}
|
|
3606
|
+
if (originalSizes && i < originalSizes.length) {
|
|
3607
|
+
const size = originalSizes[i];
|
|
3608
|
+
if (size !== undefined) {
|
|
3609
|
+
retainedSizes.push(size);
|
|
3610
|
+
retainedTotal += size;
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
if (removedCount > 0) {
|
|
3615
|
+
this.writeBuffer = retainedRecords;
|
|
3616
|
+
this.writeBufferOffsets = retainedOffsets;
|
|
3617
|
+
this.writeBufferSizes = retainedSizes;
|
|
3618
|
+
this.writeBufferTotalSize = retainedTotal;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
2662
3621
|
const afterLength = this.writeBuffer.length;
|
|
3622
|
+
if (afterLength === 0) {
|
|
3623
|
+
this.writeBufferOffsets = [];
|
|
3624
|
+
this.writeBufferSizes = [];
|
|
3625
|
+
this.writeBufferTotalSize = 0;
|
|
3626
|
+
}
|
|
2663
3627
|
if (this.opts.debugMode && beforeLength !== afterLength) {
|
|
2664
3628
|
console.log(`💾 _processWriteBuffer: Removed ${beforeLength - afterLength} items from writeBuffer (${beforeLength} -> ${afterLength})`);
|
|
2665
3629
|
}
|
|
@@ -3231,28 +4195,239 @@ class Database extends _events.EventEmitter {
|
|
|
3231
4195
|
}
|
|
3232
4196
|
|
|
3233
4197
|
/**
|
|
3234
|
-
*
|
|
4198
|
+
* Get the base line number for writeBuffer entries (number of persisted records)
|
|
4199
|
+
* @private
|
|
4200
|
+
*/
|
|
4201
|
+
_getWriteBufferBaseLineNumber() {
|
|
4202
|
+
return Array.isArray(this.offsets) ? this.offsets.length : 0;
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
/**
|
|
4206
|
+
* Convert a writeBuffer index into an absolute line number
|
|
4207
|
+
* @param {number} writeBufferIndex - Index inside writeBuffer (0-based)
|
|
4208
|
+
* @returns {number} Absolute line number (0-based)
|
|
4209
|
+
* @private
|
|
3235
4210
|
*/
|
|
3236
|
-
|
|
4211
|
+
_getAbsoluteLineNumber(writeBufferIndex) {
|
|
4212
|
+
if (typeof writeBufferIndex !== 'number' || writeBufferIndex < 0) {
|
|
4213
|
+
throw new Error('Invalid writeBuffer index');
|
|
4214
|
+
}
|
|
4215
|
+
return this._getWriteBufferBaseLineNumber() + writeBufferIndex;
|
|
4216
|
+
}
|
|
4217
|
+
_streamingRecoveryGenerator(_x, _x2) {
|
|
3237
4218
|
var _this = this;
|
|
4219
|
+
return _wrapAsyncGenerator(function* (criteria, options, alreadyYielded = 0, map = null, remainingSkipValue = 0) {
|
|
4220
|
+
if (_this._offsetRecoveryInProgress) {
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
if (!_this.fileHandler || !_this.fileHandler.file) {
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
_this._offsetRecoveryInProgress = true;
|
|
4227
|
+
const fsModule = _this._fsModule || (_this._fsModule = yield _awaitAsyncGenerator(Promise.resolve().then(() => _interopRequireWildcard(require('fs')))));
|
|
4228
|
+
let fd;
|
|
4229
|
+
try {
|
|
4230
|
+
fd = yield _awaitAsyncGenerator(fsModule.promises.open(_this.fileHandler.file, 'r'));
|
|
4231
|
+
} catch (error) {
|
|
4232
|
+
_this._offsetRecoveryInProgress = false;
|
|
4233
|
+
if (_this.opts.debugMode) {
|
|
4234
|
+
console.warn(`⚠️ Offset recovery skipped: ${error.message}`);
|
|
4235
|
+
}
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
const chunkSize = _this.opts.offsetRecoveryChunkSize || 64 * 1024;
|
|
4239
|
+
let buffer = Buffer.alloc(0);
|
|
4240
|
+
let readOffset = 0;
|
|
4241
|
+
const originalOffsets = Array.isArray(_this.offsets) ? [..._this.offsets] : [];
|
|
4242
|
+
const newOffsets = [];
|
|
4243
|
+
let offsetAdjusted = false;
|
|
4244
|
+
let limitReached = false;
|
|
4245
|
+
let lineIndex = 0;
|
|
4246
|
+
let lastLineEnd = 0;
|
|
4247
|
+
let producedTotal = alreadyYielded || 0;
|
|
4248
|
+
let remainingSkip = remainingSkipValue || 0;
|
|
4249
|
+
let remainingAlreadyYielded = alreadyYielded || 0;
|
|
4250
|
+
const limit = typeof options?.limit === 'number' ? options.limit : null;
|
|
4251
|
+
const includeOffsets = options?.includeOffsets === true;
|
|
4252
|
+
const includeLinePosition = _this.opts.includeLinePosition;
|
|
4253
|
+
const mapSet = map instanceof Set ? new Set(map) : Array.isArray(map) ? new Set(map) : null;
|
|
4254
|
+
const criteriaIsObject = criteria && typeof criteria === 'object' && !Array.isArray(criteria) && !(criteria instanceof Set);
|
|
4255
|
+
const hasCriteria = criteriaIsObject && Object.keys(criteria).length > 0;
|
|
4256
|
+
const decodeLineBuffer = lineBuffer => {
|
|
4257
|
+
let trimmed = lineBuffer;
|
|
4258
|
+
if (trimmed.length > 0 && trimmed[trimmed.length - 1] === 0x0A) {
|
|
4259
|
+
trimmed = trimmed.subarray(0, trimmed.length - 1);
|
|
4260
|
+
}
|
|
4261
|
+
if (trimmed.length > 0 && trimmed[trimmed.length - 1] === 0x0D) {
|
|
4262
|
+
trimmed = trimmed.subarray(0, trimmed.length - 1);
|
|
4263
|
+
}
|
|
4264
|
+
return trimmed;
|
|
4265
|
+
};
|
|
4266
|
+
const processLine = async (lineBuffer, lineStart) => {
|
|
4267
|
+
const lineLength = lineBuffer.length;
|
|
4268
|
+
newOffsets[lineIndex] = lineStart;
|
|
4269
|
+
const expected = originalOffsets[lineIndex];
|
|
4270
|
+
if (expected !== undefined && expected !== lineStart) {
|
|
4271
|
+
offsetAdjusted = true;
|
|
4272
|
+
if (_this.opts.debugMode) {
|
|
4273
|
+
console.warn(`⚠️ Offset mismatch detected at line ${lineIndex}: expected ${expected}, actual ${lineStart}`);
|
|
4274
|
+
}
|
|
4275
|
+
} else if (expected === undefined) {
|
|
4276
|
+
offsetAdjusted = true;
|
|
4277
|
+
}
|
|
4278
|
+
lastLineEnd = Math.max(lastLineEnd, lineStart + lineLength);
|
|
4279
|
+
let entryWithTerms = null;
|
|
4280
|
+
let shouldYield = false;
|
|
4281
|
+
const decodedBuffer = decodeLineBuffer(lineBuffer);
|
|
4282
|
+
if (decodedBuffer.length > 0) {
|
|
4283
|
+
let lineString;
|
|
4284
|
+
try {
|
|
4285
|
+
lineString = decodedBuffer.toString('utf8');
|
|
4286
|
+
} catch (error) {
|
|
4287
|
+
lineString = decodedBuffer.toString('utf8', {
|
|
4288
|
+
replacement: '?'
|
|
4289
|
+
});
|
|
4290
|
+
}
|
|
4291
|
+
try {
|
|
4292
|
+
const record = await _this.serializer.deserialize(lineString);
|
|
4293
|
+
if (record && typeof record === 'object') {
|
|
4294
|
+
entryWithTerms = _this.restoreTermIdsAfterDeserialization(record);
|
|
4295
|
+
if (includeLinePosition) {
|
|
4296
|
+
entryWithTerms._ = lineIndex;
|
|
4297
|
+
}
|
|
4298
|
+
if (mapSet) {
|
|
4299
|
+
shouldYield = mapSet.has(lineIndex);
|
|
4300
|
+
if (shouldYield) {
|
|
4301
|
+
mapSet.delete(lineIndex);
|
|
4302
|
+
}
|
|
4303
|
+
} else if (hasCriteria) {
|
|
4304
|
+
shouldYield = _this.queryManager.matchesCriteria(entryWithTerms, criteria, options);
|
|
4305
|
+
} else {
|
|
4306
|
+
shouldYield = true;
|
|
4307
|
+
}
|
|
4308
|
+
}
|
|
4309
|
+
} catch (error) {
|
|
4310
|
+
if (_this.opts.debugMode) {
|
|
4311
|
+
console.warn(`⚠️ Offset recovery failed to deserialize line ${lineIndex} at ${lineStart}: ${error.message}`);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
let yieldedEntry = null;
|
|
4316
|
+
if (shouldYield && entryWithTerms) {
|
|
4317
|
+
if (remainingSkip > 0) {
|
|
4318
|
+
remainingSkip--;
|
|
4319
|
+
} else if (remainingAlreadyYielded > 0) {
|
|
4320
|
+
remainingAlreadyYielded--;
|
|
4321
|
+
} else if (!limit || producedTotal < limit) {
|
|
4322
|
+
producedTotal++;
|
|
4323
|
+
yieldedEntry = includeOffsets ? {
|
|
4324
|
+
entry: entryWithTerms,
|
|
4325
|
+
start: lineStart,
|
|
4326
|
+
_: lineIndex
|
|
4327
|
+
} : entryWithTerms;
|
|
4328
|
+
} else {
|
|
4329
|
+
limitReached = true;
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
lineIndex++;
|
|
4333
|
+
if (yieldedEntry) {
|
|
4334
|
+
return yieldedEntry;
|
|
4335
|
+
}
|
|
4336
|
+
return null;
|
|
4337
|
+
};
|
|
4338
|
+
let recoveryFailed = false;
|
|
4339
|
+
try {
|
|
4340
|
+
while (true) {
|
|
4341
|
+
const tempBuffer = Buffer.allocUnsafe(chunkSize);
|
|
4342
|
+
const {
|
|
4343
|
+
bytesRead
|
|
4344
|
+
} = yield _awaitAsyncGenerator(fd.read(tempBuffer, 0, chunkSize, readOffset));
|
|
4345
|
+
if (bytesRead === 0) {
|
|
4346
|
+
if (buffer.length > 0) {
|
|
4347
|
+
const lineStart = readOffset - buffer.length;
|
|
4348
|
+
const yieldedEntry = yield _awaitAsyncGenerator(processLine(buffer, lineStart));
|
|
4349
|
+
if (yieldedEntry) {
|
|
4350
|
+
yield yieldedEntry;
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
break;
|
|
4354
|
+
}
|
|
4355
|
+
readOffset += bytesRead;
|
|
4356
|
+
let chunk = buffer.length > 0 ? Buffer.concat([buffer, tempBuffer.subarray(0, bytesRead)]) : tempBuffer.subarray(0, bytesRead);
|
|
4357
|
+
let processedUpTo = 0;
|
|
4358
|
+
const chunkBaseOffset = readOffset - chunk.length;
|
|
4359
|
+
while (true) {
|
|
4360
|
+
const newlineIndex = chunk.indexOf(0x0A, processedUpTo);
|
|
4361
|
+
if (newlineIndex === -1) {
|
|
4362
|
+
break;
|
|
4363
|
+
}
|
|
4364
|
+
const lineBuffer = chunk.subarray(processedUpTo, newlineIndex + 1);
|
|
4365
|
+
const lineStart = chunkBaseOffset + processedUpTo;
|
|
4366
|
+
const yieldedEntry = yield _awaitAsyncGenerator(processLine(lineBuffer, lineStart));
|
|
4367
|
+
processedUpTo = newlineIndex + 1;
|
|
4368
|
+
if (yieldedEntry) {
|
|
4369
|
+
yield yieldedEntry;
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
buffer = chunk.subarray(processedUpTo);
|
|
4373
|
+
}
|
|
4374
|
+
} catch (error) {
|
|
4375
|
+
recoveryFailed = true;
|
|
4376
|
+
if (_this.opts.debugMode) {
|
|
4377
|
+
console.warn(`⚠️ Offset recovery aborted: ${error.message}`);
|
|
4378
|
+
}
|
|
4379
|
+
} finally {
|
|
4380
|
+
yield _awaitAsyncGenerator(fd.close().catch(() => {}));
|
|
4381
|
+
_this._offsetRecoveryInProgress = false;
|
|
4382
|
+
if (recoveryFailed) {
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
_this.offsets = newOffsets;
|
|
4386
|
+
if (lineIndex < _this.offsets.length) {
|
|
4387
|
+
_this.offsets.length = lineIndex;
|
|
4388
|
+
}
|
|
4389
|
+
if (originalOffsets.length !== newOffsets.length) {
|
|
4390
|
+
offsetAdjusted = true;
|
|
4391
|
+
}
|
|
4392
|
+
_this.indexOffset = lastLineEnd;
|
|
4393
|
+
if (offsetAdjusted) {
|
|
4394
|
+
_this.shouldSave = true;
|
|
4395
|
+
try {
|
|
4396
|
+
yield _awaitAsyncGenerator(_this._saveIndexDataToFile());
|
|
4397
|
+
} catch (error) {
|
|
4398
|
+
if (_this.opts.debugMode) {
|
|
4399
|
+
console.warn(`⚠️ Failed to persist recovered offsets: ${error.message}`);
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
}).apply(this, arguments);
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
/**
|
|
4408
|
+
* Walk through records using streaming (real implementation)
|
|
4409
|
+
*/
|
|
4410
|
+
walk(_x3) {
|
|
4411
|
+
var _this2 = this;
|
|
3238
4412
|
return _wrapAsyncGenerator(function* (criteria, options = {}) {
|
|
3239
4413
|
// CRITICAL FIX: Validate state before walk operation to prevent crashes
|
|
3240
|
-
|
|
3241
|
-
if (!
|
|
4414
|
+
_this2.validateState();
|
|
4415
|
+
if (!_this2.initialized) yield _awaitAsyncGenerator(_this2.init());
|
|
3242
4416
|
|
|
3243
4417
|
// If no data at all, return empty
|
|
3244
|
-
if (
|
|
4418
|
+
if (_this2.indexOffset === 0 && _this2.writeBuffer.length === 0) return;
|
|
4419
|
+
let count = 0;
|
|
4420
|
+
let remainingSkip = options.skip || 0;
|
|
3245
4421
|
let map;
|
|
3246
4422
|
if (!Array.isArray(criteria)) {
|
|
3247
4423
|
if (criteria instanceof Set) {
|
|
3248
4424
|
map = [...criteria];
|
|
3249
4425
|
} else if (criteria && typeof criteria === 'object' && Object.keys(criteria).length > 0) {
|
|
3250
4426
|
// Only use indexManager.query if criteria has actual filters
|
|
3251
|
-
map = [...
|
|
4427
|
+
map = [..._this2.indexManager.query(criteria, options)];
|
|
3252
4428
|
} else {
|
|
3253
4429
|
// For empty criteria {} or null/undefined, get all records
|
|
3254
|
-
|
|
3255
|
-
const totalRecords = _this.indexOffset > 0 ? _this.indexOffset : _this.writeBuffer.length;
|
|
4430
|
+
const totalRecords = _this2.offsets && _this2.offsets.length > 0 ? _this2.offsets.length : _this2.writeBuffer.length;
|
|
3256
4431
|
map = [...Array(totalRecords).keys()];
|
|
3257
4432
|
}
|
|
3258
4433
|
} else {
|
|
@@ -3260,17 +4435,21 @@ class Database extends _events.EventEmitter {
|
|
|
3260
4435
|
}
|
|
3261
4436
|
|
|
3262
4437
|
// Use writeBuffer when available (unsaved data)
|
|
3263
|
-
if (
|
|
4438
|
+
if (_this2.writeBuffer.length > 0) {
|
|
3264
4439
|
let count = 0;
|
|
3265
4440
|
|
|
3266
4441
|
// If map is empty (no index results) but we have criteria, filter writeBuffer directly
|
|
3267
4442
|
if (map.length === 0 && criteria && typeof criteria === 'object' && Object.keys(criteria).length > 0) {
|
|
3268
|
-
for (let i = 0; i <
|
|
4443
|
+
for (let i = 0; i < _this2.writeBuffer.length; i++) {
|
|
3269
4444
|
if (options.limit && count >= options.limit) {
|
|
3270
4445
|
break;
|
|
3271
4446
|
}
|
|
3272
|
-
const entry =
|
|
3273
|
-
if (entry &&
|
|
4447
|
+
const entry = _this2.writeBuffer[i];
|
|
4448
|
+
if (entry && _this2.queryManager.matchesCriteria(entry, criteria, options)) {
|
|
4449
|
+
if (remainingSkip > 0) {
|
|
4450
|
+
remainingSkip--;
|
|
4451
|
+
continue;
|
|
4452
|
+
}
|
|
3274
4453
|
count++;
|
|
3275
4454
|
if (options.includeOffsets) {
|
|
3276
4455
|
yield {
|
|
@@ -3279,7 +4458,7 @@ class Database extends _events.EventEmitter {
|
|
|
3279
4458
|
_: i
|
|
3280
4459
|
};
|
|
3281
4460
|
} else {
|
|
3282
|
-
if (
|
|
4461
|
+
if (_this2.opts.includeLinePosition) {
|
|
3283
4462
|
entry._ = i;
|
|
3284
4463
|
}
|
|
3285
4464
|
yield entry;
|
|
@@ -3292,9 +4471,13 @@ class Database extends _events.EventEmitter {
|
|
|
3292
4471
|
if (options.limit && count >= options.limit) {
|
|
3293
4472
|
break;
|
|
3294
4473
|
}
|
|
3295
|
-
if (lineNumber <
|
|
3296
|
-
const entry =
|
|
4474
|
+
if (lineNumber < _this2.writeBuffer.length) {
|
|
4475
|
+
const entry = _this2.writeBuffer[lineNumber];
|
|
3297
4476
|
if (entry) {
|
|
4477
|
+
if (remainingSkip > 0) {
|
|
4478
|
+
remainingSkip--;
|
|
4479
|
+
continue;
|
|
4480
|
+
}
|
|
3298
4481
|
count++;
|
|
3299
4482
|
if (options.includeOffsets) {
|
|
3300
4483
|
yield {
|
|
@@ -3303,7 +4486,7 @@ class Database extends _events.EventEmitter {
|
|
|
3303
4486
|
_: lineNumber
|
|
3304
4487
|
};
|
|
3305
4488
|
} else {
|
|
3306
|
-
if (
|
|
4489
|
+
if (_this2.opts.includeLinePosition) {
|
|
3307
4490
|
entry._ = lineNumber;
|
|
3308
4491
|
}
|
|
3309
4492
|
yield entry;
|
|
@@ -3316,50 +4499,151 @@ class Database extends _events.EventEmitter {
|
|
|
3316
4499
|
}
|
|
3317
4500
|
|
|
3318
4501
|
// If writeBuffer is empty but we have saved data, we need to load it from file
|
|
3319
|
-
if (
|
|
4502
|
+
if (_this2.writeBuffer.length === 0 && _this2.indexOffset > 0) {
|
|
3320
4503
|
// Load data from file for querying
|
|
3321
4504
|
try {
|
|
3322
4505
|
let data;
|
|
3323
4506
|
let lines;
|
|
3324
4507
|
|
|
3325
4508
|
// Smart threshold: decide between partial reads vs full read
|
|
3326
|
-
const resultPercentage = map ? map.length /
|
|
3327
|
-
const threshold =
|
|
4509
|
+
const resultPercentage = map ? map.length / _this2.indexOffset * 100 : 100;
|
|
4510
|
+
const threshold = _this2.opts.partialReadThreshold || 60; // Default 60% threshold
|
|
3328
4511
|
|
|
3329
4512
|
// Use partial reads when:
|
|
3330
4513
|
// 1. We have specific line numbers from index
|
|
3331
4514
|
// 2. Results are below threshold percentage
|
|
3332
4515
|
// 3. Database is large enough to benefit from partial reads
|
|
3333
|
-
const shouldUsePartialReads = map && map.length > 0 && resultPercentage < threshold &&
|
|
4516
|
+
const shouldUsePartialReads = map && map.length > 0 && resultPercentage < threshold && _this2.indexOffset > 100; // Only for databases with >100 records
|
|
3334
4517
|
|
|
3335
4518
|
if (shouldUsePartialReads) {
|
|
3336
|
-
if (
|
|
3337
|
-
console.log(`🔍 Using PARTIAL READS: ${map.length}/${
|
|
4519
|
+
if (_this2.opts.debugMode) {
|
|
4520
|
+
console.log(`🔍 Using PARTIAL READS: ${map.length}/${_this2.indexOffset} records (${resultPercentage.toFixed(1)}% < ${threshold}% threshold)`);
|
|
3338
4521
|
}
|
|
3339
|
-
//
|
|
3340
|
-
const
|
|
3341
|
-
|
|
3342
|
-
|
|
4522
|
+
// OPTIMIZATION: Use ranges instead of reading entire file
|
|
4523
|
+
const ranges = _this2.getRanges(map);
|
|
4524
|
+
const groupedRanges = yield _awaitAsyncGenerator(_this2.fileHandler.groupedRanges(ranges));
|
|
4525
|
+
const fs = yield _awaitAsyncGenerator(Promise.resolve().then(() => _interopRequireWildcard(require('fs'))));
|
|
4526
|
+
const fd = yield _awaitAsyncGenerator(fs.promises.open(_this2.fileHandler.file, 'r'));
|
|
4527
|
+
try {
|
|
4528
|
+
for (const groupedRange of groupedRanges) {
|
|
4529
|
+
var _iteratorAbruptCompletion4 = false;
|
|
4530
|
+
var _didIteratorError4 = false;
|
|
4531
|
+
var _iteratorError4;
|
|
4532
|
+
try {
|
|
4533
|
+
for (var _iterator4 = _asyncIterator(_this2.fileHandler.readGroupedRange(groupedRange, fd)), _step4; _iteratorAbruptCompletion4 = !(_step4 = yield _awaitAsyncGenerator(_iterator4.next())).done; _iteratorAbruptCompletion4 = false) {
|
|
4534
|
+
const row = _step4.value;
|
|
4535
|
+
{
|
|
4536
|
+
if (options.limit && count >= options.limit) {
|
|
4537
|
+
break;
|
|
4538
|
+
}
|
|
4539
|
+
try {
|
|
4540
|
+
// CRITICAL FIX: Use serializer.deserialize instead of JSON.parse to handle array format
|
|
4541
|
+
const record = _this2.serializer.deserialize(row.line);
|
|
4542
|
+
// SPACE OPTIMIZATION: Restore term IDs to terms for user
|
|
4543
|
+
const recordWithTerms = _this2.restoreTermIdsAfterDeserialization(record);
|
|
4544
|
+
if (remainingSkip > 0) {
|
|
4545
|
+
remainingSkip--;
|
|
4546
|
+
continue;
|
|
4547
|
+
}
|
|
4548
|
+
count++;
|
|
4549
|
+
if (options.includeOffsets) {
|
|
4550
|
+
yield {
|
|
4551
|
+
entry: recordWithTerms,
|
|
4552
|
+
start: row.start,
|
|
4553
|
+
_: row._ || 0
|
|
4554
|
+
};
|
|
4555
|
+
} else {
|
|
4556
|
+
if (_this2.opts.includeLinePosition) {
|
|
4557
|
+
recordWithTerms._ = row._ || 0;
|
|
4558
|
+
}
|
|
4559
|
+
yield recordWithTerms;
|
|
4560
|
+
}
|
|
4561
|
+
} catch (error) {
|
|
4562
|
+
// CRITICAL FIX: Log deserialization errors instead of silently ignoring them
|
|
4563
|
+
// This helps identify data corruption issues
|
|
4564
|
+
if (1 || _this2.opts.debugMode) {
|
|
4565
|
+
console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`);
|
|
4566
|
+
console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`);
|
|
4567
|
+
}
|
|
4568
|
+
if (!_this2._offsetRecoveryInProgress) {
|
|
4569
|
+
var _iteratorAbruptCompletion5 = false;
|
|
4570
|
+
var _didIteratorError5 = false;
|
|
4571
|
+
var _iteratorError5;
|
|
4572
|
+
try {
|
|
4573
|
+
for (var _iterator5 = _asyncIterator(_this2._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)), _step5; _iteratorAbruptCompletion5 = !(_step5 = yield _awaitAsyncGenerator(_iterator5.next())).done; _iteratorAbruptCompletion5 = false) {
|
|
4574
|
+
const recoveredEntry = _step5.value;
|
|
4575
|
+
{
|
|
4576
|
+
yield recoveredEntry;
|
|
4577
|
+
count++;
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
} catch (err) {
|
|
4581
|
+
_didIteratorError5 = true;
|
|
4582
|
+
_iteratorError5 = err;
|
|
4583
|
+
} finally {
|
|
4584
|
+
try {
|
|
4585
|
+
if (_iteratorAbruptCompletion5 && _iterator5.return != null) {
|
|
4586
|
+
yield _awaitAsyncGenerator(_iterator5.return());
|
|
4587
|
+
}
|
|
4588
|
+
} finally {
|
|
4589
|
+
if (_didIteratorError5) {
|
|
4590
|
+
throw _iteratorError5;
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
// Skip invalid lines but continue processing
|
|
4597
|
+
// This prevents one corrupted record from stopping the entire walk operation
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
} catch (err) {
|
|
4602
|
+
_didIteratorError4 = true;
|
|
4603
|
+
_iteratorError4 = err;
|
|
4604
|
+
} finally {
|
|
4605
|
+
try {
|
|
4606
|
+
if (_iteratorAbruptCompletion4 && _iterator4.return != null) {
|
|
4607
|
+
yield _awaitAsyncGenerator(_iterator4.return());
|
|
4608
|
+
}
|
|
4609
|
+
} finally {
|
|
4610
|
+
if (_didIteratorError4) {
|
|
4611
|
+
throw _iteratorError4;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
if (options.limit && count >= options.limit) {
|
|
4616
|
+
break;
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
} finally {
|
|
4620
|
+
yield _awaitAsyncGenerator(fd.close());
|
|
4621
|
+
}
|
|
4622
|
+
return; // Exit early since we processed partial reads
|
|
3343
4623
|
} else {
|
|
3344
|
-
if (
|
|
3345
|
-
console.log(`🔍 Using STREAMING READ: ${map?.length || 0}/${
|
|
4624
|
+
if (_this2.opts.debugMode) {
|
|
4625
|
+
console.log(`🔍 Using STREAMING READ: ${map?.length || 0}/${_this2.indexOffset} records (${resultPercentage.toFixed(1)}% >= ${threshold}% threshold or small DB)`);
|
|
3346
4626
|
}
|
|
3347
4627
|
// Use streaming instead of loading all data in memory
|
|
3348
4628
|
// This prevents memory issues with large databases
|
|
3349
|
-
const streamingResults = yield _awaitAsyncGenerator(
|
|
4629
|
+
const streamingResults = yield _awaitAsyncGenerator(_this2.fileHandler.readWithStreaming(criteria, {
|
|
3350
4630
|
limit: options.limit,
|
|
3351
4631
|
skip: options.skip
|
|
3352
|
-
}, matchesCriteria,
|
|
4632
|
+
}, matchesCriteria, _this2.serializer));
|
|
3353
4633
|
|
|
3354
4634
|
// Process streaming results directly without loading all lines
|
|
3355
4635
|
for (const record of streamingResults) {
|
|
3356
4636
|
if (options.limit && count >= options.limit) {
|
|
3357
4637
|
break;
|
|
3358
4638
|
}
|
|
4639
|
+
if (remainingSkip > 0) {
|
|
4640
|
+
remainingSkip--;
|
|
4641
|
+
continue;
|
|
4642
|
+
}
|
|
3359
4643
|
count++;
|
|
3360
4644
|
|
|
3361
4645
|
// SPACE OPTIMIZATION: Restore term IDs to terms for user
|
|
3362
|
-
const recordWithTerms =
|
|
4646
|
+
const recordWithTerms = _this2.restoreTermIdsAfterDeserialization(record);
|
|
3363
4647
|
if (options.includeOffsets) {
|
|
3364
4648
|
yield {
|
|
3365
4649
|
entry: recordWithTerms,
|
|
@@ -3367,7 +4651,7 @@ class Database extends _events.EventEmitter {
|
|
|
3367
4651
|
_: 0
|
|
3368
4652
|
};
|
|
3369
4653
|
} else {
|
|
3370
|
-
if (
|
|
4654
|
+
if (_this2.opts.includeLinePosition) {
|
|
3371
4655
|
recordWithTerms._ = 0;
|
|
3372
4656
|
}
|
|
3373
4657
|
yield recordWithTerms;
|
|
@@ -3375,136 +4659,108 @@ class Database extends _events.EventEmitter {
|
|
|
3375
4659
|
}
|
|
3376
4660
|
return; // Exit early since we processed streaming results
|
|
3377
4661
|
}
|
|
3378
|
-
if (lines.length > 0) {
|
|
3379
|
-
const records = [];
|
|
3380
|
-
for (const line of lines) {
|
|
3381
|
-
if (line.trim()) {
|
|
3382
|
-
try {
|
|
3383
|
-
// CRITICAL FIX: Use serializer.deserialize instead of JSON.parse to handle array format
|
|
3384
|
-
const record = _this.serializer.deserialize(line);
|
|
3385
|
-
// SPACE OPTIMIZATION: Restore term IDs to terms for user
|
|
3386
|
-
const recordWithTerms = _this.restoreTermIdsAfterDeserialization(record);
|
|
3387
|
-
records.push(recordWithTerms);
|
|
3388
|
-
} catch (error) {
|
|
3389
|
-
// Skip invalid lines
|
|
3390
|
-
}
|
|
3391
|
-
}
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
// Use loaded records for querying
|
|
3395
|
-
let count = 0;
|
|
3396
|
-
|
|
3397
|
-
// When using partial reads, records correspond to the requested line numbers
|
|
3398
|
-
if (shouldUsePartialReads) {
|
|
3399
|
-
for (let i = 0; i < Math.min(records.length, map.length); i++) {
|
|
3400
|
-
if (options.limit && count >= options.limit) {
|
|
3401
|
-
break;
|
|
3402
|
-
}
|
|
3403
|
-
const entry = records[i];
|
|
3404
|
-
const lineNumber = map[i];
|
|
3405
|
-
if (entry) {
|
|
3406
|
-
count++;
|
|
3407
|
-
if (options.includeOffsets) {
|
|
3408
|
-
yield {
|
|
3409
|
-
entry,
|
|
3410
|
-
start: 0,
|
|
3411
|
-
_: lineNumber
|
|
3412
|
-
};
|
|
3413
|
-
} else {
|
|
3414
|
-
if (_this.opts.includeLinePosition) {
|
|
3415
|
-
entry._ = lineNumber;
|
|
3416
|
-
}
|
|
3417
|
-
yield entry;
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
}
|
|
3421
|
-
} else {
|
|
3422
|
-
// Fallback to original logic when reading all data
|
|
3423
|
-
for (const lineNumber of map) {
|
|
3424
|
-
if (options.limit && count >= options.limit) {
|
|
3425
|
-
break;
|
|
3426
|
-
}
|
|
3427
|
-
if (lineNumber < records.length) {
|
|
3428
|
-
const entry = records[lineNumber];
|
|
3429
|
-
if (entry) {
|
|
3430
|
-
count++;
|
|
3431
|
-
if (options.includeOffsets) {
|
|
3432
|
-
yield {
|
|
3433
|
-
entry,
|
|
3434
|
-
start: 0,
|
|
3435
|
-
_: lineNumber
|
|
3436
|
-
};
|
|
3437
|
-
} else {
|
|
3438
|
-
if (_this.opts.includeLinePosition) {
|
|
3439
|
-
entry._ = lineNumber;
|
|
3440
|
-
}
|
|
3441
|
-
yield entry;
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
}
|
|
3445
|
-
}
|
|
3446
|
-
}
|
|
3447
|
-
return;
|
|
3448
|
-
}
|
|
3449
4662
|
} catch (error) {
|
|
3450
4663
|
// If file reading fails, continue to file-based streaming
|
|
3451
4664
|
}
|
|
3452
4665
|
}
|
|
3453
4666
|
|
|
3454
4667
|
// Use file-based streaming for saved data
|
|
3455
|
-
const ranges =
|
|
3456
|
-
const groupedRanges = yield _awaitAsyncGenerator(
|
|
3457
|
-
const fd = yield _awaitAsyncGenerator(_fs.default.promises.open(
|
|
4668
|
+
const ranges = _this2.getRanges(map);
|
|
4669
|
+
const groupedRanges = yield _awaitAsyncGenerator(_this2.fileHandler.groupedRanges(ranges));
|
|
4670
|
+
const fd = yield _awaitAsyncGenerator(_fs.default.promises.open(_this2.fileHandler.file, 'r'));
|
|
3458
4671
|
try {
|
|
3459
4672
|
let count = 0;
|
|
3460
4673
|
for (const groupedRange of groupedRanges) {
|
|
3461
4674
|
if (options.limit && count >= options.limit) {
|
|
3462
4675
|
break;
|
|
3463
4676
|
}
|
|
3464
|
-
var
|
|
3465
|
-
var
|
|
3466
|
-
var
|
|
4677
|
+
var _iteratorAbruptCompletion6 = false;
|
|
4678
|
+
var _didIteratorError6 = false;
|
|
4679
|
+
var _iteratorError6;
|
|
3467
4680
|
try {
|
|
3468
|
-
for (var
|
|
3469
|
-
const row =
|
|
4681
|
+
for (var _iterator6 = _asyncIterator(_this2.fileHandler.readGroupedRange(groupedRange, fd)), _step6; _iteratorAbruptCompletion6 = !(_step6 = yield _awaitAsyncGenerator(_iterator6.next())).done; _iteratorAbruptCompletion6 = false) {
|
|
4682
|
+
const row = _step6.value;
|
|
3470
4683
|
{
|
|
3471
4684
|
if (options.limit && count >= options.limit) {
|
|
3472
4685
|
break;
|
|
3473
4686
|
}
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
4687
|
+
try {
|
|
4688
|
+
const entry = yield _awaitAsyncGenerator(_this2.serializer.deserialize(row.line, {
|
|
4689
|
+
compress: _this2.opts.compress,
|
|
4690
|
+
v8: _this2.opts.v8
|
|
4691
|
+
}));
|
|
4692
|
+
if (entry === null) continue;
|
|
4693
|
+
|
|
4694
|
+
// SPACE OPTIMIZATION: Restore term IDs to terms for user
|
|
4695
|
+
const entryWithTerms = _this2.restoreTermIdsAfterDeserialization(entry);
|
|
4696
|
+
if (remainingSkip > 0) {
|
|
4697
|
+
remainingSkip--;
|
|
4698
|
+
continue;
|
|
4699
|
+
}
|
|
4700
|
+
count++;
|
|
4701
|
+
if (options.includeOffsets) {
|
|
4702
|
+
yield {
|
|
4703
|
+
entry: entryWithTerms,
|
|
4704
|
+
start: row.start,
|
|
4705
|
+
_: row._ || _this2.offsets.findIndex(n => n === row.start)
|
|
4706
|
+
};
|
|
4707
|
+
} else {
|
|
4708
|
+
if (_this2.opts.includeLinePosition) {
|
|
4709
|
+
entryWithTerms._ = row._ || _this2.offsets.findIndex(n => n === row.start);
|
|
4710
|
+
}
|
|
4711
|
+
yield entryWithTerms;
|
|
4712
|
+
}
|
|
4713
|
+
} catch (error) {
|
|
4714
|
+
// CRITICAL FIX: Log deserialization errors instead of silently ignoring them
|
|
4715
|
+
// This helps identify data corruption issues
|
|
4716
|
+
if (1 || _this2.opts.debugMode) {
|
|
4717
|
+
console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`);
|
|
4718
|
+
console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`);
|
|
4719
|
+
}
|
|
4720
|
+
if (!_this2._offsetRecoveryInProgress) {
|
|
4721
|
+
var _iteratorAbruptCompletion7 = false;
|
|
4722
|
+
var _didIteratorError7 = false;
|
|
4723
|
+
var _iteratorError7;
|
|
4724
|
+
try {
|
|
4725
|
+
for (var _iterator7 = _asyncIterator(_this2._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)), _step7; _iteratorAbruptCompletion7 = !(_step7 = yield _awaitAsyncGenerator(_iterator7.next())).done; _iteratorAbruptCompletion7 = false) {
|
|
4726
|
+
const recoveredEntry = _step7.value;
|
|
4727
|
+
{
|
|
4728
|
+
yield recoveredEntry;
|
|
4729
|
+
count++;
|
|
4730
|
+
}
|
|
4731
|
+
}
|
|
4732
|
+
} catch (err) {
|
|
4733
|
+
_didIteratorError7 = true;
|
|
4734
|
+
_iteratorError7 = err;
|
|
4735
|
+
} finally {
|
|
4736
|
+
try {
|
|
4737
|
+
if (_iteratorAbruptCompletion7 && _iterator7.return != null) {
|
|
4738
|
+
yield _awaitAsyncGenerator(_iterator7.return());
|
|
4739
|
+
}
|
|
4740
|
+
} finally {
|
|
4741
|
+
if (_didIteratorError7) {
|
|
4742
|
+
throw _iteratorError7;
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
return;
|
|
3492
4747
|
}
|
|
3493
|
-
|
|
4748
|
+
// Skip invalid lines but continue processing
|
|
4749
|
+
// This prevents one corrupted record from stopping the entire walk operation
|
|
3494
4750
|
}
|
|
3495
4751
|
}
|
|
3496
4752
|
}
|
|
3497
4753
|
} catch (err) {
|
|
3498
|
-
|
|
3499
|
-
|
|
4754
|
+
_didIteratorError6 = true;
|
|
4755
|
+
_iteratorError6 = err;
|
|
3500
4756
|
} finally {
|
|
3501
4757
|
try {
|
|
3502
|
-
if (
|
|
3503
|
-
yield _awaitAsyncGenerator(
|
|
4758
|
+
if (_iteratorAbruptCompletion6 && _iterator6.return != null) {
|
|
4759
|
+
yield _awaitAsyncGenerator(_iterator6.return());
|
|
3504
4760
|
}
|
|
3505
4761
|
} finally {
|
|
3506
|
-
if (
|
|
3507
|
-
throw
|
|
4762
|
+
if (_didIteratorError6) {
|
|
4763
|
+
throw _iteratorError6;
|
|
3508
4764
|
}
|
|
3509
4765
|
}
|
|
3510
4766
|
}
|
|
@@ -3528,12 +4784,12 @@ class Database extends _events.EventEmitter {
|
|
|
3528
4784
|
* @param {boolean} options.detectChanges - Auto-detect changes (default: true)
|
|
3529
4785
|
* @returns {AsyncGenerator} Generator yielding records for modification
|
|
3530
4786
|
*/
|
|
3531
|
-
iterate(
|
|
3532
|
-
var
|
|
4787
|
+
iterate(_x4) {
|
|
4788
|
+
var _this3 = this;
|
|
3533
4789
|
return _wrapAsyncGenerator(function* (criteria, options = {}) {
|
|
3534
4790
|
// CRITICAL FIX: Validate state before iterate operation
|
|
3535
|
-
|
|
3536
|
-
if (!
|
|
4791
|
+
_this3.validateState();
|
|
4792
|
+
if (!_this3.initialized) yield _awaitAsyncGenerator(_this3.init());
|
|
3537
4793
|
|
|
3538
4794
|
// Set default options
|
|
3539
4795
|
const opts = {
|
|
@@ -3546,7 +4802,7 @@ class Database extends _events.EventEmitter {
|
|
|
3546
4802
|
};
|
|
3547
4803
|
|
|
3548
4804
|
// If no data, return empty
|
|
3549
|
-
if (
|
|
4805
|
+
if (_this3.indexOffset === 0 && _this3.writeBuffer.length === 0) return;
|
|
3550
4806
|
const startTime = Date.now();
|
|
3551
4807
|
let processedCount = 0;
|
|
3552
4808
|
let modifiedCount = 0;
|
|
@@ -3559,24 +4815,24 @@ class Database extends _events.EventEmitter {
|
|
|
3559
4815
|
|
|
3560
4816
|
try {
|
|
3561
4817
|
// Always use walk() now that the bug is fixed - it works for both small and large datasets
|
|
3562
|
-
var
|
|
3563
|
-
var
|
|
3564
|
-
var
|
|
4818
|
+
var _iteratorAbruptCompletion8 = false;
|
|
4819
|
+
var _didIteratorError8 = false;
|
|
4820
|
+
var _iteratorError8;
|
|
3565
4821
|
try {
|
|
3566
|
-
for (var
|
|
3567
|
-
const entry =
|
|
4822
|
+
for (var _iterator8 = _asyncIterator(_this3.walk(criteria, options)), _step8; _iteratorAbruptCompletion8 = !(_step8 = yield _awaitAsyncGenerator(_iterator8.next())).done; _iteratorAbruptCompletion8 = false) {
|
|
4823
|
+
const entry = _step8.value;
|
|
3568
4824
|
{
|
|
3569
4825
|
processedCount++;
|
|
3570
4826
|
|
|
3571
4827
|
// Store original record for change detection BEFORE yielding
|
|
3572
4828
|
let originalRecord = null;
|
|
3573
4829
|
if (opts.detectChanges) {
|
|
3574
|
-
originalRecord =
|
|
4830
|
+
originalRecord = _this3._createShallowCopy(entry);
|
|
3575
4831
|
originalRecords.set(entry.id, originalRecord);
|
|
3576
4832
|
}
|
|
3577
4833
|
|
|
3578
4834
|
// Create wrapper based on performance preference
|
|
3579
|
-
const entryWrapper = opts.highPerformance ?
|
|
4835
|
+
const entryWrapper = opts.highPerformance ? _this3._createHighPerformanceWrapper(entry, originalRecord) : _this3._createEntryProxy(entry, originalRecord);
|
|
3580
4836
|
|
|
3581
4837
|
// Yield the wrapper for user modification
|
|
3582
4838
|
yield entryWrapper;
|
|
@@ -3590,7 +4846,7 @@ class Database extends _events.EventEmitter {
|
|
|
3590
4846
|
}
|
|
3591
4847
|
} else if (opts.detectChanges && originalRecord) {
|
|
3592
4848
|
// Check if entry was modified by comparing with original (optimized comparison)
|
|
3593
|
-
if (
|
|
4849
|
+
if (_this3._hasRecordChanged(entry, originalRecord)) {
|
|
3594
4850
|
updateBuffer.push(entry);
|
|
3595
4851
|
modifiedCount++;
|
|
3596
4852
|
}
|
|
@@ -3602,7 +4858,7 @@ class Database extends _events.EventEmitter {
|
|
|
3602
4858
|
|
|
3603
4859
|
// Process batch when chunk size is reached
|
|
3604
4860
|
if (updateBuffer.length >= opts.chunkSize || deleteBuffer.size >= opts.chunkSize) {
|
|
3605
|
-
yield _awaitAsyncGenerator(
|
|
4861
|
+
yield _awaitAsyncGenerator(_this3._processIterateBatch(updateBuffer, deleteBuffer, opts));
|
|
3606
4862
|
|
|
3607
4863
|
// Clear buffers
|
|
3608
4864
|
updateBuffer.length = 0;
|
|
@@ -3624,21 +4880,21 @@ class Database extends _events.EventEmitter {
|
|
|
3624
4880
|
|
|
3625
4881
|
// Process remaining records in buffers
|
|
3626
4882
|
} catch (err) {
|
|
3627
|
-
|
|
3628
|
-
|
|
4883
|
+
_didIteratorError8 = true;
|
|
4884
|
+
_iteratorError8 = err;
|
|
3629
4885
|
} finally {
|
|
3630
4886
|
try {
|
|
3631
|
-
if (
|
|
3632
|
-
yield _awaitAsyncGenerator(
|
|
4887
|
+
if (_iteratorAbruptCompletion8 && _iterator8.return != null) {
|
|
4888
|
+
yield _awaitAsyncGenerator(_iterator8.return());
|
|
3633
4889
|
}
|
|
3634
4890
|
} finally {
|
|
3635
|
-
if (
|
|
3636
|
-
throw
|
|
4891
|
+
if (_didIteratorError8) {
|
|
4892
|
+
throw _iteratorError8;
|
|
3637
4893
|
}
|
|
3638
4894
|
}
|
|
3639
4895
|
}
|
|
3640
4896
|
if (updateBuffer.length > 0 || deleteBuffer.size > 0) {
|
|
3641
|
-
yield _awaitAsyncGenerator(
|
|
4897
|
+
yield _awaitAsyncGenerator(_this3._processIterateBatch(updateBuffer, deleteBuffer, opts));
|
|
3642
4898
|
}
|
|
3643
4899
|
|
|
3644
4900
|
// Final progress callback (always called)
|
|
@@ -3651,7 +4907,7 @@ class Database extends _events.EventEmitter {
|
|
|
3651
4907
|
completed: true
|
|
3652
4908
|
});
|
|
3653
4909
|
}
|
|
3654
|
-
if (
|
|
4910
|
+
if (_this3.opts.debugMode) {
|
|
3655
4911
|
console.log(`🔄 ITERATE COMPLETED: ${processedCount} processed, ${modifiedCount} modified, ${deletedCount} deleted in ${Date.now() - startTime}ms`);
|
|
3656
4912
|
}
|
|
3657
4913
|
} catch (error) {
|
|
@@ -3677,16 +4933,20 @@ class Database extends _events.EventEmitter {
|
|
|
3677
4933
|
|
|
3678
4934
|
// Update record in writeBuffer or add to writeBuffer
|
|
3679
4935
|
const index = this.writeBuffer.findIndex(r => r.id === record.id);
|
|
4936
|
+
let targetIndex;
|
|
3680
4937
|
if (index !== -1) {
|
|
3681
4938
|
// Record is already in writeBuffer, update it
|
|
3682
4939
|
this.writeBuffer[index] = record;
|
|
4940
|
+
targetIndex = index;
|
|
3683
4941
|
} else {
|
|
3684
4942
|
// Record is in file, add updated version to writeBuffer
|
|
3685
4943
|
this.writeBuffer.push(record);
|
|
4944
|
+
targetIndex = this.writeBuffer.length - 1;
|
|
3686
4945
|
}
|
|
3687
4946
|
|
|
3688
4947
|
// Update index
|
|
3689
|
-
|
|
4948
|
+
const absoluteLineNumber = this._getAbsoluteLineNumber(targetIndex);
|
|
4949
|
+
await this.indexManager.update(record, record, absoluteLineNumber);
|
|
3690
4950
|
}
|
|
3691
4951
|
if (this.opts.debugMode) {
|
|
3692
4952
|
console.log(`🔄 ITERATE: Updated ${updateBuffer.length} records in ${Date.now() - startTime}ms`);
|
|
@@ -3755,8 +5015,24 @@ class Database extends _events.EventEmitter {
|
|
|
3755
5015
|
this.writeBufferSizes = [];
|
|
3756
5016
|
}
|
|
3757
5017
|
} else {
|
|
3758
|
-
//
|
|
3759
|
-
|
|
5018
|
+
// Only save index data if it actually has content
|
|
5019
|
+
// Don't overwrite a valid index with an empty one
|
|
5020
|
+
if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
5021
|
+
let hasIndexData = false;
|
|
5022
|
+
for (const field of this.indexManager.indexedFields) {
|
|
5023
|
+
if (this.indexManager.hasUsableIndexData(field)) {
|
|
5024
|
+
hasIndexData = true;
|
|
5025
|
+
break;
|
|
5026
|
+
}
|
|
5027
|
+
}
|
|
5028
|
+
// Only save if we have actual index data OR if offsets are populated
|
|
5029
|
+
// (offsets being populated means we've processed data)
|
|
5030
|
+
if (hasIndexData || this.offsets && this.offsets.length > 0) {
|
|
5031
|
+
await this._saveIndexDataToFile();
|
|
5032
|
+
} else if (this.opts.debugMode) {
|
|
5033
|
+
console.log('⚠️ close(): Skipping index save - index is empty and no offsets');
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
3760
5036
|
}
|
|
3761
5037
|
|
|
3762
5038
|
// 2. Mark as closed (but not destroyed) to allow reopening
|
|
@@ -3790,8 +5066,40 @@ class Database extends _events.EventEmitter {
|
|
|
3790
5066
|
if (this.indexManager) {
|
|
3791
5067
|
try {
|
|
3792
5068
|
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb');
|
|
5069
|
+
const indexJSON = this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {};
|
|
5070
|
+
|
|
5071
|
+
// Check if index is empty
|
|
5072
|
+
const isEmpty = !indexJSON || Object.keys(indexJSON).length === 0 || this.indexManager.indexedFields && this.indexManager.indexedFields.every(field => {
|
|
5073
|
+
const fieldIndex = indexJSON[field];
|
|
5074
|
+
return !fieldIndex || typeof fieldIndex === 'object' && Object.keys(fieldIndex).length === 0;
|
|
5075
|
+
});
|
|
5076
|
+
|
|
5077
|
+
// PROTECTION: Don't overwrite a valid index file with empty data
|
|
5078
|
+
// If the .idx.jdb file exists and has data, and we're trying to save empty index,
|
|
5079
|
+
// skip the save to prevent corruption
|
|
5080
|
+
if (isEmpty && !this.offsets?.length) {
|
|
5081
|
+
const fs = await Promise.resolve().then(() => _interopRequireWildcard(require('fs')));
|
|
5082
|
+
if (fs.existsSync(idxPath)) {
|
|
5083
|
+
try {
|
|
5084
|
+
const existingData = JSON.parse(await fs.promises.readFile(idxPath, 'utf8'));
|
|
5085
|
+
const existingHasData = existingData.index && Object.keys(existingData.index).length > 0;
|
|
5086
|
+
const existingHasOffsets = existingData.offsets && existingData.offsets.length > 0;
|
|
5087
|
+
if (existingHasData || existingHasOffsets) {
|
|
5088
|
+
if (this.opts.debugMode) {
|
|
5089
|
+
console.log(`⚠️ _saveIndexDataToFile: Skipping save - would overwrite valid index with empty data`);
|
|
5090
|
+
}
|
|
5091
|
+
return; // Don't overwrite valid index with empty one
|
|
5092
|
+
}
|
|
5093
|
+
} catch (error) {
|
|
5094
|
+
// If we can't read existing file, proceed with save (might be corrupted)
|
|
5095
|
+
if (this.opts.debugMode) {
|
|
5096
|
+
console.log(`⚠️ _saveIndexDataToFile: Could not read existing index file, proceeding with save`);
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
3793
5101
|
const indexData = {
|
|
3794
|
-
index:
|
|
5102
|
+
index: indexJSON,
|
|
3795
5103
|
offsets: this.offsets,
|
|
3796
5104
|
// Save actual offsets for efficient file operations
|
|
3797
5105
|
indexOffset: this.indexOffset,
|