Reference

Everything XKV offers, on one page: the Go package, the xkv command, the HTTP mode, and the file. The README is the full manual, and the Go documentation is on pkg.go.dev.

Revisions

Every write is a revision. A revision has:

Field Meaning
Rev store-wide revision number; only goes up
Tx transaction id; revisions written together share it
Key, Value the key and its new value
Deleted true when the revision deletes the key
Actor who made it
Reason why; required unless the store is relaxed
Ref an outside reference: a ticket, a request id, a tool call id
Causes up to 8 earlier revisions, as key and revision number
Time when it was written

The Go Package

Call Does
Open(path, opts) opens or creates a store file; one writer per file
OpenMemory(opts) a store in memory, for tests and the browser
Put(ctx, key, value, opts...) writes one value
Delete(ctx, key, opts...) deletes a key; the delete is a revision too
Update(ctx, fn, opts...) writes several keys in one transaction
Get(ctx, key) the current revision
GetAt(ctx, key, rev) the key as the store stood at revision rev, or ErrCompacted if compaction removed that state
Revision(ctx, rev) one revision by number
History(ctx, key, limit) a key’s revisions, newest first
Why(ctx, key, depth) the tree of causes behind the current value
WhyRev(ctx, rev, depth) the same for one revision
Keys(ctx, prefix) keys that have a value
Compact(ctx, opts) drops old revisions, keeping every one a kept value points at. On Windows it fails with nothing changed while another process has the file open
Check(path) reads every record and cause of a file

Write options: Reason(s), Actor(s), Ref(s), Cause(key, rev) and CauseLatest(key). WithActor(ctx, name) sets the actor for every write made with that context.

Open options: ReadOnly, Relaxed (accept writes without a reason, recorded as “unspecified”), NoSync (skip the fsync after each transaction; a power cut can then need a repair), Repair (trim a damaged file back to its last good transaction), DefaultActor.

Errors to check with errors.Is: ErrNotFound, ErrNoReason, ErrBadCause, ErrCompacted, ErrLocked, ErrReadOnly, ErrCorrupt, ErrFailed (a write or fsync failed; reopen the store), ErrTooLarge, ErrInvalid.

Causes

A cause must exist, must belong to the key it names, and must be older than the write. A write with a bad cause is refused and writes nothing. CauseLatest(key) points at the key’s revision current when the write is made. Inside a transaction, a cause may point at a write made earlier in the same transaction.

Why follows causes 16 levels deep by default. A node whose causes the limit didn’t reach is marked Truncated; a revision reached twice is shown in full once and marked Repeat after that. Each node also carries Prev, the key’s revision just before it. If compaction removed that revision, Prev is empty and PrevCompacted is set.

The xkv Command

Command Does
xkv put FILE KEY VALUE --reason R writes a value; add --cause KEY[@REV], --actor, --ref, or --stdin for the value
xkv del FILE KEY --reason R deletes a key
xkv get FILE KEY [--at REV] prints the value
xkv log FILE KEY [--limit N] a key’s revisions, newest first
xkv why FILE KEY [--depth N] the chain of causes; --rev REV for one revision
xkv rev FILE REV one revision
xkv keys FILE [PREFIX] keys that have a value
xkv stats FILE counts
xkv check FILE checks every record and cause
xkv compact FILE [--keep N] [--keep-days D] compacts
xkv repair FILE trims a damaged file back to its last good transaction, keeping the cut bytes in a side file
xkv serve FILE [--addr HOST:PORT] the HTTP mode

Most commands take --json. The actor is --actor, else $XKV_ACTOR, else your user name. Read commands open the file read-only, so they work while xkv serve holds it.

HTTP Mode

xkv serve FILE listens on 127.0.0.1:7070 and speaks JSON. It has no authentication, so keep it on localhost.

Method and path Does
GET /v1/keys?prefix=P keys that have a value
GET /v1/keys/KEY[?at=REV] a key’s revision
PUT /v1/keys/KEY writes; body {"value", "reason", "actor", "ref", "causes": [{"key", "rev"}]}
DELETE /v1/keys/KEY deletes; same body without value
GET /v1/history/KEY[?limit=N] revisions, newest first
GET /v1/why/KEY[?depth=N] the tree of causes
GET /v1/revs/REV and /v1/revs/REV/why one revision, and its tree
GET /v1/stats counts

A cause without rev points at the key’s current revision. Errors come back as {"error": "..."} with 400, 404, 410, 413 or 500. The HTTP reference has the details.

The File

A store is one append-only file: a 32-byte header, then frames, each with a length and a CRC-32C checksum. A transaction is its revision records followed by a commit record, written with one write call and an fsync. On open, a torn end of file is trimmed back to the last complete transaction; damage in the middle is refused. An index snapshot beside the file, FILE.idx, makes large files open faster and is always checked against the file. The format is written up byte by byte.

Limits

Keys up to 4 KB, values up to 16 MB, reasons up to 4 KB, 8 causes per write, 64 MB per transaction. The index lives in memory at about 70 bytes per revision.