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/dist/Database.cjs
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
3
|
var events = require('events');
|
|
6
4
|
var asyncMutex = require('async-mutex');
|
|
7
5
|
var fs = require('fs');
|
|
@@ -1241,11 +1239,11 @@ class IndexManager {
|
|
|
1241
1239
|
// This will be handled by the QueryManager's streaming strategy
|
|
1242
1240
|
continue;
|
|
1243
1241
|
}
|
|
1244
|
-
if (typeof criteriaValue === 'object' && !Array.isArray(criteriaValue)) {
|
|
1242
|
+
if (typeof criteriaValue === 'object' && !Array.isArray(criteriaValue) && criteriaValue !== null) {
|
|
1245
1243
|
const fieldIndex = data[field];
|
|
1246
1244
|
|
|
1247
1245
|
// Handle $in operator for array queries
|
|
1248
|
-
if (criteriaValue.$in !== undefined) {
|
|
1246
|
+
if (criteriaValue.$in !== undefined && criteriaValue.$in !== null) {
|
|
1249
1247
|
const inValues = Array.isArray(criteriaValue.$in) ? criteriaValue.$in : [criteriaValue.$in];
|
|
1250
1248
|
|
|
1251
1249
|
// PERFORMANCE: Cache term mapping field check once
|
|
@@ -2137,6 +2135,14 @@ class IndexManager {
|
|
|
2137
2135
|
// Keep the current index with initialized fields
|
|
2138
2136
|
return;
|
|
2139
2137
|
}
|
|
2138
|
+
|
|
2139
|
+
// Restore totalLines from saved data
|
|
2140
|
+
if (index.totalLines !== undefined) {
|
|
2141
|
+
this.totalLines = index.totalLines;
|
|
2142
|
+
if (this.opts.debugMode) {
|
|
2143
|
+
console.log(`🔍 IndexManager.load: Restored totalLines=${this.totalLines}`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2140
2146
|
this.index = processedIndex;
|
|
2141
2147
|
}
|
|
2142
2148
|
|
|
@@ -2177,7 +2183,8 @@ class IndexManager {
|
|
|
2177
2183
|
*/
|
|
2178
2184
|
toJSON() {
|
|
2179
2185
|
const serializable = {
|
|
2180
|
-
data: {}
|
|
2186
|
+
data: {},
|
|
2187
|
+
totalLines: this.totalLines
|
|
2181
2188
|
};
|
|
2182
2189
|
|
|
2183
2190
|
// Check if this is a term mapping field for conversion
|
|
@@ -2408,35 +2415,10 @@ class SchemaManager {
|
|
|
2408
2415
|
const obj = {};
|
|
2409
2416
|
const idIndex = this.schema.indexOf('id');
|
|
2410
2417
|
|
|
2411
|
-
//
|
|
2412
|
-
//
|
|
2413
|
-
//
|
|
2414
|
-
// 1. 'id' is not in current schema
|
|
2415
|
-
// 2. Array has significantly more elements than current schema (2+ extra elements)
|
|
2416
|
-
// This suggests the old schema had more fields, and 'id' was likely the first
|
|
2417
|
-
// 3. First element is a very short string (max 20 chars) that looks like a generated ID
|
|
2418
|
-
// (typically alphanumeric, often starting with letters like 'mit...' or similar patterns)
|
|
2419
|
-
// 4. First field in current schema is not 'id' (to avoid false positives)
|
|
2420
|
-
// 5. First element is not an array (to avoid false positives with array fields)
|
|
2418
|
+
// DISABLED: Schema migration detection was causing field mapping corruption
|
|
2419
|
+
// The logic was incorrectly assuming ID was in first position when it's appended at the end
|
|
2420
|
+
// This caused fields to be shifted incorrectly during object-to-array-to-object conversion
|
|
2421
2421
|
let arrayOffset = 0;
|
|
2422
|
-
if (idIndex === -1 && arr.length >= this.schema.length + 2 && this.schema.length > 0) {
|
|
2423
|
-
// Only apply if array has at least 2 extra elements (suggests old schema had more fields)
|
|
2424
|
-
const firstElement = arr[0];
|
|
2425
|
-
const firstFieldName = this.schema[0];
|
|
2426
|
-
|
|
2427
|
-
// Only apply shift if:
|
|
2428
|
-
// - First field is not 'id'
|
|
2429
|
-
// - First element is a very short string (max 20 chars) that looks like a generated ID
|
|
2430
|
-
// - First element is not an array (to avoid false positives)
|
|
2431
|
-
// - Array has at least 2 extra elements (strong indicator of schema migration)
|
|
2432
|
-
if (firstFieldName !== 'id' && typeof firstElement === 'string' && !Array.isArray(firstElement) && firstElement.length > 0 && firstElement.length <= 20 &&
|
|
2433
|
-
// Very conservative: max 20 chars (typical ID length)
|
|
2434
|
-
/^[a-zA-Z0-9_-]+$/.test(firstElement)) {
|
|
2435
|
-
// First element is likely the ID from old schema
|
|
2436
|
-
obj.id = firstElement;
|
|
2437
|
-
arrayOffset = 1;
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
2422
|
|
|
2441
2423
|
// Map array values to object properties
|
|
2442
2424
|
// Only include fields that are in the schema
|
|
@@ -2678,12 +2660,15 @@ class Serializer {
|
|
|
2678
2660
|
* Advanced serialization with optimized JSON.stringify and buffer pooling
|
|
2679
2661
|
*/
|
|
2680
2662
|
serializeAdvanced(data, addLinebreak) {
|
|
2663
|
+
// CRITICAL FIX: Sanitize data to remove problematic characters before serialization
|
|
2664
|
+
const sanitizedData = this.sanitizeDataForJSON(data);
|
|
2665
|
+
|
|
2681
2666
|
// Validate encoding before serialization
|
|
2682
|
-
this.validateEncodingBeforeSerialization(
|
|
2667
|
+
this.validateEncodingBeforeSerialization(sanitizedData);
|
|
2683
2668
|
|
|
2684
2669
|
// Use optimized JSON.stringify without buffer pooling
|
|
2685
2670
|
// NOTE: Buffer pool removed - using direct Buffer creation for simplicity and reliability
|
|
2686
|
-
const json = this.optimizedStringify(
|
|
2671
|
+
const json = this.optimizedStringify(sanitizedData);
|
|
2687
2672
|
|
|
2688
2673
|
// CRITICAL FIX: Normalize encoding before creating buffer
|
|
2689
2674
|
const normalizedJson = this.normalizeEncoding(json);
|
|
@@ -2781,6 +2766,44 @@ class Serializer {
|
|
|
2781
2766
|
/**
|
|
2782
2767
|
* Validate encoding before serialization
|
|
2783
2768
|
*/
|
|
2769
|
+
/**
|
|
2770
|
+
* Sanitize data to remove problematic characters that break JSON parsing
|
|
2771
|
+
* CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors
|
|
2772
|
+
* by removing control characters that cannot be safely represented in JSON
|
|
2773
|
+
*/
|
|
2774
|
+
sanitizeDataForJSON(data) {
|
|
2775
|
+
const sanitizeString = str => {
|
|
2776
|
+
if (typeof str !== 'string') return str;
|
|
2777
|
+
return str
|
|
2778
|
+
// Remove control characters that break JSON parsing (but keep \n, \r, \t as they can be escaped)
|
|
2779
|
+
// Remove: NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, VT, FF, SO, SI, DLE, DC1-DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US, DEL, C1 controls
|
|
2780
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
|
|
2781
|
+
// Limit string length to prevent performance issues
|
|
2782
|
+
.substring(0, 10000);
|
|
2783
|
+
};
|
|
2784
|
+
const sanitizeArray = arr => {
|
|
2785
|
+
if (!Array.isArray(arr)) return arr;
|
|
2786
|
+
return arr.map(item => this.sanitizeDataForJSON(item)).filter(item => item !== null && item !== undefined && item !== '');
|
|
2787
|
+
};
|
|
2788
|
+
if (typeof data === 'string') {
|
|
2789
|
+
return sanitizeString(data);
|
|
2790
|
+
}
|
|
2791
|
+
if (Array.isArray(data)) {
|
|
2792
|
+
return sanitizeArray(data);
|
|
2793
|
+
}
|
|
2794
|
+
if (data && typeof data === 'object') {
|
|
2795
|
+
const sanitized = {};
|
|
2796
|
+
for (const [key, value] of Object.entries(data)) {
|
|
2797
|
+
const sanitizedValue = this.sanitizeDataForJSON(value);
|
|
2798
|
+
// Only include non-null, non-undefined values
|
|
2799
|
+
if (sanitizedValue !== null && sanitizedValue !== undefined) {
|
|
2800
|
+
sanitized[key] = sanitizedValue;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
return sanitized;
|
|
2804
|
+
}
|
|
2805
|
+
return data;
|
|
2806
|
+
}
|
|
2784
2807
|
validateEncodingBeforeSerialization(data) {
|
|
2785
2808
|
const issues = [];
|
|
2786
2809
|
const checkString = (str, path = '') => {
|
|
@@ -2875,12 +2898,15 @@ class Serializer {
|
|
|
2875
2898
|
* Standard serialization (fallback)
|
|
2876
2899
|
*/
|
|
2877
2900
|
serializeStandard(data, addLinebreak) {
|
|
2901
|
+
// CRITICAL FIX: Sanitize data to remove problematic characters before serialization
|
|
2902
|
+
const sanitizedData = this.sanitizeDataForJSON(data);
|
|
2903
|
+
|
|
2878
2904
|
// Validate encoding before serialization
|
|
2879
|
-
this.validateEncodingBeforeSerialization(
|
|
2905
|
+
this.validateEncodingBeforeSerialization(sanitizedData);
|
|
2880
2906
|
|
|
2881
2907
|
// NOTE: Buffer pool removed - using direct Buffer creation for simplicity and reliability
|
|
2882
2908
|
// CRITICAL: Normalize encoding for all string fields before stringify
|
|
2883
|
-
const normalizedData = this.deepNormalizeEncoding(
|
|
2909
|
+
const normalizedData = this.deepNormalizeEncoding(sanitizedData);
|
|
2884
2910
|
const json = JSON.stringify(normalizedData);
|
|
2885
2911
|
|
|
2886
2912
|
// CRITICAL FIX: Normalize encoding before creating buffer
|
|
@@ -3084,11 +3110,14 @@ class Serializer {
|
|
|
3084
3110
|
* Batch serialization for multiple records
|
|
3085
3111
|
*/
|
|
3086
3112
|
serializeBatch(dataArray, opts = {}) {
|
|
3113
|
+
// CRITICAL FIX: Sanitize data to remove problematic characters before serialization
|
|
3114
|
+
const sanitizedDataArray = dataArray.map(data => this.sanitizeDataForJSON(data));
|
|
3115
|
+
|
|
3087
3116
|
// Validate encoding before serialization
|
|
3088
|
-
this.validateEncodingBeforeSerialization(
|
|
3117
|
+
this.validateEncodingBeforeSerialization(sanitizedDataArray);
|
|
3089
3118
|
|
|
3090
3119
|
// Convert all objects to array format for optimization
|
|
3091
|
-
const convertedData =
|
|
3120
|
+
const convertedData = sanitizedDataArray.map(data => this.convertToArrayFormat(data));
|
|
3092
3121
|
|
|
3093
3122
|
// Track conversion statistics
|
|
3094
3123
|
this.serializationStats.arraySerializations += convertedData.filter((item, index) => Array.isArray(item) && typeof dataArray[index] === 'object' && dataArray[index] !== null).length;
|
|
@@ -3888,6 +3917,141 @@ class FileHandler {
|
|
|
3888
3917
|
}
|
|
3889
3918
|
return groupedRanges;
|
|
3890
3919
|
}
|
|
3920
|
+
|
|
3921
|
+
/**
|
|
3922
|
+
* Ensure a line is complete by reading until newline if JSON appears truncated
|
|
3923
|
+
* @param {string} line - The potentially incomplete line
|
|
3924
|
+
* @param {number} fd - File descriptor
|
|
3925
|
+
* @param {number} currentOffset - Current read offset
|
|
3926
|
+
* @returns {string} Complete line
|
|
3927
|
+
*/
|
|
3928
|
+
async ensureCompleteLine(line, fd, currentOffset) {
|
|
3929
|
+
// Fast check: if line already ends with newline, it's likely complete
|
|
3930
|
+
if (line.endsWith('\n')) {
|
|
3931
|
+
return line;
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// Check if the line contains valid JSON by trying to parse it
|
|
3935
|
+
const trimmedLine = line.trim();
|
|
3936
|
+
if (trimmedLine.length === 0) {
|
|
3937
|
+
return line;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// Try to parse as JSON to see if it's complete
|
|
3941
|
+
try {
|
|
3942
|
+
JSON.parse(trimmedLine);
|
|
3943
|
+
// If parsing succeeds, the line is complete (but missing newline)
|
|
3944
|
+
// This is unusual but possible, return as-is
|
|
3945
|
+
return line;
|
|
3946
|
+
} catch (jsonError) {
|
|
3947
|
+
// JSON is incomplete, try to read more until we find a newline
|
|
3948
|
+
const bufferSize = 2048; // Read in 2KB chunks for better performance
|
|
3949
|
+
const additionalBuffer = Buffer.allocUnsafe(bufferSize);
|
|
3950
|
+
let additionalOffset = currentOffset;
|
|
3951
|
+
let additionalContent = line;
|
|
3952
|
+
|
|
3953
|
+
// Try reading up to 20KB more to find the newline (increased for safety)
|
|
3954
|
+
const maxAdditionalRead = 20480;
|
|
3955
|
+
let totalAdditionalRead = 0;
|
|
3956
|
+
while (totalAdditionalRead < maxAdditionalRead) {
|
|
3957
|
+
const {
|
|
3958
|
+
bytesRead
|
|
3959
|
+
} = await fd.read(additionalBuffer, 0, bufferSize, additionalOffset);
|
|
3960
|
+
if (bytesRead === 0) {
|
|
3961
|
+
// EOF reached, check if the accumulated content is now valid JSON
|
|
3962
|
+
const finalTrimmed = additionalContent.trim();
|
|
3963
|
+
try {
|
|
3964
|
+
JSON.parse(finalTrimmed);
|
|
3965
|
+
// If parsing succeeds now, return the content
|
|
3966
|
+
return additionalContent;
|
|
3967
|
+
} catch {
|
|
3968
|
+
// Still invalid, return original line to avoid data loss
|
|
3969
|
+
return line;
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
const chunk = additionalBuffer.toString('utf8', 0, bytesRead);
|
|
3973
|
+
additionalContent += chunk;
|
|
3974
|
+
totalAdditionalRead += bytesRead;
|
|
3975
|
+
|
|
3976
|
+
// Check if we found a newline in the entire accumulated content
|
|
3977
|
+
const newlineIndex = additionalContent.indexOf('\n', line.length);
|
|
3978
|
+
if (newlineIndex !== -1) {
|
|
3979
|
+
// Found newline, return content up to and including the newline
|
|
3980
|
+
const completeLine = additionalContent.substring(0, newlineIndex + 1);
|
|
3981
|
+
|
|
3982
|
+
// Validate that the complete line contains valid JSON
|
|
3983
|
+
const trimmedComplete = completeLine.trim();
|
|
3984
|
+
try {
|
|
3985
|
+
JSON.parse(trimmedComplete);
|
|
3986
|
+
return completeLine;
|
|
3987
|
+
} catch {
|
|
3988
|
+
// Even with newline, JSON is invalid - this suggests data corruption
|
|
3989
|
+
// Return original line to trigger normal error handling
|
|
3990
|
+
return line;
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
additionalOffset += bytesRead;
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// If we couldn't find a newline within the limit, return the original line
|
|
3997
|
+
// This prevents infinite reading and excessive memory usage
|
|
3998
|
+
return line;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
/**
|
|
4003
|
+
* Split content into complete JSON lines, handling special characters and escaped quotes
|
|
4004
|
+
* CRITICAL FIX: Prevents "Expected ',' or ']'" and "Unterminated string" errors by ensuring
|
|
4005
|
+
* each line is a complete, valid JSON object/array, even when containing special characters
|
|
4006
|
+
* @param {string} content - Raw content containing multiple JSON lines
|
|
4007
|
+
* @returns {string[]} Array of complete JSON lines
|
|
4008
|
+
*/
|
|
4009
|
+
splitJsonLines(content) {
|
|
4010
|
+
const lines = [];
|
|
4011
|
+
let currentLine = '';
|
|
4012
|
+
let inString = false;
|
|
4013
|
+
let escapeNext = false;
|
|
4014
|
+
let braceCount = 0;
|
|
4015
|
+
let bracketCount = 0;
|
|
4016
|
+
for (let i = 0; i < content.length; i++) {
|
|
4017
|
+
const char = content[i];
|
|
4018
|
+
i > 0 ? content[i - 1] : null;
|
|
4019
|
+
currentLine += char;
|
|
4020
|
+
if (escapeNext) {
|
|
4021
|
+
escapeNext = false;
|
|
4022
|
+
continue;
|
|
4023
|
+
}
|
|
4024
|
+
if (char === '\\') {
|
|
4025
|
+
escapeNext = true;
|
|
4026
|
+
continue;
|
|
4027
|
+
}
|
|
4028
|
+
if (char === '"' && !escapeNext) {
|
|
4029
|
+
inString = !inString;
|
|
4030
|
+
continue;
|
|
4031
|
+
}
|
|
4032
|
+
if (!inString) {
|
|
4033
|
+
if (char === '{') braceCount++;else if (char === '}') braceCount--;else if (char === '[') bracketCount++;else if (char === ']') bracketCount--;else if (char === '\n' && braceCount === 0 && bracketCount === 0) {
|
|
4034
|
+
// Found complete JSON object/array at newline
|
|
4035
|
+
const trimmedLine = currentLine.trim();
|
|
4036
|
+
if (trimmedLine.length > 0) {
|
|
4037
|
+
lines.push(trimmedLine.replace(/\n$/, '')); // Remove trailing newline
|
|
4038
|
+
}
|
|
4039
|
+
currentLine = '';
|
|
4040
|
+
braceCount = 0;
|
|
4041
|
+
bracketCount = 0;
|
|
4042
|
+
inString = false;
|
|
4043
|
+
escapeNext = false;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
// Add remaining content if it's a complete JSON object/array
|
|
4049
|
+
const trimmedLine = currentLine.trim();
|
|
4050
|
+
if (trimmedLine.length > 0 && braceCount === 0 && bracketCount === 0) {
|
|
4051
|
+
lines.push(trimmedLine);
|
|
4052
|
+
}
|
|
4053
|
+
return lines.filter(line => line.trim().length > 0);
|
|
4054
|
+
}
|
|
3891
4055
|
readGroupedRange(groupedRange, fd) {
|
|
3892
4056
|
var _this = this;
|
|
3893
4057
|
return _wrapAsyncGenerator(function* () {
|
|
@@ -3915,9 +4079,16 @@ class FileHandler {
|
|
|
3915
4079
|
});
|
|
3916
4080
|
}
|
|
3917
4081
|
|
|
3918
|
-
// CRITICAL FIX:
|
|
3919
|
-
//
|
|
3920
|
-
|
|
4082
|
+
// CRITICAL FIX: For single ranges, check if JSON appears truncated and try to complete it
|
|
4083
|
+
// Only attempt completion if the line doesn't end with newline (indicating possible truncation)
|
|
4084
|
+
if (!lineString.endsWith('\n')) {
|
|
4085
|
+
const completeLine = yield _awaitAsyncGenerator(_this.ensureCompleteLine(lineString, fd, range.start + actualBuffer.length));
|
|
4086
|
+
if (completeLine !== lineString) {
|
|
4087
|
+
lineString = completeLine.trimEnd();
|
|
4088
|
+
}
|
|
4089
|
+
} else {
|
|
4090
|
+
lineString = lineString.trimEnd();
|
|
4091
|
+
}
|
|
3921
4092
|
yield {
|
|
3922
4093
|
line: lineString,
|
|
3923
4094
|
start: range.start,
|
|
@@ -3952,10 +4123,29 @@ class FileHandler {
|
|
|
3952
4123
|
});
|
|
3953
4124
|
}
|
|
3954
4125
|
|
|
4126
|
+
// CRITICAL FIX: Validate buffer completeness to prevent UTF-8 corruption
|
|
4127
|
+
// When reading non-adjacent ranges, the buffer may be incomplete (last line cut mid-character)
|
|
4128
|
+
const lastNewlineIndex = content.lastIndexOf('\n');
|
|
4129
|
+
if (lastNewlineIndex === -1 || lastNewlineIndex < content.length - 2) {
|
|
4130
|
+
// Buffer may be incomplete - truncate to last complete line
|
|
4131
|
+
if (_this.opts.debugMode) {
|
|
4132
|
+
console.warn(`⚠️ Incomplete buffer detected at offset ${firstRange.start}, truncating to last complete line`);
|
|
4133
|
+
}
|
|
4134
|
+
if (lastNewlineIndex > 0) {
|
|
4135
|
+
content = content.substring(0, lastNewlineIndex + 1);
|
|
4136
|
+
} else {
|
|
4137
|
+
// No complete lines found - may be a serious issue
|
|
4138
|
+
if (_this.opts.debugMode) {
|
|
4139
|
+
console.warn(`⚠️ No complete lines found in buffer at offset ${firstRange.start}`);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
|
|
3955
4144
|
// CRITICAL FIX: Handle ranges more carefully to prevent corruption
|
|
3956
4145
|
if (groupedRange.length === 2 && groupedRange[0].end === groupedRange[1].start) {
|
|
3957
|
-
// Special case: Adjacent ranges - split by
|
|
3958
|
-
|
|
4146
|
+
// Special case: Adjacent ranges - split by COMPLETE JSON lines, not just newlines
|
|
4147
|
+
// This prevents corruption when lines contain special characters or unescaped quotes
|
|
4148
|
+
const lines = _this.splitJsonLines(content);
|
|
3959
4149
|
for (let i = 0; i < Math.min(lines.length, groupedRange.length); i++) {
|
|
3960
4150
|
const range = groupedRange[i];
|
|
3961
4151
|
yield {
|
|
@@ -3991,6 +4181,7 @@ class FileHandler {
|
|
|
3991
4181
|
|
|
3992
4182
|
// OPTIMIZATION 4: Direct character check instead of regex/trimEnd
|
|
3993
4183
|
// Remove trailing newlines and whitespace efficiently
|
|
4184
|
+
// CRITICAL FIX: Prevents incomplete JSON line reading that caused "Expected ',' or ']'" parsing errors
|
|
3994
4185
|
// trimEnd() is actually optimized in V8, but we can check if there's anything to trim first
|
|
3995
4186
|
const len = rangeContent.length;
|
|
3996
4187
|
if (len > 0) {
|
|
@@ -4002,8 +4193,24 @@ class FileHandler {
|
|
|
4002
4193
|
}
|
|
4003
4194
|
}
|
|
4004
4195
|
if (rangeContent.length === 0) continue;
|
|
4196
|
+
|
|
4197
|
+
// CRITICAL FIX: For multiple ranges, we cannot safely expand reading
|
|
4198
|
+
// because offsets are pre-calculated. Instead, validate JSON and let
|
|
4199
|
+
// the deserializer handle incomplete lines (which will trigger recovery)
|
|
4200
|
+
const trimmedContent = rangeContent.trim();
|
|
4201
|
+
let finalContent = rangeContent;
|
|
4202
|
+
if (trimmedContent.length > 0) {
|
|
4203
|
+
try {
|
|
4204
|
+
JSON.parse(trimmedContent);
|
|
4205
|
+
// JSON is valid, use as-is
|
|
4206
|
+
} catch (jsonError) {
|
|
4207
|
+
// JSON appears incomplete - this is expected for truncated ranges
|
|
4208
|
+
// Let the deserializer handle it (will trigger streaming recovery if needed)
|
|
4209
|
+
// We don't try to expand reading here because offsets are pre-calculated
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4005
4212
|
yield {
|
|
4006
|
-
line:
|
|
4213
|
+
line: finalContent,
|
|
4007
4214
|
start: range.start,
|
|
4008
4215
|
_: range.index !== undefined ? range.index : range._ || null
|
|
4009
4216
|
};
|
|
@@ -4014,41 +4221,47 @@ class FileHandler {
|
|
|
4014
4221
|
walk(ranges) {
|
|
4015
4222
|
var _this2 = this;
|
|
4016
4223
|
return _wrapAsyncGenerator(function* () {
|
|
4017
|
-
//
|
|
4018
|
-
|
|
4019
|
-
return; // Return empty generator if file doesn't exist
|
|
4020
|
-
}
|
|
4021
|
-
const fd = yield _awaitAsyncGenerator(fs.promises.open(_this2.file, 'r'));
|
|
4224
|
+
// CRITICAL FIX: Acquire file mutex to prevent race conditions with concurrent writes
|
|
4225
|
+
const release = _this2.fileMutex ? yield _awaitAsyncGenerator(_this2.fileMutex.acquire()) : () => {};
|
|
4022
4226
|
try {
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
}
|
|
4035
|
-
} catch (err) {
|
|
4036
|
-
_didIteratorError2 = true;
|
|
4037
|
-
_iteratorError2 = err;
|
|
4038
|
-
} finally {
|
|
4227
|
+
// Check if file exists before trying to read it
|
|
4228
|
+
if (!(yield _awaitAsyncGenerator(_this2.exists()))) {
|
|
4229
|
+
return; // Return empty generator if file doesn't exist
|
|
4230
|
+
}
|
|
4231
|
+
const fd = yield _awaitAsyncGenerator(fs.promises.open(_this2.file, 'r'));
|
|
4232
|
+
try {
|
|
4233
|
+
const groupedRanges = yield _awaitAsyncGenerator(_this2.groupedRanges(ranges));
|
|
4234
|
+
for (const groupedRange of groupedRanges) {
|
|
4235
|
+
var _iteratorAbruptCompletion2 = false;
|
|
4236
|
+
var _didIteratorError2 = false;
|
|
4237
|
+
var _iteratorError2;
|
|
4039
4238
|
try {
|
|
4040
|
-
|
|
4041
|
-
|
|
4239
|
+
for (var _iterator2 = _asyncIterator(_this2.readGroupedRange(groupedRange, fd)), _step2; _iteratorAbruptCompletion2 = !(_step2 = yield _awaitAsyncGenerator(_iterator2.next())).done; _iteratorAbruptCompletion2 = false) {
|
|
4240
|
+
const row = _step2.value;
|
|
4241
|
+
{
|
|
4242
|
+
yield row;
|
|
4243
|
+
}
|
|
4042
4244
|
}
|
|
4245
|
+
} catch (err) {
|
|
4246
|
+
_didIteratorError2 = true;
|
|
4247
|
+
_iteratorError2 = err;
|
|
4043
4248
|
} finally {
|
|
4044
|
-
|
|
4045
|
-
|
|
4249
|
+
try {
|
|
4250
|
+
if (_iteratorAbruptCompletion2 && _iterator2.return != null) {
|
|
4251
|
+
yield _awaitAsyncGenerator(_iterator2.return());
|
|
4252
|
+
}
|
|
4253
|
+
} finally {
|
|
4254
|
+
if (_didIteratorError2) {
|
|
4255
|
+
throw _iteratorError2;
|
|
4256
|
+
}
|
|
4046
4257
|
}
|
|
4047
4258
|
}
|
|
4048
4259
|
}
|
|
4260
|
+
} finally {
|
|
4261
|
+
yield _awaitAsyncGenerator(fd.close());
|
|
4049
4262
|
}
|
|
4050
4263
|
} finally {
|
|
4051
|
-
|
|
4264
|
+
release();
|
|
4052
4265
|
}
|
|
4053
4266
|
})();
|
|
4054
4267
|
}
|
|
@@ -4174,7 +4387,9 @@ class FileHandler {
|
|
|
4174
4387
|
JSON.parse(lines[i]);
|
|
4175
4388
|
validLines.push(lines[i]);
|
|
4176
4389
|
} catch (error) {
|
|
4177
|
-
|
|
4390
|
+
if (this.opts.debugMode) {
|
|
4391
|
+
console.warn(`⚠️ Invalid JSON in temp file at line ${i + 1}, skipping:`, lines[i].substring(0, 100));
|
|
4392
|
+
}
|
|
4178
4393
|
hasInvalidJson = true;
|
|
4179
4394
|
}
|
|
4180
4395
|
}
|
|
@@ -4800,7 +5015,9 @@ class FileHandler {
|
|
|
4800
5015
|
content = buffer.toString('utf8');
|
|
4801
5016
|
} catch (error) {
|
|
4802
5017
|
// If UTF-8 decoding fails, try to recover by finding valid UTF-8 boundaries
|
|
4803
|
-
|
|
5018
|
+
if (this.opts.debugMode) {
|
|
5019
|
+
console.warn(`UTF-8 decoding failed for file ${this.file}, attempting recovery`);
|
|
5020
|
+
}
|
|
4804
5021
|
|
|
4805
5022
|
// Find the last complete UTF-8 character
|
|
4806
5023
|
let validLength = buffer.length;
|
|
@@ -6042,7 +6259,7 @@ class QueryManager {
|
|
|
6042
6259
|
}
|
|
6043
6260
|
return false;
|
|
6044
6261
|
}
|
|
6045
|
-
if (typeof condition === 'object' && !Array.isArray(condition)) {
|
|
6262
|
+
if (typeof condition === 'object' && !Array.isArray(condition) && condition !== null) {
|
|
6046
6263
|
const operators = Object.keys(condition).map(op => normalizeOperator(op));
|
|
6047
6264
|
if (this.opts.debugMode) {
|
|
6048
6265
|
console.log(`🔍 Field '${field}' has operators:`, operators);
|
|
@@ -6339,7 +6556,7 @@ class QueryManager {
|
|
|
6339
6556
|
if (field.startsWith('$')) continue;
|
|
6340
6557
|
if (termMappingFields.includes(field)) {
|
|
6341
6558
|
// For term mapping fields, simple equality or $in queries work well
|
|
6342
|
-
if (typeof condition === 'string' || typeof condition === 'object' && condition.$in && Array.isArray(condition.$in)) {
|
|
6559
|
+
if (typeof condition === 'string' || typeof condition === 'object' && condition !== null && condition.$in && Array.isArray(condition.$in)) {
|
|
6343
6560
|
return true;
|
|
6344
6561
|
}
|
|
6345
6562
|
}
|
|
@@ -7889,6 +8106,23 @@ class Database extends events.EventEmitter {
|
|
|
7889
8106
|
loadTime: 0
|
|
7890
8107
|
};
|
|
7891
8108
|
|
|
8109
|
+
// Initialize integrity correction tracking
|
|
8110
|
+
this.integrityCorrections = {
|
|
8111
|
+
indexSync: 0,
|
|
8112
|
+
// index.totalLines vs offsets.length corrections
|
|
8113
|
+
indexInconsistency: 0,
|
|
8114
|
+
// Index record count vs offsets mismatch
|
|
8115
|
+
writeBufferForced: 0,
|
|
8116
|
+
// WriteBuffer not cleared after save
|
|
8117
|
+
indexSaveFailures: 0,
|
|
8118
|
+
// Failed to save index data
|
|
8119
|
+
dataIntegrity: 0,
|
|
8120
|
+
// General data integrity issues
|
|
8121
|
+
utf8Recovery: 0,
|
|
8122
|
+
// UTF-8 decoding failures recovered
|
|
8123
|
+
jsonRecovery: 0 // JSON parsing failures recovered
|
|
8124
|
+
};
|
|
8125
|
+
|
|
7892
8126
|
// Initialize usage stats for QueryManager
|
|
7893
8127
|
this.usageStats = {
|
|
7894
8128
|
totalQueries: 0,
|
|
@@ -8073,7 +8307,9 @@ class Database extends events.EventEmitter {
|
|
|
8073
8307
|
}
|
|
8074
8308
|
}
|
|
8075
8309
|
if (arrayStringFields.length > 0) {
|
|
8076
|
-
|
|
8310
|
+
if (this.opts.debugMode) {
|
|
8311
|
+
console.warn(`⚠️ Warning: The following array:string indexed fields were not added to term mapping: ${arrayStringFields.join(', ')}. This may impact performance.`);
|
|
8312
|
+
}
|
|
8077
8313
|
}
|
|
8078
8314
|
}
|
|
8079
8315
|
if (this.opts.debugMode) {
|
|
@@ -8114,13 +8350,17 @@ class Database extends events.EventEmitter {
|
|
|
8114
8350
|
}
|
|
8115
8351
|
|
|
8116
8352
|
/**
|
|
8117
|
-
* Get term mapping fields from indexes (auto-detected)
|
|
8353
|
+
* Get term mapping fields from configuration or indexes (auto-detected)
|
|
8118
8354
|
* @returns {string[]} Array of field names that use term mapping
|
|
8119
8355
|
*/
|
|
8120
8356
|
getTermMappingFields() {
|
|
8121
|
-
|
|
8357
|
+
// If termMappingFields is explicitly configured, use it
|
|
8358
|
+
if (this.opts.termMappingFields && Array.isArray(this.opts.termMappingFields)) {
|
|
8359
|
+
return [...this.opts.termMappingFields];
|
|
8360
|
+
}
|
|
8122
8361
|
|
|
8123
|
-
// Auto-detect fields that benefit from term mapping
|
|
8362
|
+
// Auto-detect fields that benefit from term mapping from indexes
|
|
8363
|
+
if (!this.opts.indexes) return [];
|
|
8124
8364
|
const termMappingFields = [];
|
|
8125
8365
|
for (const [field, type] of Object.entries(this.opts.indexes)) {
|
|
8126
8366
|
// Fields that should use term mapping (only array fields)
|
|
@@ -8235,8 +8475,28 @@ class Database extends events.EventEmitter {
|
|
|
8235
8475
|
}
|
|
8236
8476
|
}
|
|
8237
8477
|
|
|
8478
|
+
// CRITICAL INTEGRITY CHECK: Ensure IndexManager is consistent with loaded offsets
|
|
8479
|
+
// This must happen immediately after load() to prevent any subsequent operations from seeing inconsistent state
|
|
8480
|
+
if (this.indexManager && this.offsets && this.offsets.length > 0) {
|
|
8481
|
+
const currentTotalLines = this.indexManager.totalLines || 0;
|
|
8482
|
+
if (currentTotalLines !== this.offsets.length) {
|
|
8483
|
+
this.indexManager.setTotalLines(this.offsets.length);
|
|
8484
|
+
if (this.opts.debugMode) {
|
|
8485
|
+
console.log(`🔧 Post-load integrity sync: IndexManager totalLines ${currentTotalLines} → ${this.offsets.length}`);
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8238
8490
|
// Manual save is now the default behavior
|
|
8239
8491
|
|
|
8492
|
+
// CRITICAL FIX: Ensure IndexManager totalLines is consistent with offsets
|
|
8493
|
+
// This prevents data integrity issues when database is initialized without existing data
|
|
8494
|
+
if (this.indexManager && this.offsets) {
|
|
8495
|
+
this.indexManager.setTotalLines(this.offsets.length);
|
|
8496
|
+
if (this.opts.debugMode) {
|
|
8497
|
+
console.log(`🔧 Initialized index totalLines to ${this.offsets.length}`);
|
|
8498
|
+
}
|
|
8499
|
+
}
|
|
8240
8500
|
this.initialized = true;
|
|
8241
8501
|
this.emit('initialized');
|
|
8242
8502
|
if (this.opts.debugMode) {
|
|
@@ -8374,11 +8634,11 @@ class Database extends events.EventEmitter {
|
|
|
8374
8634
|
this.offsets = parsedIdxData.offsets;
|
|
8375
8635
|
// CRITICAL FIX: Update IndexManager totalLines to match offsets length
|
|
8376
8636
|
// This ensures queries and length property work correctly even if offsets are reset later
|
|
8377
|
-
if (this.indexManager
|
|
8637
|
+
if (this.indexManager) {
|
|
8378
8638
|
this.indexManager.setTotalLines(this.offsets.length);
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8639
|
+
if (this.opts.debugMode) {
|
|
8640
|
+
console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}, synced IndexManager totalLines`);
|
|
8641
|
+
}
|
|
8382
8642
|
}
|
|
8383
8643
|
}
|
|
8384
8644
|
|
|
@@ -9042,6 +9302,7 @@ class Database extends events.EventEmitter {
|
|
|
9042
9302
|
});
|
|
9043
9303
|
if (this.opts.debugMode) {
|
|
9044
9304
|
console.log(`💾 Save: allData.length=${allData.length}, cleanedData.length=${cleanedData.length}`);
|
|
9305
|
+
console.log(`💾 Save: Current offsets.length before recalculation: ${this.offsets.length}`);
|
|
9045
9306
|
console.log(`💾 Save: All records in allData before serialization:`, allData.map(r => r && r.id ? {
|
|
9046
9307
|
id: String(r.id),
|
|
9047
9308
|
price: r.price,
|
|
@@ -9065,6 +9326,9 @@ class Database extends events.EventEmitter {
|
|
|
9065
9326
|
console.log(`💾 Save: First line (first 200 chars):`, lines[0].substring(0, 200));
|
|
9066
9327
|
}
|
|
9067
9328
|
}
|
|
9329
|
+
|
|
9330
|
+
// CRITICAL FIX: Always recalculate offsets from serialized data to ensure consistency
|
|
9331
|
+
// Even if _streamExistingRecords updated offsets, we need to recalculate based on actual serialized data
|
|
9068
9332
|
this.offsets = [];
|
|
9069
9333
|
let currentOffset = 0;
|
|
9070
9334
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -9074,6 +9338,9 @@ class Database extends events.EventEmitter {
|
|
|
9074
9338
|
const lineWithNewline = lines[i] + '\n';
|
|
9075
9339
|
currentOffset += Buffer.byteLength(lineWithNewline, 'utf8');
|
|
9076
9340
|
}
|
|
9341
|
+
if (this.opts.debugMode) {
|
|
9342
|
+
console.log(`💾 Save: Recalculated offsets.length=${this.offsets.length}, should match lines.length=${lines.length}`);
|
|
9343
|
+
}
|
|
9077
9344
|
|
|
9078
9345
|
// CRITICAL FIX: Ensure indexOffset matches actual file size
|
|
9079
9346
|
this.indexOffset = currentOffset;
|
|
@@ -9093,11 +9360,15 @@ class Database extends events.EventEmitter {
|
|
|
9093
9360
|
this.shouldSave = false;
|
|
9094
9361
|
this.lastSaveTime = Date.now();
|
|
9095
9362
|
|
|
9096
|
-
//
|
|
9097
|
-
if
|
|
9363
|
+
// CRITICAL FIX: Always clear deletedIds and rebuild index if there were deletions,
|
|
9364
|
+
// even if allData.length === 0 (all records were deleted)
|
|
9365
|
+
const hadDeletedRecords = deletedIdsSnapshot.size > 0;
|
|
9366
|
+
const hadUpdatedRecords = writeBufferSnapshot.length > 0;
|
|
9367
|
+
|
|
9368
|
+
// Clear writeBuffer and deletedIds after successful save
|
|
9369
|
+
// Also rebuild index if records were deleted or updated, even if allData is empty
|
|
9370
|
+
if (allData.length > 0 || hadDeletedRecords || hadUpdatedRecords) {
|
|
9098
9371
|
// Rebuild index when records were deleted or updated to maintain consistency
|
|
9099
|
-
const hadDeletedRecords = deletedIdsSnapshot.size > 0;
|
|
9100
|
-
const hadUpdatedRecords = writeBufferSnapshot.length > 0;
|
|
9101
9372
|
if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
9102
9373
|
if (hadDeletedRecords || hadUpdatedRecords) {
|
|
9103
9374
|
// Clear the index and rebuild it from the saved records
|
|
@@ -9154,8 +9425,24 @@ class Database extends events.EventEmitter {
|
|
|
9154
9425
|
}
|
|
9155
9426
|
await this.indexManager.add(record, i);
|
|
9156
9427
|
}
|
|
9428
|
+
|
|
9429
|
+
// VALIDATION: Ensure index consistency after rebuild
|
|
9430
|
+
// Check that all indexed records have valid line numbers
|
|
9431
|
+
const indexedRecordCount = this.indexManager.getIndexedRecordCount?.() || allData.length;
|
|
9432
|
+
if (indexedRecordCount !== this.offsets.length) {
|
|
9433
|
+
this.integrityCorrections.indexInconsistency++;
|
|
9434
|
+
console.log(`🔧 Auto-corrected index consistency: ${indexedRecordCount} indexed → ${this.offsets.length} offsets`);
|
|
9435
|
+
if (this.integrityCorrections.indexInconsistency > 5) {
|
|
9436
|
+
console.warn(`⚠️ Frequent index inconsistencies detected (${this.integrityCorrections.indexInconsistency} times)`);
|
|
9437
|
+
}
|
|
9438
|
+
|
|
9439
|
+
// Force consistency by setting totalLines to match offsets
|
|
9440
|
+
this.indexManager.setTotalLines(this.offsets.length);
|
|
9441
|
+
} else {
|
|
9442
|
+
this.indexManager.setTotalLines(this.offsets.length);
|
|
9443
|
+
}
|
|
9157
9444
|
if (this.opts.debugMode) {
|
|
9158
|
-
console.log(`💾 Save: Index rebuilt with ${allData.length} records`);
|
|
9445
|
+
console.log(`💾 Save: Index rebuilt with ${allData.length} records, totalLines set to ${this.offsets.length}`);
|
|
9159
9446
|
}
|
|
9160
9447
|
}
|
|
9161
9448
|
}
|
|
@@ -9176,6 +9463,22 @@ class Database extends events.EventEmitter {
|
|
|
9176
9463
|
for (const deletedId of deletedIdsSnapshot) {
|
|
9177
9464
|
this.deletedIds.delete(deletedId);
|
|
9178
9465
|
}
|
|
9466
|
+
} else if (hadDeletedRecords) {
|
|
9467
|
+
// CRITICAL FIX: Even if allData is empty, clear deletedIds and rebuild index
|
|
9468
|
+
// when records were deleted to ensure consistency
|
|
9469
|
+
if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
9470
|
+
// Clear the index since all records were deleted
|
|
9471
|
+
this.indexManager.clear();
|
|
9472
|
+
this.indexManager.setTotalLines(0);
|
|
9473
|
+
if (this.opts.debugMode) {
|
|
9474
|
+
console.log(`🧹 Cleared index after removing all ${deletedIdsSnapshot.size} deleted records`);
|
|
9475
|
+
}
|
|
9476
|
+
}
|
|
9477
|
+
|
|
9478
|
+
// Clear deletedIds even when allData is empty
|
|
9479
|
+
for (const deletedId of deletedIdsSnapshot) {
|
|
9480
|
+
this.deletedIds.delete(deletedId);
|
|
9481
|
+
}
|
|
9179
9482
|
|
|
9180
9483
|
// CRITICAL FIX: Ensure writeBuffer is completely cleared after successful save
|
|
9181
9484
|
if (this.writeBuffer.length > 0) {
|
|
@@ -9675,6 +9978,36 @@ class Database extends events.EventEmitter {
|
|
|
9675
9978
|
console.log(`🔍 FIND START: criteria=${JSON.stringify(criteria)}, writeBuffer=${this.writeBuffer.length}`);
|
|
9676
9979
|
}
|
|
9677
9980
|
try {
|
|
9981
|
+
// INTEGRITY CHECK: Validate data consistency before querying
|
|
9982
|
+
// This is a safety net for unexpected inconsistencies - should rarely trigger
|
|
9983
|
+
if (this.indexManager && this.offsets && this.offsets.length > 0) {
|
|
9984
|
+
const indexTotalLines = this.indexManager.totalLines || 0;
|
|
9985
|
+
const offsetsLength = this.offsets.length;
|
|
9986
|
+
if (indexTotalLines !== offsetsLength) {
|
|
9987
|
+
// This should be extremely rare - indicates a real bug if it happens frequently
|
|
9988
|
+
this.integrityCorrections.dataIntegrity++;
|
|
9989
|
+
|
|
9990
|
+
// Only show in debug mode - these corrections indicate real issues
|
|
9991
|
+
if (this.opts.debugMode) {
|
|
9992
|
+
console.log(`🔧 Integrity correction needed: index.totalLines ${indexTotalLines} → ${offsetsLength} (${this.integrityCorrections.dataIntegrity} total)`);
|
|
9993
|
+
}
|
|
9994
|
+
|
|
9995
|
+
// Warn if corrections are becoming frequent (indicates a real problem)
|
|
9996
|
+
if (this.integrityCorrections.dataIntegrity > 5) {
|
|
9997
|
+
console.warn(`⚠️ Frequent integrity corrections (${this.integrityCorrections.dataIntegrity} times) - this indicates a systemic issue`);
|
|
9998
|
+
}
|
|
9999
|
+
this.indexManager.setTotalLines(offsetsLength);
|
|
10000
|
+
|
|
10001
|
+
// Try to persist the fix, but don't fail the operation if it doesn't work
|
|
10002
|
+
try {
|
|
10003
|
+
await this._saveIndexDataToFile();
|
|
10004
|
+
} catch (error) {
|
|
10005
|
+
// Just track the failure - don't throw since this is a safety net
|
|
10006
|
+
this.integrityCorrections.indexSaveFailures++;
|
|
10007
|
+
}
|
|
10008
|
+
}
|
|
10009
|
+
}
|
|
10010
|
+
|
|
9678
10011
|
// Validate indexed query mode if enabled
|
|
9679
10012
|
if (this.opts.indexedQueryMode === 'strict') {
|
|
9680
10013
|
this._validateIndexedQuery(criteria, options);
|
|
@@ -9691,31 +10024,23 @@ class Database extends events.EventEmitter {
|
|
|
9691
10024
|
const writeBufferResultsWithTerms = options.restoreTerms !== false ? writeBufferResults.map(record => this.restoreTermIdsAfterDeserialization(record)) : writeBufferResults;
|
|
9692
10025
|
|
|
9693
10026
|
// Combine results, removing duplicates (writeBuffer takes precedence)
|
|
9694
|
-
// OPTIMIZATION:
|
|
10027
|
+
// OPTIMIZATION: Unified efficient approach with consistent precedence rules
|
|
9695
10028
|
let allResults;
|
|
9696
|
-
if (writeBufferResults.length > 50) {
|
|
9697
|
-
// Parallel approach for large writeBuffer
|
|
9698
|
-
const [fileResultsSet, writeBufferSet] = await Promise.all([Promise.resolve(new Set(fileResultsWithTerms.map(r => r.id))), Promise.resolve(new Set(writeBufferResultsWithTerms.map(r => r.id)))]);
|
|
9699
10029
|
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
allResults = [...fileResultsWithTerms];
|
|
9706
|
-
|
|
9707
|
-
// Replace file records with writeBuffer records and add new writeBuffer records
|
|
9708
|
-
for (const record of writeBufferResultsWithTerms) {
|
|
9709
|
-
const existingIndex = allResults.findIndex(r => r.id === record.id);
|
|
9710
|
-
if (existingIndex !== -1) {
|
|
9711
|
-
// Replace existing record with writeBuffer version
|
|
9712
|
-
allResults[existingIndex] = record;
|
|
9713
|
-
} else {
|
|
9714
|
-
// Add new record from writeBuffer
|
|
9715
|
-
allResults.push(record);
|
|
9716
|
-
}
|
|
10030
|
+
// Create efficient lookup map for writeBuffer records
|
|
10031
|
+
const writeBufferMap = new Map();
|
|
10032
|
+
writeBufferResultsWithTerms.forEach(record => {
|
|
10033
|
+
if (record && record.id) {
|
|
10034
|
+
writeBufferMap.set(record.id, record);
|
|
9717
10035
|
}
|
|
9718
|
-
}
|
|
10036
|
+
});
|
|
10037
|
+
|
|
10038
|
+
// Filter file results to exclude any records that exist in writeBuffer
|
|
10039
|
+
// This ensures writeBuffer always takes precedence
|
|
10040
|
+
const filteredFileResults = fileResultsWithTerms.filter(record => record && record.id && !writeBufferMap.has(record.id));
|
|
10041
|
+
|
|
10042
|
+
// Combine results: file results (filtered) + all writeBuffer results
|
|
10043
|
+
allResults = [...filteredFileResults, ...writeBufferResultsWithTerms];
|
|
9719
10044
|
|
|
9720
10045
|
// Remove records that are marked as deleted
|
|
9721
10046
|
const finalResults = allResults.filter(record => !this.deletedIds.has(record.id));
|
|
@@ -9963,19 +10288,6 @@ class Database extends events.EventEmitter {
|
|
|
9963
10288
|
|
|
9964
10289
|
// CRITICAL FIX: Validate state before update operation
|
|
9965
10290
|
this.validateState();
|
|
9966
|
-
|
|
9967
|
-
// CRITICAL FIX: If there's data to save, call save() to persist it
|
|
9968
|
-
// Only save if there are actual records in writeBuffer
|
|
9969
|
-
if (this.shouldSave && this.writeBuffer.length > 0) {
|
|
9970
|
-
if (this.opts.debugMode) {
|
|
9971
|
-
console.log(`🔄 UPDATE: Calling save() before update - writeBuffer.length=${this.writeBuffer.length}`);
|
|
9972
|
-
}
|
|
9973
|
-
const saveStart = Date.now();
|
|
9974
|
-
await this.save(false); // Use save(false) since we're already in queue
|
|
9975
|
-
if (this.opts.debugMode) {
|
|
9976
|
-
console.log(`🔄 UPDATE: Save completed in ${Date.now() - saveStart}ms`);
|
|
9977
|
-
}
|
|
9978
|
-
}
|
|
9979
10291
|
if (this.opts.debugMode) {
|
|
9980
10292
|
console.log(`🔄 UPDATE: Starting find() - writeBuffer=${this.writeBuffer.length}`);
|
|
9981
10293
|
}
|
|
@@ -9988,6 +10300,13 @@ class Database extends events.EventEmitter {
|
|
|
9988
10300
|
console.log(`🔄 UPDATE: Find completed in ${Date.now() - findStart}ms, found ${records.length} records`);
|
|
9989
10301
|
}
|
|
9990
10302
|
const updatedRecords = [];
|
|
10303
|
+
if (this.opts.debugMode) {
|
|
10304
|
+
console.log(`🔄 UPDATE: About to process ${records.length} records`);
|
|
10305
|
+
console.log(`🔄 UPDATE: Records:`, records.map(r => ({
|
|
10306
|
+
id: r.id,
|
|
10307
|
+
value: r.value
|
|
10308
|
+
})));
|
|
10309
|
+
}
|
|
9991
10310
|
for (const record of records) {
|
|
9992
10311
|
const recordStart = Date.now();
|
|
9993
10312
|
if (this.opts.debugMode) {
|
|
@@ -10026,12 +10345,18 @@ class Database extends events.EventEmitter {
|
|
|
10026
10345
|
// For records in the file, we need to ensure they are properly marked for replacement
|
|
10027
10346
|
const index = this.writeBuffer.findIndex(r => r.id === record.id);
|
|
10028
10347
|
let lineNumber = null;
|
|
10348
|
+
if (this.opts.debugMode) {
|
|
10349
|
+
console.log(`🔄 UPDATE: writeBuffer.findIndex for ${record.id} returned ${index}`);
|
|
10350
|
+
console.log(`🔄 UPDATE: writeBuffer length: ${this.writeBuffer.length}`);
|
|
10351
|
+
console.log(`🔄 UPDATE: writeBuffer IDs:`, this.writeBuffer.map(r => r.id));
|
|
10352
|
+
}
|
|
10029
10353
|
if (index !== -1) {
|
|
10030
10354
|
// Record is already in writeBuffer, update it
|
|
10031
10355
|
this.writeBuffer[index] = updated;
|
|
10032
10356
|
lineNumber = this._getAbsoluteLineNumber(index);
|
|
10033
10357
|
if (this.opts.debugMode) {
|
|
10034
10358
|
console.log(`🔄 UPDATE: Updated existing writeBuffer record at index ${index}`);
|
|
10359
|
+
console.log(`🔄 UPDATE: writeBuffer now has ${this.writeBuffer.length} records`);
|
|
10035
10360
|
}
|
|
10036
10361
|
} else {
|
|
10037
10362
|
// Record is in file, add updated version to writeBuffer
|
|
@@ -10041,6 +10366,7 @@ class Database extends events.EventEmitter {
|
|
|
10041
10366
|
lineNumber = this._getAbsoluteLineNumber(this.writeBuffer.length - 1);
|
|
10042
10367
|
if (this.opts.debugMode) {
|
|
10043
10368
|
console.log(`🔄 UPDATE: Added updated record to writeBuffer (will replace file record ${record.id})`);
|
|
10369
|
+
console.log(`🔄 UPDATE: writeBuffer now has ${this.writeBuffer.length} records`);
|
|
10044
10370
|
}
|
|
10045
10371
|
}
|
|
10046
10372
|
const indexUpdateStart = Date.now();
|
|
@@ -10076,6 +10402,26 @@ class Database extends events.EventEmitter {
|
|
|
10076
10402
|
try {
|
|
10077
10403
|
// CRITICAL FIX: Validate state before delete operation
|
|
10078
10404
|
this.validateState();
|
|
10405
|
+
|
|
10406
|
+
// 🔧 NEW: Validate indexed query mode for delete operations
|
|
10407
|
+
if (this.opts.indexedQueryMode === 'strict') {
|
|
10408
|
+
this._validateIndexedQuery(criteria, {
|
|
10409
|
+
operation: 'delete'
|
|
10410
|
+
});
|
|
10411
|
+
}
|
|
10412
|
+
|
|
10413
|
+
// ⚠️ NEW: Warn about non-indexed fields in permissive mode
|
|
10414
|
+
if (this.opts.indexedQueryMode !== 'strict') {
|
|
10415
|
+
const indexedFields = Object.keys(this.opts.indexes || {});
|
|
10416
|
+
const queryFields = this._extractQueryFields(criteria);
|
|
10417
|
+
const nonIndexedFields = queryFields.filter(field => !indexedFields.includes(field));
|
|
10418
|
+
if (nonIndexedFields.length > 0) {
|
|
10419
|
+
if (this.opts.debugMode) {
|
|
10420
|
+
console.warn(`⚠️ Delete operation using non-indexed fields: ${nonIndexedFields.join(', ')}`);
|
|
10421
|
+
console.warn(` This may be slow or fail silently. Consider indexing these fields.`);
|
|
10422
|
+
}
|
|
10423
|
+
}
|
|
10424
|
+
}
|
|
10079
10425
|
const records = await this.find(criteria);
|
|
10080
10426
|
const deletedIds = [];
|
|
10081
10427
|
if (this.opts.debugMode) {
|
|
@@ -11628,14 +11974,30 @@ class Database extends events.EventEmitter {
|
|
|
11628
11974
|
try {
|
|
11629
11975
|
const arrayData = JSON.parse(trimmedLine);
|
|
11630
11976
|
if (Array.isArray(arrayData) && arrayData.length > 0) {
|
|
11631
|
-
//
|
|
11632
|
-
//
|
|
11633
|
-
if (
|
|
11634
|
-
|
|
11635
|
-
|
|
11977
|
+
// CRITICAL FIX: Use schema to find ID position, not hardcoded position
|
|
11978
|
+
// The schema defines the order of fields in the array
|
|
11979
|
+
if (this.serializer && this.serializer.schemaManager && this.serializer.schemaManager.isInitialized) {
|
|
11980
|
+
const schema = this.serializer.schemaManager.getSchema();
|
|
11981
|
+
const idIndex = schema.indexOf('id');
|
|
11982
|
+
if (idIndex !== -1 && arrayData.length > idIndex) {
|
|
11983
|
+
// ID is at the position defined by schema
|
|
11984
|
+
recordId = arrayData[idIndex];
|
|
11985
|
+
} else if (arrayData.length > schema.length) {
|
|
11986
|
+
// ID might be appended after schema fields (for backward compatibility)
|
|
11987
|
+
recordId = arrayData[schema.length];
|
|
11988
|
+
} else {
|
|
11989
|
+
// Fallback: use first element
|
|
11990
|
+
recordId = arrayData[0];
|
|
11991
|
+
}
|
|
11636
11992
|
} else {
|
|
11637
|
-
//
|
|
11638
|
-
|
|
11993
|
+
// No schema available, try common positions
|
|
11994
|
+
if (arrayData.length > 2) {
|
|
11995
|
+
// Try position 2 (common in older formats)
|
|
11996
|
+
recordId = arrayData[2];
|
|
11997
|
+
} else {
|
|
11998
|
+
// Fallback: use first element
|
|
11999
|
+
recordId = arrayData[0];
|
|
12000
|
+
}
|
|
11639
12001
|
}
|
|
11640
12002
|
if (recordId !== undefined && recordId !== null) {
|
|
11641
12003
|
recordId = String(recordId);
|
|
@@ -11717,7 +12079,7 @@ class Database extends events.EventEmitter {
|
|
|
11717
12079
|
} else if (!deletedIdsSnapshot.has(String(recordWithIds.id))) {
|
|
11718
12080
|
// Keep existing record if not deleted
|
|
11719
12081
|
if (this.opts.debugMode) {
|
|
11720
|
-
console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`);
|
|
12082
|
+
console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - not in deletedIdsSnapshot`);
|
|
11721
12083
|
}
|
|
11722
12084
|
return {
|
|
11723
12085
|
type: 'kept',
|
|
@@ -11728,7 +12090,9 @@ class Database extends events.EventEmitter {
|
|
|
11728
12090
|
} else {
|
|
11729
12091
|
// Skip deleted record
|
|
11730
12092
|
if (this.opts.debugMode) {
|
|
11731
|
-
console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted`);
|
|
12093
|
+
console.log(`💾 Save: Skipped record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'}) - deleted (found in deletedIdsSnapshot)`);
|
|
12094
|
+
console.log(`💾 Save: deletedIdsSnapshot contains:`, Array.from(deletedIdsSnapshot));
|
|
12095
|
+
console.log(`💾 Save: Record ID check: String(${recordWithIds.id}) = "${String(recordWithIds.id)}", has() = ${deletedIdsSnapshot.has(String(recordWithIds.id))}`);
|
|
11732
12096
|
}
|
|
11733
12097
|
return {
|
|
11734
12098
|
type: 'deleted',
|
|
@@ -11771,6 +12135,54 @@ class Database extends events.EventEmitter {
|
|
|
11771
12135
|
const offset = parseInt(rangeKey);
|
|
11772
12136
|
switch (result.type) {
|
|
11773
12137
|
case 'unchanged':
|
|
12138
|
+
// CRITICAL FIX: Verify that unchanged records are not deleted
|
|
12139
|
+
// Extract ID from the line to check against deletedIdsSnapshot
|
|
12140
|
+
let unchangedRecordId = null;
|
|
12141
|
+
try {
|
|
12142
|
+
if (result.line.startsWith('[') && result.line.endsWith(']')) {
|
|
12143
|
+
const arrayData = JSON.parse(result.line);
|
|
12144
|
+
if (Array.isArray(arrayData) && arrayData.length > 0) {
|
|
12145
|
+
// CRITICAL FIX: Use schema to find ID position, not hardcoded position
|
|
12146
|
+
if (this.serializer && this.serializer.schemaManager && this.serializer.schemaManager.isInitialized) {
|
|
12147
|
+
const schema = this.serializer.schemaManager.getSchema();
|
|
12148
|
+
const idIndex = schema.indexOf('id');
|
|
12149
|
+
if (idIndex !== -1 && arrayData.length > idIndex) {
|
|
12150
|
+
unchangedRecordId = String(arrayData[idIndex]);
|
|
12151
|
+
} else if (arrayData.length > schema.length) {
|
|
12152
|
+
unchangedRecordId = String(arrayData[schema.length]);
|
|
12153
|
+
} else {
|
|
12154
|
+
unchangedRecordId = String(arrayData[0]);
|
|
12155
|
+
}
|
|
12156
|
+
} else {
|
|
12157
|
+
// No schema, try common positions
|
|
12158
|
+
if (arrayData.length > 2) {
|
|
12159
|
+
unchangedRecordId = String(arrayData[2]);
|
|
12160
|
+
} else {
|
|
12161
|
+
unchangedRecordId = String(arrayData[0]);
|
|
12162
|
+
}
|
|
12163
|
+
}
|
|
12164
|
+
}
|
|
12165
|
+
} else {
|
|
12166
|
+
const obj = JSON.parse(result.line);
|
|
12167
|
+
unchangedRecordId = obj.id ? String(obj.id) : null;
|
|
12168
|
+
}
|
|
12169
|
+
} catch (e) {
|
|
12170
|
+
// If we can't parse, skip this record to be safe
|
|
12171
|
+
if (this.opts.debugMode) {
|
|
12172
|
+
console.log(`💾 Save: Could not parse unchanged record to check deletion: ${e.message}`);
|
|
12173
|
+
}
|
|
12174
|
+
continue;
|
|
12175
|
+
}
|
|
12176
|
+
|
|
12177
|
+
// Skip if this record is deleted
|
|
12178
|
+
if (unchangedRecordId && deletedIdsSnapshot.has(unchangedRecordId)) {
|
|
12179
|
+
if (this.opts.debugMode) {
|
|
12180
|
+
console.log(`💾 Save: Skipping unchanged record ${unchangedRecordId} - deleted`);
|
|
12181
|
+
}
|
|
12182
|
+
deletedOffsets.add(offset);
|
|
12183
|
+
break;
|
|
12184
|
+
}
|
|
12185
|
+
|
|
11774
12186
|
// Collect unchanged lines for batch processing
|
|
11775
12187
|
unchangedLines.push(result.line);
|
|
11776
12188
|
keptRecords.push({
|
|
@@ -11967,6 +12379,74 @@ class Database extends events.EventEmitter {
|
|
|
11967
12379
|
}
|
|
11968
12380
|
return this._getWriteBufferBaseLineNumber() + writeBufferIndex;
|
|
11969
12381
|
}
|
|
12382
|
+
|
|
12383
|
+
/**
|
|
12384
|
+
* Attempts to recover a corrupted line by cleaning invalid characters and fixing common JSON issues
|
|
12385
|
+
* @param {string} line - The corrupted line to recover
|
|
12386
|
+
* @returns {string|null} - The recovered line or null if recovery is not possible
|
|
12387
|
+
*/
|
|
12388
|
+
_tryRecoverLine(line) {
|
|
12389
|
+
if (!line || typeof line !== 'string') {
|
|
12390
|
+
return null;
|
|
12391
|
+
}
|
|
12392
|
+
try {
|
|
12393
|
+
// Try parsing as-is first
|
|
12394
|
+
JSON.parse(line);
|
|
12395
|
+
return line; // Line is already valid
|
|
12396
|
+
} catch (e) {
|
|
12397
|
+
// Line is corrupted, attempt recovery
|
|
12398
|
+
}
|
|
12399
|
+
let recovered = line.trim();
|
|
12400
|
+
|
|
12401
|
+
// Remove invalid control characters (except \n, \r, \t)
|
|
12402
|
+
recovered = recovered.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
12403
|
+
|
|
12404
|
+
// Try to close unclosed strings
|
|
12405
|
+
// Count quotes and ensure they're balanced
|
|
12406
|
+
const quoteCount = (recovered.match(/"/g) || []).length;
|
|
12407
|
+
if (quoteCount % 2 !== 0) {
|
|
12408
|
+
// Odd number of quotes - try to close the string
|
|
12409
|
+
const lastQuoteIndex = recovered.lastIndexOf('"');
|
|
12410
|
+
if (lastQuoteIndex > 0) {
|
|
12411
|
+
// Check if we're inside a string (not escaped)
|
|
12412
|
+
const beforeLastQuote = recovered.substring(0, lastQuoteIndex);
|
|
12413
|
+
const escapedQuotes = (beforeLastQuote.match(/\\"/g) || []).length;
|
|
12414
|
+
const unescapedQuotes = (beforeLastQuote.match(/"/g) || []).length - escapedQuotes;
|
|
12415
|
+
if (unescapedQuotes % 2 !== 0) {
|
|
12416
|
+
// We're inside an unclosed string - try to close it
|
|
12417
|
+
recovered = recovered + '"';
|
|
12418
|
+
}
|
|
12419
|
+
}
|
|
12420
|
+
}
|
|
12421
|
+
|
|
12422
|
+
// Try to close unclosed arrays/objects
|
|
12423
|
+
const openBraces = (recovered.match(/\{/g) || []).length;
|
|
12424
|
+
const closeBraces = (recovered.match(/\}/g) || []).length;
|
|
12425
|
+
const openBrackets = (recovered.match(/\[/g) || []).length;
|
|
12426
|
+
const closeBrackets = (recovered.match(/\]/g) || []).length;
|
|
12427
|
+
|
|
12428
|
+
// Remove trailing commas before closing braces/brackets
|
|
12429
|
+
recovered = recovered.replace(/,\s*([}\]])/g, '$1');
|
|
12430
|
+
|
|
12431
|
+
// Try to close arrays
|
|
12432
|
+
if (openBrackets > closeBrackets) {
|
|
12433
|
+
recovered = recovered + ']'.repeat(openBrackets - closeBrackets);
|
|
12434
|
+
}
|
|
12435
|
+
|
|
12436
|
+
// Try to close objects
|
|
12437
|
+
if (openBraces > closeBraces) {
|
|
12438
|
+
recovered = recovered + '}'.repeat(openBraces - closeBraces);
|
|
12439
|
+
}
|
|
12440
|
+
|
|
12441
|
+
// Final validation - try to parse
|
|
12442
|
+
try {
|
|
12443
|
+
JSON.parse(recovered);
|
|
12444
|
+
return recovered;
|
|
12445
|
+
} catch (e) {
|
|
12446
|
+
// Recovery failed
|
|
12447
|
+
return null;
|
|
12448
|
+
}
|
|
12449
|
+
}
|
|
11970
12450
|
_streamingRecoveryGenerator(_x, _x2) {
|
|
11971
12451
|
var _this = this;
|
|
11972
12452
|
return _wrapAsyncGenerator(function* (criteria, options, alreadyYielded = 0, map = null, remainingSkipValue = 0) {
|
|
@@ -12166,6 +12646,17 @@ class Database extends events.EventEmitter {
|
|
|
12166
12646
|
|
|
12167
12647
|
// If no data at all, return empty
|
|
12168
12648
|
if (_this2.indexOffset === 0 && _this2.writeBuffer.length === 0) return;
|
|
12649
|
+
|
|
12650
|
+
// CRITICAL FIX: Wait for any ongoing save operations to complete
|
|
12651
|
+
// This prevents reading partially written data
|
|
12652
|
+
if (_this2.isSaving) {
|
|
12653
|
+
if (_this2.opts.debugMode) {
|
|
12654
|
+
console.log('🔍 walk(): waiting for save operation to complete');
|
|
12655
|
+
}
|
|
12656
|
+
while (_this2.isSaving) {
|
|
12657
|
+
yield _awaitAsyncGenerator(new Promise(resolve => setTimeout(resolve, 10)));
|
|
12658
|
+
}
|
|
12659
|
+
}
|
|
12169
12660
|
let count = 0;
|
|
12170
12661
|
let remainingSkip = options.skip || 0;
|
|
12171
12662
|
let map;
|
|
@@ -12311,10 +12802,49 @@ class Database extends events.EventEmitter {
|
|
|
12311
12802
|
} catch (error) {
|
|
12312
12803
|
// CRITICAL FIX: Log deserialization errors instead of silently ignoring them
|
|
12313
12804
|
// This helps identify data corruption issues
|
|
12314
|
-
if (
|
|
12805
|
+
if (_this2.opts.debugMode) {
|
|
12315
12806
|
console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`);
|
|
12316
12807
|
console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`);
|
|
12317
12808
|
}
|
|
12809
|
+
|
|
12810
|
+
// CRITICAL FIX: Attempt to recover corrupted line before giving up
|
|
12811
|
+
const recoveredLine = _this2._tryRecoverLine(row.line);
|
|
12812
|
+
if (recoveredLine) {
|
|
12813
|
+
try {
|
|
12814
|
+
const record = _this2.serializer.deserialize(recoveredLine);
|
|
12815
|
+
if (record !== null) {
|
|
12816
|
+
_this2.integrityCorrections.jsonRecovery++;
|
|
12817
|
+
console.log(`🔧 Recovered corrupted JSON line (${_this2.integrityCorrections.jsonRecovery} recoveries)`);
|
|
12818
|
+
if (_this2.integrityCorrections.jsonRecovery > 20) {
|
|
12819
|
+
console.warn(`⚠️ Frequent JSON recovery detected (${_this2.integrityCorrections.jsonRecovery} times) - may indicate data corruption`);
|
|
12820
|
+
}
|
|
12821
|
+
const recordWithTerms = _this2.restoreTermIdsAfterDeserialization(record);
|
|
12822
|
+
if (remainingSkip > 0) {
|
|
12823
|
+
remainingSkip--;
|
|
12824
|
+
continue;
|
|
12825
|
+
}
|
|
12826
|
+
count++;
|
|
12827
|
+
if (options.includeOffsets) {
|
|
12828
|
+
yield {
|
|
12829
|
+
entry: recordWithTerms,
|
|
12830
|
+
start: row.start,
|
|
12831
|
+
_: row._ || 0
|
|
12832
|
+
};
|
|
12833
|
+
} else {
|
|
12834
|
+
if (_this2.opts.includeLinePosition) {
|
|
12835
|
+
recordWithTerms._ = row._ || 0;
|
|
12836
|
+
}
|
|
12837
|
+
yield recordWithTerms;
|
|
12838
|
+
}
|
|
12839
|
+
continue; // Successfully recovered and yielded
|
|
12840
|
+
}
|
|
12841
|
+
} catch (recoveryError) {
|
|
12842
|
+
// Recovery attempt failed, continue with normal error handling
|
|
12843
|
+
if (_this2.opts.debugMode) {
|
|
12844
|
+
console.warn(`⚠️ walk(): Line recovery failed: ${recoveryError.message}`);
|
|
12845
|
+
}
|
|
12846
|
+
}
|
|
12847
|
+
}
|
|
12318
12848
|
if (!_this2._offsetRecoveryInProgress) {
|
|
12319
12849
|
var _iteratorAbruptCompletion5 = false;
|
|
12320
12850
|
var _didIteratorError5 = false;
|
|
@@ -12463,10 +12993,52 @@ class Database extends events.EventEmitter {
|
|
|
12463
12993
|
} catch (error) {
|
|
12464
12994
|
// CRITICAL FIX: Log deserialization errors instead of silently ignoring them
|
|
12465
12995
|
// This helps identify data corruption issues
|
|
12466
|
-
if (
|
|
12996
|
+
if (_this2.opts.debugMode) {
|
|
12467
12997
|
console.warn(`⚠️ walk(): Failed to deserialize record at offset ${row.start}: ${error.message}`);
|
|
12468
12998
|
console.warn(`⚠️ walk(): Problematic line (first 200 chars): ${row.line.substring(0, 200)}`);
|
|
12469
12999
|
}
|
|
13000
|
+
|
|
13001
|
+
// CRITICAL FIX: Attempt to recover corrupted line before giving up
|
|
13002
|
+
const recoveredLine = _this2._tryRecoverLine(row.line);
|
|
13003
|
+
if (recoveredLine) {
|
|
13004
|
+
try {
|
|
13005
|
+
const entry = yield _awaitAsyncGenerator(_this2.serializer.deserialize(recoveredLine, {
|
|
13006
|
+
compress: _this2.opts.compress,
|
|
13007
|
+
v8: _this2.opts.v8
|
|
13008
|
+
}));
|
|
13009
|
+
if (entry !== null) {
|
|
13010
|
+
_this2.integrityCorrections.jsonRecovery++;
|
|
13011
|
+
console.log(`🔧 Recovered corrupted JSON line (${_this2.integrityCorrections.jsonRecovery} recoveries)`);
|
|
13012
|
+
if (_this2.integrityCorrections.jsonRecovery > 20) {
|
|
13013
|
+
console.warn(`⚠️ Frequent JSON recovery detected (${_this2.integrityCorrections.jsonRecovery} times) - may indicate data corruption`);
|
|
13014
|
+
}
|
|
13015
|
+
const entryWithTerms = _this2.restoreTermIdsAfterDeserialization(entry);
|
|
13016
|
+
if (remainingSkip > 0) {
|
|
13017
|
+
remainingSkip--;
|
|
13018
|
+
continue;
|
|
13019
|
+
}
|
|
13020
|
+
count++;
|
|
13021
|
+
if (options.includeOffsets) {
|
|
13022
|
+
yield {
|
|
13023
|
+
entry: entryWithTerms,
|
|
13024
|
+
start: row.start,
|
|
13025
|
+
_: row._ || _this2.offsets.findIndex(n => n === row.start)
|
|
13026
|
+
};
|
|
13027
|
+
} else {
|
|
13028
|
+
if (_this2.opts.includeLinePosition) {
|
|
13029
|
+
entryWithTerms._ = row._ || _this2.offsets.findIndex(n => n === row.start);
|
|
13030
|
+
}
|
|
13031
|
+
yield entryWithTerms;
|
|
13032
|
+
}
|
|
13033
|
+
continue; // Successfully recovered and yielded
|
|
13034
|
+
}
|
|
13035
|
+
} catch (recoveryError) {
|
|
13036
|
+
// Recovery attempt failed, continue with normal error handling
|
|
13037
|
+
if (_this2.opts.debugMode) {
|
|
13038
|
+
console.warn(`⚠️ walk(): Line recovery failed: ${recoveryError.message}`);
|
|
13039
|
+
}
|
|
13040
|
+
}
|
|
13041
|
+
}
|
|
12470
13042
|
if (!_this2._offsetRecoveryInProgress) {
|
|
12471
13043
|
var _iteratorAbruptCompletion7 = false;
|
|
12472
13044
|
var _didIteratorError7 = false;
|
|
@@ -12759,7 +13331,11 @@ class Database extends events.EventEmitter {
|
|
|
12759
13331
|
await this.save();
|
|
12760
13332
|
// Ensure writeBuffer is cleared after save
|
|
12761
13333
|
if (this.writeBuffer.length > 0) {
|
|
12762
|
-
|
|
13334
|
+
this.integrityCorrections.writeBufferForced++;
|
|
13335
|
+
console.log(`🔧 Forced WriteBuffer clear after save (${this.writeBuffer.length} items remaining)`);
|
|
13336
|
+
if (this.integrityCorrections.writeBufferForced > 3) {
|
|
13337
|
+
console.warn(`⚠️ Frequent WriteBuffer clear issues detected (${this.integrityCorrections.writeBufferForced} times)`);
|
|
13338
|
+
}
|
|
12763
13339
|
this.writeBuffer = [];
|
|
12764
13340
|
this.writeBufferOffsets = [];
|
|
12765
13341
|
this.writeBufferSizes = [];
|
|
@@ -12880,7 +13456,8 @@ class Database extends events.EventEmitter {
|
|
|
12880
13456
|
console.log(`💾 Index data saved to ${idxPath}`);
|
|
12881
13457
|
}
|
|
12882
13458
|
} catch (error) {
|
|
12883
|
-
|
|
13459
|
+
this.integrityCorrections.indexSaveFailures++;
|
|
13460
|
+
console.warn(`⚠️ Index save failure (${this.integrityCorrections.indexSaveFailures} times): ${error.message}`);
|
|
12884
13461
|
throw error; // Re-throw to let caller handle
|
|
12885
13462
|
}
|
|
12886
13463
|
}
|
|
@@ -12951,4 +13528,3 @@ class Database extends events.EventEmitter {
|
|
|
12951
13528
|
}
|
|
12952
13529
|
|
|
12953
13530
|
exports.Database = Database;
|
|
12954
|
-
exports.default = Database;
|