Skip to content

Fix #4611: allow StartStream with mandatory stream types under per-tenant event partitioning#4613

Merged
jeremydmiller merged 2 commits into
JasperFx:masterfrom
erdtsieck:fix/4611-mandatory-type-partitioned-startstream
Jun 3, 2026
Merged

Fix #4611: allow StartStream with mandatory stream types under per-tenant event partitioning#4613
jeremydmiller merged 2 commits into
JasperFx:masterfrom
erdtsieck:fix/4611-mandatory-type-partitioned-startstream

Conversation

@erdtsieck
Copy link
Copy Markdown
Contributor

Fixes #4611.

With UseTenantPartitionedEvents enabled, StartStream actions are routed through the bulk mt_quick_append_events operation (see QuickEventAppender.registerOperationsForStreams, where forceBulkFunction is set from UseTenantPartitionedEvents).

QuickAppendEventsOperationBase.PostprocessAsync has a guard for UseMandatoryStreamTypeDeclaration: a stream whose first event comes back as version 1 is treated as an append to a non-existent stream and rejected with NonExistentStreamException. That is correct for an Append, but a legitimate StartStream of a brand-new stream also produces version 1 — so under per-tenant partitioning the guard fired for valid StartStream calls. The events were then tombstoned, and a later append to the (never-created) stream failed too.

The fix limits the guard to Append actions by excluding StreamActionType.Start. StartStream of a new stream is allowed; appending to a non-existent stream under mandatory stream types is still rejected.

Added a regression test in sharded_tenancy_per_tenant_events_tests (sharded + UseTenantPartitionedEvents + UseMandatoryStreamTypeDeclaration + a typed StartStream). The existing mandatory_stream_type_behavior and use_tenant_partitioned_events_quick_append suites still pass, so the append-to-non-existent rejection is preserved.

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>
erdtsieck and others added 2 commits June 3, 2026 07:59
…artitioned events

When UseTenantPartitionedEvents is enabled, StartStream actions are routed
through the bulk mt_quick_append_events operation (QuickEventAppender). The
PostprocessAsync guard that rejects appending to a non-existent stream under
UseMandatoryStreamTypeDeclaration (a new stream's first event comes back as
version 1) also fired for a legitimate StartStream of a brand-new stream,
throwing NonExistentStreamException. The events were then tombstoned, so a
later append to the never-created stream failed as well.

Only treat version 1 as an error for Append actions, not Start.
…artStream guard test

Two additions to the regression test for JasperFx#4611:

1. The existing test now asserts the end state the fix is supposed to produce:
   the mt_streams row exists in the assigned shard for the started stream, and
   its `type` column is populated with the aggregate-type alias. This is the
   surface the original bug actually broke — events landed in mt_events but
   the StartStream got post-process-tombstoned, so the mt_streams row was
   never created (which then made every subsequent Append throw
   NonExistentStreamException).

2. New companion test under the same sharded + UseTenantPartitionedEvents +
   UseMandatoryStreamTypeDeclaration config that calls the no-type
   StartStream overloads and asserts they still throw StreamTypeMissingException
   synchronously. The fix in QuickAppendEventsOperationBase only relaxes the
   post-process "appended to a non-existent stream" guard for Start actions
   (Stream.ActionType != Append) — it does not, and must not, weaken the
   API-level guard in EventStore.StartStream that rejects untyped StartStream
   when UseMandatoryStreamTypeDeclaration is on. This test pins that guarantee
   so a future change to the bulk-path guard can't accidentally let untyped
   streams through.

Pre-fix: the strengthened end-state test fails with the originally-reported
NonExistentStreamException; the untyped-StartStream test passes (the
API-level guard fires before the bulk path). Post-fix: both pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller force-pushed the fix/4611-mandatory-type-partitioned-startstream branch from bb49e80 to 827a251 Compare June 3, 2026 12:59
@jeremydmiller jeremydmiller merged commit 4180dab into JasperFx:master Jun 3, 2026
8 checks passed
jeremydmiller added a commit that referenced this pull request Jun 3, 2026
…on pin

