Predicate

sealed interface Predicate

What a query asks of a document.

Two rules give the tree its meaning, and both are consequences of how a path is walked rather than choices made here.

A leaf is existential over the values at its path. $.tags[*] eq "a" holds for a document with any tag equal to a, because the walk reports one value per occurrence and a leaf asks whether any of them satisfies it. A "for all" reading would need a second walk and a second definition of what a path means, which is the thing this engine most consistently refuses.

Each leaf is existential independently, so a conjunction over [*] is not correlated. This is the consequence of the rule above that surprises people, and it is a defined semantics rather than an oversight:

{"items":[{"sku":"A","qty":1},{"sku":"B","qty":5}]}    matches
{"items":[{"sku":"A","qty":5},{"sku":"B","qty":1}]} matches

and($.items[*].sku eq "A", $.items[*].qty eq 5)

The first document matches although no single element satisfies both: the sku comes from element 0 and the qty from element 1. Each leaf is settled by any value at its own path, and the conjunction is then over the two per-leaf answers rather than over elements. The indexed and unindexed answers are identical, which is the invariant holding exactly — an index changed the speed and not the answer.

To correlate, ask for it: ElemMatch. elemMatch(path("$.items[*]"), and(…)) holds only when one element satisfies the whole of its operand. It is a separate node rather than a mode of And precisely because the reading above is a defined semantics that must not change: an existing conjunction means what it has always meant, and the correlated question is a different question with a different spelling.

Splitting the document is still the other answer, and still a good one. One key per element — order:00123#item:00007 — makes each element a document, so the conjunction is over one document and the correlation is exact. It costs no engine feature, an ordered-key LSM reassembles the parent in one contiguous range scan, and $.items[*].sku collapses to $.sku. Which to reach for is a modelling decision: split when the elements are the things you query, and use ElemMatch when the document is.

Not is the document-level complement of that, so not($.tags[*] eq "a") holds for a document whose tags are all something else, for one whose tags are numbers, and for one with no tags at all. Which values count as comparable is type bracketing — a numeric predicate matches numbers only — and that rule has exactly one definition in the engine, in ColumnPredicate.matches, which every leaf lowers to.

There is deliberately no NE operator. not(path("$.a") eq 1) is the only spelling, so negation has one meaning; a separate NE would have to decide for itself what an absent path or a string-valued $.a does, and the day it disagreed with NOT EQ nothing would say which was right.

Build one with the DSL rather than by hand:

val predicate = and(
path("$.team") eq "analytics",
path("$.score") ge 10,
not(path("$.retired").exists()),
)

Inheritors

Types

Link copied to clipboard
data class And(val operands: List<Predicate>) : Predicate

Every operand holds.

Link copied to clipboard
data class AnyOf(val path: CatalogPath, val values: List<QueryValue>) : Predicate

Some value at path equals one of values. The IN case.

Link copied to clipboard
data class Compare(val path: CatalogPath, val operator: Comparison, val value: QueryValue) : Predicate

Some value at path compares operator against value.

Link copied to clipboard
data class ElemMatch(val path: CatalogPath, val operand: Predicate) : Predicate

Some single element at path satisfies operand, whose paths are relative to that element.

Link copied to clipboard
data class Exists(val path: CatalogPath) : Predicate

path carries at least one value, the JSON null included.

Link copied to clipboard
data object False : Predicate

Matches nothing. What an empty disjunction folds to.

Link copied to clipboard
data class IsNull(val path: CatalogPath) : Predicate

Some value at path is the JSON null.

Link copied to clipboard
data class Not(val operand: Predicate) : Predicate

The operand does not hold — of the document, not of a value. See Predicate.

Link copied to clipboard
data class Or(val operands: List<Predicate>) : Predicate

At least one operand holds.

Link copied to clipboard
data object True : Predicate

Matches every document. What an empty conjunction folds to.

Functions

Link copied to clipboard
infix fun Predicate.and(other: Predicate): Predicate

Both hold.

Link copied to clipboard
infix fun Predicate.or(other: Predicate): Predicate

Either holds.

Link copied to clipboard

Every path this predicate mentions of the document, deduplicated, in the order first seen.