Skip to content

Marten 9.31.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 02 Sep 13:57
· 204 commits to master since this release

Every fix in this release removes a silent wrong answer — not a crash, not an exception, but a plausible-looking result that was wrong with nothing to tell you so. A stream folded into partial aggregates. Paging that returned arbitrary pages. Monitoring that went dark rather than broken. A search reading a key that did not exist. An index that was never created.

Plus one genuinely new feature: weighted full text search with relevance ranking.

New: weighted full text indexes and ts_rank ordering

A full text index used to concatenate its members into one flat vector, so a match in a title was exactly as relevant as a match in a long description. Now it can be weighted, and ranked:

opts.Schema.For<Achievement>().WeightedFullTextIndex(idx => idx
    .Weighted(a => a.Title, TextSearchWeight.A)
    .Weighted(a => a.Tagline, TextSearchWeight.B)
    .Weighted(a => a.Description, TextSearchWeight.C));

var results = await session.Query<Achievement>()
    .Where(a => a.WebStyleSearch(term))
    .OrderByTextRank(term, TextSearchFunction.WebStyle)
    .ToListAsync();

The rank resolves the same tsvector the Where clause matched on, read from the index definition. That is the load-bearing design constraint rather than an implementation detail: a rank computed over a different vector than the filter matched on returns rows in an order that looks plausible and means nothing — a far quieter failure than returning the wrong rows.

Requires Weasel 9.29.0 (weasel#541), which made it possible to index an expression that is already a tsvector. See the full text documentation for the costs worth knowing first — GIN cannot order, so this is a post-filter sort; and adding weights to an existing index drops and recreates it.

Silent wrong answers, fixed

Async projections folded one stream into several partial aggregates (#5305, originally #4085). When a stream's events disagreed about tenant_id, the daemon sliced it per tenant and applied the pieces over each other, so an Apply saw a document with every property at its default. Marten had set ForceSingleTenancy since the original fix, but TenantedEventSlicer honoured that flag on only one of its two overloads — and the async daemon reaches the other one. The flag was being set on precisely the path that could not read it. Fixed upstream in JasperFx 2.58.0.

OrderBy after a GroupJoin/SelectMany was dropped from the SQL (#5311). Silently, and with Skip/Take it was worse: OFFSET and LIMIT were emitted while the ordering was not, and unordered paging in PostgreSQL has no stable row order — so rows repeat across pages while others never appear. Where on a bare-side selector and Count over a join were wrong in the same surface and are fixed too.

A store with monitoring off dropped the monitoring columns another store had added (#5309). Two DocumentStores over one DatabaseSchemaName that disagreed about EnableExtendedProgressionTracking kept stripping each other's mt_event_progression columns — last writer wins, silent on both sides. A service beside a seeder or a reporting job is an ordinary arrangement, and neither was doing anything wrong. The shape of that table no longer depends on configuration at all.

Full text search rewrote JSON keys, not just the column (#5314, thanks @mlh758). A member whose serialized name contained data produced d.data ->> 'd.data' — a key that does not exist. Two quiet consequences: that member contributed nothing to the search, and the expression no longer matched the index, so the GIN index could not serve the query either.

A second full text index over different members was silently discarded (#5315). Index names derive from the table rather than from the members, so two FullTextIndex() calls collided by construction and the second was thrown away — never registered, never created, never searched. Ambiguity is now refused with an explanatory exception instead of resolved by declaration order.

A DCB boundary aggregate failed far from its cause (JasperFx 2.60.0). An identity-less [BoundaryAggregate] folded by Evolve(IEvent) got no evolver and no diagnostic, surfacing much later as FetchForWritingByTags<T> throwing "No source-generated dispatcher found" — naming neither the type nor the reason. Now it generates, and a [BoundaryAggregate] with nothing to fold events with is reported as JFXEVT007.

A DCB tag version was captured after the events were read (#5300). Two batched statements do not share a READ COMMITTED snapshot, so the version could reflect a concurrent append. Captured before the read now.

Also in this release

  • A projection-run CLI command arrives with JasperFx 2.60.0 and needs no Marten change — every host referencing JasperFx.Events picks it up. It replays one projection over a stream slice or DCB tag match and prints per-event before/after state, writing nothing.
  • Bug_5268's concurrent index test no longer depends on how much unrelated transaction load happens to be in flight (#5308), so validating a dependency bump by running the whole suite at once is trustworthy again.
  • BuildSlicer now agrees with FetchAsyncPlan about global aggregates (#5307). Alignment rather than a fix — no reachable corruption depended on it, and the reasoning is written down at the call site so the question does not have to be reconstructed next time.

Upgrade notes

OrderByFragment.Expressions changed from List<string> to List<ISqlFragment>. A public API break, deliberately taken: an ordering could not carry a parameter while the clause was a list of strings, which is why the older ngram ranking inlines its search term rather than binding it. Nothing outside Marten's own LINQ internals is likely to touch this type, but it is a compile break if you did.

Ambiguous full text index selection now throws. If a document has two full text indexes sharing a regConfig, Marten refuses the query rather than picking one. This converts a silent wrong answer into an exception — which is the point, and also the kind of thing that shows up in CI rather than code review. Give the indexes different regConfig values, or register a single index covering every member you want to search.

EnableExtendedProgressionTracking still defaults to false. It briefly defaulted to true during this development cycle and that was reversed before release: turning it on by default would silently start writing six columns for every existing store that never asked for monitoring. The opt-in is unchanged.

Dependency floors: JasperFx 2.60.0, Weasel 9.29.0.