Skip to content

Commit d51fea0

Browse files
fix(json_family): fix JSON.STRAPPEND command for JSON legacy mode (#3264)
* fix(json_family): fix JSON.STRAPPEND command for JSON legacy mode Signed-off-by: Stepan Bagritsevich <[email protected]> * fix(json_family): add tests Signed-off-by: Stepan Bagritsevich <[email protected]> * refactor(json_family): address comments Signed-off-by: Stepan Bagritsevich <[email protected]> * refactor(json_family): code clean up Signed-off-by: Stepan Bagritsevich <[email protected]> * refactor(json_family): address comments 2 Signed-off-by: Stepan Bagritsevich <[email protected]> * refactor(json_family_test): remove map_single_element_vector_ flag Signed-off-by: Stepan Bagritsevich <[email protected]> * fix(json_family_test): add more tests Signed-off-by: Stepan Bagritsevich <[email protected]> --------- Signed-off-by: Stepan Bagritsevich <[email protected]>
1 parent 1acc824 commit d51fea0

File tree

3 files changed

+454
-52
lines changed

3 files changed

+454
-52
lines changed

src/server/detail/wrapped_json_path.h

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,92 @@
1111
#include "core/json/json_object.h"
1212
#include "core/json/path.h"
1313
#include "core/string_or_view.h"
14+
#include "facade/op_status.h"
15+
#include "glog/logging.h"
1416

1517
namespace dfly {
1618

19+
using facade::OpResult;
20+
using facade::OpStatus;
21+
using Nothing = std::monostate;
1722
using JsonExpression = jsoncons::jsonpath::jsonpath_expression<JsonType>;
1823

1924
template <typename T>
2025
using JsonPathEvaluateCallback = absl::FunctionRef<T(std::string_view, const JsonType&)>;
2126

27+
template <typename T = Nothing> class MutateCallbackResult {
28+
public:
29+
MutateCallbackResult() = default;
30+
31+
explicit MutateCallbackResult(bool should_be_deleted) : should_be_deleted_(should_be_deleted_) {
32+
}
33+
34+
MutateCallbackResult(bool should_be_deleted, T&& value)
35+
: should_be_deleted_(should_be_deleted), value_(std::forward<T>(value)) {
36+
}
37+
38+
bool HasValue() const {
39+
return value_.has_value();
40+
}
41+
42+
T&& GetValue() && {
43+
return std::move(value_).value();
44+
}
45+
46+
bool ShouldBeDeleted() const {
47+
return should_be_deleted_;
48+
}
49+
50+
private:
51+
bool should_be_deleted_;
52+
std::optional<T> value_;
53+
};
54+
55+
template <typename T>
56+
using JsonPathMutateCallback =
57+
absl::FunctionRef<MutateCallbackResult<T>(std::optional<std::string_view>, JsonType*)>;
58+
59+
namespace details {
60+
61+
template <typename T> void OptionalEmplace(T value, std::optional<T>* optional) {
62+
optional->emplace(std::move(value));
63+
}
64+
65+
template <typename T>
66+
void OptionalEmplace(std::optional<T> value, std::optional<std::optional<T>>* optional) {
67+
if (value.has_value()) {
68+
optional->emplace(std::move(value));
69+
}
70+
}
71+
72+
} // namespace details
73+
2274
template <typename T> class JsonCallbackResult {
2375
public:
76+
/* In the case of a restricted path (legacy mode), the result consists of a single value */
2477
using JsonV1Result = std::optional<T>;
78+
79+
/* In the case of an enhanced path (starts with $), the result is an array of multiple values */
2580
using JsonV2Result = std::vector<T>;
2681

27-
explicit JsonCallbackResult(bool legacy_mode_is_enabled)
28-
: legacy_mode_is_enabled_(legacy_mode_is_enabled) {
29-
if (!legacy_mode_is_enabled_) {
82+
JsonCallbackResult() = default;
83+
84+
explicit JsonCallbackResult(bool legacy_mode_is_enabled) {
85+
if (!legacy_mode_is_enabled) {
3086
result_ = JsonV2Result{};
3187
}
3288
}
3389

34-
void AddValue(T&& value) {
90+
void AddValue(T value) {
3591
if (IsV1()) {
36-
AsV1().emplace(std::forward<T>(value));
92+
details::OptionalEmplace(std::move(value), &AsV1());
3793
} else {
38-
AsV2().emplace_back(std::forward<T>(value));
94+
AsV2().emplace_back(std::move(value));
3995
}
4096
}
4197

4298
bool IsV1() const {
43-
return legacy_mode_is_enabled_;
99+
return std::holds_alternative<JsonV1Result>(result_);
44100
}
45101

46102
JsonV1Result& AsV1() {
@@ -51,9 +107,16 @@ template <typename T> class JsonCallbackResult {
51107
return std::get<JsonV2Result>(result_);
52108
}
53109

110+
const JsonV1Result& AsV1() const {
111+
return std::get<JsonV1Result>(result_);
112+
}
113+
114+
const JsonV2Result& AsV2() const {
115+
return std::get<JsonV2Result>(result_);
116+
}
117+
54118
private:
55119
std::variant<JsonV1Result, JsonV2Result> result_;
56-
bool legacy_mode_is_enabled_;
57120
};
58121

59122
class WrappedJsonPath {
@@ -102,6 +165,54 @@ class WrappedJsonPath {
102165
return eval_result;
103166
}
104167

168+
template <typename T>
169+
OpResult<JsonCallbackResult<T>> Mutate(JsonType* json_entry, JsonPathMutateCallback<T> cb) const {
170+
JsonCallbackResult<T> mutate_result{IsLegacyModePath()};
171+
172+
auto mutate_callback = [&cb, &mutate_result](std::optional<std::string_view> path,
173+
JsonType* val) -> bool {
174+
auto res = cb(path, val);
175+
if (res.HasValue()) {
176+
mutate_result.AddValue(std::move(res).GetValue());
177+
}
178+
return res.ShouldBeDeleted();
179+
};
180+
181+
if (HoldsJsonPath()) {
182+
const auto& json_path = AsJsonPath();
183+
json::MutatePath(json_path, mutate_callback, json_entry);
184+
} else {
185+
using namespace jsoncons::jsonpath;
186+
using namespace jsoncons::jsonpath::detail;
187+
using Evaluator = jsonpath_evaluator<JsonType, JsonType&>;
188+
using ValueType = Evaluator::value_type;
189+
using Reference = Evaluator::reference;
190+
using JsonSelector = Evaluator::path_expression_type;
191+
192+
custom_functions<JsonType> funcs = custom_functions<JsonType>();
193+
194+
std::error_code ec;
195+
static_resources<ValueType, Reference> static_resources(funcs);
196+
Evaluator e;
197+
198+
JsonSelector expr = e.compile(static_resources, path_.view(), ec);
199+
if (ec) {
200+
VLOG(1) << "Failed to mutate json with error: " << ec.message();
201+
return OpStatus::SYNTAX_ERR;
202+
}
203+
204+
dynamic_resources<ValueType, Reference> resources;
205+
206+
auto f = [&mutate_callback](const basic_path_node<char>& path, JsonType& val) {
207+
mutate_callback(to_string(path), &val);
208+
};
209+
210+
expr.evaluate(resources, *json_entry, JsonSelector::path_node_type{}, *json_entry,
211+
std::move(f), result_options::nodups | result_options::path);
212+
}
213+
return mutate_result;
214+
}
215+
105216
bool IsLegacyModePath() const {
106217
return is_legacy_mode_path_;
107218
}

src/server/json_family.cc

Lines changed: 88 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,53 @@ ParseResult<WrappedJsonPath> ParseJsonPath(std::string_view path) {
106106

107107
} // namespace json_parser
108108

109+
namespace reply_generic {
110+
111+
void Send(std::size_t value, RedisReplyBuilder* rb) {
112+
rb->SendLong(value);
113+
}
114+
115+
template <typename T> void Send(const std::optional<T>& opt, RedisReplyBuilder* rb) {
116+
if (opt.has_value()) {
117+
Send(opt.value(), rb);
118+
} else {
119+
rb->SendNull();
120+
}
121+
}
122+
123+
template <typename T> void Send(const std::vector<T>& vec, RedisReplyBuilder* rb) {
124+
if (vec.empty()) {
125+
rb->SendNullArray();
126+
} else {
127+
rb->StartArray(vec.size());
128+
for (auto&& x : vec) {
129+
Send(x, rb);
130+
}
131+
}
132+
}
133+
134+
template <typename T> void Send(const JsonCallbackResult<T>& result, RedisReplyBuilder* rb) {
135+
if (result.IsV1()) {
136+
/* The specified path was restricted (JSON legacy mode), then the result consists only of a
137+
* single value */
138+
Send(result.AsV1(), rb);
139+
} else {
140+
/* The specified path was enhanced (starts with '$'), then the result is an array of multiple
141+
* values */
142+
Send(result.AsV2(), rb);
143+
}
144+
}
145+
146+
template <typename T> void Send(const OpResult<T>& result, RedisReplyBuilder* rb) {
147+
if (result) {
148+
Send(result.value(), rb);
149+
} else {
150+
rb->SendError(result.status());
151+
}
152+
}
153+
154+
} // namespace reply_generic
155+
109156
using JsonPathV2 = variant<json::Path, JsonExpression>;
110157
using ExprCallback = absl::FunctionRef<void(string_view, const JsonType&)>;
111158

@@ -241,6 +288,35 @@ error_code JsonReplace(JsonType& instance, string_view path, json::MutateCallbac
241288
return ec;
242289
}
243290

291+
template <typename T>
292+
OpResult<JsonCallbackResult<T>> UpdateEntry(const OpArgs& op_args, std::string_view key,
293+
const WrappedJsonPath& json_path,
294+
JsonPathMutateCallback<T> cb,
295+
JsonReplaceVerify verify_op = {}) {
296+
auto it_res = op_args.GetDbSlice().FindMutable(op_args.db_cntx, key, OBJ_JSON);
297+
RETURN_ON_BAD_STATUS(it_res);
298+
299+
PrimeValue& pv = it_res->it->second;
300+
301+
JsonType* json_val = pv.GetJson();
302+
DCHECK(json_val) << "should have a valid JSON object for key '" << key << "' the type for it is '"
303+
<< pv.ObjType() << "'";
304+
305+
op_args.shard->search_indices()->RemoveDoc(key, op_args.db_cntx, pv);
306+
307+
auto mutate_res = json_path.Mutate(json_val, cb);
308+
309+
// Make sure that we don't have other internal issue with the operation
310+
if (mutate_res && verify_op) {
311+
verify_op(*json_val);
312+
}
313+
314+
it_res->post_updater.Run();
315+
op_args.shard->search_indices()->AddDoc(key, op_args.db_cntx, pv);
316+
317+
return mutate_res;
318+
}
319+
244320
// jsoncons version
245321
OpStatus UpdateEntry(const OpArgs& op_args, std::string_view key, std::string_view path,
246322
json::MutateCallback callback, JsonReplaceVerify verify_op = {}) {
@@ -837,37 +913,23 @@ OpResult<vector<StringVec>> OpObjKeys(const OpArgs& op_args, string_view key,
837913
return vec;
838914
}
839915

840-
// Retruns array of string lengths after a successful operation.
841-
OpResult<vector<OptSizeT>> OpStrAppend(const OpArgs& op_args, string_view key, string_view path,
842-
JsonPathV2 expression, facade::ArgRange strs) {
843-
vector<OptSizeT> vec;
844-
OpStatus status;
845-
auto cb = [&](const auto&, JsonType* val) {
916+
auto OpStrAppend(const OpArgs& op_args, string_view key, const WrappedJsonPath& path,
917+
facade::ArgRange strs) {
918+
auto cb = [&](const auto&, JsonType* val) -> MutateCallbackResult<std::optional<std::size_t>> {
846919
if (val->is_string()) {
847920
string new_val = val->as_string();
848921
for (string_view str : strs) {
849922
new_val += str;
850923
}
851924

852925
*val = new_val;
853-
vec.emplace_back(new_val.size());
854-
} else {
855-
vec.emplace_back(nullopt);
926+
return {false, new_val.size()};
856927
}
857-
return false;
858-
};
859-
if (holds_alternative<json::Path>(expression)) {
860-
const json::Path& json_path = std::get<json::Path>(expression);
861-
status = UpdateEntry(op_args, key, json_path, cb);
862-
} else {
863-
status = UpdateEntry(op_args, key, path, cb);
864-
}
865928

866-
if (status != OpStatus::OK) {
867-
return status;
868-
}
929+
return {false, std::nullopt};
930+
};
869931

870-
return vec;
932+
return UpdateEntry<std::optional<std::size_t>>(op_args, key, path, std::move(cb));
871933
}
872934

873935
// Returns the numbers of values cleared.
@@ -1891,22 +1953,17 @@ void JsonFamily::StrAppend(CmdArgList args, ConnectionContext* cntx) {
18911953
string_view key = ArgS(args, 0);
18921954
string_view path = ArgS(args, 1);
18931955

1894-
JsonPathV2 expression = PARSE_PATHV2(path);
1956+
WrappedJsonPath json_path = GET_OR_SEND_UNEXPECTED(json_parser::ParseJsonPath(path));
18951957
auto strs = args.subspan(2);
18961958

18971959
auto cb = [&](Transaction* t, EngineShard* shard) {
1898-
return OpStrAppend(t->GetOpArgs(shard), key, path, std::move(expression),
1899-
facade::ArgRange{strs});
1960+
return OpStrAppend(t->GetOpArgs(shard), key, json_path, facade::ArgRange{strs});
19001961
};
19011962

19021963
Transaction* trans = cntx->transaction;
1903-
OpResult<vector<OptSizeT>> result = trans->ScheduleSingleHopT(std::move(cb));
1904-
1905-
if (result) {
1906-
PrintOptVec(cntx, result);
1907-
} else {
1908-
cntx->SendError(result.status());
1909-
}
1964+
auto result = trans->ScheduleSingleHopT(std::move(cb));
1965+
auto* rb = static_cast<RedisReplyBuilder*>(cntx->reply_builder());
1966+
reply_generic::Send(result, rb);
19101967
}
19111968

19121969
void JsonFamily::ObjKeys(CmdArgList args, ConnectionContext* cntx) {

0 commit comments

Comments
 (0)