Skip to content

[tx] Updating the same indexed record twice in one transaction permanently corrupts the index #4935

Description

@lvca

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditFindings from the 2026-07 engine deep auditbugconcurrencyThreading / concurrency / MVCC

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions