diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7210740926ee..771a316abca0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,19 @@ add_third_party( LIB libdouble-conversion.a ) +add_third_party( + jsoncons + URL https://github.com/danielaparker/jsoncons/archive/refs/tags/v0.168.7.tar.gz + CMAKE_PASS_FLAGS "-DJSONCONS_BUILD_TESTS=OFF" + + LIB "none" +) + +add_library(TRDP::jsoncons INTERFACE IMPORTED) +add_dependencies(TRDP::jsoncons jsoncons_project) +set_target_properties(TRDP::jsoncons PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${JSONCONS_INCLUDE_DIR}") + Message(STATUS "THIRD_PARTY_LIB_DIR ${THIRD_PARTY_LIB_DIR}") diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 9dd5b4e1095f..599d97142f08 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -14,3 +14,4 @@ cxx_test(extent_tree_test dfly_core LABELS DFLY) cxx_test(external_alloc_test dfly_core LABELS DFLY) cxx_test(dash_test dfly_core LABELS DFLY) cxx_test(interpreter_test dfly_core LABELS DFLY) +cxx_test(json_test dfly_core TRDP::jsoncons LABELS DFLY) diff --git a/src/core/json_test.cc b/src/core/json_test.cc new file mode 100644 index 000000000000..de837cbc23bb --- /dev/null +++ b/src/core/json_test.cc @@ -0,0 +1,119 @@ +// Copyright 2022, Roman Gershman. All rights reserved. +// See LICENSE for licensing terms. +// + +#include +#include + +#include "base/gtest.h" +#include "base/logging.h" + +namespace dfly { +using namespace std; +using namespace jsoncons; +using namespace jsoncons::literals; + +class JsonTest : public ::testing::Test { + protected: + JsonTest() { + } +}; + +TEST_F(JsonTest, Basic) { + string data = R"( + { + "application": "hiking", + "reputons": [ + { + "rater": "HikingAsylum", + "assertion": "advanced", + "rated": "Marilyn C", + "rating": 0.90, + "confidence": 0.99 + } + ] + } +)"; + + json j = json::parse(data); + EXPECT_TRUE(j.contains("reputons")); + jsonpath::json_replace(j, "$.reputons[*].rating", 1.1); + EXPECT_EQ(1.1, j["reputons"][0]["rating"].as_double()); +} + +TEST_F(JsonTest, Query) { + json j = R"( +{"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}} +)"_json; + + json out = jsonpath::json_query(j, "$..*"); + EXPECT_EQ(R"([{},{"a":1},{"a":1,"b":2},1,1,2])"_json, out); + + json j2 = R"( + {"firstName":"John","lastName":"Smith","age":27,"weight":135.25,"isAlive":true,"address":{"street":"21 2nd Street","city":"New York","state":"NY","zipcode":"10021-3100"},"phoneNumbers":[{"type":"home","number":"212 555-1234"},{"type":"office","number":"646 555-4567"}],"children":[],"spouse":null} + )"_json; + + // json_query always returns arrays. + // See here: https://github.com/danielaparker/jsoncons/issues/82 + // Therefore we are going to only support the "extended" semantics + // of json API (as they are called in AWS documentation). + out = jsonpath::json_query(j2, "$.address"); + EXPECT_EQ(R"([{"street":"21 2nd Street","city":"New York", + "state":"NY","zipcode":"10021-3100"}])"_json, + out); +} + +TEST_F(JsonTest, Errors) { + auto cb = [](json_errc err, const ser_context& contexts) { return false; }; + + json_decoder decoder; + basic_json_parser parser(basic_json_decode_options{}, cb); + + string_view input{"\000bla"}; + parser.update(input.data(), input.size()); + parser.parse_some(decoder); + parser.finish_parse(decoder); + parser.check_done(); + EXPECT_FALSE(decoder.is_valid()); +} + +TEST_F(JsonTest, Delete) { + json j1 = R"({"c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}, "e": [1,2]})"_json; + + auto deleter = [](const json::string_view_type& path, json& val) { + LOG(INFO) << "path: " << path; + // val.evaluate(); + // if (val.is_object()) + // val.erase(val.object_range().begin(), val.object_range().end()); + }; + jsonpath::json_replace(j1, "$.d.*", deleter); + + auto expr = jsonpath::make_expression("$.d.*"); + + auto callback = [](const std::string& path, const json& val) { + LOG(INFO) << path << ": " << val << "\n"; + }; + expr.evaluate(j1, callback, jsonpath::result_options::path); + auto it = j1.find("d"); + ASSERT_TRUE(it != j1.object_range().end()); + + it->value().erase("a"); + EXPECT_EQ(R"({"c":{"a":1, "b":2}, "d":{"b":2, "c":3}, "e": [1,2]})"_json, j1); +} + +TEST_F(JsonTest, DeleteExt) { + jsonpath::detail::static_resources resources; + jsonpath::jsonpath_expression::evaluator_t eval; + jsonpath::jsonpath_expression::json_selector_t sel = eval.compile(resources, "$.d.*"); + json j1 = R"({"c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}, "e": [1,2]})"_json; + + jsoncons::jsonpath::detail::dynamic_resources dyn_res; + + auto f = [](const jsonpath::json_location& path, const json& val) { + LOG(INFO) << path.to_string(); + }; + + sel.evaluate(dyn_res, j1, dyn_res.root_path_node(), j1, f, jsonpath::result_options::path); +} + +} // namespace dfly diff --git a/src/facade/error.h b/src/facade/error.h index 51913f72927a..4efda1e2a37b 100644 --- a/src/facade/error.h +++ b/src/facade/error.h @@ -30,5 +30,6 @@ extern const char kSyntaxErrType[]; extern const char kScriptErrType[]; extern const char kIndexOutOfRange[]; extern const char kOutOfMemory[]; +extern const char kInvalidNumericResult[]; } // namespace dfly diff --git a/src/facade/facade.cc b/src/facade/facade.cc index d75f4aeed96b..3a9ec19f72c9 100644 --- a/src/facade/facade.cc +++ b/src/facade/facade.cc @@ -80,6 +80,7 @@ const char kSyntaxErrType[] = "syntax_error"; const char kScriptErrType[] = "script_error"; const char kIndexOutOfRange[] = "index out of range"; const char kOutOfMemory[] = "Out of memory"; +const char kInvalidNumericResult[] = "result is not a number"; const char* RespExpr::TypeName(Type t) { switch (t) { diff --git a/src/facade/op_status.h b/src/facade/op_status.h index 84b0c9630298..3d4c7c72d6bf 100644 --- a/src/facade/op_status.h +++ b/src/facade/op_status.h @@ -24,6 +24,7 @@ enum class OpStatus : uint16_t { BUSY_GROUP, STREAM_ID_SMALL, ENTRIES_ADDED_SMALL, + INVALID_NUMERIC_RESULT, }; class OpResultBase { diff --git a/src/facade/reply_builder.cc b/src/facade/reply_builder.cc index 08fc0820ee19..1213a6f29e40 100644 --- a/src/facade/reply_builder.cc +++ b/src/facade/reply_builder.cc @@ -247,6 +247,9 @@ void RedisReplyBuilder::SendError(OpStatus status) { case OpStatus::BUSY_GROUP: SendError("-BUSYGROUP Consumer Group name already exists"); break; + case OpStatus::INVALID_NUMERIC_RESULT: + SendError(kInvalidNumericResult); + break; default: LOG(ERROR) << "Unsupported status " << status; SendError("Internal error"); @@ -340,7 +343,6 @@ void RedisReplyBuilder::SendStringArr(StrPtr str_ptr, uint32_t len) { unsigned vec_indx = 1; string_view src; for (unsigned i = 0; i < len; ++i) { - if (holds_alternative(str_ptr)) { src = get(str_ptr)[i]; } else { diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index f719e81931ea..806f9b3ea1f4 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -8,13 +8,13 @@ cxx_link(dfly_transaction uring_fiber_lib dfly_core strings_lib) add_library(dragonfly_lib channel_slice.cc command_registry.cc config_flags.cc conn_context.cc debugcmd.cc dflycmd.cc - generic_family.cc hset_family.cc + generic_family.cc hset_family.cc json_family.cc list_family.cc main_service.cc rdb_load.cc rdb_save.cc replica.cc snapshot.cc script_mgr.cc server_family.cc set_family.cc stream_family.cc string_family.cc zset_family.cc version.cc) -cxx_link(dragonfly_lib dfly_transaction dfly_facade redis_lib strings_lib html_lib) +cxx_link(dragonfly_lib dfly_transaction dfly_facade redis_lib strings_lib html_lib TRDP::jsoncons) add_library(dfly_test_lib test_utils.cc) cxx_link(dfly_test_lib dragonfly_lib facade_test gtest_main_ext) @@ -31,9 +31,10 @@ cxx_test(rdb_test dfly_test_lib DATA testdata/empty.rdb testdata/redis6_small.rd cxx_test(zset_family_test dfly_test_lib LABELS DFLY) cxx_test(blocking_controller_test dragonfly_lib LABELS DFLY) cxx_test(snapshot_test dragonfly_lib LABELS DFLY) +cxx_test(json_family_test dfly_test_lib LABELS DFLY) add_custom_target(check_dfly WORKING_DIRECTORY .. COMMAND ctest -L DFLY) -add_dependencies(check_dfly dragonfly_test list_family_test +add_dependencies(check_dfly dragonfly_test json_family_test list_family_test generic_family_test memcache_parser_test rdb_test - redis_parser_test stream_family_test string_family_test) + redis_parser_test snapshot_test stream_family_test string_family_test) diff --git a/src/server/common.cc b/src/server/common.cc index 55dfcc5dd871..91638c00de15 100644 --- a/src/server/common.cc +++ b/src/server/common.cc @@ -4,6 +4,7 @@ #include "server/common.h" +#include #include #include @@ -163,6 +164,22 @@ bool ParseHumanReadableBytes(std::string_view str, int64_t* num_bytes) { return true; } +bool ParseDouble(string_view src, double* value) { + if (src.empty()) + return false; + + if (src == "-inf") { + *value = -HUGE_VAL; + } else if (src == "+inf") { + *value = HUGE_VAL; + } else { + absl::from_chars_result result = absl::from_chars(src.data(), src.end(), *value); + if (int(result.ec) != 0 || result.ptr != src.end() || isnan(*value)) + return false; + } + return true; +} + #define ADD(x) (x) += o.x TieredStats& TieredStats::operator+=(const TieredStats& o) { diff --git a/src/server/common.h b/src/server/common.h index 0194e667a52f..2ac515a1da52 100644 --- a/src/server/common.h +++ b/src/server/common.h @@ -112,6 +112,7 @@ inline void ToLower(const MutableSlice* val) { } bool ParseHumanReadableBytes(std::string_view str, int64_t* num_bytes); +bool ParseDouble(std::string_view src, double* value); const char* ObjTypeName(int type); const char* RdbTypeName(unsigned type); diff --git a/src/server/json_family.cc b/src/server/json_family.cc new file mode 100644 index 000000000000..f5c62029a989 --- /dev/null +++ b/src/server/json_family.cc @@ -0,0 +1,581 @@ +// Copyright 2022, Roman Gershman. All rights reserved. +// See LICENSE for licensing terms. +// + +#include "server/json_family.h" + +extern "C" { +#include "redis/object.h" +} + +#include + +#include +#include + +#include "base/logging.h" +#include "server/command_registry.h" +#include "server/error.h" +#include "server/journal/journal.h" +#include "server/tiered_storage.h" +#include "server/transaction.h" + +namespace dfly { + +using namespace std; +using namespace jsoncons; + +using JsonExpression = jsonpath::jsonpath_expression; +using OptBool = optional; +using OptSizeT = optional; +using JsonReplaceCb = function; +using CI = CommandId; + +namespace { + +string GetString(EngineShard* shard, const PrimeValue& pv) { + string res; + if (pv.IsExternal()) { + auto* tiered = shard->tiered_storage(); + auto [offset, size] = pv.GetExternalPtr(); + res.resize(size); + + error_code ec = tiered->Read(offset, size, res.data()); + CHECK(!ec) << "TBD: " << ec; + } else { + pv.GetString(&res); + } + + return res; +} + +inline void RecordJournal(const OpArgs& op_args, const PrimeKey& pkey, const PrimeKey& pvalue) { + if (op_args.shard->journal()) { + op_args.shard->journal()->RecordEntry(op_args.txid, pkey, pvalue); + } +} + +void SetString(const OpArgs& op_args, string_view key, const string& value) { + auto& db_slice = op_args.shard->db_slice(); + auto [it_output, added] = db_slice.AddOrFind(op_args.db_ind, key); + it_output->second.SetString(value); + db_slice.PostUpdate(op_args.db_ind, it_output); + RecordJournal(op_args, it_output->first, it_output->second); +} + +string JsonType(const json& val) { + if (val.is_null()) { + return "null"; + } else if (val.is_bool()) { + return "boolean"; + } else if (val.is_string()) { + return "string"; + } else if (val.is_int64() || val.is_uint64()) { + return "integer"; + } else if (val.is_number()) { + return "number"; + } else if (val.is_object()) { + return "object"; + } else if (val.is_array()) { + return "array"; + } + + return ""; +} + +template +void PrintOptVec(ConnectionContext* cntx, const OpResult>>& result) { + if (result->empty()) { + (*cntx)->SendNullArray(); + } else { + (*cntx)->StartArray(result->size()); + for (auto& it : *result) { + if (it.has_value()) { + if constexpr (is_floating_point_v) { + (*cntx)->SendDouble(*it); + } else { + static_assert(is_integral_v, "Integral required."); + (*cntx)->SendLong(*it); + } + } else { + (*cntx)->SendNull(); + } + } + } +} + +error_code JsonReplace(json& instance, string_view& path, JsonReplaceCb callback) { + using evaluator_t = jsoncons::jsonpath::detail::jsonpath_evaluator; + using value_type = evaluator_t::value_type; + using reference = evaluator_t::reference; + using json_selector_t = evaluator_t::path_expression_type; + using json_location_type = evaluator_t::json_location_type; + jsonpath::custom_functions funcs = jsonpath::custom_functions(); + + error_code ec; + jsoncons::jsonpath::detail::static_resources static_resources(funcs); + evaluator_t e; + json_selector_t expr = e.compile(static_resources, path, ec); + if (ec) { + return ec; + } + + jsoncons::jsonpath::detail::dynamic_resources resources; + auto f = [&callback](const json_location_type& path, reference val) { + callback(path.to_string(), val); + }; + + expr.evaluate(resources, instance, resources.root_path_node(), instance, f, + jsonpath::result_options::nodups); + return ec; +} + +bool JsonErrorHandler(json_errc ec, const ser_context&) { + VLOG(1) << "Error while decode JSON: " << make_error_code(ec).message(); + return false; +} + +OpResult GetJson(const OpArgs& op_args, string_view key) { + OpResult it_res = op_args.shard->db_slice().Find(op_args.db_ind, key, OBJ_STRING); + if (!it_res.ok()) + return it_res.status(); + + error_code ec; + json_decoder decoder; + const PrimeValue& pv = it_res.value()->second; + + string val = GetString(op_args.shard, pv); + basic_json_parser parser(basic_json_decode_options{}, &JsonErrorHandler); + + parser.update(val); + parser.finish_parse(decoder, ec); + + if (!decoder.is_valid()) { + return OpStatus::SYNTAX_ERR; + } + + return decoder.get_result(); +} + +OpResult OpGet(const OpArgs& op_args, string_view key, + vector> expressions) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + if (expressions.size() == 1) { + json out = expressions[0].second.evaluate(*result); + return out.as(); + } + + json out; + for (auto& expr : expressions) { + json eval = expr.second.evaluate(*result); + out[expr.first] = eval; + } + + return out.as(); +} + +OpResult> OpType(const OpArgs& op_args, string_view key, JsonExpression expression) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + vector vec; + auto cb = [&vec](const string_view& path, const json& val) { vec.emplace_back(JsonType(val)); }; + + expression.evaluate(*result, cb); + return vec; +} + +OpResult> OpStrLen(const OpArgs& op_args, string_view key, + JsonExpression expression) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + vector vec; + auto cb = [&vec](const string_view& path, const json& val) { + if (val.is_string()) { + vec.emplace_back(val.as_string_view().size()); + } else { + vec.emplace_back(nullopt); + } + }; + + expression.evaluate(*result, cb); + return vec; +} + +OpResult> OpObjLen(const OpArgs& op_args, string_view key, + JsonExpression expression) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + vector vec; + auto cb = [&vec](const string_view& path, const json& val) { + if (val.is_object()) { + vec.emplace_back(val.object_value().size()); + } else { + vec.emplace_back(nullopt); + } + }; + + expression.evaluate(*result, cb); + return vec; +} + +OpResult> OpArrLen(const OpArgs& op_args, string_view key, + JsonExpression expression) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + vector vec; + auto cb = [&vec](const string_view& path, const json& val) { + if (val.is_array()) { + vec.emplace_back(val.array_value().size()); + } else { + vec.emplace_back(nullopt); + } + }; + + expression.evaluate(*result, cb); + return vec; +} + +OpResult> OpToggle(const OpArgs& op_args, string_view key, string_view path) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + vector vec; + auto cb = [&vec](const string& path, json& val) { + if (val.is_bool()) { + bool current_val = val.as_bool() ^ true; + val = current_val; + vec.emplace_back(current_val); + } else { + vec.emplace_back(nullopt); + } + }; + + json j = result.value(); + error_code ec = JsonReplace(j, path, cb); + if (ec) { + VLOG(1) << "Failed to evaulate expression on json with error: " << ec.message(); + return OpStatus::SYNTAX_ERR; + } + + SetString(op_args, key, j.as_string()); + return vec; +} + +template +OpResult OpDoubleArithmetic(const OpArgs& op_args, string_view key, string_view path, + double num, Op arithmetic_op) { + OpResult result = GetJson(op_args, key); + if (!result) { + return result.status(); + } + + bool is_result_overflow = false; + double int_part; + bool has_fractional_part = (modf(num, &int_part) != 0); + json output(json_array_arg); + + auto cb = [&](const string& path, json& val) { + if (val.is_number()) { + double result = arithmetic_op(val.as(), num); + if (isinf(result)) { + is_result_overflow = true; + return; + } + + if (val.is_double() || has_fractional_part) { + val = result; + } else { + val = (uint64_t)result; + } + output.push_back(val); + } else { + output.push_back(json::null()); + } + }; + + json j = result.value(); + error_code ec = JsonReplace(j, path, cb); + if (ec) { + VLOG(1) << "Failed to evaulate expression on json with error: " << ec.message(); + return OpStatus::SYNTAX_ERR; + } + + if (is_result_overflow) { + return OpStatus::INVALID_NUMERIC_RESULT; + } + + SetString(op_args, key, j.as_string()); + return output.as_string(); +} + +} // namespace + +void JsonFamily::NumIncrBy(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + string_view num = ArgS(args, 3); + + double dnum; + if (!ParseDouble(num, &dnum)) { + (*cntx)->SendError(kWrongTypeErr); + return; + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpDoubleArithmetic(t->GetOpArgs(shard), key, path, dnum, plus{}); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.NUMINCRBY " << trans->DebugId() << ": " << key; + (*cntx)->SendSimpleString(*result); + } else { + (*cntx)->SendError(result.status()); + } +} + +void JsonFamily::NumMultBy(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + string_view num = ArgS(args, 3); + + double dnum; + if (!ParseDouble(num, &dnum)) { + (*cntx)->SendError(kWrongTypeErr); + return; + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpDoubleArithmetic(t->GetOpArgs(shard), key, path, dnum, multiplies{}); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.NUMMULTBY " << trans->DebugId() << ": " << key; + (*cntx)->SendSimpleString(*result); + } else { + (*cntx)->SendError(result.status()); + } +} + +void JsonFamily::Toggle(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpToggle(t->GetOpArgs(shard), key, path); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult> result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.TOGGLE " << trans->DebugId() << ": " << key; + PrintOptVec(cntx, result); + } else { + (*cntx)->SendError(result.status()); + } +} + +void JsonFamily::Type(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + + error_code ec; + JsonExpression expression = jsonpath::make_expression(path, ec); + + if (ec) { + VLOG(1) << "Invalid JSONPath syntax: " << ec.message(); + (*cntx)->SendError(kSyntaxErr); + return; + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpType(t->GetOpArgs(shard), key, move(expression)); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult> result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.TYPE " << trans->DebugId() << ": " << key; + if (result->empty()) { + // When vector is empty, the path doesn't exist in the corresponding json. + (*cntx)->SendNull(); + } else { + (*cntx)->SendStringArr(*result); + } + } else { + if (result.status() == OpStatus::KEY_NOTFOUND) { + (*cntx)->SendNullArray(); + } else { + (*cntx)->SendError(result.status()); + } + } +} + +void JsonFamily::ArrLen(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + + error_code ec; + JsonExpression expression = jsonpath::make_expression(path, ec); + + if (ec) { + VLOG(1) << "Invalid JSONPath syntax: " << ec.message(); + (*cntx)->SendError(kSyntaxErr); + return; + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpArrLen(t->GetOpArgs(shard), key, move(expression)); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult> result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.ARRLEN " << trans->DebugId() << ": " << key; + PrintOptVec(cntx, result); + } else { + (*cntx)->SendError(result.status()); + } +} + +void JsonFamily::ObjLen(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + + error_code ec; + JsonExpression expression = jsonpath::make_expression(path, ec); + + if (ec) { + VLOG(1) << "Invalid JSONPath syntax: " << ec.message(); + (*cntx)->SendError(kSyntaxErr); + return; + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpObjLen(t->GetOpArgs(shard), key, move(expression)); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult> result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.OBJLEN " << trans->DebugId() << ": " << key; + PrintOptVec(cntx, result); + } else { + (*cntx)->SendError(result.status()); + } +} + +void JsonFamily::StrLen(CmdArgList args, ConnectionContext* cntx) { + string_view key = ArgS(args, 1); + string_view path = ArgS(args, 2); + + error_code ec; + JsonExpression expression = jsonpath::make_expression(path, ec); + + if (ec) { + VLOG(1) << "Invalid JSONPath syntax: " << ec.message(); + (*cntx)->SendError(kSyntaxErr); + return; + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpStrLen(t->GetOpArgs(shard), key, move(expression)); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult> result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.STRLEN " << trans->DebugId() << ": " << key; + PrintOptVec(cntx, result); + } else { + (*cntx)->SendError(result.status()); + } +} + +void JsonFamily::Get(CmdArgList args, ConnectionContext* cntx) { + DCHECK_GE(args.size(), 3U); + string_view key = ArgS(args, 1); + + vector> expressions; + for (size_t i = 2; i < args.size(); ++i) { + string_view path = ArgS(args, i); + + error_code ec; + JsonExpression expr = jsonpath::make_expression(path, ec); + + if (ec) { + VLOG(1) << "Invalid JSONPath syntax: " << ec.message(); + (*cntx)->SendError(kSyntaxErr); + return; + } + + expressions.emplace_back(path, move(expr)); + } + + auto cb = [&](Transaction* t, EngineShard* shard) { + return OpGet(t->GetOpArgs(shard), key, move(expressions)); + }; + + DVLOG(1) << "Before Get::ScheduleSingleHopT " << key; + Transaction* trans = cntx->transaction; + OpResult result = trans->ScheduleSingleHopT(move(cb)); + + if (result) { + DVLOG(1) << "JSON.GET " << trans->DebugId() << ": " << key << " " << result.value(); + (*cntx)->SendBulkString(*result); + } else { + (*cntx)->SendError(result.status()); + } +} + +#define HFUNC(x) SetHandler(&JsonFamily::x) + +void JsonFamily::Register(CommandRegistry* registry) { + *registry << CI{"JSON.GET", CO::READONLY | CO::FAST, -3, 1, 1, 1}.HFUNC(Get); + *registry << CI{"JSON.TYPE", CO::READONLY | CO::FAST, 3, 1, 1, 1}.HFUNC(Type); + *registry << CI{"JSON.STRLEN", CO::READONLY | CO::FAST, 3, 1, 1, 1}.HFUNC(StrLen); + *registry << CI{"JSON.OBJLEN", CO::READONLY | CO::FAST, 3, 1, 1, 1}.HFUNC(ObjLen); + *registry << CI{"JSON.ARRLEN", CO::READONLY | CO::FAST, 3, 1, 1, 1}.HFUNC(ArrLen); + *registry << CI{"JSON.TOGGLE", CO::WRITE | CO::DENYOOM | CO::FAST, 3, 1, 1, 1}.HFUNC(Toggle); + *registry << CI{"JSON.NUMINCRBY", CO::WRITE | CO::DENYOOM | CO::FAST, 4, 1, 1, 1}.HFUNC( + NumIncrBy); + *registry << CI{"JSON.NUMMULTBY", CO::WRITE | CO::DENYOOM | CO::FAST, 4, 1, 1, 1}.HFUNC( + NumMultBy); +} + +} // namespace dfly diff --git a/src/server/json_family.h b/src/server/json_family.h new file mode 100644 index 000000000000..adba4db76567 --- /dev/null +++ b/src/server/json_family.h @@ -0,0 +1,33 @@ +// Copyright 2022, Roman Gershman. All rights reserved. +// See LICENSE for licensing terms. +// + +#pragma once + +#include "server/common.h" +#include "server/engine_shard_set.h" + +namespace dfly { + +class ConnectionContext; +class CommandRegistry; +using facade::OpResult; +using facade::OpStatus; +using facade::RedisReplyBuilder; + +class JsonFamily { + public: + static void Register(CommandRegistry* registry); + + private: + static void Get(CmdArgList args, ConnectionContext* cntx); + static void Type(CmdArgList args, ConnectionContext* cntx); + static void StrLen(CmdArgList args, ConnectionContext* cntx); + static void ObjLen(CmdArgList args, ConnectionContext* cntx); + static void ArrLen(CmdArgList args, ConnectionContext* cntx); + static void Toggle(CmdArgList args, ConnectionContext* cntx); + static void NumIncrBy(CmdArgList args, ConnectionContext* cntx); + static void NumMultBy(CmdArgList args, ConnectionContext* cntx); +}; + +} // namespace dfly diff --git a/src/server/json_family_test.cc b/src/server/json_family_test.cc new file mode 100644 index 000000000000..83eb278f7108 --- /dev/null +++ b/src/server/json_family_test.cc @@ -0,0 +1,459 @@ +// Copyright 2022, Roman Gershman. All rights reserved. +// See LICENSE for licensing terms. +// + +#include "server/json_family.h" + +#include "base/gtest.h" +#include "base/logging.h" +#include "facade/facade_test.h" +#include "server/command_registry.h" +#include "server/test_utils.h" + +using namespace testing; +using namespace std; +using namespace util; + +namespace dfly { + +class JsonFamilyTest : public BaseFamilyTest { + protected: +}; + +TEST_F(JsonFamilyTest, SetGetBasic) { + string json = R"( + { + "store": { + "book": [ + { + "category": "Fantasy", + "author": "J. K. Rowling", + "title": "Harry Potter and the Philosopher's Stone", + "isbn": 9780747532743, + "price": 5.99 + } + ] + } + } +)"; + + string xml = R"( + + + + Fantasy + J. K. Rowling + Harry Potter and the Philosopher's Stone + 9780747532743 + 5.99 + + +)"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.GET", "json", "$..*"}); + ASSERT_THAT(resp, ArgType(RespExpr::STRING)); + + resp = Run({"JSON.GET", "json", "$..book[0].price"}); + EXPECT_THAT(resp, ArgType(RespExpr::STRING)); + + resp = Run({"JSON.GET", "json", "//*"}); + EXPECT_THAT(resp, ArgType(RespExpr::ERROR)); + + resp = Run({"JSON.GET", "json", "//book[0]"}); + EXPECT_THAT(resp, ArgType(RespExpr::ERROR)); + + resp = Run({"set", "xml", xml}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.GET", "xml", "$..*"}); + EXPECT_THAT(resp, ArgType(RespExpr::ERROR)); +} + +TEST_F(JsonFamilyTest, SetGetFromPhonebook) { + string json = R"( + { + "firstName":"John", + "lastName":"Smith", + "age":27, + "weight":135.25, + "isAlive":true, + "address":{ + "street":"21 2nd Street", + "city":"New York", + "state":"NY", + "zipcode":"10021-3100" + }, + "phoneNumbers":[ + { + "type":"home", + "number":"212 555-1234" + }, + { + "type":"office", + "number":"646 555-4567" + } + ], + "children":[ + + ], + "spouse":null + } + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.GET", "json", "$.address.*"}); + EXPECT_EQ(resp, R"(["New York","NY","21 2nd Street","10021-3100"])"); + + resp = Run({"JSON.GET", "json", "$.firstName", "$.age", "$.lastName"}); + EXPECT_EQ(resp, R"({"$.age":[27],"$.firstName":["John"],"$.lastName":["Smith"]})"); + + resp = Run({"JSON.GET", "json", "$.spouse.*"}); + EXPECT_EQ(resp, "[]"); + + resp = Run({"JSON.GET", "json", "$.children.*"}); + EXPECT_EQ(resp, "[]"); + + resp = Run({"JSON.GET", "json", "$..phoneNumbers[1].*"}); + EXPECT_EQ(resp, R"(["646 555-4567","office"])"); +} + +TEST_F(JsonFamilyTest, Type) { + string json = R"( + [1, 2.3, "foo", true, null, {}, []] + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.TYPE", "json", "$[*]"}); + ASSERT_EQ(RespExpr::ARRAY, resp.type); + EXPECT_THAT(resp.GetVec(), + ElementsAre("integer", "number", "string", "boolean", "null", "object", "array")); + + resp = Run({"JSON.TYPE", "json", "$[10]"}); + EXPECT_THAT(resp, ArgType(RespExpr::NIL)); + + resp = Run({"JSON.TYPE", "not_exist_key", "$[10]"}); + EXPECT_THAT(resp, ArgType(RespExpr::NIL_ARRAY)); +} + +TEST_F(JsonFamilyTest, StrLen) { + string json = R"( + {"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":3}} + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.STRLEN", "json", "$.a.a"}); + EXPECT_THAT(resp, IntArg(1)); + + resp = Run({"JSON.STRLEN", "json", "$.a.*"}); + EXPECT_THAT(resp, IntArg(1)); + + resp = Run({"JSON.STRLEN", "json", "$.c.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(1), IntArg(2))); + + resp = Run({"JSON.STRLEN", "json", "$.c.b"}); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.STRLEN", "json", "$.d.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), + ElementsAre(ArgType(RespExpr::NIL), IntArg(1), ArgType(RespExpr::NIL))); +} + +TEST_F(JsonFamilyTest, ObjLen) { + string json = R"( + {"a":{}, "b":{"a":"a"}, "c":{"a":"a", "b":"bb"}, "d":{"a":1, "b":"b", "c":{"a":3,"b":4}}, "e":1} + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.OBJLEN", "json", "$.a"}); + EXPECT_THAT(resp, IntArg(0)); + + resp = Run({"JSON.OBJLEN", "json", "$.a.*"}); + EXPECT_THAT(resp, ArgType(RespExpr::NIL_ARRAY)); + + resp = Run({"JSON.OBJLEN", "json", "$.b"}); + EXPECT_THAT(resp, IntArg(1)); + + resp = Run({"JSON.OBJLEN", "json", "$.b.*"}); + EXPECT_THAT(resp, ArgType(RespExpr::NIL)); + + resp = Run({"JSON.OBJLEN", "json", "$.c"}); + EXPECT_THAT(resp, IntArg(2)); + + resp = Run({"JSON.OBJLEN", "json", "$.c.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(ArgType(RespExpr::NIL), ArgType(RespExpr::NIL))); + + resp = Run({"JSON.OBJLEN", "json", "$.d"}); + EXPECT_THAT(resp, IntArg(3)); + + resp = Run({"JSON.OBJLEN", "json", "$.d.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), + ElementsAre(ArgType(RespExpr::NIL), ArgType(RespExpr::NIL), IntArg(2))); + + resp = Run({"JSON.OBJLEN", "json", "$.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), + ElementsAre(IntArg(0), IntArg(1), IntArg(2), IntArg(3), ArgType(RespExpr::NIL))); +} + +TEST_F(JsonFamilyTest, ArrLen) { + string json = R"( + [[], ["a"], ["a", "b"], ["a", "b", "c"]] + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.ARRLEN", "json", "$[*]"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(0), IntArg(1), IntArg(2), IntArg(3))); + + json = R"( + [[], "a", ["a", "b"], ["a", "b", "c"], 4] + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.ARRLEN", "json", "$[*]"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), ElementsAre(IntArg(0), ArgType(RespExpr::NIL), IntArg(2), IntArg(3), + ArgType(RespExpr::NIL))); +} + +TEST_F(JsonFamilyTest, Toggle) { + string json = R"( + {"a":true, "b":false, "c":1, "d":null, "e":"foo", "f":[], "g":{}} + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.TOGGLE", "json", "$.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), + ElementsAre(IntArg(0), IntArg(1), ArgType(RespExpr::NIL), ArgType(RespExpr::NIL), + ArgType(RespExpr::NIL), ArgType(RespExpr::NIL), ArgType(RespExpr::NIL))); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([false,true,1,null,"foo",[],{}])"); + + resp = Run({"JSON.TOGGLE", "json", "$.*"}); + ASSERT_THAT(resp, ArgType(RespExpr::ARRAY)); + EXPECT_THAT(resp.GetVec(), + ElementsAre(IntArg(1), IntArg(0), ArgType(RespExpr::NIL), ArgType(RespExpr::NIL), + ArgType(RespExpr::NIL), ArgType(RespExpr::NIL), ArgType(RespExpr::NIL))); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([true,false,1,null,"foo",[],{}])"); +} + +TEST_F(JsonFamilyTest, NumIncrBy) { + string json = R"( + {"e":1.5,"a":1} + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.a", "1.1"}); + EXPECT_EQ(resp, "[2.1]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.e", "1"}); + EXPECT_EQ(resp, "[2.5]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.e", "inf"}); + EXPECT_THAT(resp, ErrArg("ERR result is not a number")); + + json = R"( + {"e":1.5,"a":1} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.e", "1.7e308"}); + EXPECT_EQ(resp, "[1.7e+308]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.e", "1.7e308"}); + EXPECT_THAT(resp, ErrArg("ERR result is not a number")); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([1,1.7e+308])"); + + json = R"( + {"a":[], "b":[1], "c":[1,2], "d":[1,2,3]} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.d[*]", "10"}); + EXPECT_EQ(resp, "[11,12,13]"); + + resp = Run({"JSON.GET", "json", "$.d[*]"}); + EXPECT_EQ(resp, "[11,12,13]"); + + json = R"( + {"a":[], "b":[1], "c":[1,2], "d":[1,2,3]} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.a[*]", "1"}); + EXPECT_EQ(resp, "[]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.b[*]", "1"}); + EXPECT_EQ(resp, "[2]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.c[*]", "1"}); + EXPECT_EQ(resp, "[2,3]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.d[*]", "1"}); + EXPECT_EQ(resp, "[2,3,4]"); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([[],[2],[2,3],[2,3,4]])"); + + json = R"( + {"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.a.*", "1"}); + EXPECT_EQ(resp, "[]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.b.*", "1"}); + EXPECT_EQ(resp, "[2]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.c.*", "1"}); + EXPECT_EQ(resp, "[2,3]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.d.*", "1"}); + EXPECT_EQ(resp, "[2,3,4]"); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([{},{"a":2},{"a":2,"b":3},{"a":2,"b":3,"c":4}])"); + + json = R"( + {"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.a.*", "1"}); + EXPECT_EQ(resp, "[null]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.b.*", "1"}); + EXPECT_EQ(resp, "[null,2]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.c.*", "1"}); + EXPECT_EQ(resp, "[null,null]"); + + resp = Run({"JSON.NUMINCRBY", "json", "$.d.*", "1"}); + EXPECT_EQ(resp, "[2,null,4]"); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([{"a":"a"},{"a":"a","b":2},{"a":"a","b":"b"},{"a":2,"b":"b","c":4}])"); +} + +TEST_F(JsonFamilyTest, NumMultBy) { + string json = R"( + {"a":[], "b":[1], "c":[1,2], "d":[1,2,3]} + )"; + + auto resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.d[*]", "2"}); + EXPECT_EQ(resp, "[2,4,6]"); + + resp = Run({"JSON.GET", "json", "$.d[*]"}); + EXPECT_EQ(resp, R"([2,4,6])"); + + json = R"( + {"a":[], "b":[1], "c":[1,2], "d":[1,2,3]} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.a[*]", "2"}); + EXPECT_EQ(resp, "[]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.b[*]", "2"}); + EXPECT_EQ(resp, "[2]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.c[*]", "2"}); + EXPECT_EQ(resp, "[2,4]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.d[*]", "2"}); + EXPECT_EQ(resp, "[2,4,6]"); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([[],[2],[2,4],[2,4,6]])"); + + json = R"( + {"a":{}, "b":{"a":1}, "c":{"a":1, "b":2}, "d":{"a":1, "b":2, "c":3}} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.a.*", "2"}); + EXPECT_EQ(resp, "[]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.b.*", "2"}); + EXPECT_EQ(resp, "[2]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.c.*", "2"}); + EXPECT_EQ(resp, "[2,4]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.d.*", "2"}); + EXPECT_EQ(resp, "[2,4,6]"); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([{},{"a":2},{"a":2,"b":4},{"a":2,"b":4,"c":6}])"); + + json = R"( + {"a":{"a":"a"}, "b":{"a":"a", "b":1}, "c":{"a":"a", "b":"b"}, "d":{"a":1, "b":"b", "c":3}} + )"; + + resp = Run({"set", "json", json}); + ASSERT_THAT(resp, "OK"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.a.*", "2"}); + EXPECT_EQ(resp, "[null]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.b.*", "2"}); + EXPECT_EQ(resp, "[null,2]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.c.*", "2"}); + EXPECT_EQ(resp, "[null,null]"); + + resp = Run({"JSON.NUMMULTBY", "json", "$.d.*", "2"}); + EXPECT_EQ(resp, "[2,null,6]"); + + resp = Run({"JSON.GET", "json", "$.*"}); + EXPECT_EQ(resp, R"([{"a":"a"},{"a":"a","b":2},{"a":"a","b":"b"},{"a":2,"b":"b","c":6}])"); +} + +} // namespace dfly diff --git a/src/server/main_service.cc b/src/server/main_service.cc index fb5b598e0266..5fdc548d35d3 100644 --- a/src/server/main_service.cc +++ b/src/server/main_service.cc @@ -24,6 +24,7 @@ extern "C" { #include "server/error.h" #include "server/generic_family.h" #include "server/hset_family.h" +#include "server/json_family.h" #include "server/list_family.h" #include "server/script_mgr.h" #include "server/server_state.h" @@ -1161,6 +1162,7 @@ void Service::RegisterCommands() { SetFamily::Register(®istry_); HSetFamily::Register(®istry_); ZSetFamily::Register(®istry_); + JsonFamily::Register(®istry_); server_family_.Register(®istry_); diff --git a/src/server/zset_family.cc b/src/server/zset_family.cc index c4e6320fb1b5..4e88f08eda48 100644 --- a/src/server/zset_family.cc +++ b/src/server/zset_family.cc @@ -11,8 +11,6 @@ extern "C" { #include "redis/zset.h" } -#include - #include "base/logging.h" #include "base/stl_util.h" #include "facade/error.h" @@ -531,22 +529,6 @@ void IntervalVisitor::AddResult(const uint8_t* vstr, unsigned vlen, long long vl } } -bool ParseScore(string_view src, double* score) { - if (src.empty()) - return false; - - if (src == "-inf") { - *score = -HUGE_VAL; - } else if (src == "+inf") { - *score = HUGE_VAL; - } else { - absl::from_chars_result result = absl::from_chars(src.data(), src.end(), *score); - if (int(result.ec) != 0 || result.ptr != src.end() || isnan(*score)) - return false; - } - return true; -}; - bool ParseBound(string_view src, ZSetFamily::Bound* bound) { if (src.empty()) return false; @@ -556,7 +538,7 @@ bool ParseBound(string_view src, ZSetFamily::Bound* bound) { src.remove_prefix(1); } - return ParseScore(src, &bound->val); + return ParseDouble(src, &bound->val); } bool ParseLexBound(string_view src, ZSetFamily::LexBound* bound) { @@ -956,7 +938,7 @@ void ZSetFamily::ZAdd(CmdArgList args, ConnectionContext* cntx) { string_view cur_arg = ArgS(args, i); double val = 0; - if (!ParseScore(cur_arg, &val)) { + if (!ParseDouble(cur_arg, &val)) { VLOG(1) << "Bad score:" << cur_arg << "|"; return (*cntx)->SendError(kInvalidFloatErr); } @@ -1135,8 +1117,7 @@ void ZSetFamily::ZInterStore(CmdArgList args, ConnectionContext* cntx) { if (shard->shard_id() == dest_shard) { ZParams zparams; zparams.override = true; - add_result = - OpAdd(t->GetOpArgs(shard), zparams, dest_key, ScoredMemberSpan{smvec}).value(); + add_result = OpAdd(t->GetOpArgs(shard), zparams, dest_key, ScoredMemberSpan{smvec}).value(); } return OpStatus::OK; }; @@ -1419,8 +1400,7 @@ void ZSetFamily::ZUnionStore(CmdArgList args, ConnectionContext* cntx) { if (shard->shard_id() == dest_shard) { ZParams zparams; zparams.override = true; - add_result = - OpAdd(t->GetOpArgs(shard), zparams, dest_key, ScoredMemberSpan{smvec}).value(); + add_result = OpAdd(t->GetOpArgs(shard), zparams, dest_key, ScoredMemberSpan{smvec}).value(); } return OpStatus::OK; }; @@ -1534,7 +1514,6 @@ void ZSetFamily::ZRankGeneric(CmdArgList args, bool reverse, ConnectionContext* string_view member = ArgS(args, 2); auto cb = [&](Transaction* t, EngineShard* shard) { - return OpRank(t->GetOpArgs(shard), key, member, reverse); };