# emdb API Reference
Companion to the rustdoc on [docs.rs/emdb](https://docs.rs/emdb).
This file documents every public method on every public type, with
the parameter notes, error semantics, and worked examples that
don't fit inside `///` comments.
The rustdoc is the authoritative type-signature reference. This
document focuses on **how to use the API correctly** — when to
prefer one method over another, what the trade-offs are, and what
the calling patterns look like in practice.
---
## Contents
- [Construction](#construction) — `Emdb::open`, `open_in_memory`, the builder
- [Core key/value operations](#core-keyvalue-operations) — `insert`, `get`, `remove`, …
- [Zero-copy reads](#zero-copy-reads) — `get_zerocopy` + `ValueRef`
- [Bulk insert](#bulk-insert) — `insert_many`
- [Iteration](#iteration) — `iter`, `keys`, `range`, cursor-style `iter_from`
- [Transactions](#transactions) — `transaction`
- [Namespaces](#namespaces) — `namespace`, `Namespace`
- [Nested groups](#nested-groups) — `Focus` (`nested` feature)
- [TTL](#ttl) — per-record expiration (`ttl` feature)
- [Persistence and durability](#persistence-and-durability) — `flush`, `checkpoint`, `FlushPolicy`
- [Operational APIs](#operational-apis) — `stats`, `backup_to`, `compact`, `clone_handle`
- [Lockfile recovery](#lockfile-recovery) — `lock_holder`, `break_lock`
- [Encryption](#encryption) — `encryption_key`, `encryption_passphrase`, key rotation (`encrypt` feature)
- [Async surface](#async-surface) — `AsyncEmdb`, `AsyncNamespace`, eager + streaming iteration (`async` feature)
- [Types reference](#types-reference) — `FlushPolicy`, `Ttl`, `ValueRef`, `EmdbStats`, `LockHolder`
- [Errors](#errors) — `Error` variants and recovery patterns
---
## Construction
### `Emdb::open(path) -> Result<Emdb>`
Open or create a file-backed database at `path`. Creates the file
if it doesn't exist. Acquires an OS-level advisory lock on
`<path>.lock` to prevent two processes from opening the same file
concurrently.
**Parameters**
- `path` — anything that converts to `&Path`. Typically a
`&str`, `String`, `PathBuf`, or `&Path`.
**Errors**
- `Error::Io` — filesystem error (permission denied, disk full,
unreachable parent directory).
- `Error::LockBusy { path }`: another process (or another handle in
this process) holds the lock. See
[Lockfile recovery](#lockfile-recovery) for diagnosis.
- `Error::LockfileError(io)`: the lock file could not be opened or
locked for another reason.
- `Error::MagicMismatch` / `Error::VersionMismatch` /
`Error::Corrupted` — the file exists but is not a valid
emdb v0.9+/v1.x database.
**Examples**
Open a fresh database:
```rust
use emdb::Emdb;
let path = std::env::temp_dir().join("emdb-doc-open.emdb");
{
let db = Emdb::open(&path)?;
db.insert("k", "v")?;
db.flush()?;
}
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{}.lock", path.display()));
# Ok::<(), emdb::Error>(())
```
Reopen an existing database:
```rust,no_run
use emdb::Emdb;
let db = Emdb::open("/var/lib/myapp/sessions.emdb")?;
println!("{} records", db.len()?);
# Ok::<(), emdb::Error>(())
```
### `Emdb::open_in_memory() -> Emdb`
Open a disposable database. Despite the name it is not held in
RAM only: it is an ordinary database in a uniquely named file
under the system temp directory, with the usual journal, sidecar
and lock. The files are removed (best effort) when the last
handle is dropped; a killed process leaves them behind. Do not use
it for data that must never touch the disk.
Use cases: unit tests, ephemeral caches, doctests.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("name", "emdb")?;
assert_eq!(db.get("name")?, Some(b"emdb".to_vec()));
# Ok::<(), emdb::Error>(())
```
### `Emdb::builder() -> EmdbBuilder`
Returns a builder for opening a database with non-default
configuration. See [`EmdbBuilder`](#emdbbuilder) for the full
list of options.
```rust,no_run
use emdb::{Emdb, FlushPolicy};
let db = Emdb::builder()
.path("/var/lib/myapp/sessions.emdb")
.flush_policy(FlushPolicy::Group)
.enable_range_scans(true)
.build()?;
# Ok::<(), emdb::Error>(())
```
### `Emdb::clone_handle() -> Emdb`
Cheap clone of the database handle. Both handles share the same
underlying engine and lockfile — there is exactly one open file,
one lock, one in-memory index. Useful for handing the database
to a spawned task without wrapping it in `Arc`.
```rust
use emdb::Emdb;
use std::thread;
let db = Emdb::open_in_memory();
db.insert("k", "main")?;
let db_clone = db.clone_handle();
let h = thread::spawn(move || -> emdb::Result<()> {
db_clone.insert("k", "worker")?;
Ok(())
});
h.join().expect("thread panicked")?;
// Both handles see the worker's write.
assert_eq!(db.get("k")?, Some(b"worker".to_vec()));
# Ok::<(), emdb::Error>(())
```
### `Emdb::path() -> &Path`
Returns the on-disk path of this database. For in-memory
databases, returns the synthetic in-memory path used internally.
---
## Core key/value operations
### `insert(key, value) -> Result<()>`
Insert or replace a value. Both `key` and `value` accept anything
that converts into `Vec<u8>`: `&str`, `String`, `&[u8]`, `Vec<u8>`,
arrays, etc.
Behaviour:
- Replaces the previous value if `key` already exists.
- Records the operation to the journal (`fsys::JournalHandle::append`).
- Updates the in-memory hash index.
- Does **not** sync to disk on its own. Call `flush()` when you
need durability.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
// String keys and values.
db.insert("user:1", "alice")?;
// Bytes.
db.insert(b"user:2", b"bob".to_vec())?;
// Replacement is silent.
db.insert("user:1", "alice-renamed")?;
assert_eq!(db.get("user:1")?, Some(b"alice-renamed".to_vec()));
# Ok::<(), emdb::Error>(())
```
### `get(key) -> Result<Option<Vec<u8>>>`
Fetch a value, returning `Ok(None)` if the key doesn't exist or
has expired (TTL feature). Allocates a fresh `Vec<u8>` for the
returned value.
For zero-copy reads, see [`get_zerocopy`](#get_zerocopykey---resultoptionvalueref).
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("k", "v")?;
assert_eq!(db.get("k")?, Some(b"v".to_vec()));
assert_eq!(db.get("missing")?, None);
# Ok::<(), emdb::Error>(())
```
### `remove(key) -> Result<Option<Vec<u8>>>`
Delete a key. Returns the prior value if one existed.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("k", "v")?;
assert_eq!(db.remove("k")?, Some(b"v".to_vec()));
assert_eq!(db.remove("k")?, None); // already gone
# Ok::<(), emdb::Error>(())
```
### `contains_key(key) -> Result<bool>`
Does the key exist? Equivalent to `get(key)?.is_some()` but skips
the value decode. For encrypted databases this matters — it
avoids the AEAD decryption path entirely.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("k", "v")?;
assert!(db.contains_key("k")?);
# Ok::<(), emdb::Error>(())
```
### `len() -> Result<usize>`
Total live record count across the default namespace.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("a", "1")?;
db.insert("b", "2")?;
assert_eq!(db.len()?, 2);
# Ok::<(), emdb::Error>(())
```
### `is_empty() -> Result<bool>`
`len()? == 0`, but cheaper since it short-circuits on the first
shard with any record.
### `clear() -> Result<()>`
Remove every record from the default namespace. Subsequently
`len()` returns `0`.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("a", "1")?;
db.insert("b", "2")?;
db.clear()?;
assert_eq!(db.len()?, 0);
# Ok::<(), emdb::Error>(())
```
---
## Zero-copy reads
### `get_zerocopy(key) -> Result<Option<ValueRef>>`
Reads directly from the kernel-managed mmap — no allocation, no
copy. Returns a [`ValueRef`](#valueref) that holds a strong handle
to the mmap region, so the borrow is safe across subsequent writer
activity (file growth, in-place updates).
For encrypted databases, falls back to an owned plaintext buffer
inside the same `ValueRef` type — AEAD decryption necessarily
allocates fresh bytes.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("greeting", "hello world")?;
if let Some(v) = db.get_zerocopy("greeting")? {
// Compare against a byte slice without copying.
let want: &[u8] = b"hello world";
assert!(v == want);
// Safe to keep across writes — kernel keeps the mapping.
db.insert("other", "data")?;
assert_eq!(v.as_slice(), b"hello world");
}
# Ok::<(), emdb::Error>(())
```
When to prefer it:
- Hot read paths reading large values where the allocation
shows up in profiles.
- Read-only consumers passing the bytes onward (network, hash,
compare) without owning them.
- Any path where the rest of the operation is a single byte
scan.
When `get` is fine:
- The value will be modified.
- The value is small enough that the allocation is irrelevant.
- Encrypted databases — there's no zero-copy benefit.
---
## Bulk insert
### `insert_many<I, K, V>(items) -> Result<()>`
Insert many records under a single writer pass. Accepts any
iterator of `(K, V)` where both types implement `AsRef<[u8]>`.
The performance benefit comes from amortising the per-record
journal-append overhead: one batched encoder pass rather than N
individual `insert()` calls.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
let records = vec![
("user:1", "alice"),
("user:2", "bob"),
("user:3", "carol"),
];
db.insert_many(records)?;
assert_eq!(db.len()?, 3);
# Ok::<(), emdb::Error>(())
```
Compatible with any `IntoIterator`, including iterator
adaptors:
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert_many((0..1000).map(|i| (format!("k{i}"), format!("v{i}"))))?;
assert_eq!(db.len()?, 1000);
# Ok::<(), emdb::Error>(())
```
---
## Iteration
emdb's primary index is hash-keyed. Iteration order is
*unspecified* unless [range scans](#range-iteration-opt-in) are
enabled.
### `iter() -> Result<EmdbIter>`
Streaming iterator over `(key, value)` pairs in unspecified
order. `iter()` snapshots the file offsets of every live record;
each `next()` decodes one of them on demand, so memory use scales
with the offset count rather than total value size.
Because the snapshot points into the append-only log, every
snapshotted record is yielded with the value it had when `iter()`
was called, even if it is overwritten or removed while you iterate.
Keys inserted after `iter()` returns are not yielded. With the
`ttl` feature, records whose TTL has passed when `next()` reaches
them are skipped. Records that fail to decode are skipped silently
(a `try_iter` that surfaces errors is planned for 1.1).
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("a", "1")?;
db.insert("b", "2")?;
let mut count = 0;
for (key, value) in db.iter()? {
println!("{} = {}", String::from_utf8_lossy(&key), String::from_utf8_lossy(&value));
count += 1;
}
assert_eq!(count, 2);
# Ok::<(), emdb::Error>(())
```
### `keys() -> Result<EmdbKeyIter>`
Streaming iterator over keys only. Same snapshot semantics as
`iter()`, but values are never decoded or copied: the right choice
when you want the key set and don't care about values.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("a", "1")?;
db.insert("b", "2")?;
let keys: Vec<Vec<u8>> = db.keys()?.collect();
assert_eq!(keys.len(), 2);
# Ok::<(), emdb::Error>(())
```
### Range iteration (opt-in)
Range methods require `EmdbBuilder::enable_range_scans(true)` at
open time. They are backed by a parallel lock-free
`crossbeam_skiplist::SkipMap` per namespace, so inserts and range
iteration run concurrently without a global lock. Every write
updates the skiplist under the same per-key lock as the hash index,
so the two indexes agree for every key once the write returns. See
the README for the memory-cost trade-off.
#### `range<R>(range) -> Result<Vec<(Vec<u8>, Vec<u8>)>>`
Eagerly collects every `(key, value)` pair in the given range.
For large ranges, prefer `range_iter` to stream.
```rust
use emdb::Emdb;
let db = Emdb::builder().enable_range_scans(true).build()?;
db.insert("a", "1")?;
db.insert("b", "2")?;
db.insert("c", "3")?;
// Inclusive..exclusive.
let pairs: Vec<_> = db.range(b"a"..b"c")?.into_iter().collect();
assert_eq!(pairs.len(), 2);
# Ok::<(), emdb::Error>(())
```
Range bounds accept any [`RangeBounds<&[u8]>`][rb] — half-open,
inclusive, full, or unbounded.
[rb]: https://doc.rust-lang.org/std/ops/trait.RangeBounds.html
#### `range_iter<R>(range) -> Result<EmdbRangeIter>`
Streaming variant. Same results as `range`, but it is a cursor
over the sorted index rather than a snapshot: each refill seeks
just past the last key yielded and reads a small page (16 entries
at first, doubling up to 512). `take(n)` therefore costs one seek
plus `n` records regardless of the range size (about 2 us for
`iter_from(..).take(10)` over 1 M keys, against 50-120 ms in 1.0.2,
which copied the whole range first). Keys come out in ascending
order, each at most once; keys inserted or removed ahead of the
cursor while it runs may or may not be seen. A `compact()` while it
runs is transparent: it continues after its last key in the
compacted file.
```rust,no_run
use emdb::Emdb;
let db = Emdb::builder().enable_range_scans(true).build()?;
// Process the first N matching records and exit early.
for (k, v) in db.range_iter(b"prefix-".as_slice()..b"prefix-z".as_slice())?.take(100) {
let _ = (k, v);
}
# Ok::<(), emdb::Error>(())
```
#### `range_prefix(prefix) -> Result<Vec<(Vec<u8>, Vec<u8>)>>`
Convenience wrapper for "every key starting with `prefix`".
```rust
use emdb::Emdb;
let db = Emdb::builder().enable_range_scans(true).build()?;
db.insert("user:1", "alice")?;
db.insert("user:2", "bob")?;
db.insert("admin:1", "carol")?;
let users = db.range_prefix("user:")?;
assert_eq!(users.len(), 2);
# Ok::<(), emdb::Error>(())
```
#### `range_prefix_iter(prefix) -> Result<EmdbRangeIter>`
Streaming version of `range_prefix`.
#### `iter_from(start) -> Result<EmdbRangeIter>`
Iterate from `start` (inclusive) to the end of the keyspace.
The cursor primitive for paginated APIs.
#### `iter_after(start) -> Result<EmdbRangeIter>`
Iterate from `start` (exclusive). The "next page" form: pass
the last key from the previous page.
```rust
use emdb::Emdb;
let db = Emdb::builder().enable_range_scans(true).build()?;
for i in 0..50 {
db.insert(format!("rec-{i:03}"), "v")?;
}
// Walk the entire keyspace ten records at a time.
let mut cursor: Option<Vec<u8>> = None;
let page_size = 10;
let mut pages = 0;
loop {
let iter = match cursor.as_deref() {
Some(c) => db.iter_after(c)?,
None => db.iter_from("")?,
};
let page: Vec<Vec<u8>> = iter.take(page_size).map(|(k, _)| k).collect();
if page.is_empty() {
break;
}
cursor = page.last().cloned();
pages += 1;
}
assert_eq!(pages, 5);
# Ok::<(), emdb::Error>(())
```
---
## Transactions
### `transaction<F, T>(f) -> Result<T>`
Run a closure that stages a write batch. Inside the closure,
inserts and removes are staged in a [`Transaction`](#transaction);
when the closure returns `Ok` they are committed as one batch, and
when it returns `Err` they are discarded.
This is a buffered write batch, not an isolated transaction.
Guaranteed:
- Nothing staged is written if the closure returns `Err`.
- Reads through `tx` see the batch's own staged writes.
- The commit holds the write lock of every key it touches while it
appends the batch (one journal write) and updates the index, so
any other write to one of those keys, including another
transaction's, happens entirely before or after the commit.
Not guaranteed:
- Isolation. `tx.get` reads the live database; another thread can
change a key between that read and the commit, so
read-modify-write logic (counters, balance transfers) can lose
updates under concurrency. Serialise such updates yourself.
- Atomic visibility. Other threads can observe some of the batch's
keys updated and others not yet while the commit is applying.
- Crash atomicity. A crash during the commit's journal write can
leave a prefix of the batch durable.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("balance:alice", b"100".to_vec())?;
db.insert("balance:bob", b"50".to_vec())?;
db.transaction(|tx| {
let alice: u64 = std::str::from_utf8(&tx.get("balance:alice")?.unwrap())
.unwrap_or("0")
.parse()
.unwrap_or(0);
let bob: u64 = std::str::from_utf8(&tx.get("balance:bob")?.unwrap())
.unwrap_or("0")
.parse()
.unwrap_or(0);
tx.insert("balance:alice", (alice - 30).to_string())?;
tx.insert("balance:bob", (bob + 30).to_string())?;
Ok::<(), emdb::Error>(())
})?;
assert_eq!(db.get("balance:alice")?, Some(b"70".to_vec()));
assert_eq!(db.get("balance:bob")?, Some(b"80".to_vec()));
# Ok::<(), emdb::Error>(())
```
### `Transaction` methods
Inside the closure, `tx` is a `&mut Transaction` with these
methods:
- `insert(key, value)` — stage an insert.
- `insert_with_ttl(key, value, ttl)` — stage an insert with
TTL.
- `remove(key)` — stage a removal. Returns the staged-or-
underlying prior value.
- `get(key)`: read the live database (no snapshot), taking
staged writes into account.
- `contains_key(key)` — same, key-only.
The transaction is dropped on return: there is no explicit
`commit()` — returning `Ok` from the closure commits, returning
`Err` rolls back.
---
## Namespaces
A namespace is an isolated keyspace inside the same database file.
Records in different namespaces share no keys, no iteration
order, and no TTL state. The default namespace is the one
addressed by `db.insert(...)` etc.; named namespaces sit alongside
it.
### `namespace(name) -> Result<Namespace>`
Open or create a named namespace. The returned handle is cheap
to clone and shares the underlying engine.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
let users = db.namespace("users")?;
let sessions = db.namespace("sessions")?;
users.insert("u1", "alice")?;
sessions.insert("u1", "abc-123")?;
// Different namespaces, different values for the same key.
assert_eq!(users.get("u1")?, Some(b"alice".to_vec()));
assert_eq!(sessions.get("u1")?, Some(b"abc-123".to_vec()));
# Ok::<(), emdb::Error>(())
```
### `drop_namespace(name) -> Result<bool>`
Remove every record in the named namespace. Returns `Ok(true)`
if the namespace existed and was dropped, `Ok(false)` if it
didn't exist.
### `list_namespaces() -> Result<Vec<String>>`
Return the names of every named namespace (excludes the default
namespace).
### `Namespace` methods
A `Namespace` exposes the same KV + iteration surface as
`Emdb`: `insert`, `insert_many`, `get`, `get_zerocopy`,
`remove`, `contains_key`, `len`, `is_empty`, `clear`, `iter`,
`keys`, `range`, `range_iter`, `range_prefix`,
`range_prefix_iter`, `iter_from`, `iter_after`. Plus:
- `name() -> &str` — the namespace name.
Under the `ttl` feature, the same TTL surface is available
scoped to the namespace: `insert_with_ttl`, `expires_at`,
`ttl`, `persist`, `sweep_expired`. `insert` / `insert_many`
inherit the parent database's `default_ttl` (when configured),
and `get` filters expired records lazily — same semantics as
the top-level `Emdb` calls.
There is **no** namespace-scoped `flush()`, `checkpoint()`,
`stats()`, `compact()`, etc. — those are file-wide operations
on the parent `Emdb`. Call them on the parent handle.
---
## Nested groups
*Requires the `nested` feature.*
The nested feature adds dotted-prefix views over a flat keyspace.
A `Focus` is a cheap wrapper that prepends a prefix to every key
operation, letting you treat `"users.alice.name"` and
`"users.alice.email"` as a logical "alice" group.
### `db.focus(prefix) -> Focus<'_>`
Get a focus on the given dotted prefix. Cheap — no I/O.
```rust,no_run
# #[cfg(feature = "nested")]
# {
use emdb::Emdb;
let db = Emdb::open_in_memory();
let alice = db.focus("users.alice");
alice.set("name", "Alice")?;
alice.set("email", "alice@example.com")?;
// Equivalent to:
// db.insert("users.alice.name", "Alice")?
// db.insert("users.alice.email", "alice@example.com")?
# }
# Ok::<(), emdb::Error>(())
```
### `db.group(prefix) / db.delete_group(prefix)`
Materialised views: `group` returns every record under the
prefix, `delete_group` removes them.
### `Focus` methods
- `set(key, value)` / `get(key)` / `remove(key)` /
`contains_key(key)` — basic operations scoped to the prefix.
- `set_with_ttl(key, value, ttl)` — TTL variant.
- `iter()` / `delete_all()` — scoped operations.
- `focus(sub)` — drill in further (e.g. `db.focus("users").focus("alice")`).
---
## TTL
*Requires the `ttl` feature (default-enabled).*
### `insert_with_ttl(key, value, ttl) -> Result<()>`
Insert a record with a per-record expiration. The `ttl` argument
is a [`Ttl`](#ttl-1) enum:
- `Ttl::None` — never expires (override the builder's default).
- `Ttl::Default` — use the builder's `default_ttl`.
- `Ttl::Duration(Duration)` — expire after the given duration.
- `Ttl::ExpiresAt(u64)` — expire at the given Unix-millis timestamp.
```rust,no_run
# #[cfg(feature = "ttl")]
# {
use std::time::Duration;
use emdb::{Emdb, Ttl};
let db = Emdb::open_in_memory();
// Specific duration.
db.insert_with_ttl("session", "abc", Ttl::Duration(Duration::from_secs(60)))?;
// Use the builder default — no default set on this in-memory DB,
// so this is equivalent to Ttl::None.
db.insert_with_ttl("permanent", "data", Ttl::Default)?;
# }
# Ok::<(), emdb::Error>(())
```
### `expires_at(key) -> Result<Option<u64>>`
The Unix-millis expiration timestamp for a key, or `None` if the
key doesn't exist or has no expiration.
### `ttl(key) -> Result<Option<Duration>>`
Time-remaining-until-expiration for a key. Returns `None` if the
key doesn't exist or has no TTL. Returns `Some(Duration::ZERO)`
if the record has already expired but not yet been swept.
### `persist(key) -> Result<bool>`
Remove the TTL on a key, making it permanent. Returns `Ok(true)`
if the key exists, `Ok(false)` otherwise.
### `sweep_expired() -> usize`
Manually remove expired records. Returns the number of records
removed. emdb does not run a background sweeper — call this on a
schedule or after batch operations to reclaim space.
```rust,no_run
# #[cfg(feature = "ttl")]
# {
use std::time::Duration;
use emdb::{Emdb, Ttl};
let db = Emdb::open_in_memory();
db.insert_with_ttl("k", "v", Ttl::Duration(Duration::from_millis(1)))?;
std::thread::sleep(Duration::from_millis(10));
let evicted = db.sweep_expired();
assert_eq!(evicted, 1);
# }
# Ok::<(), emdb::Error>(())
```
---
## Persistence and durability
### `flush() -> Result<()>`
Make all writes since the last flush durable. Routes through
`fsys::JournalHandle::sync_through(next_lsn)`. Under
`FlushPolicy::Group`, concurrent flushers share a single fsync.
```rust,no_run
use emdb::Emdb;
let path = std::env::temp_dir().join("emdb-flush-example.emdb");
let db = Emdb::open(&path)?;
db.insert("k", "v")?;
db.flush()?; // durable on disk
# let _ = std::fs::remove_file(&path);
# let _ = std::fs::remove_file(format!("{}.lock", path.display()));
# Ok::<(), emdb::Error>(())
```
### `checkpoint() -> Result<()>`
Sync the journal (like `flush()`) and rewrite the `.meta` sidecar.
It does **not** speed up the next open: every open replays the
whole journal. Earlier documentation claimed a fast-reopen effect
that never existed. `checkpoint()` fails when an earlier write or
sync failure poisoned the journal.
Idiom:
- Call `flush()` after every important write (durability).
- Call `checkpoint()` (or `flush()`) before shutdown when you need
to know the data reached disk: dropping the last handle flushes
as a best effort but cannot report an error.
```rust,no_run
use emdb::Emdb;
let db = Emdb::open("data.emdb")?;
for i in 0..1000 {
db.insert(format!("k{i}"), "v")?;
}
db.flush()?;
db.checkpoint()?; // journal synced, sidecar rewritten
# Ok::<(), emdb::Error>(())
```
---
## Operational APIs
### `stats() -> Result<EmdbStats>`
Point-in-time database introspection. Cheap — `O(namespaces)`
plus one `metadata` syscall. Safe for per-second polling.
See [`EmdbStats`](#emdbstats) for the field list.
```rust
use emdb::Emdb;
let db = Emdb::open_in_memory();
db.insert("k", "v")?;
let stats = db.stats()?;
assert_eq!(stats.live_records, 1);
println!("logical size: {} bytes", stats.logical_size_bytes);
# Ok::<(), emdb::Error>(())
```
### `backup_to(target) -> Result<()>`
Atomic snapshot to a sibling file. Writes the records to
`<target>.backup.tmp` and the sidecar to `<target>.backup.tmp.meta`,
fsyncs both, renames them over `<target>.meta` and `<target>`
(replacing an existing backup atomically) and fsyncs the
directory. Failure before the renames leaves `target` untouched.
The result is a normal openable database (no proprietary dump
format) with its own sidecar: an encrypted source produces a
backup that opens with the same key or passphrase. Writers pause
only while the live set is captured; the copy runs concurrently
with them.
```rust,no_run
use emdb::Emdb;
let db = Emdb::open("/var/lib/myapp/sessions.emdb")?;
let backup_path = format!(
"/backups/sessions-{}.emdb",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
);
db.backup_to(&backup_path)?;
# Ok::<(), emdb::Error>(())
```
Heavy operation. Wrap in `tokio::task::spawn_blocking` from async
contexts. Refuses self-target (target == source).
### `compact() -> Result<()>`
Rewrite the database file with only live records, reclaiming
space from removed and replaced entries. Atomic via the same
temp-file + rename pattern as `backup_to`. Writers wait for the
whole compaction; readers, open iterators and `ValueRef`s are not
disturbed. Memory use is bounded (a few MiB of batches plus one
offset per live record). If the final rename fails, the database
keeps using the original file.
```rust,no_run
use emdb::Emdb;
let db = Emdb::open("/var/lib/myapp/sessions.emdb")?;
println!("before: {}", db.stats()?.file_size_bytes);
db.compact()?;
println!("after: {}", db.stats()?.file_size_bytes);
# Ok::<(), emdb::Error>(())
```
### `clone_handle() -> Emdb`
See [Construction](#construction). Cheap clone, shared engine.
### `path() -> &Path`
See [Construction](#construction). On-disk path.
---
## Lockfile recovery
When a process holding the database lock dies without releasing
(SIGKILL, panic during destructor, OOM kill), the lockfile
remains and subsequent `Emdb::open` calls fail with
`Error::LockBusy`.
Since 1.0.3 the `<path>.lock` file is created owner-only and is left
in place when a handle closes (only the `<path>.lock-meta` holder
file is removed). The lock itself is the OS advisory lock on that
file, which the OS releases when the holding process exits, so a
leftover `<path>.lock` file does not block the next open. The lock
path is derived from the canonical database path, so opening the
same file through a symbolic link contends for the same lock.
### `Emdb::lock_holder(path) -> Result<Option<LockHolder>>`
Inspect who holds the lock. Returns `Ok(None)` if the database
is unlocked.
```rust,no_run
use emdb::Emdb;
if let Some(holder) = Emdb::lock_holder("data.emdb")? {
println!(
"locked by pid {} since {} ms (crate {})",
holder.pid, holder.acquired_at_ms, holder.crate_version,
);
} else {
println!("unlocked");
}
# Ok::<(), emdb::Error>(())
```
### `Emdb::break_lock(path) -> Result<()>`
Forcibly remove the lockfile. **Only call this after confirming
via OS tooling that the holder PID is dead** (`ps -p`,
`Get-Process -Id`, container inspection). emdb cannot perform
this check portably; getting it wrong corrupts the database when
two processes write concurrently.
```rust,no_run
use emdb::Emdb;
let path = "data.emdb";
if let Some(holder) = Emdb::lock_holder(path)? {
// Verify the PID is dead via OS-specific means here.
// Then:
Emdb::break_lock(path)?;
}
let db = Emdb::open(path)?;
# let _ = db;
# Ok::<(), emdb::Error>(())
```
---
## Encryption
*Requires the `encrypt` feature.*
emdb supports at-rest encryption via AES-256-GCM (default) or
ChaCha20-Poly1305. Keys can be supplied raw (32 bytes) or derived
from a passphrase via Argon2id (19 MiB, 2 iterations, 1 lane; fixed).
What it protects: the contents of every record (keys, values, TTLs,
namespace names) against someone who reads the file. Each record is
authenticated on its own. What it does not protect: the log as a
whole. Someone who can write the file can delete, reorder or replay
whole records, or roll the file back to an older copy, without
detection. Record sizes, counts and order, the meta sidecar fields
and the lock holder's PID are visible. Nonces are random 96-bit
values: plan to rotate the key well before 2^32 total writes under
it. See [ARCHITECTURE.md](ARCHITECTURE.md#encryption) for details.
A keyed open only adopts a database that has no records yet. A
non-empty plaintext database must be converted with
`Emdb::enable_encryption` first; a keyed open of it returns
`Error::InvalidConfig` and leaves it untouched. `EmdbBuilder`'s
`Debug` output never shows the key or passphrase.
### `EmdbBuilder::encryption_key(key)`
Use a raw 32-byte key. The caller is responsible for key
management.
```rust,no_run
# #[cfg(feature = "encrypt")]
# {
use emdb::Emdb;
let key = [0u8; 32]; // load from your key store
let db = Emdb::builder()
.path("/var/lib/myapp/encrypted.emdb")
.encryption_key(key)
.build()?;
# }
# Ok::<(), emdb::Error>(())
```
### `EmdbBuilder::encryption_passphrase(passphrase)`
Derive a key from a passphrase via Argon2id. The salt is stored
in the metadata sidecar; the passphrase is **never** stored.
```rust,no_run
# #[cfg(feature = "encrypt")]
# {
use emdb::Emdb;
let db = Emdb::builder()
.path("/var/lib/myapp/encrypted.emdb")
.encryption_passphrase("correct horse battery staple")
.build()?;
# }
# Ok::<(), emdb::Error>(())
```
### `EmdbBuilder::cipher(cipher)`
Select between `Cipher::Aes256Gcm` (default) and
`Cipher::ChaCha20Poly1305`.
### `Emdb::enable_encryption(path, target)` (static)
Encrypt an existing plaintext database. Reads, re-encrypts, and
atomically replaces the file. Operates offline — the database
must not be open in any process.
### `Emdb::disable_encryption(path, current)` (static)
Decrypt an existing encrypted database to plaintext. Same
offline requirement.
### `Emdb::rotate_encryption_key(path, current, new)` (static)
Rotate the encryption key. Decrypts with `current`, re-encrypts
with `new`. Atomic via temp-file + rename.
```rust,no_run
# #[cfg(feature = "encrypt")]
# {
use emdb::{Emdb, EncryptionInput};
Emdb::rotate_encryption_key(
"data.emdb",
EncryptionInput::Passphrase("old passphrase".into()),
EncryptionInput::Passphrase("new passphrase".into()),
)?;
# }
# Ok::<(), emdb::Error>(())
```
---
## EmdbBuilder
The builder is reached via `Emdb::builder()`. Every method
returns `self` so calls chain.
| Method | Purpose |
|---|---|
| `path(p)` | Explicit on-disk path. |
| `app_name(s)` | OS-aware path resolution: app name. |
| `database_name(s)` | OS-aware path resolution: database file name. |
| `data_root(p)` | Override the OS-aware data root. |
| `default_ttl(d)` | Default TTL for records inserted via plain `insert`. |
| `enable_range_scans(b)` | Maintain a parallel lock-free `SkipMap` for range queries. Memory cost. |
| `flush_policy(p)` | `OnEachFlush` (default), `Group`, or `WriteThrough`. |
| `iouring_sqpoll(idle_ms)` | Opt-in Linux io_uring `SQPOLL` kernel-side submission polling. No-op on macOS/Windows. |
| `encryption_key(k)` | Raw 32-byte key (`encrypt` feature). |
| `encryption_passphrase(s)` | Argon2id-derived key (`encrypt` feature). |
| `cipher(c)` | AES-GCM (default) or ChaCha20-Poly1305 (`encrypt` feature). |
| `build()` | Open or create the database. |
| `build_async()` | Same as `build()`, but resolves to `AsyncEmdb` (`async` feature). |
Path resolution rules:
- If `path()` was set, use it.
- Else if `app_name()` and `database_name()` were both set,
resolve to the OS-aware data dir (XDG on Linux,
Application Support on macOS, %LOCALAPPDATA% on Windows).
- Else error: `Error::InvalidConfig`.
---
## Async surface
Gated behind the `async` feature. Wraps every blocking emdb call
in `tokio::task::spawn_blocking` so the runtime's async tasks
never stall on emdb's blocking I/O. Pulls in `tokio`
(`rt` + `rt-multi-thread` + `macros` + `sync`) and
`tokio-stream`.
### Why the surface exists
The sync `Emdb` / `Namespace` API is the source of truth — every
async method delegates to it inside a `spawn_blocking` closure.
That means:
- **No runtime split.** The blocking pool already exists in any
tokio runtime; there's no second thread pool to size.
- **No API duplication.** The sync surface is what gets
benchmarked, tested, and optimised; the async wrapper inherits
every fix automatically.
- **Zero overhead when off.** The `async` feature is opt-in.
Programs that don't use async pay no `tokio` dep cost.
### Cost model
Each async method dispatches one `spawn_blocking` task
(sub-microsecond on a warm pool) and clones key + value bytes to
owned `Vec<u8>` so the closure can take them by value. On a warm
blocking pool with cold I/O, the spawn overhead is negligible
against journal append / mmap decode / fsync. On a hot in-memory
key with a single hash-table probe, the spawn overhead may
dominate — reach for the sync handle via
`AsyncEmdb::sync_handle()` and batch via `insert_many` /
`transaction` instead.
### Construction
| Method | Returns |
|---|---|
| `AsyncEmdb::open(path)` | `Result<AsyncEmdb>` — async file-backed open. |
| `AsyncEmdb::open_in_memory()` | `AsyncEmdb` — sync (no I/O). |
| `AsyncEmdb::from_sync(emdb)` | `AsyncEmdb` — wrap an existing sync handle. |
| `AsyncEmdb::builder()` | `EmdbBuilder` — alias for `Emdb::builder()`. |
| `EmdbBuilder::build_async()` | `Result<AsyncEmdb>` — builder finaliser. |
### Sync-handle bridge
```rust,ignore
let async_db = AsyncEmdb::open_in_memory();
let sync_arc = async_db.sync_handle(); // Arc<Emdb>
// Use the sync surface inside one spawn_blocking when you need an
// API the async wrapper doesn't expose (e.g. zero-copy reads,
// custom iteration patterns).
let value: Option<Vec<u8>> = tokio::task::spawn_blocking(move || {
sync_arc.get("hot-key").map(|opt| opt.map(|v| v.to_vec()))
}).await??;
```
### Core operations
Every sync method has an async mirror with the same name. The
shapes are:
| Async method | Sync equivalent |
|---|---|
| `insert(key, value).await` | `Emdb::insert` |
| `insert_many(items).await` | `Emdb::insert_many` |
| `get(key).await` | `Emdb::get` (returns owned `Vec<u8>`) |
| `remove(key).await` | `Emdb::remove` |
| `contains_key(key).await` | `Emdb::contains_key` |
| `len().await` / `is_empty().await` / `clear().await` | self-evident |
| `flush().await` / `checkpoint().await` / `compact().await` | self-evident |
| `backup_to(path).await` | `Emdb::backup_to` |
| `stats().await` | `Emdb::stats` |
| `transaction(\|tx\| { … }).await` | `Emdb::transaction` (closure runs on blocking pool) |
| `namespace(name).await` | returns `AsyncNamespace` |
| `drop_namespace(name).await` / `list_namespaces().await` | self-evident |
| `insert_with_ttl(k, v, ttl).await` / `expires_at` / `ttl` / `persist` / `sweep_expired` | `ttl` feature |
### Iteration: eager vs streaming
Two flavours, picked per call:
**Eager** — materialises the full result into an owned `Vec`
inside one `spawn_blocking` before resolving. Convenient for
small result sets; the entire result is resident before the
first await completes.
| Eager method | Returns |
|---|---|
| `iter().await` | `Result<Vec<(Vec<u8>, Vec<u8>)>>` |
| `keys().await` | `Result<Vec<Vec<u8>>>` |
| `range(range).await` | `Result<Vec<(Vec<u8>, Vec<u8>)>>` |
| `range_prefix(prefix).await` | `Result<Vec<(Vec<u8>, Vec<u8>)>>` |
| `iter_from(start).await` | `Result<Vec<(Vec<u8>, Vec<u8>)>>` |
| `iter_after(start).await` | `Result<Vec<(Vec<u8>, Vec<u8>)>>` |
**Streaming** — drives the sync iterator on a dedicated
`spawn_blocking` task that pushes items through a bounded
`tokio::sync::mpsc` channel (capacity 64). The async caller
polls a `tokio_stream::wrappers::ReceiverStream`, applying
natural backpressure to the blocking pump task. Memory in flight
is bounded by the channel depth × per-record size, not the
namespace footprint. Dropping the stream early halts the pump
task on the next `blocking_send` (no leaked threads, no orphaned
`Arc`s).
| Streaming method | Returns |
|---|---|
| `iter_stream().await` | `Result<ReceiverStream<(Vec<u8>, Vec<u8>)>>` |
| `keys_stream().await` | `Result<ReceiverStream<Vec<u8>>>` |
| `range_stream(range).await` | `Result<ReceiverStream<(Vec<u8>, Vec<u8>)>>` |
| `range_prefix_stream(prefix).await` | `Result<ReceiverStream<(Vec<u8>, Vec<u8>)>>` |
| `iter_from_stream(start).await` | `Result<ReceiverStream<(Vec<u8>, Vec<u8>)>>` |
| `iter_after_stream(start).await` | `Result<ReceiverStream<(Vec<u8>, Vec<u8>)>>` |
Pick **eager** when the result set is small, fixed-size, and the
caller wants random-access semantics over the returned `Vec`.
Pick **streaming** when the result set is large or unbounded,
the caller wants time-to-first-byte rather than time-to-last, or
the consumer is downstream-bound (forwarding to a socket,
folding into an aggregate, writing to another store) and
peak-memory under load matters more than per-record latency.
### Streaming example
```rust,ignore
use emdb::AsyncEmdb;
use tokio_stream::StreamExt;
# async fn ex() -> emdb::Result<()> {
let db = AsyncEmdb::open_in_memory();
for i in 0_u32..10_000 {
db.insert(format!("k{i:05}"), format!("v{i}")).await?;
}
let mut stream = db.iter_stream().await?;
let mut processed = 0_u64;
while let Some((_key, value)) = stream.next().await {
// Process incrementally. Memory peak ≈ 64 × per-record bytes,
// not 10,000 × per-record bytes.
processed += value.len() as u64;
}
println!("processed {processed} bytes");
# Ok(())
# }
```
### Backpressure semantics
- Bounded channel capacity is **64**. Large enough that the
async consumer rarely starves the blocking pump on a
per-record basis; small enough that the absolute footprint
stays in the low-MiB range for kilobyte records.
- The pump task uses `blocking_send`. When the channel is full,
the blocking thread suspends until the async consumer drains
a slot. No spinning.
- If the consumer drops the stream early, the next
`blocking_send` returns `Err`, the pump's `for` loop breaks,
the sync iterator is dropped, and the blocking task exits.
### AsyncNamespace
Cheap-clone handle scoped to one named namespace inside an
`AsyncEmdb`. Mirrors every method on the sync `Namespace`,
threaded through `spawn_blocking`. Construct via
`AsyncEmdb::namespace(name).await`.
**Core KV** (eager): `insert`, `insert_many`, `get`, `remove`,
`contains_key`, `len`, `is_empty`, `clear`.
**Eager iteration**: `iter`, `keys`, `range`, `range_prefix`,
`iter_from`, `iter_after`.
**Streaming iteration** (returns `tokio_stream::wrappers::ReceiverStream`):
`iter_stream`, `keys_stream`, `range_stream`,
`range_prefix_stream`, `iter_from_stream`, `iter_after_stream`.
**TTL** (`ttl` feature): `insert_with_ttl`, `expires_at`, `ttl`,
`persist`, `sweep_expired` — same semantics as `Namespace` but
async.
**Plumbing**: `name() -> &str`, `sync_handle() -> crate::Namespace`
(bridge to the sync surface for operations the async wrapper
doesn't expose, e.g. zero-copy reads).
### Send + Clone
`AsyncEmdb` and `AsyncNamespace` are both `Send + Sync + Clone`
— clone freely; clones share the same underlying engine through
`Arc`. The returned `ReceiverStream`s are `Send`, so they survive
`.await` points on multi-thread tokio runtimes.
---
## Types reference
### `FlushPolicy`
```rust,ignore
pub enum FlushPolicy {
OnEachFlush,
Group,
WriteThrough,
}
```
- `OnEachFlush` *(default)* — one fsync per `db.flush()` call.
Right choice for single-thread workloads or when durability
is already batched at the application layer.
- `Group` — concurrent `flush()` calls coalesce into one fsync
via fsys's group-commit coordinator. Right for N-producer
workloads with per-record durability.
- `WriteThrough` — open the file with platform-native
synchronous-write flags (`FILE_FLAG_WRITE_THROUGH` on Windows,
`O_SYNC` on Linux/BSD). Every `pwrite` is durable on return;
`flush()` is near-free. Right when `OnEachFlush`'s
`FlushFileBuffers` cost dominates the workload.
### `Ttl`
```rust,ignore
#[non_exhaustive]
pub enum Ttl {
/// Use the builder-configured `default_ttl`. Behaves as
/// `Ttl::Never` when no default was set.
Default,
/// Explicit no-expiration.
Never,
/// Expire after the given `Duration` from insertion.
After(Duration),
}
```
Per-record expiration, used by `insert_with_ttl`. `Ttl::Default`
delegates to the builder's `default_ttl`; `Ttl::Never` overrides
any default to give the record permanent residency; `Ttl::After`
sets an absolute expiry of `now + duration`.
### `ValueRef`
The return type of `get_zerocopy`. Holds a strong handle to the
mmap region so the borrow is safe across writer activity.
```rust,ignore
impl ValueRef {
pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;
pub fn as_slice(&self) -> &[u8];
pub fn into_vec(self) -> Vec<u8>;
}
```
`Deref<Target = [u8]>` and `PartialEq<[u8]>` impls are
available for transparent comparison.
### `EmdbStats`
```rust,ignore
#[non_exhaustive]
#[derive(Copy, Clone, Debug)]
pub struct EmdbStats {
pub live_records: usize,
pub namespace_count: usize,
pub logical_size_bytes: u64,
pub file_size_bytes: u64,
pub preallocated_bytes: u64,
pub range_scans_enabled: bool,
pub encrypted: bool,
}
```
Returned by `Emdb::stats()`. `#[non_exhaustive]` so future fields
won't break exhaustive matches.
### `LockHolder`
```rust,ignore
#[non_exhaustive]
pub struct LockHolder {
pub pid: u32,
pub acquired_at_unix_millis: u64,
pub schema_version: u32,
pub crate_version: String,
}
```
Returned by `Emdb::lock_holder()`. Use to diagnose stale locks
before calling `break_lock`. `acquired_at_unix_millis` is the
wall-clock time at which the lock was acquired, in milliseconds
since the Unix epoch.
### Iterator types
emdb's iteration methods return owned-iterator structs (one for the
default namespace, one for named namespaces). Each implements the
standard `Iterator` trait and decodes records lazily. `iter`/`keys`
types walk a snapshot of offsets captured at construction time; the
range types are cursors over the live sorted index (see
[`range_iter`](#range_iterrrange---resultemdbrangeiter)).
**Default-namespace iterators** (returned by `Emdb` methods):
| Type | Returned by | `Iterator::Item` |
|---|---|---|
| `EmdbIter` | `Emdb::iter()` | `(Vec<u8>, Vec<u8>)` |
| `EmdbKeyIter` | `Emdb::keys()` | `Vec<u8>` |
| `EmdbRangeIter` | `Emdb::range_iter()`, `Emdb::range_prefix_iter()`, `Emdb::iter_from()`, `Emdb::iter_after()` | `(Vec<u8>, Vec<u8>)` |
**Namespace iterators** (returned by `Namespace` methods):
| Type | Returned by | `Iterator::Item` |
|---|---|---|
| `NamespaceIter` | `Namespace::iter()` | `(Vec<u8>, Vec<u8>)` |
| `NamespaceKeyIter` | `Namespace::keys()` | `Vec<u8>` |
| `NamespaceRangeIter` | `Namespace::range_iter()`, `Namespace::range_prefix_iter()`, `Namespace::iter_from()`, `Namespace::iter_after()` | `(Vec<u8>, Vec<u8>)` |
All six are `Send` and hold a shared handle to the engine, so they
can move freely across threads, and dropping the source `Emdb` /
`Namespace` does **not** invalidate an iterator. Snapshot iterators
yield each snapshotted record with its value at snapshot time, even
if it is removed or overwritten afterwards. All six skip records
whose TTL has passed (`ttl` feature) and skip records that fail to
decode without reporting an error.
### `Cipher` (`encrypt` feature)
```rust,ignore
#[non_exhaustive]
pub enum Cipher {
Aes256Gcm,
ChaCha20Poly1305,
}
```
### `EncryptionInput` (`encrypt` feature)
```rust,ignore
#[non_exhaustive]
pub enum EncryptionInput {
/// Raw 32-byte AES-256 key. Used as-is.
Key([u8; 32]),
/// UTF-8 passphrase derived to a 32-byte key via Argon2id.
/// On a fresh database the salt is generated; on reopen it is
/// read from the metadata sidecar.
Passphrase(String),
}
```
Used by the static encryption-admin methods
(`Emdb::enable_encryption`, `Emdb::disable_encryption`,
`Emdb::rotate_encryption_key`). Its `Debug` output is redacted
(`Key(<redacted>)`, `Passphrase(<redacted>)`); the value itself is
owned by the caller, who should wipe it after use.
---
## Errors
`emdb::Error` is a non-exhaustive enum. The variants you need to
handle in production code:
| Variant | When | Recovery |
|---|---|---|
| `Io(io::Error)` | Filesystem error (disk full, permission denied, a record over the 256 MiB frame cap, etc.). The OS error kind and code are preserved, `Display` includes the message, and `source()` returns the `io::Error`. | Inspect the inner `io::Error`. After a write or sync failure the journal is poisoned: reopen the database. |
| `LockBusy { path }` | `Emdb::open` saw a held lock. | See [Lockfile recovery](#lockfile-recovery). |
| `LockfileError(io::Error)` | The lock file could not be opened or locked for another reason. | Inspect the inner `io::Error`. |
| `MagicMismatch` | File at the path is not an emdb v0.9+/v1.x journal. The file is left untouched. | Confirm the path. v0.7/v0.8 files are not compatible with v0.9+/v1.x. |
| `VersionMismatch { found, expected }` | Sidecar format version doesn't match this emdb release. | Migrate via the previous emdb release. |
| `Corrupted { offset, reason }` | Damaged bytes followed by valid records (an open refuses rather than discarding them), or a record that fails validation: a malformed body, a record that references an unbound namespace id, a plaintext record in an encrypted database, or (in an encrypted database whose key verified) a record whose AEAD tag fails. The file is left untouched. | Restore from backup, or truncate at `offset` to keep everything before it (see ARCHITECTURE.md, "Recovering from `Error::Corrupted`"). This fires on hardware-level corruption or on modification of the file. |
| `InvalidConfig(reason)` | Builder configuration was inconsistent, or the file does not match the open mode (for example a keyed open of an existing plaintext database, or a plain open of an encrypted one). | Fix the builder call site; convert with `Emdb::enable_encryption` / `disable_encryption`. |
| `FeatureMismatch { file_flags, build_flags }` | The file needs a feature this build lacks. | Rebuild with the feature. |
| `InvalidPath` | A nested-key API got an empty prefix (`nested` feature). | Pass a non-empty prefix. |
| `TtlOverflow` | A TTL pushed the expiry past the representable time range (`ttl` feature). | Use a shorter TTL. |
| `Encryption(reason)` | Encryption machinery failed: malformed verification block, AEAD failure while encrypting, or the OS random number generator failed (`encrypt` feature). | Treat the database or the build as broken; not fixed by another key. |
| `EncryptionKeyMismatch` | The supplied key or passphrase does not decrypt the verification block in `<path>.meta` (`encrypt` feature). | Supply the right key. |
`Result<T>` is `std::result::Result<T, emdb::Error>`.
```rust
use emdb::{Emdb, Error};
fn open_or_recover(path: &str) -> emdb::Result<Emdb> {
match Emdb::open(path) {
Ok(db) => Ok(db),
Err(err @ Error::LockBusy { .. }) => {
// Diagnose with Emdb::lock_holder; potentially break_lock; retry.
Err(err)
}
Err(e) => Err(e),
}
}
```
---
## See also
- [README](../README.md) — quick-start and architecture.
- [BENCH.md](BENCH.md) — benchmark numbers and methodology.
- [CHANGELOG.md](../CHANGELOG.md) — release history.
- [docs.rs/emdb](https://docs.rs/emdb) — generated rustdoc.