Skip to content

v2.0.2 — 2026-09-27

Choose a tag to compare

@github-actions github-actions released this 27 Sep 03:54

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 is 0x7F or 0xBF — 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/b when scanning :a). The range now ends at attribute + "\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. The checkpoint() and wal_checkpoint_threshold docs 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 .graph file or the <db>.wal sidecar, 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 .graph or 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 with Unbound 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 by link) 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-join failed with INT-031 when no rows reached it (#405). A valid query such as [?item :order-item/product ?p] (or-join [?item] ...) returned or-join variable ?item is not bound in the incoming scope on an empty database, or whenever the clauses before the or-join matched 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-join now returns an empty result when no rows reach it, like or and not-join already did. This applies to queries and rule bodies.
  • A query with $slot bind tokens run through execute() reported an internal error (#407). Such a query failed with [INT-025] internal: unsubstituted :valid-at bind slot reached the executor when it had a :valid-at slot, 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 to prepare(). Prepared queries are unchanged.
  • Bound-entity queries could drop rows when one WriteTransaction wrote the same attribute in several valid-time windows (#323). Both facts share a tx_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))]) reported predicate 'q' is involved in a negative cycle through 'p' on some runs and the reverse on others, because stratification walked the rule dependency graph in HashMap order. 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 (old last_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 Policy workflow (#395). An MSRV job runs cargo check --all-features and cargo test --lib on Rust 1.89, resolving dependencies to 1.89-compatible versions, and fails if rust-version in Cargo.toml drifts from the tested toolchain. cargo-semver-checks compares the public API with the latest release tag and blocks PRs to main; on major-version branches such as v3 it 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 .wal file 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.rs and src/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.md lists 2.x as the supported line. About 50 broken wiki links in docs/ERROR_REFERENCE.md now 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, corruption or durability stay open until their fix is in a published release (CONTRIBUTING.md). A new known-issue label 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