Skip to content

MySQL: give each tenant store its own database as schema (GH-3860) - #3862

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3860/mysql-tenant-store-schema
Aug 6, 2026
Merged

MySQL: give each tenant store its own database as schema (GH-3860)#3862
jeremydmiller merged 1 commit into
mainfrom
gh-3860/mysql-tenant-store-schema

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #3860. Completes the MySQL half of the root cause behind #3815 / #3859.

The bug

A MySQL schema is a database. PostgreSQL and SQL Server nest a schema inside the database named by the connection string, so one configured schema name is harmless there — isolation comes from the connection string. MySQL has no such nesting, so the same code discards the tenant's database entirely:

// MySqlTenantedMessageStore, both build paths
SchemaName = _persistence.EnvelopeStorageSchemaName   // identical for every tenant

Every tenant shared one physical set of tables — wolverine_incoming_envelopes, wolverine_outgoing_envelopes, dead letters, nodes, and the saga tables.

Measured against the existing suite

Running MySqlTests.MultiTenancy.static_multi_tenancy, which passes 3/3, and then looking at where the tables actually landed:

MySQL, before                          MySQL, after
--------------------------------       --------------------------------
static_multi_tenancy   8 tables        tenant_store_isolation   8 tables
tenant_db1             0 tables        tenant_db1               3 tables
tenant_db2             0 tables        tenant_db2               3 tables
tenant_db3             0 tables        tenant_db3               3 tables

The tenant databases were completely empty. That suite passes because it only asserts the tenancy source resolves the expected connection strings (Describe().DatabaseName), never that anything is stored separately — which is exactly the coverage gap this PR closes.

The fix

A tenant's database is now its schema, derived from its own connection string. The main store is deliberately left alone — it already has a database of its own, and relocating its tables would be a far larger change than the isolation bug requires.

Verification

Evidence Without fix With fix
each_tenant_database_owns_its_own_envelope_tables (×3 tenants) fails passes
tenant_store_schema_is_the_tenants_own_database fails passes
an_envelope_stored_for_one_tenant_is_invisible_to_the_others fails passes
Full MySqlTests suite 121 / 121, twice consecutively
dotnet build wolverine.slnx -c Release -f net9.0 clean, 0 warnings

Two notes on how that baseline was taken, because both bit me:

  • The tenant databases had to be dropped before measuring the red baseline. Tables left behind by a fixed run make the structural assertions pass spuriously — the first baseline I took showed only 2 of 5 failing for exactly that reason.
  • These databases are shared across every suite in the mysql collection and survive between runs, so the behavioural test measures the delta it causes rather than absolute row counts. An earlier absolute-count version passed alone and failed in the full suite.

Migration

An existing multi-tenanted MySQL deployment has all of its tenant envelope rows in the shared schema. After upgrading, each tenant reads from its own database instead, so any in-flight envelopes still in the old shared tables need to be drained or copied across before upgrading. Worth a line in the release notes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg

A MySQL schema IS a database. PostgreSQL and SQL Server nest a schema inside
the database named by the connection string, so a single configured envelope
storage schema name is harmless there -- isolation comes from the connection
string. MySQL has no such nesting, so giving every tenant store the same
SchemaName discarded the tenant's own database entirely:

    MySqlTenantedMessageStore.buildTenantStoreForConnectionString/DataSource
        SchemaName = _persistence.EnvelopeStorageSchemaName   // same for all

Every tenant therefore shared one physical set of tables -- incoming, outgoing,
dead letters, nodes and the saga tables -- with no isolation at all. Measured
against the existing static_multi_tenancy suite, which passes: all 8 tables
lived in the one configured schema and tenant_db1/2/3 were completely empty.
Nothing errored; a host configured this way looked correct.

A tenant's database is now its schema. The main store is deliberately left
alone: it already has a database of its own, and relocating its tables would
be a far larger change than the isolation bug requires.

MIGRATION: an existing multi-tenanted MySQL deployment has all of its tenant
envelope rows in the shared schema. After upgrading, each tenant reads from
its own database instead, so any in-flight envelopes still sitting in the old
shared tables need to be drained or copied across before the upgrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKfy5EzLX1i149gjUb3Tfg
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.

MySQL database-per-tenant multi-tenancy has no isolation: every tenant store resolves to one shared schema

1 participant