# WAL v1 format

`Database::init(path)` creates the final database directory (its immediate
parent must already exist), adds `wal.bin`, then synchronizes the log's
directory entry and the database directory's parent;
`Database::open(path)` validates and replays the log. Each successful async
transaction commit appends exactly one record and synchronizes its data before
publishing the candidate graph. The record frame is:

| Field | Encoding |
| --- | --- |
| Magic | 8 bytes: `MPDBWAL\0` |
| Format version | little-endian `u16`, currently `1` |
| Reserved flags | little-endian `u16`, currently `0` |
| Total record length | little-endian `u64`, including the checksum |
| Transaction ID | little-endian `u64` |
| Operation count | little-endian `u32` |
| Operations | repeated tag byte, `u64` payload length, payload bytes |
| Checksum | little-endian `u32` CRC32/ISO-HDLC over all preceding bytes |

Operation payloads use little-endian fixed-width integers. Strings and byte
sequences use a `u64` byte length followed by the bytes. Property maps encode
their entry count as `u32` and sort keys before encoding. Property value tags
are `1` Bool, `2` Int64, `3` Float64 bit pattern, `4` UTF-8 String, and `5`
Bytes.

| Tag | Operation | Payload |
| --- | --- | --- |
| 1 | Create node | Node ID, label count and strings, property map |
| 2 | Create edge | Edge ID, from ID, to ID, edge type, property map |
| 3 / 4 | Add / remove node label | Node ID, label |
| 5 / 6 | Set / remove node property | Node ID, key, and value for tag 5 |
| 7 / 8 | Set / remove edge property | Edge ID, key, and value for tag 7 |
| 9 | Delete edge | Edge ID |
| 10 / 11 | Delete node / cascade delete | Node ID |
| 12 | Create node property index | Label, property key |
| 13 | Create edge property index | Edge type, property key |

The decoder bounds-checks all counts and lengths before slicing. It distinguishes
an incomplete final tail from a complete record with invalid framing, operation
payload, or checksum. Open applies each record to a private graph candidate and
publishes that candidate only after every operation in the transaction is
valid. A bad complete record stops open with a structured file/offset error.
After validating and replaying the complete prefix, open replaces an incomplete
final tail with that prefix through a same-directory temporary file, then
synchronizes the database directory before allowing new commits. If directory
synchronization fails, initialization or recovery returns a structured I/O
error rather than accepting the store.

Snapshots are still planned to contain the database format version, last
included transaction ID, next IDs, canonical nodes and edges, and property
index definitions. The current implementation loads the entire WAL into memory
on open, has no compaction, and rejects files at or above 2 GiB. Its advisory
file lock coordinates cooperating processes using `moonbitlang/async`; it does
not promise interoperability with unrelated locking implementations.
