687 lines
12 KiB
Go
687 lines
12 KiB
Go
// Commands from https://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()
|
|
},
|
|
)
|
|
}
|