Skip to content

Check whether a row can name the locations that matched it - #11

Merged
aoreshkov merged 1 commit into
mainfrom
check-the-match-witness
Aug 9, 2026
Merged

Check whether a row can name the locations that matched it#11
aoreshkov merged 1 commit into
mainfrom
check-the-match-witness

Conversation

@aoreshkov

Copy link
Copy Markdown
Owner

§10.2 proposes that QueryCursor hand 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.

shape is there a location that justifies the match?
a positive leaf yes, and possibly several — the row names none
a positive elemMatch yes, the element — and possibly several
a disjunction yes, but only from the operands that hold
a negated leaf no. There is nothing to point at
a negated elemMatch no, and it is satisfied vacuously by a document with no elements
not(exists()) no, most obviously: the match is the absence

MatchWitnessTest drives every row through DocumentMatcher and CatalogPath.forEachNodeIn, so the table 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 — 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.md already 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 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 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, forEachNodeIn per 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 build green, checkKotlinAbi green with no dump rewritten.
  • MatchWitnessTest: 6 cases, 0 failures.
  • No format change, no .api entry, 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 shape CompositeTermPrefixTest took for R.2.

🤖 Generated with Claude Code

§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>
@aoreshkov
aoreshkov merged commit 3dde0cc into main Aug 9, 2026
2 checks passed
@aoreshkov
aoreshkov deleted the check-the-match-witness branch August 9, 2026 14:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant