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 04cf2176 support JSON.NUMINCRBY and JSON.NUMMULTBY command (#1890)
04cf2176 is described below
commit 04cf217671438e8fee06bd654c928400d4bb4efa
Author: skyitachi <[email protected]>
AuthorDate: Fri Dec 1 20:47:43 2023 +0800
support JSON.NUMINCRBY and JSON.NUMMULTBY command (#1890)
---
src/commands/cmd_json.cc | 38 +++++++++-
src/types/json.h | 43 ++++++++++++
src/types/redis_json.cc | 33 +++++++++
src/types/redis_json.h | 6 ++
tests/cppunit/types/json_test.cc | 117 +++++++++++++++++++++++++++++++
tests/gocase/unit/type/json/json_test.go | 52 ++++++++++++++
6 files changed, 288 insertions(+), 1 deletion(-)
diff --git a/src/commands/cmd_json.cc b/src/commands/cmd_json.cc
index 26a33e49..9f0e8ffc 100644
--- a/src/commands/cmd_json.cc
+++ b/src/commands/cmd_json.cc
@@ -500,6 +500,40 @@ class CommandJsonDel : public Commander {
}
};
+class CommandJsonNumIncrBy : public Commander {
+ public:
+ Status Execute(Server *svr, Connection *conn, std::string *output) override {
+ redis::Json json(svr->storage, conn->GetNamespace());
+
+ JsonValue result = JsonValue::FromString("[]").GetValue();
+ auto s = json.NumIncrBy(args_[1], args_[2], args_[3], &result);
+ if (!s.ok()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ *output = redis::BulkString(result.value.to_string());
+
+ return Status::OK();
+ }
+};
+
+class CommandJsonNumMultBy : public Commander {
+ public:
+ Status Execute(Server *svr, Connection *conn, std::string *output) override {
+ redis::Json json(svr->storage, conn->GetNamespace());
+
+ JsonValue result = JsonValue::FromString("[]").GetValue();
+ auto s = json.NumMultBy(args_[1], args_[2], args_[3], &result);
+ if (!s.ok()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ *output = redis::BulkString(result.value.to_string());
+
+ return Status::OK();
+ }
+};
+
REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1,
1, 1),
MakeCmdAttr<CommandJsonGet>("json.get", -2,
"read-only", 1, 1, 1),
MakeCmdAttr<CommandJsonInfo>("json.info", 2,
"read-only", 1, 1, 1),
@@ -514,6 +548,8 @@
REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 1
MakeCmdAttr<CommandJsonObjkeys>("json.objkeys", -2,
"read-only", 1, 1, 1),
MakeCmdAttr<CommandJsonArrPop>("json.arrpop", -2,
"write", 1, 1, 1),
MakeCmdAttr<CommanderJsonArrIndex>("json.arrindex",
-4, "read-only", 1, 1, 1),
- MakeCmdAttr<CommandJsonDel>("json.del", -2, "write",
1, 1, 1), );
+ MakeCmdAttr<CommandJsonDel>("json.del", -2, "write",
1, 1, 1),
+ MakeCmdAttr<CommandJsonNumIncrBy>("json.numincrby", 4,
"write", 1, 1, 1),
+ MakeCmdAttr<CommandJsonNumMultBy>("json.nummultby", 4,
"write", 1, 1, 1), );
} // namespace redis
diff --git a/src/types/json.h b/src/types/json.h
index 15a01c3d..5a369e20 100644
--- a/src/types/json.h
+++ b/src/types/json.h
@@ -41,6 +41,11 @@ constexpr ssize_t NOT_FOUND_INDEX = -1;
constexpr ssize_t NOT_ARRAY = -2;
struct JsonValue {
+ enum class NumOpEnum : uint8_t {
+ Incr = 1,
+ Mul = 2,
+ };
+
JsonValue() = default;
explicit JsonValue(jsoncons::basic_json<char> value) :
value(std::move(value)) {}
@@ -486,6 +491,44 @@ struct JsonValue {
return count;
}
+ Status NumOp(std::string_view path, const JsonValue &number, NumOpEnum op,
JsonValue *result) {
+ Status status = Status::OK();
+ try {
+ jsoncons::jsonpath::json_replace(value, path, [&](const std::string &
/*path*/, jsoncons::json &origin) {
+ if (!status.IsOK()) {
+ return;
+ }
+ if (!origin.is_number()) {
+ result->value.push_back(jsoncons::json::null());
+ return;
+ }
+ if (number.value.is_double() || origin.is_double()) {
+ double v = 0;
+ if (op == NumOpEnum::Incr) {
+ v = origin.as_double() + number.value.as_double();
+ } else if (op == NumOpEnum::Mul) {
+ v = origin.as_double() * number.value.as_double();
+ }
+ if (std::isinf(v)) {
+ status = {Status::RedisExecErr, "result is an infinite number"};
+ return;
+ }
+ origin = v;
+ } else {
+ if (op == NumOpEnum::Incr) {
+ origin = origin.as_integer<int64_t>() +
number.value.as_integer<int64_t>();
+ } else if (op == NumOpEnum::Mul) {
+ origin = origin.as_integer<int64_t>() *
number.value.as_integer<int64_t>();
+ }
+ }
+ result->value.push_back(origin);
+ });
+ } catch (const jsoncons::jsonpath::jsonpath_error &e) {
+ return {Status::NotOK, e.what()};
+ }
+ return status;
+ }
+
JsonValue(const JsonValue &) = default;
JsonValue(JsonValue &&) = default;
diff --git a/src/types/redis_json.cc b/src/types/redis_json.cc
index 32bc6bb8..621d83f6 100644
--- a/src/types/redis_json.cc
+++ b/src/types/redis_json.cc
@@ -421,7 +421,40 @@ rocksdb::Status Json::Del(const std::string &user_key,
const std::string &path,
if (*result == 0) {
return rocksdb::Status::OK();
}
+ return write(ns_key, &metadata, json_val);
+}
+
+rocksdb::Status Json::NumIncrBy(const std::string &user_key, const std::string
&path, const std::string &value,
+ JsonValue *result) {
+ return numop(JsonValue::NumOpEnum::Incr, user_key, path, value, result);
+}
+
+rocksdb::Status Json::NumMultBy(const std::string &user_key, const std::string
&path, const std::string &value,
+ JsonValue *result) {
+ return numop(JsonValue::NumOpEnum::Mul, user_key, path, value, result);
+}
+
+rocksdb::Status Json::numop(JsonValue::NumOpEnum op, const std::string
&user_key, const std::string &path,
+ const std::string &value, JsonValue *result) {
+ JsonValue number;
+ auto number_res = JsonValue::FromString(value);
+ if (!number_res || !number_res.GetValue().value.is_number()) {
+ return rocksdb::Status::InvalidArgument("should be a number");
+ }
+ number = std::move(number_res.GetValue());
+
+ auto ns_key = AppendNamespacePrefix(user_key);
+ JsonMetadata metadata;
+ JsonValue json_val;
+ auto s = read(ns_key, &metadata, &json_val);
+ if (!s.ok()) return s;
+ LockGuard guard(storage_->GetLockManager(), ns_key);
+
+ auto res = json_val.NumOp(path, number, op, result);
+ if (!res) {
+ return rocksdb::Status::InvalidArgument(res.Msg());
+ }
return write(ns_key, &metadata, json_val);
}
diff --git a/src/types/redis_json.h b/src/types/redis_json.h
index d42ad4be..246a3c6d 100644
--- a/src/types/redis_json.h
+++ b/src/types/redis_json.h
@@ -53,6 +53,10 @@ class Json : public Database {
std::vector<std::optional<JsonValue>> *results);
rocksdb::Status ArrIndex(const std::string &user_key, const std::string
&path, const std::string &needle,
ssize_t start, ssize_t end, std::vector<ssize_t>
*result);
+ rocksdb::Status NumIncrBy(const std::string &user_key, const std::string
&path, const std::string &value,
+ JsonValue *result);
+ rocksdb::Status NumMultBy(const std::string &user_key, const std::string
&path, const std::string &value,
+ JsonValue *result);
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);
@@ -63,6 +67,8 @@ class Json : public Database {
rocksdb::Status read(const Slice &ns_key, JsonMetadata *metadata, JsonValue
*value);
rocksdb::Status create(const std::string &ns_key, JsonMetadata &metadata,
const std::string &value);
rocksdb::Status del(const Slice &ns_key);
+ rocksdb::Status numop(JsonValue::NumOpEnum op, const std::string &user_key,
const std::string &path,
+ const std::string &value, JsonValue *result);
};
} // namespace redis
diff --git a/tests/cppunit/types/json_test.cc b/tests/cppunit/types/json_test.cc
index 7083cb72..56290451 100644
--- a/tests/cppunit/types/json_test.cc
+++ b/tests/cppunit/types/json_test.cc
@@ -535,3 +535,120 @@ TEST_F(RedisJsonTest, Del) {
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"nested":{"b":3}})");
ASSERT_EQ(result, 2);
}
+
+TEST_F(RedisJsonTest, NumIncrBy) {
+ ASSERT_TRUE(json_->Set(key_, "$", R"({ "foo": 0, "bar": "baz" })").ok());
+ JsonValue res = JsonValue::FromString("[]").GetValue();
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.foo", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1]");
+ res.value.clear();
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.foo", "2", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[3]");
+ res.value.clear();
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.foo", "0.5", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[3.5]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.bar", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[null]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.fuzz", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->Set(key_, "$", "0").ok());
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$", "0", &res).ok());
+ res.value.clear();
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1]");
+ res.value.clear();
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$", "1.5", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[2.5]");
+ res.value.clear();
+
+ // overflow case
+ ASSERT_TRUE(json_->Set(key_, "$", "1.6350000000001313e+308").ok());
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.6350000000001313e+308]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$", "2", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.6350000000001313e+308]");
+ res.value.clear();
+
+ // nested big_num object
+ ASSERT_TRUE(json_->Set(key_, "$",
R"({"l1":{"l2_a":1.6350000000001313e+308,"l2_b":2}})").ok());
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.l1.l2_a", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.6350000000001313e+308]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->NumIncrBy(key_, "$.l1.l2_a", "2", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.6350000000001313e+308]");
+ res.value.clear();
+
+ // nested big_num array
+ ASSERT_TRUE(json_->Set(key_, "$",
R"({"l1":{"l2":[0,1.6350000000001313e+308]}})").ok());
+ ASSERT_FALSE(json_->NumIncrBy(key_, "$.l1.l2[1]", "1.6350000000001313e+308",
&res).ok());
+ res.value.clear();
+
+ ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+ ASSERT_EQ(json_val_.Dump().GetValue(),
R"({"l1":{"l2":[0,1.6350000000001313e+308]}})");
+}
+
+TEST_F(RedisJsonTest, NumMultBy) {
+ ASSERT_TRUE(json_->Set(key_, "$", R"({ "foo": 1, "bar": "baz" })").ok());
+ JsonValue res = JsonValue::FromString("[]").GetValue();
+ ASSERT_TRUE(json_->NumMultBy(key_, "$.foo", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1]");
+ res.value.clear();
+ ASSERT_TRUE(json_->NumMultBy(key_, "$.foo", "2", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[2]");
+ res.value.clear();
+ ASSERT_TRUE(json_->NumMultBy(key_, "$.foo", "0.5", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.0]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->NumMultBy(key_, "$.bar", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[null]");
+ res.value.clear();
+
+ ASSERT_TRUE(json_->NumMultBy(key_, "$.fuzz", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[]");
+ res.value.clear();
+
+ // num object
+ ASSERT_TRUE(json_->Set(key_, "$", "1.0").ok());
+ ASSERT_TRUE(json_->NumMultBy(key_, "$", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.0]");
+ res.value.clear();
+ ASSERT_TRUE(json_->NumMultBy(key_, "$", "1.5", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.5]");
+ res.value.clear();
+
+ // overflow case
+ ASSERT_TRUE(json_->Set(key_, "$", "1.6350000000001313e+308").ok());
+ ASSERT_TRUE(json_->NumMultBy(key_, "$", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.6350000000001313e+308]");
+ res.value.clear();
+
+ ASSERT_FALSE(json_->NumMultBy(key_, "$", "2", &res).ok());
+ res.value.clear();
+
+ // nested big_num object
+ ASSERT_TRUE(json_->Set(key_, "$",
R"({"l1":{"l2_a":1.6350000000001313e+308,"l2_b":2}})").ok());
+ ASSERT_TRUE(json_->NumMultBy(key_, "$.l1.l2_a", "1", &res).ok());
+ ASSERT_EQ(res.Print(0, true).GetValue(), "[1.6350000000001313e+308]");
+ res.value.clear();
+
+ ASSERT_FALSE(json_->NumMultBy(key_, "$.l1.l2_a", "2", &res).ok());
+ res.value.clear();
+
+ // nested big_num array
+ ASSERT_TRUE(json_->Set(key_, "$",
R"({"l1":{"l2":[0,1.6350000000001313e+308]}})").ok());
+ ASSERT_FALSE(json_->NumMultBy(key_, "$.l1.l2[1]", "2", &res).ok());
+ res.value.clear();
+
+ ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
+ ASSERT_EQ(json_val_.Dump().GetValue(),
R"({"l1":{"l2":[0,1.6350000000001313e+308]}})");
+}
diff --git a/tests/gocase/unit/type/json/json_test.go
b/tests/gocase/unit/type/json/json_test.go
index c226c327..7b257309 100644
--- a/tests/gocase/unit/type/json/json_test.go
+++ b/tests/gocase/unit/type/json/json_test.go
@@ -462,4 +462,56 @@ func TestJson(t *testing.T) {
require.NoError(t, rdb.Do(ctx, "JSON.SET", "a1", "$",
`{"arr":[[1],[2],[3]]}`).Err())
require.Equal(t, []interface{}{int64(0), int64(-1), int64(-1)},
rdb.Do(ctx, arrIndexCmd, "a1", "$.arr.*", `1`).Val())
})
+
+ t.Run("JSON.NUMOP basics", func(t *testing.T) {
+ require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{ "foo":
0, "bar": "baz" }`).Err())
+ require.Equal(t, `[1]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$.foo", 1).Val())
+ require.Equal(t, `[1]`, rdb.Do(ctx, "JSON.GET", "a",
"$.foo").Val())
+ require.Equal(t, `[3]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$.foo", 2).Val())
+ require.Equal(t, `[3.5]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$.foo", 0.5).Val())
+
+ // wrong type
+ require.Equal(t, `[null]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$.bar", 1).Val())
+
+ require.Equal(t, `[]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$.fuzz", 1).Val())
+
+ require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `0`).Err())
+ require.Equal(t, `[1]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a", "$",
1).Val())
+ require.Equal(t, `[2.5]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$", 1.5).Val())
+
+ require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$",
`{"foo":0,"bar":42}`).Err())
+ require.Equal(t, `[1]`, rdb.Do(ctx, "JSON.NUMINCRBY", "a",
"$.foo", 1).Val())
+ require.Equal(t, `[84]`, rdb.Do(ctx, "JSON.NUMMULTBY", "a",
"$.bar", 2).Val())
+
+ // overflow case
+ require.NoError(t, rdb.Do(ctx, "JSON.SET", "big_num", "$",
"1.6350000000001313e+308").Err())
+ require.Equal(t, `[1.6350000000001313e+308]`,
+ rdb.Do(ctx, "JSON.NUMINCRBY", "big_num", "$", 1).Val())
+
+ require.Error(t, rdb.Do(ctx, "JSON.NUMMULTBY", "big_num", "$",
2).Err())
+
+ require.Equal(t, `1.6350000000001313e+308`, rdb.Do(ctx,
"JSON.GET", "big_num").Val())
+
+ require.NoError(t, rdb.Do(ctx, "JSON.SET",
"nested_obj_big_num", "$",
+
`{"l1":{"l2_a":1.6350000000001313e+308,"l2_b":2}}`).Err())
+
+ require.Equal(t, `[1.6350000000001313e+308]`,
+ rdb.Do(ctx, "JSON.NUMINCRBY", "nested_obj_big_num",
"$.l1.l2_a", 1).Val())
+
+ require.Error(t, rdb.Do(ctx, "JSON.NUMMULTBY",
"nested_obj_big_num", "$.l1.l2_a", 2).Err())
+
+ require.Equal(t,
`{"l1":{"l2_a":1.6350000000001313e+308,"l2_b":2}}`,
+ rdb.Do(ctx, "JSON.GET", "nested_obj_big_num").Val())
+
+ require.NoError(t, rdb.Do(ctx, "JSON.SET",
"nested_arr_big_num", "$",
+ `{"l1":{"l2":[0,1.6350000000001313e+308]}}`).Err())
+
+ require.Error(t, rdb.Do(ctx, "JSON.NUMINCRBY",
"nested_arr_big_num", "$.l1.l2[1]",
+ `1.6350000000001313e+308`).Err())
+ require.Error(t, rdb.Do(ctx, "JSON.NUMMULTBY",
"nested_arr_big_num", "$.l1.l2[1]", 2).Err())
+
+ require.Equal(t, `{"l1":{"l2":[0,1.6350000000001313e+308]}}`,
+ rdb.Do(ctx, "JSON.GET", "nested_arr_big_num").Val())
+ })
+
}