diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc index b35ac23c71b..86017fb514e 100644 --- a/src/commands/cmd_json.cc +++ b/src/commands/cmd_json.cc @@ -167,6 +167,35 @@ class CommandJsonType : public Commander { } }; +class CommandJsonObjkeys : public Commander { + public: + Status Execute(Server *srv, Connection *conn, std::string *output) override { + redis::Json json(srv->storage, conn->GetNamespace()); + + std::vector>> results; + + // If path not specified set it to $ + std::string path = (args_.size() > 2) ? args_[2] : "$"; + auto s = json.ObjKeys(args_[1], path, results); + if (!s.ok() && !s.IsNotFound()) return {Status::RedisExecErr, s.ToString()}; + if (s.IsNotFound()) { + *output = redis::NilString(); + return Status::OK(); + } + + *output = redis::MultiLen(results.size()); + for (const auto &item : results) { + if (item.has_value()) { + *output += redis::MultiBulkString(item.value(), false); + } else { + *output += redis::NilString(); + } + } + + return Status::OK(); + } +}; + class CommandJsonClear : public Commander { public: Status Execute(Server *svr, Connection *conn, std::string *output) override { @@ -269,6 +298,7 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr("json.set", 4, "write", 1, 1 MakeCmdAttr("json.arrappend", -4, "write", 1, 1, 1), MakeCmdAttr("json.clear", -2, "write", 1, 1, 1), MakeCmdAttr("json.arrlen", -2, "read-only", 1, 1, 1), + MakeCmdAttr("json.objkeys", -2, "read-only", 1, 1, 1), MakeCmdAttr("json.arrpop", -2, "write", 1, 1, 1), ); } // namespace redis diff --git a/src/types/json.h b/src/types/json.h index d27022ef1d1..442bef76166 100644 --- a/src/types/json.h +++ b/src/types/json.h @@ -29,6 +29,7 @@ #include #include #include +#include #include "status.h" @@ -253,6 +254,26 @@ struct JsonValue { return Status::OK(); } + Status ObjKeys(std::string_view path, std::vector>> &keys) const { + try { + jsoncons::jsonpath::json_query(value, path, + [&keys](const std::string & /*path*/, const jsoncons::json &basic_json) { + if (basic_json.is_object()) { + std::vector ret; + for (const auto &member : basic_json.object_range()) { + ret.push_back(member.key()); + } + keys.emplace_back(ret); + } else { + keys.emplace_back(std::nullopt); + } + }); + } catch (const jsoncons::jsonpath::jsonpath_error &e) { + return {Status::NotOK, e.what()}; + } + return Status::OK(); + } + StatusOr>> ArrPop(std::string_view path, int64_t index = -1) { std::vector> popped_values; diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc index c95f849695c..44da953d3b3 100644 --- a/src/types/redis_json.cc +++ b/src/types/redis_json.cc @@ -252,4 +252,18 @@ rocksdb::Status Json::ArrPop(const std::string &user_key, const std::string &pat return write(ns_key, &metadata, json_val); } + +rocksdb::Status Json::ObjKeys(const std::string &user_key, const std::string &path, + std::vector>> &keys) { + auto ns_key = AppendNamespacePrefix(user_key); + JsonMetadata metadata; + JsonValue json_val; + auto s = read(ns_key, &metadata, &json_val); + if (!s.ok()) return s; + auto keys_res = json_val.ObjKeys(path, keys); + if (!keys_res) return rocksdb::Status::InvalidArgument(keys_res.Msg()); + + return rocksdb::Status::OK(); +} + } // namespace redis diff --git a/src/types/redis_json.h b/src/types/redis_json.h index 82b9e579279..708199af09d 100644 --- a/src/types/redis_json.h +++ b/src/types/redis_json.h @@ -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> &arr_lens); + rocksdb::Status ObjKeys(const std::string &user_key, const std::string &path, + std::vector>> &keys); rocksdb::Status ArrPop(const std::string &user_key, const std::string &path, int64_t index, std::vector> *results); diff --git a/tests/gocase/unit/type/json/json_test.go b/tests/gocase/unit/type/json/json_test.go index 61c4276c2f0..b5d741cd510 100644 --- a/tests/gocase/unit/type/json/json_test.go +++ b/tests/gocase/unit/type/json/json_test.go @@ -196,6 +196,31 @@ func TestJson(t *testing.T) { require.EqualValues(t, []uint64{}, lens) }) + t.Run("JSON.OBJKEYS basics", func(t *testing.T) { + require.NoError(t, rdb.Del(ctx, "a").Err()) + // key no exists + require.EqualError(t, rdb.Do(ctx, "JSON.OBJKEYS", "not_exists", "$").Err(), redis.Nil.Error()) + // key not json + require.NoError(t, rdb.Do(ctx, "SET", "no_json", "1").Err()) + require.Error(t, rdb.Do(ctx, "JSON.OBJKEYS", "no_json", "$").Err()) + // json path no exists + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a1":[1,2]}`).Err()) + require.EqualValues(t, []interface{}{}, rdb.Do(ctx, "JSON.OBJKEYS", "a", "$.not_exists").Val()) + // json path not object + require.EqualValues(t, []interface{}{nil}, rdb.Do(ctx, "JSON.OBJKEYS", "a", "$.a1").Val()) + // default path + require.EqualValues(t, []interface{}{[]interface{}{"a1"}}, rdb.Do(ctx, "JSON.OBJKEYS", "a").Val()) + // json path has one object + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a1":{"b":1,"c":1}}`).Err()) + require.EqualValues(t, []interface{}{[]interface{}{"b", "c"}}, rdb.Do(ctx, "JSON.OBJKEYS", "a", "$.a1").Val()) + // json path has many object + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":{"a1":{"b":1,"c":1}},"b":{"a1":{"e":1,"f":1}}}`).Err()) + require.EqualValues(t, []interface{}{[]interface{}{"b", "c"}, []interface{}{"e", "f"}}, rdb.Do(ctx, "JSON.OBJKEYS", "a", "$..a1").Val()) + // json path has many object and one is not object + require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":{"a1":{"b":1,"c":1}},"b":{"a1":[1]},"c":{"a1":{"e":1,"f":1}}}`).Err()) + require.EqualValues(t, []interface{}{[]interface{}{"b", "c"}, interface{}(nil), []interface{}{"e", "f"}}, rdb.Do(ctx, "JSON.OBJKEYS", "a", "$..a1").Val()) + }) + t.Run("JSON.ARRPOP basics", func(t *testing.T) { require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `[3,"str",2.1,{},[5,6]]`).Err()) require.EqualValues(t, []interface{}{"[5,6]"}, rdb.Do(ctx, "JSON.ARRPOP", "a").Val())