Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for JSON.STRAPPEND and JSON.STRLEN command #1841

Merged
merged 18 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion src/commands/cmd_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,61 @@ class CommandJsonNumMultBy : public Commander {
}
};

class CommandJsonStrAppend : public Commander {
public:
Status Execute(Server *svr, Connection *conn, std::string *output) override {
redis::Json json(svr->storage, conn->GetNamespace());

// path default, if not provided
std::string path = "$";
if (args_.size() == 4) {
path = args_[2];
} else if (args_.size() > 4) {
return {Status::RedisExecErr, "The number of arguments is more than expected"};
}

std::vector<uint64_t> results;
auto s = json.StrAppend(args_[1], path, args_[3], results);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};

*output = IntegerArray(results);
return Status::OK();
}

static std::string IntegerArray(const std::vector<uint64_t> &values) {
std::string result = "*" + std::to_string(values.size()) + CRLF;
for (const auto &value : values) {
if (value == std::numeric_limits<uint64_t>::max()) {
result += NilString();
} else {
result += Integer(value);
}
}
return result;
}
};

class CommandJsonStrLen : 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<uint64_t> results;
auto s = json.StrLen(args_[1], path, results);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};

*output = CommandJsonStrAppend::IntegerArray(results);
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),
Expand All @@ -586,6 +641,8 @@ REDIS_REGISTER_COMMANDS(MakeCmdAttr<CommandJsonSet>("json.set", 4, "write", 1, 1
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<CommandJsonObjLen>("json.objlen", -2, "read-only", 1, 1, 1));
MakeCmdAttr<CommandJsonObjLen>("json.objlen", -2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandJsonStrAppend>("json.strappend", -3, "write", 1, 1, 1),
MakeCmdAttr<CommandJsonStrLen>("json.strlen", -2, "read-only", 1, 1, 1), );

} // namespace redis
45 changes: 45 additions & 0 deletions src/types/json.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
#include <limits>
#include <string>

#include "common/string_util.h"
#include "jsoncons_ext/jsonpath/jsonpath_error.hpp"
#include "status.h"

constexpr ssize_t NOT_FOUND_INDEX = -1;
Expand Down Expand Up @@ -159,6 +161,49 @@ struct JsonValue {
return Status::OK();
}

Status StrAppend(std::string_view path, const std::string &append_value, std::vector<uint64_t> &append_cnt) {
try {
std::string append_str;
jsoncons::json append_json = jsoncons::json::parse(append_value);
if (append_json.is_string()) {
append_str = append_json.as_string();
} else {
return {Status::NotOK, "STRAPPEND need input a string to append"};
}

jsoncons::jsonpath::json_replace(
value, path, [&append_str, &append_cnt](const std::string & /*path*/, jsoncons::json &origin) {
if (origin.is_string()) {
auto origin_str = origin.as_string();
append_cnt.push_back(origin_str.length() + append_str.length());
origin = origin_str + append_str;
} else {
append_cnt.push_back(std::numeric_limits<uint64_t>::max());
}
});
} catch (const jsoncons::jsonpath::jsonpath_error &e) {
return {Status::NotOK, e.what()};
}

return Status::OK();
}

Status StrLen(std::string_view path, std::vector<uint64_t> &str_lens) const {
try {
jsoncons::jsonpath::json_query(value, path,
[&str_lens](const std::string & /*path*/, const jsoncons::json &origin) {
if (origin.is_string()) {
str_lens.push_back(origin.as_string().length());
} else {
str_lens.push_back(std::numeric_limits<uint64_t>::max());
}
});
} catch (const jsoncons::jsonpath::jsonpath_error &e) {
return {Status::NotOK, e.what()};
}
return Status::OK();
}

StatusOr<JsonValue> Get(std::string_view path) const {
try {
return jsoncons::jsonpath::json_query(value, path);
Expand Down
39 changes: 39 additions & 0 deletions src/types/redis_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,44 @@ rocksdb::Status Json::numop(JsonValue::NumOpEnum op, const std::string &user_key
return write(ns_key, &metadata, json_val);
}

rocksdb::Status Json::StrAppend(const std::string &user_key, const std::string &path, const std::string &value,
std::vector<uint64_t> &append_cnt) {
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 append_res = json_val.StrAppend(path, value, append_cnt);
if (!append_res) return rocksdb::Status::InvalidArgument(append_res.Msg());
// need reverse order
std::reverse(append_cnt.begin(), append_cnt.end());
git-hulk marked this conversation as resolved.
Show resolved Hide resolved

bool need_overwrite = false;
for (auto append : append_cnt) {
if (append != std::numeric_limits<uint64_t>::max()) {
need_overwrite = true;
}
}
if (!need_overwrite) {
return rocksdb::Status::OK();
}

return write(ns_key, &metadata, json_val);
guojidan marked this conversation as resolved.
Show resolved Hide resolved
}

rocksdb::Status Json::StrLen(const std::string &user_key, const std::string &path, std::vector<uint64_t> &str_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 append_res = json_val.StrLen(path, str_lens);
if (!append_res) return rocksdb::Status::InvalidArgument(append_res.Msg());
return rocksdb::Status::OK();
}

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);
Expand All @@ -476,4 +514,5 @@ rocksdb::Status Json::ObjLen(const std::string &user_key, const std::string &pat

return rocksdb::Status::OK();
}

} // namespace redis
3 changes: 3 additions & 0 deletions src/types/redis_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ 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 StrAppend(const std::string &user_key, const std::string &path, const std::string &value,
std::vector<uint64_t> &append_cnt);
rocksdb::Status StrLen(const std::string &user_key, const std::string &path, std::vector<uint64_t> &lens);
rocksdb::Status ObjLen(const std::string &user_key, const std::string &path,
std::vector<std::optional<uint64_t>> &obj_lens);

