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

Support for the JSON.OBJKEYS command #1872

Merged
merged 17 commits into from
Nov 7, 2023
30 changes: 30 additions & 0 deletions src/commands/cmd_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
21 changes: 21 additions & 0 deletions src/types/json.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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;

Expand Down
14 changes: 14 additions & 0 deletions src/types/redis_json.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/types/redis_json.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
25 changes: 25 additions & 0 deletions tests/gocase/unit/type/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down