Skip to content

Fix #5451 — refuse a MapFromAggregate VectorProjection registered Inline - #5468

Merged
jeremydmiller merged 1 commit into
masterfrom
investigate/5451-vector-inline
Sep 21, 2026
Merged

jeremydmiller merged 1 commit into
masterfrom
investigate/5451-vector-inline

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5451. The diagnosis in the issue was right; this adds the measurement it said it lacked, then the refusal.

What an Inline registration actually did

The issue was written from reading the code, so I ran it before fixing it. Registering NoteAggregateVectorProjection (the existing MapFromAggregate test projection) Inline and reading content_text after each save:

content_text correct-if-async
after StartStream(NoteWritten("a body")) no row at all a body
after Append(NoteTitled("a title")) a body a title a body

Worse than "lags one change behind", strictly: the first save writes nothing, because live aggregation reads committed events and the stream has none yet, so the aggregate is null. Every later save then writes the state as of the previous event. The content hash records that stale text as current, and nothing fails.

The fix

ValidateConfiguration now refuses a non-Async registration when the map declares an aggregate type, following the lookup Fisher and Polecat already use: walk options.Projections.All, match IProjectionWrapper.InnerProjection by reference, read source.Lifecycle. It fires at DocumentStore.For(...) via AssertValidity, so this is a build-time failure, as the issue suggested.

Narrowed deliberately. Fisher and Polecat refuse every non-Async registration of their vector projections; Marten supports Inline for event-mapped projections on purpose — the class remarks call it "correct, not merely tolerated", since an event-mapped projection takes its text from the event it was handed and Inline has that in full. Only MapFromAggregate is unsound there, so only MapFromAggregate is refused.

Tests

  • a_map_from_aggregate_projection_registered_inline_is_refused — asserts the message names both MapFromAggregate and ProjectionLifecycle.Async, so it points at the fix rather than just failing.
  • the_same_projection_registered_async_still_builds
  • an_event_mapped_projection_registered_inline_still_builds — the guard that matters most, since over-refusing would break a supported configuration.

Marten.PgVector.Tests on net9.0: 101 passed, 0 failed (98 before, plus these three). The existing map_from_aggregate_tests, registered Async, still passes — so the refusal does not over-fire.

One thing found on the way, not fixed here

ProjectionLifecycle.Live is also accepted silently for this projection. JasperFx's ProjectionGraph.Add already rejects Live for any plain IProjection ("Live cannot be used for IProjection"), but the overload Marten's opts.Projections.Add(projection, lifecycle) takes does not reach that guard. That is a gap in the registration path for every IProjection, not something specific to MapFromAggregate, so I left it alone rather than widening this fix to hide it. Happy to file it — say the word.

🤖 Generated with Claude Code

https://claude.ai/code/session_01481eiEZg1Bv6pu4DrgPRg3

…line

VectorProjection's class remarks have always said MapFromAggregate requires
ProjectionLifecycle.Async, and nothing enforced it. ValidateConfiguration only
checked the conjoined tenant_id column, so an Inline registration built without
complaint.

Measured on an unfixed build rather than reasoned about, since the issue was
diagnosed from reading the code: the first save writes NO ROW at all -- live
aggregation reads committed events and the stream has none yet, so the aggregate
is null -- and the second save writes "a body", the state as of the FIRST event,
missing the NoteTitled that triggered it. Correct-if-async is "a body" then
"a title a body". The embedding and content_text therefore lag one change behind
for the life of the stream, the content hash records that stale text as current,
and nothing anywhere fails.

The refusal is narrowed to a map declaring an aggregate. An event-mapped
projection takes its text from the event it was handed, which Inline has in full,
and Marten supports that on purpose -- the class remarks call it "correct, not
merely tolerated". Fisher and Polecat refuse every non-Async registration of
theirs; this follows their lookup (walk options.Projections.All, match
IProjectionWrapper.InnerProjection by reference, read the lifecycle) without
copying their broader rule.

Live is deliberately not covered. JasperFx's ProjectionGraph.Add already rejects
Live for any plain IProjection, and Marten's overload does not reach that guard,
so a Live registration is accepted silently -- a gap in the registration path for
every IProjection rather than anything specific to this projection. Noted in the
test remarks; it wants its own issue.

Closes #5451

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01481eiEZg1Bv6pu4DrgPRg3
@jeremydmiller
jeremydmiller merged commit 66f9ffb into master Sep 21, 2026
32 checks passed
@jeremydmiller
jeremydmiller deleted the investigate/5451-vector-inline branch September 21, 2026 14:07
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.

VectorProjection with MapFromAggregate registered Inline is accepted, and silently embeds text that misses the triggering event

1 participant