# `Ramal`
[🔗](https://github.com/agustinrojas1/ramal/blob/v0.2.0/lib/ramal.ex#L1)

Data branches as BEAM processes.

A branch is a process holding an overlay over an Ash application's data. Inside `run/2`, reads see the base
data patched with the branch's changes, and writes land in the overlay instead of the data layer. A branch can
be forked, diffed, merged back through the application's own actions, or discarded.

## Example

Add `Ramal.Supervisor` to the application's supervision tree first, then:

    {:ok, branch} = Ramal.branch(MyApp.Farm, name: "move herd R7 to paddock L18")

    {:ok, herds} = Ramal.run(branch, fn -> Ash.read!(MyApp.Farm.Herd) end)

    :ok = Ramal.discard(branch)

Every function that takes a branch also takes its id, and starts the branch process from storage when it is
not running, so callers never need to know whether a branch is in memory.

# `branch_or_id`

```elixir
@type branch_or_id() :: Ramal.Branch.t() | String.t()
```

# `aggregate`

```elixir
@spec aggregate(
  Ash.Query.t() | module(),
  Ramal.Aggregate.kind(),
  atom() | nil,
  keyword()
) ::
  {:ok, term()} | {:error, Ash.Error.t()}
```

Computes an aggregate over the rows a read of `query` returns. Inside `run/2` those rows are the branch's: base rows
patched with the overlay, filtered, sorted and limited as the query says. Outside a branch it is `Ash.count/2`,
`Ash.sum/3` and the like, so the same call works in both places.

This exists because the aggregates Ash hands to the data layer (`Ash.count/2`, `aggregates do ... end`) never see the
overlay. The answer is exact and its cost follows the overlay, not the table: the database aggregates the rows the
overlay does not touch, and the branch reads only the rows it does. A query with a `limit`, an `offset` or a
`distinct` is answered by reading every matching row instead, because the aggregate then depends on which rows come
first. See `Ramal.Aggregate`.

The rows the database aggregates are not read, so they are not fingerprinted and do not join the merge staleness
check (ADR 0013): a branch that only aggregated a row and never read or wrote it does not go stale when that row
changes.

`kind` is `:count`, `:exists`, `:sum`, `:min`, `:max` or `:avg`; `field` is an attribute (not needed for `:count` and
`:exists`). `opts` go to the read: `:actor`, `:authorize?`, `:tenant`, `:domain`. Returns `{:ok, value}` or the error
Ash returned. Raises `ArgumentError` for an unknown kind or a field that is not an attribute, in a branch or not.

Confirmed against Ash 3.37 (`lib/ash.ex`): `Ash.count/2` takes no field, and the others take `(query, field, opts)`.

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)

    Ramal.run!(branch, fn ->
      Ash.create!(MyApp.Farm.Herd, %{name: "R10", head: 5})
      {:ok, total} = Ramal.aggregate(MyApp.Farm.Herd, :sum, :head)
      total
      #=> the base total plus 5
    end)

# `aggregate!`

```elixir
@spec aggregate!(
  Ash.Query.t() | module(),
  Ramal.Aggregate.kind(),
  atom() | nil,
  keyword()
) :: term()
```

Like `aggregate/4` but returns the value or raises.

## Example

    Ramal.run!(branch, fn -> Ramal.aggregate!(MyApp.Farm.Herd, :count) end)

# `approve`

```elixir
@spec approve(branch_or_id(), keyword()) ::
  {:ok, Ramal.Merge.Result.t()} | {:error, Ramal.Error.t()}
```

Approves a proposed branch: merges it exactly as `merge/2` would (staleness check, replay through the real actions,
one transaction) and records the decision on the branch: who, why, when, how many entries were replayed and which
stale rows were let through.

Takes the options of `merge/2`, so `:actor` is the actor the replayed actions run as, and two more:

  * `:decided_by` - who approves it, default `nil`
  * `:reason` - why, a string, default `nil`

A stale base or a failing replay returns the same errors as `merge/2` and leaves the branch `:proposed`, so the
decider can retry with `on_stale: :ignore` or `reject/2` it. Returns `:not_open` for a branch that is not proposed.

