Skip to content

EventProjectionScenario's wipe covers composite projections (#5169) - #5185

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/5169-composite-scenario-wipe
Aug 4, 2026
Merged

EventProjectionScenario's wipe covers composite projections (#5169)#5185
jeremydmiller merged 1 commit into
masterfrom
fix/5169-composite-scenario-wipe

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5169.

The bug

ProjectionScenario.DeleteExistingDataAsync built its wipe list from Options.StorageTypes:

await _store.Advanced.Clean.DeleteAllEventDataAsync(ct);
foreach (var storageType in _store.Options.Projections.All.SelectMany(x => x.Options.StorageTypes))
{
    await _store.Advanced.Clean.DeleteDocumentsByTypeAsync(storageType, ct);
}

A CompositeProjection never populates its own Options.StorageTypes (nor CleanUps) — its members hold theirs. So for a store whose read side is one composite over ~10 members, the loop iterated nothing. Event data was deleted, the composite's read models were not, and every scenario after the first ran against the previous scenario's documents while their events were gone. Because the events were deleted, nothing downstream could reconstruct or even notice the leftovers.

Tests stayed accidentally correct as long as they used unique ids per scenario. Any unfiltered Query<T>() inside AssertAgainstProjectedData saw prior scenarios' rows — which is how the reporter hit it: an ordered Query<InvoiceOverviewItem>() asserting [100m, 200m] returned [90m, 100m, 100m, 200m, 250m].

The fix

The wipe walks PublishedTypes() instead:

foreach (var storageType in _store.Options.Projections.All.SelectMany(x => x.PublishedTypes()).Distinct())

That is the right traversal for three independent reasons:

  • CompositeProjection<,>.PublishedTypes() already unions its stages' members, so composites expand correctly with no new machinery;
  • it is a superset of Options.StorageTypes for every ProjectionBase (PublishedTypes() = _publishedTypes ∪ Options.StorageTypes), so nothing that was wiped before stops being wiped;
  • it is what Marten's own schema build-out uses (StorageFeaturesAllPublishedTypes()), whereas StorageTypes is documented as a schema-building hint — "used to help build out schema objects if the async daemon is started before the rest of the application" — not a teardown list.

That covers the issue's suggested fix 1. Fix 2 (having the composite populate its own StorageTypes/CleanUps from its members) is deliberately not taken: duplicating the members' CleanUps onto the parent would make the rebuild teardown in #5175 truncate every member table twice, and the schema-build concern it was meant to address is already handled by PublishedTypes().

Tests

DaemonTests/EventProjections/Bug_5169_composite_scenario_wipe.cs:

  • the_composite_reports_the_document_types_its_members_write — the composite's PublishedTypes() carries both a stage-1 snapshot type and a stage-2 read model, and its Options.StorageTypes is empty (documenting why the old source could not work)
  • a_second_scenario_starts_from_a_clean_slate — the issue's repro: two sequential EventProjectionScenario runs over a composite; the second must see exactly one invoice and one overview, and the first scenario's documents must be gone

Falsified: reverting the one-line change makes a_second_scenario_starts_from_a_clean_slate fail. All 26 EventProjections + Bug_5169 tests pass on net10.0.

Adjacent to #5175, which is the same family (a composite's members being invisible from the outside) but a different mechanism — rebuild teardown reading each member's CleanUps.

🤖 Generated with Claude Code

https://claude.ai/code/session_017CTtw2kVRSZKp1p5RTxgAy

`ProjectionScenario.DeleteExistingDataAsync` derived its wipe list from
`Options.StorageTypes`. A `CompositeProjection` never populates its own `StorageTypes` —
its members hold theirs — so for a store whose entire read side is one composite the loop
iterated nothing: event data was deleted, the composite's read models were not, and every
scenario after the first ran against the previous scenario's documents, now orphaned from
any events. Because the events *were* deleted, nothing could reconstruct or notice them.

Silently, and it undercuts exactly the guarantee the harness leads with. Tests stayed
accidentally correct as long as they used unique ids per scenario; any unfiltered
`Query<T>()` inside `AssertAgainstProjectedData` saw prior scenarios' rows.

The wipe now walks `PublishedTypes()`. That is the traversal that already knows to expand a
composite into its members (`CompositeProjection<,>.PublishedTypes()` unions its stages), it
is a superset of `StorageTypes` for every `ProjectionBase`, and it is what Marten's own
schema build-out uses — unlike `StorageTypes`, which is documented as a schema-building hint
rather than a teardown list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017CTtw2kVRSZKp1p5RTxgAy
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.

EventProjectionScenario's up-front wipe is a no-op for composite projections — read models survive while their events are deleted

1 participant