Check whether a row can name the locations that matched it - #11
Merged
Conversation
§10.2 proposes that QueryCursor hand back the row and the locations that matched it, and records itself as blocked on a ruling: whether the executor may know which leaf matched. The check was made before asking for one, and the ruling is moot. Grant the permission in full and the feature still cannot be specified. "The locations that matched" is a partial question, and the partiality falls along the line the engine already draws between a predicate satisfied by something and one satisfied by the absence of something. A positive leaf has a witness and possibly several. A positive elemMatch has one, the element, and possibly several. A disjunction has witnesses only from the operands that hold. A negated leaf has none. A negated elemMatch has none and is satisfied vacuously by a document with no elements. not(exists()) has none most obviously of all, because the match is the absence. An empty witness set is this engine's standing failure mode arriving somewhere new. Empty because the match rested on an absence, and empty because nothing matched, are different facts with one spelling -- which index-and-query.md already refuses by name where an ordinal source must never answer emptily, since "no matches here" and "I cannot say" cannot be told apart. The positive half is not the easy half either. A leaf is existential and DocumentMatcher settles it on the first value that satisfies it, then folds booleans, so by the time a row exists the engine holds one bit per leaf and no locations at all. Any answer the cursor gave would be a choice -- first, all, arbitrary -- presented as a fact, and reporting all of them means abandoning the short-circuit on the per-row path of every query including the ones that never ask. MatchWitnessTest drives every row of that table through the engine's own evaluator and CatalogPath.forEachNodeIn, so it is data rather than a claim. Two documents match not($.a eq 10) for different reasons: one holds a node at $.a that is not a witness because it fails the leaf, the other holds no node at all. The refusal rests on something that works, and the test asserts that too. One CatalogPath parsed once and walked per row answers all the locations rather than the first, is the same walk the engine uses, and never has to decide what a negation witnesses -- because the caller asks about a path, where every match has a location by construction, rather than about a predicate, where they do not. 0.2.0 shipped it and the README already teaches it as the idiom. So §10.2 is not a missing feature. It is a question asked of the wrong object: a predicate is a boolean over a document, a path is a set of locations, and only the second can be asked where it matched. That closes both entries this file had filed as needing a decision rather than work, and both were settled by a test instead. When an item is recorded as blocked on a ruling, ask first whether the thing being ruled on is well-defined: a ruling grants permission and cannot make a partial question total. No format change, no .api dump rewritten, no main source changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
§10.2 proposes that
QueryCursorhand back the row and the locations that matched it, so a caller does not re-expand a path per row. It records itself as blocked on a ruling: whether the executor may know which leaf matched.The check was made before asking for one — R.2's lesson, that a soundness check is cheaper than a measurement and strictly precedes it — and the ruling is moot. Grant the permission in full and the feature still cannot be specified.
The finding: partial, not forbidden and not merely expensive
"The locations that matched" is a partial question, and the partiality falls exactly along the line the engine already draws between a predicate satisfied by something and one satisfied by the absence of something.
elemMatchelemMatchnot(exists())MatchWitnessTestdrives every row throughDocumentMatcherandCatalogPath.forEachNodeIn, so the table is data rather than a claim. Two documents matchnot($.a eq 10)for different reasons — one holds a node at$.athat is not a witness because it fails the leaf, the other holds no node at all — and a feature reporting locations has to answer something for both.An empty witness set is this engine's standing failure mode arriving somewhere new. Empty because the match rested on an absence, and empty because nothing matched, are different facts with one spelling.
.claude/rules/index-and-query.mdalready refuses exactly this shape by name, where an ordinal source must never answer emptily because "no matches here" and "I cannot say" cannot be told apart.And the positive half is not the easy half. A leaf is existential and
DocumentMatchersettles it on the first value that satisfies it, then folds booleans — so by the time a row exists the engine holds one bit per leaf and no locations whatsoever. Any answer the cursor gave would be a choice — first, all, arbitrary — presented as a fact. Reporting all of them means abandoning the short-circuit on the per-row path of every query, including the ones that never ask.The refusal rests on something that works
The last test asserts the alternative rather than just naming it: one
CatalogPath, parsed once,forEachNodeInper row. It answers all the locations rather than the first, it is the same walk the engine uses, and it never has to decide what a negation witnesses — because the caller asks about a path, where every match has a location by construction, rather than about a predicate, where they do not. 0.2.0 shipped it and the README already teaches it as the idiom;rabosh-jsonpath's filter is the same move with a condition attached.So §10.2 is not a missing feature — it is a question asked of the wrong object. A predicate is a boolean over a document; a path is a set of locations. Only the second can be asked where it matched, and the second is already public.
What this closes, and the generalisation
This was the last of the four items that existed because a document's interior was not addressable (10.1, 10.2, 10.5, 10.6). It closed last and differently: not by evidence about cost, but by finding the feature could not be specified.
It is also the second entry filed as "a decision rather than work" to be settled by a test instead — 10.3 was the first, and looked like a matter of taste until a test gave it a factual answer. When an item is recorded as blocked on a ruling, ask first whether the thing being ruled on is well-defined. A ruling grants permission; it cannot make a partial question total. Both had a check available that cost an afternoon and neither had been taken, because "this needs a decision" reads like a completed diagnosis. It is not one.
Gate
./gradlew buildgreen,checkKotlinAbigreen with no dump rewritten.MatchWitnessTest: 6 cases, 0 failures..apientry, no main source changed — the whole of this PR is one test file. A refusal's deliverable is the evidence that it was refused correctly, which is the shapeCompositeTermPrefixTesttook for R.2.🤖 Generated with Claude Code