Skip to content

Commit

Permalink
support SINTERCARD (#352)
Browse files Browse the repository at this point in the history
* support SINTERCARD
  • Loading branch information
s-barr-fetch committed Feb 18, 2024
1 parent 3a21035 commit 2537d3d
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ Implemented commands:
- SDIFFSTORE
- SINTER
- SINTERSTORE
- SINTERCARD
- SISMEMBER
- SMEMBERS
- SMISMEMBER
Expand Down
72 changes: 72 additions & 0 deletions cmd_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func commandsSet(m *Miniredis) {
m.srv.Register("SCARD", m.cmdScard)
m.srv.Register("SDIFF", m.cmdSdiff)
m.srv.Register("SDIFFSTORE", m.cmdSdiffstore)
m.srv.Register("SINTERCARD", m.cmdSintercard)
m.srv.Register("SINTER", m.cmdSinter)
m.srv.Register("SINTERSTORE", m.cmdSinterstore)
m.srv.Register("SISMEMBER", m.cmdSismember)
Expand Down Expand Up @@ -219,6 +220,77 @@ func (m *Miniredis) cmdSinterstore(c *server.Peer, cmd string, args []string) {
})
}

// SINTERCARD
func (m *Miniredis) cmdSintercard(c *server.Peer, cmd string, args []string) {
if len(args) < 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
if m.checkPubsub(c, cmd) {
return
}

opts := struct {
keys []string
limit int
}{}

numKeys, err := strconv.Atoi(args[0])
if err != nil {
setDirty(c)
c.WriteError("ERR numkeys should be greater than 0")
return
}
if numKeys < 1 {
setDirty(c)
c.WriteError("ERR numkeys should be greater than 0")
return
}

args = args[1:]
if len(args) < numKeys {
setDirty(c)
c.WriteError("ERR Number of keys can't be greater than number of args")
return
}
opts.keys = args[:numKeys]

args = args[numKeys:]
if len(args) == 2 && strings.ToLower(args[0]) == "limit" {
l, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
if l < 0 {
setDirty(c)
c.WriteError(msgLimitIsNegative)
return
}
opts.limit = l
} else if len(args) > 0 {
setDirty(c)
c.WriteError(msgSyntaxError)
return
}

withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)

count, err := db.setIntercard(opts.keys, opts.limit)
if err != nil {
c.WriteError(err.Error())
return
}
c.WriteInt(count)
})
}

// SISMEMBER
func (m *Miniredis) cmdSismember(c *server.Peer, cmd string, args []string) {
if len(args) != 2 {
Expand Down
114 changes: 114 additions & 0 deletions cmd_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,120 @@ func TestSinterstore(t *testing.T) {
})
}

// Test SINTERCARD
func TestSintercard(t *testing.T) {
s, err := Run()
ok(t, err)
defer s.Close()
c, err := proto.Dial(s.Addr())
ok(t, err)
defer c.Close()

_, _ = s.SetAdd("s1", "a", "b", "c")
_, _ = s.SetAdd("s2", "b", "c", "d")
_, _ = s.SetAdd("s3", "c", "d", "e")

t.Run("no limit set", func(t *testing.T) {
mustDo(t, c,
"SINTERCARD", "2", "s1", "s2",
proto.Int(2),
)
})

t.Run("limit greater than result", func(t *testing.T) {
mustDo(t, c,
"SINTERCARD", "2", "s1", "s2", "LIMIT", "15",
proto.Int(2),
)
})

t.Run("limit of 0", func(t *testing.T) {
mustDo(t, c,
"SINTERCARD", "2", "s1", "s2", "LIMIT", "0",
proto.Int(2),
)
})

t.Run("limit of 1", func(t *testing.T) {
mustDo(t, c,
"SINTERCARD", "2", "s1", "s2", "LIMIT", "1",
proto.Int(1),
)
})

t.Run("multi intersection", func(t *testing.T) {
mustDo(t, c,
"SINTERCARD", "3", "s1", "s2", "s3",
proto.Int(1),
)
})

t.Run("nonexisting key", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "2", "s1", "NOT_A_KEY",
proto.Int(0),
)
})

t.Run("bad numKeys", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "two", "key1", "key2",
proto.Error("ERR numkeys should be greater than 0"),
)
})

t.Run("too many keys limit", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "2", "key1", "key2", "key3", "LIMIT", "3",
proto.Error(msgSyntaxError),
)
})

t.Run("too many keys no limit", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "2", "key1", "key2", "key3",
proto.Error(msgSyntaxError),
)
})

t.Run("too few keys limit", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "4", "key1", "key2", "key3", "LIMIT", "3",
proto.Error(msgSyntaxError),
)
})

