Mutare.Ecto.Query (mutare_ecto v0.3.0)

Copy Markdown View Source

Whole-from query mutations — the ones expressible without Mutare's foreign-semantics DSL host. Each returns a whole mutated from(...) node, which Mutare's ordinary in-place selector wraps; the localized in-fragment mutations (operator swaps inside a where, via ^/dynamic) arrive with the host extensions.

  • Filter drop — remove one where/having/or_where/or_having clause from the query. "Is this filter tested?" Reuses the surviving clauses, so it always compiles, and works for both the binding form (from p in Post, where: p.active) and the bindingless form (from Post, where: [active: true]) since it operates on the clause list, not the clause value.
  • Clause drop — remove one group_by/distinct/preload/lock/select_merge/ with_ties clause, or one set operation (union:, except:, …): the from-keyword twin of Mutare.Ecto.ClauseDrop's stage drop, under the same family. "Is what this clause contributes tested?" A from expands as a whole when the metamutant compiles, so a key drops here only if the rest of the list cannot need it — it binds no variable, nothing names it, and no plan requires it. That holds the pipeline's other drops back: a join binds a variable the other clauses read, so its drop would fail the single build unless the binding were first proved unreferenced; a select: is required by a schemaless source, an update: by update_all, and a windows: is named by over/2. What remains can still depend on a dropped clause at the engine — a select that mixes an aggregate with a plain column needs its group_by on Postgres — which raises under that one mutant only (Mutare.Ecto.ClauseDrop, "A drop weakens the query, or breaks it").
  • Order flip — flip an order_by direction (:asc↔:desc, and the *_nulls_* variants). "Does any test pin the sort direction?"
  • Bound (drop) — drop a limit/offset clause. "Is the window tested at all?" The family's other half, the ±1 bump of a literal bound, is hosted pin-only instead (Mutare.Ecto.Bound). Of a repeated bound (limit: 5, limit: 10) only the last — the one Ecto keeps — drops, uncovering the previous; the overridden one never reaches the query, so its drop would be equivalent (Mutare.Ecto.AST.FromCall.effective_clause?/2).
  • JoinType — narrow a join's kind by rewriting its clause key: left_join→inner_join, and full_join→left_join/right_join (plus left_join↔right_join sideways). "Does any test exercise the orphan row this join kind keeps that a narrower kind would drop?" An outer join is written because unmatched rows must survive, so seed data built for that reason is likely to already include an orphan for which the narrower kind returns different rows — a killable mutation. The reverse (inner_join/join→left_join, *→full_join) is deliberately not offered: it widens a join the author picked precisely to exclude unmatched rows, and absent a reason to test for an orphan that shouldn't matter, the widened query usually returns identical rows — an equivalent mutant more often than a killable one. The left_join↔right_join swap and full_join→left_join are portable (every adapter supports INNER/LEFT); full_join→right_join and the RIGHT leg of left_join↔ right_join are dialect-gated (:postgres/:mysql — SQLite lacks RIGHT JOIN). The flips are the shared Mutare.Ecto.JoinType catalog's, keyed by join qualifier, which Mutare.Ecto.Clause applies to a standalone join(q, :left, …) under this same policy.
  • Combination — swap a set-operation clause's key: intersect:↔except: and intersect_all:↔except_all:. "Does any test pin which rows the combination keeps?" The shared Mutare.Ecto.Combination catalog defines the pairing, its portability, and the union exclusion.
  • Aggregate (in select/order_by) — swap an aggregate inside a select/select_merge or order_by clause value (sum↔avg, min↔max), via the shared Mutare.Ecto.Aggregate walker. "Does any test pin which aggregate the column is reduced/sorted by?" (An aggregate inside a having is delivered through the host instead — see Mutare.Ecto.Host.)
  • Scalar (in select/order_by) — mutate a value-computing form inside a select/select_merge or order_by clause value, via the shared Mutare.Ecto.Scalar catalog: the arithmetic swaps (+↔-, *↔/; "does any test pin the computed value?") and the coalesce fallback drop (coalesce(x, d) → x; "does any test exercise the NULL row the default is for?"). (The same forms inside a where/having condition are hosted instead, alongside the operator swaps.)
  • Binding reorder (source list) — when the source declares a positional binding list (from [a, b] in q, …), transpose each pair of those bindings ([a, b] → [b, a]). "Did the author bind the sources in the right order?" The list was written at the whole-from level, so its reorder is delivered here — see Mutare.Ecto.BindingReorder for the rule and its policy.

