572 lines
10 KiB
Go
572 lines
10 KiB
Go
// Commands from https://redis.io/commands#hash
|
|
|
|
package miniredis
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/alicebob/miniredis/server"
|
|
)
|
|
|
|
// commandsHash handles all hash value operations.
|
|
func commandsHash(m *Miniredis) {
|
|
m.srv.Register("HDEL", m.cmdHdel)
|
|
m.srv.Register("HEXISTS", m.cmdHexists)
|
|
m.srv.Register("HGET", m.cmdHget)
|
|
m.srv.Register("HGETALL", m.cmdHgetall)
|
|
m.srv.Register("HINCRBY", m.cmdHincrby)
|
|
m.srv.Register("HINCRBYFLOAT", m.cmdHincrbyfloat)
|
|
m.srv.Register("HKEYS", m.cmdHkeys)
|
|
m.srv.Register("HLEN", m.cmdHlen)
|
|
m.srv.Register("HMGET", m.cmdHmget)
|
|
m.srv.Register("HMSET", m.cmdHmset)
|
|
m.srv.Register("HSET", m.cmdHset)
|
|
m.srv.Register("HSETNX", m.cmdHsetnx)
|
|
m.srv.Register("HVALS", m.cmdHvals)
|
|
m.srv.Register("HSCAN", m.cmdHscan)
|
|
}
|
|
|
|
// HSET
|
|
func (m *Miniredis) cmdHset(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, field, value := args[0], args[1], args[2]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
if db.hashSet(key, field, value) {
|
|
c.WriteInt(0)
|
|
} else {
|
|
c.WriteInt(1)
|
|
}
|
|
})
|
|
}
|
|
|
|
// HSETNX
|
|
func (m *Miniredis) cmdHsetnx(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, field, value := args[0], args[1], args[2]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
if _, ok := db.hashKeys[key]; !ok {
|
|
db.hashKeys[key] = map[string]string{}
|
|
db.keys[key] = "hash"
|
|
}
|
|
_, ok := db.hashKeys[key][field]
|
|
if ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
db.hashKeys[key][field] = value
|
|
db.keyVersion[key]++
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
|
|
// HMSET
|
|
func (m *Miniredis) cmdHmset(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 := args[0], args[1:]
|
|
if len(args)%2 != 0 {
|
|
setDirty(c)
|
|
// non-default error message
|
|
c.WriteError("ERR wrong number of arguments for HMSET")
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
for len(args) > 0 {
|
|
field, value := args[0], args[1]
|
|
args = args[2:]
|
|
db.hashSet(key, field, value)
|
|
}
|
|
c.WriteOK()
|
|
})
|
|
}
|
|
|
|
// HGET
|
|
func (m *Miniredis) cmdHget(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, field := args[0], args[1]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
t, ok := db.keys[key]
|
|
if !ok {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
if t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
value, ok := db.hashKeys[key][field]
|
|
if !ok {
|
|
c.WriteNull()
|
|
return
|
|
}
|
|
c.WriteBulk(value)
|
|
})
|
|
}
|
|
|
|
// HDEL
|
|
func (m *Miniredis) cmdHdel(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, fields := args[0], args[1:]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
t, ok := db.keys[key]
|
|
if !ok {
|
|
// No key is zero deleted
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
if t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
deleted := 0
|
|
for _, f := range fields {
|
|
_, ok := db.hashKeys[key][f]
|
|
if !ok {
|
|
continue
|
|
}
|
|
delete(db.hashKeys[key], f)
|
|
deleted++
|
|
}
|
|
c.WriteInt(deleted)
|
|
|
|
// Nothing left. Remove the whole key.
|
|
if len(db.hashKeys[key]) == 0 {
|
|
db.del(key, true)
|
|
}
|
|
})
|
|
}
|
|
|
|
// HEXISTS
|
|
func (m *Miniredis) cmdHexists(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, field := args[0], args[1]
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
t, ok := db.keys[key]
|
|
if !ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
if t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
if _, ok := db.hashKeys[key][field]; !ok {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
c.WriteInt(1)
|
|
})
|
|
}
|
|
|
|
// HGETALL
|
|
func (m *Miniredis) cmdHgetall(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 {
|
|
c.WriteLen(0)
|
|
return
|
|
}
|
|
if t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
c.WriteLen(len(db.hashKeys[key]) * 2)
|
|
for _, k := range db.hashFields(key) {
|
|
c.WriteBulk(k)
|
|
c.WriteBulk(db.hashGet(key, k))
|
|
}
|
|
})
|
|
}
|
|
|
|
// HKEYS
|
|
func (m *Miniredis) cmdHkeys(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)
|
|
|
|
if !db.exists(key) {
|
|
c.WriteLen(0)
|
|
return
|
|
}
|
|
if db.t(key) != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
fields := db.hashFields(key)
|
|
c.WriteLen(len(fields))
|
|
for _, f := range fields {
|
|
c.WriteBulk(f)
|
|
}
|
|
})
|
|
}
|
|
|
|
// HVALS
|
|
func (m *Miniredis) cmdHvals(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 {
|
|
c.WriteLen(0)
|
|
return
|
|
}
|
|
if t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
c.WriteLen(len(db.hashKeys[key]))
|
|
for _, v := range db.hashKeys[key] {
|
|
c.WriteBulk(v)
|
|
}
|
|
})
|
|
}
|
|
|
|
// HLEN
|
|
func (m *Miniredis) cmdHlen(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 {
|
|
c.WriteInt(0)
|
|
return
|
|
}
|
|
if t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
c.WriteInt(len(db.hashKeys[key]))
|
|
})
|
|
}
|
|
|
|
// HMGET
|
|
func (m *Miniredis) cmdHmget(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
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 t, ok := db.keys[key]; ok && t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
f, ok := db.hashKeys[key]
|
|
if !ok {
|
|
f = map[string]string{}
|
|
}
|
|
|
|
c.WriteLen(len(args) - 1)
|
|
for _, k := range args[1:] {
|
|
v, ok := f[k]
|
|
if !ok {
|
|
c.WriteNull()
|
|
continue
|
|
}
|
|
c.WriteBulk(v)
|
|
}
|
|
})
|
|
}
|
|
|
|
// HINCRBY
|
|
func (m *Miniredis) cmdHincrby(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, field, deltas := args[0], args[1], args[2]
|
|
|
|
delta, err := strconv.Atoi(deltas)
|
|
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 != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v, err := db.hashIncr(key, field, delta)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
c.WriteInt(v)
|
|
})
|
|
}
|
|
|
|
// HINCRBYFLOAT
|
|
func (m *Miniredis) cmdHincrbyfloat(c *server.Peer, cmd string, args []string) {
|
|
if len(args) != 3 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key, field, deltas := args[0], args[1], args[2]
|
|
|
|
delta, err := strconv.ParseFloat(deltas, 64)
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidFloat)
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
|
|
if t, ok := db.keys[key]; ok && t != "hash" {
|
|
c.WriteError(msgWrongType)
|
|
return
|
|
}
|
|
|
|
v, err := db.hashIncrfloat(key, field, delta)
|
|
if err != nil {
|
|
c.WriteError(err.Error())
|
|
return
|
|
}
|
|
c.WriteBulk(formatFloat(v))
|
|
})
|
|
}
|
|
|
|
// HSCAN
|
|
func (m *Miniredis) cmdHscan(c *server.Peer, cmd string, args []string) {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(errWrongNumber(cmd))
|
|
return
|
|
}
|
|
if !m.handleAuth(c) {
|
|
return
|
|
}
|
|
|
|
key := args[0]
|
|
cursor, err := strconv.Atoi(args[1])
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidCursor)
|
|
return
|
|
}
|
|
args = args[2:]
|
|
|
|
// MATCH and COUNT options
|
|
var withMatch bool
|
|
var match string
|
|
for len(args) > 0 {
|
|
if strings.ToLower(args[0]) == "count" {
|
|
// we do nothing with count
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
_, err := strconv.Atoi(args[1])
|
|
if err != nil {
|
|
setDirty(c)
|
|
c.WriteError(msgInvalidInt)
|
|
return
|
|
}
|
|
args = args[2:]
|
|
continue
|
|
}
|
|
if strings.ToLower(args[0]) == "match" {
|
|
if len(args) < 2 {
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
withMatch = true
|
|
match, args = args[1], args[2:]
|
|
continue
|
|
}
|
|
setDirty(c)
|
|
c.WriteError(msgSyntaxError)
|
|
return
|
|
}
|
|
|
|
withTx(m, c, func(c *server.Peer, ctx *connCtx) {
|
|
db := m.db(ctx.selectedDB)
|
|
// return _all_ (matched) keys every time
|
|
|
|
if cursor != 0 {
|
|
// Invalid cursor.
|
|
c.WriteLen(2)
|
|
c.WriteBulk("0") // no next cursor
|
|
c.WriteLen(0) // no elements
|
|
return
|
|
}
|
|
if db.exists(key) && db.t(key) != "hash" {
|
|
c.WriteError(ErrWrongType.Error())
|
|
return
|
|
}
|
|
|
|
members := db.hashFields(key)
|
|
if withMatch {
|
|
members = matchKeys(members, match)
|
|
}
|
|
|
|
c.WriteLen(2)
|
|
c.WriteBulk("0") // no next cursor
|
|
// HSCAN gives key, values.
|
|
c.WriteLen(len(members) * 2)
|
|
for _, k := range members {
|
|
c.WriteBulk(k)
|
|
c.WriteBulk(db.hashGet(key, k))
|
|
}
|
|
})
|
|
}
|