Skip to content

Polecat 5.34.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 30 Sep 16:34
· 22 commits to main since this release

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 = 1 for an untouched sequence — the START value, not 0 — so a tenant whose events arrived through the shared sequence or a bulk insert read back HighestSequence = CurrentMark = 1, which every caller downstream reads as "caught up". Marten hit this identically (marten#4712). The height now comes from MAX(seq_id) over the tenant's own partition.
  • No contiguity walk, so the mark would have frozen at its first persisted value (marten#4867).
  • MarkHighWaterForTenantAsync was never implemented, so JasperFx's coordinator persisted nothing through the interface's Task.CompletedTask default.
  • DistributesAgentsPerTenant was 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.AllTenants as 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.
  • EnumStorage in 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 CompactStreamAsync and HasEventStore. 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 under AutoCreate.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