diff --git a/README.md b/README.md index ea7fb64..af543b1 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,7 @@ Honesty about scale (comprehension is the budget in v1, not throughput): | Architecture — the guided tour under the hood | [`ARCHITECTURE.md`](./ARCHITECTURE.md) | | Design — the locked engineering decisions | [`docs/DESIGN.md`](./docs/DESIGN.md) | | Reliability — durability and crash recovery | [`docs/RELIABILITY.md`](./docs/RELIABILITY.md) | +| Migration — on-disk versions and the downgrade warning | [`docs/MIGRATION.md`](./docs/MIGRATION.md) | | Manifesto — what LibreDB is and refuses to be | [`MANIFESTO.md`](./MANIFESTO.md) | | LibreDB Studio integration | [`docs/STUDIO.md`](./docs/STUDIO.md) | diff --git a/docs/CLI.md b/docs/CLI.md index f29d573..aaba735 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -224,7 +224,9 @@ file copy — with one rule. ``` - **Restore:** copy the file back and open it — recovery replays it like any - reopen. Nothing else to do. + reopen. Nothing else to do. Do not open a v1 file with LibreDB 0.1.3 or + older: that recovery truncates it to zero. See + [`MIGRATION.md`](./MIGRATION.md). - **Export as JSON:** `libredb export ` dumps the key-value layer as an import-compatible object — see [`export`](#export-path-filejson--json-dump) for exactly what it covers. diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md new file mode 100644 index 0000000..cce4a2a --- /dev/null +++ b/docs/MIGRATION.md @@ -0,0 +1,65 @@ +# Migration and downgrade + +The on-disk facts for moving between LibreDB versions. Durability of a single +file is in [`RELIABILITY.md`](./RELIABILITY.md). This page is where a search +for "downgrade" should land. + +## v0.1.x to v0.2.0 + +New databases begin with an 8-byte `LRDB` magic and version header. Each +record header also checksums its own length field. + +Files written by v0.1.x are headerless. `open()` still reads them through a +legacy path, and later appends on that file keep the legacy record framing. +Nothing rewrites an old file into the v1 layout just because a newer LibreDB +opened it. + +The header is what lets `open()` refuse a file that is not a LibreDB database +(`NOT_A_DATABASE`) and leave it byte-for-byte untouched. The length checksum +is what lets recovery refuse a damaged length field instead of treating it as +a torn tail. + +## Downgrade warning + +A file written by 0.2.0 or newer must never be opened by 0.1.3 or older. + +The old recovery cannot parse the header. It classifies the whole file as a +torn tail and silently truncates it to zero bytes. Back up before any +downgrade. A file copy taken while no writer has the database open is the +byte-exact backup; see [Backup and restore](./CLI.md#backup-and-restore). + +## Legacy behavior that changed in 0.2.0 + +- A headerless file whose only record is torn or incomplete now refuses to + open as `NOT_A_DATABASE`. 0.1.3 recovered that file as an empty database. + Refusing is the safe reading: such a file is indistinguishable from a + foreign one. +- Any file shorter than the 8-byte header is refused untouched. A crash inside + the first bytes of a brand-new database's first commit therefore needs a + manual delete. Nothing in that file was acknowledged. +- A damaged length field in a headerless v0.1.x file still reads as a torn + tail. The legacy format has no header checksum. The v1 format exists to + close that gap. + +## Converting a legacy file to v1 + +Opening a headerless file does not upgrade it. To get a v1 file on purpose, +copy the data into a database created by 0.2.0 or newer: + +- `libredb export` writes the key-value layer as JSON, and `libredb import` + into a fresh path writes a new file. That dump is logical, not byte-exact: + it carries the keys `import` can write back, not the catalog or the log. +- Or read through one `open()` and write through another into a new path. + +A non-mutating diagnosis command is tracked in +[#51](https://github.com/libredb/libredb-database/issues/51). There is no +in-place upgrade flag today. + +## Compatibility going forward + +v1 files stay readable by later 0.2.x releases. A newer on-disk version would +be a new header version, called out in the changelog the same way 0.2.0 was, +with the same rule: do not open a newer file with an older release that +cannot parse its header. The bar for calling the format a production store is +tracked separately in +[#58](https://github.com/libredb/libredb-database/issues/58). diff --git a/docs/RELIABILITY.md b/docs/RELIABILITY.md index 659cbc9..6ab802c 100644 --- a/docs/RELIABILITY.md +++ b/docs/RELIABILITY.md @@ -36,8 +36,10 @@ The failure modes *outside* the clean-crash model are handled explicitly rather a documented legacy limitation the v1 format closes. - **Downgrade warning.** A v1 file opened by LibreDB 0.1.3 or older is silently truncated to zero (the old recovery cannot recognize the header). Never downgrade past this version with live data; back up - first. Also: a headerless legacy file whose only record is torn now refuses to open (it is - indistinguishable from a foreign file); 0.1.3 opened it as empty. + first. The version story, the other legacy-behavior changes, and how to copy a headerless file into + a v1 database are in [`MIGRATION.md`](./MIGRATION.md). Also: a headerless legacy file whose only + record is torn now refuses to open (it is indistinguishable from a foreign file); 0.1.3 opened it + as empty. - **A short read is an IO fault, not missing data.** If the filesystem returns fewer bytes than the file holds, recovery throws (`code: "INCOMPLETE_READ"`) instead of mistaking the cut for a torn tail and truncating committed transactions.