blob: be5cf32a12483ef8b073cea01a1397a2c097f2d5 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
#include "command_parser.h"
#include "commander.h"
#include "error_constants.h"
#include "server/server.h"
#include "types/geohash.h"
#include "types/redis_geo.h"
namespace redis {
class CommandGeoBase : public Commander {
public:
Status ParseDistanceUnit(const std::string &param) {
if (util::ToLower(param) == "m") {
distance_unit_ = kDistanceMeter;
} else if (util::ToLower(param) == "km") {
distance_unit_ = kDistanceKilometers;
} else if (util::ToLower(param) == "ft") {
distance_unit_ = kDistanceFeet;
} else if (util::ToLower(param) == "mi") {
distance_unit_ = kDistanceMiles;
} else {
return {Status::RedisParseErr, "unsupported unit provided. please use M, KM, FT, MI"};
}
return Status::OK();
}
static Status ParseLongLat(const std::string &longitude_para, const std::string &latitude_para, double *longitude,
double *latitude) {
auto long_stat = ParseFloat(longitude_para);
auto lat_stat = ParseFloat(latitude_para);
if (!long_stat || !lat_stat) {
return {Status::RedisParseErr, errValueIsNotFloat};
}
*longitude = *long_stat;
*latitude = *lat_stat;
return ValidateLongLat(longitude, latitude);
}
static Status ValidateLongLat(double *longitude, double *latitude) {
if (*longitude < GEO_LONG_MIN || *longitude > GEO_LONG_MAX || *latitude < GEO_LAT_MIN || *latitude > GEO_LAT_MAX) {
return {Status::RedisParseErr,
"invalid longitude,latitude pair " + std::to_string(*longitude) + "," + std::to_string(*latitude)};
}
return Status::OK();
}
double GetDistanceByUnit(double distance) { return distance / GetUnitConversion(); }
double GetRadiusMeters(double radius) { return radius * GetUnitConversion(); }
double GetUnitConversion() {
double conversion = 0;
switch (distance_unit_) {
case kDistanceMeter:
conversion = 1;
break;
case kDistanceKilometers:
conversion = 1000;
break;
case kDistanceFeet:
conversion = 0.3048;
break;
case kDistanceMiles:
conversion = 1609.34;
break;
}
return conversion;
}
static size_t GetReturnedItemsCount(size_t result_length, int count) {
if (count == 0) {
return result_length;
}
const auto requested_count = static_cast<size_t>(count);
return result_length < requested_count ? result_length : requested_count;
}
protected:
DistanceUnit distance_unit_ = kDistanceMeter;
};
class CommandGeoAdd : public CommandGeoBase {
public:
CommandGeoAdd() : CommandGeoBase() {}
Status Parse(const std::vector<std::string> &args) override {
if ((args.size() - 5) % 3 != 0) {
return {Status::RedisParseErr, errWrongNumOfArguments};
}
for (size_t i = 2; i < args.size(); i += 3) {
double longitude = 0;
double latitude = 0;
auto s = ParseLongLat(args[i], args[i + 1], &longitude, &latitude);
if (!s.IsOK()) return s;
geo_points_.emplace_back(GeoPoint{longitude, latitude, args[i + 2]});
}
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
uint64_t ret = 0;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.Add(ctx, args_[1], &geo_points_, &ret);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
*output = redis::Integer(ret);
return Status::OK();
}
private:
std::vector<GeoPoint> geo_points_;
};
class CommandGeoDist : public CommandGeoBase {
public:
CommandGeoDist() : CommandGeoBase() {}
Status Parse(const std::vector<std::string> &args) override {
if (args.size() == 5) {
auto s = ParseDistanceUnit(args[4]);
if (!s.IsOK()) return s;
}
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
double distance = 0;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.Dist(ctx, args_[1], args_[2], args_[3], &distance);
if (!s.ok() && !s.IsNotFound()) {
return {Status::RedisExecErr, s.ToString()};
}
if (s.IsNotFound()) {
*output = conn->NilString();
} else {
*output = conn->Double(GetDistanceByUnit(distance));
}
return Status::OK();
}
};
class CommandGeoHash : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
for (size_t i = 2; i < args.size(); i++) {
members_.emplace_back(args[i]);
}
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<std::string> hashes;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.Hash(ctx, args_[1], members_, &hashes);
if (!s.ok() && !s.IsNotFound()) {
return {Status::RedisExecErr, s.ToString()};
}
if (s.IsNotFound()) {
hashes.resize(members_.size(), "");
}
*output = conn->MultiBulkString(hashes);
return Status::OK();
}
private:
std::vector<Slice> members_;
};
class CommandGeoPos : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
for (size_t i = 2; i < args.size(); i++) {
members_.emplace_back(args[i]);
}
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::map<std::string, GeoPoint> geo_points;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.Pos(ctx, args_[1], members_, &geo_points);
if (!s.ok() && !s.IsNotFound()) {
return {Status::RedisExecErr, s.ToString()};
}
std::vector<std::string> list;
if (s.IsNotFound()) {
list.resize(members_.size(), "");
*output = conn->MultiBulkString(list);
return Status::OK();
}
for (const auto &member : members_) {
auto iter = geo_points.find(member.ToString());
if (iter == geo_points.end()) {
list.emplace_back(conn->NilString());
} else {
list.emplace_back(redis::Array({conn->Double(iter->second.longitude), conn->Double(iter->second.latitude)}));
}
}
*output = redis::Array(list);
return Status::OK();
}
private:
std::vector<Slice> members_;
};
class CommandGeoRadius : public CommandGeoBase {
public:
CommandGeoRadius() : CommandGeoBase() {}
Status Parse(const std::vector<std::string> &args) override {
auto s = ParseLongLat(args[2], args[3], &longitude_, &latitude_);
if (!s.IsOK()) return s;
auto radius = ParseFloat(args[4]);
if (!radius) {
return {Status::RedisParseErr, errValueIsNotFloat};
}
radius_ = *radius;
s = ParseDistanceUnit(args[5]);
if (!s.IsOK()) return s;
s = ParseRadiusExtraOption();
if (!s.IsOK()) return s;
return Commander::Parse(args);
}
Status ParseRadiusExtraOption(size_t i = 6) {
while (i < args_.size()) {
if (util::ToLower(args_[i]) == "withcoord") {
with_coord_ = true;
i++;
} else if (util::ToLower(args_[i]) == "withdist") {
with_dist_ = true;
i++;
} else if (util::ToLower(args_[i]) == "withhash") {
with_hash_ = true;
i++;
} else if (util::ToLower(args_[i]) == "asc") {
sort_ = kSortASC;
i++;
} else if (util::ToLower(args_[i]) == "desc") {
sort_ = kSortDESC;
i++;
} else if (util::ToLower(args_[i]) == "count" && i + 1 < args_.size()) {
auto parse_result = ParseInt<int>(args_[i + 1], 10);
if (!parse_result) {
return {Status::RedisParseErr, errValueNotInteger};
}
count_ = *parse_result;
i += 2;
} else if ((attributes_->InitialFlags() & kCmdWrite) &&
(util::ToLower(args_[i]) == "store" || util::ToLower(args_[i]) == "storedist") &&
i + 1 < args_.size()) {
store_key_ = args_[i + 1];
if (util::ToLower(args_[i]) == "storedist") {
store_distance_ = true;
}
i += 2;
} else {
return {Status::RedisParseErr, errInvalidSyntax};
}
}
/* Trap options not compatible with STORE and STOREDIST. */
if (!store_key_.empty() && (with_dist_ || with_hash_ || with_coord_)) {
return {Status::RedisParseErr,
"STORE option in GEORADIUS is not compatible with WITHDIST, WITHHASH and WITHCOORDS options"};
}
/* COUNT without ordering does not make much sense, force ASC
* ordering if COUNT was specified but no sorting was requested.
* */
if (count_ != 0 && sort_ == kSortNone) {
sort_ = kSortASC;
}
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<GeoPoint> geo_points;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.Radius(ctx, args_[1], longitude_, latitude_, GetRadiusMeters(radius_), count_, sort_, store_key_,
store_distance_, GetUnitConversion(), &geo_points);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
if (store_key_.size() != 0) {
*output = redis::Integer(GetReturnedItemsCount(geo_points.size(), count_));
} else {
*output = GenerateOutput(conn, geo_points);
}
return Status::OK();
}
std::string GenerateOutput(const Connection *conn, const std::vector<GeoPoint> &geo_points) {
size_t returned_items_count = GetReturnedItemsCount(geo_points.size(), count_);
std::vector<std::string> list;
for (size_t i = 0; i < returned_items_count; i++) {
const auto &geo_point = geo_points[i];
if (!with_coord_ && !with_hash_ && !with_dist_) {
list.emplace_back(redis::BulkString(geo_point.member));
} else {
std::vector<std::string> one;
one.emplace_back(redis::BulkString(geo_point.member));
if (with_dist_) {
one.emplace_back(conn->Double(GetDistanceByUnit(geo_point.dist)));
}
if (with_hash_) {
one.emplace_back(conn->Double(geo_point.score));
}
if (with_coord_) {
one.emplace_back(redis::Array({conn->Double(geo_point.longitude), conn->Double(geo_point.latitude)}));
}
list.emplace_back(redis::Array(one));
}
}
return redis::Array(list);
}
static std::vector<CommandKeyRange> Range(const std::vector<std::string> &args) {
int store_key = 0;
// Check for the presence of the stored key in the command args.
for (size_t i = 6; i < args.size(); i++) {
// For the case when a user specifies both "store" and "storedist" options,
// the second key will override the first key. The behavior is kept the same
// as in ParseRadiusExtraOption method.
if ((util::ToLower(args[i]) == "store" || util::ToLower(args[i]) == "storedist") && i + 1 < args.size()) {
store_key = (int)i + 1;
i++;
}
}
if (store_key > 0) {
return {{1, 1, 1}, {store_key, store_key, 1}};
}
return {{1, 1, 1}};
}
protected:
double radius_ = 0;
bool with_coord_ = false;
bool with_dist_ = false;
bool with_hash_ = false;
int count_ = 0;
DistanceSort sort_ = kSortNone;
std::string store_key_;
bool store_distance_ = false;
private:
double longitude_ = 0;
double latitude_ = 0;
};
class CommandGeoSearch : public CommandGeoBase {
public:
CommandGeoSearch() : CommandGeoBase() {}
Status Parse(const std::vector<std::string> &args) override {
CommandParser parser(args, 1);
key_ = GET_OR_RET(parser.TakeStr());
while (parser.Good()) {
if (parser.EatEqICase("frommember")) {
auto s = setOriginType(kMember);
if (!s.IsOK()) return s;
member_ = GET_OR_RET(parser.TakeStr());
} else if (parser.EatEqICase("fromlonlat")) {
auto s = setOriginType(kLongLat);
if (!s.IsOK()) return s;
longitude_ = GET_OR_RET(parser.TakeFloat());
latitude_ = GET_OR_RET(parser.TakeFloat());
s = ValidateLongLat(&longitude_, &latitude_);
if (!s.IsOK()) return s;
} else if (parser.EatEqICase("byradius")) {
auto s = setShapeType(kGeoShapeTypeCircular);
if (!s.IsOK()) return s;
radius_ = GET_OR_RET(parser.TakeFloat());
std::string distance_raw = GET_OR_RET(parser.TakeStr());
s = ParseDistanceUnit(distance_raw);
if (!s.IsOK()) return s;
} else if (parser.EatEqICase("bybox")) {
auto s = setShapeType(kGeoShapeTypeRectangular);
if (!s.IsOK()) return s;
width_ = GET_OR_RET(parser.TakeFloat());
height_ = GET_OR_RET(parser.TakeFloat());
std::string distance_raw = GET_OR_RET(parser.TakeStr());
s = ParseDistanceUnit(distance_raw);
if (!s.IsOK()) return s;
} else if (parser.EatEqICase("asc") && sort_ == kSortNone) {
sort_ = kSortASC;
} else if (parser.EatEqICase("desc") && sort_ == kSortNone) {
sort_ = kSortDESC;
} else if (parser.EatEqICase("count")) {
count_ = GET_OR_RET(parser.TakeInt<int>(NumericRange<int>{1, std::numeric_limits<int>::max()}));
} else if (parser.EatEqICase("withcoord")) {
with_coord_ = true;
} else if (parser.EatEqICase("withdist")) {
with_dist_ = true;
} else if (parser.EatEqICase("withhash")) {
with_hash_ = true;
} else {
return {Status::RedisParseErr, "Invalid argument given"};
}
}
if (origin_point_type_ == kNone) {
return {Status::RedisParseErr, "exactly one of FROMMEMBER or FROMLONLAT can be specified for GEOSEARCH"};
}
if (member_ != "" && longitude_ != 0 && latitude_ != 0) {
return {Status::RedisParseErr, "please use only one of FROMMEMBER or FROMLONLAT"};
}
auto s = createGeoShape();
if (!s.IsOK()) {
return s;
}
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<GeoPoint> geo_points;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.Search(ctx, args_[1], geo_shape_, origin_point_type_, member_, count_, sort_, false,
GetUnitConversion(), &geo_points);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
*output = generateOutput(conn, geo_points);
return Status::OK();
}
protected:
double radius_ = 0;
double height_ = 0;
double width_ = 0;
int count_ = 0;
double longitude_ = 0;
double latitude_ = 0;
std::string member_;
std::string key_;
DistanceSort sort_ = kSortNone;
GeoShapeType shape_type_ = kGeoShapeTypeNone;
OriginPointType origin_point_type_ = kNone;
GeoShape geo_shape_;
Status setShapeType(GeoShapeType shape_type) {
if (shape_type_ != kGeoShapeTypeNone) {
return {Status::RedisParseErr, "please use only one of BYBOX or BYRADIUS"};
}
shape_type_ = shape_type;
return Status::OK();
}
Status setOriginType(OriginPointType origin_point_type) {
if (origin_point_type_ != kNone) {
return {Status::RedisParseErr, "please use only one of FROMMEMBER or FROMLONLAT"};
}
origin_point_type_ = origin_point_type;
return Status::OK();
}
Status createGeoShape() {
if (shape_type_ == kGeoShapeTypeNone) {
return {Status::RedisParseErr, "please use BYBOX or BYRADIUS"};
}
geo_shape_.type = shape_type_;
geo_shape_.conversion = GetUnitConversion();
if (shape_type_ == kGeoShapeTypeCircular) {
geo_shape_.radius = radius_;
} else {
geo_shape_.width = width_;
geo_shape_.height = height_;
}
if (origin_point_type_ == kLongLat) {
geo_shape_.xy[0] = longitude_;
geo_shape_.xy[1] = latitude_;
}
return Status::OK();
}
std::string generateOutput(const Connection *conn, const std::vector<GeoPoint> &geo_points) {
size_t returned_items_count = GetReturnedItemsCount(geo_points.size(), count_);
std::vector<std::string> output;
output.reserve(returned_items_count);
for (size_t i = 0; i < returned_items_count; i++) {
const auto &geo_point = geo_points[i];
if (!with_coord_ && !with_hash_ && !with_dist_) {
output.emplace_back(redis::BulkString(geo_point.member));
} else {
std::vector<std::string> one;
one.emplace_back(redis::BulkString(geo_point.member));
if (with_dist_) {
one.emplace_back(conn->Double(GetDistanceByUnit(geo_point.dist)));
}
if (with_hash_) {
one.emplace_back(conn->Double(geo_point.score));
}
if (with_coord_) {
one.emplace_back(redis::Array({conn->Double(geo_point.longitude), conn->Double(geo_point.latitude)}));
}
output.emplace_back(redis::Array(one));
}
}
return redis::Array(output);
}
private:
bool with_coord_ = false;
bool with_dist_ = false;
bool with_hash_ = false;
};
class CommandGeoSearchStore : public CommandGeoSearch {
public:
Status Parse(const std::vector<std::string> &args) override {
CommandParser parser(args, 1);
store_key_ = GET_OR_RET(parser.TakeStr());
key_ = GET_OR_RET(parser.TakeStr());
while (parser.Good()) {
if (parser.EatEqICase("frommember")) {
auto s = setOriginType(kMember);
if (!s.IsOK()) return s;
member_ = GET_OR_RET(parser.TakeStr());
} else if (parser.EatEqICase("fromlonlat")) {
auto s = setOriginType(kLongLat);
if (!s.IsOK()) return s;
longitude_ = GET_OR_RET(parser.TakeFloat());
latitude_ = GET_OR_RET(parser.TakeFloat());
s = ValidateLongLat(&longitude_, &latitude_);
if (!s.IsOK()) return s;
} else if (parser.EatEqICase("byradius")) {
auto s = setShapeType(kGeoShapeTypeCircular);
if (!s.IsOK()) return s;
radius_ = GET_OR_RET(parser.TakeFloat());
std::string distance_raw = GET_OR_RET(parser.TakeStr());
s = ParseDistanceUnit(distance_raw);
if (!s.IsOK()) return s;
} else if (parser.EatEqICase("bybox")) {
auto s = setShapeType(kGeoShapeTypeRectangular);
if (!s.IsOK()) return s;
width_ = GET_OR_RET(parser.TakeFloat());
height_ = GET_OR_RET(parser.TakeFloat());
std::string distance_raw = GET_OR_RET(parser.TakeStr());
s = ParseDistanceUnit(distance_raw);
if (!s.IsOK()) return s;
} else if (parser.EatEqICase("asc") && sort_ == kSortNone) {
sort_ = kSortASC;
} else if (parser.EatEqICase("desc") && sort_ == kSortNone) {
sort_ = kSortDESC;
} else if (parser.EatEqICase("count")) {
count_ = GET_OR_RET(parser.TakeInt<int>(NumericRange<int>{1, std::numeric_limits<int>::max()}));
} else if (parser.EatEqICase("storedist")) {
store_distance_ = true;
} else {
return {Status::RedisParseErr, "Invalid argument given"};
}
}
if (origin_point_type_ == kNone) {
return {Status::RedisParseErr, "exactly one of FROMMEMBER or FROMLONLAT can be specified for GEOSEARCHSTORE"};
}
if (member_ != "" && longitude_ != 0 && latitude_ != 0) {
return {Status::RedisParseErr, "please use only one of FROMMEMBER or FROMLONLAT"};
}
auto s = createGeoShape();
if (!s.IsOK()) {
return s;
}
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<GeoPoint> geo_points;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.SearchStore(ctx, args_[2], geo_shape_, origin_point_type_, member_, count_, sort_, store_key_,
store_distance_, GetUnitConversion(), &geo_points);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
*output = redis::Integer(GetReturnedItemsCount(geo_points.size(), count_));
return Status::OK();
}
static std::vector<CommandKeyRange> Range([[maybe_unused]] const std::vector<std::string> &args) {
return {{1, 1, 1}, {2, 2, 1}};
}
private:
bool store_distance_ = false;
std::string store_key_;
};
class CommandGeoRadiusByMember : public CommandGeoRadius {
public:
CommandGeoRadiusByMember() = default;
Status Parse(const std::vector<std::string> &args) override {
auto radius = ParseFloat(args[3]);
if (!radius) {
return {Status::RedisParseErr, errValueIsNotFloat};
}
radius_ = *radius;
auto s = ParseDistanceUnit(args[4]);
if (!s.IsOK()) return s;
s = ParseRadiusExtraOption(5);
if (!s.IsOK()) return s;
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<GeoPoint> geo_points;
redis::Geo geo_db(srv->storage, conn->GetNamespace());
auto s = geo_db.RadiusByMember(ctx, args_[1], args_[2], GetRadiusMeters(radius_), count_, sort_, store_key_,
store_distance_, GetUnitConversion(), &geo_points);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
if (store_key_.size() != 0) {
*output = redis::Integer(GetReturnedItemsCount(geo_points.size(), count_));
} else {
*output = GenerateOutput(conn, geo_points);
}
return Status::OK();
}
static std::vector<CommandKeyRange> Range(const std::vector<std::string> &args) {
int store_key = 0;
// Check for the presence of the stored key in the command args.
for (size_t i = 5; i < args.size(); i++) {
// For the case when a user specifies both "store" and "storedist" options,
// the second key will override the first key. The behavior is kept the same
// as in ParseRadiusExtraOption method.
if ((util::ToLower(args[i]) == "store" || util::ToLower(args[i]) == "storedist") && i + 1 < args.size()) {
store_key = (int)i + 1;
i++;
}
}
if (store_key > 0) {
return {{1, 1, 1}, {store_key, store_key, 1}};
}
return {{1, 1, 1}};
}
};
class CommandGeoRadiusReadonly : public CommandGeoRadius {
public:
CommandGeoRadiusReadonly() = default;
};
class CommandGeoRadiusByMemberReadonly : public CommandGeoRadiusByMember {
public:
CommandGeoRadiusByMemberReadonly() = default;
};
REDIS_REGISTER_COMMANDS(Geo, MakeCmdAttr<CommandGeoAdd>("geoadd", -5, "write", 1, 1, 1),
MakeCmdAttr<CommandGeoDist>("geodist", -4, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGeoHash>("geohash", -3, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGeoPos>("geopos", -3, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGeoRadius>("georadius", -6, "write", CommandGeoRadius::Range),
MakeCmdAttr<CommandGeoRadiusByMember>("georadiusbymember", -5, "write",
CommandGeoRadiusByMember::Range),
MakeCmdAttr<CommandGeoRadiusReadonly>("georadius_ro", -6, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGeoRadiusByMemberReadonly>("georadiusbymember_ro", -5, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGeoSearch>("geosearch", -7, "read-only", 1, 1, 1),
MakeCmdAttr<CommandGeoSearchStore>("geosearchstore", -8, "write", CommandGeoSearchStore::Range))
} // namespace redis