Refuse the query-language filter, on a document that satisfies P and not-P - #12
Merged
Merged
Conversation
…not-P
§10.1b proposes a filter predicate in the query language. The entry recorded it
as a decision the module layout was arranged to keep open: a query-language
filter would put RFC 9535's comparison semantics in front of the planner, and
rabosh-jsonpath sits beside the chain precisely so that cannot happen by
refactoring.
The decision did not have to be taken, because the feature is not well-formed.
Same shape as §10.2 and found the same way: check whether the thing is definable
before deciding whether it is allowed.
A Predicate is a boolean over a document. A filter selector is a selector over a
nodelist. The only bridge between them is "the document matches when the
nodelist is non-empty", which is the one anybody building this would reach for,
and it does not work. On {"tags":["a","b"]} the filter $.tags[?@ == 'b'] selects
the b and its negation $.tags[?!(@ == 'b')] selects the a. Both non-empty, so P
and not-P are both true of one document, and where(f) and where(not(f)) would
return the same row. No planner change or index could repair that, because the
defect is in the meaning rather than in the evaluation.
Neither design is at fault, which is why neither can adopt the other. A filter's
! negates a test about one node while the selector stays existential over the
rest; a Predicate's not negates the answer for the document, after the
existential has been folded. Both quantify correctly, over different things.
The second disagreement is the one a user meets first: @.tags == 'b' compares the
array with a string and is false, where path("$.tags[*]") eq "b" is existential
over the elements and is true. The third agrees -- an absent field compares false
and its negation selects every child, matching what the engine's not does for a
missing path -- and that is the dangerous part rather than the reassuring one. A
bridge that is right most of the time is the shape that ships.
FilterIsNotAPredicateTest pins this in rabosh-jsonpath and needs nothing else,
which is itself part of the answer: the differential that would compare the two
semantics directly cannot be written anywhere in this repository without
acquiring the very edge the module layout forbids. So what is pinned is the half
that decides it. The storage side's complement is already in DocumentMatcherTest,
where not(path("$.tags[*]") eq "b") is false on that document, exactly as a
document predicate must be. Two files, no edge, and the pair is the argument.
This is index-and-query.md's "a negated leaf is never a flipped operator"
arriving from outside, in a syntax users already know. That rule notes the
rewrite is the most natural-looking simplification in the layer and that it
deletes documents from a result silently.
What would revive it is a spelling that says which way it quantifies, and that
is what elemMatch already is: the filter selector expressed as a document
predicate, with the existential written down and the negation defined over the
document. 0.2.0 built it under a different name, and the name is the part that
carries the semantics.
That closes the last of phase 20 §10. 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.1b proposes a filter predicate in the query language. The entry recorded it as a decision the module layout was arranged to keep open: a query-language filter would put RFC 9535's comparison semantics in front of the planner, and
rabosh-jsonpathsits beside the chain precisely so that cannot happen by refactoring.The decision did not have to be taken, because the feature is not well-formed. Same shape as §10.2 and found the same way — check whether the thing is definable before deciding whether it is allowed.
The counterexample is two lines
A
Predicateis a boolean over a document. A filter selector is a selector over a nodelist. The only bridge between them is "the document matches when the nodelist is non-empty" — the obvious one, and the one anybody building this would reach for.{"tags":["a","b"]}$.tags[?@ == 'b']$['tags'][1]$.tags[?!(@ == 'b')]$['tags'][0]A filter and its negation both select from the same document. Under the bridge,
Pand¬Pare both true of it, andwhere(f)andwhere(not(f))would return the same row. That is not a subtlety to document around: the law of the excluded middle is whatnot(…)means, and no planner change or index could repair it, because the defect is in the meaning rather than in the evaluation.Neither design is at fault, which is exactly why neither can adopt the other. A filter's
!negates a test about one node while the selector stays existential over the rest; aPredicate'snotnegates the answer for the document, after the existential has been folded. Both quantify correctly, over different things.Two more shapes, and the third is the dangerous one:
@.tags == 'b'compares the array with a string and is false, wherepath("$.tags[*]") eq "b"is existential over the elements and is true. Same document, same apparent question, opposite answers — and RFC 9535's answer is the right one for RFC 9535.notdoes for a missing path. A bridge that is right most of the time is the shape that ships.Where it is pinned, and why that location is part of the answer
FilterIsNotAPredicateTestlives inrabosh-jsonpathand needs nothing else — the differential that would compare the two semantics directly cannot be written anywhere in this repository without acquiring the very edge the module layout forbids. So what is pinned is the half that decides the question: the RFC side alone shows a filter and its negation both selecting. The storage side's complement is already pinned inDocumentMatcherTest, wherenot(path("$.tags[*]") eq "b")is false on that document, exactly as a document predicate must be. Two files, no edge, and the pair is the whole argument.This is
.claude/rules/index-and-query.md's "a negated leaf is never a flipped operator" arriving from outside, in a syntax users already know. That rule notes the rewrite is the most natural-looking simplification in the layer and that it deletes documents from a result silently.What would revive it: a spelling that says which way it quantifies — which is what
elemMatchalready is.elemMatch(p, operand)is the filter selector expressed as a document predicate, with the existential written down and the negation defined over the document. 0.2.0 built the feature under a different name, and the name is the part that carries the semantics.This closes phase 20 §10
All nine tracked items are now settled: four built, four refused on evidence, one answered by a test.
The pattern worth keeping is that three of the refusals were the same defect — R.2's prefix scan returned a subset where a superset was required, §10.2's witness set was empty for negated shapes, and §10.1b's bridge made a filter and its negation both true. Each was filed as needing a decision or a measurement; each turned out to be a question with no well-formed answer, detectable in an afternoon. The three adversarial shapes that did all the work were the same every time: an empty result, a negation, and a value of the wrong type.
Gate
./gradlew buildgreen,checkKotlinAbigreen with no dump rewritten,-p build-logic checkgreen.FilterIsNotAPredicateTest: 3 cases, 0 failures, confirmed under--rerun-tasks..apientry, no main source changed — one test file, as a refusal's deliverable should be.🤖 Generated with Claude Code