mirror of
https://github.com/nalgeon/redka.git
synced 2025-10-15 20:40:39 +08:00
feat: command - set
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/nalgeon/redka/internal/command/key"
|
"github.com/nalgeon/redka/internal/command/key"
|
||||||
"github.com/nalgeon/redka/internal/command/list"
|
"github.com/nalgeon/redka/internal/command/list"
|
||||||
"github.com/nalgeon/redka/internal/command/server"
|
"github.com/nalgeon/redka/internal/command/server"
|
||||||
|
"github.com/nalgeon/redka/internal/command/set"
|
||||||
str "github.com/nalgeon/redka/internal/command/string"
|
str "github.com/nalgeon/redka/internal/command/string"
|
||||||
"github.com/nalgeon/redka/internal/command/zset"
|
"github.com/nalgeon/redka/internal/command/zset"
|
||||||
"github.com/nalgeon/redka/internal/redis"
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
@@ -150,6 +151,38 @@ func Parse(args [][]byte) (redis.Cmd, error) {
|
|||||||
case "hvals":
|
case "hvals":
|
||||||
return hash.ParseHVals(b)
|
return hash.ParseHVals(b)
|
||||||
|
|
||||||
|
// set
|
||||||
|
case "sadd":
|
||||||
|
return set.ParseSAdd(b)
|
||||||
|
case "scard":
|
||||||
|
return set.ParseSCard(b)
|
||||||
|
case "sdiff":
|
||||||
|
return set.ParseSDiff(b)
|
||||||
|
case "sdiffstore":
|
||||||
|
return set.ParseSDiffStore(b)
|
||||||
|
case "sinter":
|
||||||
|
return set.ParseSInter(b)
|
||||||
|
case "sinterstore":
|
||||||
|
return set.ParseSInterStore(b)
|
||||||
|
case "sismember":
|
||||||
|
return set.ParseSIsMember(b)
|
||||||
|
case "smembers":
|
||||||
|
return set.ParseSMembers(b)
|
||||||
|
case "smove":
|
||||||
|
return set.ParseSMove(b)
|
||||||
|
case "spop":
|
||||||
|
return set.ParseSPop(b)
|
||||||
|
case "srandmember":
|
||||||
|
return set.ParseSRandMember(b)
|
||||||
|
case "srem":
|
||||||
|
return set.ParseSRem(b)
|
||||||
|
case "sscan":
|
||||||
|
return set.ParseSScan(b)
|
||||||
|
case "sunion":
|
||||||
|
return set.ParseSUnion(b)
|
||||||
|
case "sunionstore":
|
||||||
|
return set.ParseSUnionStore(b)
|
||||||
|
|
||||||
// sorted set
|
// sorted set
|
||||||
case "zadd":
|
case "zadd":
|
||||||
return zset.ParseZAdd(b)
|
return zset.ParseZAdd(b)
|
||||||
|
38
internal/command/set/sadd.go
Normal file
38
internal/command/set/sadd.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Adds one or more members to a set.
|
||||||
|
// Creates the key if it doesn't exist.
|
||||||
|
// SADD key member [member ...]
|
||||||
|
// https://redis.io/commands/sadd
|
||||||
|
type SAdd struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
members []any
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSAdd(b redis.BaseCmd) (*SAdd, error) {
|
||||||
|
cmd := &SAdd{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.key),
|
||||||
|
parser.Anys(&cmd.members),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SAdd) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
count, err := red.Set().Add(cmd.key, cmd.members...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(count)
|
||||||
|
return count, nil
|
||||||
|
}
|
130
internal/command/set/sadd_test.go
Normal file
130
internal/command/set/sadd_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSAddParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SAdd
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sadd",
|
||||||
|
want: SAdd{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sadd key",
|
||||||
|
want: SAdd{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sadd key one",
|
||||||
|
want: SAdd{key: "key", members: []any{"one"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sadd key one two",
|
||||||
|
want: SAdd{key: "key", members: []any{"one", "two"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sadd key one two thr",
|
||||||
|
want: SAdd{key: "key", members: []any{"one", "two", "thr"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSAdd, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
testx.AssertEqual(t, cmd.members, test.want.members)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSAddExec(t *testing.T) {
|
||||||
|
t.Run("create single", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSAdd, "sadd key one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("key")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("create multiple", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSAdd, "sadd key one two")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("key")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("create/update", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSAdd, "sadd key one two")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("key")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("update multiple", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSAdd, "sadd key one two")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("key")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "value")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSAdd, "sadd key one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertErr(t, err, core.ErrKeyType)
|
||||||
|
testx.AssertEqual(t, res, nil)
|
||||||
|
testx.AssertEqual(t, conn.Out(), core.ErrKeyType.Error()+" (sadd)")
|
||||||
|
})
|
||||||
|
}
|
30
internal/command/set/scard.go
Normal file
30
internal/command/set/scard.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import "github.com/nalgeon/redka/internal/redis"
|
||||||
|
|
||||||
|
// Returns the number of members in a set.
|
||||||
|
// SCARD key
|
||||||
|
// https://redis.io/commands/scard
|
||||||
|
type SCard struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSCard(b redis.BaseCmd) (*SCard, error) {
|
||||||
|
cmd := &SCard{BaseCmd: b}
|
||||||
|
if len(cmd.Args()) != 1 {
|
||||||
|
return cmd, redis.ErrInvalidArgNum
|
||||||
|
}
|
||||||
|
cmd.key = string(cmd.Args()[0])
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SCard) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
n, err := red.Set().Len(cmd.key)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
93
internal/command/set/scard_test.go
Normal file
93
internal/command/set/scard_test.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSCardParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SCard
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "scard",
|
||||||
|
want: SCard{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "scard key",
|
||||||
|
want: SCard{key: "key"},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "scard key one",
|
||||||
|
want: SCard{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSCard, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSCardExec(t *testing.T) {
|
||||||
|
t.Run("card", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSCard, "scard key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one")
|
||||||
|
_, _ = db.Set().Delete("key", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSCard, "scard key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSCard, "scard key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "value")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSCard, "scard key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
}
|
38
internal/command/set/sdiff.go
Normal file
38
internal/command/set/sdiff.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns the difference of multiple sets.
|
||||||
|
// SDIFF key [key ...]
|
||||||
|
// https://redis.io/commands/sdiff
|
||||||
|
type SDiff struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSDiff(b redis.BaseCmd) (*SDiff, error) {
|
||||||
|
cmd := &SDiff{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.Strings(&cmd.keys),
|
||||||
|
).Required(1).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SDiff) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
elems, err := red.Set().Diff(cmd.keys...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteArray(len(elems))
|
||||||
|
for _, elem := range elems {
|
||||||
|
w.WriteBulk(elem)
|
||||||
|
}
|
||||||
|
return elems, nil
|
||||||
|
}
|
148
internal/command/set/sdiff_test.go
Normal file
148
internal/command/set/sdiff_test.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSDiffParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SDiff
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sdiff",
|
||||||
|
want: SDiff{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sdiff key",
|
||||||
|
want: SDiff{keys: []string{"key"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sdiff k1 k2",
|
||||||
|
want: SDiff{keys: []string{"k1", "k2"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSDiff, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.keys, test.want.keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSDiffExec(t *testing.T) {
|
||||||
|
t.Run("non-empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr", "fiv")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "fou", "six")
|
||||||
|
_, _ = db.Set().Add("key3", "thr", "six")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,fiv,one")
|
||||||
|
})
|
||||||
|
t.Run("no keys", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("single key", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 3)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "3,one,thr,two")
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two")
|
||||||
|
_, _ = db.Set().Add("key2", "one", "fou")
|
||||||
|
_, _ = db.Set().Add("key3", "two", "fiv")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("first not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
_, _ = db.Set().Add("key3", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("rest not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1,one")
|
||||||
|
})
|
||||||
|
t.Run("all not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_ = db.Str().Set("key2", "two")
|
||||||
|
_, _ = db.Set().Add("key3", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiff, "sdiff key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1,one")
|
||||||
|
})
|
||||||
|
}
|
37
internal/command/set/sdiffstore.go
Normal file
37
internal/command/set/sdiffstore.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stores the difference of multiple sets in a key.
|
||||||
|
// SDIFFSTORE destination key [key ...]
|
||||||
|
// https://redis.io/commands/sdiffstore
|
||||||
|
type SDiffStore struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
dest string
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSDiffStore(b redis.BaseCmd) (*SDiffStore, error) {
|
||||||
|
cmd := &SDiffStore{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.dest),
|
||||||
|
parser.Strings(&cmd.keys),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SDiffStore) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
n, err := red.Set().DiffStore(cmd.dest, cmd.keys...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
188
internal/command/set/sdiffstore_test.go
Normal file
188
internal/command/set/sdiffstore_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSDiffStoreParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SDiffStore
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sdiffstore",
|
||||||
|
want: SDiffStore{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sdiffstore dest",
|
||||||
|
want: SDiffStore{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sdiffstore dest key",
|
||||||
|
want: SDiffStore{dest: "dest", keys: []string{"key"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sdiffstore dest k1 k2",
|
||||||
|
want: SDiffStore{dest: "dest", keys: []string{"k1", "k2"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSDiffStore, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.dest, test.want.dest)
|
||||||
|
testx.AssertEqual(t, cmd.keys, test.want.keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSDiffStoreExec(t *testing.T) {
|
||||||
|
t.Run("store", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr", "fiv")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "fou", "six")
|
||||||
|
_, _ = db.Set().Add("key3", "thr", "six")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("fiv"), core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("rewrite dest", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("single key", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value(nil))
|
||||||
|
})
|
||||||
|
t.Run("first not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
_, _ = db.Set().Add("key3", "thr")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value(nil))
|
||||||
|
})
|
||||||
|
t.Run("rest not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("source key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
_ = db.Str().Set("key3", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("dest key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
_ = db.Str().Set("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSDiffStore, "sdiffstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertErr(t, err, core.ErrKeyType)
|
||||||
|
testx.AssertEqual(t, res, nil)
|
||||||
|
testx.AssertEqual(t, conn.Out(), core.ErrKeyType.Error()+" (sdiffstore)")
|
||||||
|
|
||||||
|
sval, _ := db.Str().Get("dest")
|
||||||
|
testx.AssertEqual(t, sval, core.Value("old"))
|
||||||
|
})
|
||||||
|
}
|
26
internal/command/set/set_test.go
Normal file
26
internal/command/set/set_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka"
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDB(tb testing.TB) (*redka.DB, redis.Redka) {
|
||||||
|
tb.Helper()
|
||||||
|
db, err := redka.Open(":memory:", nil)
|
||||||
|
if err != nil {
|
||||||
|
tb.Fatal(err)
|
||||||
|
}
|
||||||
|
return db, redis.RedkaDB(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortValues(vals []core.Value) {
|
||||||
|
sort.Slice(vals, func(i, j int) bool {
|
||||||
|
return slices.Compare(vals[i], vals[j]) < 0
|
||||||
|
})
|
||||||
|
}
|
38
internal/command/set/sinter.go
Normal file
38
internal/command/set/sinter.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns the intersect of multiple sets.
|
||||||
|
// SINTER key [key ...]
|
||||||
|
// https://redis.io/commands/sinter
|
||||||
|
type SInter struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSInter(b redis.BaseCmd) (*SInter, error) {
|
||||||
|
cmd := &SInter{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.Strings(&cmd.keys),
|
||||||
|
).Required(1).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SInter) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
elems, err := red.Set().Inter(cmd.keys...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteArray(len(elems))
|
||||||
|
for _, elem := range elems {
|
||||||
|
w.WriteBulk(elem)
|
||||||
|
}
|
||||||
|
return elems, nil
|
||||||
|
}
|
135
internal/command/set/sinter_test.go
Normal file
135
internal/command/set/sinter_test.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSInterParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SInter
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sinter",
|
||||||
|
want: SInter{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sinter key",
|
||||||
|
want: SInter{keys: []string{"key"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sinter k1 k2",
|
||||||
|
want: SInter{keys: []string{"k1", "k2"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSInter, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.keys, test.want.keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSInterExec(t *testing.T) {
|
||||||
|
t.Run("non-empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "thr", "fou")
|
||||||
|
_, _ = db.Set().Add("key3", "one", "two", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,thr,two")
|
||||||
|
})
|
||||||
|
t.Run("no keys", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("single key", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 3)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "3,one,thr,two")
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "thr")
|
||||||
|
_, _ = db.Set().Add("key3", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("all not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_ = db.Str().Set("key2", "one")
|
||||||
|
_, _ = db.Set().Add("key3", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInter, "sinter key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
}
|
37
internal/command/set/sinterstore.go
Normal file
37
internal/command/set/sinterstore.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stores the intersect of multiple sets in a key.
|
||||||
|
// SINTERSTORE destination key [key ...]
|
||||||
|
// https://redis.io/commands/sinterstore
|
||||||
|
type SInterStore struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
dest string
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSInterStore(b redis.BaseCmd) (*SInterStore, error) {
|
||||||
|
cmd := &SInterStore{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.dest),
|
||||||
|
parser.Strings(&cmd.keys),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SInterStore) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
n, err := red.Set().InterStore(cmd.dest, cmd.keys...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
172
internal/command/set/sinterstore_test.go
Normal file
172
internal/command/set/sinterstore_test.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSInterStoreParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SInterStore
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sinterstore",
|
||||||
|
want: SInterStore{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sinterstore dest",
|
||||||
|
want: SInterStore{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sinterstore dest key",
|
||||||
|
want: SInterStore{dest: "dest", keys: []string{"key"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sinterstore dest k1 k2",
|
||||||
|
want: SInterStore{dest: "dest", keys: []string{"k1", "k2"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSInterStore, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.dest, test.want.dest)
|
||||||
|
testx.AssertEqual(t, cmd.keys, test.want.keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSInterStoreExec(t *testing.T) {
|
||||||
|
t.Run("store", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "thr", "fou")
|
||||||
|
_, _ = db.Set().Add("key3", "one", "two", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("thr"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("rewrite dest", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("single key", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value(nil))
|
||||||
|
})
|
||||||
|
t.Run("source key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value(nil))
|
||||||
|
})
|
||||||
|
t.Run("source key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_ = db.Str().Set("key3", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value(nil))
|
||||||
|
})
|
||||||
|
t.Run("dest key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_ = db.Str().Set("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSInterStore, "sinterstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertErr(t, err, core.ErrKeyType)
|
||||||
|
testx.AssertEqual(t, res, nil)
|
||||||
|
testx.AssertEqual(t, conn.Out(), core.ErrKeyType.Error()+" (sinterstore)")
|
||||||
|
|
||||||
|
sval, _ := db.Str().Get("dest")
|
||||||
|
testx.AssertEqual(t, sval, core.Value("old"))
|
||||||
|
})
|
||||||
|
}
|
41
internal/command/set/sismember.go
Normal file
41
internal/command/set/sismember.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Determines whether a member belongs to a set.
|
||||||
|
// SISMEMBER key member
|
||||||
|
// https://redis.io/commands/sismember
|
||||||
|
type SIsMember struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
member []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSIsMember(b redis.BaseCmd) (*SIsMember, error) {
|
||||||
|
cmd := &SIsMember{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.key),
|
||||||
|
parser.Bytes(&cmd.member),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SIsMember) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
ok, err := red.Set().Exists(cmd.key, cmd.member)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
w.WriteInt(1)
|
||||||
|
} else {
|
||||||
|
w.WriteInt(0)
|
||||||
|
}
|
||||||
|
return ok, nil
|
||||||
|
}
|
98
internal/command/set/sismember_test.go
Normal file
98
internal/command/set/sismember_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSIsMemberParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SIsMember
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sismember",
|
||||||
|
want: SIsMember{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sismember key",
|
||||||
|
want: SIsMember{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sismember key one",
|
||||||
|
want: SIsMember{key: "key", member: []byte("one")},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sismember key one two",
|
||||||
|
want: SIsMember{},
|
||||||
|
err: redis.ErrSyntaxError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSIsMember, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
testx.AssertEqual(t, cmd.member, test.want.member)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSIsMemberExec(t *testing.T) {
|
||||||
|
t.Run("elem found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSIsMember, "sismember key one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, true)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
})
|
||||||
|
t.Run("elem not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSIsMember, "sismember key two")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, false)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSIsMember, "sismember key one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, false)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSIsMember, "sismember key one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, false)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
}
|
33
internal/command/set/smembers.go
Normal file
33
internal/command/set/smembers.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import "github.com/nalgeon/redka/internal/redis"
|
||||||
|
|
||||||
|
// Returns all members of a set.
|
||||||
|
// SMEMBERS key
|
||||||
|
// https://redis.io/commands/smembers
|
||||||
|
type SMembers struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSMembers(b redis.BaseCmd) (*SMembers, error) {
|
||||||
|
cmd := &SMembers{BaseCmd: b}
|
||||||
|
if len(cmd.Args()) != 1 {
|
||||||
|
return cmd, redis.ErrInvalidArgNum
|
||||||
|
}
|
||||||
|
cmd.key = string(cmd.Args()[0])
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SMembers) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
items, err := red.Set().Items(cmd.key)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteArray(len(items))
|
||||||
|
for _, val := range items {
|
||||||
|
w.WriteBulk(val)
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
83
internal/command/set/smembers_test.go
Normal file
83
internal/command/set/smembers_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSMembersParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SMembers
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "smembers",
|
||||||
|
want: SMembers{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "smembers key",
|
||||||
|
want: SMembers{key: "key"},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "smembers key one",
|
||||||
|
want: SMembers{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSMembers, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSMembersExec(t *testing.T) {
|
||||||
|
t.Run("items", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMembers, "smembers key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, []core.Value{
|
||||||
|
core.Value("one"), core.Value("thr"), core.Value("two"),
|
||||||
|
})
|
||||||
|
testx.AssertEqual(t, conn.Out(), "3,one,thr,two")
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMembers, "smembers key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, []core.Value(nil))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "value")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMembers, "smembers key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, []core.Value(nil))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
}
|
44
internal/command/set/smove.go
Normal file
44
internal/command/set/smove.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Moves a member from one set to another.
|
||||||
|
// SMOVE source destination member
|
||||||
|
// https://redis.io/commands/smove
|
||||||
|
type SMove struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
src string
|
||||||
|
dest string
|
||||||
|
member []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSMove(b redis.BaseCmd) (*SMove, error) {
|
||||||
|
cmd := &SMove{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.src),
|
||||||
|
parser.String(&cmd.dest),
|
||||||
|
parser.Bytes(&cmd.member),
|
||||||
|
).Required(3).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SMove) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
err := red.Set().Move(cmd.src, cmd.dest, cmd.member)
|
||||||
|
if err == core.ErrNotFound {
|
||||||
|
w.WriteInt(0)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(1)
|
||||||
|
return 1, nil
|
||||||
|
}
|
159
internal/command/set/smove_test.go
Normal file
159
internal/command/set/smove_test.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSMoveParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SMove
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "smove",
|
||||||
|
want: SMove{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "smove src",
|
||||||
|
want: SMove{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "smove src dest",
|
||||||
|
want: SMove{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "smove src dest one",
|
||||||
|
want: SMove{src: "src", dest: "dest", member: []byte("one")},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSMove, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.src, test.want.src)
|
||||||
|
testx.AssertEqual(t, cmd.dest, test.want.dest)
|
||||||
|
testx.AssertEqual(t, cmd.member, test.want.member)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSMoveExec(t *testing.T) {
|
||||||
|
t.Run("move", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("src", "one", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMove, "smove src dest one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
sone, _ := db.Set().Exists("src", "one")
|
||||||
|
testx.AssertEqual(t, sone, false)
|
||||||
|
done, _ := db.Set().Exists("dest", "one")
|
||||||
|
testx.AssertEqual(t, done, true)
|
||||||
|
})
|
||||||
|
t.Run("dest not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("src", "one", "two")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMove, "smove src dest one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
sone, _ := db.Set().Exists("src", "one")
|
||||||
|
testx.AssertEqual(t, sone, false)
|
||||||
|
done, _ := db.Set().Exists("dest", "one")
|
||||||
|
testx.AssertEqual(t, done, true)
|
||||||
|
})
|
||||||
|
t.Run("src elem not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("src", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMove, "smove src dest one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
sone, _ := db.Set().Exists("src", "one")
|
||||||
|
testx.AssertEqual(t, sone, false)
|
||||||
|
done, _ := db.Set().Exists("dest", "one")
|
||||||
|
testx.AssertEqual(t, done, false)
|
||||||
|
})
|
||||||
|
t.Run("src key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("dest", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMove, "smove src dest one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
sone, _ := db.Set().Exists("src", "one")
|
||||||
|
testx.AssertEqual(t, sone, false)
|
||||||
|
done, _ := db.Set().Exists("dest", "one")
|
||||||
|
testx.AssertEqual(t, done, false)
|
||||||
|
})
|
||||||
|
t.Run("dest type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("src", "one", "two")
|
||||||
|
_ = db.Str().Set("dest", "str")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMove, "smove src dest one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertErr(t, err, core.ErrKeyType)
|
||||||
|
testx.AssertEqual(t, res, nil)
|
||||||
|
testx.AssertEqual(t, conn.Out(), core.ErrKeyType.Error()+" (smove)")
|
||||||
|
|
||||||
|
sone, _ := db.Set().Exists("src", "one")
|
||||||
|
testx.AssertEqual(t, sone, true)
|
||||||
|
done, _ := db.Set().Exists("dest", "one")
|
||||||
|
testx.AssertEqual(t, done, false)
|
||||||
|
})
|
||||||
|
t.Run("src type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("src", "one")
|
||||||
|
_, _ = db.Set().Add("dest", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSMove, "smove src dest one")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
sone, _ := db.Set().Exists("src", "one")
|
||||||
|
testx.AssertEqual(t, sone, false)
|
||||||
|
done, _ := db.Set().Exists("dest", "one")
|
||||||
|
testx.AssertEqual(t, done, false)
|
||||||
|
})
|
||||||
|
}
|
37
internal/command/set/spop.go
Normal file
37
internal/command/set/spop.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns a random member from a set after removing it.
|
||||||
|
// SPOP key
|
||||||
|
// https://redis.io/commands/spop
|
||||||
|
type SPop struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSPop(b redis.BaseCmd) (*SPop, error) {
|
||||||
|
cmd := &SPop{BaseCmd: b}
|
||||||
|
if len(cmd.Args()) != 1 {
|
||||||
|
return cmd, redis.ErrInvalidArgNum
|
||||||
|
}
|
||||||
|
cmd.key = string(cmd.Args()[0])
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SPop) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
elem, err := red.Set().Pop(cmd.key)
|
||||||
|
if err == core.ErrNotFound {
|
||||||
|
w.WriteNull()
|
||||||
|
return elem, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteBulk(elem)
|
||||||
|
return elem, nil
|
||||||
|
}
|
86
internal/command/set/spop_test.go
Normal file
86
internal/command/set/spop_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSPopParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SPop
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "spop",
|
||||||
|
want: SPop{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "spop key",
|
||||||
|
want: SPop{key: "key"},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "spop key 5",
|
||||||
|
want: SPop{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSPop, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSPopExec(t *testing.T) {
|
||||||
|
t.Run("pop", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSPop, "spop key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
s := res.(core.Value).String()
|
||||||
|
testx.AssertEqual(t, s == "one" || s == "two" || s == "thr", true)
|
||||||
|
s = conn.Out()
|
||||||
|
testx.AssertEqual(t, s == "one" || s == "two" || s == "thr", true)
|
||||||
|
|
||||||
|
slen, _ := db.Set().Len("key")
|
||||||
|
testx.AssertEqual(t, slen, 2)
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSPop, "spop key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, core.Value(nil))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "(nil)")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "value")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSPop, "spop key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, core.Value(nil))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "(nil)")
|
||||||
|
})
|
||||||
|
}
|
37
internal/command/set/srandmember.go
Normal file
37
internal/command/set/srandmember.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get a random member from a set.
|
||||||
|
// SRANDMEMBER key
|
||||||
|
// https://redis.io/commands/srandmember
|
||||||
|
type SRandMember struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSRandMember(b redis.BaseCmd) (*SRandMember, error) {
|
||||||
|
cmd := &SRandMember{BaseCmd: b}
|
||||||
|
if len(cmd.Args()) != 1 {
|
||||||
|
return cmd, redis.ErrInvalidArgNum
|
||||||
|
}
|
||||||
|
cmd.key = string(cmd.Args()[0])
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SRandMember) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
elem, err := red.Set().Random(cmd.key)
|
||||||
|
if err == core.ErrNotFound {
|
||||||
|
w.WriteNull()
|
||||||
|
return elem, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteBulk(elem)
|
||||||
|
return elem, nil
|
||||||
|
}
|
83
internal/command/set/srandmember_test.go
Normal file
83
internal/command/set/srandmember_test.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSRandMemberParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SRandMember
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "srandmember",
|
||||||
|
want: SRandMember{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "srandmember key",
|
||||||
|
want: SRandMember{key: "key"},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "srandmember key 5",
|
||||||
|
want: SRandMember{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSRandMember, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSRandMemberExec(t *testing.T) {
|
||||||
|
t.Run("random", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRandMember, "srandmember key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
s := res.(core.Value).String()
|
||||||
|
testx.AssertEqual(t, s == "one" || s == "two" || s == "thr", true)
|
||||||
|
s = conn.Out()
|
||||||
|
testx.AssertEqual(t, s == "one" || s == "two" || s == "thr", true)
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRandMember, "srandmember key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, core.Value(nil))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "(nil)")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "value")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRandMember, "srandmember key")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, core.Value(nil))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "(nil)")
|
||||||
|
})
|
||||||
|
}
|
37
internal/command/set/srem.go
Normal file
37
internal/command/set/srem.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Removes one or more members from a set.
|
||||||
|
// SREM key member [member ...]
|
||||||
|
// https://redis.io/commands/srem
|
||||||
|
type SRem struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
members []any
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSRem(b redis.BaseCmd) (*SRem, error) {
|
||||||
|
cmd := &SRem{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.key),
|
||||||
|
parser.Anys(&cmd.members),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SRem) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
count, err := red.Set().Delete(cmd.key, cmd.members...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(count)
|
||||||
|
return count, nil
|
||||||
|
}
|
110
internal/command/set/srem_test.go
Normal file
110
internal/command/set/srem_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSRemParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SRem
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "srem",
|
||||||
|
want: SRem{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "srem key",
|
||||||
|
want: SRem{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "srem key one",
|
||||||
|
want: SRem{key: "key", members: []any{"one"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "srem key one two",
|
||||||
|
want: SRem{key: "key", members: []any{"one", "two"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "srem key one two thr",
|
||||||
|
want: SRem{key: "key", members: []any{"one", "two", "thr"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSRem, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.want.key)
|
||||||
|
testx.AssertEqual(t, cmd.members, test.want.members)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSRemExec(t *testing.T) {
|
||||||
|
t.Run("some", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRem, "srem key one thr")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("key")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("none", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRem, "srem key fou fiv")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
slen, _ := db.Set().Len("key")
|
||||||
|
testx.AssertEqual(t, slen, 3)
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRem, "srem key one two")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_ = db.Str().Set("key", "str")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSRem, "srem key one two")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
}
|
54
internal/command/set/sscan.go
Normal file
54
internal/command/set/sscan.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Iterates over members of a set.
|
||||||
|
// SSCAN key cursor [MATCH pattern] [COUNT count]
|
||||||
|
// https://redis.io/commands/sscan
|
||||||
|
type SScan struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
key string
|
||||||
|
cursor int
|
||||||
|
match string
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSScan(b redis.BaseCmd) (*SScan, error) {
|
||||||
|
cmd := &SScan{BaseCmd: b}
|
||||||
|
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.key),
|
||||||
|
parser.Int(&cmd.cursor),
|
||||||
|
parser.Named("match", parser.String(&cmd.match)),
|
||||||
|
parser.Named("count", parser.Int(&cmd.count)),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return cmd, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// all elements by default
|
||||||
|
if cmd.match == "" {
|
||||||
|
cmd.match = "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SScan) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
res, err := red.Set().Scan(cmd.key, cmd.cursor, cmd.match, cmd.count)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteArray(2)
|
||||||
|
w.WriteInt(res.Cursor)
|
||||||
|
w.WriteArray(len(res.Items))
|
||||||
|
for _, val := range res.Items {
|
||||||
|
w.WriteBulk(val)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
223
internal/command/set/sscan_test.go
Normal file
223
internal/command/set/sscan_test.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/rset"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSScanParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
key string
|
||||||
|
cursor int
|
||||||
|
match string
|
||||||
|
count int
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sscan",
|
||||||
|
key: "",
|
||||||
|
cursor: 0,
|
||||||
|
match: "*",
|
||||||
|
count: 0,
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key",
|
||||||
|
key: "",
|
||||||
|
cursor: 0,
|
||||||
|
match: "*",
|
||||||
|
count: 0,
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15",
|
||||||
|
key: "key",
|
||||||
|
cursor: 15,
|
||||||
|
match: "*",
|
||||||
|
count: 0,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15 match *",
|
||||||
|
key: "key",
|
||||||
|
cursor: 15,
|
||||||
|
match: "*",
|
||||||
|
count: 0,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15 match * count 5",
|
||||||
|
key: "key",
|
||||||
|
cursor: 15,
|
||||||
|
match: "*",
|
||||||
|
count: 5,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15 count 5 match *",
|
||||||
|
key: "key",
|
||||||
|
cursor: 15,
|
||||||
|
match: "*",
|
||||||
|
count: 5,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15 match m2* count 5",
|
||||||
|
key: "key",
|
||||||
|
cursor: 15,
|
||||||
|
match: "m2*",
|
||||||
|
count: 5,
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key ten",
|
||||||
|
key: "",
|
||||||
|
cursor: 0,
|
||||||
|
match: "",
|
||||||
|
count: 0,
|
||||||
|
err: redis.ErrInvalidInt,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15 *",
|
||||||
|
key: "",
|
||||||
|
cursor: 0,
|
||||||
|
match: "",
|
||||||
|
count: 0,
|
||||||
|
err: redis.ErrSyntaxError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sscan key 15 * 5",
|
||||||
|
key: "",
|
||||||
|
cursor: 0,
|
||||||
|
match: "",
|
||||||
|
count: 0,
|
||||||
|
err: redis.ErrSyntaxError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSScan, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.key, test.key)
|
||||||
|
testx.AssertEqual(t, cmd.cursor, test.cursor)
|
||||||
|
testx.AssertEqual(t, cmd.match, test.match)
|
||||||
|
testx.AssertEqual(t, cmd.count, test.count)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSScanExec(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key", "m11", "m12", "m21", "m22", "m31")
|
||||||
|
|
||||||
|
t.Run("sscan all", func(t *testing.T) {
|
||||||
|
{
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 0")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 5)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 5)
|
||||||
|
testx.AssertEqual(t, sres.Items[0], core.Value("m11"))
|
||||||
|
testx.AssertEqual(t, sres.Items[4], core.Value("m31"))
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,5,5,m11,m12,m21,m22,m31")
|
||||||
|
}
|
||||||
|
{
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 5")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 0)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,0,0")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("sscan pattern", func(t *testing.T) {
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 0 match m2*")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 4)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 2)
|
||||||
|
testx.AssertEqual(t, sres.Items[0].String(), "m21")
|
||||||
|
testx.AssertEqual(t, sres.Items[1].String(), "m22")
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,4,2,m21,m22")
|
||||||
|
})
|
||||||
|
t.Run("sscan count", func(t *testing.T) {
|
||||||
|
{
|
||||||
|
// page 1
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 0 match * count 2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 2)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 2)
|
||||||
|
testx.AssertEqual(t, sres.Items[0].String(), "m11")
|
||||||
|
testx.AssertEqual(t, sres.Items[1].String(), "m12")
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,2,2,m11,m12")
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// page 2
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 2 match * count 2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 4)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 2)
|
||||||
|
testx.AssertEqual(t, sres.Items[0].String(), "m21")
|
||||||
|
testx.AssertEqual(t, sres.Items[1].String(), "m22")
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,4,2,m21,m22")
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// page 3
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 4 match * count 2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 5)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 1)
|
||||||
|
testx.AssertEqual(t, sres.Items[0].String(), "m31")
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,5,1,m31")
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// no more pages
|
||||||
|
cmd := redis.MustParse(ParseSScan, "sscan key 5 match * count 2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
|
||||||
|
sres := res.(rset.ScanResult)
|
||||||
|
testx.AssertEqual(t, sres.Cursor, 0)
|
||||||
|
testx.AssertEqual(t, len(sres.Items), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,0,0")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
38
internal/command/set/sunion.go
Normal file
38
internal/command/set/sunion.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns the union of multiple sets.
|
||||||
|
// SUNION key [key ...]
|
||||||
|
// https://redis.io/commands/sunion
|
||||||
|
type SUnion struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSUnion(b redis.BaseCmd) (*SUnion, error) {
|
||||||
|
cmd := &SUnion{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.Strings(&cmd.keys),
|
||||||
|
).Required(1).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SUnion) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
elems, err := red.Set().Union(cmd.keys...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteArray(len(elems))
|
||||||
|
for _, elem := range elems {
|
||||||
|
w.WriteBulk(elem)
|
||||||
|
}
|
||||||
|
return elems, nil
|
||||||
|
}
|
121
internal/command/set/sunion_test.go
Normal file
121
internal/command/set/sunion_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSUnionParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SUnion
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sunion",
|
||||||
|
want: SUnion{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sunion key",
|
||||||
|
want: SUnion{keys: []string{"key"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sunion k1 k2",
|
||||||
|
want: SUnion{keys: []string{"k1", "k2"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSUnion, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.keys, test.want.keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSUnionExec(t *testing.T) {
|
||||||
|
t.Run("non-empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "thr")
|
||||||
|
_, _ = db.Set().Add("key3", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnion, "sunion key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 4)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "4,fou,one,thr,two")
|
||||||
|
})
|
||||||
|
t.Run("no keys", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnion, "sunion key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("single key", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnion, "sunion key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 3)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "3,one,thr,two")
|
||||||
|
})
|
||||||
|
t.Run("key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "two")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnion, "sunion key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,one,two")
|
||||||
|
})
|
||||||
|
t.Run("all not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnion, "sunion key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
})
|
||||||
|
t.Run("key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_ = db.Str().Set("key2", "two")
|
||||||
|
_, _ = db.Set().Add("key3", "thr")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnion, "sunion key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, len(res.([]core.Value)), 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2,one,thr")
|
||||||
|
})
|
||||||
|
}
|
37
internal/command/set/sunionstore.go
Normal file
37
internal/command/set/sunionstore.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/nalgeon/redka/internal/parser"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stores the union of multiple sets in a key.
|
||||||
|
// SUNIONSTORE destination key [key ...]
|
||||||
|
// https://redis.io/commands/sunionstore
|
||||||
|
type SUnionStore struct {
|
||||||
|
redis.BaseCmd
|
||||||
|
dest string
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseSUnionStore(b redis.BaseCmd) (*SUnionStore, error) {
|
||||||
|
cmd := &SUnionStore{BaseCmd: b}
|
||||||
|
err := parser.New(
|
||||||
|
parser.String(&cmd.dest),
|
||||||
|
parser.Strings(&cmd.keys),
|
||||||
|
).Required(2).Run(cmd.Args())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *SUnionStore) Run(w redis.Writer, red redis.Redka) (any, error) {
|
||||||
|
n, err := red.Set().UnionStore(cmd.dest, cmd.keys...)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteError(cmd.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
w.WriteInt(n)
|
||||||
|
return n, nil
|
||||||
|
}
|
172
internal/command/set/sunionstore_test.go
Normal file
172
internal/command/set/sunionstore_test.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package set
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/nalgeon/redka/internal/core"
|
||||||
|
"github.com/nalgeon/redka/internal/redis"
|
||||||
|
"github.com/nalgeon/redka/internal/testx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSUnionStoreParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cmd string
|
||||||
|
want SUnionStore
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cmd: "sunionstore",
|
||||||
|
want: SUnionStore{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sunionstore dest",
|
||||||
|
want: SUnionStore{},
|
||||||
|
err: redis.ErrInvalidArgNum,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sunionstore dest key",
|
||||||
|
want: SUnionStore{dest: "dest", keys: []string{"key"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cmd: "sunionstore dest k1 k2",
|
||||||
|
want: SUnionStore{dest: "dest", keys: []string{"k1", "k2"}},
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cmd, func(t *testing.T) {
|
||||||
|
cmd, err := redis.Parse(ParseSUnionStore, test.cmd)
|
||||||
|
testx.AssertEqual(t, err, test.err)
|
||||||
|
if err == nil {
|
||||||
|
testx.AssertEqual(t, cmd.dest, test.want.dest)
|
||||||
|
testx.AssertEqual(t, cmd.keys, test.want.keys)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSUnionStoreExec(t *testing.T) {
|
||||||
|
t.Run("store", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two", "thr")
|
||||||
|
_, _ = db.Set().Add("key2", "two", "thr", "fou")
|
||||||
|
_, _ = db.Set().Add("key3", "one", "two", "thr", "fou")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 4)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "4")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{
|
||||||
|
core.Value("fou"), core.Value("one"), core.Value("thr"), core.Value("two"),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("rewrite dest", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("single key", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one", "two")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 2)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "2")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
sortValues(items)
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one"), core.Value("two")})
|
||||||
|
})
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 0)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "0")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value(nil))
|
||||||
|
})
|
||||||
|
t.Run("source key not found", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_, _ = db.Set().Add("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("source key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_ = db.Str().Set("key3", "one")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1 key2 key3")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertNoErr(t, err)
|
||||||
|
testx.AssertEqual(t, res, 1)
|
||||||
|
testx.AssertEqual(t, conn.Out(), "1")
|
||||||
|
|
||||||
|
items, _ := db.Set().Items("dest")
|
||||||
|
testx.AssertEqual(t, items, []core.Value{core.Value("one")})
|
||||||
|
})
|
||||||
|
t.Run("dest key type mismatch", func(t *testing.T) {
|
||||||
|
db, red := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = db.Set().Add("key1", "one")
|
||||||
|
_, _ = db.Set().Add("key2", "one")
|
||||||
|
_ = db.Str().Set("dest", "old")
|
||||||
|
|
||||||
|
cmd := redis.MustParse(ParseSUnionStore, "sunionstore dest key1 key2")
|
||||||
|
conn := redis.NewFakeConn()
|
||||||
|
res, err := cmd.Run(conn, red)
|
||||||
|
testx.AssertErr(t, err, core.ErrKeyType)
|
||||||
|
testx.AssertEqual(t, res, nil)
|
||||||
|
testx.AssertEqual(t, conn.Out(), core.ErrKeyType.Error()+" (sunionstore)")
|
||||||
|
|
||||||
|
sval, _ := db.Str().Get("dest")
|
||||||
|
testx.AssertEqual(t, sval, core.Value("old"))
|
||||||
|
})
|
||||||
|
}
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/nalgeon/redka/internal/core"
|
"github.com/nalgeon/redka/internal/core"
|
||||||
"github.com/nalgeon/redka/internal/rhash"
|
"github.com/nalgeon/redka/internal/rhash"
|
||||||
"github.com/nalgeon/redka/internal/rkey"
|
"github.com/nalgeon/redka/internal/rkey"
|
||||||
|
"github.com/nalgeon/redka/internal/rset"
|
||||||
"github.com/nalgeon/redka/internal/rstring"
|
"github.com/nalgeon/redka/internal/rstring"
|
||||||
"github.com/nalgeon/redka/internal/rzset"
|
"github.com/nalgeon/redka/internal/rzset"
|
||||||
)
|
)
|
||||||
@@ -68,6 +69,26 @@ type RList interface {
|
|||||||
Trim(key string, start, stop int) (int, error)
|
Trim(key string, start, stop int) (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RSet is a set repository.
|
||||||
|
type RSet interface {
|
||||||
|
Add(key string, elems ...any) (int, error)
|
||||||
|
Delete(key string, elems ...any) (int, error)
|
||||||
|
Diff(keys ...string) ([]core.Value, error)
|
||||||
|
DiffStore(dest string, keys ...string) (int, error)
|
||||||
|
Exists(key, elem any) (bool, error)
|
||||||
|
Inter(keys ...string) ([]core.Value, error)
|
||||||
|
InterStore(dest string, keys ...string) (int, error)
|
||||||
|
Items(key string) ([]core.Value, error)
|
||||||
|
Len(key string) (int, error)
|
||||||
|
Move(src, dest string, elem any) error
|
||||||
|
Pop(key string) (core.Value, error)
|
||||||
|
Random(key string) (core.Value, error)
|
||||||
|
Scan(key string, cursor int, pattern string, count int) (rset.ScanResult, error)
|
||||||
|
Scanner(key, pattern string, pageSize int) *rset.Scanner
|
||||||
|
Union(keys ...string) ([]core.Value, error)
|
||||||
|
UnionStore(dest string, keys ...string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
// RStr is a string repository.
|
// RStr is a string repository.
|
||||||
type RStr interface {
|
type RStr interface {
|
||||||
Get(key string) (core.Value, error)
|
Get(key string) (core.Value, error)
|
||||||
@@ -108,6 +129,7 @@ type Redka struct {
|
|||||||
hash RHash
|
hash RHash
|
||||||
key RKey
|
key RKey
|
||||||
list RList
|
list RList
|
||||||
|
set RSet
|
||||||
str RStr
|
str RStr
|
||||||
zset RZSet
|
zset RZSet
|
||||||
}
|
}
|
||||||
@@ -118,6 +140,7 @@ func RedkaDB(db *redka.DB) Redka {
|
|||||||
hash: db.Hash(),
|
hash: db.Hash(),
|
||||||
key: db.Key(),
|
key: db.Key(),
|
||||||
list: db.List(),
|
list: db.List(),
|
||||||
|
set: db.Set(),
|
||||||
str: db.Str(),
|
str: db.Str(),
|
||||||
zset: db.ZSet(),
|
zset: db.ZSet(),
|
||||||
}
|
}
|
||||||
@@ -129,6 +152,7 @@ func RedkaTx(tx *redka.Tx) Redka {
|
|||||||
hash: tx.Hash(),
|
hash: tx.Hash(),
|
||||||
key: tx.Key(),
|
key: tx.Key(),
|
||||||
list: tx.List(),
|
list: tx.List(),
|
||||||
|
set: tx.Set(),
|
||||||
str: tx.Str(),
|
str: tx.Str(),
|
||||||
zset: tx.ZSet(),
|
zset: tx.ZSet(),
|
||||||
}
|
}
|
||||||
@@ -144,10 +168,16 @@ func (r Redka) Key() RKey {
|
|||||||
return r.key
|
return r.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the list repository.
|
||||||
func (r Redka) List() RList {
|
func (r Redka) List() RList {
|
||||||
return r.list
|
return r.list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set returns the set repository.
|
||||||
|
func (r Redka) Set() RSet {
|
||||||
|
return r.set
|
||||||
|
}
|
||||||
|
|
||||||
// Str returns the string repository.
|
// Str returns the string repository.
|
||||||
func (r Redka) Str() RStr {
|
func (r Redka) Str() RStr {
|
||||||
return r.str
|
return r.str
|
||||||
|
@@ -175,8 +175,13 @@ func TestDiff(t *testing.T) {
|
|||||||
_, _ = set.Add("key1", "one", "two", "thr")
|
_, _ = set.Add("key1", "one", "two", "thr")
|
||||||
|
|
||||||
items, err := set.Diff("key1")
|
items, err := set.Diff("key1")
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return slices.Compare(items[i], items[j]) < 0
|
||||||
|
})
|
||||||
testx.AssertNoErr(t, err)
|
testx.AssertNoErr(t, err)
|
||||||
testx.AssertEqual(t, items, []core.Value(nil))
|
testx.AssertEqual(t, items, []core.Value{
|
||||||
|
core.Value("one"), core.Value("thr"), core.Value("two"),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
t.Run("empty", func(t *testing.T) {
|
t.Run("empty", func(t *testing.T) {
|
||||||
db, set := getDB(t)
|
db, set := getDB(t)
|
||||||
@@ -788,7 +793,21 @@ func TestMove(t *testing.T) {
|
|||||||
done, _ := set.Exists("dest", "one")
|
done, _ := set.Exists("dest", "one")
|
||||||
testx.AssertEqual(t, done, true)
|
testx.AssertEqual(t, done, true)
|
||||||
})
|
})
|
||||||
t.Run("src not found", func(t *testing.T) {
|
t.Run("src elem not found", func(t *testing.T) {
|
||||||
|
db, set := getDB(t)
|
||||||
|
defer db.Close()
|
||||||
|
_, _ = set.Add("src", "two")
|
||||||
|
_, _ = set.Add("dest", "thr", "fou")
|
||||||
|
|
||||||
|
err := set.Move("src", "dest", "one")
|
||||||
|
testx.AssertErr(t, err, core.ErrNotFound)
|
||||||
|
|
||||||
|
dkey, _ := db.Key().Get("dest")
|
||||||
|
testx.AssertEqual(t, dkey.Version, 1)
|
||||||
|
dlen, _ := set.Len("dest")
|
||||||
|
testx.AssertEqual(t, dlen, 2)
|
||||||
|
})
|
||||||
|
t.Run("src key not found", func(t *testing.T) {
|
||||||
db, set := getDB(t)
|
db, set := getDB(t)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
_, _ = set.Add("dest", "thr", "fou")
|
_, _ = set.Add("dest", "thr", "fou")
|
||||||
|
@@ -245,7 +245,7 @@ func (tx *Tx) Delete(key string, elems ...any) (int, error) {
|
|||||||
// If the first key does not exist or is not a set, returns an empty slice.
|
// If the first key does not exist or is not a set, returns an empty slice.
|
||||||
// If any of the remaining keys do not exist or are not sets, ignores them.
|
// If any of the remaining keys do not exist or are not sets, ignores them.
|
||||||
func (tx *Tx) Diff(keys ...string) ([]core.Value, error) {
|
func (tx *Tx) Diff(keys ...string) ([]core.Value, error) {
|
||||||
if len(keys) < 2 {
|
if len(keys) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
others := keys[1:]
|
others := keys[1:]
|
||||||
|
Reference in New Issue
Block a user