fix: JSON.STRAPPEND (#3597)

* fix: JSON.STRAPPEND

JSON.STRAPPEND was completely broken.

First, it accepts exactly 3 arguments, i.e. a single value to append.
Secondly, the value must be a json string, not the regular string. Meaning it must be in double quotes.
So, before we parsed: `JSON.STRAPPEND key $.field bar` and now we parse:
`JSON.STRAPPEND key $.field "bar"`

In addition fixed the behavior of JSON.STRLEN to return "no such key" error in case the
json key does not exist and path is specified.
---------

Signed-off-by: Roman Gershman <roman@dragonflydb.io>
This commit is contained in:
Roman Gershman 2024-08-29 15:29:34 +03:00 committed by GitHub
parent 88229cf365
commit b74bd5a7b2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 46 deletions

View file

@ -822,24 +822,23 @@ auto OpObjKeys(const OpArgs& op_args, string_view key, const WrappedJsonPath& js
return JsonEvaluateOperation<StringVec>(op_args, key, json_path, std::move(cb), {true, true});
}
auto OpStrAppend(const OpArgs& op_args, string_view key, const WrappedJsonPath& path,
facade::ArgRange strs) {
using StrAppendResult = std::optional<std::size_t>;
OpResult<JsonCallbackResult<StrAppendResult>> OpStrAppend(const OpArgs& op_args, string_view key,
const WrappedJsonPath& path,
string_view value) {
auto cb = [&](std::optional<std::string_view>,
JsonType* val) -> MutateCallbackResult<std::optional<std::size_t>> {
if (val->is_string()) {
string new_val = val->as_string();
for (string_view str : strs) {
new_val += str;
}
JsonType* val) -> MutateCallbackResult<StrAppendResult> {
if (!val->is_string())
return {false, std::nullopt};
*val = new_val;
return {false, new_val.size()};
}
return {false, std::nullopt};
string new_val = absl::StrCat(val->as_string_view(), value);
size_t len = new_val.size();
*val = std::move(new_val);
return {false, len}; // do not delete, new value len
};
return UpdateEntry<std::optional<std::size_t>>(op_args, key, path, std::move(cb));
return UpdateEntry<StrAppendResult>(op_args, key, path, std::move(cb));
}
// Returns the numbers of values cleared.
@ -1476,7 +1475,8 @@ void JsonFamily::ArrIndex(CmdArgList args, ConnectionContext* cntx) {
WrappedJsonPath json_path = GET_OR_SEND_UNEXPECTED(ParseJsonPath(path));
optional<JsonType> search_value = JsonFromString(ArgS(args, 2));
optional<JsonType> search_value =
dfly::JsonFromString(ArgS(args, 2), PMR_NS::get_default_resource());
if (!search_value) {
cntx->SendError(kSyntaxErr);
return;
@ -1651,12 +1651,19 @@ void JsonFamily::Clear(CmdArgList args, ConnectionContext* cntx) {
void JsonFamily::StrAppend(CmdArgList args, ConnectionContext* cntx) {
string_view key = ArgS(args, 0);
string_view path = ArgS(args, 1);
string_view value = ArgS(args, 2);
WrappedJsonPath json_path = GET_OR_SEND_UNEXPECTED(ParseJsonPath(path));
auto strs = args.subspan(2);
// We try parsing the value into json string object first.
optional<JsonType> parsed_json = dfly::JsonFromString(value, PMR_NS::get_default_resource());
if (!parsed_json || !parsed_json->is_string()) {
return cntx->SendError("expected string value", kSyntaxErrType);
};
string_view json_string = parsed_json->as_string_view();
auto cb = [&](Transaction* t, EngineShard* shard) {
return OpStrAppend(t->GetOpArgs(shard), key, json_path, facade::ArgRange{strs});
return OpStrAppend(t->GetOpArgs(shard), key, json_path, json_string);
};
Transaction* trans = cntx->transaction;
@ -1903,7 +1910,7 @@ void JsonFamily::Register(CommandRegistry* registry) {
*registry << CI{"JSON.FORGET", CO::WRITE, -2, 1, 1, acl::JSON}.HFUNC(
Del); // An alias of JSON.DEL.
*registry << CI{"JSON.OBJKEYS", CO::READONLY | CO::FAST, -2, 1, 1, acl::JSON}.HFUNC(ObjKeys);
*registry << CI{"JSON.STRAPPEND", CO::WRITE | CO::DENYOOM | CO::FAST, -4, 1, 1, acl::JSON}.HFUNC(
*registry << CI{"JSON.STRAPPEND", CO::WRITE | CO::DENYOOM | CO::FAST, 4, 1, 1, acl::JSON}.HFUNC(
StrAppend);
*registry << CI{"JSON.CLEAR", CO::WRITE | CO::FAST, -2, 1, 1, acl::JSON}.HFUNC(Clear);
*registry << CI{"JSON.ARRPOP", CO::WRITE | CO::FAST, -2, 1, 1, acl::JSON}.HFUNC(ArrPop);

View file

@ -1329,7 +1329,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
/* Test simple response from only one value */
resp = Run({"JSON.STRAPPEND", "json", "$.a.a", "a", "b"});
resp = Run({"JSON.STRAPPEND", "json", "$.a.a", "\"ab\""});
EXPECT_THAT(resp, IntArg(3));
resp = Run({"JSON.GET", "json"});
@ -1337,7 +1337,9 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp,
R"({"a":{"a":"aab"},"b":{"a":"a","b":1},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"});
const char kVal[] = "\"a\"";
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", kVal});
EXPECT_THAT(resp, IntArg(4));
resp = Run({"JSON.GET", "json"});
@ -1345,7 +1347,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp,
R"({"a":{"a":"aaba"},"b":{"a":"a","b":1},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", "$.c.b", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.c.b", kVal});
EXPECT_THAT(resp, IntArg(3));
resp = Run({"JSON.GET", "json"});
@ -1358,7 +1360,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
In JSON V2, the response is an array of all possible values
*/
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), ArgType(RespExpr::NIL)));
@ -1367,7 +1369,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp,
R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"a","b":"bba"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", "$.c.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.c.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(4)));
@ -1376,7 +1378,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp,
R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"aa","b":"bbaa"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", "$.d.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.d.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(),
ElementsAre(ArgType(RespExpr::NIL), IntArg(2), ArgType(RespExpr::NIL)));
@ -1393,14 +1395,14 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3), IntArg(4)));
resp = Run({"JSON.GET", "json"});
EXPECT_EQ(resp, R"({"a":{"a":"aa","b":"aaa","c":"aaaa"},"b":{"a":"aaa","b":"aa","c":"a"}})");
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(4), IntArg(3), IntArg(2)));
@ -1414,7 +1416,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3), ArgType(RespExpr::NIL)));
@ -1423,7 +1425,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp,
R"({"a":{"a":"aa","b":"aaa","c":["aaaaa","aaaaa"]},"b":{"a":"aaa","b":["aaaaa","aaaaa"],"c":"a"}})");
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(4), ArgType(RespExpr::NIL), IntArg(2)));
@ -1439,7 +1441,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3), ArgType(RespExpr::NIL)));
@ -1448,7 +1450,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp,
R"({"a":{"a":"aa","b":"aaa","c":{"c":"aaaaa"}},"b":{"a":"aaa","b":{"b":"aaaaa"},"c":"a"}})");
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", "$.b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(4), ArgType(RespExpr::NIL), IntArg(2)));
@ -1464,7 +1466,7 @@ TEST_F(JsonFamilyTest, StrAppend) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", "$..a", "bar"});
resp = Run({"JSON.STRAPPEND", "json", "$..a", "\"bar\""});
ASSERT_THAT(resp, ArgType(RespExpr::ARRAY));
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(6), IntArg(6), ArgType(RespExpr::NIL)));
@ -1482,7 +1484,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
/* Test simple response from only one value */
resp = Run({"JSON.STRAPPEND", "json", ".a.a", "a", "b"});
resp = Run({"JSON.STRAPPEND", "json", ".a.a", "\"ab\""});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(3));
@ -1491,7 +1493,9 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp,
R"({"a":{"a":"aab"},"b":{"a":"a","b":1},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"});
const char kVal[] = "\"a\"";
resp = Run({"JSON.STRAPPEND", "json", ".a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(4));
@ -1500,7 +1504,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp,
R"({"a":{"a":"aaba"},"b":{"a":"a","b":1},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", ".c.b", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".c.b", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(3));
@ -1515,7 +1519,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
string.
*/
resp = Run({"JSON.STRAPPEND", "json", ".b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(2));
@ -1524,7 +1528,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp,
R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"a","b":"bba"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", ".c.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".c.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(4));
@ -1533,7 +1537,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp,
R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"aa","b":"bbaa"},"d":{"a":1,"b":"b","c":3}})");
resp = Run({"JSON.STRAPPEND", "json", ".d.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".d.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(2));
@ -1549,14 +1553,14 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(4));
resp = Run({"JSON.GET", "json"});
EXPECT_EQ(resp, R"({"a":{"a":"aa","b":"aaa","c":"aaaa"},"b":{"a":"aaa","b":"aa","c":"a"}})");
resp = Run({"JSON.STRAPPEND", "json", ".b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(2));
@ -1570,7 +1574,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(3));
@ -1579,7 +1583,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp,
R"({"a":{"a":"aa","b":"aaa","c":["aaaaa","aaaaa"]},"b":{"a":"aaa","b":["aaaaa","aaaaa"],"c":"a"}})");
resp = Run({"JSON.STRAPPEND", "json", ".b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(2));
@ -1595,7 +1599,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".a.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(3));
@ -1604,7 +1608,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp,
R"({"a":{"a":"aa","b":"aaa","c":{"c":"aaaaa"}},"b":{"a":"aaa","b":{"b":"aaaaa"},"c":"a"}})");
resp = Run({"JSON.STRAPPEND", "json", ".b.*", "a"});
resp = Run({"JSON.STRAPPEND", "json", ".b.*", kVal});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(2));
@ -1620,7 +1624,7 @@ TEST_F(JsonFamilyTest, StrAppendLegacyMode) {
resp = Run({"JSON.SET", "json", ".", json});
EXPECT_EQ(resp, "OK");
resp = Run({"JSON.STRAPPEND", "json", "..a", "bar"});
resp = Run({"JSON.STRAPPEND", "json", "..a", "\"bar\""});
ASSERT_THAT(resp, ArgType(RespExpr::INT64));
EXPECT_THAT(resp, IntArg(6));

View file

@ -321,8 +321,9 @@ def test_jsonstrlen(r: redis.Redis):
assert r.json().strlen("doc1", "$.nested2.a") == [None]
# Test missing key
with pytest.raises(redis.ResponseError):
r.json().strlen("non_existing_doc", "$..a")
# Note: Dragonfly returns NIL in the accordance to the official docs
# with pytest.raises(redis.ResponseError):
# r.json().strlen("non_existing_doc", "$..a")
def test_toggle(r: redis.Redis):