# wal-db v0.2.0 — Foundation
**The first working release.** v0.2.0 turns the scaffold into a correct
single-writer write-ahead log: the four-call API (`open` / `append` / `sync` /
`iter`), per-record CRC32C checksums, platform-correct durability on Linux,
macOS, and Windows, and recovery that truncates a torn tail on open. Twelve new
public types, a torn-write property test, a cross-process durability test, and
criterion baselines. Zero `unsafe` outside a single documented macOS syscall
wrapper. The lock-free multi-writer path and group commit follow in 0.3.
## 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. Every state change is appended
to a durable log before it is acknowledged, and that log rebuilds state after a
crash. `wal-db` publishes that primitive as one audited, benchmarked crate so the
engines that need it stop re-deriving the durability contract and getting it
subtly wrong. The core is synchronous; async is left to the consumer.
## What's new in 0.2.0
### The four-call API — `Wal`
`Wal` is the log, generic over its backend as `Wal<S = FileStore>` so the plain
name is the file-backed log and Tier 1 never asks you to name a type parameter.
The common case is four calls:
```rust
use wal_db::Wal;
let wal = Wal::open("/var/lib/myapp/app.wal")?;
let lsn = wal.append(b"a state change")?; // in the page cache
wal.sync()?; // on stable storage
for entry in wal.iter()? { // replay on restart
let entry = entry?;
// apply(entry.lsn(), entry.data())
}
# Ok::<(), wal_db::WalError>(())
```
`append` takes `&self` — the signature the lock-free path will keep in 0.3 — so
code written now does not change when multi-writer lands. In 0.2 appends are
serialised through an internal mutex.
### The durability contract, made explicit
`append` returns when the record is in the OS page cache; `sync` returns when it
is on stable storage. They are deliberately separate calls, because the cost gap
between them is the difference between a usable database and a slow one, and
because conflating them is how data gets lost.
The flush is platform-correct, which is not the same call everywhere:
- **Linux** — `fdatasync`.
- **Windows** — `FlushFileBuffers`.
- **macOS** — `fcntl(F_FULLFSYNC)`, **not** plain `fsync`. The standard library's
`sync_data`/`sync_all` issue `fsync` on macOS, which flushes the page cache to
the device but leaves the data in the device's own write cache, where a power
loss still takes it. The one block of `unsafe` in the crate is this syscall,
documented with a `// SAFETY:` justification and gated to macOS.
### Torn-write recovery
Every record carries a CRC32C (Castagnoli) checksum over its length, LSN, and
payload, computed with the hardware CRC instruction on x86-64 and aarch64. On
`open`, the log scans forward and stops at the first record that is incomplete or
fails its checksum — the torn tail a crash mid-append leaves behind — and
truncates it. The records before it survive; the next append continues from a
clean boundary with no gap in the sequence numbers.
A corrupt length prefix can never trigger a wild allocation: every on-disk length
is validated against `max_record_size` before a payload byte is read.
```rust
use wal_db::Wal;
# fn main() -> Result<(), wal_db::WalError> {
# let dir = tempfile::tempdir().map_err(wal_db::WalError::from)?;
# let path = dir.path().join("app.wal");
// Reopening after a crash truncates the torn tail automatically.
let wal = Wal::open(&path)?;
for entry in wal.iter()? {
match entry {
Ok(record) => { /* apply */ }
Err(e) => { eprintln!("recovery stopped: {e}"); break; }
}
}
# Ok(())
# }
```
### Pluggable backends — `WalStore`, `FileStore`, `MemStore`
`Wal::open` uses the file-backed `FileStore`, which does all I/O positioned
(`pread`/`pwrite` on Unix, `seek_read`/`seek_write` on Windows) so a recovery
read never disturbs the append position. Any type implementing `WalStore` can
stand in via `Wal::with_store`; the shipped `MemStore` is the in-memory case, for
tests, examples, and benchmarking the framing path without touching a disk.
### Configuration — `WalConfig`
A builder, starting with `max_record_size`. The builder shape means the
parameters arriving later (sync policy, segment size, group-commit window) will
not break existing call sites.
```rust
use wal_db::{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_max_record_size(1024 * 1024);
let wal = Wal::open_with(&path, config)?;
# let _ = wal; Ok(())
# }
```
### Errors — `WalError`
A domain error type implementing `error_forge::ForgeError`, so it carries the
portfolio's stable `kind` / `is_fatal` metadata, while preserving the underlying
`io::Error` through `std::error::Error::source` for code that needs the OS error
kind. `#[non_exhaustive]`, with three variants in 0.2: `Io`, `RecordTooLarge`,
and `Corruption`.
### Tests and benchmarks
- A **torn-write property test** (`proptest`): for a sequence of records written
and then truncated at any byte offset, recovery returns all-and-only the
records that survived in full — always a prefix — and never errors on a cleanly
truncated log.
- A **cross-process durability test**: a child process appends, syncs, and exits
hard (no destructors); the parent reopens the file and verifies the records
survived. This exercises the real platform flush — `F_FULLFSYNC` on macOS,
`fdatasync` on Linux, `FlushFileBuffers` on Windows — across an actual process
boundary.
- **criterion baselines** for the append and append-and-sync paths.
## Breaking changes
**Relative to the 0.1 scaffold, yes — and intentionally.** 0.1 shipped no
functional code, only structure, so none of this breaks a real consumer:
- The crate is now standard-library only. The scaffold's `no_std` posture is
gone: a file-backed, fsync-driven log is inherently `std`, and the mandated
`error-forge` dependency is itself `std`.
- The placeholder `pack-io` optional dependency and the `std` / `batching`
feature flags are removed. The default feature set is empty. Typed record
framing returns as an additive `serial-io` feature in 0.4.
**On-disk format is unstable across 0.x.** Logs written by 0.2 are not guaranteed
readable by later 0.x releases. The format is documented normatively and frozen
for 1.x in 0.3.
## 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 test --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo +1.85 build --all-features
cargo audit
cargo deny check
cargo bench --bench wal_bench
```
All green on both platforms. Counts at this tag:
- 34 unit tests
- 6 integration tests (4 round-trip, 1 torn-write property test, 1 cross-process
durability)
- 18 doctests
Baseline criterion numbers (single-writer, development machine):
- `append` to an in-memory store (framing a 256-byte record, no I/O): ~130 ns.
- `append` + `sync` to a file (one record made fully durable): ~1.1 ms,
dominated by the disk flush.
These are honest starting points. The append path is mutex-serialised in 0.2;
the lock-free, sub-100 ns hot path is the work of 0.3 and 0.6.
## What's next
- **0.3.0 — Core.** Lock-free multi-writer append (atomic LSN allocation, atomic
byte-region reservation), group commit (N concurrent syncs coalesced into one
fsync), segment rotation, and the on-disk format freeze with a normative
`docs/ON_DISK_FORMAT.md`. `loom` model checks for the concurrent paths.
## Installation
```toml
[dependencies]
wal-db = "0.2"
```
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)
- [CHANGELOG](https://github.com/jamesgober/wal-db/blob/main/CHANGELOG.md)
---
**Full diff:** [`v0.1.0...v0.2.0`](https://github.com/jamesgober/wal-db/compare/v0.1.0...v0.2.0).
**Changelog:** [`CHANGELOG.md`](https://github.com/jamesgober/wal-db/blob/main/CHANGELOG.md#020---2026-06-05).