summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/db.c102
-rw-r--r--src/server.c4
-rw-r--r--src/server.h5
-rw-r--r--src/stream.h1
-rw-r--r--src/t_hash.c54
-rw-r--r--src/t_list.c23
-rw-r--r--src/t_set.c46
-rw-r--r--src/t_stream.c104
-rw-r--r--src/t_zset.c62
-rw-r--r--tests/unit/keyspace.tcl207
10 files changed, 608 insertions, 0 deletions
diff --git a/src/db.c b/src/db.c
index f9570dc75..b8abb6877 100644
--- a/src/db.c
+++ b/src/db.c
@@ -1079,6 +1079,108 @@ void moveCommand(client *c) {
addReply(c,shared.cone);
}
+void copyCommand(client *c) {
+ robj *o;
+ redisDb *src, *dst;
+ int srcid;
+ long long dbid, expire;
+ int j, replace = 0, delete = 0;
+
+ /* Obtain source and target DB pointers
+ * Default target DB is the same as the source DB
+ * Parse the REPLACE option and targetDB option. */
+ src = c->db;
+ dst = c->db;
+ srcid = c->db->id;
+ dbid = c->db->id;
+ for (j = 3; j < c->argc; j++) {
+ int additional = c->argc - j - 1;
+ if (!strcasecmp(c->argv[j]->ptr,"replace")) {
+ replace = 1;
+ } else if (!strcasecmp(c->argv[j]->ptr, "db") && additional >= 1) {
+ if (getLongLongFromObject(c->argv[j+1], &dbid) == C_ERR ||
+ dbid < INT_MIN || dbid > INT_MAX ||
+ selectDb(c, dbid) == C_ERR)
+ {
+ addReplyError(c,"invalid DB index");
+ return;
+ }
+ dst = c->db;
+ selectDb(c,srcid); /* Back to the source DB */
+ j++; /* Consume additional arg. */
+ } else {
+ addReply(c, shared.syntaxerr);
+ return;
+ }
+ }
+
+ if ((server.cluster_enabled == 1) && (srcid != 0 || dbid != 0)) {
+ addReplyError(c,"Copying to another database is not allowed in cluster mode");
+ return;
+ }
+
+ /* If the user select the same DB as
+ * the source DB and using newkey as the same key
+ * it is probably an error. */
+ robj *key = c->argv[1];
+ robj *newkey = c->argv[2];
+ if (src == dst && (sdscmp(key->ptr, newkey->ptr) == 0)) {
+ addReply(c,shared.sameobjecterr);
+ return;
+ }
+
+ /* Check if the element exists and get a reference */
+ o = lookupKeyWrite(c->db, key);
+ if (!o) {
+ addReply(c,shared.czero);
+ return;
+ }
+ expire = getExpire(c->db,key);
+
+ /* Return zero if the key already exists in the target DB.
+ * If REPLACE option is selected, delete newkey from targetDB. */
+ if (lookupKeyWrite(dst,newkey) != NULL) {
+ if (replace) {
+ delete = 1;
+ } else {
+ addReply(c,shared.czero);
+ return;
+ }
+ }
+
+ /* Duplicate object according to object's type. */
+ robj *newobj;
+ switch(o->type) {
+ case OBJ_STRING: newobj = dupStringObject(o); break;
+ case OBJ_LIST: newobj = listTypeDup(o); break;
+ case OBJ_SET: newobj = setTypeDup(o); break;
+ case OBJ_ZSET: newobj = zsetDup(o); break;
+ case OBJ_HASH: newobj = hashTypeDup(o); break;
+ case OBJ_STREAM: newobj = streamDup(o); break;
+ case OBJ_MODULE:
+ addReplyError(c, "Copying module type object is not supported");
+ return;
+ default: {
+ addReplyError(c, "unknown type object");
+ return;
+ };
+ }
+
+ if (delete) {
+ dbDelete(dst,newkey);
+ }
+
+ dbAdd(dst,newkey,newobj);
+ if (expire != -1) setExpire(c, dst, newkey, expire);
+
+ /* OK! key copied */
+ signalModifiedKey(c,dst,c->argv[2]);
+ notifyKeyspaceEvent(NOTIFY_GENERIC,"copy_to",c->argv[2],dst->id);
+
+ server.dirty++;
+ addReply(c,shared.cone);
+}
+
/* Helper function for dbSwapDatabases(): scans the list of keys that have
* one or more blocked clients for B[LR]POP or other blocking commands
* and signal the keys as ready if they are of the right type. See the comment
diff --git a/src/server.c b/src/server.c
index 685910f19..cef9f7185 100644
--- a/src/server.c
+++ b/src/server.c
@@ -630,6 +630,10 @@ struct redisCommand redisCommandTable[] = {
"write fast @keyspace",
0,NULL,1,1,1,0,0,0},
+ {"copy",copyCommand,-3,
+ "write use-memory @keyspace",
+ 0,NULL,1,2,1,0,0,0},
+
/* Like for SET, we can't mark rename as a fast command because
* overwriting the target key may result in an implicit slow DEL. */
{"rename",renameCommand,3,
diff --git a/src/server.h b/src/server.h
index c7ddc9e84..66ee066a1 100644
--- a/src/server.h
+++ b/src/server.h
@@ -1796,6 +1796,7 @@ void listTypeInsert(listTypeEntry *entry, robj *value, int where);
int listTypeEqual(listTypeEntry *entry, robj *o);
void listTypeDelete(listTypeIterator *iter, listTypeEntry *entry);
void listTypeConvert(robj *subject, int enc);
+robj *listTypeDup(robj *o);
void unblockClientWaitingData(client *c);
void popGenericCommand(client *c, int where);
void listElementsRemoved(client *c, robj *key, int where, robj *o);
@@ -2027,6 +2028,7 @@ unsigned long zslGetRank(zskiplist *zsl, double score, sds o);
int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore);
long zsetRank(robj *zobj, sds ele, int reverse);
int zsetDel(robj *zobj, sds ele);
+robj *zsetDup(robj *o);
void genericZpopCommand(client *c, robj **keyv, int keyc, int where, int emitkey, robj *countarg);
sds ziplistGetObject(unsigned char *sptr);
int zslValueGteMin(double value, zrangespec *spec);
@@ -2105,6 +2107,7 @@ int setTypeRandomElement(robj *setobj, sds *sdsele, int64_t *llele);
unsigned long setTypeRandomElements(robj *set, unsigned long count, robj *aux_set);
unsigned long setTypeSize(const robj *subject);
void setTypeConvert(robj *subject, int enc);
+robj *setTypeDup(robj *o);
/* Hash data type */
#define HASH_SET_TAKE_FIELD (1<<0)
@@ -2129,6 +2132,7 @@ sds hashTypeCurrentObjectNewSds(hashTypeIterator *hi, int what);
robj *hashTypeLookupWriteOrCreate(client *c, robj *key);
robj *hashTypeGetValueObject(robj *o, sds field);
int hashTypeSet(robj *o, sds field, sds value, int flags);
+robj *hashTypeDup(robj *o);
/* Pub / Sub */
int pubsubUnsubscribeAllChannels(client *c, int notify);
@@ -2336,6 +2340,7 @@ void bgsaveCommand(client *c);
void bgrewriteaofCommand(client *c);
void shutdownCommand(client *c);
void moveCommand(client *c);
+void copyCommand(client *c);
void renameCommand(client *c);
void renamenxCommand(client *c);
void lpushCommand(client *c);
diff --git a/src/stream.h b/src/stream.h
index 3d692cd9d..7e1a24417 100644
--- a/src/stream.h
+++ b/src/stream.h
@@ -118,5 +118,6 @@ int streamCompareID(streamID *a, streamID *b);
void streamFreeNACK(streamNACK *na);
void streamIncrID(streamID *id);
void streamPropagateConsumerCreation(client *c, robj *key, robj *groupname, sds consumername);
+robj *streamDup(robj *o);
#endif
diff --git a/src/t_hash.c b/src/t_hash.c
index 89193a41e..5c844ce30 100644
--- a/src/t_hash.c
+++ b/src/t_hash.c
@@ -504,6 +504,60 @@ void hashTypeConvert(robj *o, int enc) {
}
}
+/* This is a helper function for the COPY command.
+ * Duplicate a hash object, with the guarantee that the returned object
+ * has the same encoding as the original one.
+ *
+ * The resulting object always has refcount set to 1 */
+robj *hashTypeDup(robj *o) {
+ robj *hobj;
+ hashTypeIterator *hi;
+
+ serverAssert(o->type == OBJ_HASH);
+
+ switch (o->encoding) {
+ case OBJ_ENCODING_ZIPLIST:
+ hobj = createHashObject();
+ break;
+ case OBJ_ENCODING_HT:
+ hobj = createHashObject();
+ hashTypeConvert(hobj, OBJ_ENCODING_HT);
+ dict *d = o->ptr;
+ dictExpand(hobj->ptr, dictSize(d));
+ break;
+ default:
+ serverPanic("Wrong encoding.");
+ break;
+ }
+
+ if(o->encoding == OBJ_ENCODING_ZIPLIST){
+ unsigned char *zl = o->ptr;
+ size_t sz = ziplistBlobLen(zl);
+ unsigned char *new_zl = zmalloc(sz);
+ memcpy(new_zl, zl, sz);
+ zfree(hobj->ptr);
+ hobj->ptr = new_zl;
+ } else if(o->encoding == OBJ_ENCODING_HT){
+ hi = hashTypeInitIterator(o);
+ while (hashTypeNext(hi) != C_ERR) {
+ sds field, value;
+ sds newfield, newvalue;
+ /* Extract a field-value pair from an original hash object.*/
+ field = hashTypeCurrentFromHashTable(hi, OBJ_HASH_KEY);
+ value = hashTypeCurrentFromHashTable(hi, OBJ_HASH_VALUE);
+ newfield = sdsdup(field);
+ newvalue = sdsdup(value);
+
+ /* Add a field-value pair to a new hash object. */
+ dictAdd(hobj->ptr,newfield,newvalue);
+ }
+ hashTypeReleaseIterator(hi);
+ } else {
+ serverPanic("Unknown hash encoding");
+ }
+ return hobj;
+}
+
/*-----------------------------------------------------------------------------
* Hash type commands
*----------------------------------------------------------------------------*/
diff --git a/src/t_list.c b/src/t_list.c
index 25dda3178..cdfc0ff51 100644
--- a/src/t_list.c
+++ b/src/t_list.c
@@ -190,6 +190,29 @@ void listTypeConvert(robj *subject, int enc) {
}
}
+/* This is a helper function for the COPY command.
+ * Duplicate a list object, with the guarantee that the returned object
+ * has the same encoding as the original one.
+ *
+ * The resulting object always has refcount set to 1 */
+robj *listTypeDup(robj *o) {
+ robj *lobj;
+
+ serverAssert(o->type == OBJ_LIST);
+
+ switch (o->encoding) {
+ case OBJ_ENCODING_QUICKLIST:
+ lobj = createQuicklistObject();
+ break;
+ default:
+ serverPanic("Wrong encoding.");
+ break;
+ }
+ zfree(lobj->ptr);
+ lobj->ptr = quicklistDup(o->ptr);
+ return lobj;
+}
+
/*-----------------------------------------------------------------------------
* List Commands
*----------------------------------------------------------------------------*/
diff --git a/src/t_set.c b/src/t_set.c
index 2cca15ae2..cb4c5002f 100644
--- a/src/t_set.c
+++ b/src/t_set.c
@@ -261,6 +261,52 @@ void setTypeConvert(robj *setobj, int enc) {
}
}
+/* This is a helper function for the COPY command.
+ * Duplicate a set object, with the guarantee that the returned object
+ * has the same encoding as the original one.
+ *
+ * The resulting object always has refcount set to 1 */
+robj *setTypeDup(robj *o) {
+ robj *set;
+ setTypeIterator *si;
+ sds elesds;
+ int64_t intobj;
+
+ serverAssert(o->type == OBJ_SET);
+
+ /* Create a new set object that have the same encoding as the original object's encoding */
+ switch (o->encoding) {
+ case OBJ_ENCODING_INTSET:
+ set = createIntsetObject();
+ break;
+ case OBJ_ENCODING_HT:
+ set = createSetObject();
+ dict *d = o->ptr;
+ dictExpand(set->ptr, dictSize(d));
+ break;
+ default:
+ serverPanic("Wrong encoding.");
+ break;
+ }
+ if (set->encoding == OBJ_ENCODING_INTSET) {
+ intset *is = o->ptr;
+ size_t size = intsetBlobLen(is);
+ intset *newis = zmalloc(size);
+ memcpy(newis,is,size);
+ zfree(set->ptr);
+ set->ptr = newis;
+ } else if (set->encoding == OBJ_ENCODING_HT) {
+ si = setTypeInitIterator(o);
+ while (setTypeNext(si, &elesds, &intobj) != -1) {
+ setTypeAdd(set, elesds);
+ }
+ setTypeReleaseIterator(si);
+ } else {
+ serverPanic("Unknown set encoding");
+ }
+ return set;
+}
+
void saddCommand(client *c) {
robj *set;
int j, added = 0;
diff --git a/src/t_stream.c b/src/t_stream.c
index d6f6a3011..02e4c3242 100644
--- a/src/t_stream.c
+++ b/src/t_stream.c
@@ -106,6 +106,110 @@ void streamNextID(streamID *last_id, streamID *new_id) {
}
}
+/* This is a helper function for the COPY command.
+ * Duplicate a Stream object, with the guarantee that the returned object
+ * has the same encoding as the original one.
+ *
+ * The resulting object always has refcount set to 1 */
+robj *streamDup(robj *o) {
+ robj *sobj;
+
+ serverAssert(o->type == OBJ_STREAM);
+
+ switch (o->encoding) {
+ case OBJ_ENCODING_STREAM:
+ sobj = createStreamObject();
+ break;
+ default:
+ serverPanic("Wrong encoding.");
+ break;
+ }
+
+ stream *s;
+ stream *new_s;
+ s = o->ptr;
+ new_s = sobj->ptr;
+
+ raxIterator ri;
+ uint64_t rax_key[2];
+ raxStart(&ri, s->rax);
+ raxSeek(&ri, "^", NULL, 0);
+ size_t lp_bytes = 0; /* Total bytes in the listpack. */
+ unsigned char *lp = NULL; /* listpack pointer. */
+ /* Get a reference to the listpack node. */
+ while (raxNext(&ri)) {
+ lp = ri.data;
+ lp_bytes = lpBytes(lp);
+ unsigned char *new_lp = zmalloc(lp_bytes);
+ memcpy(new_lp, lp, lp_bytes);
+ memcpy(rax_key, ri.key, sizeof(rax_key));
+ raxInsert(new_s->rax, (unsigned char *)&rax_key, sizeof(rax_key),
+ new_lp, NULL);
+ }
+ new_s->length = s->length;
+ new_s->last_id = s->last_id;
+ raxStop(&ri);
+
+ if (s->cgroups == NULL) return sobj;
+
+ /* Consumer Groups */
+ raxIterator ri_cgroups;
+ raxStart(&ri_cgroups, s->cgroups);
+ raxSeek(&ri_cgroups, "^", NULL, 0);
+ while (raxNext(&ri_cgroups)) {
+ streamCG *cg = ri_cgroups.data;
+ streamCG *new_cg = streamCreateCG(new_s, (char *)ri_cgroups.key,
+ ri_cgroups.key_len, &cg->last_id);
+
+ serverAssert(new_cg != NULL);
+
+ /* Consumer Group PEL */
+ raxIterator ri_cg_pel;
+ raxStart(&ri_cg_pel,cg->pel);
+ raxSeek(&ri_cg_pel,"^",NULL,0);
+ while(raxNext(&ri_cg_pel)){
+ streamNACK *nack = ri_cg_pel.data;
+ streamNACK *new_nack = streamCreateNACK(NULL);
+ new_nack->delivery_time = nack->delivery_time;
+ new_nack->delivery_count = nack->delivery_count;
+ raxInsert(new_cg->pel, ri_cg_pel.key, sizeof(streamID), new_nack, NULL);
+ }
+ raxStop(&ri_cg_pel);
+
+ /* Consumers */
+ raxIterator ri_consumers;
+ raxStart(&ri_consumers, cg->consumers);
+ raxSeek(&ri_consumers, "^", NULL, 0);
+ while (raxNext(&ri_consumers)) {
+ streamConsumer *consumer = ri_consumers.data;
+ streamConsumer *new_consumer;
+ new_consumer = zmalloc(sizeof(*new_consumer));
+ new_consumer->name = sdsdup(consumer->name);
+ new_consumer->pel = raxNew();
+ raxInsert(new_cg->consumers,(unsigned char *)new_consumer->name,
+ sdslen(new_consumer->name), new_consumer, NULL);
+ new_consumer->seen_time = consumer->seen_time;
+
+ /* Consumer PEL */
+ raxIterator ri_cpel;
+ raxStart(&ri_cpel, consumer->pel);
+ raxSeek(&ri_cpel, "^", NULL, 0);
+ while (raxNext(&ri_cpel)) {
+ streamNACK *new_nack = raxFind(new_cg->pel,ri_cpel.key,sizeof(streamID));
+
+ serverAssert(new_nack != raxNotFound);
+
+ new_nack->consumer = new_consumer;
+ raxInsert(new_consumer->pel,ri_cpel.key,sizeof(streamID),new_nack,NULL);
+ }
+ raxStop(&ri_cpel);
+ }
+ raxStop(&ri_consumers);
+ }
+ raxStop(&ri_cgroups);
+ return sobj;
+}
+
/* This is just a wrapper for lpAppend() to directly use a 64 bit integer
* instead of a string. */
unsigned char *lpAppendInteger(unsigned char *lp, int64_t value) {
diff --git a/src/t_zset.c b/src/t_zset.c
index 5b848a1b9..07581673e 100644
--- a/src/t_zset.c
+++ b/src/t_zset.c
@@ -1553,6 +1553,68 @@ long zsetRank(robj *zobj, sds ele, int reverse) {
}
}
+/* This is a helper function for the COPY command.
+ * Duplicate a sorted set object, with the guarantee that the returned object
+ * has the same encoding as the original one.
+ *
+ * The resulting object always has refcount set to 1 */
+robj *zsetDup(robj *o) {
+ robj *zobj;
+ zset *zs;
+ zset *new_zs;
+
+ serverAssert(o->type == OBJ_ZSET);
+
+ /* Create a new sorted set object that have the same encoding as the original object's encoding */
+ switch (o->encoding) {
+ case OBJ_ENCODING_ZIPLIST:
+ zobj = createZsetZiplistObject();
+ break;
+ case OBJ_ENCODING_SKIPLIST:
+ zobj = createZsetObject();
+ zs = o->ptr;
+ new_zs = zobj->ptr;
+ dictExpand(new_zs->dict,dictSize(zs->dict));
+ break;
+ default:
+ serverPanic("Wrong encoding.");
+ break;
+ }
+ if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
+ unsigned char *zl = o->ptr;
+ size_t sz = ziplistBlobLen(zl);
+ unsigned char *new_zl = zmalloc(sz);
+ memcpy(new_zl, zl, sz);
+ zfree(zobj->ptr);
+ zobj->ptr = new_zl;
+ } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
+ zs = o->ptr;
+ new_zs = zobj->ptr;
+ zskiplist *zsl = zs->zsl;
+ zskiplistNode *ln;
+ sds ele;
+ long llen = zsetLength(o);
+
+ /* We copy the skiplist elements from the greatest to the
+ * smallest (that's trivial since the elements are already ordered in
+ * the skiplist): this improves the load process, since the next loaded
+ * element will always be the smaller, so adding to the skiplist
+ * will always immediately stop at the head, making the insertion
+ * O(1) instead of O(log(N)). */
+ ln = zsl->tail;
+ while (llen--) {
+ ele = ln->ele;
+ sds new_ele = sdsdup(ele);
+ zskiplistNode *znode = zslInsert(new_zs->zsl,ln->score,new_ele);
+ dictAdd(new_zs->dict,new_ele,&znode->score);
+ ln = ln->backward;
+ }
+ } else {
+ serverPanic("Unknown sorted set encoding");
+ }
+ return zobj;
+}
+
/*-----------------------------------------------------------------------------
* Sorted set commands
*----------------------------------------------------------------------------*/
diff --git a/tests/unit/keyspace.tcl b/tests/unit/keyspace.tcl
index d4e7bf51c..a74651db9 100644
--- a/tests/unit/keyspace.tcl
+++ b/tests/unit/keyspace.tcl
@@ -169,6 +169,213 @@ start_server {tags {"keyspace"}} {
format $res
} {0}
+ test {COPY basic usage for string} {
+ r set mykey foobar
+ set res {}
+ r copy mykey mynewkey
+ lappend res [r get mynewkey]
+ lappend res [r dbsize]
+ r copy mykey mynewkey DB 10
+ r select 10
+ lappend res [r get mynewkey]
+ lappend res [r dbsize]
+ r select 9
+ format $res
+ } [list foobar 2 foobar 1]
+
+ test {COPY for string does not replace an existing key without REPLACE option} {
+ r set mykey2 hello
+ catch {r copy mykey2 mynewkey DB 10} e
+ set e
+ } {0}
+
+ test {COPY for string can replace an existing key with REPLACE option} {
+ r copy mykey2 mynewkey DB 10 REPLACE
+ r select 10
+ r get mynewkey
+ } {hello}
+
+ test {COPY for string ensures that copied data is independent of copying data} {
+ r flushdb
+ r select 9
+ r set mykey foobar
+ set res {}
+ r copy mykey mynewkey DB 10
+ r select 10
+ lappend res [r get mynewkey]
+ r set mynewkey hoge
+ lappend res [r get mynewkey]
+ r select 9
+ lappend res [r get mykey]
+ r select 10
+ r flushdb
+ r select 9
+ format $res
+ } [list foobar hoge foobar]
+
+ test {COPY for string does not copy data to no-integer DB} {
+ r set mykey foobar
+ catch {r copy mykey mynewkey DB notanumber} e
+ set e
+ } {*ERR*invalid DB index}
+
+ test {COPY can copy key expire metadata as well} {
+ r set mykey foobar ex 100
+ r copy mykey mynewkey REPLACE
+ assert {[r ttl mynewkey] > 0 && [r ttl mynewkey] <= 100}
+ assert {[r get mynewkey] eq "foobar"}
+ }
+
+ test {COPY does not create an expire if it does not exist} {
+ r set mykey foobar
+ assert {[r ttl mykey] == -1}
+ r copy mykey mynewkey REPLACE
+ assert {[r ttl mynewkey] == -1}
+ assert {[r get mynewkey] eq "foobar"}
+ }
+
+ test {COPY basic usage for list} {
+ r del mylist mynewlist
+ r lpush mylist a b c d
+ r copy mylist mynewlist
+ set digest [r debug digest-value mylist]
+ assert_equal $digest [r debug digest-value mynewlist]
+ assert_equal 1 [r object refcount mylist]
+ assert_equal 1 [r object refcount mynewlist]
+ r del mylist
+ assert_equal $digest [r debug digest-value mynewlist]
+ }
+
+ test {COPY basic usage for intset set} {
+ r del set1 newset1
+ r sadd set1 1 2 3
+ assert_encoding intset set1
+ r copy set1 newset1
+ set digest [r debug digest-value set1]
+ assert_equal $digest [r debug digest-value newset1]
+ assert_equal 1 [r object refcount set1]
+ assert_equal 1 [r object refcount newset1]
+ r del set1
+ assert_equal $digest [r debug digest-value newset1]
+ }
+
+ test {COPY basic usage for hashtable set} {
+ r del set2 newset2
+ r sadd set2 1 2 3 a
+ assert_encoding hashtable set2
+ r copy set2 newset2
+ set digest [r debug digest-value set2]
+ assert_equal $digest [r debug digest-value newset2]
+ assert_equal 1 [r object refcount set2]
+ assert_equal 1 [r object refcount newset2]
+ r del set2
+ assert_equal $digest [r debug digest-value newset2]
+ }
+
+ test {COPY basic usage for ziplist sorted set} {
+ r del zset1 newzset1
+ r zadd zset1 123 foobar
+ assert_encoding ziplist zset1
+ r copy zset1 newzset1
+ set digest [r debug digest-value zset1]
+ assert_equal $digest [r debug digest-value newzset1]
+ assert_equal 1 [r object refcount zset1]
+ assert_equal 1 [r object refcount newzset1]
+ r del zset1
+ assert_equal $digest [r debug digest-value newzset1]
+ }
+
+ test {COPY basic usage for skiplist sorted set} {
+ r del zset2 newzset2
+ set original_max [lindex [r config get zset-max-ziplist-entries] 1]
+ r config set zset-max-ziplist-entries 0
+ for {set j 0} {$j < 130} {incr j} {
+ r zadd zset2 [randomInt 50] ele-[randomInt 10]
+ }
+ assert_encoding skiplist zset2
+ r copy zset2 newzset2
+ set digest [r debug digest-value zset2]
+ assert_equal $digest [r debug digest-value newzset2]
+ assert_equal 1 [r object refcount zset2]
+ assert_equal 1 [r object refcount newzset2]
+ r del zset2
+ assert_equal $digest [r debug digest-value newzset2]
+ r config set zset-max-ziplist-entries $original_max
+ }
+
+ test {COPY basic usage for ziplist hash} {
+ r del hash1 newhash1
+ r hset hash1 tmp 17179869184
+ assert_encoding ziplist hash1
+ r copy hash1 newhash1
+ set digest [r debug digest-value hash1]
+ assert_equal $digest [r debug digest-value newhash1]
+ assert_equal 1 [r object refcount hash1]
+ assert_equal 1 [r object refcount newhash1]
+ r del hash1
+ assert_equal $digest [r debug digest-value newhash1]
+ }
+
+ test {COPY basic usage for hashtable hash} {
+ r del hash2 newhash2
+ set original_max [lindex [r config get hash-max-ziplist-entries] 1]
+ r config set hash-max-ziplist-entries 0
+ for {set i 0} {$i < 64} {incr i} {
+ r hset hash2 [randomValue] [randomValue]
+ }
+ assert_encoding hashtable hash2
+ r copy hash2 newhash2
+ set digest [r debug digest-value hash2]
+ assert_equal $digest [r debug digest-value newhash2]
+ assert_equal 1 [r object refcount hash2]
+ assert_equal 1 [r object refcount newhash2]
+ r del hash2
+ assert_equal $digest [r debug digest-value newhash2]
+ r config set hash-max-ziplist-entries $original_max
+ }
+
+ test {COPY basic usage for stream} {
+ r del mystream mynewstream
+ for {set i 0} {$i < 1000} {incr i} {
+ r XADD mystream * item 2 value b
+ }
+ r copy mystream mynewstream
+ set digest [r debug digest-value mystream]
+ assert_equal $digest [r debug digest-value mynewstream]
+ assert_equal 1 [r object refcount mystream]
+ assert_equal 1 [r object refcount mynewstream]
+ r del mystream
+ assert_equal $digest [r debug digest-value mynewstream]
+ }
+
+ test {COPY basic usage for stream-cgroups} {
+ r del x
+ r XADD x 100 a 1
+ set id [r XADD x 101 b 1]
+ r XADD x 102 c 1
+ r XADD x 103 e 1
+ r XADD x 104 f 1
+ r XADD x 105 g 1
+ r XGROUP CREATE x g1 0
+ r XGROUP CREATE x g2 0
+ r XREADGROUP GROUP g1 Alice COUNT 1 STREAMS x >
+ r XREADGROUP GROUP g1 Bob COUNT 1 STREAMS x >
+ r XREADGROUP GROUP g1 Bob NOACK COUNT 1 STREAMS x >
+ r XREADGROUP GROUP g2 Charlie COUNT 4 STREAMS x >
+ r XGROUP SETID x g1 $id
+ r XREADGROUP GROUP g1 Dave COUNT 3 STREAMS x >
+ r XDEL x 103
+
+ r copy x newx
+ set info [r xinfo stream x full]
+ assert_equal $info [r xinfo stream newx full]
+ assert_equal 1 [r object refcount x]
+ assert_equal 1 [r object refcount newx]
+ r del x
+ assert_equal $info [r xinfo stream newx full]
+ r flushdb
+ }
+
test {MOVE basic usage} {
r set mykey foobar
r move mykey 10