This is an automated email from the ASF dual-hosted git repository.
hulk pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new a79a386a Support for the JSON.OBJKEYS command (#1872)
a79a386a is described below
commit a79a386a71095b5a700c56b3d557cb79119618bb
Author: 纪华裕 <[email protected]>
AuthorDate: Tue Nov 7 13:01:10 2023 +0800
Support for the JSON.OBJKEYS command (#1872)
---
src/commands/cmd_json.cc | 30 ++++++++++++++++++++++++++++++
src/types/json.h | 21 +++++++++++++++++++++
src/types/redis_json.cc | 14 ++++++++++++++
src/types/redis_json.h | 2 ++
tests/gocase/unit/type/json/json_test.go | 25 +++++++++++++++++++++++++
5 files changed, 92 insertions(+)
diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc
index b35ac23c..86017fb5 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<std::optional<std::vector<std::string>>> 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<CommandJsonSet>("json.set", 4, "write", 1, 1
MakeCmdAttr<CommandJsonArrAppend>("json.arrappend",
-4, "write", 1, 1, 1),
MakeCmdAttr<CommandJsonClear>("json.clear", -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
diff --git a/src/types/json.h b/src/types/json.h
index d27022ef..442bef76 100644
--- a/src/types/json.h
+++ b/src/types/json.h
@@ -29,6 +29,7 @@
#include <jsoncons_ext/jsonpath/json_query.hpp>
#include <jsoncons_ext/jsonpath/jsonpath_error.hpp>
#include <limits>
+#include <string>
#include "status.h"
@@ -253,6 +254,26 @@ struct JsonValue {
return Status::OK();
}
+ Status ObjKeys(std::string_view path,
std::vector<std::optional<std::vector<std::string>>> &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<std::string> 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<std::vector<std::optional<JsonValue>>> ArrPop(std::string_view
path, int64_t index = -1) {
std::vector<std::optional<JsonValue>> popped_values;
diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc
index c95f8496..44da953d 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<std::optional<std::vector<std::string>>> &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 82b9e579..708199af 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<std::optional<uint64_t>> &arr_lens);
+ 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,
std::vector<std::optional<JsonValue>> *results);
diff --git a/tests/gocase/unit/type/json/json_test.go
b/tests/gocase/unit/type/json/json_test.go
index 61c4276c..b5d741cd 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())