Added ZRemRangeByLex and ZRemRangeByRank to embedded API.

This commit is contained in:
Kelvin Clement Mwinuka
2024-05-14 02:11:10 +08:00
parent aac4349480
commit aa7b5fa8cc
8 changed files with 1075 additions and 999 deletions

View File

@@ -14,7 +14,8 @@ build:
env CGO_ENABLED=1 CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 DEST=bin/linux/x86_64 make build-server env CGO_ENABLED=1 CC=x86_64-linux-musl-gcc GOOS=linux GOARCH=amd64 DEST=bin/linux/x86_64 make build-server
run: run:
make build && docker-compose up --build make build && \
docker-compose up --build
test-unit: test-unit:
env RACE=false OUT=internal/modules/admin/testdata make build-modules-test && \ env RACE=false OUT=internal/modules/admin/testdata make build-modules-test && \

File diff suppressed because it is too large Load Diff

View File

@@ -17,6 +17,7 @@ package echovault
import ( import (
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
"strconv" "strconv"
"strings"
) )
// SetOptions modifies the behaviour for the Set command // SetOptions modifies the behaviour for the Set command
@@ -124,12 +125,12 @@ func (server *EchoVault) Set(key, value string, options SetOptions) (string, err
// //
// `kvPairs` - map[string]string - a map representing all the keys and values to be set. // `kvPairs` - map[string]string - a map representing all the keys and values to be set.
// //
// Returns: "OK" if the set is successful. // Returns: true if the set is successful.
// //
// Errors: // Errors:
// //
// "key <key> does already exists" - when the NX flag is set to true and the key already exists. // "key <key> already exists" - when the NX flag is set to true and the key already exists.
func (server *EchoVault) MSet(kvPairs map[string]string) (string, error) { func (server *EchoVault) MSet(kvPairs map[string]string) (bool, error) {
cmd := []string{"MSET"} cmd := []string{"MSET"}
for k, v := range kvPairs { for k, v := range kvPairs {
@@ -138,10 +139,15 @@ func (server *EchoVault) MSet(kvPairs map[string]string) (string, error) {
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true) b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil { if err != nil {
return "", err return false, err
} }
return internal.ParseStringResponse(b) s, err := internal.ParseStringResponse(b)
if err != nil {
return false, err
}
return strings.EqualFold(s, "ok"), nil
} }
// Get retrieves the value at the provided key. // Get retrieves the value at the provided key.
@@ -279,7 +285,7 @@ func (server *EchoVault) PTTL(key string) (int, error) {
// `options` - ExpireOptions // `options` - ExpireOptions
// //
// Returns: true if the key's expiry was successfully updated. // Returns: true if the key's expiry was successfully updated.
func (server *EchoVault) Expire(key string, seconds int, options ExpireOptions) (int, error) { func (server *EchoVault) Expire(key string, seconds int, options ExpireOptions) (bool, error) {
cmd := []string{"EXPIRE", key, strconv.Itoa(seconds)} cmd := []string{"EXPIRE", key, strconv.Itoa(seconds)}
switch { switch {
@@ -295,10 +301,10 @@ func (server *EchoVault) Expire(key string, seconds int, options ExpireOptions)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true) b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil { if err != nil {
return 0, err return false, err
} }
return internal.ParseIntegerResponse(b) return internal.ParseBooleanResponse(b)
} }
// PExpire set the given key's expiry in milliseconds from now. // PExpire set the given key's expiry in milliseconds from now.
@@ -313,7 +319,7 @@ func (server *EchoVault) Expire(key string, seconds int, options ExpireOptions)
// `options` - PExpireOptions // `options` - PExpireOptions
// //
// Returns: true if the key's expiry was successfully updated. // Returns: true if the key's expiry was successfully updated.
func (server *EchoVault) PExpire(key string, milliseconds int, options PExpireOptions) (int, error) { func (server *EchoVault) PExpire(key string, milliseconds int, options PExpireOptions) (bool, error) {
cmd := []string{"PEXPIRE", key, strconv.Itoa(milliseconds)} cmd := []string{"PEXPIRE", key, strconv.Itoa(milliseconds)}
switch { switch {
@@ -329,10 +335,10 @@ func (server *EchoVault) PExpire(key string, milliseconds int, options PExpireOp
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true) b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil { if err != nil {
return 0, err return false, err
} }
return internal.ParseIntegerResponse(b) return internal.ParseBooleanResponse(b)
} }
// ExpireAt set the given key's expiry in unix epoch seconds. // ExpireAt set the given key's expiry in unix epoch seconds.

View File

@@ -80,7 +80,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time int time int
expireOpts ExpireOptions expireOpts ExpireOptions
pexpireOpts PExpireOptions pexpireOpts PExpireOptions
want int want bool
wantErr bool wantErr bool
}{ }{
{ {
@@ -92,7 +92,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: time.Time{}}, "key1": {Value: "value1", ExpireAt: time.Time{}},
}, },
want: 1, want: true,
wantErr: false, wantErr: false,
}, },
{ {
@@ -104,7 +104,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key2": {Value: "value2", ExpireAt: time.Time{}}, "key2": {Value: "value2", ExpireAt: time.Time{}},
}, },
want: 1, want: true,
wantErr: false, wantErr: false,
}, },
{ {
@@ -116,11 +116,11 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key3": {Value: "value3", ExpireAt: time.Time{}}, "key3": {Value: "value3", ExpireAt: time.Time{}},
}, },
want: 1, want: true,
wantErr: false, wantErr: false,
}, },
{ {
name: "Return 0 when NX flag is provided and key already has an expiry time", name: "Return false when NX flag is provided and key already has an expiry time",
cmd: "EXPIRE", cmd: "EXPIRE",
key: "key4", key: "key4",
time: 1000, time: 1000,
@@ -128,7 +128,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)}, "key4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
want: 0, want: false,
wantErr: false, wantErr: false,
}, },
{ {
@@ -140,11 +140,11 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)}, "key5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 1, want: true,
wantErr: false, wantErr: false,
}, },
{ {
name: "Return 0 when key does not have an expiry and the XX flag is provided", name: "Return false when key does not have an expiry and the XX flag is provided",
cmd: "EXPIRE", cmd: "EXPIRE",
time: 1000, time: 1000,
expireOpts: ExpireOptions{XX: true}, expireOpts: ExpireOptions{XX: true},
@@ -152,7 +152,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key6": {Value: "value6", ExpireAt: time.Time{}}, "key6": {Value: "value6", ExpireAt: time.Time{}},
}, },
want: 0, want: false,
wantErr: false, wantErr: false,
}, },
{ {
@@ -164,11 +164,11 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)}, "key7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 1, want: true,
wantErr: false, wantErr: false,
}, },
{ {
name: "Return 0 when GT flag is passed and current expiry time is greater than provided time", name: "Return false when GT flag is passed and current expiry time is greater than provided time",
cmd: "EXPIRE", cmd: "EXPIRE",
key: "key8", key: "key8",
time: 1000, time: 1000,
@@ -176,11 +176,11 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, "key8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
want: 0, want: false,
wantErr: false, wantErr: false,
}, },
{ {
name: "Return 0 when GT flag is passed and key does not have an expiry time", name: "Return false when GT flag is passed and key does not have an expiry time",
cmd: "EXPIRE", cmd: "EXPIRE",
key: "key9", key: "key9",
time: 1000, time: 1000,
@@ -188,7 +188,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key9": {Value: "value9", ExpireAt: time.Time{}}, "key9": {Value: "value9", ExpireAt: time.Time{}},
}, },
want: 0, want: false,
wantErr: false, wantErr: false,
}, },
{ {
@@ -200,11 +200,11 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)}, "key10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
want: 1, want: true,
wantErr: false, wantErr: false,
}, },
{ {
name: "Return 0 when LT flag is passed and current expiry time is less than provided time", name: "Return false when LT flag is passed and current expiry time is less than provided time",
cmd: "EXPIRE", cmd: "EXPIRE",
key: "key11", key: "key11",
time: 50000, time: 50000,
@@ -212,7 +212,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key11": {Value: "value11", ExpireAt: mockClock.Now().Add(30 * time.Second)}, "key11": {Value: "value11", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 0, want: false,
wantErr: false, wantErr: false,
}, },
} }
@@ -223,7 +223,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
presetKeyData(server, context.Background(), k, d) presetKeyData(server, context.Background(), k, d)
} }
} }
var got int var got bool
var err error var err error
if strings.EqualFold(tt.cmd, "PEXPIRE") { if strings.EqualFold(tt.cmd, "PEXPIRE") {
got, err = server.PExpire(tt.key, tt.time, tt.pexpireOpts) got, err = server.PExpire(tt.key, tt.time, tt.pexpireOpts)
@@ -752,13 +752,13 @@ func TestEchoVault_MSET(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
kvPairs map[string]string kvPairs map[string]string
want string want bool
wantErr bool wantErr bool
}{ }{
{ {
name: "Set multiple keys", name: "Set multiple keys",
kvPairs: map[string]string{"key1": "value1", "key2": "10", "key3": "3.142"}, kvPairs: map[string]string{"key1": "value1", "key2": "10", "key3": "3.142"},
want: "OK", want: true,
wantErr: false, wantErr: false,
}, },
} }

View File

@@ -87,6 +87,8 @@ type ZMPopOptions struct {
// //
// ByLex returns the elements within the lexicographical ranges specified. // ByLex returns the elements within the lexicographical ranges specified.
// //
// Rev reverses the result from the previous filters.
//
// Offset specifies the offset to from which to start the ZRange process. // Offset specifies the offset to from which to start the ZRange process.
// //
// Count specifies the number of elements to return. // Count specifies the number of elements to return.
@@ -94,6 +96,7 @@ type ZRangeOptions struct {
WithScores bool WithScores bool
ByScore bool ByScore bool
ByLex bool ByLex bool
Rev bool
Offset uint Offset uint
Count uint Count uint
} }
@@ -864,6 +867,62 @@ func (server *EchoVault) ZRemRangeByScore(key string, min float64, max float64)
return internal.ParseIntegerResponse(b) return internal.ParseIntegerResponse(b)
} }
// ZRemRangeByLex Removes the elements that are lexicographically between min and max.
//
// Parameters:
//
// `key` - string - The keys to the sorted set.
//
// `min` - string - The minimum lexicographic boundary.
//
// `max` - string - The maximum lexicographic boundary.
//
// Returns: The number of elements that were successfully removed.
//
// Errors:
//
// "value at <key> is not a sorted set" - when a key exists but is not a sorted set.
func (server *EchoVault) ZRemRangeByLex(key, min, max string) (int, error) {
b, err := server.handleCommand(
server.context, internal.EncodeCommand([]string{"ZREMRANGEBYLEX", key, min, max}),
nil,
false,
true,
)
if err != nil {
return 0, err
}
return internal.ParseIntegerResponse(b)
}
// ZRemRangeByRank Removes the elements that are ranked between min and max.
//
// Parameters:
//
// `key` - string - The keys to the sorted set.
//
// `min` - int - The minimum rank boundary.
//
// `max` - int - The maximum rank boundary.
//
// Returns: The number of elements that were successfully removed.
//
// Errors:
//
// "value at <key> is not a sorted set" - when a key exists but is not a sorted set.
func (server *EchoVault) ZRemRangeByRank(key string, min, max int) (int, error) {
b, err := server.handleCommand(
server.context, internal.EncodeCommand([]string{"ZREMRANGEBYRANK", key, strconv.Itoa(min), strconv.Itoa(max)}),
nil,
false,
true,
)
if err != nil {
return 0, err
}
return internal.ParseIntegerResponse(b)
}
// ZRange Returns the range of elements in the sorted set. // ZRange Returns the range of elements in the sorted set.
// //
// Parameters: // Parameters:
@@ -889,6 +948,8 @@ func (server *EchoVault) ZRange(key, start, stop string, options ZRangeOptions)
cmd = append(cmd, "BYSCORE") cmd = append(cmd, "BYSCORE")
case options.ByLex: case options.ByLex:
cmd = append(cmd, "BYLEX") cmd = append(cmd, "BYLEX")
case options.Rev:
cmd = append(cmd, "REV")
default: default:
cmd = append(cmd, "BYSCORE") cmd = append(cmd, "BYSCORE")
} }
@@ -941,6 +1002,8 @@ func (server *EchoVault) ZRangeStore(destination, source, start, stop string, op
cmd = append(cmd, "BYSCORE") cmd = append(cmd, "BYSCORE")
case options.ByLex: case options.ByLex:
cmd = append(cmd, "BYLEX") cmd = append(cmd, "BYLEX")
case options.Rev:
cmd = append(cmd, "REV")
default: default:
cmd = append(cmd, "BYSCORE") cmd = append(cmd, "BYSCORE")
} }

