diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc index 887cd6c6752..8f788953814 100644 --- a/src/commands/cmd_json.cc +++ b/src/commands/cmd_json.cc @@ -45,6 +45,14 @@ std::string OptionalsToString(const Connection *conn, Optionals &opts) { return str; } +std::string SizeToString(const std::vector &elems) { + std::string result = MultiLen(elems.size()); + for (const auto &elem : elems) { + result += redis::Integer(elem); + } + return result; +} + class CommandJsonSet : public Commander { public: Status Execute(Server *srv, Connection *conn, std::string *output) override { @@ -632,6 +640,40 @@ class CommandJsonMSet : public Commander { } }; +class CommandJsonDebug : public Commander { + public: + Status Execute(Server *svr, Connection *conn, std::string *output) override { + redis::Json json(svr->storage, conn->GetNamespace()); + + std::string path = "$"; + + if (!util::EqualICase(args_[1], "memory")) { + return {Status::RedisExecErr, "ERR wrong number of arguments for 'json.debug' command"}; + } + + if (args_.size() == 4) { + path = args_[3]; + } else if (args_.size() > 4) { + return {Status::RedisExecErr, "The number of arguments is more than expected"}; + } + + std::vector results; + auto s = json.DebugMemory(args_[2], path, &results); + + if (s.IsNotFound()) { + if (args_.size() == 3) { + *output = redis::Integer(0); + } else { + *output = SizeToString(results); + } + return Status::OK(); + } + if (!s.ok()) return {Status::RedisExecErr, s.ToString()}; + + *output = SizeToString(results); + 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.info", 2, "read-only", 1, 1, 1), @@ -655,6 +697,7 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr("json.set", 4, "write", 1, 1 MakeCmdAttr("json.strappend", -3, "write", 1, 1, 1), MakeCmdAttr("json.strlen", -2, "read-only", 1, 1, 1), MakeCmdAttr("json.mget", -3, "read-only", 1, -2, 1), - MakeCmdAttr("json.mset", -4, "write", 1, -3, 3), ); + MakeCmdAttr("json.mset", -4, "write", 1, -3, 3), + MakeCmdAttr("json.debug", -3, "read-only", 2, 2, 1)); } // namespace redis diff --git a/src/types/json.h b/src/types/json.h index 2f5c0c45c9e..fe651ccceaa 100644 --- a/src/types/json.h +++ b/src/types/json.h @@ -217,6 +217,30 @@ struct JsonValue { return results; } + StatusOr> GetBytes(std::string_view path, JsonStorageFormat format, + int max_nesting_depth = std::numeric_limits::max()) const { + std::vector results; + Status s; + try { + jsoncons::jsonpath::json_query(value, path, [&](const std::string & /*path*/, const jsoncons::json &origin) { + if (!s) return; + std::string buffer; + JsonValue query_value(origin); + if (format == JsonStorageFormat::JSON) { + s = query_value.Dump(&buffer, max_nesting_depth); + } else if (format == JsonStorageFormat::CBOR) { + s = query_value.DumpCBOR(&buffer, max_nesting_depth); + } + results.emplace_back(buffer.size()); + }); + } catch (const jsoncons::jsonpath::jsonpath_error &e) { + return {Status::NotOK, e.what()}; + } + if (!s) return {Status::NotOK, s.Msg()}; + + return results; + } + StatusOr Get(std::string_view path) const { try { return jsoncons::jsonpath::json_query(value, path); diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc index ca87998129e..9929b22c6e4 100644 --- a/src/types/redis_json.cc +++ b/src/types/redis_json.cc @@ -627,4 +627,24 @@ std::vector Json::readMulti(const std::vector &ns_keys, return statuses; } +rocksdb::Status Json::DebugMemory(const std::string &user_key, const std::string &path, std::vector *results) { + auto ns_key = AppendNamespacePrefix(user_key); + JsonMetadata metadata; + if (path == "$") { + std::string bytes; + Slice rest; + auto s = GetMetadata(GetOptions{}, {kRedisJson}, ns_key, &bytes, &metadata, &rest); + if (!s.ok()) return s; + results->emplace_back(rest.size()); + } else { + JsonValue json_val; + auto s = read(ns_key, &metadata, &json_val); + if (!s.ok()) return s; + auto str_bytes = json_val.GetBytes(path, metadata.format, storage_->GetConfig()->json_max_nesting_depth); + if (!str_bytes) return rocksdb::Status::InvalidArgument(str_bytes.Msg()); + *results = std::move(*str_bytes); + } + return rocksdb::Status::OK(); +} + } // namespace redis diff --git a/src/types/redis_json.h b/src/types/redis_json.h index f72b2c00425..8dc212356e8 100644 --- a/src/types/redis_json.h +++ b/src/types/redis_json.h @@ -68,6 +68,7 @@ class Json : public Database { std::vector &results); rocksdb::Status MSet(const std::vector &user_keys, const std::vector &paths, const std::vector &values); + rocksdb::Status DebugMemory(const std::string &user_key, const std::string &path, std::vector *results); private: rocksdb::Status write(Slice ns_key, JsonMetadata *metadata, const JsonValue &json_val); diff --git a/tests/gocase/unit/type/json/json_test.go b/tests/gocase/unit/type/json/json_test.go index 0f2bbf8f3f6..e609d14fd5a 100644 --- a/tests/gocase/unit/type/json/json_test.go +++ b/tests/gocase/unit/type/json/json_test.go @@ -631,6 +631,29 @@ func TestJson(t *testing.T) { EqualJSON(t, `[{"a": 4, "b": 5, "nested": {"a": 6}, "c": null}]`, rdb.Do(ctx, "JSON.GET", "a1", "$").Val()) EqualJSON(t, `[4]`, rdb.Do(ctx, "JSON.GET", "a1", "$.a").Val()) }) + + t.Run("JSON.DEBUG MEMORY basics", func(t *testing.T) { + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"b":true,"x":1, "y":1.2, "z": {"x":[1,2,3], "y": null}, "v":{"x":"y"},"f":{"x":[]}}`).Err()) + //object + var result1 = make([]interface{}, 0) + result1 = append(result1, int64(43)) + require.Equal(t, result1, rdb.Do(ctx, "JSON.DEBUG", "MEMORY", "a", "$").Val()) + //integer string array empty_array + var result2 = make([]interface{}, 0) + result2 = append(result2, int64(1), int64(1), int64(2), int64(4)) + require.Equal(t, result2, rdb.Do(ctx, "JSON.DEBUG", "MEMORY", "a", "$..x").Val()) + //null object + var result3 = make([]interface{}, 0) + result3 = append(result3, int64(9), int64(1)) + require.Equal(t, result3, rdb.Do(ctx, "JSON.DEBUG", "MEMORY", "a", "$..y").Val()) + //no no_exists + require.Equal(t, []interface{}{}, rdb.Do(ctx, "JSON.DEBUG", "MEMORY", "a", "$..no_exists").Val()) + //no key no path + require.Equal(t, rdb.Do(ctx, "JSON.DEBUG", "MEMORY", "not_exists").Val(), int64(0)) + //no key have path + require.Equal(t, []interface{}{}, rdb.Do(ctx, "JSON.DEBUG", "MEMORY", "not_exists", "$").Val()) + + }) } func EqualJSON(t *testing.T, expected string, actual interface{}) {