Thin facade over the host-configured Ecto.Repo.
Mailglass does not own a Repo — the host application does. Every context
module that needs Postgres routes through this facade, which resolves the
real Repo via Application.get_env(:mailglass, :repo) at call time.
Runtime resolution is deliberate: tests inject a test repo through
config/test.exs, host apps inject their Repo through
config :mailglass, repo: MyApp.Repo, and neither path requires
recompiling mailglass.
This module re-exports only what mailglass itself uses. Callers that need lower-level operations call the host Repo directly.
SQLSTATE 45A01 translation
Every write that touches mailglass_events can raise the immutability
trigger (BEFORE UPDATE OR DELETE raises SQLSTATE 45A01). The facade
rescues %Postgrex.Error{} at every call site and reraises as
Mailglass.EventLedgerImmutableError so callers pattern-match a
mailglass-owned error, never the raw Postgrex struct. The translation
is centralized in translate_postgrex_error/2 — adding new write
functions means wiring the same rescue clause.
Schema prefix injection (v2.0)
Every delegated read/write injects prefix: Mailglass.Config.schema() via
Keyword.put_new, routing all operations to the configured Postgres schema
("mailglass" by default, "public" for pre-2.0 opt-out). An explicit
caller-supplied :prefix wins over the injected default. This is the
FACADE-01/FACADE-02 load-bearing mechanism for schema isolation.
transact/2, multi/2, and query!/2 do NOT inject prefix — the prefix
must be threaded per-step in Ecto.Multi builders (see multi_opts/1).
A future raw caller passing a mailglass table to query!/2 must qualify
the table inline; the facade cannot rewrite arbitrary SQL.
Summary
Functions
Delegates to the host Repo's aggregate/4 (with optional opts for schema prefix).
Delegates to the host Repo's all/2.
Delegates to the host Repo's delete/2, translating event-ledger immutability errors.
Delegates to the host Repo's delete_all/2. Used by
Mailglass.Webhook.Pruner for retention-policy
DELETEs against mailglass_webhook_events.
Delegates to the host Repo's get/3.
Delegates to the host Repo's insert/2, translating event-ledger immutability errors.
Executes an Ecto.Multi against the host-configured repo and returns
the canonical {:ok, changes} / {:error, step, reason, changes} shape.
Returns opts with prefix: Mailglass.Config.schema() injected via
Keyword.put_new, for use as the step-level opts in Ecto.Multi builders
in domain modules (Events, Outbound, Suppression.Escalation).
Delegates to the host Repo's one/2.
Delegates to the host Repo's query!/2. Raw passthrough — no SQLSTATE
translation.
Delegates to Ecto.Repo.transact/2 on the host-configured Repo.
Delegates to the host Repo's update/2, translating event-ledger immutability errors.
Functions
@spec aggregate(Ecto.Queryable.t(), atom(), atom(), keyword()) :: term() | nil
Delegates to the host Repo's aggregate/4 (with optional opts for schema prefix).
@spec all( Ecto.Queryable.t(), keyword() ) :: [struct()]
Delegates to the host Repo's all/2.
@spec delete( struct() | Ecto.Changeset.t(), keyword() ) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}
Delegates to the host Repo's delete/2, translating event-ledger immutability errors.
@spec delete_all( Ecto.Queryable.t(), keyword() ) :: {non_neg_integer(), nil | [term()]}
Delegates to the host Repo's delete_all/2. Used by
Mailglass.Webhook.Pruner for retention-policy
DELETEs against mailglass_webhook_events.
Does NOT translate SQLSTATE 45A01 — that trigger fires only on
mailglass_events UPDATE/DELETE, not mailglass_webhook_events
(which is intentionally mutable + prunable per CONTEXT split).
@spec get(Ecto.Queryable.t(), term(), keyword()) :: struct() | nil
Delegates to the host Repo's get/3.
@spec insert( Ecto.Changeset.t() | struct(), keyword() ) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}
Delegates to the host Repo's insert/2, translating event-ledger immutability errors.
Executes an Ecto.Multi against the host-configured repo and returns
the canonical {:ok, changes} / {:error, step, reason, changes} shape.
Added in so Mailglass.Outbound can compose
two Multis via a public function — repo/0 is deliberately
private to keep the facade narrow.
Raises Mailglass.ConfigError{type: :missing} when :repo is not
configured (same path as transact/2). Event-ledger-immutability
errors (SQLSTATE 45A01) are translated via the same rescue as the
other write helpers.
NOTE: multi/2 does NOT inject prefix at the executor level — Ecto.Multi
does not propagate executor opts into inner step SQL. Use multi_opts/1
to thread prefix per-step in Multi builder chains.
Returns opts with prefix: Mailglass.Config.schema() injected via
Keyword.put_new, for use as the step-level opts in Ecto.Multi builders
in domain modules (Events, Outbound, Suppression.Escalation).
Ecto.Multi does NOT propagate the transaction/executor opts into inner
step SQL. Every builder step that writes a mailglass table must carry its
own :prefix; call multi_opts/1 (or multi_opts()) to build that opts
list rather than constructing it by hand.
An explicit caller-supplied :prefix wins over the injected default
(Keyword.put_new semantics).
Examples
Ecto.Multi.new()
|> Ecto.Multi.insert(:delivery, changeset, Repo.multi_opts())
|> Ecto.Multi.insert_all(:events, Event, rows, Repo.multi_opts(on_conflict: :nothing))
@spec one( Ecto.Queryable.t(), keyword() ) :: struct() | nil
Delegates to the host Repo's one/2.
Delegates to the host Repo's query!/2. Raw passthrough — no SQLSTATE
translation.
Intentionally does NOT rescue %Postgrex.Error{}: the webhook
ingest Multi calls query!/2 from inside Repo.transact/1
to run SET LOCAL statement_timeout = '2s' + SET LOCAL lock_timeout = '500ms'. Those SET LOCAL statements never produce SQLSTATE
45A01 — the immutability trigger fires only on UPDATE/DELETE against
mailglass_events rows — so translation would add latency for no gain
and muddle the semantics. Callers that need the trigger's translated
error use insert/2, update/2, delete/2, transact/1, or
multi/1 instead.
NOTE: the facade cannot rewrite arbitrary SQL. A future raw caller
that touches a mailglass table must schema-qualify the table inline
(e.g. "mailglass.mailglass_events") — the current two callers both
run schema-agnostic SET LOCAL statements.
Delegates to Ecto.Repo.transact/2 on the host-configured Repo.
Preferred over Ecto.Repo.transaction/2 because Ecto 3.13+ transact/2
accepts a zero-arity function that returns {:ok, result} or
{:error, reason} and rolls the transaction back on :error without
requiring Ecto.Repo.rollback/1.
Raises Mailglass.ConfigError of type :missing when :repo is not
configured. Raises Mailglass.EventLedgerImmutableError when the
mailglass_events immutability trigger fires (SQLSTATE 45A01).
Examples
Mailglass.Repo.transact(fn ->
{:ok, inserted} = Mailglass.Events.append(multi)
{:ok, inserted}
end)
@spec update( Ecto.Changeset.t(), keyword() ) :: {:ok, struct()} | {:error, Ecto.Changeset.t()}
Delegates to the host Repo's update/2, translating event-ledger immutability errors.