Each mutation is returned as a Mutare.Ecto.Tag: its label is the finer operator/kind a swap family names (order/join/aggregate — nil for a structural drop), and its attribution (Mutare.Mutator.Mutation.at/2/at_drop/1) names the inner clause the rewrite changed, so the site is reported there rather than at the whole from (see Mutare.Ecto.Walk on attribution). config carries dialects: for the join gate. The from is read apart and rebuilt through Mutare.Ecto.AST.FromCall, which preserves the written form and handles emptied clause lists. A scalar from/1 (from(Post), no clauses) yields nothing, while a source binding list (from([a, b] in query)) can still reorder.

Summary

Types

One whole-from producer, named in Mutare.Ecto.Surface's vocabulary: a from_drop family walked over the clause list (:filter_drop, :bound, :clause_drop), a from capability walked over the clauses carrying it (the :ordering/:aggregate/:scalar value swaps, the :join_type/:combination key swaps), or the source-level :binding_reorder.

Functions

Whole-from mutations for a from(...) node as self-tagging Mutare.Ecto.Tags (the label the finer operator/kind for a swap family, nil for a structural drop; the attribution the inner clause the site is reported at), or [].

The whole-from mutations for from under an already-resolved %Config{} — the body mutations/2 delegates to — restricted to producers (default: every producer, in mutations/2's order) over the clauses whose key satisfies clause? (default: every clause; :binding_reorder rewrites the source, not a clause, so the predicate never reaches it).

Types

producer()

@type producer() ::
  :filter_drop
  | :bound
  | :clause_drop
  | :ordering
  | :join_type
  | :combination
  | :aggregate
  | :scalar
  | :binding_reorder

One whole-from producer, named in Mutare.Ecto.Surface's vocabulary: a from_drop family walked over the clause list (:filter_drop, :bound, :clause_drop), a from capability walked over the clauses carrying it (the :ordering/:aggregate/:scalar value swaps, the :join_type/:combination key swaps), or the source-level :binding_reorder.

Functions

mutations(call, context)

@spec mutations(Mutare.Ecto.AST.QueryCall.t(), Mutare.Ecto.Context.t()) :: [
  Mutare.Ecto.SubMutator.tagged()
]

Whole-from mutations for a from(...) node as self-tagging Mutare.Ecto.Tags (the label the finer operator/kind for a swap family, nil for a structural drop; the attribution the inner clause the site is reported at), or [].

mutations_for(from, config, producers \\ [:filter_drop, :bound, :clause_drop, :ordering, :join_type, :combination, :aggregate, :scalar, :binding_reorder], clause? \\ fn _key -> true end)

@spec mutations_for(
  Mutare.Ecto.AST.FromCall.t(),
  Mutare.Ecto.Config.t(),
  [producer()],
  (atom() ->
     boolean())
) ::
  [Mutare.Ecto.SubMutator.tagged()]

The whole-from mutations for from under an already-resolved %Config{} — the body mutations/2 delegates to — restricted to producers (default: every producer, in mutations/2's order) over the clauses whose key satisfies clause? (default: every clause; :binding_reorder rewrites the source, not a clause, so the predicate never reaches it).

Exposed so Mutare.Ecto.Subquery can mutate an inner from using just the producers whose mutations can affect the wrapper's result (with the parsed FromCall and config, not a full callback context): the row-set producers under every wrapper, and the select projection's :aggregate/:scalar swaps under a value-wrapper only — rather than running all eight and filtering out inapplicable mutations. An unknown producer is a programming error and raises.