DB effect facade + backend behaviour.
Nodes call Bloccs.Effects.DB.insert/3 / get/3 / all/3 / one/3 against
ctx.effects.db; the facade dispatches to the bound backend (DB.Mock in
tests, DB.Ecto against a real repo in production). Scope enforcement
("table:action", where action is insert or read) is the backend's job.
Backends @behaviour Bloccs.Effects.DB.
Reads
get/3 looks a row up by primary key (id); all/3 and one/3 filter by a
restricted equality spec — a map of column => value, ANDed (deliberately
not a query language). All three require a "table:read" scope and return rows
as string-keyed maps, regardless of backend. Reads belong in effect_shell
(a read touches the world); feed the result into pure logic by emitting the
enriched payload to a downstream node, or read-and-reply in one shell for
request/response.
Summary
Types
A column assignment for update/4: a literal value sets col = value; the
tuple {:inc, n} sets col = col + n (a negative n decrements), which is an
atomic read-free increment — the right primitive for counters.
An ANDed equality filter: %{column => value}. Empty matches all.
A step for the declarative transaction/2 form.
Functions
Fetch every row in table matching filter (empty filter = all rows).
Delete the row in table with primary key id. Returns {:ok, rows_deleted}.
Fetch the row in table whose primary key (id) is id, or nil.
Insert attrs into table through the bound DB backend.
Fetch the single row in table matching filter. Returns {:ok, nil} for no
match and {:error, :multiple_results} if more than one matches.
Run a unit of work atomically. The second argument is either
Update the row in table with primary key id, applying changes
(%{column => value | {:inc, n}}). Returns {:ok, rows_updated} (0 or 1).
Types
@type cap() :: struct()
A column assignment for update/4: a literal value sets col = value; the
tuple {:inc, n} sets col = col + n (a negative n decrements), which is an
atomic read-free increment — the right primitive for counters.
@type declaration() :: %{allow: [String.t()]}
@type filter() :: map()
An ANDed equality filter: %{column => value}. Empty matches all.
@type op() :: {:insert, table(), attrs()} | {:update, table(), id :: term(), %{optional(term()) => change()}} | {:delete, table(), id :: term()}
A step for the declarative transaction/2 form.
Callbacks
@callback delete(cap(), table(), id :: term()) :: {:ok, non_neg_integer()} | {:error, term()}
@callback new(declaration()) :: cap()
Functions
Fetch every row in table matching filter (empty filter = all rows).
@spec delete(cap(), table(), term()) :: {:ok, non_neg_integer()} | {:error, term()}
Delete the row in table with primary key id. Returns {:ok, rows_deleted}.
Fetch the row in table whose primary key (id) is id, or nil.
Insert attrs into table through the bound DB backend.
Fetch the single row in table matching filter. Returns {:ok, nil} for no
match and {:error, :multiple_results} if more than one matches.
@spec transaction(cap(), (cap() -> {:ok, term()} | {:error, term()}) | [op()]) :: {:ok, term()} | {:error, term()}
Run a unit of work atomically. The second argument is either:
a function
fn db -> {:ok, result} | {:error, reason} end—dbis the same capability, so DB calls inside run in the transaction; returning{:error, _}(or raising) rolls everything back; or- a list of ops (
op/0) run in order, short-circuiting on the first{:error, _}(returned as{:error, {index, reason}}).
Returns {:ok, result} (the function's result, or the list of per-op results)
or {:error, reason}. Each inner op is scope-checked like a standalone call.
@spec update(cap(), table(), term(), %{optional(term()) => change()}) :: {:ok, non_neg_integer()} | {:error, term()}
Update the row in table with primary key id, applying changes
(%{column => value | {:inc, n}}). Returns {:ok, rows_updated} (0 or 1).