diff --git a/src/server/detail/wrapped_json_path.h b/src/server/detail/wrapped_json_path.h index 435cb2f35..7bca419ef 100644 --- a/src/server/detail/wrapped_json_path.h +++ b/src/server/detail/wrapped_json_path.h @@ -11,36 +11,92 @@ #include "core/json/json_object.h" #include "core/json/path.h" #include "core/string_or_view.h" +#include "facade/op_status.h" +#include "glog/logging.h" namespace dfly { +using facade::OpResult; +using facade::OpStatus; +using Nothing = std::monostate; using JsonExpression = jsoncons::jsonpath::jsonpath_expression; template using JsonPathEvaluateCallback = absl::FunctionRef; +template class MutateCallbackResult { + public: + MutateCallbackResult() = default; + + explicit MutateCallbackResult(bool should_be_deleted) : should_be_deleted_(should_be_deleted_) { + } + + MutateCallbackResult(bool should_be_deleted, T&& value) + : should_be_deleted_(should_be_deleted), value_(std::forward(value)) { + } + + bool HasValue() const { + return value_.has_value(); + } + + T&& GetValue() && { + return std::move(value_).value(); + } + + bool ShouldBeDeleted() const { + return should_be_deleted_; + } + + private: + bool should_be_deleted_; + std::optional value_; +}; + +template +using JsonPathMutateCallback = + absl::FunctionRef(std::optional, JsonType*)>; + +namespace details { + +template void OptionalEmplace(T value, std::optional* optional) { + optional->emplace(std::move(value)); +} + +template +void OptionalEmplace(std::optional value, std::optional>* optional) { + if (value.has_value()) { + optional->emplace(std::move(value)); + } +} + +} // namespace details + template class JsonCallbackResult { public: + /* In the case of a restricted path (legacy mode), the result consists of a single value */ using JsonV1Result = std::optional; + + /* In the case of an enhanced path (starts with $), the result is an array of multiple values */ using JsonV2Result = std::vector; - explicit JsonCallbackResult(bool legacy_mode_is_enabled) - : legacy_mode_is_enabled_(legacy_mode_is_enabled) { - if (!legacy_mode_is_enabled_) { + JsonCallbackResult() = default; + + explicit JsonCallbackResult(bool legacy_mode_is_enabled) { + if (!legacy_mode_is_enabled) { result_ = JsonV2Result{}; } } - void AddValue(T&& value) { + void AddValue(T value) { if (IsV1()) { - AsV1().emplace(std::forward(value)); + details::OptionalEmplace(std::move(value), &AsV1()); } else { - AsV2().emplace_back(std::forward(value)); + AsV2().emplace_back(std::move(value)); } } bool IsV1() const { - return legacy_mode_is_enabled_; + return std::holds_alternative(result_); } JsonV1Result& AsV1() { @@ -51,9 +107,16 @@ template class JsonCallbackResult { return std::get(result_); } + const JsonV1Result& AsV1() const { + return std::get(result_); + } + + const JsonV2Result& AsV2() const { + return std::get(result_); + } + private: std::variant result_; - bool legacy_mode_is_enabled_; }; class WrappedJsonPath { @@ -102,6 +165,54 @@ class WrappedJsonPath { return eval_result; } + template + OpResult> Mutate(JsonType* json_entry, JsonPathMutateCallback cb) const { + JsonCallbackResult mutate_result{IsLegacyModePath()}; + + auto mutate_callback = [&cb, &mutate_result](std::optional path, + JsonType* val) -> bool { + auto res = cb(path, val); + if (res.HasValue()) { + mutate_result.AddValue(std::move(res).GetValue()); + } + return res.ShouldBeDeleted(); + }; + + if (HoldsJsonPath()) { + const auto& json_path = AsJsonPath(); + json::MutatePath(json_path, mutate_callback, json_entry); + } else { + using namespace jsoncons::jsonpath; + using namespace jsoncons::jsonpath::detail; + using Evaluator = jsonpath_evaluator; + using ValueType = Evaluator::value_type; + using Reference = Evaluator::reference; + using JsonSelector = Evaluator::path_expression_type; + + custom_functions funcs = custom_functions(); + + std::error_code ec; + static_resources static_resources(funcs); + Evaluator e; + + JsonSelector expr = e.compile(static_resources, path_.view(), ec); + if (ec) { + VLOG(1) << "Failed to mutate json with error: " << ec.message(); + return OpStatus::SYNTAX_ERR; + } + + dynamic_resources resources; + + auto f = [&mutate_callback](const basic_path_node& path, JsonType& val) { + mutate_callback(to_string(path), &val); + }; + + expr.evaluate(resources, *json_entry, JsonSelector::path_node_type{}, *json_entry, + std::move(f), result_options::nodups | result_options::path); + } + return mutate_result; + } + bool IsLegacyModePath() const { return is_legacy_mode_path_; } diff --git a/src/server/json_family.cc b/src/server/json_family.cc index 89d06e65e..0e64f839f 100644 --- a/src/server/json_family.cc +++ b/src/server/json_family.cc @@ -106,6 +106,53 @@ ParseResult ParseJsonPath(std::string_view path) { } // namespace json_parser +namespace reply_generic { + +void Send(std::size_t value, RedisReplyBuilder* rb) { + rb->SendLong(value); +} + +template void Send(const std::optional& opt, RedisReplyBuilder* rb) { + if (opt.has_value()) { + Send(opt.value(), rb); + } else { + rb->SendNull(); + } +} + +template void Send(const std::vector& vec, RedisReplyBuilder* rb) { + if (vec.empty()) { + rb->SendNullArray(); + } else { + rb->StartArray(vec.size()); + for (auto&& x : vec) { + Send(x, rb); + } + } +} + +template void Send(const JsonCallbackResult& result, RedisReplyBuilder* rb) { + if (result.IsV1()) { + /* The specified path was restricted (JSON legacy mode), then the result consists only of a + * single value */ + Send(result.AsV1(), rb); + } else { + /* The specified path was enhanced (starts with '$'), then the result is an array of multiple + * values */ + Send(result.AsV2(), rb); + } +} + +template void Send(const OpResult& result, RedisReplyBuilder* rb) { + if (result) { + Send(result.value(), rb); + } else { + rb->SendError(result.status()); + } +} + +} // namespace reply_generic + using JsonPathV2 = variant; using ExprCallback = absl::FunctionRef; @@ -241,6 +288,35 @@ error_code JsonReplace(JsonType& instance, string_view path, json::MutateCallbac return ec; } +template +OpResult> UpdateEntry(const OpArgs& op_args, std::string_view key, + const WrappedJsonPath& json_path, + JsonPathMutateCallback cb, + JsonReplaceVerify verify_op = {}) { + auto it_res = op_args.GetDbSlice().FindMutable(op_args.db_cntx, key, OBJ_JSON); + RETURN_ON_BAD_STATUS(it_res); + + PrimeValue& pv = it_res->it->second; + + JsonType* json_val = pv.GetJson(); + DCHECK(json_val) << "should have a valid JSON object for key '" << key << "' the type for it is '" + << pv.ObjType() << "'"; + + op_args.shard->search_indices()->RemoveDoc(key, op_args.db_cntx, pv); + + auto mutate_res = json_path.Mutate(json_val, cb); + + // Make sure that we don't have other internal issue with the operation + if (mutate_res && verify_op) { + verify_op(*json_val); + } + + it_res->post_updater.Run(); + op_args.shard->search_indices()->AddDoc(key, op_args.db_cntx, pv); + + return mutate_res; +} + // jsoncons version OpStatus UpdateEntry(const OpArgs& op_args, std::string_view key, std::string_view path, json::MutateCallback callback, JsonReplaceVerify verify_op = {}) { @@ -837,12 +913,9 @@ OpResult> OpObjKeys(const OpArgs& op_args, string_view key, return vec; } -// Retruns array of string lengths after a successful operation. -OpResult> OpStrAppend(const OpArgs& op_args, string_view key, string_view path, - JsonPathV2 expression, facade::ArgRange strs) { - vector vec; - OpStatus status; - auto cb = [&](const auto&, JsonType* val) { +auto OpStrAppend(const OpArgs& op_args, string_view key, const WrappedJsonPath& path, + facade::ArgRange strs) { + auto cb = [&](const auto&, JsonType* val) -> MutateCallbackResult> { if (val->is_string()) { string new_val = val->as_string(); for (string_view str : strs) { @@ -850,24 +923,13 @@ OpResult> OpStrAppend(const OpArgs& op_args, string_view key, s } *val = new_val; - vec.emplace_back(new_val.size()); - } else { - vec.emplace_back(nullopt); + return {false, new_val.size()}; } - return false; + + return {false, std::nullopt}; }; - if (holds_alternative(expression)) { - const json::Path& json_path = std::get(expression); - status = UpdateEntry(op_args, key, json_path, cb); - } else { - status = UpdateEntry(op_args, key, path, cb); - } - if (status != OpStatus::OK) { - return status; - } - - return vec; + return UpdateEntry>(op_args, key, path, std::move(cb)); } // Returns the numbers of values cleared. @@ -1891,22 +1953,17 @@ void JsonFamily::StrAppend(CmdArgList args, ConnectionContext* cntx) { string_view key = ArgS(args, 0); string_view path = ArgS(args, 1); - JsonPathV2 expression = PARSE_PATHV2(path); + WrappedJsonPath json_path = GET_OR_SEND_UNEXPECTED(json_parser::ParseJsonPath(path)); auto strs = args.subspan(2); auto cb = [&](Transaction* t, EngineShard* shard) { - return OpStrAppend(t->GetOpArgs(shard), key, path, std::move(expression), - facade::ArgRange{strs}); + return OpStrAppend(t->GetOpArgs(shard), key, json_path, facade::ArgRange{strs}); }; Transaction* trans = cntx->transaction; - OpResult> result = trans->ScheduleSingleHopT(std::move(cb)); - - if (result) { - PrintOptVec(cntx, result); - } else { - cntx->SendError(result.status()); - } + auto result = trans->ScheduleSingleHopT(std::move(cb)); + auto* rb = static_cast(cntx->reply_builder()); + reply_generic::Send(result, rb); } void JsonFamily::ObjKeys(CmdArgList args, ConnectionContext* cntx) { diff --git a/src/server/json_family_test.cc b/src/server/json_family_test.cc index dc6166850..50d280cb4 100644 --- a/src/server/json_family_test.cc +++ b/src/server/json_family_test.cc @@ -675,6 +675,8 @@ TEST_F(JsonFamilyTest, StrAppend) { auto resp = Run({"JSON.SET", "json", ".", json}); ASSERT_THAT(resp, "OK"); + /* Test simple response from only one value */ + resp = Run({"JSON.STRAPPEND", "json", "$.a.a", "a", "b"}); EXPECT_THAT(resp, IntArg(3)); @@ -691,26 +693,31 @@ 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"}); + EXPECT_THAT(resp, IntArg(3)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + resp, + R"({"a":{"a":"aaba"},"b":{"a":"a","b":1},"c":{"a":"a","b":"bba"},"d":{"a":1,"b":"b","c":3}})"); + + /* + Test response from several possible values + In JSON V2, the response is an array of all possible values + */ + resp = Run({"JSON.STRAPPEND", "json", "$.b.*", "a"}); - ASSERT_EQ(RespExpr::ARRAY, resp.type); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), ArgType(RespExpr::NIL))); resp = Run({"JSON.GET", "json"}); EXPECT_EQ( resp, - R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"a","b":"bb"},"d":{"a":1,"b":"b","c":3}})"); + 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"}); - ASSERT_EQ(RespExpr::ARRAY, resp.type); - EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3))); - - resp = Run({"JSON.GET", "json"}); - EXPECT_EQ( - resp, - R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"aa","b":"bba"},"d":{"a":1,"b":"b","c":3}})"); - - resp = Run({"JSON.STRAPPEND", "json", "$.c.b", "a"}); - EXPECT_THAT(resp, IntArg(4)); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(4))); resp = Run({"JSON.GET", "json"}); EXPECT_EQ( @@ -718,7 +725,7 @@ TEST_F(JsonFamilyTest, StrAppend) { 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"}); - ASSERT_EQ(RespExpr::ARRAY, resp.type); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); EXPECT_THAT(resp.GetVec(), ElementsAre(ArgType(RespExpr::NIL), IntArg(2), ArgType(RespExpr::NIL))); @@ -727,21 +734,248 @@ TEST_F(JsonFamilyTest, StrAppend) { resp, R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"aa","b":"bbaa"},"d":{"a":1,"b":"ba","c":3}})"); + json = R"( + {"a":{"a":"a", "b":"aa", "c":"aaa"}, "b":{"a":"aaa", "b":"aa", "c":"a"}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"}); + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(4), IntArg(3), IntArg(2))); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ(resp, R"({"a":{"a":"aa","b":"aaa","c":"aaaa"},"b":{"a":"aaaa","b":"aaa","c":"aa"}})"); + + json = R"( + {"a":{"a":"a", "b":"aa", "c":["aaaaa", "aaaaa"]}, "b":{"a":"aaa", "b":["aaaaa", "aaaaa"], "c":"a"}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3), ArgType(RespExpr::NIL))); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(4), ArgType(RespExpr::NIL), IntArg(2))); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + resp, + R"({"a":{"a":"aa","b":"aaa","c":["aaaaa","aaaaa"]},"b":{"a":"aaaa","b":["aaaaa","aaaaa"],"c":"aa"}})"); + + json = R"( + {"a":{"a":"a", "b":"aa", "c":{"c": "aaaaa"}}, "b":{"a":"aaa", "b":{"b": "aaaaa"}, "c":"a"}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", "$.a.*", "a"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3), ArgType(RespExpr::NIL))); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(4), ArgType(RespExpr::NIL), IntArg(2))); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + resp, + R"({"a":{"a":"aa","b":"aaa","c":{"c":"aaaaa"}},"b":{"a":"aaaa","b":{"b":"aaaaa"},"c":"aa"}})"); + json = R"( {"a":"foo", "inner": {"a": "bye"}, "inner1": {"a": 7}} )"; resp = Run({"JSON.SET", "json", ".", json}); - ASSERT_THAT(resp, "OK"); + EXPECT_EQ(resp, "OK"); resp = Run({"JSON.STRAPPEND", "json", "$..a", "bar"}); - ASSERT_EQ(RespExpr::ARRAY, resp.type); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(6), IntArg(6), ArgType(RespExpr::NIL))); resp = Run({"JSON.GET", "json"}); EXPECT_EQ(resp, R"({"a":"foobar","inner":{"a":"byebar"},"inner1":{"a":7}})"); } +TEST_F(JsonFamilyTest, StrAppendLegacyMode) { + string json = R"( + {"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":3}} + )"; + + auto resp = Run({"JSON.SET", "json", ".", json}); + ASSERT_THAT(resp, "OK"); + + /* Test simple response from only one value */ + + resp = Run({"JSON.STRAPPEND", "json", ".a.a", "a", "b"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(3)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_THAT( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(4)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_THAT( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(3)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_THAT( + resp, + R"({"a":{"a":"aaba"},"b":{"a":"a","b":1},"c":{"a":"a","b":"bba"},"d":{"a":1,"b":"b","c":3}})"); + + /* + Test response from several possible values + In JSON legacy mode, the response contains only one value - the new length of the last updated + string. + */ + + resp = Run({"JSON.STRAPPEND", "json", ".b.*", "a"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_THAT( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(4)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_THAT( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_THAT( + resp, + R"({"a":{"a":"aaba"},"b":{"a":"aa","b":1},"c":{"a":"aa","b":"bbaa"},"d":{"a":1,"b":"ba","c":3}})"); + + json = R"( + {"a":{"a":"a", "b":"aa", "c":"aaa"}, "b":{"a":"aaa", "b":"aa", "c":"a"}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"}); + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ(resp, R"({"a":{"a":"aa","b":"aaa","c":"aaaa"},"b":{"a":"aaaa","b":"aaa","c":"aa"}})"); + + json = R"( + {"a":{"a":"a", "b":"aa", "c":["aaaaa", "aaaaa"]}, "b":{"a":"aaa", "b":["aaaaa", "aaaaa"], "c":"a"}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(3)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + resp, + R"({"a":{"a":"aa","b":"aaa","c":["aaaaa","aaaaa"]},"b":{"a":"aaaa","b":["aaaaa","aaaaa"],"c":"aa"}})"); + + json = R"( + {"a":{"a":"a", "b":"aa", "c":{"c": "aaaaa"}}, "b":{"a":"aaa", "b":{"b": "aaaaa"}, "c":"a"}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", ".a.*", "a"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(3)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + 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"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ( + resp, + R"({"a":{"a":"aa","b":"aaa","c":{"c":"aaaaa"}},"b":{"a":"aaaa","b":{"b":"aaaaa"},"c":"aa"}})"); + + json = R"( + {"a":"foo", "inner": {"a": "bye"}, "inner1": {"a": 7}} + )"; + + resp = Run({"JSON.SET", "json", ".", json}); + EXPECT_EQ(resp, "OK"); + + resp = Run({"JSON.STRAPPEND", "json", "..a", "bar"}); + ASSERT_THAT(resp, ArgType(RespExpr::INT64)); + EXPECT_THAT(resp, IntArg(6)); + + resp = Run({"JSON.GET", "json"}); + EXPECT_EQ(resp, R"({"a":"foobar","inner":{"a":"byebar"},"inner1":{"a":7}})"); +} + TEST_F(JsonFamilyTest, Clear) { string json = R"( [[], [0], [0,1], [0,1,2], 1, true, null, "d"]