intent: the flat per-item post is one transaction - a refused row leaves nothing (#7179) - #7245
Merged
Conversation
…eaves nothing (#7179) Posts.java.template's per-item mode (posts: with forEach:) had the same multi-write shape PR #7167 fixed in the document-shaped posting and no unit of work: one save per derived row, one transaction each. A row the target repository refused - a validation, a constraint, a required column the derived row leaves null - left the rows before it durable. That is worse here than in the posting, because this mode's idempotency guard is coarser: it asks whether ANY row back-references the source, so the partial set read as a finished post and every redelivery afterwards was a no-op. The missing rows were never written by anything. The half-post was PERMANENT. The rows one source event derives are now mapped in memory first and written inside one UnitOfWork.run(...): all of them commit or none does, so a failed tick leaves nothing and the redelivery writes the whole set. That also makes the existing guard exact rather than needing to be replaced - with atomicity the back-reference is present for a whole post and absent for no post, and there is no partial set left for it to misread. The single-row mode keeps its one save: one repository call is already one transaction. Verification is a new IntentPostsAtomicityIT, the runtime half this mode never had - it publishes the app (which compiles the generated handler) and drives the sequence the issue describes: a goods issue whose second line carries no quantity derives a movement the target refuses, the ledger must hold NOTHING, the line is then repaired and the event redelivered, and the ledger must hold the WHOLE post. Against the unfixed template that run ends with exactly one durable row and stays there. IntentEngineIT covers the emission by POSITION (derivation before the unit, the sole save site inside it, no unit in the single-row mode) - the flat mode had no rendering coverage at all until now. Fixes #7179 Co-Authored-By: Claude Opus 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.
The defect
Posts.java.template's per-item mode (posts:withforEach:) had the same multi-write shape PR #7167 fixed in the document-shaped posting — and no unit of work: onesaveper derived row, one transaction each. A row the target repository refuses (a validation, a constraint, a required column the derived row leaves null) left the rows before it durable.It is worse here than in the posting, because this mode's idempotency guard is coarser. It asks whether any row back-references the source, so the partial set read as a finished post and every redelivery afterwards was a no-op: the missing rows were never written by anything. The half-post was permanent — named in #7167's own body as out of scope, with no follow-up filed until #7179.
The fix
The rows one source event derives are mapped in memory first and written inside one
UnitOfWork.run(...): all of them commit or none does, so a failed tick leaves nothing and the redelivery writes the whole set.That also makes the existing guard exact rather than something to replace. The issue offered "and/or the guard counts rows against the derived set"; with atomicity the counting has nothing to add — the back-reference is present for a whole post and absent for no post, and there is no partial set left to misread. The reasoning is recorded next to the guard so it does not get widened later into a delete-and-rewrite, which is amendment semantics this construct does not have (
posts:is fire-once byidempotentBy:, unlikepostings:since #7071).The single-row mode keeps its one
save— one repository call is already one transaction — with a line saying so.Verification
IntentPostsAtomicityIT(new) — the runtime half this mode never had. It publishes the app (which compiles the generated handler, so the lambda's captures are proven legal) and drives the sequence the issue describes: a goods issue whose second line carries no quantity derives a movement the target refuses → the ledger must hold nothing → the line is repaired and the event redelivered (reopen, post again) → the ledger must hold the whole post, exactly once, with no duplicate of the row that had succeeded.size() Expected: <0> Actual: <1>— one durable row, and the redelivery never completes it. The defect verbatim.IntentEngineIT— 75/75 green with the new emission test, which asserts by position that the derivation precedes the unit, that the sole save site is inside it, and that the single-row handler opens no unit. The flatposts:mode had no rendering coverage in any IT before this (onlyGluePostsTestat the glue level), which is part of why the shape shipped unnoticed.mvn formatter:validateclean.Not touched here, deliberately
IntentEmissionCoverageITwould have been the other natural home for an emission assertion, but it is red on master —a master being deleted must suspend the per-line totals write-backfails on both H2 and PostgreSQL in master's own CI for86b9e1e4(filed as #7240), before any added assertion in that method would run. The focused IT above gives both the compile proof and a stronger behavioural one, so nothing was added to a fixture that cannot currently go green.Separately: a quoted string in a
posts:set:renders as a bare identifier (row.Note = issued;) — YAML unquotes the value beforeGlueIntentGenerator.postSetExpr's already-quoted branch can see it, so the generated handler does not compile. Found while building the fixture, filed as #7246 rather than left as a note in a PR body — which is exactly how #7179 came to exist.Fixes #7179
🤖 Generated with Claude Code