jexidb 2.1.3 → 2.1.5
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/README.md +339 -191
- package/dist/Database.cjs +713 -137
- package/package.json +4 -7
- package/src/Database.mjs +435 -75
- package/src/FileHandler.mjs +235 -33
- package/src/SchemaManager.mjs +3 -31
- package/src/Serializer.mjs +65 -8
- package/src/managers/IndexManager.mjs +15 -4
- package/src/managers/QueryManager.mjs +3 -3
package/src/Database.mjs
CHANGED
|
@@ -403,7 +403,19 @@ class Database extends EventEmitter {
|
|
|
403
403
|
saveTime: 0,
|
|
404
404
|
loadTime: 0
|
|
405
405
|
}
|
|
406
|
-
|
|
406
|
+
|
|
407
|
+
// Initialize integrity correction tracking
|
|
408
|
+
this.integrityCorrections = {
|
|
409
|
+
indexSync: 0, // index.totalLines vs offsets.length corrections
|
|
410
|
+
indexInconsistency: 0, // Index record count vs offsets mismatch
|
|
411
|
+
writeBufferForced: 0, // WriteBuffer not cleared after save
|
|
412
|
+
indexSaveFailures: 0, // Failed to save index data
|
|
413
|
+
dataIntegrity: 0, // General data integrity issues
|
|
414
|
+
utf8Recovery: 0, // UTF-8 decoding failures recovered
|
|
415
|
+
jsonRecovery: 0 // JSON parsing failures recovered
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
407
419
|
// Initialize usage stats for QueryManager
|
|
408
420
|
this.usageStats = {
|
|
409
421
|
totalQueries: 0,
|
|
@@ -596,7 +608,9 @@ class Database extends EventEmitter {
|
|
|
596
608
|
}
|
|
597
609
|
}
|
|
598
610
|
if (arrayStringFields.length > 0) {
|
|
599
|
-
|
|
611
|
+
if (this.opts.debugMode) {
|
|
612
|
+
console.warn(`⚠️ Warning: The following array:string indexed fields were not added to term mapping: ${arrayStringFields.join(', ')}. This may impact performance.`)
|
|
613
|
+
}
|
|
600
614
|
}
|
|
601
615
|
}
|
|
602
616
|
|
|
@@ -638,22 +652,27 @@ class Database extends EventEmitter {
|
|
|
638
652
|
}
|
|
639
653
|
|
|
640
654
|
/**
|
|
641
|
-
* Get term mapping fields from indexes (auto-detected)
|
|
655
|
+
* Get term mapping fields from configuration or indexes (auto-detected)
|
|
642
656
|
* @returns {string[]} Array of field names that use term mapping
|
|
643
657
|
*/
|
|
644
658
|
getTermMappingFields() {
|
|
659
|
+
// If termMappingFields is explicitly configured, use it
|
|
660
|
+
if (this.opts.termMappingFields && Array.isArray(this.opts.termMappingFields)) {
|
|
661
|
+
return [...this.opts.termMappingFields]
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Auto-detect fields that benefit from term mapping from indexes
|
|
645
665
|
if (!this.opts.indexes) return []
|
|
646
|
-
|
|
647
|
-
// Auto-detect fields that benefit from term mapping
|
|
666
|
+
|
|
648
667
|
const termMappingFields = []
|
|
649
|
-
|
|
668
|
+
|
|
650
669
|
for (const [field, type] of Object.entries(this.opts.indexes)) {
|
|
651
670
|
// Fields that should use term mapping (only array fields)
|
|
652
671
|
if (type === 'array:string') {
|
|
653
672
|
termMappingFields.push(field)
|
|
654
673
|
}
|
|
655
674
|
}
|
|
656
|
-
|
|
675
|
+
|
|
657
676
|
return termMappingFields
|
|
658
677
|
}
|
|
659
678
|
|
|
@@ -745,7 +764,7 @@ class Database extends EventEmitter {
|
|
|
745
764
|
|
|
746
765
|
// Reset closed state when reinitializing
|
|
747
766
|
this.closed = false
|
|
748
|
-
|
|
767
|
+
|
|
749
768
|
// Initialize managers (protected against double initialization)
|
|
750
769
|
this.initializeManagers()
|
|
751
770
|
|
|
@@ -768,12 +787,33 @@ class Database extends EventEmitter {
|
|
|
768
787
|
await this.load()
|
|
769
788
|
}
|
|
770
789
|
}
|
|
771
|
-
|
|
790
|
+
|
|
791
|
+
// CRITICAL INTEGRITY CHECK: Ensure IndexManager is consistent with loaded offsets
|
|
792
|
+
// This must happen immediately after load() to prevent any subsequent operations from seeing inconsistent state
|
|
793
|
+
if (this.indexManager && this.offsets && this.offsets.length > 0) {
|
|
794
|
+
const currentTotalLines = this.indexManager.totalLines || 0
|
|
795
|
+
if (currentTotalLines !== this.offsets.length) {
|
|
796
|
+
this.indexManager.setTotalLines(this.offsets.length)
|
|
797
|
+
if (this.opts.debugMode) {
|
|
798
|
+
console.log(`🔧 Post-load integrity sync: IndexManager totalLines ${currentTotalLines} → ${this.offsets.length}`)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
772
803
|
// Manual save is now the default behavior
|
|
773
|
-
|
|
804
|
+
|
|
805
|
+
// CRITICAL FIX: Ensure IndexManager totalLines is consistent with offsets
|
|
806
|
+
// This prevents data integrity issues when database is initialized without existing data
|
|
807
|
+
if (this.indexManager && this.offsets) {
|
|
808
|
+
this.indexManager.setTotalLines(this.offsets.length)
|
|
809
|
+
if (this.opts.debugMode) {
|
|
810
|
+
console.log(`🔧 Initialized index totalLines to ${this.offsets.length}`)
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
774
814
|
this.initialized = true
|
|
775
815
|
this.emit('initialized')
|
|
776
|
-
|
|
816
|
+
|
|
777
817
|
if (this.opts.debugMode) {
|
|
778
818
|
console.log(`✅ Database initialized with ${this.writeBuffer.length} records`)
|
|
779
819
|
}
|
|
@@ -927,11 +967,11 @@ class Database extends EventEmitter {
|
|
|
927
967
|
this.offsets = parsedIdxData.offsets
|
|
928
968
|
// CRITICAL FIX: Update IndexManager totalLines to match offsets length
|
|
929
969
|
// This ensures queries and length property work correctly even if offsets are reset later
|
|
930
|
-
if (this.indexManager
|
|
970
|
+
if (this.indexManager) {
|
|
931
971
|
this.indexManager.setTotalLines(this.offsets.length)
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
972
|
+
if (this.opts.debugMode) {
|
|
973
|
+
console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}, synced IndexManager totalLines`)
|
|
974
|
+
}
|
|
935
975
|
}
|
|
936
976
|
}
|
|
937
977
|
|
|
@@ -1606,6 +1646,7 @@ class Database extends EventEmitter {
|
|
|
1606
1646
|
|
|
1607
1647
|
if (this.opts.debugMode) {
|
|
1608
1648
|
console.log(`💾 Save: allData.length=${allData.length}, cleanedData.length=${cleanedData.length}`)
|
|
1649
|
+
console.log(`💾 Save: Current offsets.length before recalculation: ${this.offsets.length}`)
|
|
1609
1650
|
console.log(`💾 Save: All records in allData before serialization:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
|
|
1610
1651
|
console.log(`💾 Save: Sample cleaned record:`, cleanedData[0] ? Object.keys(cleanedData[0]) : 'null')
|
|
1611
1652
|
}
|
|
@@ -1624,6 +1665,8 @@ class Database extends EventEmitter {
|
|
|
1624
1665
|
}
|
|
1625
1666
|
}
|
|
1626
1667
|
|
|
1668
|
+
// CRITICAL FIX: Always recalculate offsets from serialized data to ensure consistency
|
|
1669
|
+
// Even if _streamExistingRecords updated offsets, we need to recalculate based on actual serialized data
|
|
1627
1670
|
this.offsets = []
|
|
1628
1671
|
let currentOffset = 0
|
|
1629
1672
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1634,6 +1677,10 @@ class Database extends EventEmitter {
|
|
|
1634
1677
|
currentOffset += Buffer.byteLength(lineWithNewline, 'utf8')
|
|
1635
1678
|
}
|
|
1636
1679
|
|
|
1680
|
+
if (this.opts.debugMode) {
|
|
1681
|
+
console.log(`💾 Save: Recalculated offsets.length=${this.offsets.length}, should match lines.length=${lines.length}`)
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1637
1684
|
// CRITICAL FIX: Ensure indexOffset matches actual file size
|
|
1638
1685
|
this.indexOffset = currentOffset
|
|
1639
1686
|
|
|
@@ -1655,11 +1702,15 @@ class Database extends EventEmitter {
|
|
|
1655
1702
|
this.shouldSave = false
|
|
1656
1703
|
this.lastSaveTime = Date.now()
|
|
1657
1704
|
|
|
1658
|
-
//
|
|
1659
|
-
if
|
|
1660
|
-
// Rebuild index when records were deleted or updated to maintain consistency
|
|
1705
|
+
// CRITICAL FIX: Always clear deletedIds and rebuild index if there were deletions,
|
|
1706
|
+
// even if allData.length === 0 (all records were deleted)
|
|
1661
1707
|
const hadDeletedRecords = deletedIdsSnapshot.size > 0
|
|
1662
1708
|
const hadUpdatedRecords = writeBufferSnapshot.length > 0
|
|
1709
|
+
|
|
1710
|
+
// Clear writeBuffer and deletedIds after successful save
|
|
1711
|
+
// Also rebuild index if records were deleted or updated, even if allData is empty
|
|
1712
|
+
if (allData.length > 0 || hadDeletedRecords || hadUpdatedRecords) {
|
|
1713
|
+
// Rebuild index when records were deleted or updated to maintain consistency
|
|
1663
1714
|
if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
1664
1715
|
if (hadDeletedRecords || hadUpdatedRecords) {
|
|
1665
1716
|
// Clear the index and rebuild it from the saved records
|
|
@@ -1713,8 +1764,26 @@ class Database extends EventEmitter {
|
|
|
1713
1764
|
|
|
1714
1765
|
await this.indexManager.add(record, i)
|
|
1715
1766
|
}
|
|
1767
|
+
|
|
1768
|
+
// VALIDATION: Ensure index consistency after rebuild
|
|
1769
|
+
// Check that all indexed records have valid line numbers
|
|
1770
|
+
const indexedRecordCount = this.indexManager.getIndexedRecordCount?.() || allData.length
|
|
1771
|
+
if (indexedRecordCount !== this.offsets.length) {
|
|
1772
|
+
this.integrityCorrections.indexInconsistency++
|
|
1773
|
+
console.log(`🔧 Auto-corrected index consistency: ${indexedRecordCount} indexed → ${this.offsets.length} offsets`)
|
|
1774
|
+
|
|
1775
|
+
if (this.integrityCorrections.indexInconsistency > 5) {
|
|
1776
|
+
console.warn(`⚠️ Frequent index inconsistencies detected (${this.integrityCorrections.indexInconsistency} times)`)
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// Force consistency by setting totalLines to match offsets
|
|
1780
|
+
this.indexManager.setTotalLines(this.offsets.length)
|
|
1781
|
+
} else {
|
|
1782
|
+
this.indexManager.setTotalLines(this.offsets.length)
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1716
1785
|
if (this.opts.debugMode) {
|
|
1717
|
-
console.log(`💾 Save: Index rebuilt with ${allData.length} records`)
|
|
1786
|
+
console.log(`💾 Save: Index rebuilt with ${allData.length} records, totalLines set to ${this.offsets.length}`)
|
|
1718
1787
|
}
|
|
1719
1788
|
}
|
|
1720
1789
|
}
|
|
@@ -1738,6 +1807,22 @@ class Database extends EventEmitter {
|
|
|
1738
1807
|
for (const deletedId of deletedIdsSnapshot) {
|
|
1739
1808
|
this.deletedIds.delete(deletedId)
|
|
1740
1809
|
}
|
|
1810
|
+
} else if (hadDeletedRecords) {
|
|
1811
|
+
// CRITICAL FIX: Even if allData is empty, clear deletedIds and rebuild index
|
|
1812
|
+
// when records were deleted to ensure consistency
|
|
1813
|
+
if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
1814
|
+
// Clear the index since all records were deleted
|
|
1815
|
+
this.indexManager.clear()
|
|
1816
|
+
this.indexManager.setTotalLines(0)
|
|
1817
|
+
if (this.opts.debugMode) {
|
|
1818
|
+
console.log(`🧹 Cleared index after removing all ${deletedIdsSnapshot.size} deleted records`)
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Clear deletedIds even when allData is empty
|
|
1823
|
+
for (const deletedId of deletedIdsSnapshot) {
|
|
1824
|
+
this.deletedIds.delete(deletedId)
|
|
1825
|
+
}
|
|
1741
1826
|
|
|
1742
1827
|
// CRITICAL FIX: Ensure writeBuffer is completely cleared after successful save
|
|
1743
1828
|
if (this.writeBuffer.length > 0) {
|
|
@@ -2244,7 +2329,7 @@ class Database extends EventEmitter {
|
|
|
2244
2329
|
*/
|
|
2245
2330
|
async find(criteria = {}, options = {}) {
|
|
2246
2331
|
this._validateInitialization('find')
|
|
2247
|
-
|
|
2332
|
+
|
|
2248
2333
|
// CRITICAL FIX: Validate state before find operation
|
|
2249
2334
|
this.validateState()
|
|
2250
2335
|
|
|
@@ -2257,6 +2342,38 @@ class Database extends EventEmitter {
|
|
|
2257
2342
|
}
|
|
2258
2343
|
|
|
2259
2344
|
try {
|
|
2345
|
+
// INTEGRITY CHECK: Validate data consistency before querying
|
|
2346
|
+
// This is a safety net for unexpected inconsistencies - should rarely trigger
|
|
2347
|
+
if (this.indexManager && this.offsets && this.offsets.length > 0) {
|
|
2348
|
+
const indexTotalLines = this.indexManager.totalLines || 0
|
|
2349
|
+
const offsetsLength = this.offsets.length
|
|
2350
|
+
|
|
2351
|
+
if (indexTotalLines !== offsetsLength) {
|
|
2352
|
+
// This should be extremely rare - indicates a real bug if it happens frequently
|
|
2353
|
+
this.integrityCorrections.dataIntegrity++
|
|
2354
|
+
|
|
2355
|
+
// Only show in debug mode - these corrections indicate real issues
|
|
2356
|
+
if (this.opts.debugMode) {
|
|
2357
|
+
console.log(`🔧 Integrity correction needed: index.totalLines ${indexTotalLines} → ${offsetsLength} (${this.integrityCorrections.dataIntegrity} total)`)
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// Warn if corrections are becoming frequent (indicates a real problem)
|
|
2361
|
+
if (this.integrityCorrections.dataIntegrity > 5) {
|
|
2362
|
+
console.warn(`⚠️ Frequent integrity corrections (${this.integrityCorrections.dataIntegrity} times) - this indicates a systemic issue`)
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
this.indexManager.setTotalLines(offsetsLength)
|
|
2366
|
+
|
|
2367
|
+
// Try to persist the fix, but don't fail the operation if it doesn't work
|
|
2368
|
+
try {
|
|
2369
|
+
await this._saveIndexDataToFile()
|
|
2370
|
+
} catch (error) {
|
|
2371
|
+
// Just track the failure - don't throw since this is a safety net
|
|
2372
|
+
this.integrityCorrections.indexSaveFailures++
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2260
2377
|
// Validate indexed query mode if enabled
|
|
2261
2378
|
if (this.opts.indexedQueryMode === 'strict') {
|
|
2262
2379
|
this._validateIndexedQuery(criteria, options)
|
|
@@ -2279,36 +2396,25 @@ class Database extends EventEmitter {
|
|
|
2279
2396
|
|
|
2280
2397
|
|
|
2281
2398
|
// Combine results, removing duplicates (writeBuffer takes precedence)
|
|
2282
|
-
// OPTIMIZATION:
|
|
2399
|
+
// OPTIMIZATION: Unified efficient approach with consistent precedence rules
|
|
2283
2400
|
let allResults
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2401
|
+
|
|
2402
|
+
// Create efficient lookup map for writeBuffer records
|
|
2403
|
+
const writeBufferMap = new Map()
|
|
2404
|
+
writeBufferResultsWithTerms.forEach(record => {
|
|
2405
|
+
if (record && record.id) {
|
|
2406
|
+
writeBufferMap.set(record.id, record)
|
|
2407
|
+
}
|
|
2408
|
+
})
|
|
2290
2409
|
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
)
|
|
2410
|
+
// Filter file results to exclude any records that exist in writeBuffer
|
|
2411
|
+
// This ensures writeBuffer always takes precedence
|
|
2412
|
+
const filteredFileResults = fileResultsWithTerms.filter(record =>
|
|
2413
|
+
record && record.id && !writeBufferMap.has(record.id)
|
|
2414
|
+
)
|
|
2415
|
+
|
|
2416
|
+
// Combine results: file results (filtered) + all writeBuffer results
|
|
2295
2417
|
allResults = [...filteredFileResults, ...writeBufferResultsWithTerms]
|
|
2296
|
-
} else {
|
|
2297
|
-
// Sequential approach for small writeBuffer (original logic)
|
|
2298
|
-
allResults = [...fileResultsWithTerms]
|
|
2299
|
-
|
|
2300
|
-
// Replace file records with writeBuffer records and add new writeBuffer records
|
|
2301
|
-
for (const record of writeBufferResultsWithTerms) {
|
|
2302
|
-
const existingIndex = allResults.findIndex(r => r.id === record.id)
|
|
2303
|
-
if (existingIndex !== -1) {
|
|
2304
|
-
// Replace existing record with writeBuffer version
|
|
2305
|
-
allResults[existingIndex] = record
|
|
2306
|
-
} else {
|
|
2307
|
-
// Add new record from writeBuffer
|
|
2308
|
-
allResults.push(record)
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
2418
|
|
|
2313
2419
|
// Remove records that are marked as deleted
|
|
2314
2420
|
const finalResults = allResults.filter(record => !this.deletedIds.has(record.id))
|
|
@@ -2566,19 +2672,6 @@ class Database extends EventEmitter {
|
|
|
2566
2672
|
|
|
2567
2673
|
// CRITICAL FIX: Validate state before update operation
|
|
2568
2674
|
this.validateState()
|
|
2569
|
-
|
|
2570
|
-
// CRITICAL FIX: If there's data to save, call save() to persist it
|
|
2571
|
-
// Only save if there are actual records in writeBuffer
|
|
2572
|
-
if (this.shouldSave && this.writeBuffer.length > 0) {
|
|
2573
|
-
if (this.opts.debugMode) {
|
|
2574
|
-
console.log(`🔄 UPDATE: Calling save() before update - writeBuffer.length=${this.writeBuffer.length}`)
|
|
2575
|
-
}
|
|
2576
|
-
const saveStart = Date.now()
|
|
2577
|
-
await this.save(false) // Use save(false) since we're already in queue
|
|
2578
|
-
if (this.opts.debugMode) {
|
|
2579
|
-
console.log(`🔄 UPDATE: Save completed in ${Date.now() - saveStart}ms`)
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
2675
|
|
|
2583
2676
|
if (this.opts.debugMode) {
|
|
2584
2677
|
console.log(`🔄 UPDATE: Starting find() - writeBuffer=${this.writeBuffer.length}`)
|
|
@@ -2591,7 +2684,12 @@ class Database extends EventEmitter {
|
|
|
2591
2684
|
}
|
|
2592
2685
|
|
|
2593
2686
|
const updatedRecords = []
|
|
2594
|
-
|
|
2687
|
+
|
|
2688
|
+
if (this.opts.debugMode) {
|
|
2689
|
+
console.log(`🔄 UPDATE: About to process ${records.length} records`)
|
|
2690
|
+
console.log(`🔄 UPDATE: Records:`, records.map(r => ({ id: r.id, value: r.value })))
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2595
2693
|
for (const record of records) {
|
|
2596
2694
|
const recordStart = Date.now()
|
|
2597
2695
|
if (this.opts.debugMode) {
|
|
@@ -2628,13 +2726,20 @@ class Database extends EventEmitter {
|
|
|
2628
2726
|
// For records in the file, we need to ensure they are properly marked for replacement
|
|
2629
2727
|
const index = this.writeBuffer.findIndex(r => r.id === record.id)
|
|
2630
2728
|
let lineNumber = null
|
|
2631
|
-
|
|
2729
|
+
|
|
2730
|
+
if (this.opts.debugMode) {
|
|
2731
|
+
console.log(`🔄 UPDATE: writeBuffer.findIndex for ${record.id} returned ${index}`)
|
|
2732
|
+
console.log(`🔄 UPDATE: writeBuffer length: ${this.writeBuffer.length}`)
|
|
2733
|
+
console.log(`🔄 UPDATE: writeBuffer IDs:`, this.writeBuffer.map(r => r.id))
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2632
2736
|
if (index !== -1) {
|
|
2633
2737
|
// Record is already in writeBuffer, update it
|
|
2634
2738
|
this.writeBuffer[index] = updated
|
|
2635
2739
|
lineNumber = this._getAbsoluteLineNumber(index)
|
|
2636
2740
|
if (this.opts.debugMode) {
|
|
2637
2741
|
console.log(`🔄 UPDATE: Updated existing writeBuffer record at index ${index}`)
|
|
2742
|
+
console.log(`🔄 UPDATE: writeBuffer now has ${this.writeBuffer.length} records`)
|
|
2638
2743
|
}
|
|
2639
2744
|
} else {
|
|
2640
2745
|
// Record is in file, add updated version to writeBuffer
|
|
@@ -2644,6 +2749,7 @@ class Database extends EventEmitter {
|
|
|
2644
2749
|
lineNumber = this._getAbsoluteLineNumber(this.writeBuffer.length - 1)
|
|
2645
2750
|
if (this.opts.debugMode) {
|
|
2646
2751
|
console.log(`🔄 UPDATE: Added updated record to writeBuffer (will replace file record ${record.id})`)
|
|
2752
|
+
console.log(`🔄 UPDATE: writeBuffer now has ${this.writeBuffer.length} records`)
|
|
2647
2753
|
}
|
|
2648
2754
|
}
|
|
2649
2755
|
|
|
@@ -2679,13 +2785,32 @@ class Database extends EventEmitter {
|
|
|
2679
2785
|
*/
|
|
2680
2786
|
async delete(criteria) {
|
|
2681
2787
|
this._validateInitialization('delete')
|
|
2682
|
-
|
|
2788
|
+
|
|
2683
2789
|
return this.operationQueue.enqueue(async () => {
|
|
2684
2790
|
this.isInsideOperationQueue = true
|
|
2685
2791
|
try {
|
|
2686
2792
|
// CRITICAL FIX: Validate state before delete operation
|
|
2687
2793
|
this.validateState()
|
|
2688
|
-
|
|
2794
|
+
|
|
2795
|
+
// 🔧 NEW: Validate indexed query mode for delete operations
|
|
2796
|
+
if (this.opts.indexedQueryMode === 'strict') {
|
|
2797
|
+
this._validateIndexedQuery(criteria, { operation: 'delete' })
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// ⚠️ NEW: Warn about non-indexed fields in permissive mode
|
|
2801
|
+
if (this.opts.indexedQueryMode !== 'strict') {
|
|
2802
|
+
const indexedFields = Object.keys(this.opts.indexes || {})
|
|
2803
|
+
const queryFields = this._extractQueryFields(criteria)
|
|
2804
|
+
const nonIndexedFields = queryFields.filter(field => !indexedFields.includes(field))
|
|
2805
|
+
|
|
2806
|
+
if (nonIndexedFields.length > 0) {
|
|
2807
|
+
if (this.opts.debugMode) {
|
|
2808
|
+
console.warn(`⚠️ Delete operation using non-indexed fields: ${nonIndexedFields.join(', ')}`)
|
|
2809
|
+
console.warn(` This may be slow or fail silently. Consider indexing these fields.`)
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2689
2814
|
const records = await this.find(criteria)
|
|
2690
2815
|
const deletedIds = []
|
|
2691
2816
|
|
|
@@ -4289,14 +4414,30 @@ class Database extends EventEmitter {
|
|
|
4289
4414
|
try {
|
|
4290
4415
|
const arrayData = JSON.parse(trimmedLine)
|
|
4291
4416
|
if (Array.isArray(arrayData) && arrayData.length > 0) {
|
|
4292
|
-
//
|
|
4293
|
-
//
|
|
4417
|
+
// CRITICAL FIX: Use schema to find ID position, not hardcoded position
|
|
4418
|
+
// The schema defines the order of fields in the array
|
|
4419
|
+
if (this.serializer && this.serializer.schemaManager && this.serializer.schemaManager.isInitialized) {
|
|
4420
|
+
const schema = this.serializer.schemaManager.getSchema()
|
|
4421
|
+
const idIndex = schema.indexOf('id')
|
|
4422
|
+
if (idIndex !== -1 && arrayData.length > idIndex) {
|
|
4423
|
+
// ID is at the position defined by schema
|
|
4424
|
+
recordId = arrayData[idIndex]
|
|
4425
|
+
} else if (arrayData.length > schema.length) {
|
|
4426
|
+
// ID might be appended after schema fields (for backward compatibility)
|
|
4427
|
+
recordId = arrayData[schema.length]
|
|
4428
|
+
} else {
|
|
4429
|
+
// Fallback: use first element
|
|
4430
|
+
recordId = arrayData[0]
|
|
4431
|
+
}
|
|
4432
|
+
} else {
|
|
4433
|
+
// No schema available, try common positions
|
|
4294
4434
|
if (arrayData.length > 2) {
|
|
4295
|
-
|
|
4435
|
+
// Try position 2 (common in older formats)
|
|
4296
4436
|
recordId = arrayData[2]
|
|
4297
4437
|
} else {
|
|
4298
|
-
|
|
4438
|
+
// Fallback: use first element
|
|
4299
4439
|
recordId = arrayData[0]
|
|
4440
|
+
}
|
|
4300
4441
|
}
|
|
4301
4442
|
if (recordId !== undefined && recordId !== null) {
|
|
4302
4443
|
recordId = String(recordId)
|
|
@@ -4369,7 +4510,7 @@ class Database extends EventEmitter {
|
|
|
4369
4510
|
} else if (!deletedIdsSnapshot.has(String(recordWithIds.id))) {
|
|
4370
4511
|
// Keep existing record if not deleted
|
|
4371
4512
|
if (this.opts.debugMode) {
|
|
4372
|
-
console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`)
|
|
4513
|
+
console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - not in deletedIdsSnapshot`)
|
|
4373
4514
|
}
|
|
4374
4515
|
return {
|
|
4375
4516
|
type: 'kept',
|
|
@@ -4380,7 +4521,9 @@ class Database extends EventEmitter {
|
|
|
4380
4521
|
} else {
|
|
4381
4522
|
// Skip deleted record
|
|
4382
4523
|
if (this.opts.debugMode) {
|
|
4383
|
-
console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted`)
|
|
4524
|
+
console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted (found in deletedIdsSnapshot)`)
|
|
4525
|
+
console.log(`💾 Save: deletedIdsSnapshot contains:`, Array.from(deletedIdsSnapshot))
|
|
4526
|
+
console.log(`💾 Save: Record ID check: String(${recordWithIds.id}) = "${String(recordWithIds.id)}", has() = ${deletedIdsSnapshot.has(String(recordWithIds.id))}`)
|
|
4384
4527
|
}
|
|
4385
4528
|
return {
|
|
4386
4529
|
type: 'deleted',
|
|
@@ -4426,6 +4569,54 @@ class Database extends EventEmitter {
|
|
|
4426
4569
|
|
|
4427
4570
|
switch (result.type) {
|
|
4428
4571
|
case 'unchanged':
|
|
4572
|
+
// CRITICAL FIX: Verify that unchanged records are not deleted
|
|
4573
|
+
// Extract ID from the line to check against deletedIdsSnapshot
|
|
4574
|
+
let unchangedRecordId = null
|
|
4575
|
+
try {
|
|
4576
|
+
if (result.line.startsWith('[') && result.line.endsWith(']')) {
|
|
4577
|
+
const arrayData = JSON.parse(result.line)
|
|
4578
|
+
if (Array.isArray(arrayData) && arrayData.length > 0) {
|
|
4579
|
+
// CRITICAL FIX: Use schema to find ID position, not hardcoded position
|
|
4580
|
+
if (this.serializer && this.serializer.schemaManager && this.serializer.schemaManager.isInitialized) {
|
|
4581
|
+
const schema = this.serializer.schemaManager.getSchema()
|
|
4582
|
+
const idIndex = schema.indexOf('id')
|
|
4583
|
+
if (idIndex !== -1 && arrayData.length > idIndex) {
|
|
4584
|
+
unchangedRecordId = String(arrayData[idIndex])
|
|
4585
|
+
} else if (arrayData.length > schema.length) {
|
|
4586
|
+
unchangedRecordId = String(arrayData[schema.length])
|
|
4587
|
+
} else {
|
|
4588
|
+
unchangedRecordId = String(arrayData[0])
|
|
4589
|
+
}
|
|
4590
|
+
} else {
|
|
4591
|
+
// No schema, try common positions
|
|
4592
|
+
if (arrayData.length > 2) {
|
|
4593
|
+
unchangedRecordId = String(arrayData[2])
|
|
4594
|
+
} else {
|
|
4595
|
+
unchangedRecordId = String(arrayData[0])
|
|
4596
|
+
}
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
} else {
|
|
4600
|
+
const obj = JSON.parse(result.line)
|
|
4601
|
+
unchangedRecordId = obj.id ? String(obj.id) : null
|
|
4602
|
+
}
|
|
4603
|
+
} catch (e) {
|
|
4604
|
+
// If we can't parse, skip this record to be safe
|
|
4605
|
+
if (this.opts.debugMode) {
|
|
4606
|
+
console.log(`💾 Save: Could not parse unchanged record to check deletion: ${e.message}`)
|
|
4607
|
+
}
|
|
4608
|
+
continue
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
// Skip if this record is deleted
|
|
4612
|
+
if (unchangedRecordId && deletedIdsSnapshot.has(unchangedRecordId)) {
|
|
4613
|
+
if (this.opts.debugMode) {
|
|
4614
|
+
console.log(`💾 Save: Skipping unchanged record ${unchangedRecordId} - deleted`)
|
|
4615
|
+
}
|
|
4616
|
+
deletedOffsets.add(offset)
|
|
4617
|
+
break
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4429
4620
|
// Collect unchanged lines for batch processing
|
|
4430
4621
|
unchangedLines.push(result.line)
|
|
4431
4622
|
keptRecords.push({ offset, type: 'unchanged', line: result.line })
|
|
@@ -4620,6 +4811,78 @@ class Database extends EventEmitter {
|
|
|
4620
4811
|
return this._getWriteBufferBaseLineNumber() + writeBufferIndex
|
|
4621
4812
|
}
|
|
4622
4813
|
|
|
4814
|
+
|
|
4815
|
+
/**
|
|
4816
|
+
* Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
|
|
4817
|
+
* @param {string} line - The corrupted line to recover
|
|
4818
|
+
* @returns {string|null} - The recovered line or null if recovery is not possible
|
|
4819
|
+
*/
|
|
4820
|
+
_tryRecoverLine(line) {
|
|
4821
|
+
if (!line || typeof line !== 'string') {
|
|
4822
|
+
return null
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
try {
|
|
4826
|
+
// Try parsing as-is first
|
|
4827
|
+
JSON.parse(line)
|
|
4828
|
+
return line // Line is already valid
|
|
4829
|
+
} catch (e) {
|
|
4830
|
+
// Line is corrupted, attempt recovery
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
let recovered = line.trim()
|
|
4834
|
+
|
|
4835
|
+
// Remove invalid control characters (except \n, \r, \t)
|
|
4836
|
+
recovered = recovered.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
4837
|
+
|
|
4838
|
+
// Try to close unclosed strings
|
|
4839
|
+
// Count quotes and ensure they're balanced
|
|
4840
|
+
const quoteCount = (recovered.match(/"/g) || []).length
|
|
4841
|
+
if (quoteCount % 2 !== 0) {
|
|
4842
|
+
// Odd number of quotes - try to close the string
|
|
4843
|
+
const lastQuoteIndex = recovered.lastIndexOf('"')
|
|
4844
|
+
if (lastQuoteIndex > 0) {
|
|
4845
|
+
// Check if we're inside a string (not escaped)
|
|
4846
|
+
const beforeLastQuote = recovered.substring(0, lastQuoteIndex)
|
|
4847
|
+
const escapedQuotes = (beforeLastQuote.match(/\\"/g) || []).length
|
|
4848
|
+
const unescapedQuotes = (beforeLastQuote.match(/"/g) || []).length - escapedQuotes
|
|
4849
|
+
|
|
4850
|
+
if (unescapedQuotes % 2 !== 0) {
|
|
4851
|
+
// We're inside an unclosed string - try to close it
|
|
4852
|
+
recovered = recovered + '"'
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
// Try to close unclosed arrays/objects
|
|
4858
|
+
const openBraces = (recovered.match(/\{/g) || []).length
|
|
4859
|
+
const closeBraces = (recovered.match(/\}/g) || []).length
|
|
4860
|
+
const openBrackets = (recovered.match(/\[/g) || []).length
|
|
4861
|
+
const closeBrackets = (recovered.match(/\]/g) || []).length
|
|
4862
|
+
|
|
4863
|
+
// Remove trailing commas before closing braces/brackets
|
|
4864
|
+
recovered = recovered.replace(/,\s*([}\]])/g, '$1')
|
|
4865
|
+
|
|
4866
|
+
// Try to close arrays
|
|
4867
|
+
if (openBrackets > closeBrackets) {
|
|
4868
|
+
recovered = recovered + ']'.repeat(openBrackets - closeBrackets)
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
// Try to close objects
|
|
4872
|
+
if (openBraces > closeBraces) {
|
|
4873
|
+
recovered = recovered + '}'.repeat(openBraces - closeBraces)
|
|
4874
|
+
}
|
|
4875
|
+
|
|
4876
|
+
// Final validation - try to parse
|
|
4877
|
+
try {
|
|
4878
|
+
JSON.parse(recovered)
|
|
4879
|
+
return recovered
|
|
4880
|
+
} catch (e) {
|
|
4881
|
+
// Recovery failed
|
|
4882
|
+
return null
|
|
4883
|
+
}
|
|
4884
|
+
}
|
|
4885
|
+
|
|
4623
4886
|
async *_streamingRecoveryGenerator(criteria, options, alreadyYielded = 0, map = null, remainingSkipValue = 0) {
|
|
4624
4887
|
if (this._offsetRecoveryInProgress) {
|
|
4625
4888
|
return
|
|
@@ -4845,6 +5108,17 @@ class Database extends EventEmitter {
|
|
|
4845
5108
|
// If no data at all, return empty
|
|
4846
5109
|
if (this.indexOffset === 0 && this.writeBuffer.length === 0) return
|
|
4847
5110
|
|
|
5111
|
+
// CRITICAL FIX: Wait for any ongoing save operations to complete
|
|
5112
|
+
// This prevents reading partially written data
|
|
5113
|
+
if (this.isSaving) {
|
|
5114
|
+
if (this.opts.debugMode) {
|
|
5115
|
+
console.log('🔍 walk(): waiting for save operation to complete')
|
|
5116
|
+
}
|
|
5117
|
+
while (this.isSaving) {
|
|
5118
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
5119
|
+
}
|
|
5120
|
+
}
|
|
5121
|
+
|
|
4848
5122
|
let count = 0
|
|
4849
5123
|
let remainingSkip = options.skip || 0
|
|
4850
5124
|
|
|
@@ -4983,10 +5257,50 @@ class Database extends EventEmitter {
|
|
|
4983
5257
|
} catch (error) {
|
|
4984
5258
|
// CRITICAL FIX: Log deserialization errors instead of silently ignoring them
|
|
4985
5259
|
// This helps identify data corruption issues
|
|
4986
|
-
if (
|
|
5260
|
+
if (this.opts.debugMode) {
|
|
4987
5261
|
console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`)
|
|
4988
5262
|
console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`)
|
|
4989
5263
|
}
|
|
5264
|
+
|
|
5265
|
+
// CRITICAL FIX: Attempt to recover corrupted line before giving up
|
|
5266
|
+
const recoveredLine = this._tryRecoverLine(row.line)
|
|
5267
|
+
if (recoveredLine) {
|
|
5268
|
+
try {
|
|
5269
|
+
const record = this.serializer.deserialize(recoveredLine)
|
|
5270
|
+
if (record !== null) {
|
|
5271
|
+
this.integrityCorrections.jsonRecovery++
|
|
5272
|
+
console.log(`🔧 Recovered corrupted JSON line (${this.integrityCorrections.jsonRecovery} recoveries)`)
|
|
5273
|
+
|
|
5274
|
+
if (this.integrityCorrections.jsonRecovery > 20) {
|
|
5275
|
+
console.warn(`⚠️ Frequent JSON recovery detected (${this.integrityCorrections.jsonRecovery} times) - may indicate data corruption`)
|
|
5276
|
+
}
|
|
5277
|
+
|
|
5278
|
+
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record)
|
|
5279
|
+
|
|
5280
|
+
if (remainingSkip > 0) {
|
|
5281
|
+
remainingSkip--
|
|
5282
|
+
continue
|
|
5283
|
+
}
|
|
5284
|
+
|
|
5285
|
+
count++
|
|
5286
|
+
if (options.includeOffsets) {
|
|
5287
|
+
yield { entry: recordWithTerms, start: row.start, _: row._ || 0 }
|
|
5288
|
+
} else {
|
|
5289
|
+
if (this.opts.includeLinePosition) {
|
|
5290
|
+
recordWithTerms._ = row._ || 0
|
|
5291
|
+
}
|
|
5292
|
+
yield recordWithTerms
|
|
5293
|
+
}
|
|
5294
|
+
continue // Successfully recovered and yielded
|
|
5295
|
+
}
|
|
5296
|
+
} catch (recoveryError) {
|
|
5297
|
+
// Recovery attempt failed, continue with normal error handling
|
|
5298
|
+
if (this.opts.debugMode) {
|
|
5299
|
+
console.warn(`⚠️ walk(): Line recovery failed: ${recoveryError.message}`)
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
|
|
4990
5304
|
if (!this._offsetRecoveryInProgress) {
|
|
4991
5305
|
for await (const recoveredEntry of this._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)) {
|
|
4992
5306
|
yield recoveredEntry
|
|
@@ -5091,10 +5405,50 @@ class Database extends EventEmitter {
|
|
|
5091
5405
|
} catch (error) {
|
|
5092
5406
|
// CRITICAL FIX: Log deserialization errors instead of silently ignoring them
|
|
5093
5407
|
// This helps identify data corruption issues
|
|
5094
|
-
if (
|
|
5408
|
+
if (this.opts.debugMode) {
|
|
5095
5409
|
console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`)
|
|
5096
5410
|
console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`)
|
|
5097
5411
|
}
|
|
5412
|
+
|
|
5413
|
+
// CRITICAL FIX: Attempt to recover corrupted line before giving up
|
|
5414
|
+
const recoveredLine = this._tryRecoverLine(row.line)
|
|
5415
|
+
if (recoveredLine) {
|
|
5416
|
+
try {
|
|
5417
|
+
const entry = await this.serializer.deserialize(recoveredLine, { compress: this.opts.compress, v8: this.opts.v8 })
|
|
5418
|
+
if (entry !== null) {
|
|
5419
|
+
this.integrityCorrections.jsonRecovery++
|
|
5420
|
+
console.log(`🔧 Recovered corrupted JSON line (${this.integrityCorrections.jsonRecovery} recoveries)`)
|
|
5421
|
+
|
|
5422
|
+
if (this.integrityCorrections.jsonRecovery > 20) {
|
|
5423
|
+
console.warn(`⚠️ Frequent JSON recovery detected (${this.integrityCorrections.jsonRecovery} times) - may indicate data corruption`)
|
|
5424
|
+
}
|
|
5425
|
+
|
|
5426
|
+
const entryWithTerms = this.restoreTermIdsAfterDeserialization(entry)
|
|
5427
|
+
|
|
5428
|
+
if (remainingSkip > 0) {
|
|
5429
|
+
remainingSkip--
|
|
5430
|
+
continue
|
|
5431
|
+
}
|
|
5432
|
+
|
|
5433
|
+
count++
|
|
5434
|
+
if (options.includeOffsets) {
|
|
5435
|
+
yield { entry: entryWithTerms, start: row.start, _: row._ || this.offsets.findIndex(n => n === row.start) }
|
|
5436
|
+
} else {
|
|
5437
|
+
if (this.opts.includeLinePosition) {
|
|
5438
|
+
entryWithTerms._ = row._ || this.offsets.findIndex(n => n === row.start)
|
|
5439
|
+
}
|
|
5440
|
+
yield entryWithTerms
|
|
5441
|
+
}
|
|
5442
|
+
continue // Successfully recovered and yielded
|
|
5443
|
+
}
|
|
5444
|
+
} catch (recoveryError) {
|
|
5445
|
+
// Recovery attempt failed, continue with normal error handling
|
|
5446
|
+
if (this.opts.debugMode) {
|
|
5447
|
+
console.warn(`⚠️ walk(): Line recovery failed: ${recoveryError.message}`)
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
}
|
|
5451
|
+
|
|
5098
5452
|
if (!this._offsetRecoveryInProgress) {
|
|
5099
5453
|
for await (const recoveredEntry of this._streamingRecoveryGenerator(criteria, options, count, map, remainingSkip)) {
|
|
5100
5454
|
yield recoveredEntry
|
|
@@ -5338,7 +5692,13 @@ class Database extends EventEmitter {
|
|
|
5338
5692
|
await this.save()
|
|
5339
5693
|
// Ensure writeBuffer is cleared after save
|
|
5340
5694
|
if (this.writeBuffer.length > 0) {
|
|
5341
|
-
|
|
5695
|
+
this.integrityCorrections.writeBufferForced++
|
|
5696
|
+
console.log(`🔧 Forced WriteBuffer clear after save (${this.writeBuffer.length} items remaining)`)
|
|
5697
|
+
|
|
5698
|
+
if (this.integrityCorrections.writeBufferForced > 3) {
|
|
5699
|
+
console.warn(`⚠️ Frequent WriteBuffer clear issues detected (${this.integrityCorrections.writeBufferForced} times)`)
|
|
5700
|
+
}
|
|
5701
|
+
|
|
5342
5702
|
this.writeBuffer = []
|
|
5343
5703
|
this.writeBufferOffsets = []
|
|
5344
5704
|
this.writeBufferSizes = []
|
|
@@ -5463,7 +5823,8 @@ class Database extends EventEmitter {
|
|
|
5463
5823
|
console.log(`💾 Index data saved to ${idxPath}`)
|
|
5464
5824
|
}
|
|
5465
5825
|
} catch (error) {
|
|
5466
|
-
|
|
5826
|
+
this.integrityCorrections.indexSaveFailures++
|
|
5827
|
+
console.warn(`⚠️ Index save failure (${this.integrityCorrections.indexSaveFailures} times): ${error.message}`)
|
|
5467
5828
|
throw error // Re-throw to let caller handle
|
|
5468
5829
|
}
|
|
5469
5830
|
}
|
|
@@ -5539,5 +5900,4 @@ class Database extends EventEmitter {
|
|
|
5539
5900
|
}
|
|
5540
5901
|
|
|
5541
5902
|
export { Database }
|
|
5542
|
-
export default Database
|
|
5543
5903
|
|