# `Mutare.Ecto.RepoWrite`
[🔗](https://github.com/foxbenjaminfox/mutare_ecto/blob/v0.4.2/lib/mutare/ecto/repo_write.ex#L1)

Mutations on the **persisting Repo writes** — `insert`/`update`/`delete`/`insert_or_update`
and their `!` variants. Two families, both matched by resolving the call's module to one of the
configured `repo:` modules (so direct, aliased, and `use Ecto.Repo`-defined forms all match):

  * **`:persistence`** — replace the write with the equivalent *non-persisting*
    `Ecto.Changeset.apply_action/2`, surfacing **untested persistence**:

        Repo.insert(cs)
        →
        Elixir.Ecto.Changeset.apply_action(
          Elixir.Map.replace!(Elixir.Ecto.Changeset.change(cs), :repo, Elixir.MyApp.Repo),
          :insert
        )

        Repo.insert_or_update!(cs)
        →
        Elixir.Kernel.then(
          Elixir.Map.replace!(Elixir.Ecto.Changeset.change(cs), :repo, Elixir.MyApp.Repo),
          fn changeset ->
            Elixir.Ecto.Changeset.apply_action!(
              changeset,
              Elixir.Kernel.if(Elixir.Ecto.get_meta(changeset.data, :state) == :loaded,
                do: :update,
                else: :insert
              )
            )
          end
        )

    The argument is normalised through `Ecto.Changeset.change/1`, which is **total over both
    shapes** Repo writes accept — a bare struct (`insert`/`delete`) *and* a changeset
    (`update`/`insert_or_update`) — so the rewrite never breaks on the struct case.
    `apply_action` preserves the `{:ok, struct}` / `{:error, changeset}` shape (and
    `apply_action!` raises `Ecto.InvalidChangesetError`, exactly like `insert!`), so the mutant
    diverges from the real write on **only** the success path: it skips persistence and the
    DB-enforced constraints (`unique_constraint`, `foreign_key_constraint`, …) that fire only on
    the real call. So it survives unless a test drives a *successful* write and asserts a
    persistence consequence (a row present, `id`/timestamps assigned, a constraint violated) —
    a precise, well-defined kill condition. Excludes `insert_all`/`update_all` (bulk, no
    changeset).

    **Error-path parity is what makes that kill condition precise**, so the rewrite reproduces
    the two pieces of metadata a real write sets on the returned changeset
    (`Ecto.Repo.Schema`'s `put_repo_and_action/4`), which `apply_action/2` on its own does not:

      * the **Repo** — hence the `Map.replace!(…, :repo, …)` stage, carrying the configured
        `repo:` the call resolved to (with several configured, the one this write is on). (A
        plain call, not `%{… | repo: …}`, so the one stage composes into the nested and piped
        forms alike.)
      * the **action** — fixed per write for `insert`/`update`/`delete`, but *chosen at runtime*
        for `insert_or_update`, which Ecto routes to insert or update on the changeset data's
        `__meta__` state. The mutant reads that same state, so an invalid **loaded** changeset
        still comes back `action: :update` (and `insert_or_update!` still raises
        `Ecto.InvalidChangesetError` with the message "could not perform **update**"). Hard-coding
        `:insert` there made the mutant differ from the baseline on the *invalid* path, killing
        it with tests that never exercise persistence at all — see NOTES "Persistence: the
        rewrite restates the write's Repo and action".

    With those restored, an invalid changeset yields the same `{:error, changeset}` in mutant
    and baseline. Two residual divergences are deliberate: `changeset.repo_opts` stays `[]` (the
    real value carries the write's options *and*, when the Repo enables `:stacktrace`, a live
    process stacktrace — unreproducible, and varying run to run), and Ecto's own argument
    guards are not restated (`insert_or_update` raises `ArgumentError` for a bare struct or a
    changeset in a state other than `:built`/`:loaded`, where the mutant returns a result).
    Both are programmer-error paths no persistence test asserts on.

  * **`:on_conflict`** — swap an explicit `on_conflict:` atom on an `insert`/`insert!`/`insert_all`
    (the writes that take the option) to change the conflict-handling behaviour:
    `:nothing`→`:raise` (silent-skip → crash — a survivor means no test exercises the upsert's
    conflict path), `:raise`→`:nothing` (crash → silent-skip — kill with a test asserting a
    duplicate fails), and `:replace_all`→`:nothing` (overwrite-row → keep-old-row — kill with a
    test that asserts the conflicting row was actually overwritten). Every *target* (`:raise`,
    `:nothing`) is valid with no `conflict_target` on all dialects, so each swap is crash-free.
    A swap **to `:raise` also drops any written `conflict_target:` pair** — Ecto forbids the
    combination (`ArgumentError`, ":conflict_target option is forbidden when :on_conflict is
    :raise", raised in the planner before any SQL), so keeping the pair would turn the mutant
    into an unconditional error (raising on *every* insert, conflict or not — a
    trivially-killed non-mutant) instead of the intended crash-on-conflict. A swap to
    `:nothing` keeps the pair: `:nothing` accepts a target, which restricts the conflicts handled —
    `ON CONFLICT (email) DO NOTHING` skips only conflicts on the named constraint while a
    conflict on any other unique constraint raises in mutant and baseline alike, so preserving
    the specified conflict target preserves the semantics. `:replace_all` is a swap **source** only,
    never a target — the reverse needs a `conflict_target` on Postgres, so a target-less swap
    would be a runtime crash (a trivially-killed non-mutant). Non-atom `on_conflict:` values (a
    `{:replace, …}` tuple, a keyword-list update, a query) are left untouched.

The `:persistence` rewrite is one **stage chain** over the value the write would have
persisted — `change()`, then setting `:repo`, then the `apply_action` call — from a single
table (`stages/3`), nested as calls around the written argument, *not* as a pipe on it: `|>`
binds tighter than most operators, so piping an argument that is itself an operator expression
would re-associate it, where a nested call's parentheses cannot. A piped write
(`cs |> Repo.insert()`) reaches the plugin as the direct call `Repo.insert(cs)`, so the same
chain serves both spellings; core keeps the pipe the user wrote in the report. The
`:on_conflict` swap rebuilds the call in its written form via `Mutare.Calls`.

# `mutations`

```elixir
@spec mutations(Macro.t(), Mutare.Ecto.Context.t()) :: [Mutare.Ecto.Tag.t()]
```

RepoWrite mutations for `node` as `:persistence`/`:on_conflict` tags, or `[]`.

---

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