Context
In LMDB (and in heed, the typed Rust wrapper around LMDB that Meilisearch uses), opening a named database inside a transaction returns a handle. LMDB's contract for that handle is: it is private to the transaction that opened it until that transaction commits; if the transaction aborts instead, the handle is closed automatically. Internally, LMDB's mdb_txn_end implements this by clearing the handle's validity flag and bumping its sequence number in an env-wide table on abort. Reusing the handle afterward fails LMDB's validity check and returns EINVAL. A related case: explicitly deleting a database (mdb_drop with delete=true) also closes its handle env-wide, and a later transaction abort does not undo that closure.
ZeroDB's Database type (crates/zerodb-core/src/rotxn.rs) has no equivalent bookkeeping — it is a plain value (a name plus an internal selector), not an entry in an env-wide table carrying a validity flag and generation counter. It therefore has no way to go stale: reusing a Database handle whose creating transaction was aborted silently works instead of returning an error.
This is tracked in docs/DIVERGENCES.md — the repo's catalog of sanctioned, known behavior differences from LMDB — as an entry describing exactly this behavior, currently marked critical by maintainer directive (2026-07-22) and still proposed, not yet approved. It surfaced on 2026-07-20 during fuzz testing that runs paired zerodb/LMDB adapters through randomized operation sequences, part of the differential test harness that compares zerodb against C LMDB (crates/zerodb-oracle). The harness's own bookkeeping bug that first exposed the case has since been fixed, with regression coverage added in crates/zerodb-oracle/tests/dbi_handle_lifetime.rs.
Task
Decide, and write a short design document (ADR) in docs/adr/ that must be approved before implementation, whether ZeroDB should:
- add an env-level handle registry so each
Database handle carries a generation counter that every operation validates (a real engine design change), or
- only reproduce LMDB's rejection at the
heed-zerodb adapter boundary, leaving the core engine as-is.
No code should change until a maintainer approves the design document — this changes observable error behavior, not just an internal detail.
Why it matters
No current consumer is affected today: milli (Meilisearch's indexing core) and hannoy (Meilisearch's HNSW vector index) always commit the transactions in which they create their databases, so neither ever reuses a handle from an aborted one. But this is the one known case where usage LMDB explicitly defines as invalid — reusing a handle after its creating transaction aborted — is silently accepted by ZeroDB instead of raising an error. LMDB turns that consumer bug into an immediate, loud EINVAL; ZeroDB currently masks it. Left unresolved, this both hides a class of bug in any future consumer code and is a known gap versus the LMDB behavior ZeroDB is meant to match.
Risk
Low for existing consumers — nothing in milli or hannoy exercises this path today. But the fix itself changes the observable error semantics of handle reuse, which is why it needs the design document and maintainer sign-off first rather than a quick patch.
See also: docs/DIVERGENCES.md — full divergence tracking table; crates/zerodb-oracle/tests/dbi_handle_lifetime.rs — regression test coverage.
Context
In LMDB (and in heed, the typed Rust wrapper around LMDB that Meilisearch uses), opening a named database inside a transaction returns a handle. LMDB's contract for that handle is: it is private to the transaction that opened it until that transaction commits; if the transaction aborts instead, the handle is closed automatically. Internally, LMDB's
mdb_txn_endimplements this by clearing the handle's validity flag and bumping its sequence number in an env-wide table on abort. Reusing the handle afterward fails LMDB's validity check and returnsEINVAL. A related case: explicitly deleting a database (mdb_dropwith delete=true) also closes its handle env-wide, and a later transaction abort does not undo that closure.ZeroDB's
Databasetype (crates/zerodb-core/src/rotxn.rs) has no equivalent bookkeeping — it is a plain value (a name plus an internal selector), not an entry in an env-wide table carrying a validity flag and generation counter. It therefore has no way to go stale: reusing aDatabasehandle whose creating transaction was aborted silently works instead of returning an error.This is tracked in
docs/DIVERGENCES.md— the repo's catalog of sanctioned, known behavior differences from LMDB — as an entry describing exactly this behavior, currently marked critical by maintainer directive (2026-07-22) and still proposed, not yet approved. It surfaced on 2026-07-20 during fuzz testing that runs paired zerodb/LMDB adapters through randomized operation sequences, part of the differential test harness that compares zerodb against C LMDB (crates/zerodb-oracle). The harness's own bookkeeping bug that first exposed the case has since been fixed, with regression coverage added incrates/zerodb-oracle/tests/dbi_handle_lifetime.rs.Task
Decide, and write a short design document (ADR) in
docs/adr/that must be approved before implementation, whether ZeroDB should:Databasehandle carries a generation counter that every operation validates (a real engine design change), orheed-zerodbadapter boundary, leaving the core engine as-is.No code should change until a maintainer approves the design document — this changes observable error behavior, not just an internal detail.
Why it matters
No current consumer is affected today: milli (Meilisearch's indexing core) and hannoy (Meilisearch's HNSW vector index) always commit the transactions in which they create their databases, so neither ever reuses a handle from an aborted one. But this is the one known case where usage LMDB explicitly defines as invalid — reusing a handle after its creating transaction aborted — is silently accepted by ZeroDB instead of raising an error. LMDB turns that consumer bug into an immediate, loud
EINVAL; ZeroDB currently masks it. Left unresolved, this both hides a class of bug in any future consumer code and is a known gap versus the LMDB behavior ZeroDB is meant to match.Risk
Low for existing consumers — nothing in milli or hannoy exercises this path today. But the fix itself changes the observable error semantics of handle reuse, which is why it needs the design document and maintainer sign-off first rather than a quick patch.
See also:
docs/DIVERGENCES.md— full divergence tracking table;crates/zerodb-oracle/tests/dbi_handle_lifetime.rs— regression test coverage.