The overlay: what a branch changed, as a plain map.
Keys are {resource, key} pairs, called refs here, where key is the row's primary key. Values are
{:created, record}, {:updated, record} or :deleted. The record is whatever the caller stores, normally the
full Ash struct as the action left it.
Everything in this module is a pure function over maps and lists. It never calls Ash, so it is tested with plain maps. The pieces that need Ash (running the filter, sorting by Ash expressions, listing attributes) arrive as functions from the caller.
How entries combine
The overlay only records the net effect on the base, so putting a second entry for a ref follows these rules:
| existing | put_created | put_updated | put_deleted |
|---|---|---|---|
| none | :created | :updated | :deleted |
:created | :created (replaced) | :created (record replaced) | no entry: the base never had the row |
:updated | :updated | :updated | :deleted |
:deleted | :updated: the base row is replaced | :updated | :deleted |
A :deleted entry therefore always means the base had the row. Rejecting a duplicate primary key or an update of
a missing row is the write layer's job (the data layer would do it on the real base); the overlay does not police it.
Example
iex> ref = {MyApp.Herd, "R7"}
iex> overlay = Ramal.Overlay.new() |> Ramal.Overlay.put_updated(ref, %{id: "R7", name: "R7b"})
iex> Ramal.Overlay.slice(overlay, MyApp.Herd)
%{"R7" => {:updated, %{id: "R7", name: "R7b"}}}
Summary
Functions
The overlay as a list of Ramal.Change structs, computed against the base rows as the branch first saw them.
Applies other's entries on top of overlay, as if each had been put in turn.
An empty overlay.
Applies a resource's overlay slice to the rows the base returned for a query.
Records that the branch created the row ref with record.
Records that the branch deleted the row ref.
Records that the branch updated the row ref; record is the row as the update left it.
The entries for one resource, keyed by primary key.
Types
Functions
@spec diff(t(), %{optional(ref()) => record()}, keyword()) :: {:ok, [Ramal.Change.t()]} | {:error, Ramal.Error.t()}
The overlay as a list of Ramal.Change structs, computed against the base rows as the branch first saw them.
base maps each ref the branch touched on an existing row to that row's snapshot. It never queries anything: the
snapshot is what the branch kept at first touch. Created rows need no snapshot. Rows whose updates netted out to
no difference are left out.
The result is sorted by ref so it is deterministic. The overlay is a map and does not remember the order of writes;
Ramal.diff/1 orders changes by the branch log.
Returns {:error, %Ramal.Error{reason: :missing_snapshot}} when an updated or deleted entry has no snapshot, which
means the write layer forgot to record one.
Options
:attributes- a function from a record to a%{atom => value}map. Defaults to the record's own keys without the__meta__-style ones. The Ash-aware caller passes one built from the resource's attributes.
Example
iex> overlay = %{{MyApp.Herd, 1} => {:updated, %{id: 1, name: "b"}}}
iex> base = %{{MyApp.Herd, 1} => %{id: 1, name: "a"}}
iex> {:ok, [change]} = Ramal.Overlay.diff(overlay, base)
iex> {change.kind, change.changes}
{:updated, %{name: {"a", "b"}}}
Applies other's entries on top of overlay, as if each had been put in turn.
Forking uses it: a fork starts as merge_into(new(), parent). Because overlays are immutable maps, the fork and
the parent never see each other's later changes. The combination rules are the ones in the module doc, so merging
a :deleted entry over a :created one removes the row from the result.
Example
iex> parent = %{{MyApp.Herd, 1} => {:created, %{id: 1}}}
iex> Ramal.Overlay.merge_into(Ramal.Overlay.new(), parent) == parent
true
@spec new() :: t()
An empty overlay.
Example
iex> Ramal.Overlay.new()
%{}
Applies a resource's overlay slice to the rows the base returned for a query.
base_rows are the rows the data layer returned for the query's filter, without limit and offset. The result is
what the same query would return on a base that already held the branch's changes:
- a base row whose key is
:deletedin the slice is dropped; - a base row whose key is
:updatedis replaced by the overlay record, or dropped when that record no longer satisfiespredicate; - an overlay record the base did not return is added when it satisfies
predicate. This covers created rows and updated rows that only match the filter after the update; - the rows are sorted, then
offsetandlimitare applied. Doing that last is what lets a row created in the branch appear in a limited query.
predicate is called only on overlay records, since the base already applied the filter to its own rows. Callers
evaluate the query's filter in it, for example with Ash.Filter.Runtime.
Options
:key(required) - a function returning the primary key of a row, the same keys the slice uses:sort- a comparator(a, b -> boolean)asEnum.sort/2takes it; returntruewhenagoes before or equal tob, which keeps the sort stable. Without it, base order is kept and added rows follow in key order:offset- rows to skip after sorting, default0:limit- the maximum number of rows, default no limit:replace- a function(base_row, overlay_record -> record)that builds the row that takes a base row's place, for state the base row carries and the overlay record cannot, default the overlay record itself
Example
iex> base = [%{id: 1, n: 10}, %{id: 2, n: 20}]
iex> slice = %{2 => :deleted, 3 => {:created, %{id: 3, n: 30}}}
iex> Ramal.Overlay.patch(base, slice, fn _ -> true end, key: & &1.id, sort: &(&1.n >= &2.n), limit: 2)
[%{id: 3, n: 30}, %{id: 1, n: 10}]
Records that the branch created the row ref with record.
See the table in the module doc for how it combines with an existing entry.
Example
iex> Ramal.Overlay.put_created(%{}, {MyApp.Herd, 1}, %{id: 1})
%{{MyApp.Herd, 1} => {:created, %{id: 1}}}
Records that the branch deleted the row ref.
Deleting a row the branch itself created removes the entry: the base never had that row, so there is nothing to hide.
Example
iex> overlay = Ramal.Overlay.put_deleted(%{}, {MyApp.Herd, 1})
iex> overlay
%{{MyApp.Herd, 1} => :deleted}
iex> overlay |> Ramal.Overlay.put_created({MyApp.Herd, 2}, %{id: 2}) |> Ramal.Overlay.put_deleted({MyApp.Herd, 2})
%{{MyApp.Herd, 1} => :deleted}
Records that the branch updated the row ref; record is the row as the update left it.
A row the branch created stays :created, with the new record.
Example
iex> overlay = Ramal.Overlay.put_created(%{}, {MyApp.Herd, 1}, %{id: 1, v: 1})
iex> Ramal.Overlay.put_updated(overlay, {MyApp.Herd, 1}, %{id: 1, v: 2})
%{{MyApp.Herd, 1} => {:created, %{id: 1, v: 2}}}
The entries for one resource, keyed by primary key.
Example
iex> overlay = %{{MyApp.Herd, 1} => :deleted, {MyApp.Paddock, 9} => {:created, %{id: 9}}}
iex> Ramal.Overlay.slice(overlay, MyApp.Herd)
%{1 => :deleted}