Three new files, 7 tests covering #4617 section 3a remaining items
(return-shape coverage, event-metadata propagation) plus the single-DB
conjoined variant of the #4611 regression already pinned for sharded.

## 3a — Return-shape coverage
(`AppendWrite/append_return_shapes_under_partitioning.cs`, 3 tests)

The Append + StartStream overload matrix has multiple shapes (`params
object[]`, `IEnumerable<object>`, single-event-via-params-of-one). Each must
route through the bulk `mt_quick_append_events` function and land in the
owning tenant's partition. Pinned per the spec's "Return shapes" item so a
future overload-resolution change or appender refactor can't silently
reshape what hits the partitioned tables.

  - `append_via_params_object_array_lands_in_tenants_partition`
  - `append_via_IEnumerable_lands_in_tenants_partition`
  - `append_single_event_via_params_array_of_one_lands_in_tenants_partition`
    (single-event boundary case — the loop bound at array_length(event_ids, 1))

## 3a — Event metadata propagation
(`AppendWrite/event_metadata_propagation_under_partitioning.cs`, 2 tests)

Per the spec's TenantPropagation pin (#4424) and metadata-flow pin:

  - `event_TenantId_equals_stream_TenantId_under_partitioning` — shared
    fixture; pins the TenantPropagation invariant that every event's
    `TenantId` equals the stream's tenant, never null.
  - `event_optional_metadata_propagation_under_partitioning` — its own
    DocumentStore because CorrelationId / CausationId / Headers / UserName
    metadata columns are opt-in (MetadataConfig.* = true). Pins that the
    full set propagates from session → each event in the bulk batch, AND
    that TenantId stays paired with the right tenant alongside the opt-in
    metadata.

## Section 5 — #4611 single-DB regression pin
(`Regressions/Bug_4611_mandatory_stream_type_under_partitioning.cs`, 2 tests)

The sharded variant already lives in `Sharded/sharded_tenancy_per_tenant_events.cs`
from PR #3. This is the single-DB conjoined variant the spec specifically
calls out as missing. Own-store because UseMandatoryStreamTypeDeclaration is
a store-wide flag.

  - `StartStream_with_aggregate_type_followed_by_Append_works` — Start +
    Append both succeed; mt_streams row exists with `type` = aggregate alias.
  - `untyped_StartStream_still_throws_StreamTypeMissingException` — companion
    pin: the API-level guard in EventStore.StartStream STILL fires
    synchronously for the no-type overload. PR #4613's fix didn't (and
    shouldn't) weaken this guard.

## Verified

- **net9.0**: 118/118 pass (was 111 → +7)
- **net10.0**: 118/118 pass

## Subsequent PRs per the #4617 checklist

- 3c remaining: EventProjection, FlatTableProjection, MultiStreamProjection
  RollUpByTenant + AcrossTenants / AddGlobalProjection, custom
  IAggregateGrouper + fan-out, raw IProjection, same-projection lifecycle
  equivalence.
- 3d remaining: AddMartenManagedTenantsAsync(Guid[]) N-format pin,
  sharded reuses ONE MartenDatabase per shard, IDynamicTenantSource lifecycle.
- 3e remaining: DeleteAllTenantDataAsync orphan-sequence leak pin (needs
  own-store), RemoveMartenManagedTenantsAsync, AssertDatabaseMatchesConfigurationAsync
  drift, DDL generation, PerTenantEventSequences schema-object correctness,
  AutoCreate matrix.
- 3f DCB partitioned coverage.
- Section 4 daemon in-depth.
- Section 5 remaining: 42P01 / 42P16 / 42P07 / MT002-rebuild regression families.
- 3a remaining: Quick vs QuickWithServerTimestamps timestamp-source pin,
  seq_id gap-after-failed-batch.
- 3b deferred items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

StartStream doesn't create mt_streams row under per-tenant event partitioning (sharded) → NonExistentStream on later append (9.5.0)

2 participants