Skip to content

intent: the flat per-item post is one transaction - a refused row leaves nothing (#7179) - #7245

Merged
delchev merged 2 commits into
masterfrom
issue-7179-posts-atomic
Sep 10, 2026
Merged

intent: the flat per-item post is one transaction - a refused row leaves nothing (#7179)#7245
delchev merged 2 commits into
masterfrom
issue-7179-posts-atomic

Conversation

@delchev

@delchev delchev commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

The defect

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 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 by idempotentBy:, unlike postings: 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.
  • Negative control: against the unfixed template that same run fails with 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 flat posts: mode had no rendering coverage in any IT before this (only GluePostsTest at the glue level), which is part of why the shape shipped unnoticed.
  • mvn formatter:validate clean.

Not touched here, deliberately

IntentEmissionCoverageIT would have been the other natural home for an emission assertion, but it is red on mastera master being deleted must suspend the per-line totals write-back fails on both H2 and PostgreSQL in master's own CI for 86b9e1e4 (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 before GlueIntentGenerator.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

…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>
@delchev
delchev merged commit 7ddb576 into master Sep 10, 2026
11 checks passed
@delchev
delchev deleted the issue-7179-posts-atomic branch September 10, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

postings: the per-item posting mode is not atomic and its back-reference guard makes a half-post permanent

1 participant