Skip to content

Commit

Permalink
Merge pull request #244 from alicebob/copy
Browse files Browse the repository at this point in the history
COPY
  • Loading branch information
alicebob committed Jan 12, 2022
2 parents e8861fe + 48bcccc commit febc6fc
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 2 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
84 changes: 84 additions & 0 deletions cmd_generic.go
Expand Up @@ -35,6 +35,7 @@ func commandsGeneric(m *Miniredis) {
m.srv.Register("TTL", m.cmdTTL)
m.srv.Register("TYPE", m.cmdType)
m.srv.Register("SCAN", m.cmdScan)
m.srv.Register("COPY", m.cmdCopy)
}

// generic expire command for EXPIRE, PEXPIRE, EXPIREAT, PEXPIREAT
Expand Down Expand Up @@ -541,3 +542,86 @@ 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 {
setDirty(c)
c.WriteError(errWrongNumber(cmd))
return
}
if !m.handleAuth(c) {
return
}
if m.checkPubsub(c, cmd) {
return
}

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) {
fromDB, toDB := ctx.selectedDB, opts.destinationDB
if toDB == -1 {
toDB = fromDB
}

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

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)
})
}
67 changes: 67 additions & 0 deletions cmd_generic_test.go
Expand Up @@ -762,3 +762,70 @@ func TestRenamenx(t *testing.T) {
)
})
}

func TestCopy(t *testing.T) {
s, err := Run()
ok(t, err)
defer s.Close()
c, err := proto.Dial(s.Addr())
ok(t, err)
defer c.Close()

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) {
must0(t, c, "COPY", "nosuch", "to")
})

// should return 0 when trying to overwrite an existing key:
t.Run("existing key", func(t *testing.T) {
s.Set("existingkey", "value")
s.Set("newkey", "newvalue")
must0(t, c, "COPY", "newkey", "existingkey")
// 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),
)
})
}
8 changes: 8 additions & 0 deletions direct.go
Expand Up @@ -793,3 +793,11 @@ func (db *RedisDB) HllMerge(destKey string, sourceKeys ...string) error {

return db.hllMerge(append([]string{destKey}, sourceKeys...))
}

// 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(),
}
}
125 changes: 125 additions & 0 deletions integration/generic_test.go
Expand Up @@ -299,3 +299,128 @@ func TestPersist(t *testing.T) {
c.Do("TTL", "foo")
})
}

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 febc6fc

Please sign in to comment.