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

A mutation-testing plugin for Ecto — a `Mutare.Mutator` that mutates the Ecto
surface (Repo calls, changeset pipelines, and the query DSL) while respecting
SQL semantics.

Enable it with a module entry, adding `repo:` when Repo-call mutations are needed:

    # .mutare.exs
    [mutators: [:all, {Mutare.Ecto, repo: MyApp.Repo}]]

Listing it both registers the plugin's macro routing (via `c:Mutare.CallRouting.call_routes/0`,
discovered automatically) and enables its mutations. Query, changeset, and schema handling do
not require `repo:`; that option only identifies the module(s) matched by the Repo-call families.

This module delegates to sub-mutators selected by node type
(`Mutare.Ecto.Dispatcher`): `Mutare.Ecto.RepoAggregate` and `Mutare.Ecto.RepoWrite` (Repo calls), `Mutare.Ecto.Changeset`
and `Mutare.Ecto.ValidationBoundary` (changeset pipelines), `Mutare.Ecto.Query` (whole-`from` mutations), `Mutare.Ecto.Clause` and
`Mutare.Ecto.QueryTerminal` (standalone/pipe clause macros and `first`/`last`),
`Mutare.Ecto.BindingReorder` (positional binding transpositions on any binding-list macro),
`Mutare.Ecto.ClauseDrop` (removing a standalone/pipe clause stage — `q |> where(…)` → `q`),
`Mutare.Ecto.Dynamic` (in-fragment mutations of a free-standing `dynamic/1,2`, rewritten whole-call
in place), and `Mutare.Ecto.Host` (localized in-fragment `where`/`having` mutations, via the SQL
catalog in `Mutare.Ecto.Fragment`).

## Configuration

Each entry takes:

  * `repo:` — the Repo module recognized by the `Repo.aggregate` and write-call families: one
    module, or a list (`repo: [MyApp.Repo, MyApp.ReplicaRepo]`) when the app has several.
    Optional when only query/changeset mutations are wanted; without it, Repo-call families are
    inert.

  * `families:` — narrow the SQL catalog. Accepts `:default` (the unset default — every family
    **except** the opt-in `:string_literal`/`:atom_literal`/`:boolean_literal` arms, which are off
    for safety), `:all` (every family, including those arms), an explicit list, or a
    base-minus-exclusions `{:default | :all, except: [families]}`. Every family is independently
    toggleable; see `families/0` for the full set and `default_families/0` for the default subset.

        # turn the string/atom/boolean literal arms back on
        {Mutare.Ecto, repo: R, families: :all}

        # the easy way to drop a default-on arm
        {Mutare.Ecto, repo: R, families: {:default, except: [:integer_literal]}}

    Even when the string/atom arms are enabled, a literal at a known DSL form's **structural**
    argument (the `fragment` template, an interval unit, a cast type) is never mutated — see
    `Mutare.Ecto.Fragment`. Combined with `:as` (which renames the family in the report),
    `families:` both narrows a run and lets a sub-family be **reported under its own name**:

        {Mutare.Ecto, repo: R, families: [:comparison], as: :ecto_comparison}

    For a **per-site** subset rather than a run-wide one, each family is also a `# mutare:ignore`
    variant: `# mutare:ignore[ecto:comparison]` suppresses just the comparison mutants on that
    line, the rest still running (see `variants/0`).

  * `condition_delivery:` — `:auto` (default) weaves conditions where supported and rebuilds
    the rest statically, among them any condition holding an author macro, whose expansion
    may bring syntax the dynamic builder rejects (`Mutare.Ecto.StaticCondition`). `:static`
    forces whole-call rebuilds of every condition. It does not change free-standing dynamics
    or bound delivery.

  * `dialects:` — gate dialect-specific mutations (default `[]`, the portable core). `:postgres`
    enables `like`↔`ilike`; `:postgres`/`:mysql` enable the `LEFT`↔`RIGHT` join swap (SQLite
    lacks `RIGHT JOIN`).

  * `as:` — rename the recorded family (a core convention; `:as` is consumed by Mutare and never
    reaches the plugin). List the plugin twice with different `families:`/`as:` to split the
    catalog into separately-named report families, or with different `repo:`/`as:` to report
    each repo's mutants under its own name (a single entry with `repo: [A, B]` covers both under
    one name).