t.Run("too few keys no limit", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "4", "key1", "key2", "key3",
proto.Error("ERR Number of keys can't be greater than number of args"),
)
})

t.Run("bad limit", func(t *testing.T) {
mustDo(
t, c,
"SINTERCARD", "2", "key1", "key2", "LIMIT", "five",
proto.Error(msgInvalidInt),
)
})

t.Run("wrong type", func(t *testing.T) {
_ = s.Set("str", "value")

mustDo(t, c,
"SINTERCARD", "1", "str",
proto.Error(msgWrongType),
)
})
}

// Test SUNION
func TestSunion(t *testing.T) {
s, err := Run()
Expand Down
48 changes: 48 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,54 @@ func (db *RedisDB) setInter(keys []string) (setKey, error) {
return s, nil
}

// setIntercard implements the logic behind SINTER*
// len keys needs to be > 0
func (db *RedisDB) setIntercard(keys []string, limit int) (int, error) {
// all keys must either not exist, or be of type "set".
allExist := true
for _, key := range keys {
exists := db.exists(key)
allExist = allExist && exists
if exists && db.t(key) != "set" {
return 0, ErrWrongType
}
}

if !allExist {
return 0, nil
}

smallestKey := keys[0]
smallestIdx := 0
for i, key := range keys {
if len(db.setKeys[key]) < len(db.setKeys[smallestKey]) {
smallestKey = key
smallestIdx = i
}
}
keys[smallestIdx] = keys[len(keys)-1]
keys = keys[:len(keys)-1]

count := 0
for item := range db.setKeys[smallestKey] {
inIntersection := true
for _, key := range keys {
if _, ok := db.setKeys[key][item]; !ok {
inIntersection = false
break
}
}
if inIntersection {
count++
if count == limit {
break
}
}
}

return count, nil
}

// setUnion implements the logic behind SUNION*
func (db *RedisDB) setUnion(keys []string) (setKey, error) {
key := keys[0]
Expand Down
40 changes: 40 additions & 0 deletions integration/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,46 @@ func TestSetSinter(t *testing.T) {
})
}

func TestSetSintercard(t *testing.T) {
skip(t)
testRaw(t, func(c *client) {
c.Do("SADD", "s1", "aap", "noot", "mies")
c.Do("SADD", "s2", "noot", "mies", "vuur")
c.Do("SADD", "s3", "mies", "wim")
c.Do("SINTERCARD", "1", "s1")
c.Do("SINTERCARD", "2", "s1", "s2")
c.Do("SINTERCARD", "3", "s1", "s2", "s3")
c.Do("SINTERCARD", "1", "nosuch")
c.Do("SINTERCARD", "5", "s1", "nosuch", "s2", "nosuch", "s3")
c.Do("SINTERCARD", "2", "s1", "s1")

c.Do("SINTERCARD", "1", "s1", "LIMIT", "1")
c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "0")
c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "1")
c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "2")
c.Do("SINTERCARD", "2", "s1", "s2", "LIMIT", "3")

// failure cases
c.Error("wrong number", "SINTERCARD")
c.Error("wrong number", "SINTERCARD", "0")
c.Error("wrong number", "SINTERCARD", "")
c.Error("wrong number", "SINTERCARD", "s1")
c.Error("greater than 0", "SINTERCARD", "s1", "s2")
c.Error("greater than 0", "SINTERCARD", "-2", "s1", "s2")
c.Error("greater than 0", "SINTERCARD", "-1", "s1", "s2")
c.Error("greater than 0", "SINTERCARD", "0", "s1", "s2")
c.Error("syntax error", "SINTERCARD", "1", "s1", "s2")
c.Error("can't be greater", "SINTERCARD", "3", "s1", "s2")
// Wrong type
c.Do("SET", "str", "I am a string")
c.Error("wrong kind", "SINTERCARD", "2", "s1", "str")
c.Error("wrong kind", "SINTERCARD", "2", "nosuch", "str")
c.Error("wrong kind", "SINTERCARD", "2", "str", "nosuch")
c.Error("wrong kind", "SINTERCARD", "2", "str", "s1")
c.Error("can't be negative", "SINTERCARD", "2", "s1", "s2", "LIMIT", "-1")
})
}

func TestSetSunion(t *testing.T) {
skip(t)
testRaw(t, func(c *client) {
Expand Down
1 change: 1 addition & 0 deletions redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const (
msgRankIsZero = "ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list"
msgCountIsNegative = "ERR COUNT can't be negative"
msgMaxLengthIsNegative = "ERR MAXLEN can't be negative"
msgLimitIsNegative = "ERR LIMIT can't be negative"
msgMemorySubcommand = "ERR unknown subcommand '%s'. Try MEMORY HELP."
)

Expand Down

0 comments on commit 2537d3d

Please sign in to comment.