## Example

    {:ok, %Ramal.Merge.Result{branch: branch}} =
      Ramal.approve(proposed, decided_by: manager, actor: manager, reason: "checked on site")

    branch.proposal.decision.status
    #=> :approved

# `branch`

```elixir
@spec branch(module(), keyword()) ::
  {:ok, Ramal.Branch.t()} | {:error, Ramal.Error.t()}
```

Creates an open branch over `domain` and starts its process.

## Options

  * `:name` - a label for people
  * `:metadata` - a map the host uses freely

Returns `{:error, %Ramal.Error{reason: :not_started}}` when `Ramal.Supervisor` is not running, and
`:no_branchable_resources` when no resource of the domain uses `Ramal.Resource` (or all of them opted out), since
such a branch could never change what a read returns, and `:limit_reached` when the supervisor's
`:max_active_branches` or `:branch_rate` is reached (both off by default, see `Ramal.Supervisor`).

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm, name: "what if", metadata: %{ticket: 42})
    branch.status
    #=> :open

# `current`

```elixir
@spec current() :: Ramal.Branch.t() | nil
```

The branch the calling process is running in, or `nil` outside `run/2`.

## Example

    Ramal.current()
    #=> nil
    Ramal.run!(branch, fn -> Ramal.current().id end)
    #=> branch.id

# `diff`

```elixir
@spec diff(branch_or_id()) :: {:ok, [Ramal.Change.t()]} | {:error, Ramal.Error.t()}
```

The changes a branch holds, as `Ramal.Change` structs in the order the branch first touched each row.

Never queries the database: updated and deleted rows are compared with the copy the branch kept when it first wrote
them. A row the branch changed and then changed back is left out. A fork's diff includes what it inherited.

## Example

    {:ok, [change]} = Ramal.diff(branch)
    change.kind
    #=> :updated
    change.changes
    #=> %{head: {30, 31}}

# `discard`

```elixir
@spec discard(branch_or_id()) :: :ok | {:error, Ramal.Error.t()}
```

Discards an open or proposed branch (a proposer withdrawing a proposal): its process stops and its overlay and log are
dropped.

The record stays in storage with status `:discarded`, so `list/2` can show it and later calls return
`:not_open`. Discarding an already discarded branch is a no-op. A merged branch cannot be discarded.

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    :ok = Ramal.discard(branch)
    {:error, %Ramal.Error{reason: :not_open}} = Ramal.run(branch, fn -> :never end)

# `fork`

```elixir
@spec fork(branch_or_id(), keyword()) ::
  {:ok, Ramal.Branch.t()} | {:error, Ramal.Error.t()}
```

Forks a branch: a new open branch, with its own process, that starts with the parent's overlay, log and base.

The fork sees the parent's changes as of the moment of the fork and nothing after; the parent never sees the fork's.
Both share the same database. `metadata.forked_from` holds the parent's id. A fork counts against the same limits
as `branch/2` (`:limit_reached`), counted for the parent's domain.

## Options

  * `:name` - default: the parent's name
  * `:metadata` - default: the parent's metadata

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    {:ok, fork} = Ramal.fork(branch, name: "variant")
    fork.metadata.forked_from == branch.id
    #=> true

# `get`

```elixir
@spec get(String.t()) :: {:ok, Ramal.Branch.t()} | {:error, Ramal.Error.t()}
```

The branch with that id, with its overlay and log as they are now.

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    {:ok, ^branch} = Ramal.get(branch.id)

# `list`

```elixir
@spec list(module(), keyword()) ::
  {:ok, [Ramal.Branch.t()]} | {:error, Ramal.Error.t()}
```

The branches of `domain` as last persisted, oldest first.

This reads storage and never loads a process, so a branch that is in memory shows the state of its last persist
(its creation, its last unload, or its status change). Use `get/1` for the live state.

## Options

  * `:status` - only branches with that status (`:open`, `:proposed`, `:merged`, `:rejected`, `:discarded`)

## Example

    {:ok, open} = Ramal.list(MyApp.Farm, status: :open)

# `merge`

```elixir
@spec merge(branch_or_id(), keyword()) ::
  {:ok, Ramal.Merge.Result.t()} | {:error, Ramal.Error.t()}
```

Merges a branch into the database by replaying its log through the application's own actions, in one transaction.