View File

@@ -333,8 +333,6 @@ func Test_AdminCommand(t *testing.T) {
}) })
t.Run("Test MODULE LOAD command", func(t *testing.T) { t.Run("Test MODULE LOAD command", func(t *testing.T) {
t.Parallel()
port, err := internal.GetFreePort() port, err := internal.GetFreePort()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@@ -503,8 +501,6 @@ func Test_AdminCommand(t *testing.T) {
}) })
t.Run("Test MODULE UNLOAD command", func(t *testing.T) { t.Run("Test MODULE UNLOAD command", func(t *testing.T) {
t.Parallel()
port, err := internal.GetFreePort() port, err := internal.GetFreePort()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@@ -527,6 +523,7 @@ func Test_AdminCommand(t *testing.T) {
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return
} }
respConn := resp.NewConn(conn) respConn := resp.NewConn(conn)
@@ -692,8 +689,6 @@ func Test_AdminCommand(t *testing.T) {
}) })
t.Run("Test MODULE LIST command", func(t *testing.T) { t.Run("Test MODULE LIST command", func(t *testing.T) {
t.Parallel()
port, err := internal.GetFreePort() port, err := internal.GetFreePort()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
@@ -716,6 +711,7 @@ func Test_AdminCommand(t *testing.T) {
conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil { if err != nil {
t.Error(err) t.Error(err)
return
} }
respConn := resp.NewConn(conn) respConn := resp.NewConn(conn)

View File

@@ -359,7 +359,7 @@ func handleExpire(params internal.HandlerFuncParams) ([]byte, error) {
} }
if _, err = params.KeyLock(params.Context, key); err != nil { if _, err = params.KeyLock(params.Context, key); err != nil {
return nil, err return []byte(":0\r\n"), err
} }
defer params.KeyUnlock(params.Context, key) defer params.KeyUnlock(params.Context, key)
@@ -496,7 +496,7 @@ PXAT - Expire at the exat time in unix milliseconds (positive integer).`,
Command: "mset", Command: "mset",
Module: constants.GenericModule, Module: constants.GenericModule,
Categories: []string{constants.WriteCategory, constants.SlowCategory}, Categories: []string{constants.WriteCategory, constants.SlowCategory},
Description: "(MSET key value [key value ...]) Automatically generic or modify multiple key/value pairs.", Description: "(MSET key value [key value ...]) Automatically set or modify multiple key/value pairs.",
Sync: true, Sync: true,
KeyExtractionFunc: msetKeyFunc, KeyExtractionFunc: msetKeyFunc,
HandlerFunc: handleMSet, HandlerFunc: handleMSet,

View File

@@ -1769,7 +1769,7 @@ The elements are ordered from lowest score to highest score`,
Categories: []string{constants.SortedSetCategory, constants.ReadCategory, constants.SlowCategory}, Categories: []string{constants.SortedSetCategory, constants.ReadCategory, constants.SlowCategory},
Description: `(ZLEXCOUNT key min max) Returns the number of elements in within the sorted set within the Description: `(ZLEXCOUNT key min max) Returns the number of elements in within the sorted set within the
lexicographical range between min and max. Returns 0, if the keys does not exist or if all the members do not have lexicographical range between min and max. Returns 0, if the keys does not exist or if all the members do not have
the same score. If the value held at key is not a sorted set, an error is returned`, the same score. If the value held at key is not a sorted set, an error is returned.`,
Sync: false, Sync: false,
KeyExtractionFunc: zlexcountKeyFunc, KeyExtractionFunc: zlexcountKeyFunc,
HandlerFunc: handleZLEXCOUNT, HandlerFunc: handleZLEXCOUNT,
@@ -1779,7 +1779,7 @@ the same score. If the value held at key is not a sorted set, an error is return
Module: constants.SortedSetModule, Module: constants.SortedSetModule,
Categories: []string{constants.SortedSetCategory, constants.ReadCategory, constants.SlowCategory}, Categories: []string{constants.SortedSetCategory, constants.ReadCategory, constants.SlowCategory},
Description: `(ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] Description: `(ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
[WITHSCORES]) Returns the range of elements in the sorted set`, [WITHSCORES]) Returns the range of elements in the sorted set.`,
Sync: false, Sync: false,
KeyExtractionFunc: zrangeKeyCount, KeyExtractionFunc: zrangeKeyCount,
HandlerFunc: handleZRANGE, HandlerFunc: handleZRANGE,
@@ -1789,7 +1789,7 @@ the same score. If the value held at key is not a sorted set, an error is return
Module: constants.SortedSetModule, Module: constants.SortedSetModule,
Categories: []string{constants.SortedSetCategory, constants.WriteCategory, constants.SlowCategory}, Categories: []string{constants.SortedSetCategory, constants.WriteCategory, constants.SlowCategory},
Description: `ZRANGESTORE destination source start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] Description: `ZRANGESTORE destination source start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count]
[WITHSCORES] Retrieve the range of elements in the sorted set and store it in destination`, [WITHSCORES] Retrieve the range of elements in the sorted set and store it in destination.`,
Sync: true, Sync: true,
KeyExtractionFunc: zrangeStoreKeyFunc, KeyExtractionFunc: zrangeStoreKeyFunc,
HandlerFunc: handleZRANGESTORE, HandlerFunc: handleZRANGESTORE,