Skip to content

Commit

Permalink
support for the JSON.TOGGLE command (#1875)
Browse files Browse the repository at this point in the history
Co-authored-by: Twice <twice@apache.org>
Co-authored-by: hulk <hulk.website@gmail.com>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent 7c045b2 commit 1083142
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 1 deletion.
31 changes: 30 additions & 1 deletion src/commands/cmd_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,35 @@ class CommandJsonClear : public Commander {
}
};

class CommandJsonToggle : public Commander {
public:
Status Execute(Server *svr, Connection *conn, std::string *output) override {
redis::Json json(svr->storage, conn->GetNamespace());

std::string path = (args_.size() > 2) ? args_[2] : "$";
std::vector<std::optional<bool>> results;
auto s = json.Toggle(args_[1], path, results);

if (s.IsNotFound()) {
*output = redis::NilString();
return Status::OK();
}

*output = redis::MultiLen(results.size());
for (auto it = results.rbegin(); it != results.rend(); ++it) {
if (it->has_value()) {
*output += redis::Integer(it->value());
} else {
*output += redis::NilString();
}
}

if (!s.ok()) return {Status::RedisExecErr, s.ToString()};

return Status::OK();
}
};

class CommandJsonArrLen : public Commander {
public:
Status Execute(Server *svr, Connection *conn, std::string *output) override {
Expand Down Expand Up @@ -297,8 +326,8 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 1
MakeCmdAttr<CommandJsonType>("json.type", -2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandJsonArrAppend>("json.arrappend", -4, "write", 1, 1, 1),
MakeCmdAttr<CommandJsonClear>("json.clear", -2, "write", 1, 1, 1),
MakeCmdAttr<CommandJsonToggle>("json.toggle", -2, "write", 1, 1, 1),
MakeCmdAttr<CommandJsonArrLen>("json.arrlen", -2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandJsonObjkeys>("json.objkeys", -2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandJsonArrPop>("json.arrpop", -2, "write", 1, 1, 1), );

} // namespace redis
17 changes: 17 additions & 0 deletions src/types/json.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,23 @@ struct JsonValue {
return types;
}

StatusOr<std::vector<std::optional<bool>>> Toggle(std::string_view path) {
std::vector<std::optional<bool>> result;
try {
jsoncons::jsonpath::json_replace(value, path, [&result](const std::string & /*path*/, jsoncons::json &val) {
if (val.is_bool()) {
val = !val.as_bool();
result.emplace_back(val.as_bool());
} else {
result.emplace_back(std::nullopt);
}
});
} catch (const jsoncons::jsonpath::jsonpath_error &e) {
return {Status::NotOK, e.what()};
}
return result;
}

StatusOr<size_t> Clear(std::string_view path) {
size_t count = 0;
try {
Expand Down
18 changes: 18 additions & 0 deletions src/types/redis_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,24 @@ rocksdb::Status Json::ArrLen(const std::string &user_key, const std::string &pat
return rocksdb::Status::OK();
}

rocksdb::Status Json::Toggle(const std::string &user_key, const std::string &path,
std::vector<std::optional<bool>> &result) {
auto ns_key = AppendNamespacePrefix(user_key);

LockGuard guard(storage_->GetLockManager(), ns_key);

JsonMetadata metadata;
JsonValue origin;
auto s = read(ns_key, &metadata, &origin);
if (!s.ok()) return s;

auto toggle_res = origin.Toggle(path);
if (!toggle_res) return rocksdb::Status::InvalidArgument(toggle_res.Msg());
result = *toggle_res;

return write(ns_key, &metadata, origin);
}

rocksdb::Status Json::ArrPop(const std::string &user_key, const std::string &path, int64_t index,
std::vector<std::optional<JsonValue>> *results) {
auto ns_key = AppendNamespacePrefix(user_key);
Expand Down
2 changes: 2 additions & 0 deletions src/types/redis_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class Json : public Database {
rocksdb::Status Clear(const std::string &user_key, const std::string &path, size_t *result);
rocksdb::Status ArrLen(const std::string &user_key, const std::string &path,
std::vector<std::optional<uint64_t>> &arr_lens);
rocksdb::Status Toggle(const std::string &user_key, const std::string &path,
std::vector<std::optional<bool>> &result);
rocksdb::Status ObjKeys(const std::string &user_key, const std::string &path,
std::vector<std::optional<std::vector<std::string>>> &keys);
rocksdb::Status ArrPop(const std::string &user_key, const std::string &path, int64_t index,
Expand Down
58 changes: 58 additions & 0 deletions tests/cppunit/types/json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,64 @@ TEST_F(RedisJsonTest, ArrLen) {
ASSERT_TRUE(res.empty());
}

TEST_F(RedisJsonTest, Toggle) {
std::vector<std::optional<bool>> res;
ASSERT_TRUE(json_->Set(key_, "$", "true").ok());
ASSERT_TRUE(json_->Toggle(key_, "$", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), "false");
ASSERT_EQ(res.size(), 1);
ASSERT_THAT(res, testing::ElementsAre(false));
res.clear();

ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":true})").ok());
ASSERT_TRUE(json_->Toggle(key_, "$.bool", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":false})");
ASSERT_EQ(res.size(), 1);
ASSERT_THAT(res, testing::ElementsAre(false));
res.clear();

ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":true,"bools":{"bool":true}})").ok());
ASSERT_TRUE(json_->Toggle(key_, "$.bool", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":false,"bools":{"bool":true}})");
ASSERT_EQ(res.size(), 1);
ASSERT_THAT(res, testing::ElementsAre(false));
res.clear();

ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":true,"bools":{"bool":true}})").ok());
ASSERT_TRUE(json_->Toggle(key_, "$..bool", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":false,"bools":{"bool":false}})");
ASSERT_EQ(res.size(), 2);
ASSERT_THAT(res, testing::ElementsAre(false, false));
res.clear();

ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":false,"bools":{"bool":true}})").ok());
ASSERT_TRUE(json_->Toggle(key_, "$..bool", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":true,"bools":{"bool":false}})");
ASSERT_EQ(res.size(), 2);
ASSERT_THAT(res, testing::ElementsAre(false, true));
res.clear();

ASSERT_TRUE(json_->Set(key_, "$", R"({"bool":false,"bools":{"bool":true},"incorrectbool":{"bool":88}})").ok());
ASSERT_TRUE(json_->Toggle(key_, "$..bool", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"bool":true,"bools":{"bool":false},"incorrectbool":{"bool":88}})");
ASSERT_EQ(res.size(), 3);
ASSERT_THAT(res, testing::ElementsAre(std::nullopt, false, true));
res.clear();

ASSERT_TRUE(json_->Set(key_, "$", "[true,true,99]").ok());
ASSERT_TRUE(json_->Toggle(key_, "$..*", res).ok());
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), "[false,false,99]");
ASSERT_EQ(res.size(), 3);
ASSERT_THAT(res, testing::ElementsAre(std::nullopt, false, false));
}

TEST_F(RedisJsonTest, ArrPop) {
std::vector<std::optional<JsonValue>> res;

Expand Down
30 changes: 30 additions & 0 deletions tests/gocase/unit/type/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,34 @@ func TestJson(t *testing.T) {
require.ErrorContains(t, rdb.Do(ctx, "JSON.ARRPOP", "a", "$", "0", "1").Err(), "wrong number of arguments")
})

t.Run("JSON.TOGGLE basics", func(t *testing.T) {
require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `true`).Err())
require.EqualValues(t, []interface{}{int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `false`)

require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":true}`).Err())
require.EqualValues(t, []interface{}{int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$.bool").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":false}`)

require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":true,"bools":{"bool":true}}`).Err())
require.EqualValues(t, []interface{}{int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$.bool").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":false,"bools":{"bool":true}}`)

require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":true,"bools":{"bool":true}}`).Err())
require.EqualValues(t, []interface{}{int64(0), int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..bool").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":false,"bools":{"bool":false}}`)

require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"bool":false,"bools":{"bool":true}}`).Err())
require.EqualValues(t, []interface{}{int64(1), int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..bool").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":true,"bools":{"bool":false}}`)

require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"incorrectbool":99,"bools":{"bool":true},"bool":{"bool":false}}`).Err())
require.EqualValues(t, []interface{}{nil, int64(1), int64(0)}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..bool").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `{"bool":{"bool":true},"bools":{"bool":false},"incorrectbool":99}`)

require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `[99,true,99]`).Err())
require.EqualValues(t, []interface{}{nil, int64(0), nil}, rdb.Do(ctx, "JSON.TOGGLE", "a", "$..*").Val())
require.Equal(t, rdb.Do(ctx, "JSON.GET", "a").Val(), `[99,false,99]`)
})

}

0 comments on commit 1083142

Please sign in to comment.