Skip to content

WriteTransaction reads disagree with the committed result for retract-then-reassert #477

Closed

Description

@adityamukho

Problem

Inside one WriteTransaction, a fact that is retracted and then asserted again reads as live. After commit() the same fact is retracted.

Each tx.execute() stages its facts with its own synthetic tx_count (stage_pending_facts), so reads inside the transaction see statement order. commit() stamps every fact with one tx_count, and net_asserted_facts hides a triple whenever a retraction shares the newest assertion's tx_count. Retract-then-assert is therefore visible before the commit and gone after it.

Assert-then-retract is consistent: the triple is hidden both before and after the commit.

Reproduce

let db = Minigraf::in_memory().unwrap();
db.execute("(transact [[:e0 :a0 1]])").unwrap();
let mut tx = db.begin_write().unwrap();
tx.execute("(retract [[:e0 :a0 1]])").unwrap();
tx.execute("(transact [[:e0 :a0 1]])").unwrap();
// in-tx query [:e0 :a0 ?v]  -> 1 row
tx.commit().unwrap();
// query [:e0 :a0 ?v]        -> 0 rows

Decide

Which result is correct?

The decision also affects the on-disk walk (OnDiskReader::live_scan: a retraction in the deciding tx group hides the triple) and the tests/model_based_test.rs model (#385). The model currently encodes "retraction wins" and does not check reads inside a transaction.

Decision (2026-10-07)

Broadened: when one transaction writes the same (e, a, v) more than once, only the latest write survives, in any mix of transact and retract.

  • Across statements of one WriteTransaction, the last statement that writes a triple decides it. At commit, the triple's records from earlier statements are dropped, so the committed result matches the reads inside the transaction. One tx_id cannot tell several versions of a fact apart. This replaces API-011's cross-statement rejection.
  • Inside one (transact [...]), two different windows for one triple stay an error (API-011). Identical repeats are allowed, and so are duplicate triples in one retract.
  • LogWriter keeps the same rules, so no built file holds facts a normal write couldn't produce: API-019 (inverted window), API-011 (second window of one fact in one transaction) and the new API-020 (an assertion and a retraction of one fact in one transaction, either order). An unfiltered ETL target is identical to a source that keeps the rules. A source with older records that break them fails at the first such record and needs a repair step first. (Revised 2026-10-08 from verbatim copying, which let invalid facts into new v8 files.)

Design: docs/superpowers/specs/2026-10-07-write-validation-and-statement-order-design.md (with #436).

Activity

  1. added this to the v3.0.0 milestone on Oct 7, 2026
  2. added
    bugSomething isn't working
    rustPull requests that update rust code
    on Oct 7, 2026
  3. added 2 commits that reference this issue on Oct 8, 2026
  4. added
    known-issueBug present in a published release and not yet fixed in one; listed in the pinned known-issues issue
    on Oct 8, 2026
  5. adityamukho commented on Oct 8, 2026

    @adityamukho
    CollaboratorAuthor

    Fixed on v3 in PR #480 (merge 1c29900); ships in v3.0.0. Listed in #421 as a v2.x known issue (v0.20.0 through v2.0.3).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdata-integrityData integrity issueknown-issueBug present in a published release and not yet fixed in one; listed in the pinned known-issues issuerustPull requests that update rust code

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions