Fix #4602: Query<T>() throws IndexOutOfRangeException on tenant-mapped projection documents#4603
Merged
jeremydmiller merged 1 commit intoJun 2, 2026
Conversation
A LINQ Query<T>() threw IndexOutOfRangeException "Ordinal must be between 0 and 1" when a document both mapped tenant_id onto a member (ITenanted or .Metadata(m => m.TenantId.MapTo(...))) and was a projection/aggregate target (numeric revisions enabled by ProjectionDocumentPolicy, no revision member). LoadAsync was unaffected; only the LINQ path broke. Cause: the closed-shape descriptor builds one ReadBinders array shared by all four storage styles. Version/RevisionColumn.ShouldSelect returns false for the QueryOnly style when the column has no member, so QueryOnly's SELECT omits mt_version — but the shared ReadBinders still included the version/revision binder, shifting every later ordinal so the tenant binder read past the end of the QueryOnly row. Fix: give the descriptor a QueryOnlyReadBinders array that drops the version/revision binder when it's present only for the non-QueryOnly concurrency rule (no member), and have ClosedShapeQueryOnlySelector read from it. Same array instance as ReadBinders when nothing is dropped, so no extra allocation for the common case. The non-QueryOnly selectors (lightweight, identity-map, dirty-tracking) keep using ReadBinders and are unchanged. Adds Bug_4602 regression test: the two failing combinations (ITenanted + projection, MapTo + projection), the IRevisioned workaround, and two controls isolating each feature alone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RorySan
added a commit
to RorySan/marten9-tenant-repro
that referenced
this pull request
Jun 2, 2026
Test now matches what landed in JasperFx/marten#4603: control_projection case marks PlainDoc MultiTenanted so a conjoined projection target is valid. PR.md rewritten as a fix+test PR with root cause and verification notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Jun 2, 2026
PR #4603 fixed the QueryOnly ordinal mismatch and shipped a 5-test repro covering the headline case (tenant-mapped projection document). The bug envelope is meaningfully wider: - NOT projection-target-specific. UseNumericRevisions / UseOptimisticConcurrency turned on directly on a non-projection mapping (e.g. via o.Schema.For<T>().UseNumericRevisions(true)) hits the same descriptor state and the same crash. - NOT tenant_id-specific. Any after-version metadata member mapped via .Metadata(m => m.X.MapTo(...)) shifts the QueryOnly ordinals: last_modified, created_at, correlation_id, causation_id, last_modified_by. - Affects hierarchical mappings too. doc_type sits before the version binder so it reads correctly, but the version → tenant_id shift still trips QueryOnly (error bound becomes "0 and 2" instead of "0 and 1" because the SELECT is one column wider). Adds 5 probes covering this envelope, each parameterized across all four selectors (QueryOnly, Lightweight, IdentityMap, DirtyTracking) for a 20-test matrix. The QueryOnly case in each probe is the regression proof; the Lightweight / IdentityMap / DirtyTracking cases are canaries — those selectors were never broken (their SELECTs include mt_version for the write-back path), so they pin the fix scope. A future refactor that accidentally trimmed ReadBinders itself (instead of just QueryOnlyReadBinders) would crash all 15 non-QueryOnly cases immediately. Verified on master: - Bare master: 5/20 fail (exactly the 5 QueryOnly cases) - After #4603: 20/20 pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 3, 2026
Merged
This was referenced Jun 11, 2026
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.
Fix #4602: Query<T>() throws IndexOutOfRangeException on tenant-mapped projection documents
Fixes #4602.
A LINQ
Query<T>()threwIndexOutOfRangeException: "Ordinal must be between 0 and 1"when adocument both mapped
tenant_idonto a member (viaITenantedor.Metadata(m => m.TenantId.MapTo(...))) and was a projection/aggregate target (numericrevisions enabled by
ProjectionDocumentPolicy, with no revision member).LoadAsync<T>wasunaffected — only the LINQ path broke.
Cause
The closed-shape descriptor builds one
ReadBindersarray shared by all four storage styles.RevisionColumn.ShouldSelect(andVersionColumn.ShouldSelect) returnfalsefor the QueryOnlystyle when the column has no member:
So QueryOnly's SELECT omits
mt_version, but the sharedReadBindersstill contained theversion/revision binder (it was added whenever
UseNumericRevisions, matching the non-QueryOnlyshape).
ClosedShapeQueryOnlySelector.ApplyMetadatawalksReadBindersassigning ordinals inlockstep, so every binder after the absent
mt_versionwas off by one — the tenant binder then readone column past the end of the QueryOnly row:
LoadAsyncuses the lightweight/identity-map styles, whose SELECT does includemt_version, so itstayed in lockstep — which is why only the LINQ path broke.
Fix
Give the descriptor a
QueryOnlyReadBindersarray that drops the version/revision binder when it'spresent only for the non-QueryOnly concurrency rule (i.e. no member), and have
ClosedShapeQueryOnlySelectorread from it. It's the same array instance asReadBinderswhennothing is dropped, so there's no extra allocation for the common case. The non-QueryOnly selectors
(lightweight, identity-map, dirty-tracking) keep using
ReadBindersand are unchanged.Tests
src/EventSourcingTests/Bugs/Bug_4602_query_tenant_mapped_projection_document.cs:can_query_an_ITenanted_projection_documentcan_query_a_MapTo_projection_documentworkaround_IRevisioned_member_realigns_the_selectcontrol_tenant_mapped_but_not_a_projection_targetcontrol_projection_target_but_not_tenant_mappedVerified the two repro tests fail on
master(sameIndexOutOfRangeException) and pass with the fix.Also re-ran the closed-shape numeric-revision / optimistic-concurrency tests
(
CoreTests/Storage/Identification/closed_shape_*) and theDocumentDbTestsconcurrency + versionmetadata suites (91 tests) — all green.
Standalone repro: https://github.com/RorySan/marten9-tenant-repro