blob: 584f412937e74e39585cfadba834a1225889001b [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 <cstdint>
#include "commander.h"
#include "commands/ttl_util.h"
#include "error_constants.h"
#include "server/redis_reply.h"
#include "server/server.h"
#include "storage/redis_db.h"
#include "time_util.h"
namespace redis {
namespace {
std::string HashSubkeyEncodingModeName(HashSubkeyEncodingMode mode) {
switch (mode) {
case HashSubkeyEncodingMode::kLegacy:
return "legacy";
case HashSubkeyEncodingMode::kFieldExpiration:
return "field-expiration";
}
return "unknown";
}
std::string JsonStorageFormatName(JsonStorageFormat format) {
switch (format) {
case JsonStorageFormat::JSON:
return "json";
case JsonStorageFormat::CBOR:
return "cbor";
}
return "unknown";
}
} // namespace
class CommandType : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
RedisType type = kRedisNone;
auto s = redis.Type(ctx, args_[1], &type);
if (s.ok()) {
if (type >= RedisTypeNames.size()) return {Status::RedisExecErr, "Invalid type"};
*output = redis::SimpleString(RedisTypeNames[type]);
return Status::OK();
}
return {Status::RedisExecErr, s.ToString()};
}
};
class CommandMove : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
GET_OR_RET(ParseInt<int64_t>(args[2], 10));
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
uint32_t count = 0;
redis::Database redis(srv->storage, conn->GetNamespace());
rocksdb::Status s = redis.Exists(ctx, {args_[1]}, &count);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
*output = count ? redis::Integer(1) : redis::Integer(0);
return Status::OK();
}
};
class CommandMoveX : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::string &key = args_[1], &token = args_[2];
redis::Database redis(srv->storage, conn->GetNamespace());
std::string ns;
AuthResult auth_result = srv->AuthenticateUser(token, &ns);
switch (auth_result) {
case AuthResult::NO_REQUIRE_PASS:
return {Status::NotOK, "Forbidden to move key when requirepass is empty"};
case AuthResult::INVALID_PASSWORD:
return {Status::NotOK, "Invalid password"};
case AuthResult::IS_USER:
case AuthResult::IS_ADMIN:
break;
}
Database::CopyResult res = Database::CopyResult::DONE;
std::string ns_key = redis.AppendNamespacePrefix(key);
std::string new_ns_key = ComposeNamespaceKey(ns, key, srv->storage->IsSlotIdEncoded());
auto s = redis.Copy(ctx, ns_key, new_ns_key, true, true, &res);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
if (res == Database::CopyResult::DONE) {
*output = redis::Integer(1);
} else {
*output = redis::Integer(0);
}
return Status::OK();
}
};
class CommandObject : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
if (util::ToLower(args_[1]) == "dump") {
redis::Database redis(srv->storage, conn->GetNamespace());
std::vector<std::string> infos;
auto s = redis.Dump(ctx, args_[2], &infos);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
output->append(redis::MultiLen(infos.size()));
for (const auto &info : infos) {
output->append(redis::BulkString(info));
}
} else {
return {Status::RedisExecErr, "object subcommand must be dump"};
}
return Status::OK();
}
};
class CommandTTL : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
int64_t ttl = 0;
auto s = redis.TTL(ctx, args_[1], &ttl);
if (s.ok()) {
*output = redis::Integer(ttl > 0 ? ttl / 1000 : ttl);
return Status::OK();
}
return {Status::RedisExecErr, s.ToString()};
}
};
class CommandPTTL : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
int64_t ttl = 0;
auto s = redis.TTL(ctx, args_[1], &ttl);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
*output = redis::Integer(ttl);
return Status::OK();
}
};
class CommandExists : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<rocksdb::Slice> keys;
keys.reserve(args_.size() - 1);
for (size_t i = 1; i < args_.size(); i++) {
keys.emplace_back(args_[i]);
}
uint32_t cnt = 0;
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.Exists(ctx, keys, &cnt);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
*output = redis::Integer(cnt);
return Status::OK();
}
};
class CommandExpire : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
ttl_ = GET_OR_RET(ParseInt<int64_t>(args[2], 10));
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.Expire(ctx, args_[1], ttl_ * 1000 + util::GetTimeStampMS());
if (s.ok()) {
*output = redis::Integer(1);
} else {
*output = redis::Integer(0);
}
return Status::OK();
}
private:
uint64_t ttl_ = 0;
};
class CommandPExpire : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
seconds_ = GET_OR_RET(ParseInt<int64_t>(args[2], 10)) + util::GetTimeStampMS();
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.Expire(ctx, args_[1], seconds_);
if (s.ok()) {
*output = redis::Integer(1);
} else {
*output = redis::Integer(0);
}
return Status::OK();
}
private:
uint64_t seconds_ = 0;
};
class CommandExpireAt : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
auto parse_result = ParseInt<int64_t>(args[2], 10);
if (!parse_result) {
return {Status::RedisParseErr, errValueNotInteger};
}
timestamp_ = *parse_result;
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.Expire(ctx, args_[1], timestamp_ * 1000);
if (s.ok()) {
*output = redis::Integer(1);
} else {
*output = redis::Integer(0);
}
return Status::OK();
}
private:
uint64_t timestamp_ = 0;
};
class CommandPExpireAt : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
auto parse_result = ParseInt<int64_t>(args[2], 10);
if (!parse_result) {
return {Status::RedisParseErr, errValueNotInteger};
}
timestamp_ = *parse_result;
return Commander::Parse(args);
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.Expire(ctx, args_[1], timestamp_);
if (s.ok()) {
*output = redis::Integer(1);
} else {
*output = redis::Integer(0);
}
return Status::OK();
}
private:
uint64_t timestamp_ = 0;
};
class CommandExpireTime : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
uint64_t timestamp = 0;
auto s = redis.GetExpireTime(ctx, args_[1], &timestamp);
if (s.ok()) {
*output = timestamp > 0 ? redis::Integer(timestamp / 1000) : redis::Integer(-1);
} else if (s.IsNotFound()) {
*output = redis::Integer(-2);
} else {
return {Status::RedisExecErr, s.ToString()};
}
return Status::OK();
}
};
class CommandPExpireTime : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
uint64_t timestamp = 0;
auto s = redis.GetExpireTime(ctx, args_[1], &timestamp);
if (s.ok()) {
*output = timestamp > 0 ? redis::Integer(timestamp) : redis::Integer(-1);
} else if (s.IsNotFound()) {
*output = redis::Integer(-2);
} else {
return {Status::RedisExecErr, s.ToString()};
}
return Status::OK();
}
};
class CommandPersist : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
int64_t ttl = 0;
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.TTL(ctx, args_[1], &ttl);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
if (ttl == -1 || ttl == -2) {
*output = redis::Integer(0);
return Status::OK();
}
s = redis.Expire(ctx, args_[1], 0);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
*output = redis::Integer(1);
return Status::OK();
}
};
class CommandDel : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
std::vector<rocksdb::Slice> keys;
keys.reserve(args_.size() - 1);
for (size_t i = 1; i < args_.size(); i++) {
keys.emplace_back(args_[i]);
}
uint64_t cnt = 0;
redis::Database redis(srv->storage, conn->GetNamespace());
auto s = redis.MDel(ctx, keys, &cnt);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
*output = redis::Integer(cnt);
return Status::OK();
}
};
class CommandRename : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
Database::CopyResult res = Database::CopyResult::DONE;
std::string ns_key = redis.AppendNamespacePrefix(args_[1]);
std::string new_ns_key = redis.AppendNamespacePrefix(args_[2]);
auto s = redis.Copy(ctx, ns_key, new_ns_key, false, true, &res);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
if (res == Database::CopyResult::KEY_NOT_EXIST) return {Status::RedisExecErr, "no such key"};
*output = redis::RESP_OK;
return Status::OK();
}
};
class CommandRenameNX : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
Database::CopyResult res = Database::CopyResult::DONE;
std::string ns_key = redis.AppendNamespacePrefix(args_[1]);
std::string new_ns_key = redis.AppendNamespacePrefix(args_[2]);
auto s = redis.Copy(ctx, ns_key, new_ns_key, true, true, &res);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
switch (res) {
case Database::CopyResult::KEY_NOT_EXIST:
return {Status::RedisExecErr, "no such key"};
case Database::CopyResult::DONE:
*output = redis::Integer(1);
break;
case Database::CopyResult::KEY_ALREADY_EXIST:
*output = redis::Integer(0);
break;
}
return Status::OK();
}
};
class CommandCopy : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
CommandParser parser(args, 3);
while (parser.Good()) {
if (parser.EatEqICase("db")) {
auto db_num = GET_OR_RET(parser.TakeInt());
// There's only one database in Kvrocks, so the DB must be 0 here.
if (db_num != 0) {
return {Status::RedisParseErr, errInvalidSyntax};
}
} else if (parser.EatEqICase("replace")) {
replace_ = true;
} else {
return parser.InvalidSyntax();
}
}
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
Database::CopyResult res = Database::CopyResult::DONE;
std::string ns_key = redis.AppendNamespacePrefix(args_[1]);
std::string new_ns_key = redis.AppendNamespacePrefix(args_[2]);
auto s = redis.Copy(ctx, ns_key, new_ns_key, !replace_, false, &res);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
switch (res) {
case Database::CopyResult::KEY_NOT_EXIST:
return {Status::RedisExecErr, "no such key"};
case Database::CopyResult::DONE:
*output = redis::Integer(1);
break;
case Database::CopyResult::KEY_ALREADY_EXIST:
*output = redis::Integer(0);
break;
}
return Status::OK();
}
private:
bool replace_ = false;
};
template <bool ReadOnly>
class CommandSort : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
CommandParser parser(args, 2);
while (parser.Good()) {
if (parser.EatEqICase("BY")) {
if (!sort_argument_.sortby.empty()) return {Status::InvalidArgument, "don't use multiple BY parameters"};
sort_argument_.sortby = GET_OR_RET(parser.TakeStr());
if (sort_argument_.sortby.find('*') == std::string::npos) {
sort_argument_.dontsort = true;
} else {
/* TODO:
* If BY is specified with a real pattern, we can't accept it in cluster mode,
* unless we can make sure the keys formed by the pattern are in the same slot
* as the key to sort.
* If BY is specified with a real pattern, we can't accept
* it if no full ACL key access is applied for this command. */
}
} else if (parser.EatEqICase("LIMIT")) {
sort_argument_.offset = GET_OR_RET(parser.template TakeInt<int>());
sort_argument_.count = GET_OR_RET(parser.template TakeInt<int>());
} else if (parser.EatEqICase("GET")) {
/* TODO:
* If GET is specified with a real pattern, we can't accept it in cluster mode,
* unless we can make sure the keys formed by the pattern are in the same slot
* as the key to sort. */
sort_argument_.getpatterns.push_back(GET_OR_RET(parser.TakeStr()));
} else if (parser.EatEqICase("ASC")) {
sort_argument_.desc = false;
} else if (parser.EatEqICase("DESC")) {
sort_argument_.desc = true;
} else if (parser.EatEqICase("ALPHA")) {
sort_argument_.alpha = true;
} else if (parser.EatEqICase("STORE")) {
if constexpr (ReadOnly) {
return {Status::RedisParseErr, "SORT_RO is read-only and does not support the STORE parameter"};
}
sort_argument_.storekey = GET_OR_RET(parser.TakeStr());
} else {
return parser.InvalidSyntax();
}
}
return Status::OK();
}
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
RedisType type = kRedisNone;
if (auto s = redis.Type(ctx, args_[1], &type); !s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
if (type != RedisType::kRedisList && type != RedisType::kRedisSet && type != RedisType::kRedisZSet) {
return {Status::RedisWrongType, "Operation against a key holding the wrong kind of value"};
}
/* When sorting a set with no sort specified, we must sort the output
* so the result is consistent across scripting and replication.
*
* The other types (list, sorted set) will retain their native order
* even if no sort order is requested, so they remain stable across
* scripting and replication.
*
* TODO: support CLIENT_SCRIPT flag, (!storekey_.empty() || c->flags & CLIENT_SCRIPT)) */
if (sort_argument_.dontsort && type == RedisType::kRedisSet && (!sort_argument_.storekey.empty())) {
/* Force ALPHA sorting */
sort_argument_.dontsort = false;
sort_argument_.alpha = true;
sort_argument_.sortby = "";
}
std::vector<std::optional<std::string>> sorted_elems;
Database::SortResult res = Database::SortResult::DONE;
if (auto s = redis.Sort(ctx, type, args_[1], sort_argument_, &sorted_elems, &res); !s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
switch (res) {
case Database::SortResult::UNKNOWN_TYPE:
return {Status::RedisErrorNoPrefix, "Unknown Type"};
case Database::SortResult::DOUBLE_CONVERT_ERROR:
return {Status::RedisErrorNoPrefix, "One or more scores can't be converted into double"};
case Database::SortResult::LIMIT_EXCEEDED:
return {Status::RedisErrorNoPrefix,
"The number of elements to be sorted exceeds SORT_LENGTH_LIMIT = " + std::to_string(SORT_LENGTH_LIMIT)};
case Database::SortResult::DONE:
if (sort_argument_.storekey.empty()) {
std::vector<std::string> output_vec;
output_vec.reserve(sorted_elems.size());
for (const auto &elem : sorted_elems) {
output_vec.emplace_back(elem.has_value() ? redis::BulkString(elem.value()) : conn->NilString());
}
*output = redis::Array(output_vec);
} else {
*output = Integer(sorted_elems.size());
}
break;
}
return Status::OK();
}
private:
SortArgument sort_argument_;
};
class CommandKMetadata : public Commander {
public:
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
redis::Database redis(srv->storage, conn->GetNamespace());
std::string &key = args_[1];
std::string nskey = redis.AppendNamespacePrefix(key);
std::string raw_metadata;
Slice rest;
Metadata metadata(kRedisNone, false);
auto s = redis.GetMetadata(ctx, RedisTypes::All(), nskey, &raw_metadata, &metadata, &rest);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
std::map<std::string, std::string> response = {
{redis::BulkString("type"), redis::BulkString(metadata.TypeName())},
{redis::BulkString("expire"), redis::Integer(metadata.expire)},
{redis::BulkString("flags"), redis::Integer(metadata.flags)},
};
if (!metadata.IsSingleKVType()) {
response.insert({redis::BulkString("size"), redis::Integer(metadata.size)});
response.insert({redis::BulkString("version"), redis::Integer(metadata.version)});
}
if (metadata.Type() == kRedisHash) {
HashMetadata hash_metadata(false);
Slice metadata_bytes(raw_metadata);
s = Database::ParseMetadata({kRedisHash}, &metadata_bytes, &hash_metadata);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
response.insert({redis::BulkString("mode"), redis::BulkString(HashSubkeyEncodingModeName(hash_metadata.mode))});
if (hash_metadata.IsFieldExpirationEncoding()) {
response.insert({redis::BulkString("persist"), redis::Integer(hash_metadata.persist)});
response.insert({redis::BulkString("lower"), redis::Integer(hash_metadata.lower)});
response.insert({redis::BulkString("upper"), redis::Integer(hash_metadata.upper)});
}
} else if (metadata.Type() == kRedisList) {
ListMetadata list_metadata(false);
Slice metadata_bytes(raw_metadata);
s = Database::ParseMetadata({kRedisList}, &metadata_bytes, &list_metadata);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
response.insert({redis::BulkString("head"), redis::Integer(list_metadata.head)});
response.insert({redis::BulkString("tail"), redis::Integer(list_metadata.tail)});
} else if (metadata.Type() == kRedisJson) {
JsonMetadata json_metadata(false);
Slice metadata_bytes(raw_metadata);
s = Database::ParseMetadata({kRedisJson}, &metadata_bytes, &json_metadata);
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
response.insert({redis::BulkString("format"), redis::BulkString(JsonStorageFormatName(json_metadata.format))});
}
*output = conn->Map(response);
return Status::OK();
}
};
REDIS_REGISTER_COMMANDS(Key, MakeCmdAttr<CommandTTL>("ttl", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandPTTL>("pttl", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandType>("type", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandMove>("move", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandMoveX>("movex", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandObject>("object", 3, "read-only", 2, 2, 1),
MakeCmdAttr<CommandExists>("exists", -2, "read-only", 1, -1, 1),
MakeCmdAttr<CommandPersist>("persist", 2, "write", 1, 1, 1),
MakeCmdAttr<CommandExpire>("expire", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandPExpire>("pexpire", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandExpireAt>("expireat", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandPExpireAt>("pexpireat", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandExpireTime>("expiretime", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandPExpireTime>("pexpiretime", 2, "read-only", 1, 1, 1),
MakeCmdAttr<CommandDel>("del", -2, "write no-dbsize-check", 1, -1, 1),
MakeCmdAttr<CommandDel>("unlink", -2, "write no-dbsize-check", 1, -1, 1),
MakeCmdAttr<CommandRename>("rename", 3, "write", 1, 2, 1),
MakeCmdAttr<CommandRenameNX>("renamenx", 3, "write", 1, 2, 1),
MakeCmdAttr<CommandCopy>("copy", -3, "write", 1, 2, 1),
MakeCmdAttr<CommandSort<false>>("sort", -2, "write slow", 1, 1, 1),
MakeCmdAttr<CommandSort<true>>("sort_ro", -2, "read-only slow", 1, 1, 1),
MakeCmdAttr<CommandKMetadata>("kmetadata", 2, "read-only", 1, 1, 1))
} // namespace redis