Ramal.Capture (Ramal v0.2.0)

Copy Markdown View Source

The change Ramal.Resource installs on every create, update and destroy action.

Inside Ramal.run/2 it replaces "write this row to the data layer" with "put this row in the branch's overlay". Everything else about the action still happens: argument casting, validations, policies, defaults, timestamps and the action's own before_action and after_action hooks. Outside a branch it does nothing.

What the action returns is what the action would have returned: a created or updated record, or the destroyed one. The write is also appended to the branch log so that Ramal.merge/2 can replay the action later.

How it works, confirmed against Ash 3.37 (lib/ash/changeset/changeset.ex and lib/ash/actions/*):

  • Ash.Changeset.around_action/3 wraps the before-action hooks, the data-layer call and the after-action hooks. The around hook adds one more before_action, so that it is the last of them and sees the changeset as every other hook left it.
  • That hook builds the record with Ash.Changeset.apply_attributes/2, which also applies defaults, and calls Ash.Changeset.set_result/2. The create, update and destroy actions skip the data layer when a result is set and carry on with relationship management and the after-action hooks.
  • An update runs fully_atomic_changeset first and, when it succeeds, updates the row in the database without calling any hook. atomic/3 therefore reports {:not_atomic, reason} while a branch is current, and the resource transformer sets require_atomic? false so Ash falls back to the path above.
  • Bulk actions call the data layer directly and never look at the result, so they are refused. Upserts too: the branch cannot know whether the row exists in the base.

A failed action leaves the branch as it found it: the writes of the action and of the actions its hooks ran are taken back. A write made by a hook of another captured action is logged with nested?: true, because replaying the outer action runs the hook again.

Example

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

Ramal.run!(branch, fn ->
  herd = Ash.create!(MyApp.Farm.Herd, %{name: "R7", head: 40})
  Ash.destroy!(herd)
end)
# The database was not written; the branch log holds the create and the destroy.