jexidb 2.1.1 → 2.1.3
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 +7981 -231
- package/package.json +9 -2
- package/src/Database.mjs +372 -154
- package/src/SchemaManager.mjs +325 -268
- package/src/Serializer.mjs +20 -1
- package/src/managers/QueryManager.mjs +74 -18
- package/.babelrc +0 -13
- package/.gitattributes +0 -2
- package/CHANGELOG.md +0 -140
- package/babel.config.json +0 -5
- package/docs/API.md +0 -1057
- package/docs/EXAMPLES.md +0 -701
- package/docs/README.md +0 -194
- package/examples/iterate-usage-example.js +0 -157
- package/examples/simple-iterate-example.js +0 -115
- package/jest.config.js +0 -24
- package/scripts/README.md +0 -47
- package/scripts/benchmark-array-serialization.js +0 -108
- package/scripts/clean-test-files.js +0 -75
- package/scripts/prepare.js +0 -31
- package/scripts/run-tests.js +0 -80
- package/scripts/score-mode-demo.js +0 -45
- package/test/$not-operator-with-and.test.js +0 -282
- package/test/README.md +0 -8
- package/test/close-init-cycle.test.js +0 -256
- package/test/coverage-method.test.js +0 -93
- package/test/critical-bugs-fixes.test.js +0 -1069
- package/test/deserialize-corruption-fixes.test.js +0 -296
- package/test/exists-method.test.js +0 -318
- package/test/explicit-indexes-comparison.test.js +0 -219
- package/test/filehandler-non-adjacent-ranges-bug.test.js +0 -175
- package/test/index-line-number-regression.test.js +0 -100
- package/test/index-missing-index-data.test.js +0 -91
- package/test/index-persistence.test.js +0 -491
- package/test/index-serialization.test.js +0 -314
- package/test/indexed-query-mode.test.js +0 -360
- package/test/insert-session-auto-flush.test.js +0 -353
- package/test/iterate-method.test.js +0 -272
- package/test/legacy-operator-compat.test.js +0 -154
- package/test/query-operators.test.js +0 -238
- package/test/regex-array-fields.test.js +0 -129
- package/test/score-method.test.js +0 -298
- package/test/setup.js +0 -17
- package/test/term-mapping-minimal.test.js +0 -154
- package/test/term-mapping-simple.test.js +0 -257
- package/test/term-mapping.test.js +0 -514
- package/test/writebuffer-flush-resilience.test.js +0 -204
package/src/Database.mjs
CHANGED
|
@@ -545,6 +545,32 @@ class Database extends EventEmitter {
|
|
|
545
545
|
return
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
+
// Handle legacy 'schema' option migration
|
|
549
|
+
if (this.opts.schema) {
|
|
550
|
+
// If fields is already provided and valid, ignore schema
|
|
551
|
+
if (this.opts.fields && typeof this.opts.fields === 'object' && Object.keys(this.opts.fields).length > 0) {
|
|
552
|
+
if (this.opts.debugMode) {
|
|
553
|
+
console.log(`⚠️ Both 'schema' and 'fields' options provided. Ignoring 'schema' and using 'fields'. [${this.instanceId}]`)
|
|
554
|
+
}
|
|
555
|
+
} else if (Array.isArray(this.opts.schema)) {
|
|
556
|
+
// Schema as array is no longer supported
|
|
557
|
+
throw new Error('The "schema" option as an array is no longer supported. Please use "fields" as an object instead. Example: { fields: { id: "number", name: "string" } }')
|
|
558
|
+
} else if (typeof this.opts.schema === 'object' && this.opts.schema !== null) {
|
|
559
|
+
// Schema as object - migrate to fields
|
|
560
|
+
this.opts.fields = { ...this.opts.schema }
|
|
561
|
+
if (this.opts.debugMode) {
|
|
562
|
+
console.log(`⚠️ Migrated 'schema' option to 'fields'. Please update your code to use 'fields' instead of 'schema'. [${this.instanceId}]`)
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
throw new Error('The "schema" option must be an object. Example: { schema: { id: "number", name: "string" } }')
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Validate that fields is provided (mandatory)
|
|
570
|
+
if (!this.opts.fields || typeof this.opts.fields !== 'object' || Object.keys(this.opts.fields).length === 0) {
|
|
571
|
+
throw new Error('The "fields" option is mandatory and must be an object with at least one field definition. Example: { fields: { id: "number", name: "string" } }')
|
|
572
|
+
}
|
|
573
|
+
|
|
548
574
|
// CRITICAL FIX: Initialize serializer first - this was missing and causing crashes
|
|
549
575
|
this.serializer = new Serializer(this.opts)
|
|
550
576
|
|
|
@@ -1027,12 +1053,22 @@ class Database extends EventEmitter {
|
|
|
1027
1053
|
}
|
|
1028
1054
|
}
|
|
1029
1055
|
|
|
1030
|
-
// Reinitialize schema from saved configuration
|
|
1031
|
-
|
|
1056
|
+
// Reinitialize schema from saved configuration (only if fields not provided)
|
|
1057
|
+
// Note: fields option takes precedence over saved schema
|
|
1058
|
+
if (!this.opts.fields && config.schema && this.serializer) {
|
|
1032
1059
|
this.serializer.initializeSchema(config.schema)
|
|
1033
1060
|
if (this.opts.debugMode) {
|
|
1034
1061
|
console.log(`📂 Loaded schema from ${idxPath}:`, config.schema.join(', '))
|
|
1035
1062
|
}
|
|
1063
|
+
} else if (this.opts.fields && this.serializer) {
|
|
1064
|
+
// Use fields option instead of saved schema
|
|
1065
|
+
const fieldNames = Object.keys(this.opts.fields)
|
|
1066
|
+
if (fieldNames.length > 0) {
|
|
1067
|
+
this.serializer.initializeSchema(fieldNames)
|
|
1068
|
+
if (this.opts.debugMode) {
|
|
1069
|
+
console.log(`📂 Schema initialized from fields option:`, fieldNames.join(', '))
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1036
1072
|
}
|
|
1037
1073
|
}
|
|
1038
1074
|
}
|
|
@@ -1263,7 +1299,8 @@ class Database extends EventEmitter {
|
|
|
1263
1299
|
|
|
1264
1300
|
// CRITICAL FIX: Capture writeBuffer and deletedIds at the start to prevent race conditions
|
|
1265
1301
|
const writeBufferSnapshot = [...this.writeBuffer]
|
|
1266
|
-
|
|
1302
|
+
// CRITICAL FIX: Normalize deleted IDs to strings for consistent comparison
|
|
1303
|
+
const deletedIdsSnapshot = new Set(Array.from(this.deletedIds).map(id => String(id)))
|
|
1267
1304
|
|
|
1268
1305
|
// OPTIMIZATION: Process pending index updates in batch before save
|
|
1269
1306
|
if (this.pendingIndexUpdates && this.pendingIndexUpdates.length > 0) {
|
|
@@ -1282,40 +1319,25 @@ class Database extends EventEmitter {
|
|
|
1282
1319
|
this.pendingIndexUpdates = []
|
|
1283
1320
|
}
|
|
1284
1321
|
|
|
1285
|
-
// CRITICAL FIX:
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
//
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
// CRITICAL FIX: Verify write buffer is empty after I/O completion
|
|
1292
|
-
// But allow for ongoing insertions during high-volume scenarios
|
|
1293
|
-
if (this.writeBuffer.length > 0) {
|
|
1294
|
-
if (this.opts.debugMode) {
|
|
1295
|
-
console.log(`💾 Save: WriteBuffer still has ${this.writeBuffer.length} items after flush - this may indicate ongoing insertions`)
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// If we have a reasonable number of items, continue processing
|
|
1299
|
-
if (this.writeBuffer.length < 10000) { // Reasonable threshold
|
|
1300
|
-
if (this.opts.debugMode) {
|
|
1301
|
-
console.log(`💾 Save: Continuing to process remaining ${this.writeBuffer.length} items`)
|
|
1302
|
-
}
|
|
1303
|
-
// Continue with the save process - the remaining items will be included in the final save
|
|
1304
|
-
} else {
|
|
1305
|
-
// Too many items remaining - likely a real problem
|
|
1306
|
-
throw new Error(`WriteBuffer has too many items after flush: ${this.writeBuffer.length} items remaining (threshold: 10000)`)
|
|
1307
|
-
}
|
|
1322
|
+
// CRITICAL FIX: DO NOT flush writeBuffer before processing existing records
|
|
1323
|
+
// This prevents duplicating updated records in the file.
|
|
1324
|
+
// The _streamExistingRecords() will handle replacing old records with updated ones from writeBufferSnapshot.
|
|
1325
|
+
// After processing, all records (existing + updated + new) will be written to file in one operation.
|
|
1326
|
+
if (this.opts.debugMode) {
|
|
1327
|
+
console.log(`💾 Save: writeBufferSnapshot captured with ${writeBufferSnapshot.length} records (will be processed with existing records)`)
|
|
1308
1328
|
}
|
|
1309
1329
|
|
|
1310
1330
|
// OPTIMIZATION: Parallel operations - cleanup and data preparation
|
|
1311
1331
|
let allData = []
|
|
1312
1332
|
let orphanedCount = 0
|
|
1313
1333
|
|
|
1314
|
-
// Check if there are
|
|
1334
|
+
// Check if there are records to save from writeBufferSnapshot
|
|
1335
|
+
// CRITICAL FIX: Process writeBufferSnapshot records (both new and updated) with existing records
|
|
1336
|
+
// Updated records will replace old ones via _streamExistingRecords, new records will be added
|
|
1315
1337
|
if (this.opts.debugMode) {
|
|
1316
1338
|
console.log(`💾 Save: writeBuffer.length=${this.writeBuffer.length}, writeBufferSnapshot.length=${writeBufferSnapshot.length}`)
|
|
1317
1339
|
}
|
|
1318
|
-
if (this.writeBuffer.length > 0) {
|
|
1340
|
+
if (this.writeBuffer.length > 0 || writeBufferSnapshot.length > 0) {
|
|
1319
1341
|
if (this.opts.debugMode) {
|
|
1320
1342
|
console.log(`💾 Save: WriteBuffer has ${writeBufferSnapshot.length} records, using streaming approach`)
|
|
1321
1343
|
}
|
|
@@ -1349,20 +1371,57 @@ class Database extends EventEmitter {
|
|
|
1349
1371
|
// Add streaming operation
|
|
1350
1372
|
parallelOperations.push(
|
|
1351
1373
|
this._streamExistingRecords(deletedIdsSnapshot, writeBufferSnapshot).then(existingRecords => {
|
|
1374
|
+
// CRITICAL FIX: _streamExistingRecords already handles updates via updatedRecordsMap
|
|
1375
|
+
// So existingRecords already contains updated records from writeBufferSnapshot
|
|
1376
|
+
// We only need to add records from writeBufferSnapshot that are NEW (not updates)
|
|
1352
1377
|
allData = [...existingRecords]
|
|
1353
1378
|
|
|
1354
|
-
// OPTIMIZATION: Use
|
|
1355
|
-
|
|
1379
|
+
// OPTIMIZATION: Use Set for faster lookups of existing record IDs
|
|
1380
|
+
// CRITICAL FIX: Normalize IDs to strings for consistent comparison
|
|
1381
|
+
const existingRecordIds = new Set(existingRecords.filter(r => r && r.id).map(r => String(r.id)))
|
|
1356
1382
|
|
|
1383
|
+
// CRITICAL FIX: Create a map of records in existingRecords by ID for comparison
|
|
1384
|
+
const existingRecordsById = new Map()
|
|
1385
|
+
existingRecords.forEach(r => {
|
|
1386
|
+
if (r && r.id) {
|
|
1387
|
+
existingRecordsById.set(String(r.id), r)
|
|
1388
|
+
}
|
|
1389
|
+
})
|
|
1390
|
+
|
|
1391
|
+
// Add only NEW records from writeBufferSnapshot (not updates, as those are already in existingRecords)
|
|
1392
|
+
// CRITICAL FIX: Also ensure that if an updated record wasn't properly replaced, we replace it now
|
|
1357
1393
|
for (const record of writeBufferSnapshot) {
|
|
1358
|
-
if (!
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1394
|
+
if (!record || !record.id) continue
|
|
1395
|
+
if (deletedIdsSnapshot.has(String(record.id))) continue
|
|
1396
|
+
|
|
1397
|
+
const recordIdStr = String(record.id)
|
|
1398
|
+
const existingRecord = existingRecordsById.get(recordIdStr)
|
|
1399
|
+
|
|
1400
|
+
if (!existingRecord) {
|
|
1401
|
+
// This is a new record, not an update
|
|
1402
|
+
allData.push(record)
|
|
1403
|
+
if (this.opts.debugMode) {
|
|
1404
|
+
console.log(`💾 Save: Adding NEW record to allData:`, { id: recordIdStr, price: record.price, app_id: record.app_id, currency: record.currency })
|
|
1405
|
+
}
|
|
1406
|
+
} else {
|
|
1407
|
+
// This is an update - verify that existingRecords contains the updated version
|
|
1408
|
+
// If not, replace it (this handles edge cases where substitution might have failed)
|
|
1409
|
+
const existingIndex = allData.findIndex(r => r && r.id && String(r.id) === recordIdStr)
|
|
1410
|
+
if (existingIndex !== -1) {
|
|
1411
|
+
// Verify if the existing record is actually the updated one
|
|
1412
|
+
// Compare key fields to detect if replacement is needed
|
|
1413
|
+
const needsReplacement = JSON.stringify(allData[existingIndex]) !== JSON.stringify(record)
|
|
1414
|
+
if (needsReplacement) {
|
|
1415
|
+
if (this.opts.debugMode) {
|
|
1416
|
+
console.log(`💾 Save: REPLACING existing record with updated version in allData:`, {
|
|
1417
|
+
old: { id: String(allData[existingIndex].id), price: allData[existingIndex].price },
|
|
1418
|
+
new: { id: recordIdStr, price: record.price }
|
|
1419
|
+
})
|
|
1420
|
+
}
|
|
1421
|
+
allData[existingIndex] = record
|
|
1422
|
+
} else if (this.opts.debugMode) {
|
|
1423
|
+
console.log(`💾 Save: Record already correctly updated in allData:`, { id: recordIdStr })
|
|
1424
|
+
}
|
|
1366
1425
|
}
|
|
1367
1426
|
}
|
|
1368
1427
|
}
|
|
@@ -1408,15 +1467,83 @@ class Database extends EventEmitter {
|
|
|
1408
1467
|
console.log(`💾 Save: _streamExistingRecords returned ${existingRecords.length} records`)
|
|
1409
1468
|
console.log(`💾 Save: existingRecords:`, existingRecords)
|
|
1410
1469
|
}
|
|
1411
|
-
//
|
|
1412
|
-
|
|
1470
|
+
// CRITICAL FIX: _streamExistingRecords already handles updates via updatedRecordsMap
|
|
1471
|
+
// So existingRecords already contains updated records from writeBufferSnapshot
|
|
1472
|
+
// We only need to add records from writeBufferSnapshot that are NEW (not updates)
|
|
1473
|
+
allData = [...existingRecords]
|
|
1474
|
+
|
|
1475
|
+
// OPTIMIZATION: Use Set for faster lookups of existing record IDs
|
|
1476
|
+
// CRITICAL FIX: Normalize IDs to strings for consistent comparison
|
|
1477
|
+
const existingRecordIds = new Set(existingRecords.filter(r => r && r.id).map(r => String(r.id)))
|
|
1478
|
+
|
|
1479
|
+
// CRITICAL FIX: Create a map of records in existingRecords by ID for comparison
|
|
1480
|
+
const existingRecordsById = new Map()
|
|
1481
|
+
existingRecords.forEach(r => {
|
|
1482
|
+
if (r && r.id) {
|
|
1483
|
+
existingRecordsById.set(String(r.id), r)
|
|
1484
|
+
}
|
|
1485
|
+
})
|
|
1486
|
+
|
|
1487
|
+
// Add only NEW records from writeBufferSnapshot (not updates, as those are already in existingRecords)
|
|
1488
|
+
// CRITICAL FIX: Also ensure that if an updated record wasn't properly replaced, we replace it now
|
|
1489
|
+
for (const record of writeBufferSnapshot) {
|
|
1490
|
+
if (!record || !record.id) continue
|
|
1491
|
+
if (deletedIdsSnapshot.has(String(record.id))) continue
|
|
1492
|
+
|
|
1493
|
+
const recordIdStr = String(record.id)
|
|
1494
|
+
const existingRecord = existingRecordsById.get(recordIdStr)
|
|
1495
|
+
|
|
1496
|
+
if (!existingRecord) {
|
|
1497
|
+
// This is a new record, not an update
|
|
1498
|
+
allData.push(record)
|
|
1499
|
+
if (this.opts.debugMode) {
|
|
1500
|
+
console.log(`💾 Save: Adding NEW record to allData:`, { id: recordIdStr, price: record.price, app_id: record.app_id, currency: record.currency })
|
|
1501
|
+
}
|
|
1502
|
+
} else {
|
|
1503
|
+
// This is an update - verify that existingRecords contains the updated version
|
|
1504
|
+
// If not, replace it (this handles edge cases where substitution might have failed)
|
|
1505
|
+
const existingIndex = allData.findIndex(r => r && r.id && String(r.id) === recordIdStr)
|
|
1506
|
+
if (existingIndex !== -1) {
|
|
1507
|
+
// Verify if the existing record is actually the updated one
|
|
1508
|
+
// Compare key fields to detect if replacement is needed
|
|
1509
|
+
const needsReplacement = JSON.stringify(allData[existingIndex]) !== JSON.stringify(record)
|
|
1510
|
+
if (needsReplacement) {
|
|
1511
|
+
if (this.opts.debugMode) {
|
|
1512
|
+
console.log(`💾 Save: REPLACING existing record with updated version in allData:`, {
|
|
1513
|
+
old: { id: String(allData[existingIndex].id), price: allData[existingIndex].price },
|
|
1514
|
+
new: { id: recordIdStr, price: record.price }
|
|
1515
|
+
})
|
|
1516
|
+
}
|
|
1517
|
+
allData[existingIndex] = record
|
|
1518
|
+
} else if (this.opts.debugMode) {
|
|
1519
|
+
console.log(`💾 Save: Record already correctly updated in allData:`, { id: recordIdStr })
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (this.opts.debugMode) {
|
|
1526
|
+
const updatedCount = writeBufferSnapshot.filter(r => r && r.id && existingRecordIds.has(String(r.id))).length
|
|
1527
|
+
const newCount = writeBufferSnapshot.filter(r => r && r.id && !existingRecordIds.has(String(r.id))).length
|
|
1528
|
+
console.log(`💾 Save: Combined data - existingRecords: ${existingRecords.length}, updatedFromBuffer: ${updatedCount}, newFromBuffer: ${newCount}, total: ${allData.length}`)
|
|
1529
|
+
console.log(`💾 Save: WriteBuffer record IDs:`, writeBufferSnapshot.map(r => r && r.id ? String(r.id) : 'no-id'))
|
|
1530
|
+
console.log(`💾 Save: Existing record IDs:`, Array.from(existingRecordIds))
|
|
1531
|
+
console.log(`💾 Save: All records in allData:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
|
|
1532
|
+
console.log(`💾 Save: Sample existing record:`, existingRecords[0] ? { id: String(existingRecords[0].id), price: existingRecords[0].price, app_id: existingRecords[0].app_id, currency: existingRecords[0].currency } : 'null')
|
|
1533
|
+
console.log(`💾 Save: Sample writeBuffer record:`, writeBufferSnapshot[0] ? { id: String(writeBufferSnapshot[0].id), price: writeBufferSnapshot[0].price, app_id: writeBufferSnapshot[0].app_id, currency: writeBufferSnapshot[0].currency } : 'null')
|
|
1534
|
+
}
|
|
1413
1535
|
}).catch(error => {
|
|
1414
1536
|
if (this.opts.debugMode) {
|
|
1415
1537
|
console.log(`💾 Save: _streamExistingRecords failed:`, error.message)
|
|
1416
1538
|
}
|
|
1417
1539
|
// CRITICAL FIX: Use safe fallback to preserve existing data instead of losing it
|
|
1418
1540
|
return this._loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot).then(fallbackRecords => {
|
|
1419
|
-
|
|
1541
|
+
// CRITICAL FIX: Avoid duplicating updated records
|
|
1542
|
+
const fallbackRecordIds = new Set(fallbackRecords.map(r => r.id))
|
|
1543
|
+
const newRecordsFromBuffer = writeBufferSnapshot.filter(record =>
|
|
1544
|
+
!deletedIdsSnapshot.has(String(record.id)) && !fallbackRecordIds.has(record.id)
|
|
1545
|
+
)
|
|
1546
|
+
allData = [...fallbackRecords, ...newRecordsFromBuffer]
|
|
1420
1547
|
if (this.opts.debugMode) {
|
|
1421
1548
|
console.log(`💾 Save: Fallback preserved ${fallbackRecords.length} existing records, total: ${allData.length}`)
|
|
1422
1549
|
}
|
|
@@ -1426,7 +1553,7 @@ class Database extends EventEmitter {
|
|
|
1426
1553
|
console.log(`💾 Save: CRITICAL - Data loss may occur, only writeBuffer will be saved`)
|
|
1427
1554
|
}
|
|
1428
1555
|
// Last resort: at least save what we have in writeBuffer
|
|
1429
|
-
allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id))
|
|
1556
|
+
allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(String(record.id)))
|
|
1430
1557
|
})
|
|
1431
1558
|
})
|
|
1432
1559
|
)
|
|
@@ -1440,7 +1567,12 @@ class Database extends EventEmitter {
|
|
|
1440
1567
|
// CRITICAL FIX: Use safe fallback to preserve existing data instead of losing it
|
|
1441
1568
|
try {
|
|
1442
1569
|
const fallbackRecords = await this._loadExistingRecordsFallback(deletedIdsSnapshot, writeBufferSnapshot)
|
|
1443
|
-
|
|
1570
|
+
// CRITICAL FIX: Avoid duplicating updated records
|
|
1571
|
+
const fallbackRecordIds = new Set(fallbackRecords.map(r => r.id))
|
|
1572
|
+
const newRecordsFromBuffer = writeBufferSnapshot.filter(record =>
|
|
1573
|
+
!deletedIdsSnapshot.has(String(record.id)) && !fallbackRecordIds.has(record.id)
|
|
1574
|
+
)
|
|
1575
|
+
allData = [...fallbackRecords, ...newRecordsFromBuffer]
|
|
1444
1576
|
if (this.opts.debugMode) {
|
|
1445
1577
|
console.log(`💾 Save: Fallback preserved ${fallbackRecords.length} existing records, total: ${allData.length}`)
|
|
1446
1578
|
}
|
|
@@ -1450,23 +1582,48 @@ class Database extends EventEmitter {
|
|
|
1450
1582
|
console.log(`💾 Save: CRITICAL - Data loss may occur, only writeBuffer will be saved`)
|
|
1451
1583
|
}
|
|
1452
1584
|
// Last resort: at least save what we have in writeBuffer
|
|
1453
|
-
allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id))
|
|
1585
|
+
allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(String(record.id)))
|
|
1454
1586
|
}
|
|
1455
1587
|
}
|
|
1456
1588
|
} else {
|
|
1457
1589
|
// No existing data, use only writeBuffer
|
|
1458
|
-
allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(record.id))
|
|
1590
|
+
allData = writeBufferSnapshot.filter(record => !deletedIdsSnapshot.has(String(record.id)))
|
|
1459
1591
|
}
|
|
1460
1592
|
}
|
|
1461
1593
|
|
|
1462
1594
|
// CRITICAL FIX: Calculate offsets based on actual serialized data that will be written
|
|
1463
1595
|
// This ensures consistency between offset calculation and file writing
|
|
1464
|
-
|
|
1465
|
-
|
|
1596
|
+
// CRITICAL FIX: Remove term IDs before serialization to ensure proper serialization
|
|
1597
|
+
const cleanedData = allData.map(record => {
|
|
1598
|
+
if (!record || typeof record !== 'object') {
|
|
1599
|
+
if (this.opts.debugMode) {
|
|
1600
|
+
console.log(`💾 Save: WARNING - Invalid record in allData:`, record)
|
|
1601
|
+
}
|
|
1602
|
+
return record
|
|
1603
|
+
}
|
|
1604
|
+
return this.removeTermIdsForSerialization(record)
|
|
1605
|
+
})
|
|
1606
|
+
|
|
1607
|
+
if (this.opts.debugMode) {
|
|
1608
|
+
console.log(`💾 Save: allData.length=${allData.length}, cleanedData.length=${cleanedData.length}`)
|
|
1609
|
+
console.log(`💾 Save: All records in allData before serialization:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
|
|
1610
|
+
console.log(`💾 Save: Sample cleaned record:`, cleanedData[0] ? Object.keys(cleanedData[0]) : 'null')
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const jsonlData = cleanedData.length > 0
|
|
1614
|
+
? this.serializer.serializeBatch(cleanedData)
|
|
1466
1615
|
: ''
|
|
1467
1616
|
const jsonlString = jsonlData.toString('utf8')
|
|
1468
1617
|
const lines = jsonlString.split('\n').filter(line => line.trim())
|
|
1469
1618
|
|
|
1619
|
+
if (this.opts.debugMode) {
|
|
1620
|
+
console.log(`💾 Save: Serialized ${lines.length} lines`)
|
|
1621
|
+
console.log(`💾 Save: All records in allData after serialization check:`, allData.map(r => r && r.id ? { id: String(r.id), price: r.price, app_id: r.app_id, currency: r.currency } : 'no-id'))
|
|
1622
|
+
if (lines.length > 0) {
|
|
1623
|
+
console.log(`💾 Save: First line (first 200 chars):`, lines[0].substring(0, 200))
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1470
1627
|
this.offsets = []
|
|
1471
1628
|
let currentOffset = 0
|
|
1472
1629
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1484,56 +1641,9 @@ class Database extends EventEmitter {
|
|
|
1484
1641
|
console.log(`💾 Save: Calculated indexOffset: ${this.indexOffset}, allData.length: ${allData.length}`)
|
|
1485
1642
|
}
|
|
1486
1643
|
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
// Add main file write operation
|
|
1491
|
-
parallelWriteOperations.push(
|
|
1492
|
-
this.fileHandler.writeBatch([jsonlData])
|
|
1493
|
-
)
|
|
1494
|
-
|
|
1495
|
-
// Add index file operations - ALWAYS save offsets, even without indexed fields
|
|
1496
|
-
if (this.indexManager) {
|
|
1497
|
-
const idxPath = this.normalizedFile.replace('.jdb', '.idx.jdb')
|
|
1498
|
-
|
|
1499
|
-
// OPTIMIZATION: Parallel data preparation
|
|
1500
|
-
const indexDataPromise = Promise.resolve({
|
|
1501
|
-
index: this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0 ? this.indexManager.toJSON() : {},
|
|
1502
|
-
offsets: this.offsets, // Save actual offsets for efficient file operations
|
|
1503
|
-
indexOffset: this.indexOffset // Save file size for proper range calculations
|
|
1504
|
-
})
|
|
1505
|
-
|
|
1506
|
-
// Add term mapping data if needed
|
|
1507
|
-
const termMappingFields = this.getTermMappingFields()
|
|
1508
|
-
if (termMappingFields.length > 0 && this.termManager) {
|
|
1509
|
-
const termDataPromise = this.termManager.saveTerms()
|
|
1510
|
-
|
|
1511
|
-
// Combine index data and term data
|
|
1512
|
-
const combinedDataPromise = Promise.all([indexDataPromise, termDataPromise]).then(([indexData, termData]) => {
|
|
1513
|
-
indexData.termMapping = termData
|
|
1514
|
-
return indexData
|
|
1515
|
-
})
|
|
1516
|
-
|
|
1517
|
-
// Add index file write operation
|
|
1518
|
-
parallelWriteOperations.push(
|
|
1519
|
-
combinedDataPromise.then(indexData => {
|
|
1520
|
-
const idxFileHandler = new FileHandler(idxPath, this.fileMutex, this.opts)
|
|
1521
|
-
return idxFileHandler.writeAll(JSON.stringify(indexData, null, 2))
|
|
1522
|
-
})
|
|
1523
|
-
)
|
|
1524
|
-
} else {
|
|
1525
|
-
// Add index file write operation without term mapping
|
|
1526
|
-
parallelWriteOperations.push(
|
|
1527
|
-
indexDataPromise.then(indexData => {
|
|
1528
|
-
const idxFileHandler = new FileHandler(idxPath, this.fileMutex, this.opts)
|
|
1529
|
-
return idxFileHandler.writeAll(JSON.stringify(indexData, null, 2))
|
|
1530
|
-
})
|
|
1531
|
-
)
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
// Execute parallel write operations
|
|
1536
|
-
await Promise.all(parallelWriteOperations)
|
|
1644
|
+
// CRITICAL FIX: Write main data file first
|
|
1645
|
+
// Index will be saved AFTER reconstruction to ensure it contains correct data
|
|
1646
|
+
await this.fileHandler.writeBatch([jsonlData])
|
|
1537
1647
|
|
|
1538
1648
|
if (this.opts.debugMode) {
|
|
1539
1649
|
console.log(`💾 Saved ${allData.length} records to ${this.normalizedFile}`)
|
|
@@ -1547,21 +1657,36 @@ class Database extends EventEmitter {
|
|
|
1547
1657
|
|
|
1548
1658
|
// Clear writeBuffer and deletedIds after successful save only if we had data to save
|
|
1549
1659
|
if (allData.length > 0) {
|
|
1550
|
-
// Rebuild index when records were deleted to maintain consistency
|
|
1660
|
+
// Rebuild index when records were deleted or updated to maintain consistency
|
|
1551
1661
|
const hadDeletedRecords = deletedIdsSnapshot.size > 0
|
|
1662
|
+
const hadUpdatedRecords = writeBufferSnapshot.length > 0
|
|
1552
1663
|
if (this.indexManager && this.indexManager.indexedFields && this.indexManager.indexedFields.length > 0) {
|
|
1553
|
-
if (hadDeletedRecords) {
|
|
1554
|
-
// Clear the index and rebuild it from the
|
|
1664
|
+
if (hadDeletedRecords || hadUpdatedRecords) {
|
|
1665
|
+
// Clear the index and rebuild it from the saved records
|
|
1666
|
+
// This ensures that lineNumbers point to the correct positions in the file
|
|
1555
1667
|
this.indexManager.clear()
|
|
1556
1668
|
if (this.opts.debugMode) {
|
|
1557
|
-
|
|
1669
|
+
if (hadDeletedRecords && hadUpdatedRecords) {
|
|
1670
|
+
console.log(`🧹 Rebuilding index after removing ${deletedIdsSnapshot.size} deleted records and updating ${writeBufferSnapshot.length} records`)
|
|
1671
|
+
} else if (hadDeletedRecords) {
|
|
1672
|
+
console.log(`🧹 Rebuilding index after removing ${deletedIdsSnapshot.size} deleted records`)
|
|
1673
|
+
} else {
|
|
1674
|
+
console.log(`🧹 Rebuilding index after updating ${writeBufferSnapshot.length} records`)
|
|
1675
|
+
}
|
|
1558
1676
|
}
|
|
1559
1677
|
|
|
1560
1678
|
// Rebuild index from the saved records
|
|
1561
1679
|
// CRITICAL: Process term mapping for records loaded from file to ensure ${field}Ids are available
|
|
1680
|
+
if (this.opts.debugMode) {
|
|
1681
|
+
console.log(`💾 Save: Rebuilding index from ${allData.length} records in allData`)
|
|
1682
|
+
}
|
|
1562
1683
|
for (let i = 0; i < allData.length; i++) {
|
|
1563
1684
|
let record = allData[i]
|
|
1564
1685
|
|
|
1686
|
+
if (this.opts.debugMode && i < 3) {
|
|
1687
|
+
console.log(`💾 Save: Rebuilding index record[${i}]:`, { id: String(record.id), price: record.price, app_id: record.app_id, currency: record.currency })
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1565
1690
|
// CRITICAL FIX: Ensure records have ${field}Ids for term mapping fields
|
|
1566
1691
|
// Records from writeBuffer already have ${field}Ids from processTermMapping
|
|
1567
1692
|
// Records from file need to be processed to restore ${field}Ids
|
|
@@ -1588,6 +1713,9 @@ class Database extends EventEmitter {
|
|
|
1588
1713
|
|
|
1589
1714
|
await this.indexManager.add(record, i)
|
|
1590
1715
|
}
|
|
1716
|
+
if (this.opts.debugMode) {
|
|
1717
|
+
console.log(`💾 Save: Index rebuilt with ${allData.length} records`)
|
|
1718
|
+
}
|
|
1591
1719
|
}
|
|
1592
1720
|
}
|
|
1593
1721
|
|
|
@@ -1675,12 +1803,21 @@ class Database extends EventEmitter {
|
|
|
1675
1803
|
this.termManager.decrementTermCount(termId)
|
|
1676
1804
|
}
|
|
1677
1805
|
} else if (oldRecord[field] && Array.isArray(oldRecord[field])) {
|
|
1678
|
-
//
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1806
|
+
// Check if field contains term IDs (numbers) or terms (strings)
|
|
1807
|
+
const firstValue = oldRecord[field][0]
|
|
1808
|
+
if (typeof firstValue === 'number') {
|
|
1809
|
+
// Field contains term IDs (from find with restoreTerms: false)
|
|
1810
|
+
for (const termId of oldRecord[field]) {
|
|
1682
1811
|
this.termManager.decrementTermCount(termId)
|
|
1683
1812
|
}
|
|
1813
|
+
} else if (typeof firstValue === 'string') {
|
|
1814
|
+
// Field contains terms (strings) - convert to term IDs
|
|
1815
|
+
for (const term of oldRecord[field]) {
|
|
1816
|
+
const termId = this.termManager.termToId.get(term)
|
|
1817
|
+
if (termId) {
|
|
1818
|
+
this.termManager.decrementTermCount(termId)
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1684
1821
|
}
|
|
1685
1822
|
}
|
|
1686
1823
|
}
|
|
@@ -1933,6 +2070,7 @@ class Database extends EventEmitter {
|
|
|
1933
2070
|
}
|
|
1934
2071
|
|
|
1935
2072
|
// Apply schema enforcement - convert to array format and back to enforce schema
|
|
2073
|
+
// This will discard any fields not in the schema
|
|
1936
2074
|
const schemaEnforcedRecord = this.applySchemaEnforcement(record)
|
|
1937
2075
|
|
|
1938
2076
|
// Don't store in this.data - only use writeBuffer and index
|
|
@@ -2462,11 +2600,20 @@ class Database extends EventEmitter {
|
|
|
2462
2600
|
|
|
2463
2601
|
const updated = { ...record, ...updateData }
|
|
2464
2602
|
|
|
2603
|
+
// DEBUG: Log the update operation details
|
|
2604
|
+
if (this.opts.debugMode) {
|
|
2605
|
+
console.log(`🔄 UPDATE: Original record ID: ${record.id}, type: ${typeof record.id}`)
|
|
2606
|
+
console.log(`🔄 UPDATE: Updated record ID: ${updated.id}, type: ${typeof updated.id}`)
|
|
2607
|
+
console.log(`🔄 UPDATE: Update data keys:`, Object.keys(updateData))
|
|
2608
|
+
console.log(`🔄 UPDATE: Updated record keys:`, Object.keys(updated))
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2465
2611
|
// Process term mapping for update
|
|
2466
2612
|
const termMappingStart = Date.now()
|
|
2467
2613
|
this.processTermMapping(updated, true, record)
|
|
2468
2614
|
if (this.opts.debugMode) {
|
|
2469
2615
|
console.log(`🔄 UPDATE: Term mapping completed in ${Date.now() - termMappingStart}ms`)
|
|
2616
|
+
console.log(`🔄 UPDATE: After term mapping - ID: ${updated.id}, type: ${typeof updated.id}`)
|
|
2470
2617
|
}
|
|
2471
2618
|
|
|
2472
2619
|
// CRITICAL FIX: Remove old terms from index before adding new ones
|
|
@@ -2477,9 +2624,11 @@ class Database extends EventEmitter {
|
|
|
2477
2624
|
}
|
|
2478
2625
|
}
|
|
2479
2626
|
|
|
2480
|
-
// Update record in writeBuffer or add to writeBuffer if not present
|
|
2627
|
+
// CRITICAL FIX: Update record in writeBuffer or add to writeBuffer if not present
|
|
2628
|
+
// For records in the file, we need to ensure they are properly marked for replacement
|
|
2481
2629
|
const index = this.writeBuffer.findIndex(r => r.id === record.id)
|
|
2482
2630
|
let lineNumber = null
|
|
2631
|
+
|
|
2483
2632
|
if (index !== -1) {
|
|
2484
2633
|
// Record is already in writeBuffer, update it
|
|
2485
2634
|
this.writeBuffer[index] = updated
|
|
@@ -2489,11 +2638,12 @@ class Database extends EventEmitter {
|
|
|
2489
2638
|
}
|
|
2490
2639
|
} else {
|
|
2491
2640
|
// Record is in file, add updated version to writeBuffer
|
|
2492
|
-
//
|
|
2641
|
+
// CRITICAL FIX: Ensure the old record in file will be replaced by checking if it exists in offsets
|
|
2642
|
+
// The save() method will handle replacement via _streamExistingRecords which checks updatedRecordsMap
|
|
2493
2643
|
this.writeBuffer.push(updated)
|
|
2494
2644
|
lineNumber = this._getAbsoluteLineNumber(this.writeBuffer.length - 1)
|
|
2495
2645
|
if (this.opts.debugMode) {
|
|
2496
|
-
console.log(`🔄 UPDATE: Added
|
|
2646
|
+
console.log(`🔄 UPDATE: Added updated record to writeBuffer (will replace file record ${record.id})`)
|
|
2497
2647
|
}
|
|
2498
2648
|
}
|
|
2499
2649
|
|
|
@@ -2628,16 +2778,7 @@ class Database extends EventEmitter {
|
|
|
2628
2778
|
return
|
|
2629
2779
|
}
|
|
2630
2780
|
|
|
2631
|
-
//
|
|
2632
|
-
if (this.opts.schema && Array.isArray(this.opts.schema)) {
|
|
2633
|
-
this.serializer.initializeSchema(this.opts.schema)
|
|
2634
|
-
if (this.opts.debugMode) {
|
|
2635
|
-
console.log(`🔍 Schema initialized from options: ${this.opts.schema.join(', ')} [${this.instanceId}]`)
|
|
2636
|
-
}
|
|
2637
|
-
return
|
|
2638
|
-
}
|
|
2639
|
-
|
|
2640
|
-
// Try to initialize from fields configuration (new format)
|
|
2781
|
+
// Initialize from fields configuration (mandatory)
|
|
2641
2782
|
if (this.opts.fields && typeof this.opts.fields === 'object') {
|
|
2642
2783
|
const fieldNames = Object.keys(this.opts.fields)
|
|
2643
2784
|
if (fieldNames.length > 0) {
|
|
@@ -2649,7 +2790,7 @@ class Database extends EventEmitter {
|
|
|
2649
2790
|
}
|
|
2650
2791
|
}
|
|
2651
2792
|
|
|
2652
|
-
// Try to auto-detect schema from existing data
|
|
2793
|
+
// Try to auto-detect schema from existing data (fallback for migration scenarios)
|
|
2653
2794
|
if (this.data && this.data.length > 0) {
|
|
2654
2795
|
this.serializer.initializeSchema(this.data, true) // autoDetect = true
|
|
2655
2796
|
if (this.opts.debugMode) {
|
|
@@ -2658,10 +2799,6 @@ class Database extends EventEmitter {
|
|
|
2658
2799
|
return
|
|
2659
2800
|
}
|
|
2660
2801
|
|
|
2661
|
-
// CRITICAL FIX: Don't initialize schema from indexes
|
|
2662
|
-
// This was causing data loss because only indexed fields were preserved
|
|
2663
|
-
// Let schema be auto-detected from actual data instead
|
|
2664
|
-
|
|
2665
2802
|
if (this.opts.debugMode) {
|
|
2666
2803
|
console.log(`🔍 No schema initialization possible - will auto-detect on first insert [${this.instanceId}]`)
|
|
2667
2804
|
}
|
|
@@ -3499,24 +3636,83 @@ class Database extends EventEmitter {
|
|
|
3499
3636
|
const lineNumbers = limitedEntries.map(([lineNumber]) => lineNumber)
|
|
3500
3637
|
const scoresByLineNumber = new Map(limitedEntries)
|
|
3501
3638
|
|
|
3502
|
-
|
|
3503
|
-
const ranges = this.getRanges(lineNumbers)
|
|
3504
|
-
const groupedRanges = await this.fileHandler.groupedRanges(ranges)
|
|
3639
|
+
const persistedCount = Array.isArray(this.offsets) ? this.offsets.length : 0
|
|
3505
3640
|
|
|
3506
|
-
|
|
3507
|
-
const
|
|
3641
|
+
// Separate lineNumbers into file records and writeBuffer records
|
|
3642
|
+
const fileLineNumbers = []
|
|
3643
|
+
const writeBufferLineNumbers = []
|
|
3644
|
+
|
|
3645
|
+
for (const lineNumber of lineNumbers) {
|
|
3646
|
+
if (lineNumber >= persistedCount) {
|
|
3647
|
+
// This lineNumber points to writeBuffer
|
|
3648
|
+
writeBufferLineNumbers.push(lineNumber)
|
|
3649
|
+
} else {
|
|
3650
|
+
// This lineNumber points to file
|
|
3651
|
+
fileLineNumbers.push(lineNumber)
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3508
3654
|
|
|
3509
3655
|
const results = []
|
|
3510
3656
|
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3657
|
+
// Read records from file
|
|
3658
|
+
if (fileLineNumbers.length > 0) {
|
|
3659
|
+
const ranges = this.getRanges(fileLineNumbers)
|
|
3660
|
+
if (ranges.length > 0) {
|
|
3661
|
+
// Create a map from start offset to lineNumber for accurate mapping
|
|
3662
|
+
const startToLineNumber = new Map()
|
|
3663
|
+
for (const range of ranges) {
|
|
3664
|
+
if (range.index !== undefined) {
|
|
3665
|
+
startToLineNumber.set(range.start, range.index)
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
const groupedRanges = await this.fileHandler.groupedRanges(ranges)
|
|
3670
|
+
|
|
3671
|
+
const fs = await import('fs')
|
|
3672
|
+
const fd = await fs.promises.open(this.fileHandler.file, 'r')
|
|
3673
|
+
|
|
3674
|
+
try {
|
|
3675
|
+
for (const groupedRange of groupedRanges) {
|
|
3676
|
+
for await (const row of this.fileHandler.readGroupedRange(groupedRange, fd)) {
|
|
3677
|
+
try {
|
|
3678
|
+
const record = this.serializer.deserialize(row.line)
|
|
3679
|
+
|
|
3680
|
+
// Get line number from the row, fallback to start offset mapping
|
|
3681
|
+
let lineNumber = row._ !== null && row._ !== undefined ? row._ : (startToLineNumber.get(row.start) ?? 0)
|
|
3682
|
+
|
|
3683
|
+
// Restore term IDs to terms
|
|
3684
|
+
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record)
|
|
3685
|
+
|
|
3686
|
+
// Add line number
|
|
3687
|
+
recordWithTerms._ = lineNumber
|
|
3688
|
+
|
|
3689
|
+
// Add score if includeScore is true (default is true)
|
|
3690
|
+
if (opts.includeScore !== false) {
|
|
3691
|
+
recordWithTerms.score = scoresByLineNumber.get(lineNumber) || 0
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
results.push(recordWithTerms)
|
|
3695
|
+
} catch (error) {
|
|
3696
|
+
// Skip invalid lines
|
|
3697
|
+
if (this.opts.debugMode) {
|
|
3698
|
+
console.error('Error deserializing record in score():', error)
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
} finally {
|
|
3704
|
+
await fd.close()
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
// Read records from writeBuffer
|
|
3710
|
+
if (writeBufferLineNumbers.length > 0 && this.writeBuffer) {
|
|
3711
|
+
for (const lineNumber of writeBufferLineNumbers) {
|
|
3712
|
+
const writeBufferIndex = lineNumber - persistedCount
|
|
3713
|
+
if (writeBufferIndex >= 0 && writeBufferIndex < this.writeBuffer.length) {
|
|
3714
|
+
const record = this.writeBuffer[writeBufferIndex]
|
|
3715
|
+
if (record) {
|
|
3520
3716
|
// Restore term IDs to terms
|
|
3521
3717
|
const recordWithTerms = this.restoreTermIdsAfterDeserialization(record)
|
|
3522
3718
|
|
|
@@ -3529,16 +3725,9 @@ class Database extends EventEmitter {
|
|
|
3529
3725
|
}
|
|
3530
3726
|
|
|
3531
3727
|
results.push(recordWithTerms)
|
|
3532
|
-
} catch (error) {
|
|
3533
|
-
// Skip invalid lines
|
|
3534
|
-
if (this.opts.debugMode) {
|
|
3535
|
-
console.error('Error deserializing record in score():', error)
|
|
3536
|
-
}
|
|
3537
3728
|
}
|
|
3538
3729
|
}
|
|
3539
3730
|
}
|
|
3540
|
-
} finally {
|
|
3541
|
-
await fd.close()
|
|
3542
3731
|
}
|
|
3543
3732
|
|
|
3544
3733
|
// Re-sort results to maintain score order (since reads might be out of order)
|
|
@@ -3880,9 +4069,11 @@ class Database extends EventEmitter {
|
|
|
3880
4069
|
for (let i = 0; i < lines.length && i < this.offsets.length; i++) {
|
|
3881
4070
|
try {
|
|
3882
4071
|
const record = this.serializer.deserialize(lines[i])
|
|
3883
|
-
if (record && !deletedIdsSnapshot.has(record.id)) {
|
|
4072
|
+
if (record && !deletedIdsSnapshot.has(String(record.id))) {
|
|
3884
4073
|
// Check if this record is not being updated in writeBuffer
|
|
3885
|
-
|
|
4074
|
+
// CRITICAL FIX: Normalize IDs to strings for consistent comparison
|
|
4075
|
+
const normalizedRecordId = String(record.id)
|
|
4076
|
+
const updatedRecord = writeBufferSnapshot.find(r => r && r.id && String(r.id) === normalizedRecordId)
|
|
3886
4077
|
if (!updatedRecord) {
|
|
3887
4078
|
existingRecords.push(record)
|
|
3888
4079
|
}
|
|
@@ -3926,11 +4117,25 @@ class Database extends EventEmitter {
|
|
|
3926
4117
|
// existingRecords.length = this.offsets.length
|
|
3927
4118
|
|
|
3928
4119
|
// Create a map of updated records for quick lookup
|
|
4120
|
+
// CRITICAL FIX: Normalize IDs to strings for consistent comparison
|
|
3929
4121
|
const updatedRecordsMap = new Map()
|
|
3930
|
-
writeBufferSnapshot.forEach(record => {
|
|
3931
|
-
|
|
4122
|
+
writeBufferSnapshot.forEach((record, index) => {
|
|
4123
|
+
if (record && record.id !== undefined && record.id !== null) {
|
|
4124
|
+
// Normalize ID to string for consistent comparison
|
|
4125
|
+
const normalizedId = String(record.id)
|
|
4126
|
+
updatedRecordsMap.set(normalizedId, record)
|
|
4127
|
+
if (this.opts.debugMode) {
|
|
4128
|
+
console.log(`💾 Save: Added to updatedRecordsMap: ID=${normalizedId} (original: ${record.id}, type: ${typeof record.id}), index=${index}`)
|
|
4129
|
+
}
|
|
4130
|
+
} else if (this.opts.debugMode) {
|
|
4131
|
+
console.log(`⚠️ Save: Skipped record in writeBufferSnapshot[${index}] - missing or invalid ID:`, record ? { id: record.id, keys: Object.keys(record) } : 'null')
|
|
4132
|
+
}
|
|
3932
4133
|
})
|
|
3933
4134
|
|
|
4135
|
+
if (this.opts.debugMode) {
|
|
4136
|
+
console.log(`💾 Save: updatedRecordsMap size: ${updatedRecordsMap.size}, keys:`, Array.from(updatedRecordsMap.keys()))
|
|
4137
|
+
}
|
|
4138
|
+
|
|
3934
4139
|
// OPTIMIZATION: Cache file stats to avoid repeated stat() calls
|
|
3935
4140
|
let fileSize = 0
|
|
3936
4141
|
if (this._cachedFileStats && this._cachedFileStats.timestamp > Date.now() - 1000) {
|
|
@@ -4096,7 +4301,8 @@ class Database extends EventEmitter {
|
|
|
4096
4301
|
if (recordId !== undefined && recordId !== null) {
|
|
4097
4302
|
recordId = String(recordId)
|
|
4098
4303
|
// Check if this record needs full parsing (updated or deleted)
|
|
4099
|
-
|
|
4304
|
+
// CRITICAL FIX: Normalize ID to string for consistent comparison
|
|
4305
|
+
needsFullParse = updatedRecordsMap.has(recordId) || deletedIdsSnapshot.has(String(recordId))
|
|
4100
4306
|
} else {
|
|
4101
4307
|
needsFullParse = true
|
|
4102
4308
|
}
|
|
@@ -4111,7 +4317,8 @@ class Database extends EventEmitter {
|
|
|
4111
4317
|
const idMatch = trimmedLine.match(/"id"\s*:\s*"([^"]+)"|"id"\s*:\s*(\d+)/)
|
|
4112
4318
|
if (idMatch) {
|
|
4113
4319
|
recordId = idMatch[1] || idMatch[2]
|
|
4114
|
-
|
|
4320
|
+
// CRITICAL FIX: Normalize ID to string for consistent comparison
|
|
4321
|
+
needsFullParse = updatedRecordsMap.has(String(recordId)) || deletedIdsSnapshot.has(String(recordId))
|
|
4115
4322
|
} else {
|
|
4116
4323
|
needsFullParse = true
|
|
4117
4324
|
}
|
|
@@ -4136,11 +4343,22 @@ class Database extends EventEmitter {
|
|
|
4136
4343
|
// Use record directly (no need to restore term IDs)
|
|
4137
4344
|
const recordWithIds = record
|
|
4138
4345
|
|
|
4139
|
-
|
|
4346
|
+
// CRITICAL FIX: Normalize ID to string for consistent comparison
|
|
4347
|
+
const normalizedId = String(recordWithIds.id)
|
|
4348
|
+
if (this.opts.debugMode) {
|
|
4349
|
+
console.log(`💾 Save: Checking record ID=${normalizedId} (original: ${recordWithIds.id}, type: ${typeof recordWithIds.id}) in updatedRecordsMap`)
|
|
4350
|
+
console.log(`💾 Save: updatedRecordsMap.has(${normalizedId}): ${updatedRecordsMap.has(normalizedId)}`)
|
|
4351
|
+
if (!updatedRecordsMap.has(normalizedId)) {
|
|
4352
|
+
console.log(`💾 Save: Record ${normalizedId} NOT found in updatedRecordsMap. Available keys:`, Array.from(updatedRecordsMap.keys()))
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
if (updatedRecordsMap.has(normalizedId)) {
|
|
4140
4356
|
// Replace with updated version
|
|
4141
|
-
const updatedRecord = updatedRecordsMap.get(
|
|
4357
|
+
const updatedRecord = updatedRecordsMap.get(normalizedId)
|
|
4142
4358
|
if (this.opts.debugMode) {
|
|
4143
|
-
console.log(`💾 Save:
|
|
4359
|
+
console.log(`💾 Save: ✅ REPLACING record ${recordWithIds.id} with updated version`)
|
|
4360
|
+
console.log(`💾 Save: Old record:`, { id: recordWithIds.id, price: recordWithIds.price, app_id: recordWithIds.app_id, currency: recordWithIds.currency })
|
|
4361
|
+
console.log(`💾 Save: New record:`, { id: updatedRecord.id, price: updatedRecord.price, app_id: updatedRecord.app_id, currency: updatedRecord.currency })
|
|
4144
4362
|
}
|
|
4145
4363
|
return {
|
|
4146
4364
|
type: 'updated',
|
|
@@ -4148,7 +4366,7 @@ class Database extends EventEmitter {
|
|
|
4148
4366
|
id: recordWithIds.id,
|
|
4149
4367
|
needsParse: false
|
|
4150
4368
|
}
|
|
4151
|
-
} else if (!deletedIdsSnapshot.has(recordWithIds.id)) {
|
|
4369
|
+
} else if (!deletedIdsSnapshot.has(String(recordWithIds.id))) {
|
|
4152
4370
|
// Keep existing record if not deleted
|
|
4153
4371
|
if (this.opts.debugMode) {
|
|
4154
4372
|
console.log(`💾 Save: Kept record ${recordWithIds.id} (${recordWithIds.name || 'Unnamed'})`)
|