feat(server): implement json.arrinsert command (#104) (#372)

Signed-off-by: iko1 <me@remotecpp.dev>
This commit is contained in:
iko1 2022-10-13 11:20:27 +02:00 committed by GitHub
parent ad014ebb86
commit 28706715dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 156 additions and 9 deletions

View File

@ -141,28 +141,37 @@ bool JsonErrorHandler(json_errc ec, const ser_context&) {
return false;
}
OpResult<json> GetJson(const OpArgs& op_args, string_view key) {
OpResult<PrimeIterator> it_res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_STRING);
if (!it_res.ok())
return it_res.status();
optional<json> ConstructJsonFromString(string_view val) {
error_code ec;
json_decoder<json> decoder;
const PrimeValue& pv = it_res.value()->second;
string val = GetString(op_args.shard, pv);
basic_json_parser<char> parser(basic_json_decode_options<char>{}, &JsonErrorHandler);
parser.update(val);
parser.finish_parse(decoder, ec);
if (!decoder.is_valid()) {
return OpStatus::SYNTAX_ERR;
return nullopt;
}
return decoder.get_result();
}
OpResult<json> GetJson(const OpArgs& op_args, string_view key) {
OpResult<PrimeIterator> it_res = op_args.shard->db_slice().Find(op_args.db_cntx, key, OBJ_STRING);
if (!it_res.ok())
return it_res.status();
const PrimeValue& pv = it_res.value()->second;
string val = GetString(op_args.shard, pv);
optional<json> j = ConstructJsonFromString(val);
if (!j) {
return OpStatus::SYNTAX_ERR;
}
return *j;
}
// Returns the index of the next right bracket
optional<size_t> GetNextIndex(string_view str) {
size_t current_idx = 0;
@ -692,8 +701,113 @@ OpResult<vector<OptSizeT>> OpArrTrim(const OpArgs& op_args, string_view key, str
return vec;
}
// Returns numeric vector that represents the new length of the array at each path.
OpResult<vector<OptSizeT>> OpArrInsert(const OpArgs& op_args, string_view key, string_view path,
int index, const vector<json>& new_values) {
OpResult<json> result = GetJson(op_args, key);
if (!result) {
return result.status();
}
bool out_of_boundaries_encountered = false;
vector<OptSizeT> vec;
// Insert user-supplied value into the supplied index that should be valid.
// If at least one index isn't valid within an array in the json doc, the operation is discarded.
// Negative indexes start from the end of the array.
auto cb = [&](const string& path, json& val) {
if (out_of_boundaries_encountered) {
return;
}
if (!val.is_array()) {
vec.emplace_back(nullopt);
return;
}
size_t removal_index;
if (index < 0) {
if (val.empty()) {
out_of_boundaries_encountered = true;
return;
}
int temp_index = index + val.size();
if (temp_index < 0) {
out_of_boundaries_encountered = true;
return;
}
removal_index = temp_index;
} else {
if ((size_t)index > val.size()) {
out_of_boundaries_encountered = true;
return;
}
removal_index = index;
}
auto it = next(val.array_range().begin(), removal_index);
for (auto& new_val : new_values) {
it = val.insert(it, new_val);
it++;
}
vec.emplace_back(val.size());
};
json j = result.value();
error_code ec = JsonReplace(j, path, cb);
if (ec) {
VLOG(1) << "Failed to evaluate expression on json with error: " << ec.message();
return OpStatus::SYNTAX_ERR;
}
if (out_of_boundaries_encountered) {
return OpStatus::OUT_OF_RANGE;
}
SetString(op_args, key, j.as_string());
return vec;
}
} // namespace
void JsonFamily::ArrInsert(CmdArgList args, ConnectionContext* cntx) {
string_view key = ArgS(args, 1);
string_view path = ArgS(args, 2);
int index = -1;
if (!absl::SimpleAtoi(ArgS(args, 3), &index)) {
VLOG(1) << "Failed to convert the following value to numeric: " << ArgS(args, 3);
(*cntx)->SendError(kInvalidIntErr);
return;
}
vector<json> new_values;
for (size_t i = 4; i < args.size(); i++) {
optional<json> val = ConstructJsonFromString(ArgS(args, i));
if (!val) {
(*cntx)->SendError(kSyntaxErr);
return;
}
new_values.emplace_back(move(*val));
}
auto cb = [&](Transaction* t, EngineShard* shard) {
return OpArrInsert(t->GetOpArgs(shard), key, path, index, new_values);
};
Transaction* trans = cntx->transaction;
OpResult<vector<OptSizeT>> result = trans->ScheduleSingleHopT(move(cb));
if (result) {
PrintOptVec(cntx, result);
} else {
(*cntx)->SendError(result.status());
}
}
void JsonFamily::ArrTrim(CmdArgList args, ConnectionContext* cntx) {
string_view key = ArgS(args, 1);
string_view path = ArgS(args, 2);
@ -1102,6 +1216,8 @@ void JsonFamily::Register(CommandRegistry* registry) {
*registry << CI{"JSON.CLEAR", CO::WRITE | CO::DENYOOM | CO::FAST, 3, 1, 1, 1}.HFUNC(Clear);
*registry << CI{"JSON.ARRPOP", CO::WRITE | CO::DENYOOM | CO::FAST, -3, 1, 1, 1}.HFUNC(ArrPop);
*registry << CI{"JSON.ARRTRIM", CO::WRITE | CO::DENYOOM | CO::FAST, 5, 1, 1, 1}.HFUNC(ArrTrim);
*registry << CI{"JSON.ARRINSERT", CO::WRITE | CO::DENYOOM | CO::FAST, -4, 1, 1, 1}.HFUNC(
ArrInsert);
}
} // namespace dfly

View File

@ -34,6 +34,7 @@ class JsonFamily {
static void Clear(CmdArgList args, ConnectionContext* cntx);
static void ArrPop(CmdArgList args, ConnectionContext* cntx);
static void ArrTrim(CmdArgList args, ConnectionContext* cntx);
static void ArrInsert(CmdArgList args, ConnectionContext* cntx);
};
} // namespace dfly

View File

@ -746,4 +746,34 @@ TEST_F(JsonFamilyTest, ArrTrim) {
EXPECT_EQ(resp, R"({"a":[1,3,2],"nested":{"a":false}})");
}
TEST_F(JsonFamilyTest, ArrInsert) {
string json = R"(
[[], ["a"], ["a", "b"]]
)";
auto resp = Run({"set", "json", json});
ASSERT_THAT(resp, "OK");
resp = Run({"JSON.ARRINSERT", "json", "$[*]", "0", R"("a")"});
ASSERT_EQ(RespExpr::ARRAY, resp.type);
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(1), IntArg(2), IntArg(3)));
resp = Run({"GET", "json"});
EXPECT_EQ(resp, R"([["a"],["a","a"],["a","a","b"]])");
resp = Run({"JSON.ARRINSERT", "json", "$[*]", "-1", R"("b")"});
ASSERT_EQ(RespExpr::ARRAY, resp.type);
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(2), IntArg(3), IntArg(4)));
resp = Run({"GET", "json"});
EXPECT_EQ(resp, R"([["b","a"],["a","b","a"],["a","a","b","b"]])");
resp = Run({"JSON.ARRINSERT", "json", "$[*]", "1", R"("c")"});
ASSERT_EQ(RespExpr::ARRAY, resp.type);
EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(3), IntArg(4), IntArg(5)));
resp = Run({"GET", "json"});
EXPECT_EQ(resp, R"([["b","c","a"],["a","c","b","a"],["a","c","a","b","b"]])");
}
} // namespace dfly