Skip to content

feat(#335): per-tenant managed partitioning parity with Marten (documents + streams) - #338

Merged
jeremydmiller merged 2 commits into
mainfrom
feat/335-tenant-partitioning-parity
Jul 19, 2026
Merged

feat(#335): per-tenant managed partitioning parity with Marten (documents + streams)#338
jeremydmiller merged 2 commits into
mainfrom
feat/335-tenant-partitioning-parity

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #335. Depends on (and bumps to) Weasel 9.18.0, which ships the JasperFx/weasel#362 ManagedTenantPartitions parity work (JasperFx/weasel#374).

What's here

Weasel 9.17.0 → 9.18.0 — picks up TenantDropBehavior (RetainData/DeleteData purge-before-merge), AllowOrdinalSharing + explicit-ordinal bucketing overloads, MigrateAllTablesAsync back-fill, and TenantPartitionAddResult/TablePartitionStatus[] batch status reporting.

pc_streams partitions alongside pc_events (issue scope 2, mt_streams parity): under UseTenantPartitionedEvents, tenant_ordinal joins the streams PK after (tenant_id, id) so every existing reader keeps its prefix seek; the stream INSERT stamps the planner-resolved ordinal and the version UPDATE partition-eliminates on it.

Tenant-partitioned document tables (scope 1): the DocumentTable "single-tenant only" throw is lifted for the managed strategy. Conjoined document tables gain a tenant_ordinal int PK column on the store's shared ManagedTenantPartitions — one pc_tenant_partitions registry per database, shared with the event store. The ordinal is resolved server-side in every write statement by joining the registry into the MERGE USING source ((VALUES ...) v LEFT JOIN pc_tenant_partitions tp ON tp.tenant_id = v.tenant_id), which keeps every positional parameter slot unchanged across the upsert/insert/overwrite/update-only/version-checked-bulk shapes and makes cross-process ordinal state incapable of mis-routing rows. The ON clause carries tenant_ordinal for partition elimination (Marten #4223 analogue).

Policy APIs (scope 3): Policies.AllDocumentsAreMultiTenantedWithPartitioning() (forces conjoined tenancy) and Policies.PartitionMultiTenantedDocumentsUsingPolecatManagement(), plus a per-type ForDocument<T>(p => p.DisableTenantPartitioning = true) escape hatch. The daemon's DeadLetterEvent document is always excluded (Marten parity).

Runtime tenant onboarding (scopes 4+5): store.Advanced.AddPolecatManagedTenantsAsync(ct, params tenantIds) returns per-table TablePartitionStatus[], with an explicit-ordinal dictionary overload for tenant bucketing; RemovePolecatManagedTenantsAsync takes TenantDropBehaviorDeleteData physically purges the tenants' rows from every managed table before MERGE RANGE (the weasel#362 item-1 semantics), fully-released ordinals drop their pc_events_sequence_{ordinal}, and in-process caches evict so a removed tenant can be re-onboarded fresh.

Lazy provisioning everywhere: a new shared TenantPartitionOrdinalRegistry (composed by TenantEventSequenceRegistry, so events/streams/documents share one tenant → ordinal cache) is resolved by the session flush pipeline, the daemon's projection batch, and both bulk-insert surfaces before operations execute. Table creation hydrates the manager first, so a table created after tenants exist bakes the full boundary set into its CREATE.

No reserved default-tenant partition: Polecat has no global projections, and integer ordinals sidestep Marten's __default__ suffix-identifier constraint entirely — documented rather than ported.

Testing

15 new integration tests in the serialized tenant-partitioning collection (streams schema/physical placement/append-update path; document schema shape + ordinal stamping via $PARTITION, ForTenant override, bulk insert, per-type opt-out, config guards, add/remove with both drop behaviors incl. re-onboarding, and events+streams+documents sharing one registry/ordinal). Full net10 suite locally: 1480 passed / 0 failed on a fresh SQL Server 2025 container.

Follow-up filed: JasperFx/wolverine#3496 (EF Core integration adopting the same Weasel 9.18.0 surface).

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits July 18, 2026 21:26
Weasel 9.18.0 ships weasel#362 (PR weasel#374) — ManagedTenantPartitions
parity with the PostgreSQL ManagedListPartitions: TenantDropBehavior
(RetainData/DeleteData purge-before-merge), AllowOrdinalSharing +
explicit-ordinal add overloads (tenant bucketing under the 15,000-partition
ceiling), MigrateAllTablesAsync new-table back-fill, and
TenantPartitionAddResult / TablePartitionStatus batch status reporting.
The prerequisite for polecat#335.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ents + streams)

Brings Polecat's managed per-tenant partitioning to full Marten
equivalence on Weasel 9.18.0:

- pc_streams now partitions alongside pc_events under
  UseTenantPartitionedEvents (mt_streams parity): tenant_ordinal joins
  the PK after (tenant_id, id) so readers keep their prefix seek, the
  stream INSERT stamps the planner-resolved ordinal, and the version
  UPDATE partition-eliminates on it.
- Conjoined document tables can be tenant-partitioned: the DocumentTable
  "single-tenant only" restriction is lifted for the managed strategy —
  tables gain a tenant_ordinal PK column driven by the one
  pc_tenant_partitions registry per database. Every write path (MERGE
  upsert/insert/overwrite, update-only, version-checked bulk) resolves
  the ordinal SERVER-SIDE by joining the registry into the MERGE source,
  so parameter slots are unchanged and cross-process ordinal state can
  never mis-route rows; the ON clause carries tenant_ordinal for
  partition elimination.
- Policy APIs: Policies.AllDocumentsAreMultiTenantedWithPartitioning()
  (forces conjoined tenancy) and
  Policies.PartitionMultiTenantedDocumentsUsingPolecatManagement(),
  with a per-type ForDocument<T>(p => p.DisableTenantPartitioning)
  escape hatch; the daemon's DeadLetterEvent document is always
  excluded (Marten parity).
- Runtime tenant onboarding:
  store.Advanced.AddPolecatManagedTenantsAsync (batch add with
  TablePartitionStatus[] reporting + explicit-ordinal bucketing
  overload) and RemovePolecatManagedTenantsAsync with
  TenantDropBehavior — DeleteData purges the tenants' rows before
  MERGE RANGE (weasel#362 item 1), fully-released ordinals drop their
  pc_events_sequence_{ordinal}, and in-process caches are evicted so a
  removed tenant can be re-onboarded.
- Tenants are provisioned lazily on first write everywhere: the session
  flush pipeline, the daemon's projection batch, and both bulk-insert
  surfaces resolve ordinals through the new shared
  TenantPartitionOrdinalRegistry (TenantEventSequenceRegistry now
  composes it, so events/streams/documents share one tenant -> ordinal
  cache). Table creation hydrates the manager first so tables created
  after tenants exist bake the full boundary set.

No reserved default-tenant partition is needed: Polecat has no global
projections, and integer ordinals sidestep Marten's suffix-identifier
constraint entirely.

15 new integration tests (streams schema/placement/update path; document
schema shape, ordinal stamping, ForTenant, bulk, opt-out, config guards,
add/remove with both drop behaviors, shared-registry coherence across
pc_events/pc_streams/documents); docs updated.

Closes #335.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 1fe4064 into main Jul 19, 2026
7 checks passed
@jeremydmiller
jeremydmiller deleted the feat/335-tenant-partitioning-parity branch July 19, 2026 03:00
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.

Per-tenant managed partitioning parity with Marten (documents + streams)

1 participant