← 1.0.0 scope SAPE-6

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 ErrNotSapedbinternal/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

  1. Two builds from two different commits, distinguishable by sapedb version.
  2. 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.
  3. 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.
  4. 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".
  5. 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.
  6. 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