First every base row the branch read or wrote is compared with what the database holds now; if any changed, the
merge is refused unless `on_stale` allows it. Then the log is replayed oldest first with real validations and
policies. If anything fails, nothing is written and the branch stays open. On success the branch is `:merged`, its
process stops, and the result holds what the replay wrote. See `Ramal.Merge` for the details and the limits.

## Options

  * `:actor` - the actor for every replayed action. Default: the actor each write was captured with.
  * `:authorize?` - default `true`
  * `:on_stale` - `:error` (default), `:ignore`, or a function from the stale rows to `:continue` or `:abort`
  * `:mode` - only `:replay`

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    Ramal.run!(branch, fn -> Ash.update!(herd, %{head: 31}, authorize?: false) end)
    {:ok, %Ramal.Merge.Result{replayed: 1}} = Ramal.merge(branch, authorize?: false)

# `propose`

```elixir
@spec propose(branch_or_id(), keyword()) ::
  {:ok, Ramal.Branch.t()} | {:error, Ramal.Error.t()}
```

Proposes an open branch for a decision: the branch is frozen (status `:proposed`, no more reads, writes or forks) and
persisted, with who proposed it and why. Someone with the right to decide then calls `approve/2`, which merges it, or
`reject/2`. The proposer can still `discard/1` it to withdraw it.

Ramal does not decide who may approve: the host checks that before calling `approve/2` or `reject/2`, in its own
policies. The people involved are whatever terms the host passes (a user struct, an id); a record loses its
`sensitive?` attributes when stored (ADR 0010).

## Options

  * `:proposed_by` - who proposes it, default `nil`
  * `:reason` - why, a string, default `nil`

Returns `{:error, %Ramal.Error{reason: :not_open}}` for a branch that is not open, including one already proposed.
List the pending ones with `list(domain, status: :proposed)`; the diff to review is `diff/1`.

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    Ramal.run!(branch, fn -> Ash.update!(herd, %{head: 31}, authorize?: false) end)
    {:ok, proposed} = Ramal.propose(branch, proposed_by: user, reason: "R7 has 31 head after the count")
    proposed.status
    #=> :proposed

# `reject`

```elixir
@spec reject(branch_or_id(), keyword()) ::
  {:ok, Ramal.Branch.t()} | {:error, Ramal.Error.t()}
```

Rejects a proposed branch: the decision is recorded and persisted, the branch becomes `:rejected` and its process
stops. The overlay and log are kept so the rejected diff can still be read with `get/1` and `diff/1`; `discard/1`
does not apply to a rejected branch.

## Options

  * `:decided_by` - who rejects it, default `nil`
  * `:reason` - why, a string, default `nil`

Returns `:not_open` for a branch that is not proposed.

## Example

    {:ok, rejected} = Ramal.reject(proposed, decided_by: manager, reason: "count it again")
    rejected.status
    #=> :rejected

# `run`

```elixir
@spec run(branch_or_id(), (-&gt; result)) :: {:ok, result} | {:error, Ramal.Error.t()}
when result: term()
```

Runs `fun` with the branch as the current branch of the calling process and returns `{:ok, result}`.

The previous state is restored when `fun` returns and also when it raises, throws or exits. Branches do not nest:
calling `run/2` inside another `run/2` returns `{:error, %Ramal.Error{reason: :nested_branch}}` without running
`fun`. Tasks started inside `fun` do not inherit the branch; call `run/2` inside the task with the same branch.

Returns `:not_found` for an unknown id and `:not_open` for a merged or discarded branch.

Creates, updates and destroys inside `fun` land in the branch instead of the data layer (`Ramal.Capture`), and the
notifications they produce are dropped unless the resource sets `notify_in_branch? true`.

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    {:ok, nil} = Ramal.run(branch, fn -> Ramal.current() && nil end)

# `run!`

```elixir
@spec run!(branch_or_id(), (-&gt; result)) :: result when result: term()
```

Like `run/2` but returns the result and raises `Ramal.Error` instead of returning an error tuple.

## Example

    {:ok, branch} = Ramal.branch(MyApp.Farm)
    Ramal.run!(branch, fn -> :done end)
    #=> :done

---

*Consult [api-reference.md](api-reference.md) for complete listing*
