Skip to content

Commit

Permalink
implement all COPY options
Browse files Browse the repository at this point in the history
  • Loading branch information
alicebob committed Jan 12, 2022
1 parent 1cd8fc3 commit 48bcccc
Show file tree
Hide file tree
Showing 11 changed files with 314 additions and 60 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -34,6 +34,7 @@ Implemented commands:
- SWAPDB
- QUIT
- Key
- COPY
- DEL
- EXISTS
- EXPIRE
Expand Down
4 changes: 2 additions & 2 deletions cmd_connection.go
Expand Up @@ -227,7 +227,7 @@ func (m *Miniredis) cmdSelect(c *server.Peer, cmd string, args []string) {
return
}
if id < 0 {
c.WriteError("ERR DB index is out of range")
c.WriteError(msgDBIndexOutOfRange)
setDirty(c)
return
}
Expand Down Expand Up @@ -262,7 +262,7 @@ func (m *Miniredis) cmdSwapdb(c *server.Peer, cmd string, args []string) {
return
}
if id1 < 0 || id2 < 0 {
c.WriteError("ERR DB index is out of range")
c.WriteError(msgDBIndexOutOfRange)
setDirty(c)
return
}
Expand Down
64 changes: 57 additions & 7 deletions cmd_generic.go
Expand Up @@ -35,7 +35,6 @@ func commandsGeneric(m *Miniredis) {
m.srv.Register("TTL", m.cmdTTL)
m.srv.Register("TYPE", m.cmdType)
m.srv.Register("SCAN", m.cmdScan)
// COPY
m.srv.Register("COPY", m.cmdCopy)
}