Expand Down
34 changes: 34 additions & 0 deletions tests/cppunit/types/json_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class RedisJsonTest : public TestBase {

std::unique_ptr<redis::Json> json_;
JsonValue json_val_;
std::vector<uint64_t> append_cnt_;
};

using ::testing::MatchesRegex;
Expand Down Expand Up @@ -658,3 +659,36 @@ TEST_F(RedisJsonTest, NumMultBy) {
ASSERT_TRUE(json_->Get(key_, {}, &json_val_).ok());
ASSERT_EQ(json_val_.Dump().GetValue(), R"({"l1":{"l2":[0,1.6350000000001313e+308]}})");
}

TEST_F(RedisJsonTest, StrAppend) {
ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})").ok());
ASSERT_TRUE(json_->StrAppend(key_, "$.a", "\"be\"", append_cnt_).ok());
ASSERT_EQ(append_cnt_.size(), 1);
ASSERT_EQ(append_cnt_[0], 5);

append_cnt_.clear();
ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})").ok());
ASSERT_TRUE(json_->StrAppend(key_, "$..a", "\"be\"", append_cnt_).ok());
ASSERT_EQ(append_cnt_.size(), 3);
std::vector<int64_t> result1 = {5, 7, -1};
for (int i = 0; i < 3; ++i) {
ASSERT_EQ(append_cnt_[i], result1[i]);
}
}

TEST_F(RedisJsonTest, StrLen) {
append_cnt_.clear();
ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})").ok());
ASSERT_TRUE(json_->StrLen(key_, "$.a", append_cnt_).ok());
ASSERT_EQ(append_cnt_.size(), 1);
ASSERT_EQ(append_cnt_[0], 3);

append_cnt_.clear();
ASSERT_TRUE(json_->Set(key_, "$", R"({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})").ok());
ASSERT_TRUE(json_->StrLen(key_, "$..a", append_cnt_).ok());
ASSERT_EQ(append_cnt_.size(), 3);
std::vector<int64_t> result1 = {3, 5, -1};
for (int i = 0; i < 3; ++i) {
ASSERT_EQ(append_cnt_[i], result1[i]);
}
}
24 changes: 24 additions & 0 deletions tests/gocase/unit/type/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,30 @@ func TestJson(t *testing.T) {
require.EqualError(t, err, redis.Nil.Error())
})

t.Run("JSON.STRAPPEND basics", func(t *testing.T) {
var result1 = make([]interface{}, 0)
result1 = append(result1, int64(5))
require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
require.Equal(t, rdb.Do(ctx, "JSON.STRAPPEND", "a", "$.a", "\"be\"").Val(), result1)

var result2 = make([]interface{}, 0)
result2 = append(result2, int64(5), int64(7), interface{}(nil))
require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
require.Equal(t, rdb.Do(ctx, "JSON.STRAPPEND", "a", "$..a", "\"be\"").Val(), result2)
})

t.Run("JSON.STRLEN basics", func(t *testing.T) {
var result1 = make([]interface{}, 0)
result1 = append(result1, int64(3))
require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
require.Equal(t, rdb.Do(ctx, "JSON.STRLEN", "a", "$.a").Val(), result1)

var result2 = make([]interface{}, 0)
result2 = append(result2, int64(3), int64(5), interface{}(nil))
require.NoError(t, rdb.Do(ctx, "JSON.SET", "a", "$", `{"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}}`).Err())
require.Equal(t, rdb.Do(ctx, "JSON.STRLEN", "a", "$..a").Val(), result2)
})

t.Run("Merge basics", func(t *testing.T) {
require.NoError(t, rdb.Do(ctx, "JSON.SET", "key", "$", `{"a":2}`).Err())
require.NoError(t, rdb.Do(ctx, "JSON.MERGE", "key", "$.a", `3`).Err())
Expand Down