Repository navigation
Polecat 5.34.0
The tenancy, monitoring and diagnostics release. Several of these fix answers that were not merely missing but confidently wrong.
Async projections on a tenant-partitioned store actually run (#697, #703)
On a store using UseTenantPartitionedEvents, every async projection silently never advanced. Triage found four independent defects, any one of which is enough on its own:
- The high-water height was read from the tenant's SEQUENCE, not its committed events. SQL Server reports
current_value = 1for an untouched sequence — the START value, not 0 — so a tenant whose events arrived through the shared sequence or a bulk insert read backHighestSequence = CurrentMark = 1, which every caller downstream reads as "caught up". Marten hit this identically (marten#4712). The height now comes fromMAX(seq_id)over the tenant's own partition. - No contiguity walk, so the mark would have frozen at its first persisted value (marten#4867).
MarkHighWaterForTenantAsyncwas never implemented, so JasperFx's coordinator persisted nothing through the interface'sTask.CompletedTaskdefault.DistributesAgentsPerTenantwas never overridden, so the distributor started one store-global agent instead of one per tenant.
#703 is the same failure reaching a Wolverine-managed host, which does not run Polecat's own coordinator: EventStoreAgents gates on DistributesAgentsPerTenant && database.TenantIds.Count > 0, and TenantIds was empty. PolecatDatabase.DescribeAsync now fills it from pc_tenant_partitions, re-read on every call because a tenant's partition exists only once that tenant has appended.
Two stalls a naive fix introduces are pinned by their own tests: a gap that never fills must not hold a tenant forever, and a tenant with no persisted mark must not be held at zero.
DeleteAllTenantDataAsync (#680)
Deletes every row belonging to one tenant and leaves the others intact — documents of every conjoined type, pc_events and pc_streams with their inline snapshots, DCB tag tables, natural-key lookups, full-text token tables, flat-table projections, and that tenant's pc_event_progression rows. Marten/Fisher parity.
Works on any conjoined store. The previous path required managed partitioning and dropped a partition, so a conjoined store without partitioning had no supported offboarding route. Tables are discovered from sys.columns and delete order comes from the live foreign-key graph, so a table added by a deployment this process did not configure is still erased. Runs in one transaction. It deletes data, not registration.
Document diagnostics grew a defined contract (#706, #708)
IDocumentStoreDiagnostics had five semantics wrong, and jasperfx#870 recorded all of them:
- Soft-deleted rows were returned as live. Now excluded unless asked for — and a load by id returns the row flagged rather than hidden, because hiding it says the document does not exist.
- A request naming a sub-class returned every sub-class. Now filtered, and sub-classes are listed so naming one is reachable.
- No tenant meant all tenants. Now the default tenant, with
DocumentQueryOptions.AllTenantsas the explicit opt-in (#708) — refused rather than narrowed on a store that cannot honour it. - Ids were matched as
cast(id as nvarchar(max)), which cannot use the primary-key index and made an upper-case Guid miss. Now converted to the stored identity type. EnumStoragein the descriptor was hard-coded to"AsInteger"whatever the store was configured with, so a console interpreting stored JSON was told the wrong thing confidently for every store using string enums. The serializer had exposed the true value all along.
New alongside: Subject, LoadDocumentAsync with metadata, SerializerCasing, structured Indexes/DuplicatedFields, and an IDocumentStoreDiagnosticsWriter — which goes through an ordinary session, because a write bypassing it would bypass Polecat's own versioning, soft-delete style, tenancy routing and metadata stamping.
Where/OrderBy are refused with DocumentCriteriaNotSupportedException pending jasperfx#869, rather than silently returning the unfiltered page — which a console cannot tell apart from a filter that matched everything.
Progression liveness (#705)
ShardState.LastUpdated carries the progression row's own last_updated, and the high-water detector now re-stamps it on every cycle — including the idle one, which is precisely the state a monitor cannot otherwise distinguish from "no longer maintained" (CritterWatch#1359). Costs one small single-row MERGE per detection cycle on an idle store.
Also in
- #698 — tenant-aware
CompactStreamAsyncandHasEventStore. Measured and pinned: on a conjoined store the tenant-less overload refuses another tenant's stream rather than no-opping or compacting a same-keyed default-tenant stream. - #685 (part) — the JSON index is now a Weasel schema object, so it reaches the generated script, is compared by
AssertDatabaseMatchesConfigurationAsync(), and can be refused underAutoCreate.None. This also fixed a Weasel bug (weasel#661) that silently dropped JSON indexes on migration. The full-text half of #685 is deliberately still open — the token table, its index, the trigger and the backfill remain raw DDL. - #704 — the compliance composite builder's
Add, and the async phantom-deletion fact.
Dependencies
JasperFx 2.76.1 → 2.78.0, Weasel 9.36.0 → 9.38.0.
Verification
Full suite on a freshly created SQL Server 2025 database, plus CI on the default and edge (SQL Server 2022) lanes. DocumentStoreDiagnosticsCompliance newly enrolled at 50 facts; the whole compliance namespace at 657 tests, zero failures.
🤖 Generated with Claude Code