Merges a branch into the base: checks that the base did not move under the branch, then replays the branch's log through the application's real actions inside one transaction.
Ramal.merge/2 is the public entry point. The steps, in order:
- The branch process is claimed, which fails unless the branch is open and not already being merged. While it is claimed the process refuses writes, reads of its slice, discards and a second merge.
- Every base row the branch fingerprinted is read again, locked (
FOR UPDATE) where the data layer supports it, and compared. A row that is gone or whose fingerprint differs is stale.on_staledecides what happens. - The log is replayed oldest first with no branch current, so every action runs for real: validations and
policies see the base as it is now. Entries marked
nested?are skipped, because replaying the action that wrote them runs their hook again. - On success the branch is marked
:merged, persisted, and its process stops. On any failure the transaction is rolled back and the branch stays open.
Rows a replayed create writes keep the primary key they had in the branch (the key is forced onto the changeset), so later log entries and the branch's own diff still name the right rows.
Options
:actor- the actor for every replayed action. Default: the actor each entry was recorded with.:authorize?- defaulttrue:on_stale-:error(default),:ignore, or a function from the stale rows to:continueor:abort:mode- only:replay
Data layers that cannot run transactions, such as Ash.DataLayer.Ets, cannot roll a failed replay back. The staleness
check still runs before the first write, so a stale base writes nothing. A replayed action that fails after earlier
entries were written leaves those writes in place, the branch stays open, and the :merge_failed error lists the
resources in details.not_rolled_back. Ash.transact/3 runs the function as it is for such a data layer and returns
its {:error, _} inside {:ok, _}, which the merge unwraps.
Known limits, documented in docs/adr/0007-merge.md: tenants are refused inside a branch (ADR 0009), so the staleness check has none, a top-level action
that targets a row created by a hook cannot be replayed, and changes made with Ash.Changeset.change_attribute/3
outside the action's own inputs are not in the log.
Example
{:ok, %Ramal.Merge.Result{replayed: 1}} = Ramal.merge(branch, actor: current_user)
Summary
Functions
Merges the branch with id. See the module doc for the steps and the options.
Functions
@spec run(String.t(), keyword()) :: {:ok, Ramal.Merge.Result.t()} | {:error, Ramal.Error.t()}
Merges the branch with id. See the module doc for the steps and the options.
Returns {:error, %Ramal.Error{reason: :stale}} with the stale rows in details.rows, :merge_failed with the log
index, resource, action, Ash error and not_rolled_back resources in details when a replayed action fails,
:not_open for a merged, discarded or already-merging branch, and :nested_branch when called inside Ramal.run/2.
Example
{:error, %Ramal.Error{reason: :stale, details: %{rows: rows}}} = Ramal.merge(branch)