Closes #4606: fix 42P16 inherited-FK on eager-apply + sharded per-tenant events#4609
Merged
Merged
Conversation
Follow-up to #4598. The eager `ApplyAllConfiguredChangesToDatabaseAsync()` startup pattern, combined with `MultiTenantedWithShardedDatabases` and `UseTenantPartitionedEvents`, made runtime-provisioned sharded tenants fail their first event append. The user-facing symptom was `23514: no partition of relation "mt_events" found for row`, with the actual root cause hidden behind Weasel's silently-logged `42P16: cannot drop inherited constraint "mt_events_tenant_id_stream_id_fkey"` in the partition-attach path. ## Root cause When `UseTenantPartitionedEvents` is on, BOTH `mt_streams` and `mt_events` are list-partitioned by `tenant_id`. Marten declared an explicit FK on the parent `mt_events` referencing `mt_streams(tenant_id, id)`. Eager schema apply + the first per-tenant partition (`mt_streams_alpha`) triggers Postgres to auto-propagate a partition-targeting FK onto `mt_events`: fkey_mt_events_stream_id_tenant_id → mt_streams (Marten-declared, local) mt_events_tenant_id_stream_id_fkey → mt_streams_alpha (PG-propagated, inherited) The next partition attach for `mt_events_alpha` runs Weasel's `additivelyMigrateTablesForNewPartitions`, which sees the auto-propagated FK as "extra" (not in Marten's declared set) and emits `ALTER TABLE mt_events DROP CONSTRAINT IF EXISTS mt_events_tenant_id_stream_id_fkey` — Postgres refuses with 42P16 because the constraint is inherited (`coninhcount = 1`). The error is logged via `NullLogger` and the partition attach is dropped on the floor; the next append fails with 23514 because the partition that was supposed to exist isn't there. ## Fix Skip the explicit `mt_events → mt_streams` FK declaration in `EventsTable.cs` when `UseTenantPartitionedEvents` is on. Without the parent FK, Postgres never auto-propagates the inherited variant, so Weasel's partition-attach has nothing to incorrectly drop and the attach succeeds. The non-partitioned and `UseArchivedStreamPartitioning` shapes keep their FK unchanged — only the per-tenant-partitioning combination hits this. ## Trade-off Database-level referential integrity between `mt_events` and `mt_streams` is lost in the `UseTenantPartitionedEvents` configuration. Marten's append path always inserts/updates the stream row before persisting events (application-level integrity preserved), so the only at-risk surface is external tooling that writes to `mt_events` directly without ensuring the stream exists in `mt_streams` — already outside Marten's contract. The alternative would be a Weasel-side fix to skip inherited FKs in the table-delta introspection. That's the architecturally cleaner fix but requires a coordinated Weasel release; this change unblocks the eager-apply pattern on the current Marten release line without that dependency. ## Tests `sharded_eager_apply_per_tenant_events_tests.eager_apply_then_AddTenantToShardAsync_then_append_succeeds` — the headline regression. Reproduces the exact eager-apply + runtime-provision + first-append shape from the #4606 report; would throw `23514` on master and now succeeds. Regression sweep on net9.0: - All sharded test suites (`sharded_tenancy_tests` + `sharded_tenancy_per_tenant_events_tests` + the new eager-apply test): 21/21 PASS. - DefaultTenancy `use_tenant_partitioned_events_*`: 32/33 PASS — the single fail is the same pre-existing `delete_projection_progress_with_tenant_id_drops_only_that_tenants_rows` partition-setup race that's unrelated to this work (also fails on master). - Conjoined + ForeignKey event-sourcing tests: 75/75 PASS. Closes #4606. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Closes #4606. Follow-up to #4598 / #4605 (explicitly out of scope there).
The eager
ApplyAllConfiguredChangesToDatabaseAsync()startup pattern, combined withMultiTenantedWithShardedDatabasesandUseTenantPartitionedEvents, makes runtime-provisioned sharded tenants fail their first event append. User-facing symptom:23514: no partition of relation "mt_events" found for row. Actual root cause hidden behind Weasel's silently-logged42P16: cannot drop inherited constraint "mt_events_tenant_id_stream_id_fkey"in the partition-attach path.Root cause
When
UseTenantPartitionedEventsis on, BOTHmt_streamsandmt_eventsare list-partitioned bytenant_id. Marten declares an explicit FK on the parentmt_eventsreferencingmt_streams(tenant_id, id). Eager schema apply + the first per-tenant partition (mt_streams_alpha) triggers Postgres to auto-propagate a partition-targeting FK ontomt_events:The next partition attach for
mt_events_alpharuns Weasel'sadditivelyMigrateTablesForNewPartitions, which treats the auto-propagated FK as "extra" (not in Marten's declared set) and emitsALTER TABLE mt_events DROP CONSTRAINT IF EXISTS mt_events_tenant_id_stream_id_fkey. Postgres refuses with 42P16 because the constraint is inherited. The error is logged viaNullLoggerand the partition attach is dropped on the floor; the next append fails with 23514 because the partition that was supposed to exist isn't there.Fix
Skip the explicit
mt_events → mt_streamsFK declaration inEventsTable.cswhenUseTenantPartitionedEventsis on. Without the parent FK, Postgres never auto-propagates the inherited variant, so Weasel's partition-attach has nothing to incorrectly drop and the attach succeeds. The non-partitioned andUseArchivedStreamPartitioningshapes keep their FK unchanged — only the per-tenant-partitioning combination hits this.Trade-off
Database-level referential integrity between
mt_eventsandmt_streamsis lost in theUseTenantPartitionedEventsconfiguration. Marten's append path always inserts/updates the stream row before persisting events (application-level integrity preserved), so the only at-risk surface is external tooling that writes tomt_eventsdirectly without ensuring the stream exists inmt_streams— already outside Marten's contract.The alternative would be a Weasel-side fix to skip inherited FKs in the table-delta introspection. That's the architecturally cleaner fix but requires a coordinated Weasel release; this change unblocks the eager-apply pattern on the current Marten release line without that dependency.
Tests
sharded_eager_apply_per_tenant_events_tests.eager_apply_then_AddTenantToShardAsync_then_append_succeeds— the headline regression. Reproduces the exact eager-apply + runtime-provision + first-append shape from the #4606 report; would throw23514on master and now succeeds.Regression sweep (net9.0)
sharded_tenancy_tests+sharded_tenancy_per_tenant_events_tests+ new eager-apply test)use_tenant_partitioned_events_*The single use_tenant_partitioned_events fail (
delete_projection_progress_with_tenant_id_drops_only_that_tenants_rows) is a pre-existing partition-setup race that also fails on master and is unrelated to this work.Related: #4598, #4605 (the "Note on a related-but-separate bug" section), CritterWatch#267 (form D test) and CritterWatch#271 (eager-apply harness).
🤖 Generated with Claude Code