Expand description
The half of JSONPath that names more than one place.
Value::path answers one value and is the fast way to ask for one field,
and it refuses [*] and .. on purpose because it has nowhere to put a
second answer. This is the other half. $..price and $.items[*].sku and
$.a[0:10:2] each name a set, and the JSON.* surface is written against
sets rather than against single values: JSON.GET $..price on a document
with four prices answers four numbers, and JSON.SET with the same path
writes four times.
use yo_doc::{Path, Value, from_json};
let doc = from_json(br#"{"items":[{"sku":"a","price":3},{"sku":"b","price":5}]}"#)?;
let v = Value::new(&doc).expect("readable");
let mut hits = Vec::new();
Path::parse(b"$..price")?.select(&v, &mut hits);
let prices: Vec<i64> = hits.iter().filter_map(Value::as_int).collect();
assert_eq!(prices, [3, 5]);§What is here
The root $, a child by name written either way, [*] and .*, the
descent .., an index counting from either end, a union of indices or names
in one bracket, and a slice with an optional step. That is RFC 9535 without
its filter selector.
The filter, [?(@.price < 10)], is here too. It is the only selector whose
answer depends on the document rather than only on the path, and the only one
that can look somewhere other than where it stands, because $ inside an
expression is the whole document.
use yo_doc::{Path, Value, from_json};
let doc = from_json(br#"{"items":[{"sku":"a","price":3},{"sku":"b","price":15}]}"#)?;
let v = Value::new(&doc).expect("readable");
let mut hits = Vec::new();
Path::parse(b"$.items[?(@.price < 10)].sku")?.select(&v, &mut hits);
let cheap: Vec<&str> = hits.iter().filter_map(Value::as_text).collect();
assert_eq!(cheap, ["a"]);§What a filter’s operators mean
An operand is a path from the current node, a path from the root, or a
literal, and a path answers a set rather than one value. A comparison is true
when some pair drawn from the two sides satisfies it, so @.tags[*] == "x"
asks whether any tag is x. A side that answers nothing satisfies nothing,
which is why @.missing == 1 and @.missing < 1 are both false, and != is
the negation of the whole comparison rather than a comparison of its own,
which is why @.missing != 1 is true.
An ordering comparison only arises between two values of the same sort.
Numbers order as numbers, strings order by their characters, false is below
true, and two nulls are equal, so null >= null is true and null < null
is not. Everything else is false: a string is not below a number, and an
array or an object is not below anything at all, not even an equal one.
Equality is the whole value, and it crosses the integer and float split so
1 == 1.0, but it crosses nothing else, so 0 == false and 1 == "1" are
both false. All of that was read off RedisJSON 8.10.1 rather than off the
RFC, which leaves most of it open.
An expression with no operator in it asks whether the operand is there.
[?(@.price)] keeps the members that have a price, including the ones whose
price is null or false, because it is a question about the document and
not about the value. The one thing that is false on its own is the literal
false, so [?(false)] keeps nothing while [?(0)] and [?(null)] keep
everything.
=~ is a regular expression, and the flavour is the one in
yo_common::re, which is what ARGREP uses. RedisJSON’s is the Rust
regex crate’s, so the two agree on everything anyone writes by hand and
part company on the corners, which is a row in the divergence register.
The parentheses everyone writes around a filter are not part of it, so
[?@.a == 1] and [? (@.a == 1)] are the same filter. A filter iterates the
children of an array and of an object alike, and it works under a write as
well as a read: JSON.SET, JSON.DEL and JSON.NUMINCRBY all take one.
§The operators past the comparisons
in asks whether the left value is one of the elements of the array on the
right, and nin is its negation over both whole sides. anyof and noneof
ask whether two arrays share an element, and subsetof asks whether every
element of the left array is on the right, which makes [] a subset of
anything. size, which is also spelled sizeof, takes a bare number and is
the length of a string, an array or an object, and empty takes true or
false over the same three, so a number has neither and satisfies neither.
The right hand side of all five may be a path rather than a literal, and it
is the values that path answered that are the collection, so 3 in @.list
and @.tags anyof $.wanted both read.
The postfix methods are .length(), .count(), .min(), .max(), .sum()
and .avg(). count() is how many values the operand answered and is a
number even when that number is zero, so @.nope.count() == 0 is true, and
it is the only one of the six that answers for an operand that answered
nothing. The four aggregates want an array of numbers and answer nothing for
an empty one or for an array with anything else in it. A name that is not one
of the six answers nothing rather than refusing the path, which is what
@.p.size() does.
Arithmetic is + - * / % over numbers, * and / and % bind tighter than
+ and -, and parentheses group. Only * is an operator wherever it
stands, so @.p*2 == 6 reads the way it looks. The other four are name
characters and need their spaces: @.total-vat is a key called total-vat,
@.a+1 is a key called a+1, and @.total - vat is the subtraction. The
exception is straight after a ], where no name can be running, so
@.list[0]-1 is a subtraction with no spaces in it at all. A leading - or
+ is a sign, it answers a number or nothing so -@.name on a string
answers nothing, and one is as many as go in a row: --@.p is refused and
-(-@.p) is how a second one is written.
Arithmetic and the methods want one node, which is the one place the set rule
above does not hold. An operand that answered two answers nothing rather than
a pair of sums, so @.list[*] + 1 on a list of two is nothing and
@.list[*].length() is nothing as well. count() is outside that because
counting is what it is for. This is worth knowing before writing a wildcard
into an arithmetic operand, because what comes back is nothing rather than
what it looks like it asks for.
The postfix ~ answers the key names of an object, one string each, and
nothing at all for an array or a scalar. It is a set rather than an array
value, so @.p~ == "x" is true of any object with an x in it, and the
operators that want a collection read the whole set as one: @.p~ size 2 is
an object with two keys, and @.p~ subsetof ["x","y"] is an object with no
other key. It is a collection on the right of those operators too, so
"x" in @.p~ asks whether the object has an x. in and =~ do not take
it on the left and are false whatever is on the other side, which is the
reference’s behaviour rather than a rule with a reason behind it.
An object with no keys answers a set that is there and empty, and something
that is not an object answers no set at all, and every one of the collection
operators tells the two apart. On {} the tests @.p~ subsetof ["x"],
@.p~ empty true and @.p~ size 0 are all true, and on a number or a
missing key none of the three is.
§A path that is an expression
JSON.GET, JSON.MGET and JSON.RESP take a path that is not a way through
a document at all but a sum over one. $.a + $.b, $.list.length() and
$.o~ are all projections, and what comes back is what the expression worked
out rather than where it was found. Nothing else about them is different from
the same expression inside a filter: the same operators, the same methods,
the same one node rule. Path::is_projection tells the two apart and
Path::project runs one.
A projection never fails the way a path does. $.nope + 1 answers []
rather than raising the “path does not exist” a legacy path would, and it
answers an array even when it was written in the legacy syntax, so .a + 1
is [4] and not 4.
The first thing at the top level is a path however it is written, which is
how .a + 1 and 2 + 3 both parse. The second is a member really called
2, plus three, and answers []. Inside parentheses the ordinary rules are
back, so (2) is the number two. @ is refused up here, since there is no
current node outside a filter.
The kind of number that comes out follows the reference and is not always the
kind the arithmetic suggests. A whole number stays whole through +, -,
* and %, / is always a fraction even when it divides evenly, length()
and count() are whole, and min(), max(), sum() and avg() are
fractions. Dividing or taking a remainder by zero answers nothing.
Every other JSON.* command refuses a projection rather than reading it as a
path, so JSON.NUMINCRBY key "$.a + 1" 1 is an error and not a write
somewhere nobody asked for.
§Two orderings that are not Redis’s
Matches come back in document order, and for an object that is key order, because that is the order members are stored in. RedisJSON walks an object in the order the client wrote it. This is the same difference the JSON writer has and it is the same one row in the register.
A descent walks a node before its children, which is what every JSONPath
implementation does, so $..a on a document with an a inside an a
answers the outer one first.
Structs§
- Path
- A parsed path.
Enums§
- Computed
- One value a projection worked out.