Repository navigation
Description
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 rowsDecide
Which result is correct?
- Statement order wins. The last statement on a triple decides it. At commit, drop the earlier records of a triple that a later statement in the same transaction overrides, or reject the mix.
- Retraction wins, as committed today. In-transaction reads then have to apply the same rule (one
tx_countfor all pending facts). - Reject asserting and retracting the same triple in one transaction with a new error code. This is a behaviour break, so it would go in v3.0.0 like bug: a :find variable that is never bound returns "No results found" instead of an error #406/bug: unknown aggregate / window function only errors once a row matches; returns empty on no data #414/bug: transact accepts empty or inverted valid-time windows (valid-to <= valid-from) #436.
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. Onetx_idcannot 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 oneretract. LogWriterkeeps 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
fix(log_writer): keep the write rules in every built file (#477)
Merge pull request #480 from project-minigraf/fix/436-477-write-seman…
adityamukho commentedon Oct 8, 2026
Fixed on
v3in 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).test: widen property tests to joins, negation, predicates and time tr…