You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kvrocks.apache.org by hu...@apache.org on 2023/12/05 01:55:46 UTC

(kvrocks) branch unstable updated: Support for the JSON.OBJLEN command (#1860)

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 b889b0e6 Support for the JSON.OBJLEN command (#1860)
b889b0e6 is described below

commit b889b0e6fed8e9552333cc8bb26b34968f59a7f4
Author: Xin Huang <57...@users.noreply.github.com>
AuthorDate: Tue Dec 5 09:55:40 2023 +0800

    Support for the JSON.OBJLEN command (#1860)
    
    Co-authored-by: git-hulk <hu...@gmail.com>
---
 src/commands/cmd_json.cc                 | 36 +++++++++++++++++++++++++++++++-
 src/types/json.h                         | 16 ++++++++++++++
 src/types/redis_json.cc                  | 13 ++++++++++++
 src/types/redis_json.h                   |  2 ++
 tests/gocase/unit/type/json/json_test.go | 27 ++++++++++++++++++++++++
 5 files changed, 93 insertions(+), 1 deletion(-)

diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc
index 396c0e85..a6c47ef1 100644
--- a/src/commands/cmd_json.cc
+++ b/src/commands/cmd_json.cc
@@ -389,6 +389,39 @@ class CommandJsonArrPop : public Commander {
   int64_t index_ = -1;
 };
 
+class CommandJsonObjLen : public Commander {
+ public:
+  Status Execute(Server *svr, Connection *conn, std::string *output) override {
+    redis::Json json(svr->storage, conn->GetNamespace());
+
+    std::string path = "$";
+    if (args_.size() == 3) {
+      path = args_[2];
+    } else if (args_.size() > 3) {
+      return {Status::RedisExecErr, "The number of arguments is more than expected"};
+    }
+
+    std::vector<std::optional<uint64_t>> results;
+    auto s = json.ObjLen(args_[1], path, results);
+    if (s.IsNotFound()) {
+      *output = redis::NilString();
+      return Status::OK();
+    }
+    if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
+
+    *output = redis::MultiLen(results.size());
+    for (const auto &len : results) {
+      if (len.has_value()) {
+        *output += redis::Integer(len.value());
+      } else {
+        *output += redis::NilString();
+      }
+    }
+
+    return Status::OK();
+  }
+};
+
 class CommandJsonArrTrim : public Commander {
  public:
   Status Parse(const std::vector<std::string> &args) override {
@@ -552,6 +585,7 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 1
                         // JSON.FORGET is an alias for JSON.DEL, refer: https://redis.io/commands/json.forget/
                         MakeCmdAttr<CommandJsonDel>("json.forget", -2, "write", 1, 1, 1),
                         MakeCmdAttr<CommandJsonNumIncrBy>("json.numincrby", 4, "write", 1, 1, 1),
-                        MakeCmdAttr<CommandJsonNumMultBy>("json.nummultby", 4, "write", 1, 1, 1), );
+                        MakeCmdAttr<CommandJsonNumMultBy>("json.nummultby", 4, "write", 1, 1, 1),
+                        MakeCmdAttr<CommandJsonObjLen>("json.objlen", -2, "read-only", 1, 1, 1));
 
 }  // namespace redis
diff --git a/src/types/json.h b/src/types/json.h
index 5a369e20..672437e9 100644
--- a/src/types/json.h
+++ b/src/types/json.h
@@ -422,6 +422,22 @@ struct JsonValue {
     return Status::OK();
   }
 
+  Status ObjLen(std::string_view path, std::vector<std::optional<uint64_t>> &obj_lens) const {
+    try {
+      jsoncons::jsonpath::json_query(value, path,
+                                     [&obj_lens](const std::string & /*path*/, const jsoncons::json &basic_json) {
+                                       if (basic_json.is_object()) {
+                                         obj_lens.emplace_back(static_cast<uint64_t>(basic_json.size()));
+                                       } else {
+                                         obj_lens.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 ee4f3678..7db5c709 100644
--- a/src/types/redis_json.cc
+++ b/src/types/redis_json.cc
@@ -463,4 +463,17 @@ rocksdb::Status Json::numop(JsonValue::NumOpEnum op, const std::string &user_key
   return write(ns_key, &metadata, json_val);
 }
 
+rocksdb::Status Json::ObjLen(const std::string &user_key, const std::string &path,
+                             std::vector<std::optional<uint64_t>> &obj_lens) {
+  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 len_res = json_val.ObjLen(path, obj_lens);
+  if (!len_res) return rocksdb::Status::InvalidArgument(len_res.Msg());
+
+  return rocksdb::Status::OK();
+}
 }  // namespace redis
diff --git a/src/types/redis_json.h b/src/types/redis_json.h
index 246a3c6d..732dc949 100644
--- a/src/types/redis_json.h
+++ b/src/types/redis_json.h
@@ -61,6 +61,8 @@ class Json : public Database {
   rocksdb::Status ArrTrim(const std::string &user_key, const std::string &path, int64_t start, int64_t stop,
                           std::vector<std::optional<uint64_t>> &results);
   rocksdb::Status Del(const std::string &user_key, const std::string &path, size_t *result);
+  rocksdb::Status ObjLen(const std::string &user_key, const std::string &path,
+                         std::vector<std::optional<uint64_t>> &obj_lens);
 
  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 a2d0485f..aee0ee69 100644
--- a/tests/gocase/unit/type/json/json_test.go
+++ b/tests/gocase/unit/type/json/json_test.go
@@ -530,4 +530,31 @@ func TestJson(t *testing.T) {
 			rdb.Do(ctx, "JSON.GET", "nested_arr_big_num").Val())
 	})
 
+	t.Run("JSON.OBJLEN basics", func(t *testing.T) {
+		require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"x":[3], "nested": {"x": {"y":2, "z": 1}}}`).Err())
+		vals, err := rdb.Do(ctx, "JSON.OBJLEN", "a", "$..x").Slice()
+		require.NoError(t, err)
+		require.EqualValues(t, 2, len(vals))
+		// the first `x` path is not an object, so it should return nil
+		require.EqualValues(t, nil, vals[0])
+		// the second `x` path is an object with two fields, so it should return 2
+		require.EqualValues(t, 2, vals[1])
+
+		vals, err = rdb.Do(ctx, "JSON.OBJLEN", "a", "$.nested").Slice()
+		require.NoError(t, err)
+		require.EqualValues(t, 1, len(vals))
+		require.EqualValues(t, 1, vals[0])
+
+		vals, err = rdb.Do(ctx, "JSON.OBJLEN", "a", "$").Slice()
+		require.NoError(t, err)
+		require.EqualValues(t, 1, len(vals))
+		require.EqualValues(t, 2, vals[0])
+
+		vals, err = rdb.Do(ctx, "JSON.OBJLEN", "a", "$.no_exists_path").Slice()
+		require.NoError(t, err)
+		require.EqualValues(t, 0, len(vals))
+
+		err = rdb.Do(ctx, "JSON.OBJLEN", "no-such-json-key", "$").Err()
+		require.EqualError(t, err, redis.Nil.Error())
+	})
 }