Skip to content

Commit 11fac51

Browse files
committed
add deleterang command and test
1 parent 1639c90 commit 11fac51

File tree

4 files changed

+200
-0
lines changed

4 files changed

+200
-0
lines changed

src/commands/cmd_key.cc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "commander.h"
2424
#include "commands/ttl_util.h"
2525
#include "error_constants.h"
26+
#include "rocksdb/slice.h"
2627
#include "server/redis_reply.h"
2728
#include "server/server.h"
2829
#include "storage/redis_db.h"
@@ -356,6 +357,33 @@ class CommandDel : public Commander {
356357
}
357358
};
358359

360+
class CommandDeleteRange : public Commander {
361+
Status Parse(const std::vector<std::string> &args) override {
362+
if (args.size() != 3) {
363+
if (args.size() != 2) {
364+
return {Status::RedisParseErr, errInvalidSyntax};
365+
}
366+
if (args[1].find('*') != args[1].size() - 1) {
367+
return {Status::RedisParseErr, errInvalidSyntax};
368+
}
369+
args_[1] = args[1].substr(0, args[1].size() - 1);
370+
}
371+
return Status::OK();
372+
}
373+
374+
public:
375+
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
376+
rocksdb::Slice key_begin, key_end;
377+
key_begin = args_[1];
378+
key_end = args_.size() == 3 ? args_[2] : "";
379+
redis::Database redis(srv->storage, conn->GetNamespace());
380+
auto s = redis.DeleteRange(ctx, key_begin, key_end);
381+
if (!s.ok()) return {Status::RedisExecErr, s.ToString()};
382+
*output = redis::RESP_OK;
383+
return Status::OK();
384+
}
385+
};
386+
359387
class CommandRename : public Commander {
360388
public:
361389
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
@@ -595,6 +623,7 @@ REDIS_REGISTER_COMMANDS(Key, MakeCmdAttr<CommandTTL>("ttl", 2, "read-only", 1, 1
595623
MakeCmdAttr<CommandPExpireTime>("pexpiretime", 2, "read-only", 1, 1, 1),
596624
MakeCmdAttr<CommandDel>("del", -2, "write no-dbsize-check", 1, -1, 1),
597625
MakeCmdAttr<CommandDel>("unlink", -2, "write no-dbsize-check", 1, -1, 1),
626+
MakeCmdAttr<CommandDeleteRange>("deleterange", -2, "write no-dbsize-check", 1, -1, 1),
598627
MakeCmdAttr<CommandRename>("rename", 3, "write", 1, 2, 1),
599628
MakeCmdAttr<CommandRenameNX>("renamenx", 3, "write", 1, 2, 1),
600629
MakeCmdAttr<CommandCopy>("copy", -3, "write", 1, 2, 1),

src/storage/redis_db.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,17 @@ rocksdb::Status Database::MDel(engine::Context &ctx, const std::vector<Slice> &k
207207
return storage_->Write(ctx, storage_->DefaultWriteOptions(), batch->GetWriteBatch());
208208
}
209209

210+
rocksdb::Status Database::DeleteRange(engine::Context &ctx, const Slice &start, const Slice &end) {
211+
std::string ns_start = AppendNamespacePrefix(start);
212+
std::string ns_end;
213+
if (!end.empty()) {
214+
ns_end = AppendNamespacePrefix(end);
215+
} else {
216+
ns_end = util::StringNext(ns_start);
217+
}
218+
return storage_->DeleteRange(ctx, ns_start, ns_end);
219+
}
220+
210221
rocksdb::Status Database::Exists(engine::Context &ctx, const std::vector<Slice> &keys, int *ret) {
211222
std::vector<std::string> ns_keys;
212223
ns_keys.reserve(keys.size());

src/storage/redis_db.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class Database {
111111
[[nodiscard]] rocksdb::Status Expire(engine::Context &ctx, const Slice &user_key, uint64_t timestamp);
112112
[[nodiscard]] rocksdb::Status Del(engine::Context &ctx, const Slice &user_key);
113113
[[nodiscard]] rocksdb::Status MDel(engine::Context &ctx, const std::vector<Slice> &keys, uint64_t *deleted_cnt);
114+
[[nodiscard]] rocksdb::Status DeleteRange(engine::Context &ctx, const Slice &start, const Slice &end);
114115
[[nodiscard]] rocksdb::Status Exists(engine::Context &ctx, const std::vector<Slice> &keys, int *ret);
115116
[[nodiscard]] rocksdb::Status TTL(engine::Context &ctx, const Slice &user_key, int64_t *ttl);
116117
[[nodiscard]] rocksdb::Status GetExpireTime(engine::Context &ctx, const Slice &user_key, uint64_t *timestamp);
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package deleterange
21+
22+
import (
23+
"context"
24+
"fmt"
25+
"strconv"
26+
"testing"
27+
28+
"github.com/apache/kvrocks/tests/gocase/util"
29+
"github.com/redis/go-redis/v9"
30+
"github.com/stretchr/testify/require"
31+
"golang.org/x/exp/slices"
32+
)
33+
34+
func TestDeleteRange(t *testing.T) {
35+
srv := util.StartServer(t, map[string]string{})
36+
defer srv.Close()
37+
ctx := context.Background()
38+
rdb := srv.NewClient()
39+
defer func() { require.NoError(t, rdb.Close()) }()
40+
DeleteRangeTest(t, rdb, ctx)
41+
}
42+
43+
func DeleteRangeTest(t *testing.T, rdb *redis.Client, ctx context.Context) {
44+
t.Run("DELETERANGE ALL", func(t *testing.T) {
45+
require.NoError(t, rdb.FlushDB(ctx).Err())
46+
util.Populate(t, rdb, "key:", 1000, 10)
47+
require.NoError(t, rdb.Do(ctx, "deleterange", "*").Err())
48+
keys := scanAll(t, rdb)
49+
require.Len(t, keys, 0)
50+
})
51+
52+
t.Run("DELETERANGE BY PERFERIX", func(t *testing.T) {
53+
require.NoError(t, rdb.FlushDB(ctx).Err())
54+
55+
for _, key := range []string{"aa", "aab", "aabb", "ab", "abb", "ba", "cc", "cd", "dd"} {
56+
require.NoError(t, rdb.Set(ctx, key, "hello", 0).Err())
57+
}
58+
deleterange(t, rdb, "aa*")
59+
keys := scanAll(t, rdb)
60+
require.Equal(t, []string{"ab", "abb", "ba", "cc", "cd", "dd"}, keys)
61+
62+
deleterange(t, rdb, "c*")
63+
keys = scanAll(t, rdb)
64+
require.Equal(t, []string{"ab", "abb", "ba", "dd"}, keys)
65+
66+
deleterange(t, rdb, "d*")
67+
keys = scanAll(t, rdb)
68+
require.Equal(t, []string{"ab", "abb", "ba"}, keys)
69+
70+
deleterange(t, rdb, "a*")
71+
keys = scanAll(t, rdb)
72+
require.Equal(t, []string{"ba"}, keys)
73+
74+
deleterange(t, rdb, "*")
75+
keys = scanAll(t, rdb)
76+
require.Equal(t, []string(nil), keys)
77+
})
78+
79+
t.Run("DELETERANGE with multi namespace", func(t *testing.T) {
80+
require.NoError(t, rdb.FlushDB(ctx).Err())
81+
require.NoError(t, rdb.ConfigSet(ctx, "requirepass", "foobared").Err())
82+
83+
tokens := []string{"test_ns_token1", "test_ns_token2"}
84+
keyPrefixes := []string{"key1*", "key2*"}
85+
namespaces := []string{"test_ns1", "test_ns2"}
86+
87+
for i := 0; i < 2; i++ {
88+
require.NoError(t, rdb.Do(ctx, "AUTH", "foobared").Err())
89+
require.NoError(t, rdb.Do(ctx, "NAMESPACE", "ADD", namespaces[i], tokens[i]).Err())
90+
require.NoError(t, rdb.Do(ctx, "AUTH", tokens[i]).Err())
91+
92+
for k := 0; k < 1000; k++ {
93+
require.NoError(t, rdb.Set(ctx, fmt.Sprintf("%s:%d", keyPrefixes[i], k), "hello", 0).Err())
94+
}
95+
for k := 0; k < 100; k++ {
96+
require.NoError(t, rdb.Set(ctx, strconv.Itoa(k), "hello", 0).Err())
97+
}
98+
}
99+
100+
for i := 0; i < 2; i++ {
101+
require.NoError(t, rdb.Do(ctx, "AUTH", tokens[i]).Err())
102+
require.NoError(t, rdb.Do(ctx, "deleterange", keyPrefixes[i]).Err())
103+
104+
keys := scanAll(t, rdb, "match", keyPrefixes[i])
105+
require.Len(t, keys, 0)
106+
107+
keys = scanAll(t, rdb)
108+
require.Len(t, keys, 100)
109+
}
110+
})
111+
112+
t.Run("Deleterange reject invalid input", func(t *testing.T) {
113+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "hello").Err(), ".*syntax error.*")
114+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "hel*o").Err(), ".*syntax error.*")
115+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "*hello").Err(), ".*syntax error.*")
116+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "[").Err(), ".*syntax error.*")
117+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "\\").Err(), ".*syntax error.*")
118+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "[a").Err(), ".*syntax error.*")
119+
util.ErrorRegexp(t, rdb.Do(ctx, "DELETERANGE", "[a-]").Err(), ".*syntax error.*")
120+
})
121+
}
122+
123+
func scan(t testing.TB, rdb *redis.Client, c string, args ...interface{}) (cursor string, keys []string) {
124+
args = append([]interface{}{"SCAN", c}, args...)
125+
r := rdb.Do(context.Background(), args...)
126+
require.NoError(t, r.Err())
127+
require.Len(t, r.Val(), 2)
128+
129+
rs := r.Val().([]interface{})
130+
cursor = rs[0].(string)
131+
132+
for _, key := range rs[1].([]interface{}) {
133+
keys = append(keys, key.(string))
134+
}
135+
136+
return
137+
}
138+
139+
func deleterange(t testing.TB, rdb *redis.Client, c string, args ...interface{}) {
140+
args = append([]interface{}{"DELETERANGE", c}, args...)
141+
r := rdb.Do(context.Background(), args...)
142+
require.NoError(t, r.Err())
143+
}
144+
145+
func scanAll(t testing.TB, rdb *redis.Client, args ...interface{}) (keys []string) {
146+
c := "0"
147+
for {
148+
cursor, keyList := scan(t, rdb, c, args...)
149+
150+
c = cursor
151+
keys = append(keys, keyList...)
152+
153+
if c == "0" {
154+
slices.Sort(keys)
155+
keys = slices.Compact(keys)
156+
return
157+
}
158+
}
159+
}

0 commit comments

Comments
 (0)