feat: command - set

This commit is contained in:
Anton
2024-05-18 18:50:55 +05:00
parent 90fd5e9c7f
commit 470ed7bcd3
35 changed files with 2688 additions and 3 deletions

View File

@@ -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)

View 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
}

View 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)")
})
}

View 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
}

View 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")
})
}

View 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
}

View 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")
})
}

View 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
}

View 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"))
})
}

View 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
})
}

View 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
}

View 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")
})
}

View 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
}

View 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"))
})
}

View 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
}

View 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")
})
}

View 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
}

View 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")
})
}

View 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
}

View 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)
})
}

View 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
}

View 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)")
})
}

View 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
}

View 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)")
})
}

View 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
}

View 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")
})
}

View 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
}

View 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")
}
})
}

View 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
}

View 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")
})
}

View 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
}

View 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"))
})
}

View File

@@ -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

View File

@@ -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")

View File

@@ -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:]