Upgrade rehearsal: one build writes, the next reads it all back, and the older build refuses politely
- Status
- To do
- Component
- server
- Priority
- Highest — the only ticket here in the data-loss category
- Blocked by
- SAPE-1
- Blocks
- —
Description
Rehearse an upgrade before a user has to perform one. Build the server from two different commits. Use
the first to create a database and write to it, then open the same files with the second and read
everything back. Then run it the other way: the older build meets a file the newer one wrote, and must
refuse in a way that says what to do about it.
Why now
Nobody has ever upgraded a sapedb. There is no release to upgrade from and no migration path to take,
so unless this is rehearsed deliberately, the first upgrade in the product's history happens on a
stranger's machine with their data in it. That is the data-loss category, and it is the one category
where finding out afterwards is not an option.
Measured 2026-09-20. git tag in packages/sapedb lists zero
tags, and there is no migration code anywhere. The refusal path is half-built: pager.go:572
already returns fmt.Errorf("%w: file says %d, this build reads %d", ErrFormat, format, Format),
which is the right shape. But no test exercises it. Searching the test files for ErrFormat
finds nothing; the four hits are all ErrNotSapedb —
internal/pager/pager_test.go:288, internal/pager/led_test.go:167,
internal/pager/crypt_test.go:131. So the tested refusal is "this is not a sapedb file",
and the untested one is "this is a sapedb file from a version you cannot read" — which is the one an
upgrade actually produces.
Acceptance criteria
- Two builds from two different commits, distinguishable by
sapedb version.
- Build one creates a database and writes a known set of documents across more than one collection,
including at least one with an optional field set and one with it absent.
- Build two opens that database and reads back every document, compared field by field
against what was written. Not a count. The failure this catches is a document that survives
with a field missing, and a count agrees with itself while that happens.
- Reverse direction: build two writes, build one opens. It refuses — it does not
open, does not read partial data, and does not return garbage. Asserted as a specific error, not as
"some error occurred".
- The refusal message names what the file is and what this build can read, and says what to do. A test
asserts the message contains both numbers, so the message cannot silently degrade to a bare
ErrFormat.
- The whole rehearsal runs without publishing anything. Two local builds from two commits is the
entire apparatus; it does not wait on SAPE-4.
Out of scope
- Writing a migration. Nothing needs migrating yet — the point is to prove the refusal is clean before
any format change exists, so that when one does, the path is already known.
- Upgrading with the daemon running. Stop, swap, start. Live upgrade is not promised.
- Automatic backup before upgrade. The rehearsal must work on a database with no copy, because that is
what people will have.
- Downgrade support. The requirement is refusal, not compatibility.