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/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
- // CRITICAL FIX: Handle schema migration where 'id' was first field in old schema
2412
- // but is not in current schema. Check if first element looks like an ID.
2413
- // Only do this if:
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(data);
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(data);
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(data);
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(data);
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(dataArray);
3117
+ this.validateEncodingBeforeSerialization(sanitizedDataArray);
3089
3118
 
3090
3119
  // Convert all objects to array format for optimization
3091
- const convertedData = dataArray.map(data => this.convertToArrayFormat(data));
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: Remove trailing newlines and whitespace for single range too
3919
- // Optimized: Use trimEnd() which efficiently removes all trailing whitespace (faster than manual checks)
3920
- lineString = lineString.trimEnd();
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 newlines to prevent corruption
3958
- const lines = content.split('\n').filter(line => line.trim().length > 0);
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: rangeContent,
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
- // Check if file exists before trying to read it
4018
- if (!(yield _awaitAsyncGenerator(_this2.exists()))) {
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
- const groupedRanges = yield _awaitAsyncGenerator(_this2.groupedRanges(ranges));
4024
- for (const groupedRange of groupedRanges) {
4025
- var _iteratorAbruptCompletion2 = false;
4026
- var _didIteratorError2 = false;
4027
- var _iteratorError2;
4028
- try {
4029
- for (var _iterator2 = _asyncIterator(_this2.readGroupedRange(groupedRange, fd)), _step2; _iteratorAbruptCompletion2 = !(_step2 = yield _awaitAsyncGenerator(_iterator2.next())).done; _iteratorAbruptCompletion2 = false) {
4030
- const row = _step2.value;
4031
- {
4032
- yield row;
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
- if (_iteratorAbruptCompletion2 && _iterator2.return != null) {
4041
- yield _awaitAsyncGenerator(_iterator2.return());
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
- if (_didIteratorError2) {
4045
- throw _iteratorError2;
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
- yield _awaitAsyncGenerator(fd.close());
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
- console.warn(`⚠️ Invalid JSON in temp file at line ${i + 1}, skipping:`, lines[i].substring(0, 100));
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
- console.warn(`UTF-8 decoding failed for file ${this.file}, attempting recovery`);
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
- console.warn(`⚠️ Warning: The following array:string indexed fields were not added to term mapping: ${arrayStringFields.join(', ')}. This may impact performance.`);
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
- if (!this.opts.indexes) return [];
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 && this.offsets.length > 0) {
8637
+ if (this.indexManager) {
8378
8638
  this.indexManager.setTotalLines(this.offsets.length);
8379
- }
8380
- if (this.opts.debugMode) {
8381
- console.log(`📂 Loaded ${this.offsets.length} offsets from ${idxPath}`);
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
- // Clear writeBuffer and deletedIds after successful save only if we had data to save
9097
- if (allData.length > 0) {
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: Use parallel processing for better performance when writeBuffer has many records
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
- // Merge efficiently: keep file results not in writeBuffer, then add all writeBuffer results
9701
- const filteredFileResults = await Promise.resolve(fileResultsWithTerms.filter(r => !writeBufferSet.has(r.id)));
9702
- allResults = [...filteredFileResults, ...writeBufferResultsWithTerms];
9703
- } else {
9704
- // Sequential approach for small writeBuffer (original logic)
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
- // For arrays without explicit ID, use the first element as a fallback
11632
- // or try to find the ID field if it exists
11633
- if (arrayData.length > 2) {
11634
- // ID is typically at position 2 in array format [age, city, id, name]
11635
- recordId = arrayData[2];
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
- // For arrays without ID field, use first element as fallback
11638
- recordId = arrayData[0];
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 (1 || _this2.opts.debugMode) {
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 (1 || _this2.opts.debugMode) {
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
- console.warn('⚠️ WriteBuffer not cleared after save() - forcing clear');
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
- console.warn('Failed to save index data:', error.message);
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;