Ramal.Merge (Ramal v0.2.0)

Copy Markdown View Source

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:

  1. 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.
  2. 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_stale decides what happens.
  3. 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.
  4. 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? - default true
  • :on_stale - :error (default), :ignore, or a function from the stale rows to :continue or :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

run(id, opts \\ [])

@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)