fix(intent): re-parenting repairs both sides, from every write path - #6845
Merged
Conversation
Re-parenting a record left the VACATED group's totals permanently stale, from two independent gaps. The "-rekeyed" event exists precisely to carry the row whose grouping moved, but it was published only from the full-row update(). A grouping FK moved by a workflow setter, by a resolves: lookup or by a task-form writer goes through the targeted primitive updateProperties, which publishes no "-updated" either - so NEITHER side of that move was ever recomputed. And roll-ups never consumed "-rekeyed" at all: their handlers recompute the parent named by the incoming payload, and the create/update/delete events all name the parent the child belongs to now, so the parent it was moved away from went on counting it forever. Three parts: - The entity's .model now carries groupingKeys - the union of every aggregate key over it AND every roll-up `via` FK whose child it is (it was aggregateKeys, aggregates-only, which is why a re-parented roll-up child was invisible), plus groupingSourcePk. - The DAO compares those columns on BOTH write paths. update() keeps publishing the previous row (the group it moved into is recomputed off "-updated" like any other change); updateProperties snapshots the row as JSON before applying the incoming values and, when a tracked column actually moved, publishes the previous row AND the written one - on that path neither side has an event otherwise. Nothing but the generated aggregate / roll-up handlers subscribes to "-rekeyed", so a targeted write still re-fires no reaction. - Roll-ups bind it, as a fourth RollupOnRekey handler. It is the same recompute keyed on the payload's FK, so one class repairs whichever side the payload names. Closes #6819
delchev
force-pushed
the
fix/rekey-targeted-writes
branch
from
August 19, 2026 13:19
24eef42 to
0c72939
Compare
This was referenced Aug 20, 2026
delchev
added a commit
that referenced
this pull request
Aug 20, 2026
The merged expectation: a count roll-up now emits four handlers - the create/update/delete trio this branch adds, plus the RollupOnRekey #6845 merged meanwhile. The stale comment claiming the vacated parent was 'tracked separately' now points at that handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
delchev
added a commit
that referenced
this pull request
Aug 20, 2026
…6837) * fix(intent): a count roll-up recomputes when a child changes parents A `rollups:` entry with the default `op: count` got a create and a delete handler but no `-updated` one - the generator assumed a count can only move when a child is created or destroyed. A child changes parents by an ordinary EDIT of its parent relation, so re-parenting recomputed neither count: the parent that received the child never counted it. Emit the update handler for every op. The recompute is the same query in all three cases and it reads the child rows back from the store, so it is idempotent and never op-specific - an edit that touched nothing the roll-up reads finds the value unchanged and writes nothing. The parent a child moved AWAY from is still stale until roll-ups also consume the `-rekeyed` event, which is filed separately; this alone makes the new parent correct. Covered by a `GlueRollupCountTest` unit test on the emitted descriptors and by IntentEngineIT, which now asserts the generated `LoanMemberRollupOnUpdate` binds the child's `-updated` topic and recomputes exactly like the create handler. Closes #6820 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(intent): the count roll-up trio composes with the rekey handler The merged expectation: a count roll-up now emits four handlers - the create/update/delete trio this branch adds, plus the RollupOnRekey #6845 merged meanwhile. The stale comment claiming the vacated parent was 'tracked separately' now points at that handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
delchev
added a commit
that referenced
this pull request
Aug 20, 2026
…h their writes (#6869) * fix(templates): the rekey pair and the transition announce commit with their writes (#6867) #6854 established the contract - announce an entity change by handing the topic to the WRITE, never by publishing beside it - and converted the generated repository's main paths. Two write+publish pairs survived it, each a live instance of #6816's failure class (row committed, event lost for good when the broker is briefly down, downstream reactions silently never run): - The DAO's targeted "-rekeyed" pair (#6845): one mutation had to record TWO bodies - the row as it stood, naming the group it left, and the row as written, naming the one it joined - and the targeted write could carry only one topic. The store and JavaRepository gain the event-carrying overloads (save and updateProperties with additionalEvents, mirroring update's trio), recorded only when the row actually existed to be written; the template hands both notices to the base write. A model without grouping keys regenerates byte-identically. - The transition controller (Transition.java.template): updateProperty + post-commit re-read + bare publish becomes one targeted write carrying the "-transitioned" topic, so the flip and its announcement commit together and the event payload is the row exactly as the statement left it - never a re-read a concurrent write could have moved on. The reload stays only for the HTTP response and the notify block. Deliberately NOT converted here, with the reasoning on #6867: the five deferred/ordered announces (SetField, Writer, Numbering, StepEvent via Process.executeAfterCommit - deferred so consumers observe the whole synchronous Flowable chain's writes, which write-attachment would regress - and Generate's completion announce, ordered after the target and its items across several transactions). Those need a durable-enqueue primitive, a follow-up of its own. Resolve.java.template follows once #6841, which rewrites that exact region, lands. JavaEventOutboxIT gains the retarget phase - one targeted mutation, two notices, both delivered and both cleared - and IntentEmissionCoverageIT pins the new contract, including that no bare Producer.sendToTopic remains in a generated repository or transition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(templates): the resolve routing announce commits with its write (#6867) With #6841 merged, the lookup's routing status was a targeted updateProperty followed by a post-commit re-read and a bare publish of "-transitioned" - the same write-beside-publish pair the rest of this branch removes. The topic now rides the routing write into the outbox: the flip and its announcement commit together, the payload is the row exactly as the statement left it, and a rejected move (the ValidationException the routing write catches) records no event at all - a status the record could not take is not a transition. The emission oracles pin the new contract: IntentEngineIT's two-writes ordering assertion anchors on the topic-carrying updateProperties, and IntentEmissionCoverageIT's resolve block asserts the topic on the write and refuses any bare Producer.sendToTopic in the generated lookup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6819.
Re-parenting a record left the vacated group's totals permanently stale, from two independent gaps.
1.
-rekeyedwas unreachable from the targeted writes. The event exists precisely to carry the row whose grouping moved, but it was published only from the full-rowupdate(). A grouping FK moved by a workflow setter, by aresolves:lookup or by a task-form writer goes throughupdateProperties— which publishes no-updatedeither, so neither side of that move was ever recomputed. The move a process makes, on records nobody re-opens by hand, was the one that silently skipped the repair.2. Roll-ups never consumed
-rekeyedat all. A roll-up handler recomputes the parent named by the incoming payload, and the create/update/delete events all name the parent the child belongs to now — so the parent it was moved away from went on counting it forever. Only aggregates subscribed to-rekeyed, andaggregateKeyswas populated exclusively frommodel.getAggregates(), never from a roll-up'sviaFK.The change
groupingKeys(wasaggregateKeys) on the entity's.model— now the union of every aggregate key over the entity and every roll-upviaFK whose child it is, plusgroupingSourcePk. The old name was aggregates-only, which is exactly why a re-parented roll-up child was invisible.update()keeps publishing the previous row (the group it moved into is recomputed off-updatedlike any other change).updatePropertiessnapshots the row as JSON before the incoming values are applied and, when a tracked column actually moved, publishes the previous row and the written one — on that path neither side has an event otherwise. Nothing but the generated aggregate / roll-up handlers subscribes to-rekeyed, so a targeted write still re-fires no reaction; that is what lets it signal them without re-publishing-updated.RollupOnRekeyhandler. It is the same idempotent recompute keyed on the payload's FK, so one class repairs whichever side it is fed.The publish stays gated on a tracked column having actually changed, so an ordinary edit costs nothing extra and the transitive cascade still terminates at rest.
Tests
GlueRollupRekeyTest(new) — every roll-up binds-rekeyed, with the payload-keyed criteria, additional to the create/delete handlers.EdmIntentGeneratorTest.rollupChildCarriesItsParentFkAsAGroupingKey(new) — a roll-up child carries its parent FK; the parent carries nothing.GlueRollupLatestTest,EdmIntentGeneratorTest— updated for the fourth handler and the renamed model keys.IntentEmissionCoverageIT— the targeted path's before/after comparison and its two publishes, plus the roll-up child's tracking and theClaimLineClaimRollupOnRekeyhandler. These assertions fail onmaster.Run green locally: engine-intent unit tests, the full
-P unit-testsbuild,IntentEmissionCoverageIT,IntentEngineIT,JavaTemplateIT,ModelGenerationIT,formatter:validate.Overlap with #6837
#6837 (a count roll-up recomputes on child update) touches the same
buildRollupstail and the sameengine-intent/CLAUDE.mdbullet, and itsGlueRollupCountTestcounts the emitted handlers — whichever merges second needs a small rebase (the count becomes 4). The two are complementary: #6837 fixes the parent a child moved to on the full-row path, this one fixes the parent it moved away from, and both sides on the targeted path.Docs
engine-intentguide gets a rekey bullet, and the assistant guide a line saying re-parenting is handled.