Mutare.Ecto.Query (mutare_ecto v0.1.1)

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.

  • Clause 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.
  • 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_joininner_join, and full_joinleft_join/right_join (plus left_joinright_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 hold the orphan that makes the narrower kind disagree — a strong, killable mutation. The reverse (inner_join/joinleft_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_joinright_join swap and full_joinleft_join are portable (every adapter supports INNER/LEFT); full_joinright_join and the RIGHT leg of left_joinright_join are dialect-gated (:postgres/:mysql — SQLite lacks RIGHT JOIN).
  • 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 pairing, its portability, and the union exclusion are the shared Mutare.Ecto.Combination catalog's.
  • Aggregate (in select/order_by) — swap an aggregate inside a select/select_merge or order_by clause value (sumavg, minmax), 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 — the rule and its policy are Mutare.Ecto.BindingReorder's.

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 keeps the written form (and owns the emptied-clause-list rule). 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), 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 clause? admits (default: every clause; :binding_reorder rewrites the source, not a clause, so the predicate never reaches it).

Types

producer()

@type producer() ::
  :filter_drop
  | :bound
  | :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), 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, :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 clause? admits (default: every clause; :binding_reorder rewrites the source, not a clause, so the predicate never reaches it).

Exposed so Mutare.Ecto.Subquery can compose exactly the producers a subquery wrapper observes into an inner from (where it holds 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 what it never wanted. An unknown producer is a programming error and fails loudly.