Repository navigation
Untyped select: reads are projected after the merged sort - #6839
Conversation
Each provider projected an untyped select: read to dictionaries before the cross-provider merge, and ClipMergedInitial can only read a sort key, a path tiebreak or a dedup key off a MeshNode, so a query served by two providers was clipped in provider-arrival order and returned one provider's top N. MeshQuery now runs such a read through the providers and the merge as nodes and projects only the final sorted, clipped window. Closes #6830 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new node-based route replaces relevance ordering with path ordering when a storage provider omits scores.
1 open finding
What changed in this PR
Defers untyped select: projection until after MeshWeaver merges, sorts, and clips query results, addressing #6830.
Changes:
- Routes projected untyped queries through the node-based merge.
- Adds two-provider ordering and projection regression tests.
- Documents projection timing.
| File | Description |
|---|---|
| test/MeshWeaver.Hosting.Test/UntypedProjectionMergedSortTest.cs | Tests merged ordering, selected fields, and typed-result parity. |
| src/MeshWeaver.Hosting/Persistence/Query/MeshQuery.cs | Projects results after merging on both query entry points. |
| src/MeshWeaver.Documentation/Data/DataMesh/QuerySyntax.md | Explains projection after sorting and clipping. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (typeof(T) == typeof(object) && Projects(request)) | ||
| return (IObservable<QueryResultChange<T>>)(object)ProjectAfterMerge(Query<MeshNode>(request), request); |
There was a problem hiding this comment.
Confirmed and fixed in 6bda942. The provider ranked free-text matches by fuzzy score before its load cap, but it left Scores null, so ClipMergedInitial scored every row 0 and returned path order. That was already true for TYPED free-text reads, and routing untyped select: reads through the node merge extended it to them. The root fix is in StorageAdapterMeshQueryProvider. RunRankedQueryNodes takes the score off the matched node before any projection or content drop and carries it on the Initial frame, aligned with the rows that pass the type filter. Live deltas are unchanged.
Regressions in UntypedProjectionMergedSortTest use a fixture where relevance order is the reverse of path order. The expected order comes from QueryEvaluator.GetFuzzyScore itself, with a guard that it disagrees with path order.
- Core entry (
IMeshQueryCore.Query), typed and untyped, with and withoutselect:. Skip=1 Limit=1pages the relevance order.- The public entry (
MeshQuery.Query<T>(request)) needs a hub for its serializer options. It is pinned at the frame it merges: the securedIMeshQueryProvider.Query<T>Initial carries one descending score per row, the same for typed and untyped reads.
Negative control: with Scores = null restored, 5 of the 10 cases fail (for example, expected needle, the needle somewhere, … but got the path order a long haystack…, the needle somewhere, needle). With the fix all 10 pass, and so do the adjacent query suites (PagedQueryTotalOrder, MeshQueryMergeContract, ContentFieldSort, FilterOnlyQueryClipsNewestFirst).
Test Results 15 files - 11 15 suites - 11 50m 24s ⏱️ - 9m 33s Results for commit 6bda942. ± Comparison against base commit cab0ae9. This pull request removes 1636 and adds 19 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
…ection-merged-sort
…the scores it ranked by StorageAdapterMeshQueryProvider ranked free-text matches by fuzzy score before its load cap but left QueryResultChange.Scores null, so MeshQuery.ClipMergedInitial scored every row 0 and answered path order (weaker matches first, Skip paging the wrong window). The score is now taken off the matched node before projection and carried on the Initial frame. Regressions for the core entry (typed + untyped, with and without select:), the secured provider frame the public entry merges, and Skip; negative control: Scores = null fails 5 of 10. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Closes #6830
Root cause
An untyped read (
Query<object>) carryingselect:was projected to dictionaries by each provider, before the cross-provider merge saw a row.MeshQuery.ClipMergedInitialcan only read a sort key, a path tiebreak or a dedup key off aMeshNode, so its authoritative sort was skipped (hits.All(h => h.Item is MeshNode)) and the window was taken in score, then provider-ARRIVAL order — a dictionary has no path to tiebreak on.select:path,name sort:<anything> limit:Nserved by two providers (a store plus the seeded static nodes, or several partitions) returned one provider's top N. Teaching the merge to read dictionaries cannot work: the sort key need not be among the selected fields.Fix
MeshQueryruns an untyped read that carries aselect:through the providers and the merge as nodes (Query<MeshNode>on both the public and theIMeshQueryCoresurface) and projects only the final, sorted and clipped window (and every later delta) toselect:dictionaries — the issue's suggested direction. The SQL providers still narrow their columns to theselect:list (plus a content sort key) when asked for nodes, so nothing extra is read. An untyped read WITHOUTselect:is unchanged (the providers already hand back nodes), which keepsFilterOnlyQueryClipsNewestFirstTest.AMixedBatchKeepsEveryItemUnderTheDefaultOrderingmeaningful. No provider changes, so no paired Plugins PR: the Postgres/Cosmos/Snowflake providers'projectSelectpath is simply no longer taken through the merge forQuery<object>.Tests
MeshWeaver.Hosting.Test/UntypedProjectionMergedSortTest— a storage-backed provider and the static-node provider (the pairing every portal runs), names interleaved so the global window takes rows from both:select:path,name sort:name-desc limit:3→ F, E, D;sort:name→ A, B, C;select:name sort:path-desc limit:2(sort key not selected) → A, CQuery<MeshNode>, already right since Query sort on a content field: typed key, nulls last #6704) answer row for rowNegative control (
MeshQuery.csreverted to main, test unchanged):total: 5, failed: 5, succeeded: 0, e.g.Expected collection {"F", "D", "B"} to equal {"F", "E", "D"} because 'namespace:Book nodeType:Chapter select:path,name sort:name-desc limit:3' over two providers answers the mesh's window in the requested order, not one provider's.Fix restored:
total: 5, failed: 0; fullMeshWeaver.Hosting.Testtotal: 1340, failed: 0, skipped: 1; fullMeshWeaver.Graph.Testtotal: 3164, failed: 0(run on an earlier, broader cut that deferred EVERY untyped read; the final cut narrows it toselect:reads).Doc:
Doc/DataMesh/QuerySyntax→select("The untyped projection happens AFTER the merged sort").Related: #6704 / MeshWeaver.Plugins#3587 (typed callers).
Recycle on deploy: none.
Pairs-with: none — private members only.
🤖 Generated with Claude Code