Skip to content

Commit

Permalink
implement all copy args
Browse files Browse the repository at this point in the history
m.Copy() now needs explicit DBs where you copy from and where you copy
to.
  • Loading branch information
alicebob committed Jan 12, 2022
1 parent 9f287e2 commit faf68cd
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 81 deletions.
4 changes: 2 additions & 2 deletions cmd_connection.go
Original file line number Diff line number Diff line change
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
60 changes: 53 additions & 7 deletions cmd_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,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 @@ -557,20 +557,66 @@ 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) {
if !m.db(fromDB).exists(opts.from) {
c.WriteInt(0)
return
}

if !db.copy(from, to) {
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)
})
}
34 changes: 31 additions & 3 deletions cmd_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -793,11 +793,39 @@ func TestCopy(t *testing.T) {
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")
worked, err := s.Copy("d1", "d2")
ok(t, err)
assert(t, worked, "copied the key")
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),
)
})
}
56 changes: 0 additions & 56 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,38 +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] = copyHashKey(db.hashKeys[from])
case "list":
db.listKeys[to] = db.listKeys[from]
case "set":
db.setKeys[to] = copySetKey(db.setKeys[from])
case "zset":
db.sortedsetKeys[to] = copySortedSet(db.sortedsetKeys[from])
case "stream":
db.streamKeys[to] = db.streamKeys[from].copy()
case "hll":
db.hllKeys[to] = db.hllKeys[from].copy()
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 Expand Up @@ -730,27 +698,3 @@ func (db *RedisDB) hllMerge(keys []string) error {

return nil
}

func copyHashKey(orig hashKey) hashKey {
cpy := hashKey{}
for k, v := range orig {
cpy[k] = v
}
return cpy
}

func copySetKey(orig setKey) setKey {
cpy := setKey{}
for k, v := range orig {
cpy[k] = v
}
return cpy
}

func copySortedSet(orig sortedSet) sortedSet {
cpy := sortedSet{}
for k, v := range orig {
cpy[k] = v
}
return cpy
}
19 changes: 6 additions & 13 deletions direct.go
Original file line number Diff line number Diff line change
Expand Up @@ -794,17 +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
// 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)
}
24 changes: 24 additions & 0 deletions integration/generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ 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
Expand All @@ -314,6 +319,25 @@ func TestCopy(t *testing.T) {
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")
})

// deep copies?
t.Run("hash", func(t *testing.T) {
c.Do("HSET", "temp", "paris", "12")
Expand Down
59 changes: 59 additions & 0 deletions miniredis.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,3 +637,62 @@ func (m *Miniredis) at(i int, d time.Duration) time.Duration {
now := m.effectiveNow()
return ts.Sub(now)
}

// copy does not mind if dst already exists.
func (m *Miniredis) copy(
srcDB *RedisDB, src string,
destDB *RedisDB, dst string,
) error {
if !srcDB.exists(src) {
return ErrKeyNotFound
}

switch srcDB.t(src) {
case "string":
destDB.stringKeys[dst] = srcDB.stringKeys[src]
case "hash":
destDB.hashKeys[dst] = copyHashKey(srcDB.hashKeys[src])
case "list":
destDB.listKeys[dst] = srcDB.listKeys[src]
case "set":
destDB.setKeys[dst] = copySetKey(srcDB.setKeys[src])
case "zset":
destDB.sortedsetKeys[dst] = copySortedSet(srcDB.sortedsetKeys[src])
case "stream":
destDB.streamKeys[dst] = srcDB.streamKeys[src].copy()
case "hll":
destDB.hllKeys[dst] = srcDB.hllKeys[src].copy()
default:
panic("missing case")
}
destDB.keys[dst] = srcDB.keys[src]
destDB.keyVersion[dst]++
if v, ok := srcDB.ttl[src]; ok {
destDB.ttl[dst] = v
}
return nil
}

func copyHashKey(orig hashKey) hashKey {
cpy := hashKey{}
for k, v := range orig {
cpy[k] = v
}
return cpy
}

func copySetKey(orig setKey) setKey {
cpy := setKey{}
for k, v := range orig {
cpy[k] = v
}
return cpy
}

func copySortedSet(orig sortedSet) sortedSet {
cpy := sortedSet{}
for k, v := range orig {
cpy[k] = v
}
return cpy
}
1 change: 1 addition & 0 deletions redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
msgXtrimInvalidStrategy = "ERR unsupported XTRIM strategy. Please use MAXLEN, MINID"
msgXtrimInvalidMaxLen = "ERR value is not an integer or out of range"
msgXtrimInvalidLimit = "ERR syntax error, LIMIT cannot be used without the special ~ option"
msgDBIndexOutOfRange = "ERR DB index is out of range"
)

func errWrongNumber(cmd string) string {
Expand Down

0 comments on commit faf68cd

Please sign in to comment.