**Structural positions held back from core's families.** Listing the plugin also *suppresses* a
little noise elsewhere: `call_routes/0` routes the action atom of `Ecto.Changeset.apply_action/2`
and `apply_action!/2` `:raw`, so no core family ever mutates it — the atom is metadata (it only
sets an error changeset's `action`), never behaviour. Likewise a changeset stage's **written
field atom** (`validate_length(cs, :name, …)` — a column name, which core would swap for an
unknown field Ecto raises on), the **option keys** of `validate_number` (`greater_than:`,
`message:`, … — Ecto rejects unsupported options; the plugin's `:validation_boundary` swaps
strict and non-strict keys, and core still mutates the bound *values*), and a written
**`count:` mode** of `validate_length` (`:mutare` is no mode). Core still mutates
`validate_length`'s keys — Ecto ignores an unknown key, so the swap is a live mutant, not a crash — see
`Mutare.Ecto.Changeset.Routing`. This is **not** gated by `families:`, which selects what the
plugin *produces*; the routes apply whenever the plugin is listed.

**Equivalence-sensitive families.** Some mutants include a **report note** —
`… SURVIVED  — kill may require …` — describing fixture data needed to distinguish their results.
Each note names the **specific** data a kill needs (a boundary row, a non-NULL row, NULL rows in
a column, an orphan row, …); `Mutare.Ecto.Equivalence` defines the set and each family's reason,
and the note is attached by `finalize/2` (`c:Mutare.Mutator.finalize/2`) on every delivery path.
`equivalence_sensitive_families/0` returns that set; with the `:as` convention you can
additionally *group* them under their own report name:

    {Mutare.Ecto, repo: R, families: Mutare.Ecto.equivalence_sensitive_families(), as: :ecto_boundary_null}

Without a `families:`/`as:` split every mutation is recorded under `:ecto`.

## Macro routing

`call_routes/0` registers the compile-time DSL so Mutare core never splices a runtime selector
into a query expression (which would fail the single build). `schema`/`embedded_schema` are
routed `:raw` — a mutated field name or type is a broken schema, not a mutant. Every query-building
macro (`from`, `where`/`having`, `join`, `order_by`, `limit`, …) routes through the per-argument
classifier `Mutare.Ecto.Host.Routing`, whose `:hosted` positions the selector host
`Mutare.Ecto.Host` weaves behind Ecto's `^`/`dynamic` injection — a `where`/`having` condition,
or a literal `limit`/`offset` bound (pin-only — `Mutare.Ecto.Bound`); every routed node is still
offered whole to `mutate/2` for the in-place families. A free-standing `dynamic/1,2` registers
`:raw` (its arguments are left as written, the call itself still offered) and is mutated
whole-call by `Mutare.Ecto.Dynamic`. The changeset stages (`Mutare.Ecto.Changeset.stages/0`)
route through `Mutare.Ecto.Changeset.Routing`, which only pins the structural positions above.

Resolution of these macros relies on Mutare's `use`-expansion (so the
`use Ecto.Schema`-injected `import Ecto.Schema`, and a `use MyAppWeb, :live_view`-bundled
`import Ecto.Query`, are visible) — hence the deployment requirement `required_modules/0`
declares: Ecto must be loadable in the Mutare process.

# `default_families`

```elixir
@spec default_families() :: [atom()]
```

The families enabled by default (the `families: :default` / unset set) — every family except the
opt-in `:string_literal`/`:atom_literal`/`:boolean_literal` arms, which are off for safety until
explicitly enabled.

# `equivalence_sensitive_families`

```elixir
@spec equivalence_sensitive_families() :: [atom()]
```

The families whose survivors may require specific fixture data to kill
(see "Equivalence-sensitive families" above; `Mutare.Ecto.Equivalence` defines the per-family reasons).

# `families`

```elixir
@spec families() :: [atom()]
```

Every SQL family the plugin can emit — the `families: :all` set, for a `families:` subset.

# `required_modules`

The declared deployment requirement (`c:Mutare.Mutator.required_modules/0`): the Ecto surface
the plugin routes (`Ecto.Schema`, `Ecto.Query`) must be loadable in the Mutare process. Core
checks the declaration **once** at startup — at `Mutare.Mutator.Spec` resolution (before
`init/1`) — so an external-source run (where Ecto and the target app's modules are not on the
code path) aborts with a `Mutare.EnvironmentError` instead of silently producing incomplete or
invalid routing.

# `variants`

```elixir
@spec variants() :: [atom() | String.t()]
```

The `# mutare:ignore` variant vocabulary: every SQL **family** the plugin can emit (`families/0`),
plus the finer **operator/kind** labels its swap and value families tag — a comparison's operator
(`<`), a literal's kind (`zero`), an aggregate (`sum`), a sort direction (`asc`), a NULLs placement
(`nulls_first`), a join kind (`left`), a set operation (`intersect`). Assembled from each producer's
own labels so the vocabulary can't drift from what is emitted.

Every recorded mutant carries its family label and, for a swap/value family, the finer label too —
so a qualified directive suppresses **either** the whole family or one operator at a site, the rest
still running. The per-site analogue of the run-wide `families:` filter, only finer. With the
default config every mutant is recorded under `:ecto`, so a directive reads
`# mutare:ignore[ecto:comparison]` or `# mutare:ignore[ecto:<]`; an `:as`-renamed run reads
`# mutare:ignore[<as>:<label>]` (the vocabulary is unchanged). A **bare** `# mutare:ignore[ecto]`
still suppresses every mutant at the site.

    from(u in User, where: u.age > 18 and u.height < 90) # mutare:ignore[ecto:<]
    #                                            ^ only the `<` swap is suppressed; the `>` swap,
    #                                              and the 18/90 literal swaps, all keep running

---

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