Status: accepted, 2026-10-22. Issue: #6.
Context
Design sections 3, 7 and 8 describe fingerprints (for the staleness check at merge) and snapshots (for diff/1) as
two things kept "alongside" each other without fixing a shape, and say a fork copies the fingerprints.
Decision
Shape:
branch.baseis%{{resource, key} => %{fingerprint: integer | nil, snapshot: record | nil}}. One map, one entry per touched base row, so a row is never described twice.- When: the read preparation fingerprints every base row a read returns, before patching, in one call to the branch
process per read. The first write to a row adds its snapshot and keeps the fingerprint an earlier read took, because
staleness is measured from the first touch. A row is fingerprinted only when every attribute is loaded; a read with
selectskips it, as its hash would never equal a full read's. - What:
:erlang.phash2of the attribute map (Ramal.Resource.attributes/1, which leaves out relationships, calculations, aggregates and unloaded attributes), not of the struct, whose metadata differs between reads. - Cost: a read inside a branch hashes its rows and sends a map of
{ref, hash}to the branch process. Not measured;bench/will, before the README says anything about it. - Diff:
Ramal.Branch.diff/2is pure;Ramal.diff/1passes the Ash-aware attributes function. Order is the log order of first touch, and rows the log does not mention come last. - Fork:
Ramal.Branch.fork/2keeps overlay, log and base and changes id,inserted_at, name (default: the parent's) and metadata (default: the parent's, plusforked_from). It starts a process likebranch/2and emits[:ramal, :branch, :fork]followed by:load. - Write replies:
Server.write/2replies with%{id, domain}instead of the whole branch, so a captured write no longer copies the overlay back to the caller.update/2still does.
Consequences
- A partially selected read gives merge less to compare; rows only ever read that way are not checked for staleness.
- Reads that load many rows pay a hash per row. Relationship loads are reads too, so they are fingerprinted.
- Forking does not check that the parent's rows still match the database; merge does.