Repository navigation
Marten 9.43.0
Bumps the JasperFx family to 2.78.0 and lands four issues, one of them a data-corruption fix worth reading even if you skip the rest.
Fixes
#5539 — a zero-event quick append could permanently wedge a stream. mt_quick_append_events called with zero events read mt_streams.version without a lock and ended with a blind write-back, so under READ COMMITTED it could overwrite a concurrent appender's version and leave the stream row below its own events. Every later append then computed a version that already existed and failed on pk_mt_events_stream_and_version — permanently, with no recovery short of repairing the row by hand. A non-empty append could never do this: its first insert collides on that same primary key and rolls back, which is why only the empty call was exposed. The write-back is now skipped when nothing was appended, and uses greatest() so it can never lower a version whoever calls it.
Field context: a production console on 9.32.0 sat at mt_streams.version 608676 against max(mt_events.version) 608678 for 42 days, roughly 28 failed appends a minute. If you have a stream failing every append on that constraint, this is likely why.
#5541 — ShardState.LastUpdated, liveness rather than progress. Sequence cannot distinguish caught up with nothing new from no longer maintained: both hold the same number forever. The progression row's last_updated now reaches ShardState and ProjectionProgressRow, and — the part that needed a product change — an idle high-water cycle now restamps the HighWaterMark row. Previously an idle cycle issued no SQL at all, so that stamp froze at the last real mark advance and read as "abandoned weeks ago" on a perfectly healthy store. The restamp never touches last_seq_id, so it cannot nudge the mark.
Diagnostics surface (monitoring consoles)
#5543 — IDocumentStoreDiagnostics adopts the defined contract (#870). Five behaviours were previously "whatever the store happened to do", and Marten was on the wrong side of all five:
| Before | Now | |
|---|---|---|
| Soft deletes | returned as live | excluded unless asked; a load by id returns the row flagged |
| Hierarchies | every sub-class in the table | filtered to the requested type; each row reports its own |
| Missing tenant | all tenants | the default tenant |
"" tenant |
a tenant literally named "" |
the default tenant |
| Ids | id::text = @id |
converted to the stored identity type — uses the PK index, matches an upper-case Guid |
Also: LoadDocumentAsync returning StoredDocument with metadata, populated DocumentQueryResult.Documents, a new IDocumentStoreDiagnosticsWriter (save-from-JSON and delete through a normal session, with an expected-version guard), DocumentStoreUsage.SerializerCasing, and structured index / duplicated-field descriptors. Query criteria Marten cannot yet translate are refused rather than silently ignored.
#5544 — DocumentQueryOptions.AllTenants. The explicit replacement for the all-tenants read that #5543 removed. Conjoined tenancy reads every tenant, ordered by tenant then id so paging cannot repeat a row. A store spreading tenants across databases refuses rather than quietly returning only the default tenant's rows.
TenantId now means the default tenant, not every tenant. If you relied on the old behaviour, set AllTenants = true.
Also
#5542 — asynchronous aggregate events are ordered by stream sequence.
Internals: a diagnostic document read now resolves a tenant's database through TryFindDatabase rather than FindOrCreateDatabase, so asking a read-only question about an unknown tenant can no longer provision that tenant.