Drop a standalone/pipe query clause — the composable counterpart of Mutare.Ecto.Query's
whole-from clause drops, analogous to Mutare.Ecto.Changeset's validator drop.
Where Query removes a where:/limit:/group_by: clause from a from(…) keyword list, this
removes the same kinds of clause written as a standalone call or pipe stage:
q |> where([u], u.active) → q "is this filter tested?"
q |> limit(10) → q "is the page size pinned?"
q |> group_by([u], u.role) → q "is this grouping tested?"A surviving mutant means no test exercises what that clause contributes — the primary motivating mutation for a query builder (NOTES "ClauseDrop: the pipe form was the common one").
Delivery is the shared stage drop (Mutare.Ecto.StageDrop): the call collapses to its query
argument, in both spellings — a pipe stage reaches the plugin as the direct call it is sugar
for, and the report keeps the pipe the user wrote (q |> where(…) → q). The call is
resolved through Mutare.Calls, so it matches the direct, aliased, and (common)
import Ecto.Query forms alike, and never a same-named user function.
Families
The recorded family mirrors Mutare.Ecto.Query, so the same semantic mutation carries the
same family regardless of the syntax used:
where/or_where/having/or_having→:filter_drop;limit/offset→:bound(the bound family also covers then→n±1bumps);- every other clause builder (
group_by,distinct,select/select_merge,join,preload,lock,with_cte,windows, the set-operation macros) →:clause_drop.
order_by/prepend_order_by are deliberately not droppable (an unordered query's row order
is SQL-unspecified); the implicit-direction flip in Mutare.Ecto.Ordering is the reliable
ordering mutant instead.
A drop weakens the query, or breaks it
Which one depends on the stages after the dropped one, and a stage is mutated alone — a
pipeline is assembled at runtime, often across functions, and mutate/2 is offered one call
at a time — so both kinds are emitted under the same family:
- Nothing later needs the stage — a filtering join, a
group_by, adistinct, apreload. The mutant is the query without it, and a kill means a test pins what the stage contributes. - A later stage needs what it provided — a join's binding (by position or by
as:name), thewindowsname anover/2uses, thelimitawith_tiesqualifies, the CTE a join reads, a schemaless source'sselect. The mutant is no query at all: it raises as the pipeline is built (with_ties, a named binding), as Ecto plans it (a positional binding, a window, the missingselect), or at the engine (the CTE's table). Any test that executes the query kills it, whatever it asserts — so the kill shows that the stage runs, not that its effect is tested. - In between: dropping a join ahead of another runs, with every later positional
binding shifted onto the freed slot —
[p, c]'scnow names the next join's table.
None of this reaches the single build. These macros assemble the query at runtime, so a broken
dependency raises only under its own mutant — the inactive branches, and every other mutant
in the file, are untouched. The from form differs exactly there: Ecto expands the whole
keyword list at compile time, so a dropped join: would leave where: c.x naming an unbound
variable in the metamutant itself. That is why Mutare.Ecto.Query drops a limit: together
with its with_ties:, and drops only the keys nothing else in the list can need (its "Clause
drop"); a from join drop would first have to prove the binding unreferenced, and none is
offered. Telling the two kinds apart here needs the rest of the pipeline in view (NOTES
"Stage drops: a dependency break is not told from a weakened query").
Summary
Functions
Stage-drop mutations for an Ecto.Query clause macro as tags, or [].
Functions
@spec mutations(Macro.t(), Mutare.Ecto.Context.t()) :: [Mutare.Ecto.Tag.t()]
Stage-drop mutations for an Ecto.Query clause macro as tags, or [].