Skip to content

Commit

Permalink
feat: support subrange expressions in jsonpathv2 (#3036)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
romange committed May 11, 2024
1 parent b7fdc41 commit a9956e9
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 3 deletions.
4 changes: 4 additions & 0 deletions src/core/json/json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions src/core/json/jsonpath_grammar.y
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ using namespace std;
%token
LBRACKET "["
RBRACKET "]"
COLON ":"
LPARENT "("
RPARENT ")"
ROOT "$"
Expand Down Expand Up @@ -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
%%
Expand Down
1 change: 1 addition & 0 deletions src/core/json/jsonpath_lexer.lex
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
42 changes: 42 additions & 0 deletions src/core/json/jsonpath_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
9 changes: 7 additions & 2 deletions src/core/json/path.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 9 additions & 1 deletion src/core/json/path.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down

0 comments on commit a9956e9

Please sign in to comment.