Skip to content

Closes #4606: fix 42P16 inherited-FK on eager-apply + sharded per-tenant events#4609

Merged
jeremydmiller merged 1 commit into
masterfrom
fix/4606-eager-apply-inherited-fk
Jun 3, 2026
Merged

Closes #4606: fix 42P16 inherited-FK on eager-apply + sharded per-tenant events#4609
jeremydmiller merged 1 commit into
masterfrom
fix/4606-eager-apply-inherited-fk

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Closes #4606. Follow-up to #4598 / #4605 (explicitly out of scope there).

The eager ApplyAllConfiguredChangesToDatabaseAsync() startup pattern, combined with MultiTenantedWithShardedDatabases and UseTenantPartitionedEvents, 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-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 declares 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, conislocal=t, coninhcount=0)
mt_events_tenant_id_stream_id_fkey  → mt_streams_alpha     (PG-propagated, conislocal=f, coninhcount=1)

The next partition attach for mt_events_alpha runs Weasel's additivelyMigrateTablesForNewPartitions, which treats 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. 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 (net9.0)

Suite Result
All sharded test suites (sharded_tenancy_tests + sharded_tenancy_per_tenant_events_tests + new eager-apply test) 21/21 PASS
DefaultTenancy use_tenant_partitioned_events_* 32/33 PASS (single fail is pre-existing, unrelated)
Conjoined + ForeignKey event-sourcing tests 75/75 PASS

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

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>
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.

42P16 dropping inherited FK constraint when sharded per-tenant event partitions are attached after eager schema apply

1 participant