diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc index 6b5445c7f03..d17b3b556be 100644 --- a/src/commands/cmd_json.cc +++ b/src/commands/cmd_json.cc @@ -148,9 +148,33 @@ class CommandJsonType : public Commander { } }; +class CommandJsonClear : public Commander { + public: + Status Execute(Server *svr, Connection *conn, std::string *output) override { + redis::Json json(svr->storage, conn->GetNamespace()); + + int result = 0; + + // If path not specified set it to $ + std::string path = (args_.size() > 2) ? args_[2] : "$"; + auto s = json.Clear(args_[1], path, &result); + + if (s.IsNotFound()) { + *output = redis::NilString(); + return Status::OK(); + } + + if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; + + *output = redis::Integer(result); + return Status::OK(); + } +}; + REDIS_REGISTER_COMMANDS(MakeCmdAttr("json.set", 4, "write", 1, 1, 1), MakeCmdAttr("json.get", -2, "read-only", 1, 1, 1), MakeCmdAttr("json.type", -2, "read-only", 1, 1, 1), - MakeCmdAttr("json.arrappend", -4, "write", 1, 1, 1), ); + MakeCmdAttr("json.arrappend", -4, "write", 1, 1, 1), + MakeCmdAttr("json.clear", -2, "write", 1, 1, 1), ); } // namespace redis diff --git a/src/types/json.h b/src/types/json.h index a3dd4c9521e..a099c470f53 100644 --- a/src/types/json.h +++ b/src/types/json.h @@ -172,6 +172,36 @@ struct JsonValue { return Status::OK(); } + Status Clear(std::string_view path, int *result) { + try { + int cleared_count = 0; + jsoncons::jsonpath::json_replace(value, path, + [&cleared_count](const std::string & + /*path*/, + jsoncons::json &val) { + bool is_array = val.is_array() && !val.empty(); + bool is_object = val.is_object() && !val.empty(); + bool is_number = val.is_number() && val.as() != 0; + + if (is_array) + val = jsoncons::json::array(); + else if (is_object) + val = jsoncons::json::object(); + else if (is_number) + val = 0; + else + return; + + cleared_count++; + }); + + *result = cleared_count; + } catch (const jsoncons::jsonpath::jsonpath_error &e) { + return {Status::NotOK, e.what()}; + } + return Status::OK(); + } + JsonValue(const JsonValue &) = default; JsonValue(JsonValue &&) = default; diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc index 7381fef64b0..f7b4e6d22e0 100644 --- a/src/types/redis_json.cc +++ b/src/types/redis_json.cc @@ -163,4 +163,24 @@ rocksdb::Status Json::Type(const std::string &user_key, const std::string &path, return rocksdb::Status::OK(); } +rocksdb::Status Json::Clear(const std::string &user_key, const std::string &path, int *result) { + auto ns_key = AppendNamespacePrefix(user_key); + + LockGuard guard(storage_->GetLockManager(), ns_key); + + JsonValue json_val; + JsonMetadata metadata; + auto s = read(ns_key, &metadata, &json_val); + + if (!s.ok()) return s; + + auto res = json_val.Clear(path, result); + if (!res.OK()) return s; + + if (*result == 0) { + return rocksdb::Status::OK(); + } + + return write(ns_key, &metadata, json_val); +} } // namespace redis diff --git a/src/types/redis_json.h b/src/types/redis_json.h index d9492706dde..c8b5b9b3841 100644 --- a/src/types/redis_json.h +++ b/src/types/redis_json.h @@ -38,6 +38,7 @@ class Json : public Database { rocksdb::Status Type(const std::string &user_key, const std::string &path, std::vector *results); rocksdb::Status ArrAppend(const std::string &user_key, const std::string &path, const std::vector &values, std::vector *result_count); + rocksdb::Status Clear(const std::string &user_key, const std::string &path, int *result); private: rocksdb::Status write(Slice ns_key, JsonMetadata *metadata, const JsonValue &json_val); diff --git a/tests/cppunit/types/json_test.cc b/tests/cppunit/types/json_test.cc index e3be9774f16..79ff4fd5994 100644 --- a/tests/cppunit/types/json_test.cc +++ b/tests/cppunit/types/json_test.cc @@ -181,3 +181,47 @@ TEST_F(RedisJsonTest, ArrAppend) { R"({"x":[1,2,{"x":[1,2,{"y":[1,2,3],"z":3}],"y":[{"y":1},1,2,3]},1],"y":[1,2,3,1,2,3]})"); res.clear(); } + +TEST_F(RedisJsonTest, Clear) { + int result = 0; + + ASSERT_TRUE( + json_ + ->Set(key_, "$", + R"({"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14})") + .ok()); + + ASSERT_TRUE(json_->Clear(key_, "$", &result).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), "{}"); + ASSERT_EQ(result, 1); + + ASSERT_TRUE( + json_ + ->Set(key_, "$", + R"({"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14})") + .ok()); + + ASSERT_TRUE(json_->Clear(key_, "$.obj", &result).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"arr":[1,2,3],"bool":true,"float":3.14,"int":42,"obj":{},"str":"foo"})"); + ASSERT_EQ(result, 1); + + ASSERT_TRUE(json_->Clear(key_, "$.arr", &result).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"arr":[],"bool":true,"float":3.14,"int":42,"obj":{},"str":"foo"})"); + ASSERT_EQ(result, 1); + + ASSERT_TRUE( + json_ + ->Set(key_, "$", + R"({"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14})") + .ok()); + ASSERT_TRUE(json_->Clear(key_, "$.*", &result).ok()); + ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok()); + ASSERT_EQ(json_val_.Dump().GetValue(), R"({"arr":[],"bool":true,"float":0,"int":0,"obj":{},"str":"foo"})"); + ASSERT_EQ(result, 4); + + ASSERT_TRUE(json_->Clear(key_, "$.some", &result).ok()); + ASSERT_EQ(result, 0); +} diff --git a/tests/gocase/unit/type/json/json_test.go b/tests/gocase/unit/type/json/json_test.go index 5a60ee0295d..2a0cd01e3b2 100644 --- a/tests/gocase/unit/type/json/json_test.go +++ b/tests/gocase/unit/type/json/json_test.go @@ -130,4 +130,26 @@ func TestJson(t *testing.T) { _, err = rdb.Do(ctx, "JSON.TYPE", "not_exists", "$").StringSlice() require.EqualError(t, err, redis.Nil.Error()) }) + + t.Run("Clear JSON values", func(t *testing.T) { + require.NoError(t, rdb.Do(ctx, "JSON.SET", "bb", "$", `{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14}`).Err()) + + require.NoError(t, rdb.Do(ctx, "JSON.CLEAR", "bb", "$").Err()) + require.Equal(t, `{}`, rdb.Do(ctx, "JSON.GET", "bb").Val()) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "bb", "$", `{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14}`).Err()) + require.NoError(t, rdb.Do(ctx, "JSON.CLEAR", "bb", "$.obj").Err()) + require.Equal(t, `{"arr":[1,2,3],"bool":true,"float":3.14,"int":42,"obj":{},"str":"foo"}`, rdb.Do(ctx, "JSON.GET", "bb").Val()) + + require.NoError(t, rdb.Do(ctx, "JSON.CLEAR", "bb", "$.arr").Err()) + require.Equal(t, `{"arr":[],"bool":true,"float":3.14,"int":42,"obj":{},"str":"foo"}`, rdb.Do(ctx, "JSON.GET", "bb").Val()) + + require.NoError(t, rdb.Do(ctx, "JSON.SET", "bb", "$", `{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14}`).Err()) + require.NoError(t, rdb.Do(ctx, "JSON.CLEAR", "bb", "$.*").Err()) + require.Equal(t, `{"arr":[],"bool":true,"float":0,"int":0,"obj":{},"str":"foo"}`, rdb.Do(ctx, "JSON.GET", "bb").Val()) + + _, err := rdb.Do(ctx, "JSON.CLEAR", "bb", "$.some").Result() + require.NoError(t, err) + }) + }