Severity: CRITICAL - reproduced on current main.
Location: LocalDatabase.java:1068-1078 (eager per-save index diffing in the tx-active branch), TransactionIndexContext.java:296-368 (addIndexKeyLock, no cross-key reconciliation), index/lsm/LSMTreeIndex.java:437,515.
In the tx-active branch of updateRecord(), every save() calls indexer.updateDocument(originalRecord, document) where originalRecord is always rebuilt from the records **committed** buffer (serialization is deferred, so the buffer never advances inside the tx). Save #1 buffers REMOVE(orig), ADD(mid); save #2 buffers REMOVE(orig), ADD(final). ADD(mid)is never cancelled and at commitputReplaypersists **both**midandfinal`.
Repro (fails on main): unique LSM index on email; commit orig; in one tx doc.set("email","mid"); doc.save(); doc.set("email","final"); doc.save(); commit. Then:
lookupByKey(email=mid) returns the record (whose email is final) - a phantom entry;
- a later insert of another record with
email=mid`` throws DuplicatedKeyException ... already assigned to #x at `TransactionIndexContext.checkUniqueIndexKeys:404` - the unique constraint blocks a legitimate value forever.
Reachable from ordinary SQL: UPDATE ... SET x=1; UPDATE ... SET x=2 in one tx, or any script/graph mutation that saves twice.
Fix: compute the index diff against the last-buffered indexed state per RID (track per-tx "current indexed keys" per record in TransactionIndexContext), or drop the eager save-time updateDocument and buffer during phase 1. Add the repro above as a regression test.
Filed from an internal engine deep-audit (2026-07). Traced to source; verify against current main before fixing and add a regression test.
Severity: CRITICAL - reproduced on current
main.Location:
LocalDatabase.java:1068-1078(eager per-save index diffing in the tx-active branch),TransactionIndexContext.java:296-368(addIndexKeyLock, no cross-key reconciliation),index/lsm/LSMTreeIndex.java:437,515.In the tx-active branch of
updateRecord(), everysave()callsindexer.updateDocument(originalRecord, document)whereoriginalRecordis always rebuilt from the records **committed** buffer (serialization is deferred, so the buffer never advances inside the tx). Save #1 buffersREMOVE(orig), ADD(mid); save #2 buffersREMOVE(orig), ADD(final).ADD(mid)is never cancelled and at commitputReplaypersists **both**midandfinal`.Repro (fails on main): unique LSM index on
email; commitorig; in one txdoc.set("email","mid"); doc.save(); doc.set("email","final"); doc.save();commit. Then:lookupByKey(email=mid)returns the record (whose email isfinal) - a phantom entry;email=mid`` throwsDuplicatedKeyException ... already assigned to #xat `TransactionIndexContext.checkUniqueIndexKeys:404` - the unique constraint blocks a legitimate value forever.Reachable from ordinary SQL:
UPDATE ... SET x=1; UPDATE ... SET x=2in one tx, or any script/graph mutation that saves twice.Fix: compute the index diff against the last-buffered indexed state per RID (track per-tx "current indexed keys" per record in
TransactionIndexContext), or drop the eager save-timeupdateDocumentand buffer during phase 1. Add the repro above as a regression test.Filed from an internal engine deep-audit (2026-07). Traced to source; verify against current
mainbefore fixing and add a regression test.