Expand Down Expand Up @@ -546,7 +545,7 @@ func (m *Miniredis) cmdScan(c *server.Peer, cmd string, args []string) {

// COPY
func (m *Miniredis) cmdCopy(c *server.Peer, cmd string, args []string) {
if len(args) != 2 {
if len(args) < 2 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
Expand All @@ -558,20 +557,71 @@ func (m *Miniredis) cmdCopy(c *server.Peer, cmd string, args []string) {
return
}

from, to := args[0], args[1]
var opts = struct {
from string
to string
destinationDB int
replace bool
}{
destinationDB: -1,
}

opts.from, opts.to, args = args[0], args[1], args[2:]
for len(args) > 0 {
switch strings.ToLower(args[0]) {
case "db":
if len(args) < 2 {
setDirty(c)
c.WriteError(msgSyntaxError)
return
}
db, err := strconv.Atoi(args[1])
if err != nil {
setDirty(c)
c.WriteError(msgInvalidInt)
return
}
if db < 0 {
setDirty(c)
c.WriteError(msgDBIndexOutOfRange)
return
}
opts.destinationDB = db
args = args[2:]
case "replace":
opts.replace = true
args = args[1:]
default:
setDirty(c)
c.WriteError(msgSyntaxError)
return
}
}

withTx(m, c, func(c *server.Peer, ctx *connCtx) {
db := m.db(ctx.selectedDB)
fromDB, toDB := ctx.selectedDB, opts.destinationDB
if toDB == -1 {
toDB = fromDB
}

if !db.exists(from) {
c.WriteInt(0)
if fromDB == toDB && opts.from == opts.to {
c.WriteError("ERR source and destination objects are the same")
return
}

if !db.copy(from, to) {
if !m.db(fromDB).exists(opts.from) {
c.WriteInt(0)
return
}

if !opts.replace {
if m.db(toDB).exists(opts.to) {
c.WriteInt(0)
return
}
}

m.copy(m.db(fromDB), opts.from, m.db(toDB), opts.to)
c.WriteInt(1)
})
}
49 changes: 43 additions & 6 deletions cmd_generic_test.go
Expand Up @@ -771,12 +771,13 @@ func TestCopy(t *testing.T) {
ok(t, err)
defer c.Close()

s.Set("key1", "value")
s.CheckGet(t, "key1", "value")
s.Copy("key1", "key2")
// should return 1 after a successful copy operation:
must1(t, c, "COPY", "key1", "key2")
s.CheckGet(t, "key2", "value")
t.Run("basic", func(t *testing.T) {
s.Set("key1", "value")
// should return 1 after a successful copy operation:
must1(t, c, "COPY", "key1", "key2")
s.CheckGet(t, "key2", "value")
equals(t, "string", s.Type("key2"))
})

// should return 0 when trying to copy a nonexistent key:
t.Run("nonexistent key", func(t *testing.T) {
Expand All @@ -791,4 +792,40 @@ func TestCopy(t *testing.T) {
// existing key value should remain unchanged:
s.CheckGet(t, "existingkey", "value")
})

t.Run("destination db", func(t *testing.T) {
s.Set("akey1", "value")
must1(t, c, "COPY", "akey1", "akey2", "DB", "2")
s.Select(2)
s.CheckGet(t, "akey2", "value")
equals(t, "string", s.Type("akey2"))
})
s.Select(0)

t.Run("replace", func(t *testing.T) {
s.Set("rkey1", "value")
s.Set("rkey2", "another")
must1(t, c, "COPY", "rkey1", "rkey2", "REPLACE")
s.CheckGet(t, "rkey2", "value")
equals(t, "string", s.Type("rkey2"))
})

t.Run("direct", func(t *testing.T) {
s.Set("d1", "value")
ok(t, s.Copy(0, "d1", 0, "d2"))
equals(t, "string", s.Type("d2"))
s.CheckGet(t, "d2", "value")
})

t.Run("errors", func(t *testing.T) {
mustDo(t, c, "COPY",
proto.Error(errWrongNumber("copy")),
)
mustDo(t, c, "COPY", "foo",
proto.Error(errWrongNumber("copy")),
)
mustDo(t, c, "COPY", "foo", "bar", "baz",
proto.Error(msgSyntaxError),
)
})
}
31 changes: 0 additions & 31 deletions db.go
Expand Up @@ -112,37 +112,6 @@ func (db *RedisDB) rename(from, to string) {
db.del(from, true)
}

func (db *RedisDB) copy(from, to string) bool {
if _, ok := db.keys[to]; ok {
return false
}
db.keys[to] = from
switch db.t(from) {
case "string":
db.stringKeys[to] = db.stringKeys[from]
case "hash":
db.hashKeys[to] = db.hashKeys[from]
case "list":
db.listKeys[to] = db.listKeys[from]
case "set":
db.setKeys[to] = db.setKeys[from]
case "zset":
db.sortedsetKeys[to] = db.sortedsetKeys[from]
case "stream":
db.streamKeys[to] = db.streamKeys[from]
case "hll":
db.hllKeys[to] = db.hllKeys[from]
default:
panic("missing case")
}
db.keys[to] = db.keys[from]
db.keyVersion[to]++
if v, ok := db.ttl[from]; ok {
db.ttl[to] = v
}
return true
}

func (db *RedisDB) del(k string, delTTL bool) {
if !db.exists(k) {
return
Expand Down
20 changes: 6 additions & 14 deletions direct.go
Expand Up @@ -794,18 +794,10 @@ func (db *RedisDB) HllMerge(destKey string, sourceKeys ...string) error {
return db.hllMerge(append([]string{destKey}, sourceKeys...))
}

func (m *Miniredis) Copy(src, dest string) (bool, error) {
return m.DB(m.selectedDB).Copy(src, dest)
}

func (db *RedisDB) Copy(src, dest string) (bool, error) {
db.master.Lock()
defer db.master.Unlock()
defer db.master.signal.Broadcast()

if !db.exists(src) {
return false, ErrKeyNotFound
}
// return db.copy(src, dest), nil
return true, nil
// Copy a value.
// Needs the IDs of both the source and dest DBs (which can differ).
// Returns ErrKeyNotFound if src does not exist.
// Overwrites dest if it already exists (unlike the redis command, which needs a flag to allow that).
func (m *Miniredis) Copy(srcDB int, src string, destDB int, dest string) error {
return m.copy(m.DB(srcDB), src, m.DB(destDB), dest)
}
6 changes: 6 additions & 0 deletions hll.go
Expand Up @@ -34,3 +34,9 @@ func (h *hll) Bytes() []byte {
dataBytes, _ := h.inner.MarshalBinary()
return dataBytes
}

func (h *hll) copy() *hll {
return &hll{
inner: h.inner.Clone(),
}
}
112 changes: 112 additions & 0 deletions integration/generic_test.go
Expand Up @@ -304,11 +304,123 @@ func TestCopy(t *testing.T) {
testRaw(t, func(c *client) {
c.Error("wrong number", "COPY")
c.Error("wrong number", "COPY", "a")
c.Error("syntax", "COPY", "a", "b", "c")
c.Error("syntax", "COPY", "a", "b", "DB")
c.Error("range", "COPY", "a", "b", "DB", "-1")
c.Error("integer", "COPY", "a", "b", "DB", "foo")
c.Error("syntax", "COPY", "a", "b", "DB", "1", "REPLACE", "foo")

c.Do("SET", "a", "1")
c.Do("COPY", "a", "b") // returns 1 - successfully copied
c.Do("EXISTS", "b")
c.Do("GET", "b")
c.Do("TYPE", "b")

c.Do("COPY", "nonexistent", "c") // returns 1 - not successfully copied
c.Do("RENAME", "b", "c") // rename the copied key

t.Run("replace option", func(t *testing.T) {
c.Do("SET", "fromme", "1")
c.Do("HSET", "replaceme", "foo", "bar")
c.Do("COPY", "fromme", "replaceme", "REPLACE")
c.Do("TYPE", "replaceme")
c.Do("GET", "replaceme")
})

t.Run("different DB", func(t *testing.T) {
c.Do("SELECT", "2")
c.Do("SET", "fromme", "1")
c.Do("COPY", "fromme", "replaceme", "DB", "3")
c.Do("EXISTS", "replaceme") // your value is in another db
c.Do("SELECT", "3")
c.Do("EXISTS", "replaceme")
c.Do("TYPE", "replaceme")
c.Do("GET", "replaceme")
})
c.Do("SELECT", "0")

t.Run("copy to self", func(t *testing.T) {
// copy to self is never allowed
c.Do("SET", "double", "1")
c.Error("the same", "COPY", "double", "double")
c.Error("the same", "COPY", "double", "double", "REPLACE")
c.Do("COPY", "double", "double", "DB", "2") // different DB is fine
c.Do("SELECT", "2")
c.Do("TYPE", "double")

c.Error("the same", "COPY", "noexisting", "noexisting") // "copy to self?" check comes before key check
})
c.Do("SELECT", "0")

// deep copies?
t.Run("hash", func(t *testing.T) {
c.Do("HSET", "temp", "paris", "12")
c.Do("HSET", "temp", "oslo", "-5")
c.Do("COPY", "temp", "temp2")
c.Do("TYPE", "temp2")
c.Do("HGET", "temp2", "oslo")
c.Do("HSET", "temp2", "oslo", "-7")
c.Do("HGET", "temp", "oslo")
c.Do("HGET", "temp2", "oslo")
})

t.Run("list", func(t *testing.T) {
c.Do("LPUSH", "list", "aap", "noot", "mies")
c.Do("COPY", "list", "list2")
c.Do("TYPE", "list2")
c.Do("LPUSH", "list", "vuur")
c.Do("LRANGE", "list", "0", "-1")
c.Do("LRANGE", "list2", "0", "-1")
})

t.Run("set", func(t *testing.T) {
c.Do("SADD", "set", "aap", "noot", "mies")
c.Do("COPY", "set", "set2")
c.Do("TYPE", "set2")
c.DoSorted("SMEMBERS", "set2")
c.Do("SADD", "set", "vuur")
c.DoSorted("SMEMBERS", "set")
c.DoSorted("SMEMBERS", "set2")
})

t.Run("sorted set", func(t *testing.T) {
c.Do("ZADD", "zset", "1", "aap", "2", "noot", "3", "mies")
c.Do("COPY", "zset", "zset2")
c.Do("TYPE", "zset2")
c.Do("ZCARD", "zset")
c.Do("ZCARD", "zset2")
c.Do("ZADD", "zset", "4", "vuur")
c.Do("ZCARD", "zset")
c.Do("ZCARD", "zset2")
})

t.Run("stream", func(t *testing.T) {
c.Do("XADD",
"planets",
"0-1",
"name", "Mercury",
)
c.Do("COPY", "planets", "planets2")
c.Do("XLEN", "planets2")
c.Do("TYPE", "planets2")

c.Do("XADD",
"planets",
"18446744073709551000-0",
"name", "Earth",
)
c.Do("XLEN", "planets")
c.Do("XLEN", "planets2")
})

t.Run("stream", func(t *testing.T) {
c.Do("PFADD", "hlog", "42")
c.DoApprox(2, "PFCOUNT", "hlog")
c.Do("COPY", "hlog", "hlog2")
// c.Do("TYPE", "hlog2") broken
c.Do("PFADD", "hlog", "44")
c.Do("PFCOUNT", "hlog")
c.Do("PFCOUNT", "hlog2")
})
})
}

0 comments on commit 48bcccc

Please sign in to comment.