The condition the host cannot weave, and its whole-call delivery.
The host delivers a hosted condition's mutants by weaving: the condition becomes a
^-pinned dynamic/2 that re-declares the condition's bindings (Mutare.Ecto.Host). Some
conditions cannot take that form, though each is valid as written.
A subquery in a having. Weaving changes how Ecto builds the clause — from the
compile-time filter builder to the runtime dynamic path — and the two do not accept the same
expressions:
from p in "posts",
group_by: p.user_id,
having: count(p.id) > subquery(from t in "thresholds", select: max(t.value))Written statically this is valid; as having: ^dynamic([p], …) Ecto raises "subqueries are
not allowed in having expressions". It raises when the query is built, not when the
module compiles, and it raises on the woven original branch as readily as on a mutant — so
weaving it would break the unmutated code path of every test that reaches the function.
A binding declaration the plugin cannot re-declare — a computed index ([{p, index}]) or
an interpolated name that calls a function ([{^name(), p}]), each of which a re-declaration
would evaluate a second time (Mutare.Ecto.Host.Bindings). The woven dynamic/2 has no
faithful binding list to carry, and an empty one would leave the condition's variables
unbound. A condition that is itself a ^ pin is the exception: it is woven pin-only, with no
dynamic/2 and so no bindings (Mutare.Ecto.Host.Target's root-pin rule), whatever the
declaration.
Repeated explicit on: predicates. Ecto combines these before escaping the join's
condition, so each written predicate must be rebuilt statically. A sole explicit predicate
on an association join can weave: Ecto attaches association constraints later, in planning.
A filter that is, or may become, the literal true. Ecto's runtime filter path discards a
where/having condition that expands to true, whatever its operator
(Ecto.Query.Builder.Filter.apply/3); the static build keeps it. Alone that loses nothing, but
where: false, or_where: true becomes where: false, and where: true, or_where: p.x > 1
becomes where: p.x > 1. A woven branch that is the literal true is lost the same way,
whether it is the original (baseline rows change) or a mutant (or_where: coalesce(true, x)
→ true delivers another query than it reports). So a filter condition is rebuilt when it,
or a mutant of the plugin's own catalog, is the literal true, read through the filter/1
that Ecto's escape erases (filter(true)). A pin's mutants cannot be:
they keep the pin. A join's on: keeps a true on both paths.
A condition typed by its clause. The static builder escapes a condition as :boolean, the
dynamic one as :any, and the type reaches whatever the condition passes it on to:
coalesce's operands, arithmetic operands, a unary minus. A pin there is cast by that type
(where: coalesce(^"true", false) binds true statically and the string "true" in a
dynamic, which SQLite reads as false), and a non-boolean literal there is tagged with it. So a
condition is rebuilt when a pin or a non-boolean literal takes the condition's own type, in the
condition or in a catalog mutant of it. A root pin is exempt: it is woven pin-only, never into
a dynamic/2, and Ecto builds it at runtime natively too.
A fragment splice. fragment("? IN (?)", p.id, splice(^values)) writes one placeholder per
list element, each naming the one parameter the list is (Ecto.Query.Builder.splice!/2). The
static planner binds the list's elements once; the dynamic path appends the parameter at each
placeholder, so a woven values = [1, 2] binds four values to two placeholders and the
baseline fails. So a condition that splices is rebuilt.
A call Ecto can only be expanding (Mutare.Ecto.Walk.opaque_call?/2): an author macro,
registered or not, or a module attribute. Its expansion is escaped with the condition, and the
source does not show it: member_sql(p.id, ^values) may expand to the splice above, and
over_threshold(p) to a subquery the having rejects in a dynamic. So a condition holding one
is rebuilt, whatever the expansion turns out to be. A pin's interior and a subquery wrapper's
argument (exists(from …)) are not read: Ecto evaluates both as Elixir, apart from the
condition's escape, so a call there expands the same way on either path. An ignore directive
leaves delivery intact; it is not a baseline-safety switch.
The rule
delivery/5 applies the configured policy, rebuilds a filter that is or may become the literal
true, a condition typed by its clause, one that splices and one holding a call Ecto can
only be expanding (above), then delivery/4 decides from the receiving
clause, the expression, its predicate kind, and the declaration it is read under. A condition is rebuilt when the
expression carries a subquery (Mutare.Ecto.Subquery.present?/1) that the clause rejects in a
dynamic (Mutare.Ecto.Surface.dynamic_subqueries?/1 — everything but where/or_where), or when
it is not a :root_pin and its declaration does not read (t:Mutare.Ecto.Host.Bindings.result/0
is :error); every other condition is woven. Both the host and mutations/2 enumerate the
same conditions (Mutare.Ecto.Host.Condition) and ask delivery/5 of each, so each condition is
delivered once: woven, or rebuilt here.
Only a predicate is either's, and Mutare.Ecto.Host.Condition locates nothing else
(Mutare.Ecto.Host.Condition.shape/1). A keyword filter carries a subquery as readily
(having: [score: subquery(…)] — Ecto's filter builder accumulates a pair value's), and sits
under an unread declaration as readily, but the weave would carry nothing for it — its pairs
are routed to core one by one — so neither is this module's rebuild, which would otherwise
read the list as a predicate and rename its column keys.
Delivery
Hosting is a delivery optimization, not a semantic category. The mutants are the ones the
weave would have carried — the plugin's own catalog (Mutare.Ecto.Host.Catalog.own_catalog/2)
and the ^-pin interiors sub-contracted to core (Mutare.Ecto.Island) — each delivered as
the whole call rebuilt around one mutated condition, through Mutare's ordinary in-place
selector (the delivery Mutare.Ecto.Dynamic and Mutare.Ecto.Query use). Every selector
branch is then a statically built clause under the written declaration, which Ecto accepts. A
catalog mutant still reports at the expression it changed (Mutare.Ecto.Walk anchors it); a
pin-interior mutant reports at the whole call, as it does from Mutare.Ecto.Dynamic. The
families: filter and equivalence notes apply as on any mutate/2 path.
The subquery rule follows Ecto's current behaviour without depending on it: were a later Ecto to accept the dynamic form, this delivery would remain valid, only more verbose than the weave.
Summary
Types
How one hosted condition's mutants are delivered — see delivery/4.
The clause receiving a condition (Mutare.Ecto.Host.JoinOn.receiver/0), or, as
{:static_from, receiver}, one in a from that holds a filter Ecto's runtime path discards
(from_receiver/3).
Functions
How the host delivers condition, a predicate of kind (Mutare.Ecto.Host.Condition.shape/1)
received by clause (a condition macro's name, a from clause key, or :on) under the
declaration bindings reads as: {:woven, list}, carrying the binding list the woven
dynamic/2 re-declares (none for a :root_pin, which is woven without one), or :rebuilt, by
mutations/2 — see "The rule" in the moduledoc.
Apply the configured delivery policy before considering automatic hosting.
The receiver of the condition at index of a from's clauses. A from is built
statically only up to its first runtime clause, and a woven condition is one: every clause
after it is applied at runtime, where Ecto's filter builder discards a filter that is the
literal true (see "A filter that is, or may become, the literal true"). So a condition
followed by a filter that is, or may expand to, the literal true (or [], which Ecto
escapes to it) is rebuilt.
Every single-point mutant of each hosted condition in call that delivery/4 rebuilds, as
the whole call rebuilt around the mutated condition — a condition macro
(having(q, [p], …), direct or piped), a standalone join's on:, or a from's keyword
clauses. [] for a call with no such condition, which is nearly every call.
The half of delivery/4 read off the clause and the expression alone: whether clause
accepts condition as a ^-pinned dynamic/2 — false only for a subquery in a clause that
rejects a dynamic one.
Types
@type delivery() :: {:woven, bindings :: [Macro.t()]} | :rebuilt
How one hosted condition's mutants are delivered — see delivery/4.
@type receiver() ::
Mutare.Ecto.Host.JoinOn.receiver()
| {:static_from, Mutare.Ecto.Host.JoinOn.receiver()}
The clause receiving a condition (Mutare.Ecto.Host.JoinOn.receiver/0), or, as
{:static_from, receiver}, one in a from that holds a filter Ecto's runtime path discards
(from_receiver/3).
Functions
@spec delivery( receiver(), Macro.t(), Mutare.Ecto.Host.Condition.predicate_kind(), Mutare.Ecto.Host.Bindings.result() ) :: delivery()
How the host delivers condition, a predicate of kind (Mutare.Ecto.Host.Condition.shape/1)
received by clause (a condition macro's name, a from clause key, or :on) under the
declaration bindings reads as: {:woven, list}, carrying the binding list the woven
dynamic/2 re-declares (none for a :root_pin, which is woven without one), or :rebuilt, by
mutations/2 — see "The rule" in the moduledoc.
@spec delivery( receiver(), Macro.t(), Mutare.Ecto.Host.Condition.predicate_kind(), Mutare.Ecto.Host.Bindings.result(), Mutare.Ecto.Config.t() ) :: delivery()
Apply the configured delivery policy before considering automatic hosting.
@spec from_receiver(atom(), Mutare.Ecto.AST.KeywordList.t(), non_neg_integer()) :: receiver()
The receiver of the condition at index of a from's clauses. A from is built
statically only up to its first runtime clause, and a woven condition is one: every clause
after it is applied at runtime, where Ecto's filter builder discards a filter that is the
literal true (see "A filter that is, or may become, the literal true"). So a condition
followed by a filter that is, or may expand to, the literal true (or [], which Ecto
escapes to it) is rebuilt.
@spec mutations(Mutare.Ecto.AST.QueryCall.t(), Mutare.Ecto.Context.t()) :: [ Mutare.Ecto.SubMutator.tagged() | Mutare.Mutator.Mutation.t() ]
Every single-point mutant of each hosted condition in call that delivery/4 rebuilds, as
the whole call rebuilt around the mutated condition — a condition macro
(having(q, [p], …), direct or piped), a standalone join's on:, or a from's keyword
clauses. [] for a call with no such condition, which is nearly every call.
The half of delivery/4 read off the clause and the expression alone: whether clause
accepts condition as a ^-pinned dynamic/2 — false only for a subquery in a clause that
rejects a dynamic one.