// Commands from http://redis.io/commands#list
package miniredis
import (
"strconv"
"strings"
"time"
"github.com/alicebob/miniredis/server"
)
type leftright int
const (
left leftright = iota
right
)
// commandsList handles list commands (mostly L*)
func commandsList(m *Miniredis) {
m.srv.Register("BLPOP", m.cmdBlpop)
m.srv.Register("BRPOP", m.cmdBrpop)
m.srv.Register("BRPOPLPUSH", m.cmdBrpoplpush)
m.srv.Register("LINDEX", m.cmdLindex)
m.srv.Register("LINSERT", m.cmdLinsert)
m.srv.Register("LLEN", m.cmdLlen)
m.srv.Register("LPOP", m.cmdLpop)
m.srv.Register("LPUSH", m.cmdLpush)
m.srv.Register("LPUSHX", m.cmdLpushx)
m.srv.Register("LRANGE", m.cmdLrange)
m.srv.Register("LREM", m.cmdLrem)
m.srv.Register("LSET", m.cmdLset)
m.srv.Register("LTRIM", m.cmdLtrim)
m.srv.Register("RPOP", m.cmdRpop)
m.srv.Register("RPOPLPUSH", m.cmdRpoplpush)
m.srv.Register("RPUSH", m.cmdRpush)
m.srv.Register("RPUSHX", m.cmdRpushx)
}
// BLPOP
func (m *Miniredis) cmdBlpop(c *server.Peer, cmd string, args []string) {
m.cmdBXpop(c, cmd, args, left)
}
// BRPOP
func (m *Miniredis) cmdBrpop(c *server.Peer, cmd string, args []string) {
m.cmdBXpop(c, cmd, args, right)
}
func (m *Miniredis) cmdBXpop(c *server.Peer, cmd string, args []string, lr leftright) {
if len(args) < 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
timeoutS := args[len(args)-1]
keys := args[:len(args)-1]
timeout, err := strconv.Atoi(timeoutS)
if err != nil {
setDirty(c)
c.WriteError(msgInvalidTimeout)
return
}
if timeout < 0 {
setDirty(c)
c.WriteError(msgNegTimeout)
return
}
blocking(
m,
c,
time.Duration(timeout)*time.Second,
func(c *server.Peer, ctx *connCtx) bool {
db := m.db(ctx.selectedDB)
for _, key := range keys {
if !db.exists(key) {
continue
}
if db.t(key) != "list" {
c.WriteError(msgWrongType)
return true
}
if len(db.listKeys[key]) == 0 {
continue
}
c.WriteLen(2)
c.WriteBulk(key)
var v string
switch lr {
case left:
v = db.listLpop(key)
case right:
v = db.listPop(key)
}
c.WriteBulk(v)
return true
}
return false
},
func(c *server.Peer) {
// timeout
c.WriteNull()
},
)
}
// LINDEX
func (m *Miniredis) cmdLindex(c *server.Peer, cmd string, args []string) {
if len(args) != 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key, offsets := args[0], args[1]
offset, err := strconv.Atoi(offsets)
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
t, ok := db.keys[key]
if !ok {
// No such key
c.WriteNull()
return
}
if t != "list" {
c.WriteError(msgWrongType)
return
}
l := db.listKeys[key]
if offset < 0 {
offset = len(l) + offset
}
if offset < 0 || offset > len(l)-1 {
c.WriteNull()
return
}
c.WriteBulk(l[offset])
})
}
// LINSERT
func (m *Miniredis) cmdLinsert(c *server.Peer, cmd string, args []string) {
if len(args) != 4 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
where := 0
switch strings.ToLower(args[1]) {
case "before":
where = -1
case "after":
where = +1
default:
setDirty(c)
c.WriteError(msgSyntaxError)
return
}
pivot := args[2]
value := args[3]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
t, ok := db.keys[key]
if !ok {
// No such key
c.WriteInt(0)
return
}
if t != "list" {
c.WriteError(msgWrongType)
return
}
l := db.listKeys[key]
for i, el := range l {
if el != pivot {
continue
}
if where < 0 {
l = append(l[:i], append(listKey{value}, l[i:]...)...)
} else {
if i == len(l)-1 {
l = append(l, value)
} else {
l = append(l[:i+1], append(listKey{value}, l[i+1:]...)...)
}
}
db.listKeys[key] = l
db.keyVersion[key]++
c.WriteInt(len(l))
return
}
c.WriteInt(-1)
})
}
// LLEN
func (m *Miniredis) cmdLlen(c *server.Peer, cmd string, args []string) {
if len(args) != 1 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
t, ok := db.keys[key]
if !ok {
// No such key. That's zero length.
c.WriteInt(0)
return
}
if t != "list" {
c.WriteError(msgWrongType)
return
}
c.WriteInt(len(db.listKeys[key]))
})
}
// LPOP
func (m *Miniredis) cmdLpop(c *server.Peer, cmd string, args []string) {
m.cmdXpop(c, cmd, args, left)
}
// RPOP
func (m *Miniredis) cmdRpop(c *server.Peer, cmd string, args []string) {
m.cmdXpop(c, cmd, args, right)
}
func (m *Miniredis) cmdXpop(c *server.Peer, cmd string, args []string, lr leftright) {
if len(args) != 1 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if !db.exists(key) {
// non-existing key is fine
c.WriteNull()
return
}
if db.t(key) != "list" {
c.WriteError(msgWrongType)
return
}
var elem string
switch lr {
case left:
elem = db.listLpop(key)
case right:
elem = db.listPop(key)
}
c.WriteBulk(elem)
})
}
// LPUSH
func (m *Miniredis) cmdLpush(c *server.Peer, cmd string, args []string) {
m.cmdXpush(c, cmd, args, left)
}
// RPUSH
func (m *Miniredis) cmdRpush(c *server.Peer, cmd string, args []string) {
m.cmdXpush(c, cmd, args, right)
}
func (m *Miniredis) cmdXpush(c *server.Peer, cmd string, args []string, lr leftright) {
if len(args) < 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key, args := args[0], args[1:]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if db.exists(key) && db.t(key) != "list" {
c.WriteError(msgWrongType)
return
}
var newLen int
for _, value := range args {
switch lr {
case left:
newLen = db.listLpush(key, value)
case right:
newLen = db.listPush(key, value)
}
}
c.WriteInt(newLen)
})
}
// LPUSHX
func (m *Miniredis) cmdLpushx(c *server.Peer, cmd string, args []string) {
m.cmdXpushx(c, cmd, args, left)
}
// RPUSHX
func (m *Miniredis) cmdRpushx(c *server.Peer, cmd string, args []string) {
m.cmdXpushx(c, cmd, args, right)
}
func (m *Miniredis) cmdXpushx(c *server.Peer, cmd string, args []string, lr leftright) {
if len(args) < 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key, args := args[0], args[1:]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if !db.exists(key) {
c.WriteInt(0)
return
}
if db.t(key) != "list" {
c.WriteError(msgWrongType)
return
}
var newLen int
for _, value := range args {
switch lr {
case left:
newLen = db.listLpush(key, value)
case right:
newLen = db.listPush(key, value)
}
}
c.WriteInt(newLen)
})
}
// LRANGE
func (m *Miniredis) cmdLrange(c *server.Peer, cmd string, args []string) {
if len(args) != 3 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
start, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
end, err := strconv.Atoi(args[2])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if t, ok := db.keys[key]; ok && t != "list" {
c.WriteError(msgWrongType)
return
}
l := db.listKeys[key]
if len(l) == 0 {
c.WriteLen(0)
return
}
rs, re := redisRange(len(l), start, end, false)
c.WriteLen(re - rs)
for _, el := range l[rs:re] {
c.WriteBulk(el)
}
})
}
// LREM
func (m *Miniredis) cmdLrem(c *server.Peer, cmd string, args []string) {
if len(args) != 3 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
count, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
value := args[2]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if !db.exists(key) {
c.WriteInt(0)
return
}
if db.t(key) != "list" {
c.WriteError(msgWrongType)
return
}
l := db.listKeys[key]
if count < 0 {
reverseSlice(l)
}
deleted := 0
newL := []string{}
toDelete := len(l)
if count < 0 {
toDelete = -count
}
if count > 0 {
toDelete = count
}
for _, el := range l {
if el == value {
if toDelete > 0 {
deleted++
toDelete--
continue
}
}
newL = append(newL, el)
}
if count < 0 {
reverseSlice(newL)
}
if len(newL) == 0 {
db.del(key, true)
} else {
db.listKeys[key] = newL
db.keyVersion[key]++
}
c.WriteInt(deleted)
})
}
// LSET
func (m *Miniredis) cmdLset(c *server.Peer, cmd string, args []string) {
if len(args) != 3 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
index, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
value := args[2]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if !db.exists(key) {
c.WriteError(msgKeyNotFound)
return
}
if db.t(key) != "list" {
c.WriteError(msgWrongType)
return
}
l := db.listKeys[key]
if index < 0 {
index = len(l) + index
}
if index < 0 || index > len(l)-1 {
c.WriteError(msgOutOfRange)
return
}
l[index] = value
db.keyVersion[key]++
c.WriteOK()
})
}
// LTRIM
func (m *Miniredis) cmdLtrim(c *server.Peer, cmd string, args []string) {
if len(args) != 3 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
key := args[0]
start, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
end, err := strconv.Atoi(args[2])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
t, ok := db.keys[key]
if !ok {
c.WriteOK()
return
}
if t != "list" {
c.WriteError(msgWrongType)
return
}
l := db.listKeys[key]
rs, re := redisRange(len(l), start, end, false)
l = l[rs:re]
if len(l) == 0 {
db.del(key, true)
} else {
db.listKeys[key] = l
db.keyVersion[key]++
}
c.WriteOK()
})
}
// RPOPLPUSH
func (m *Miniredis) cmdRpoplpush(c *server.Peer, cmd string, args []string) {
if len(args) != 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
src, dst := args[0], args[1]
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
if !db.exists(src) {
c.WriteNull()
return
}
if db.t(src) != "list" || (db.exists(dst) && db.t(dst) != "list") {
c.WriteError(msgWrongType)
return
}
elem := db.listPop(src)
db.listLpush(dst, elem)
c.WriteBulk(elem)
})
}
// BRPOPLPUSH
func (m *Miniredis) cmdBrpoplpush(c *server.Peer, cmd string, args []string) {
if len(args) != 3 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
src := args[0]
dst := args[1]
timeout, err := strconv.Atoi(args[2])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidTimeout)
return
}
if timeout < 0 {
setDirty(c)
c.WriteError(msgNegTimeout)
return
}
blocking(
m,
c,
time.Duration(timeout)*time.Second,
func(c *server.Peer, ctx *connCtx) bool {
db := m.db(ctx.selectedDB)
if !db.exists(src) {
return false
}
if db.t(src) != "list" || (db.exists(dst) && db.t(dst) != "list") {
c.WriteError(msgWrongType)
return true
}
if len(db.listKeys[src]) == 0 {
return false
}
elem := db.listPop(src)
db.listLpush(dst, elem)
c.WriteBulk(elem)
return true
},
func(c *server.Peer) {
// timeout
c.WriteNull()
},
)
}