Repository navigation
Description
This issue lists every bug that is present in the current published release line (v2.x) and not yet fixed in a published release. It is updated whenever a known-issue is added or its fix ships. The process is in CONTRIBUTING.md.
Current release: v2.0.11 (format v7). Support policy: PHILOSOPHY.md §10.
Data integrity
Same-transaction multi-valued facts read back as one value — #371 (also #287)
- Affected: every release from v0.6.0 through v2.0.11, including all v1.x and v2.x releases.
- Symptom: two or more values of the same attribute for one entity written in a single
transact(or retracted in a singleretract) read back as one value, and which value depends on the query shape. A batched retract can leave retracted values visible. - Workaround: write or retract each value of a multi-valued attribute in its own
transact/retractcall. - Fix: v3.0.0 (file format v8; index keys carry the value bytes). Cannot be fixed on v2.x without a format change.
Indexes already damaged by the pre-v2.0.1 rebuild bug are not repaired — #373
- Affected: files whose indexes were rebuilt on open by v2.0.0 or earlier after a crash during
save()/checkpoint()(the bug: after a kill during an active run the EAVT index silently loses most entities' entries while AEVT stays intact — entity-bound lookups return [] and no rebuild path exists #370 bug, fixed in v2.0.1). - Symptom: entity-bound queries (
[:some/ident ?a ?v]) return[]while attribute-driven queries ([?e :attr ?v]) still find the facts.checkpoint()copies the damage forward. - Workaround: none inside Minigraf on v2.x. v2.0.1 prevents new damage. To check a file, compare an entity-bound query with an attribute-driven query for the same entity.
- Fix: v3.0.0. Opening a v2.x file with v3.0.0 rebuilds every index from the fact pages during the upgrade to format v8, which repairs this damage. v3.0.0 also adds
Minigraf::verify()andMinigraf::rebuild_indexes()(feat: Minigraf::verify and rebuild_indexes (#373) #458).
A fact retracted and re-asserted in one WriteTransaction is lost at commit — #477
- Affected: every release from v0.20.0 through v2.0.11.
- Symptom: inside one
WriteTransaction,(retract ...)of a fact followed by(transact ...)of the same fact reads as live in queries run inside the transaction, but aftercommit()the fact is retracted. Assert-then-retract is consistent. - Workaround: don't retract and re-assert the same fact in one
WriteTransaction. Commit the retraction first, or leave out the retraction when the fact is asserted again. - Fix: v3.0.0 (the last statement that writes a fact decides it at commit). Merged on
v3in PR fix: reject empty valid-time windows; last statement wins in a transaction (#436, #477) #480.
A later assertion does not replace an earlier valid-time window of the same fact — #435
- Affected: every release from v0.4.0 (when valid time was added) through v2.0.11.
- Symptom: asserting a fact
[e a v]that is already live, in a later transaction, keeps both valid-time windows instead of replacing the earlier one. Queries then return one identical row per assertion (for example[:find ?e ?a ?v :where [?e ?a ?v]]returns the same row twice when the fact was asserted twice, in different milliseconds, with the default valid time). Atransactwith:valid-todoes not close an open window, so:valid-atafter that date still finds the fact. Stored data is intact; only query results are affected. - Workaround: don't assert a fact that is already live. To change its valid time, retract it first and assert it with the new window in a later transaction.
- Fix: v3.0.0 (each fact has exactly one current window per transaction time; a later assertion replaces the earlier window, and two windows of one fact in one transaction are rejected with API-011). Merged on
v3in PR fix: one current valid-time window per (e, a, v) (#435) #460.
Durability and recovery
save()/checkpoint() is not crash-atomic — #374
- Affected: all v2.x releases.
- Symptom: a crash or kill during a checkpoint leaves the old header pointing at overwritten index pages. Since v2.0.1 the next open detects this and rebuilds the indexes from the fact pages, so no committed data is lost, but that open costs time proportional to the whole graph. On v2.0.0 and earlier the rebuild itself was wrong (bug: after a kill during an active run the EAVT index silently loses most entities' entries while AEVT stays intact — entity-bound lookups return [] and no rebuild path exists #370, above).
- Workaround: upgrade to v2.0.1 or later. Committed transactions are durable in the WAL before any checkpoint, so fewer checkpoints means fewer exposure windows.
- Fix: v3.0.0 (copy-on-write index pages).
A .wal from a later checkpoint is replayed onto an older .graph — #535
- Affected: every release from v0.5.0 (when the WAL was added) through v2.0.11, file-backed databases.
- Symptom: opening a
.graphfile next to a.walwritten after a later checkpoint (most often after restoring a backup.graphand leaving the current.walin place) succeeds without an error. Transactions committed between the two checkpoints are missing, the WAL's later transactions are applied on top, and the next checkpoint makes the gap permanent. - Workaround: when restoring a backup, stop every process using the database, move the current
.graphand.walaside, make sure no.walis left in the directory, then copy in the backup's.graphand, only if the backup has one, its.wal. - Fix: v3.0.0 (merged on
v3in PR fix(storage): refuse a WAL from a later checkpoint with STG-047 (#535) #537). Opening refuses a WAL created after a later checkpoint than the file holds, withSTG-047, and leaves both files unmodified. v2.x WALs do not record a generation, so v2.x cannot detect this case; use the workaround.
Fixed in v2.0.11
These bugs are fixed in v2.0.11 (released 2026-10-26). Upgrade to v2.0.11 or later. The file format is unchanged (v7).
- An expression's
=/!=did not equate a keyword with a ref to its entity — An expression = / != does not equate a keyword with a ref to the entity it names #744. Affected v2.x through v2.0.10. An expression now compares as patterns and joins do, in queries, rule bodies, negation and binding expressions. Fixed in PR fix(query): expression = / != equate a keyword with a ref to its entity (#744) — backport #749; also fixed in v3.0.0 (PR fix(query): expression = / != equate a keyword with a ref to its entity (#744) #748). Stored data was never affected. - A rule call with
0.0inconsistently returned-0.0rows — v2.x: a rule call with a float literal returns -0.0's row in some runs (float_constant_test flaky) #750. Affected v2.x through v2.0.10. Equal zero floats now hash alike, so every run returns both rows. Fixed in PR fix(types): -0.0 and 0.0 hash alike; a rule call's 0.0 returns -0.0's row in every run (#750) #752; v3.0.0 already pairs the two everywhere (streaming engine 3/9: IndexNestedLoopJoin and HashJoin (#473) #545). Stored data was never affected. checkpoint()inside aWriteTransactiondeadlocked — checkpoint/verify/rebuild_indexes on a thread holding a WriteTransaction deadlock instead of INT-001 #734. Affected every v2.x release through v2.0.10. It now returnsINT-001and leaves the transaction usable;prepareandPreparedQuery::executeenforce the same guard. Fixed in PR fix(db): checkpoint inside a WriteTransaction returns INT-001 instead of deadlocking (#734) — backport #740; also fixed in v3.0.0 (PR fix(db): calls inside a WriteTransaction return INT-001 instead of deadlocking (#734) #739, including itsverifyandrebuild_indexesmethods).
Fixed in v2.0.10
These bugs are fixed in v2.0.10 (released 2026-10-23). Upgrade to v2.0.10 or later. The file format is unchanged.
- A transaction of more than 1,000,000 facts was lost on recovery, with every later one — WAL entries of more than 1M facts are never replayed: the transaction and every later one are lost #686. Affected v0.5.0 through v2.0.9. WAL replay took an entry of more than 1M facts as damage and stopped there, and the next open cut the WAL off at it, so after a crash, kill or close without a checkpoint that transaction and every later uncheckpointed one were gone. Replay now takes a fact count as damage only when the bytes after it cannot hold that many facts. Fixed in PR fix(wal): replay WAL entries of more than 1,000,000 facts (#686) — backport #699; also fixed in v3.0.0 (PR fix(wal): replay WAL entries of more than 1,000,000 facts (#686) #698).
- A float constant in a pattern matched any float within
f64::EPSILON— Float constants in patterns match any float within f64::EPSILON #706. Affected v0.3.0 through v2.0.9.[?e :attr 0.0], or a$slotbound to a float, matched every float in(-2.2e-16, 2.2e-16). Float constants now compare by IEEE==, as joins and=do. Stored data was never affected. Fixed in PR fix(query): float constants match equal floats only (#706) — backport #711; also fixed in v3.0.0. - Prepared queries ignored the database's
max_derived_facts/max_results— v2.x: prepared queries ignore OpenOptions max_derived_facts / max_results #679. Affected v0.19.0 through v2.0.9. A prepared rule query ran with the default rule limits instead of theOpenOptionsones. It now uses the database's limits, asexecutedoes. Results were never wrong; only the limit was. Fixed in PR fix(prepared): prepared queries use the database's rule limits (#679) #682; also fixed in v3.0.0 (PR feat(engine): query memory accounting and query_memory_limit, QRY-012 (#549 PR 1) #676).
Fixed in v2.0.9
These bugs are fixed in v2.0.9 (released 2026-10-21). Upgrade to v2.0.9 or later. The file format is unchanged.
- A fact with more than four elements lost the elements after its valid-time map — Fact with more than 4 elements: elements after the valid-time map are silently dropped #649. Affected v0.4.0 through v2.0.8.
(transact [[e a v {…} x …]])stored[e a v]and dropped the rest without an error. Such a fact now fails withPRS-108and the transaction writes nothing. Fixed in PR fix(parser): reject a fact with more than four elements, PRS-108 (#649) [v2.x backport] #656; also fixed in v3.0.0 (PR fix(parser): reject a fact with more than four elements, PRS-108 (#649) #654). - A pseudo-attribute in a rule body read the evaluator's copy of the fact — A pseudo-attribute in a rule body reads the rule evaluator's copy of the fact #661. Affected v0.15.0 through v2.0.8.
:db/valid-from,:db/valid-to,:db/tx-countand:db/tx-idin a rule withoutnot/not-join/or/or-joinreturned the time the query ran, forever, and a working copy's counters. They now read the stored fact. Stored data was never affected. Fixed in PR fix(rules): a pseudo-attribute in a rule body reads the stored fact (#661) #666; also fixed in v3.0.0 (PR fix(rules): a pseudo-attribute in a rule body reads the stored fact (#661) #665).
Fixed in v2.0.8
These bugs are fixed in v2.0.8 (released 2026-10-20). Upgrade to v2.0.8 or later. The file format is unchanged. Stored data was never affected; only query results.
- A
:db/valid-atpattern in anot-joinbody could readnil— not-join body reads :db/valid-at as nil on its per-row path #615. Affected v0.13.1 through v2.0.7. With an expression in the body, inside anorbranch, or in a query that invokes a rule, thenot-joinkept rows it should remove. Fixed in PR fix(not-join): :db/valid-at binds the query's valid time on the per-row path (#615, backport) #618; also fixed in v3.0.0 (PR fix(not-join): :db/valid-at binds the query's valid time on the per-row path (#615) #616). - A
:db/valid-atpattern in a rule definition readnil— :db/valid-at in a rule definition binds nil (INT-022 in a positive pattern) #617. Affected v0.13.1 through v2.0.7. In a rule body the query failed with INT-022; in the rule's negation bodies the negation never matched on it. Fixed in PR fix(rules): bind the query's valid time for :db/valid-at in rule definitions (#617, backport) #621; also fixed in v3.0.0 (PR fix(rules): bind the query's valid time for :db/valid-at in rule definitions (#617) #619). - An
orbranch holding only an expression over outer variables never matched — An or branch holding only an expression over outer variables never matches #620. Affected every release withorthrough v2.0.7. Fixed in PR fix(or): seed an or branch whose expression reads an outer variable (#620, v2.x backport) #623; also fixed in v3.0.0 (PR fix(or): seed an or branch whose expression reads an outer variable (#620) #622). - An expression reading a variable bound on
10000
ly by an
ordropped every row — An expression reading a variable bound only by an or drops every row #625. Affected every release withorthrough v2.0.7. Fixed in PR fix(query): run an expression reading an or-bound variable after the ors (#625, backport) #630; also fixed in v3.0.0 (PR fix(query): run an expression reading an or-bound variable after the ors (#625) #629). - A rule calling a rule with
not/not-join/orreturned too few rows — A rule calling a rule with not / not-join returns too few rows #628. Affected every release withnotin rules through v2.0.7. Fixed in PR fix(rules): stratify a rule above a not/or rule it calls (#628, backport) #634; also fixed in v3.0.0 (PR fix(rules): stratify a rule above a not/or rule it calls (#628) #631). - Recursion through a rule with
not/not-join/orreturned too few rows — Recursion through a rule with not / not-join / or returns too few rows #633. Affected every release withnotin rules through v2.0.7. Fixed in PR fix(rules): recursion through a rule with not / not-join / or (#633, backport) #636; also fixed in v3.0.0 (PR fix(rules): recursion through a rule with not / not-join / or (#633) #635). - An
orbranch never matched a keyword value against the entity it names — An or branch never matches a keyword value against the entity it names (v2.x) #637. Affected v2.x through v2.0.7. Fixed in PR fix(or): an or branch meets a keyword value as the entity it names (#637) #638; v3.0.0 fixed it in streaming engine 4/9: not, not-join, or and Distinct (#473) #546 (PR feat(engine): or and or-join on the streaming engine (#546 PR 2) #626).
Fixed in v2.0.7
- Prepared queries ignored a literal
:as-of,:valid-ator:any-valid-time— Prepared queries ignore a literal :as-of, :valid-at or :any-valid-time #603. Affected v0.18.0 through v2.0.6. A prepared query with a literal (not$slot):as-ofor:valid-atran at the current transaction time and valid now, and a prepared pseudo-attribute query with:any-valid-timefailed withINT-026. Fixed in PR fix(prepared): keep a literal :as-of / :valid-at when binding slots (#603) #605; also fixed in v3.0.0 (PR fix(prepared): keep a literal :as-of / :valid-at when binding slots (#603) #604). - Rule queries with a literal argument could return too few rows or fail with
INT-022— Recursive rule calling another rule fails with INT-022 when its second argument is bound #608. Affected v1.2.0 through v2.0.6. Magic-sets rewriting took a rule's demand from one call site; the demand is now the one every call site supplies, and a rule some call reads in full is evaluated in full. Fixed in PR fix(magic-sets): guard a rule only by demand every call site supplies (#608) #611; also fixed in v3.0.0 (PR fix(magic-sets): guard a rule only by demand every call site supplies (#608) #609). - An
orsharing no variable with the earlier clauses returned no rows — or whose variables no earlier clause binds returns no rows #610. It now returns the cross product of the incoming rows and the branch rows, in queries and rule bodies. Fixed in PR fix(or): join an or sharing no variable with earlier clauses as a cross product (#610, backport) #613; also fixed in v3.0.0 (PR fix(or): join an or sharing no variable with earlier clauses as a cross product (#610) #612).
Fixed in v2.0.6
These bugs are fixed in v2.0.6 (released 2026-10-14). Upgrade to v2.0.6 or later. The file format is unchanged. Stored data was never affected; only queries running on another thread while a transaction committed or a checkpoint ran.
- A query could see part of a committed transaction — bug: a query can see part of a committed transaction #554. Affected
WriteTransactionfrom v0.5.0 andexecute("(transact ...)")/execute("(retract ...)")from v0.6.0 through v2.0.5. A commit added its facts to the in-memory store one at a time; it now adds them under one lock. Fixed in PR fix: queries see whole transactions and one state per query (#554, #555) #559; also fixed in v3.0.0 (PR fix(storage): add a committed transaction's facts under one lock (#554) #556). - Patterns of one query could read different database states under concurrent writes — bug: patterns of one query can read different database states under concurrent writes #555. Affected v1.1.0 through v2.0.5. A query's lookups now run under one lock. Fixed in PR fix: queries see whole transactions and one state per query (#554, #555) #559; also fixed in v3.0.0 (PR fix(query): read all of a query's lookups under one lock (#555) #557, and pinned snapshots, PRs feat(storage): checkpoint publishes under one lock (streaming engine 1/9, PR 1 of 3) #578 and feat(query): queries, cursors and BrowserDb read from pinned snapshots (#543, PR 3 of 3) #580).
- A query during a checkpoint could miss committed facts — bug: a query during a checkpoint can miss committed facts (v2.x) #558. Affected v0.9.0 through v2.0.5, file-backed databases. A checkpoint now holds the store's write lock from its first page write until its new state is published, so queries wait for it. Fixed in PR fix(storage): queries wait for a checkpoint instead of reading it half-written (#558) #560; v3.0.0 was not affected.
Fixed in v2.0.5
These bugs are fixed in v2.0.5 (released 2026-10-11). Upgrade to v2.0.5 or later. The file format is unchanged.
- A commit appended after a torn WAL entry was lost on recovery — durability: a commit appended after a torn WAL entry is lost on recovery #513. Affected v0.5.0 through v2.0.4, file-backed databases. After a crash during a WAL append, or a failed append in a running process, the next commits went in after the partial entry, so a second crash before any checkpoint lost them although they were reported as committed. Opening a database for writing now cuts the WAL back to its last valid entry before appending, and a failed append makes later writes through that handle fail with
WAL-007until the database is reopened or acheckpoint()succeeds. Fixed in PR fix(wal): never append after a torn WAL entry (#513, v2.x backport) #516; also fixed in v3.0.0 (PR fix(wal): never append after a torn WAL entry (#513) #514). - A file shorter than one page opened as a new empty database — A file shorter than one page opens as a new empty database (read-write re-initializes it) #506. Affected every release before v2.0.5. A database cut below 4 KiB by an interrupted copy, or an unrelated file at the path, was silently overwritten with a new header. It is now refused with
STG-045and left unmodified, unless its bytes are zeros or a prefix of the header a new database writes (a torn first write). Fixed in PR fix(storage): refuse a sub-page file that is not a torn first write (#506, v2.x backport) #509; also fixed in v3.0.0. - A rule query failed with
INT-020on a database with 1,000,000 or more live facts — bug: a rule query fails with INT-020 on any database with 1,000,000 or more live facts #528. Affected v0.18.0 through v2.0.4.max_resultscounted the database's own facts as well as the derived ones; it now counts only the facts the rules derive. Fixed in PR fix(query): count only derived facts against max_results (#528, v2.x backport) #538; also fixed in v3.0.0 (PR fix(query): count only derived facts against max_results (#528) #534).
Fixed in v2.0.4
These bugs are fixed in v2.0.4 (released 2026-10-08). Upgrade to v2.0.4 or later. The file format is unchanged.
BrowserDbcould leave a dirty page out of the IndexedDB flush without an error — fix(browser): an unreadable dirty page is an error, not dropped from the IndexedDB flush #470. AffectedBrowserDb(@minigraf/browser, thebrowserfeature) in v0.20.0 through v2.0.3; native and WASI builds were not affected.checkpoint(),importGraph()and every write skipped a dirty page that could not be read back from the in-memory page buffer and still reported success. No code path was known to trigger it, so no data loss was observed. An unreadable dirty page is now an error. Also fixed in v3.0.0 (ci: test and lint the wasm32 targets again; fix browser panics and dropped pages #469).
Fixed in v2.0.3
These bugs are fixed in v2.0.3 (released 2026-10-06). Upgrade to v2.0.3 or later. The file format is unchanged.
- Queries returned wrong results with more than 65,535 uncheckpointed facts — Queries return wrong results with more than 65,535 uncheckpointed facts #445. Affected v0.7.0 through v2.0.2: in-memory databases with more than 65,535 facts, and file databases with more than 65,535 facts written since the last checkpoint. Entity-bound queries and attribute scans could silently lose or duplicate facts; stored data was always intact, and a checkpoint made file databases correct again. Fixed in PR fix(storage): index pending facts past 65,535 correctly (#445) #446.
- Transaction counter rewound after reopening next to an already-checkpointed WAL — Transaction counter rewinds after reopening next to an already-checkpointed WAL #447. Affected v0.5.0 through v2.0.2, file databases where a
.walfile outlived a checkpoint. New transactions reused transaction numbers already in the file, so:as-ofqueries mixed old and new transactions. The counter now never moves backwards on WAL replay. Fixed in PR fix(storage): never rewind the tx counter on WAL replay (#447) #448. History already mixed by this bug and made permanent by a checkpoint is not repaired.
Fixed in v2.0.2
These bugs are fixed in v2.0.2 (released 2026-09-27). Upgrade to v2.0.2 or later.
- A power loss could lose a new database or WAL file, or bring back a deleted WAL — durability: fsync the parent directory after creating the .graph/WAL and after deleting the WAL #389. Affected all releases before v2.0.2 on Linux and other POSIX systems. The parent directory is now fsynced after the
.graphor WAL file is created and after the WAL is deleted. Process kills were never affected, and neither was Windows. - Bound-entity queries could drop rows — perf: point query on a bound entity scans the entity's whole version history — O(retract/reassert count), not O(live facts) #323. Affected releases before v2.0.2. When one
WriteTransactionwrote the same attribute in several valid-time windows, an entity-bound query ([:e :attr ?v]) could silently return only one window's value, while a full scan returned all of them. Bound-entity queries now always match a full scan.
Tracker for the data-integrity work in v3.0.0: #383.
Activity
docs: stability/support policy, support tiers and known-issues process (
test(storage): crash at every write/sync in save() loses no checkpoin…
docs(contributing): leave #421 entry removal to the existing rule