v2.0.2 — 2026-09-27
Release Notes
Final planned release on the v2.x line. File format is unchanged (v7). No breaking API changes; one new user-facing error code (API-010). v2.x gets data-integrity and security fixes for 12 months after v3.0.0 ships (support policy).
Performance
-
Bound-entity point queries no longer pay for other attributes' history (#323).
[:e :attr ?v]now range-scans only(e, :attr)in the EAVT index instead of every record the entity has ever written. Reading a rarely changed attribute of a heavily rewritten entity no longer slows down as that entity's history grows: with 2,000 retract/reassert cycles on another attribute of the same entity, it drops from 4.58 ms to 19.0 µs. Reading the heavily rewritten attribute itself, and attribute scans ([?e :attr ?v]), are about 1.35–1.4× faster (4.64 ms → 3.36 ms), from cheaper net-assert grouping and removing a redundant dedup pass. The file format is unchanged. -
Attribute scans are bounded to exactly the queried attribute (#381).
[?e :attr ?v]computed the end of its AEVT range by incrementing the attribute's last byte. For attributes whose last byte is0x7For0xBF— including about 1 in 64 non-ASCII characters, such as:丿or:ÿ— the result was not valid UTF-8, so the scan ran to the end of the whole AEVT index and read every later fact before filtering. It also read facts for prefix siblings (:ab,:a/bwhen scanning:a). The range now ends atattribute + "\0", which covers exactly one attribute. Scanning 100:丿facts in a checkpointed file with 40,000 facts on neighbouring attributes drops from 45.4 ms to 251 µs. Results are unchanged. -
Checkpoints no longer re-encode the whole index (#315).
checkpoint()decoded and re-serialised every entry of all four covering indexes and re-read every page of the file for its checksum, so checkpointing after one new fact cost almost as much as after thousands. Index leaves that receive no new entries are now copied verbatim, only the leaves that do are decoded and re-split, and unchanged fact pages are no longer re-hashed. Leaves are still bounds-checked and the leaf chain is checked for cycles, so a damaged index fails the checkpoint instead of being copied forward. Checkpoint after one new fact drops from 246 ms to 26.3 ms on a 100k-fact file and from 23.9 ms to 4.0 ms at 10k. Cost still grows with graph size, because the index pages are copied on every checkpoint; the file format is unchanged. Thecheckpoint()andwal_checkpoint_thresholddocs now state that a checkpoint is not a durability boundary: the WAL is already crash-durable.
Fixed
- A power loss could lose a new database or WAL file, or bring back a deleted WAL (#389). On Linux and other POSIX systems, a created or deleted file survives a power loss only once its parent directory is fsynced; Minigraf fsynced file contents but never a directory. Creating the
.graphfile or the<db>.walsidecar, and deleting the WAL after a checkpoint, now fsync the parent directory after the file operation. A resurrected WAL was already harmless, since replay skips entries at or below the last checkpointed transaction, but a lost.graphor WAL lost committed data. Windows needs no directory sync and is unchanged. Process kills were never affected, because they leave the OS page cache intact. - Recursive rules failed with a literal start when the recursion goes through another rule (#297).
(query [:find ?y :where (chain :a ?y)])over(chain ?x ?y) :- (link ?x ?mid) (chain ?mid ?y)failed withUnbound variable in rule head: ?mid(INT-022). The magic-sets rewrite, which runs when a rule argument is given as a literal, dropped earlier rule calls from the rules that pass bindings to the next call, so a variable bound only by such a call (here?mid, bound bylink) was never bound. #300 fixed the case where a fact pattern binds it. Those calls are now kept, and results match the same query without a literal start. or-joinfailed with INT-031 when no rows reached it (#405). A valid query such as[?item :order-item/product ?p] (or-join [?item] ...)returnedor-join variable ?item is not bound in the incoming scopeon an empty database, or whenever the clauses before theor-joinmatched nothing, but worked once matching data existed. A safety check read the bound variables from the incoming rows, so with no rows every join variable looked unbound.or-joinnow returns an empty result when no rows reach it, likeorandnot-joinalready did. This applies to queries and rule bodies.- A query with
$slotbind tokens run throughexecute()reported an internal error (#407). Such a query failed with[INT-025] internal: unsubstituted :valid-at bind slot reached the executorwhen it had a:valid-atslot, and with slots in other positions it could run and quietly return nothing.Minigraf::execute(),WriteTransaction::execute()and the REPL now reject it before execution with the new user error API-010, which names the slots and points toprepare(). Prepared queries are unchanged. - Bound-entity queries could drop rows when one
WriteTransactionwrote the same attribute in several valid-time windows (#323). Both facts share atx_count, and the selective lookup path de-duplicated on(entity, attribute, tx_count, asserted), so one window's value was silently lost, while the same query via a full scan returned both. That de-duplication has been removed, and bound-entity queries now always match a full scan. - The INT-054 negative-cycle error named its two predicates in a random order (#410). Registering
(rule [(p ?x) (not (q ?x))])and then(rule [(q ?x) (not (p ?x))])reportedpredicate 'q' is involved in a negative cycle through 'p'on some runs and the reverse on others, because stratification walked the rule dependency graph inHashMaporder. It now walks rules in sorted order, so the same rules always give the same message. Which rules are accepted or rejected is unchanged.
Tests
- Crash at every point inside
save()(#374, #390). A fault-injection test makes a checkpoint fail at each page write and each sync in turn, on a file already written by several checkpoints, then reopens what reached storage. At all 47 crash points the file reopens with either the old header and exactly the previously checkpointed facts (oldlast_checkpointed_tx_count, so WAL replay re-applies the in-flight transaction) or the new header and all facts, and every fact is found by both entity and attribute lookups. 45 of the 47 reopens take the index-rebuild path. This checks the v2.x behaviour described in the known-issues list (#421); it models a process kill, not a power loss.
CI
- New
Policyworkflow (#395). An MSRV job runscargo check --all-featuresandcargo test --libon Rust 1.89, resolving dependencies to 1.89-compatible versions, and fails ifrust-versioninCargo.tomldrifts from the tested toolchain.cargo-semver-checkscompares the public API with the latest release tag and blocks PRs tomain; on major-version branches such asv3it reports breaks without failing.cargo-deny(deny.toml) allows only permissive licenses compatible with MIT OR Apache-2.0, rejects duplicate crate versions and wildcard requirements, and requires every crate to come from crates.io.
Documentation
-
Unsafe WAL recovery advice removed from
docs/ERROR_REFERENCE.md. The WAL-001, WAL-002 and STG-011 resolutions said a.walfile could be deleted with no data loss, or that a database could be rebuilt from the WAL alone. Both are wrong: transactions committed since the last checkpoint exist only in the WAL, and the WAL holds nothing older than that checkpoint. The resolutions now say to copy both files first, to checkpoint with the version that wrote the WAL, and what is lost if the WAL is deleted. -
CLAUDE.md now matches the milestones (v2.0.2 final v2.x release, v3.0.0 format v8 and data integrity, v3.1.0 features) and documents
error.rs,magic_sets.rs,fault_inject.rsandsrc/browser/. ROADMAP lists #405 and #407 in the v2.0.2 scope. -
README: MSRV (1.89) stated, v2.x known issue (#371) shown near the top, Maven coordinates corrected to
io.github.project-minigraf, Android listed on Maven Central, and binding download locations point to the binding repos. -
.github/SECURITY.mdlists 2.x as the supported line. About 50 broken wiki links indocs/ERROR_REFERENCE.mdnow use full wiki URLs. -
Stability and support policy (#397). PHILOSOPHY.md §7 now states a format policy instead of "frozen for decades": what counts as a format change, that v(N+1) always reads v(N), and that older formats are dropped only in a major release. A new support policy gives v2.x data-integrity and security fixes for 12 months after v3.0.0 ships. "Production-ready", "12–15 months to production" and the check mark on "never losing data" are replaced with claims that hold today. SECURITY.md links the policy.
-
Support tiers (#400). The Rust crate and Python are Tier 1 (fully tested, released with every core release). Node.js, browser WASM, WASI, Java/JVM, Android, Swift and C are Tier 2 (experimental, best-effort releases). Tier 1 platforms are Linux ext4/xfs, macOS APFS and Windows NTFS on local disk. The README platform table shows each binding's tier.
-
Known-issues process (#399). Issues labelled
data-integrity,corruptionordurabilitystay open until their fix is in a published release (CONTRIBUTING.md). A newknown-issuelabel and a pinned issue (#421) list each bug in the current release with affected versions, workaround and fix version; the README links it from a new "Known issues" section. #287 was reopened under this rule. The bug report template asks for the affected version range, binding and filesystem.
Known issues
- Same-transaction multi-valued facts can read back as one value (#371, #287); the fix needs file format v8 and ships in v3.0.0. Workaround: write or retract each value of a multi-valued attribute in its own call.
save()is not crash-atomic (#374), and indexes damaged by the pre-v2.0.1 rebuild bug are not repaired (#373). All v2.x known issues, with affected versions and workarounds, are listed in the pinned issue #421.
Notes
- v3.0.0 drops read support for file formats v1–v6. v3.0.0 reads format v7 and migrates it to v8. Every v1.x and v2.x release migrates v1–v6 files to v7 on open, so open any older file once with v2.x before upgrading to v3.0.0.
- On v2.x, reading a heavily rewritten attribute still costs time proportional to its history, because v7 index keys carry neither the value nor the assert/retract flag; the structural fix needs the v8 keys and is tracked for v3.0.0 in #379.
- Checkpoints on v2.x still copy every index page, so their cost grows with graph size. Copy-on-write index pages on the v3 branch, which also make
save()crash-atomic, are needed for checkpoints proportional to the change alone (#374).
Download minigraf 2.0.2
| File | Platform | Checksum |
|---|---|---|
| minigraf-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| minigraf-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| minigraf-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| minigraf-aarch64-unknown-linux-gnu.tar.xz | ARM64 Linux | checksum |
| minigraf-x86_64-unknown-linux-gnu.tar.xz | x64 Linux | checksum |