0004. Read patching: where it hooks in and what it does differently from the first design
Copy Markdown View SourceStatus: accepted, 2026-10-22. Issue: #4.
Context
Design section 4.1 described the read patch in outline and left five Ash behaviours to confirm. They were checked
against Ash 3.37 (deps/ash).
Decision
- Where:
Ramal.Preparationis a global read preparation that, when a branch is current, adds abefore_actionand anafter_actionhook to the query.before_actionhooks run after all preparations and before the data-layer query is built, so they can still removelimitandoffset.after_actionhooks receive the base rows before relationship loads, runtime calculations and the not-found check ofAsh.get/3, so all of them see the patched rows. - Filter and sort:
Ash.Filter.Runtime.filter_matches/4evaluates the query's filter on the overlay records in one call. Ash.Actions.Sort.runtime_sort/3 sorts a list and has no comparator form, so the preparation callsOverlay.patch/4without:sort,:offsetand:limit, then sorts, drops and takes itself. The design's plan to pass a comparator topatch/4stays valid for callers that have one; the preparation is not one. - No stripping without overlay entries: when the overlay has nothing for the resource, the query keeps its limit and offset and Postgres pages. The design stripped them on every read in a branch; that made every read of an untouched table fetch all its filtered rows for no gain.
- Select: overlay records are whole structs, so they are trimmed to the query's
selectto match base rows. - Branch id in tasks: the id is also written to the query's
sharedcontext (ADR 0002). Ash builds the first level of a load in the calling process but the levels below it in a task, so only the shared context reaches them. - Domain check at runtime:
Ramal.branch/2returns:no_branchable_resourcesinstead of a compile-time domain verifier, which would require every host domain to add an extension. - Fingerprints are not recorded here. They belong to issue 6 together with the diff that uses them.
Consequences
- Paginated reads are paged by the database and then patched, so pages can be short or miss branch-created rows. Documented, not fixed, in 0.1.
authorize_resultschecks do not run on overlay records; policy filters do, through the query's filter.- Aggregates and database-computed calculations ignore the overlay and emit
[:ramal, :read, :unpatched_aggregate]. - The hooks call the branch server once per read (Ramal.Branch.Server.slice/2), and only the slice of that resource crosses the process boundary.