Fix #4614: keep mt_version as integer for IRevisioned (V8) documents#4615
Merged
Merged
Conversation
jeremydmiller
added a commit
that referenced
this pull request
Jun 3, 2026
…sions #4610 fixed `values.Contains(p.EnumMember)` against an EnumStorage.AsString document member by routing the case through `EnumIsOneOfWhereFragment` in `EnumerableContains.Parse`. That worked on net9.0 but the regression test file failed on net10.0 in CI for PR #4613 and #4615 with the same `Writing values of 'YourEnum[]' is not supported for parameters having NpgsqlDbType '-2147483639'` shape from the original bug. Root cause: on net10.0 the C# compiler resolves `array.Contains(x)` to `MemoryExtensions.Contains` (via the implicit `T[]` → `ReadOnlySpan<T>` conversion), not `Enumerable.Contains`. The match runs through `MemoryExtensionsContains.Parse`, not `EnumerableContains.Parse`, and the former had the identical enum-array-as-raw-CommandParameter bug — #4610's fix had only patched the latter. Mirror the same fix into `MemoryExtensionsContains.Parse`. The existing Bug_enum_asstring_array_contains regression tests now cover both parsers because they target the user-facing shape (`values.Contains(p.Status)`) and each .NET version routes that to a different parser. Verified: - Bug_enum_asstring_array_contains: 6/6 pass on net9.0 - Bug_enum_asstring_array_contains: 6/6 pass on net10.0 - Full LinqTests on net10.0: 1269 passed, 0 failed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marten 9 widened the mt_version column from `integer` to `bigint` for every projected document (#3733), then partially reverted in #4533 — the .NET IRevisioned.Version went back to int, but the column itself stayed bigint. Result: V8 → V9 upgrades silently migrate SingleStreamProjection aggregate tables from `integer` to `bigint`, even though their version (the per-stream event count) comfortably fits Int32 and was the V8 default. This restores the V8 column width for IRevisioned-backed documents while preserving the bigint column for the legitimate ILongVersioned shape that MultiStreamProjection-projected documents use (Version is the global event sequence number; can exceed Int32 under high event volume). Discriminator: the document type's interface choice, not the projection kind. IRevisioned → integer column; ILongVersioned → bigint column. The Marten projection registration paths already steer users toward the right interface (SingleStream-projected aggregates typically implement IRevisioned; MultiStream-projected docs implement ILongVersioned), so no public knob is introduced — the variant is policy-driven via VersionedPolicy. Migration semantics are non-destructive in both directions: - V8 integer + IRevisioned (desired integer) → no-op (the headline fix) - V8 integer + ILongVersioned (desired bigint) → widen (existing path preserved) - 9.x bigint + IRevisioned (desired integer) → no-op (tolerate; no force-narrow) - 9.x bigint + ILongVersioned (desired bigint) → no-op - uuid + numeric revisions → drop + recreate as desired width Refusing to narrow bigint → integer is the safety call: Postgres won't verify without scanning the table, and a `USING mt_version::integer` cast would silently truncate any out-of-range value. Documents whose interface is IRevisioned are practically bounded by Int32, but a wider on-disk column is harmless — leave it alone. Implementation: - RevisionColumnInt32 sibling of RevisionColumn — `integer` column, integer parameter type, integer-aware migration policy. - RevisionArgumentInt32 sibling of RevisionArgument for the codegen path. - DocumentMetadataCollection.Revision gets an internal setter so VersionedPolicy can swap to the Int32 variant when IRevisioned is detected. - DocumentRevisionBinder accepts a column NpgsqlDbType so its BindParameter and WriteToBulkAsync emit `integer`/`bigint` to match the column. Read side stays as long — Npgsql widens int→long via the field-value handler. - ClosedShape{Insert,Update,Upsert}Operation: revision parameter slots now bind with the binder's ColumnDbType (integer for IRevisioned, bigint for ILongVersioned) so the CASE-in-VALUES expression's branches type-align with the target column. Postgres won't auto-narrow on the VALUES strict path, so this match has to be exact. - DocumentTable.SelectColumns also matches RevisionColumnInt32 explicitly (it's a sibling, not a subclass of RevisionColumn) so the version slot stays canonical in the SELECT projection. Tests (8 in CoreTests/Bugs/Bug_4614_revision_column_int_for_IRevisioned.cs): - Fresh IRevisioned table → integer column. - Fresh ILongVersioned table → bigint column. - V8 integer + IRevisioned → no migration (headline regression). - V8 integer + ILongVersioned → widens to bigint (legitimate path preserved). - 9.x bigint + IRevisioned → tolerated, no force-narrow. - IRevisioned round-trip insert + update + read on integer column. - ILongVersioned round-trip insert + update + read on bigint column. - SingleStreamProjection<TIRevisionedDoc> end-to-end through the inline projection path on the integer column. Regression sweeps clean: CoreTests (424 passed), DocumentDbTests (999 passed), EventSourcingTests (1358 passed). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c9cdadf to
0f655e3
Compare
This was referenced Jun 3, 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.
Problem
Users upgrading Marten 8 → 9 see their
SingleStreamProjectionaggregate document tables silently migratemt_versionfromintegertobigint. The widening was unintentional — leftover from #3733 (which widenedRevisionColumntoMetadataColumn<long>), then partially reverted in #4533 (IRevisioned.Versionwent back tointbut the column itself stayedbigint).The user-visible symptom is an
ALTER TABLE … ALTER COLUMN mt_version TYPE bigintshowing up on every aggregate document table at first V9 apply, with no real reason —IRevisioned-backed documents track a per-stream version that comfortably fits Int32 and was the V8 default.Fix
The version column width now varies by the document's interface:
IRevisioned(int)integermt_streams.version(per-stream counter)ILongVersioned(long)bigintDiscriminator is the interface choice, not the projection kind. This decouples the column-width decision from the existing
UseVersionFromMatchingStreamflag (which is about SQL semantics — where the version comes from — and shouldn't also control DDL).Migration matrix
Non-destructive in both directions:
integerinteger(IRevisioned)integerbigint(ILongVersioned)ALTER … TYPE bigint— legitimate V8 → V9 widening preservedbigintinteger(IRevisioned)USING ::integerforce-narrow that could silently truncatebigintbigint(ILongVersioned)uuidRefusing the bigint → integer narrowing is the safety call. A
USING mt_version::integercast would truncate any out-of-range value without Postgres scanning to verify, and an IRevisioned-backed table that's been on 9.x for a while has no real reason to need its column narrowed — the wider column is harmless on disk.Implementation
RevisionColumnInt32sibling ofRevisionColumn— integer column, integer parameter type, integer-aware migration policy.RevisionArgumentInt32sibling ofRevisionArgumentfor the codegen path.DocumentMetadataCollection.Revisiongets an internal setter soVersionedPolicy.Apply()can swap to the Int32 variant whenIRevisionedis detected on the document type.DocumentRevisionBinderaccepts a columnNpgsqlDbTypesoBindParameterandWriteToBulkAsyncemitinteger/bigintto match the column. Read side stays aslong— Npgsql widens int → long via the field-value handler.ClosedShape{Insert,Update,Upsert}Operation: revision parameter slots now bind with the binder'sColumnDbType(integer for IRevisioned, bigint for ILongVersioned) so theCASEexpression's branches type-align with the target column. Postgres won't auto-narrow on the strict VALUES path, so this match has to be exact.DocumentTable.SelectColumnsmatchesRevisionColumnInt32explicitly (sibling, not subclass ofRevisionColumn) so the version slot stays canonical in the SELECT projection.Tests
8 tests in
src/CoreTests/Bugs/Bug_4614_revision_column_int_for_IRevisioned.cs:IRevisionedtable →integercolumn.ILongVersionedtable →bigintcolumn.integer+IRevisioned→ no migration (headline regression).integer+ILongVersioned→ widens tobigint(legitimate path preserved).bigint+IRevisioned→ tolerated, no force-narrow (data-safety guarantee).IRevisionedround-trip insert + update + read on integer column.ILongVersionedround-trip insert + update + read on bigint column.SingleStreamProjection<IRevisionedDoc>end-to-end through the inline projection path on the integer column.Regression sweep
All clean:
🤖 Generated with Claude Code