wal-db 1.0.0

Write-ahead log primitive for Rust storage engines. Durable, recoverable, lock-free append path. The WAL substrate under lsm-db, txn-db, raft-io, and Hive DB.
Documentation
# wal-db v0.4.0 — Recovery hardening + typed records

**Recovery that survives anything, and records that can be typed.** v0.4.0 adds a
continuous fuzz harness that proves the recovery path never panics or
over-allocates on arbitrary bytes, a skip-bad-records policy for forensic partial
recovery, and an optional `pack-io` feature that lets records be typed values
instead of raw bytes. All additive — the default byte-record API is unchanged.

## What is wal-db?

A write-ahead log primitive for Rust storage engines — the durability substrate
under `lsm-db`, `txn-db`, `raft-io`, and Hive DB. The append path is lock-free,
concurrent commits coalesce into one fsync, durability is platform-correct, the
log can be striped across bounded segment files, and recovery is provable from a
torn write.

## What's new in 0.4.0

### Fuzz-hardened recovery

`fuzz/` adds a `cargo-fuzz` target that feeds arbitrary bytes to the recovery
path — open (which scans and truncates) and full iteration — under both recovery
policies, and asserts it never panics, over-allocates, or reads past the input.
The record-size cap means a crafted length prefix is rejected before any payload
allocation, so memory stays bounded no matter the input. Run it with:

```bash
cargo +nightly fuzz run recover
```

A representative local run executed **12 million inputs in 46 seconds with zero
crashes** and flat memory. CI runs it on every push.

### Recovery policies — `RecoveryPolicy`

`Wal::open` always truncates a torn tail so the append boundary is clean. For
corruption *inside* an already-recovered log — bit rot — a recovery policy now
controls how iteration reacts:

```rust
use wal_db::{RecoveryPolicy, Wal, WalConfig};

# fn main() -> Result<(), wal_db::WalError> {
# let dir = tempfile::tempdir().map_err(wal_db::WalError::from)?;
# let path = dir.path().join("app.wal");
let config = WalConfig::new().with_recovery_policy(RecoveryPolicy::SkipBadRecords);
let wal = Wal::open_with(&path, config)?;

for entry in wal.iter()? {
    match entry {
        Ok(record) => { /* use it */ }
        Err(e) => eprintln!("skipped a damaged record: {e}"), // iteration continues
    }
}
# Ok(())
# }
```

`StopAtFirstError` (the default) yields the first damage and stops.
`SkipBadRecords` surfaces each damaged record as an error — never silently — then
resumes at the next one. Skipping only works while a damaged record's length
prefix is intact enough to locate the next record; an unreadable length still
stops iteration, because there is no way to know where the next record begins.

### Typed records — the `pack-io` feature

By default a record is bytes. With the `pack-io` feature, a record can be any type
that derives `Serialize`/`Deserialize`:

```toml
[dependencies]
wal-db = { version = "0.4", features = ["pack-io"] }
```

```rust
use wal_db::{MemStore, Wal};
use wal_db::pack_io::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Event { id: u64, name: String }

# fn main() -> Result<(), wal_db::WalError> {
let wal = Wal::with_store(MemStore::new())?;
wal.append_typed(&Event { id: 1, name: "start".into() })?;

let event: Event = wal.iter()?.next().unwrap()?.decode()?;
assert_eq!(event, Event { id: 1, name: "start".into() });
# Ok(())
# }
```

`Wal::append_typed` serialises a value into one record; `Record::decode` reads it
back. The derives come from the re-exported `wal_db::pack_io`, so consumers do not
add the dependency themselves. A new `WalError::Encoding` variant reports a codec
failure. Everything here is opt-in: with the feature off, none of it exists and
the byte-record API is untouched.

## Breaking changes

**None.** The `pack-io` feature, `RecoveryPolicy`, the new config methods, and the
typed-record API are all additive, and `WalError::Encoding` is added under
`#[non_exhaustive]`. A log written by 0.3.x is read by 0.4.0 unchanged.

## Verification

Run on Windows x86_64 and Linux (WSL2 Ubuntu), Rust stable 1.95.x and MSRV
1.85.0; macOS is covered by the CI matrix:

```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo +1.85 clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo test                                    # default features
RUSTFLAGS="--cfg loom" cargo test --test loom_wal
cargo +nightly fuzz run recover -- -max_total_time=60
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +1.85 build --all-features
cargo audit
cargo deny check
```

All green on both platforms. Counts at this tag:

- 50 unit tests by default, 52 with `--all-features` (typed-record tests)
- 10 integration tests (round-trip, segmented, torn-write property test,
  cross-process durability)
- 2 loom model-check tests
- 1 fuzz target (12M+ inputs, no crashes)
- 22 doctests by default, 24 with `--all-features`

## What's next

- **0.5.0 — Feature complete + benchmarks.** LSN seeking (`Wal::iter_from`),
  LSN-based truncation for compaction, an optional async-friendly wrapper, and a
  recorded baseline benchmark suite. Feature freeze.

## Installation

```toml
[dependencies]
wal-db = "0.4"

# Typed records:
wal-db = { version = "0.4", features = ["pack-io"] }
```

MSRV: Rust 1.85.

## Documentation

- [README]https://github.com/jamesgober/wal-db/blob/main/README.md
- [API Reference]https://github.com/jamesgober/wal-db/blob/main/docs/API.md
- [On-Disk Format]https://github.com/jamesgober/wal-db/blob/main/docs/ON_DISK_FORMAT.md
- [CHANGELOG]https://github.com/jamesgober/wal-db/blob/main/CHANGELOG.md

---

**Full diff:** [`v0.3.1...v0.4.0`](https://github.com/jamesgober/wal-db/compare/v0.3.1...v0.4.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/wal-db/blob/main/CHANGELOG.md#040---2026-06-05).