Mutare.Ecto.StaticCondition (mutare_ecto v0.4.0)

Copy Markdown View Source

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

delivery()

@type delivery() :: {:woven, bindings :: [Macro.t()]} | :rebuilt

How one hosted condition's mutants are delivered — see delivery/4.

receiver()

@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

delivery(clause, condition, kind, bindings)

@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.

delivery(clause, condition, kind, bindings, config)

@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.

from_receiver(key, clauses, index)

@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.

mutations(call, context)

@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.

weavable?(clause, condition)

@spec weavable?(atom(), Macro.t()) :: boolean()

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.