ID |
|
|---|---|
Status |
Backlog |
Bucket |
cleanup |
Blocked by |
|
Created |
2026-05-27 |
Updated |
2026-05-27 |
Wire @condition through to mutation WHERE (emit half + new placements)
@condition on mutations is half-built today: MutationInputResolver.java admits input-field-level @condition(override: true) (R215, lines ~482-498) but the directive is a no-op at emit (no .where(…) clause is produced). Argument-level @condition on a non-@table mutation argument is rejected outright (line 446). Input-field-level @condition without override: is rejected. This item closes the emit half and lifts the two admission rejections so the directive does something useful.
Was Strand B of R188 (split out so R188 can land as a focused @value removal). The PK-default partition R188 ships is the baseline this item layers @condition predicates on top of.
Design
Placements
Three placements admitted:
| Placement | Effect | Use when |
|---|---|---|
Input field, no |
Predicate AND-s into WHERE alongside the implicit PK predicates. |
Per-row gate: "update by ID, but only if status matches." |
Input field, |
Predicate replaces the implicit PK WHERE. |
Row identified by something other than PK. R215 wiring. |
Non- |
Predicate AND-s into WHERE, shared across all rows in a bulk call. |
Per-call filter token: tenant ID, search term, soft-delete flag. |
Explicitly not admitted:
-
@conditionon the@mutationfield itself. No forcing function; reachable from the placements above. -
@conditionon the input type (INPUT_OBJECTSDL location is not added to the directive). No forcing function. -
@conditionon the@tablearg of a@mutationfield. Same rejection as today; diagnostic recommends an input field or non-@tableargument instead.
If either omitted placement turns out to be painful, file a follow-up with a concrete forcing case attached.
Composition
All non-override @condition predicates AND together. At most one input-field-level @condition(override: true) is permitted per input (two overrides on different fields each claim the row-spec is ambiguous; reject).
When an override is present, it replaces the implicit PK WHERE; non-override predicates still AND in. When no override is present, the implicit PK predicates drive WHERE and the non-override predicates AND alongside. Inner explicit @condition`s are always preserved regardless of override (the `filmsOuterOverrideTableInput regression-fence applies on the mutation side).
@condition does not move fields between R188’s SET/WHERE partition. It contributes predicates AND-ed into WHERE; it does not remove columns from SET.
Row-identity disjunction (R144 update)
The R144 check stays a disjunction: the field has a row-identity proof iff at least one of
-
PK columns are covered by input fields, OR
-
an input-field
@condition(override: true)is present, OR -
multiRow: trueis set on the@mutationdirective
holds; else the field is UnclassifiedField via mutation-input.where-identifies-row. No sealed RowIdentity taxonomy; the disjunction is checked directly in MutationInputResolver, and the emitter forks once on the three cases.
R188 ships with the override-true case present-but-no-emit; this item makes it emit, so the disjunction’s middle arm becomes load-bearing rather than just an admission.
Implementation
Admission and resolution
-
MutationInputResolver.java: -
Lift the rejection at lines ~438-440 (
if (foundTia.argCondition().isPresent()) { return …rejection… }): argument-level@conditionon a non-@tableargument of a@mutationfield is admitted.@conditionon the@tablearg stays rejected with a diagnostic recommending an input field or non-@tableargument. -
Admit input-field-level
@conditionwithoutoverride:(today onlyoverride: trueadmits, perMutationInputResolver.java:482-498). Both override and non-override forms now compose into the field record. -
ConditionResolver.resolveArg(…)is already in place; no new resolver surface required.
Model
MutationField.java: MutationUpdateTableField, MutationDeleteTableField, and `MutationDmlRecordField’s DELETE-equivalent path gain two slots:
-
Optional<ConditionFilter> overrideCondition: the composed override predicate (at most one input-field-level@condition(override: true), plus any inner non-override@condition`s AND-ed for the regression-fence). Absent when no input field carries `override: true. -
List<ConditionFilter> additionalConditions: non-override@condition`s from input fields and non-@table` arguments, in source order.
multiRow: stays on @mutation as today. MutationUpsertTableField gets the same two slots prospectively for R145’s tenant-scoping use; UPSERT’s conflict target is a separate axis.
The composition step in MutationInputResolver.resolveInput reads input-field-level @condition annotations off InputField.condition(), reads each non-@table argument’s directive via ConditionResolver.resolveArg, applies the at-most-one-override rule, and writes the two slots.
Emitter cascade
TypeFetcherGenerator.java UPDATE and DELETE arms (single-row, bulk, and payload-returning variants) fork once on slot state:
if (overrideCondition.isPresent()) → .where(override AND additionalConditions...) else if (multiRow) → no WHERE; additionalConditions, if any, AND-ed in else → .where(implicitPk AND additionalConditions...)
Threading: each ConditionFilter’s reflected `MethodRef.Param extractions resolve against the call-site Map exactly as today’s argument-level @condition does. For the bulk arm, the filter operates over the v-table reference, matching query-side bulk-condition emission. The emitter never re-derives the disjunction; the slots are pre-resolved at classify time.
Reflection-shape invariant
Each mutation-side @condition method takes (Table t, <scope-appropriate scalars>): a subset of the input’s field set, or the surrounding argument’s value. Never a per-row jOOQ record; the bulk-emit path has no v-table analogue. argMapping: is supported (rebind GraphQL field names to differently-named Java parameters), mirroring query-side leaf @condition. Enforced at reflection time in ConditionResolver, diagnostic key mutation-condition.method-shape-table-plus-scope-scalars.
This narrows the mutation-side reflection contract against the query-side one (which permits row-record parameters via different paths). The uniform narrowing across single-row and bulk mutations is a deliberate choice; mixing per-row-record and v-table shapes inside one directive would require classify-time bulk awareness the resolver doesn’t have today.
Structural rules
Applied at classify time in MutationInputResolver:
-
At most one input-field-level
override: trueper input. Two overrides on different fields each claim row-identity responsibility; reject with a diagnostic naming the conflicting fields. -
@conditionon the@tableinput arg stays rejected. Diagnostic: "argument-level@conditionon the@tableinput arg is rejected; put it on a non-@tableargument or on an input field of the@tableinput." -
Inner explicit
@condition`s are always preserved.The `filmsOuterOverrideTableInput regression-fence applies on the mutation side; the composition step AND-s inner non-override filters into the override slot when override is present.
Tests
Pipeline (each admit case asserts the slot values on the field record, not just admit/reject):
-
Input-field
@condition, nooverride:, PK in input: admit; WHERE =pk = ? AND condition(?). Single-row and bulk variants. -
Input-field
@condition(override: true), PK NOT in input: admit; WHERE =condition(?)only. Override slot populated;additionalConditionsempty. -
Input-field
@condition(override: true), PK in input: admit; WHERE =condition(?). Override suppresses implicit PK. SET still contains every non-PK column. -
Non-
@tableargument@condition: admit; WHERE =pk = ? AND argCondition(?). Lifts the line-446 rejection. -
@conditionon the@tablearg: reject with the migration-pointing diagnostic. -
Two input fields with
@condition(override: true): reject (at-most-one-override). -
Mixed layers: one input field with
override: trueplus one input field with non-override@condition: admit; override populates the override slot, non-override populatesadditionalConditions, both compose into WHERE. -
multiRow: trueplus non-override@condition: admit; broadcast WHERE additionally filtered by the condition. -
DELETE parity: non-override input-field
@conditionon a DELETE input → admit; WHERE =pk = ? AND condition(?). Input-field@condition(override: true)on a DELETE input where PK is not in input → admit; WHERE =condition(?)only. Non-@tableargument@conditionon a DELETE → admit; WHERE =pk = ? AND argCondition(?). -
Reflection-shape asymmetry: a
@conditionmethod whose signature includes a per-row jOOQ record parameter passes the query-side reflection contract but is rejected on the mutation side withmutation-condition.method-shape-table-plus-scope-scalars.
Compilation: add one sakila fixture exercising input-field-level non-override @condition: e.g. UpdateFilmStatus with a status field carrying @condition resolving to a method on a small Conditions class. Forces the compile tier to verify the reflection contract types check against real jOOQ generated classes.
Execution: add to DmlMutationsExecutionTest / DmlBulkMutationsExecutionTest:
-
Bulk UPDATE with non-override input-field
@conditionagainstfilm.release_year. End-to-end proof of "PK + gate." -
Override
@conditionon an UnboundField (R215 wiring closeout): round trip confirming the override predicate drives WHERE alone and the affected rows are exactly the predicate’s matches.
User documentation
-
Revise
docs/manual/reference/directives/condition.adoc. Add a "Use on@mutationfields" subsection naming the two placements (input field with or withoutoverride:, non-@tableargument), the composition rule (non-override predicates AND in, single override replaces PK WHERE), and a link tomutation.adocfor the UPDATE-specific story. -
Cross-link from
docs/manual/reference/directives/mutation.adoc(R188 already mentions@conditionas the escape hatch for non-PK row identity; this item makes that statement true at emit).
Roadmap entries
-
R215 (already shipped, admit-but-no-emit half): on this item Done, append a follow-on note to R215’s
changelog.mdentry recording the emit-side closeout. -
R145 (UPSERT): R145 inherits the two-placement
@conditionmechanism for layering predicates on top of the conflict-target match (tenant-scoped UPSERT). -
R222 (dimensional model pivot): the two slots (
overrideCondition,additionalConditions) on the UPDATE/DELETE field record are R222’sPredicateCarrierslice fodder; no sealed taxonomy to relocate.
Out of scope
-
@conditionon@mutationfields directly. The admitted placements cover known cases. -
@conditiononINPUT_OBJECTSDL location. Same reasoning. -
Validating the
@conditionmethod’s body (selectivity, uniqueness against catalog indexes). Author’s responsibility;multiRow: trueandoverride: trueare the structural acknowledgements. -
INSERT. INSERT has no WHERE clause; the mutation resolver rejects
@conditionon an INSERT input with "INSERT has no WHERE clause;@conditionis not applicable."