feat: support subrange expressions in jsonpathv2 (#3036)

Support for expressions like `$.arr[1:-2]`.

The change applies to our own implementation of jsonpath,
jsoncons already supports this format.

Signed-off-by: Roman Gershman <roman@dragonflydb.io>
This commit is contained in:
Roman Gershman 2024-05-11 22:30:55 +03:00 committed by GitHub
parent b7fdc41d2e
commit a9956e9723
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 65 additions and 3 deletions

View file

@ -135,6 +135,10 @@ TEST_F(JsonTest, Path) {
jsonpath::json_query(j2, "$.field[-1]", [&](const std::string& path, const json& val) {
EXPECT_EQ(5, val.as<int>());
});
jsonpath::json_query(j2, "$.field[-6:1]", [&](const std::string& path, const json& val) {
EXPECT_EQ(1, val.as<int>());
});
}
TEST_F(JsonTest, Delete) {

View file

@ -47,6 +47,7 @@ using namespace std;
%token
LBRACKET "["
RBRACKET "]"
COLON ":"
LPARENT "("
RPARENT ")"
ROOT "$"
@ -86,6 +87,7 @@ identifier: UNQ_STR
bracket_index: WILDCARD { $$ = PathSegment{SegmentType::INDEX, IndexExpr::All()}; }
| INT { $$ = PathSegment(SegmentType::INDEX, IndexExpr($1, $1)); }
| INT COLON INT { $$ = PathSegment(SegmentType::INDEX, IndexExpr::HalfOpen($1, $3)); }
function_expr: UNQ_STR { driver->AddFunction($1); } LPARENT ROOT relative_location RPARENT
%%

View file

@ -45,6 +45,7 @@
"$" return Parser::make_ROOT(loc());
".." return Parser::make_DESCENT(loc());
"." return Parser::make_DOT(loc());
":" return Parser::make_COLON(loc());
"[" return Parser::make_LBRACKET(loc());
"]" return Parser::make_RBRACKET(loc());
"*" return Parser::make_WILDCARD(loc());

View file

@ -479,4 +479,46 @@ TYPED_TEST(JsonPathTest, Mutate) {
}
}
TYPED_TEST(JsonPathTest, SubRange) {
TypeParam json = ValidJson<TypeParam>(R"({"arr": [1, 2, 3, 4, 5]})");
ASSERT_EQ(0, this->Parse("$.arr[1:2]"));
Path path = this->driver_.TakePath();
ASSERT_EQ(2, path.size());
EXPECT_THAT(path[1], SegType(SegmentType::INDEX));
vector<int> arr;
auto cb = [&arr](optional<string_view> key, const TypeParam& val) {
ASSERT_FALSE(key);
arr.push_back(to_int(val));
};
EvaluatePath(path, json, cb);
ASSERT_THAT(arr, ElementsAre(2));
arr.clear();
ASSERT_EQ(0, this->Parse("$.arr[0:2]"));
path = this->driver_.TakePath();
EvaluatePath(path, json, cb);
ASSERT_THAT(arr, ElementsAre(1, 2));
arr.clear();
ASSERT_EQ(0, this->Parse("$.arr[2:-1]"));
path = this->driver_.TakePath();
EvaluatePath(path, json, cb);
ASSERT_THAT(arr, ElementsAre(3, 4));
arr.clear();
ASSERT_EQ(0, this->Parse("$.arr[-2:-1]"));
path = this->driver_.TakePath();
EvaluatePath(path, json, cb);
ASSERT_THAT(arr, ElementsAre(4));
arr.clear();
ASSERT_EQ(0, this->Parse("$.arr[-2:-2]"));
path = this->driver_.TakePath();
EvaluatePath(path, json, cb);
ASSERT_THAT(arr, ElementsAre());
arr.clear();
}
} // namespace dfly::json

View file

@ -55,14 +55,19 @@ IndexExpr IndexExpr::Normalize(size_t array_len) const {
return IndexExpr(1, 0); // empty range.
IndexExpr res = *this;
auto wrap = [=](int negative) {
unsigned positive = -negative;
return positive > array_len ? 0 : array_len - positive;
};
if (res.second >= int(array_len)) {
res.second = array_len - 1;
} else if (res.second < 0) {
res.second = res.second % array_len;
res.second = wrap(res.second);
DCHECK_GE(res.second, 0);
}
if (res.first < 0) {
res.first = res.first % array_len;
res.first = wrap(res.first);
DCHECK_GE(res.first, 0);
}
return res;

View file

@ -55,9 +55,10 @@ class AggFunction {
int valid_ = -1;
};
// Bracket index representation
// Bracket index representation, IndexExpr is a closed range, i.e. both ends are inclusive.
// Single index is: <I, I>, wildcard: <0, INT_MAX>,
// [begin:end): <begin, end - 1>
// IndexExpr is 0-based, with negative indices referring to the array size of the applied object.
struct IndexExpr : public std::pair<int, int> {
bool Empty() const {
return first > second;
@ -68,7 +69,14 @@ struct IndexExpr : public std::pair<int, int> {
}
using pair::pair;
// Returns subrange with length `array_len`.
IndexExpr Normalize(size_t array_len) const;
// Returns IndexExpr representing [left_closed, right_open) range.
static IndexExpr HalfOpen(int left_closed, int right_open) {
return IndexExpr(left_closed, right_open - 1);
}
};
class PathSegment {