diff --git a/src/help.h b/src/help.h index cb12f4c8d29..1898c0c0002 100644 --- a/src/help.h +++ b/src/help.h @@ -214,6 +214,16 @@ struct commandHelp { "Set the string value of a key and return its old value", 1, "1.0.0" }, + { "HAPPEND", + "key field value", + "Append a value to a hash field", + 5, + "2.8.0" }, + { "HAPPENDX", + "key field value", + "Append a value to a hash field, only if the key exists", + 5, + "2.8.0" }, { "HDEL", "key field [field ...]", "Delete one or more hash fields", diff --git a/src/redis.c b/src/redis.c index 0acf58aaad1..366a1a537ed 100644 --- a/src/redis.c +++ b/src/redis.c @@ -193,6 +193,8 @@ struct redisCommand redisCommandTable[] = { {"hvals",hvalsCommand,2,"rS",0,NULL,1,1,1,0,0}, {"hgetall",hgetallCommand,2,"r",0,NULL,1,1,1,0,0}, {"hexists",hexistsCommand,3,"r",0,NULL,1,1,1,0,0}, + {"happend",happendCommand,4,"wm",0,NULL,1,1,1,0,0}, + {"happendx",happendxCommand,4,"wm",0,NULL,1,1,1,0,0}, {"incrby",incrbyCommand,3,"wm",0,NULL,1,1,1,0,0}, {"decrby",decrbyCommand,3,"wm",0,NULL,1,1,1,0,0}, {"incrbyfloat",incrbyfloatCommand,3,"wm",0,NULL,1,1,1,0,0}, diff --git a/src/redis.h b/src/redis.h index 774ed30bad7..dfbe90c1efd 100644 --- a/src/redis.h +++ b/src/redis.h @@ -1438,6 +1438,8 @@ void zinterstoreCommand(redisClient *c); void hkeysCommand(redisClient *c); void hvalsCommand(redisClient *c); void hgetallCommand(redisClient *c); +void happendCommand(redisClient *c); +void happendxCommand(redisClient *c); void hexistsCommand(redisClient *c); void configCommand(redisClient *c); void hincrbyCommand(redisClient *c); diff --git a/src/t_hash.c b/src/t_hash.c index 9484e531be0..0b419932534 100644 --- a/src/t_hash.c +++ b/src/t_hash.c @@ -163,6 +163,88 @@ int hashTypeExists(robj *o, robj *field) { return 0; } +/* Append to an element. + * Return total length of appended string. + * This function will take care of incrementing the reference count of the + * retained fields and value objects. */ +size_t hashTypeAppend(robj *o, robj *field, robj *append) { + int update = 0; + long long totlen = 0; + robj *value; + + if (o->encoding == REDIS_ENCODING_ZIPLIST) { + unsigned char *zl, *fptr, *vptr; + unsigned char *vstr = NULL; + unsigned int vlen = UINT_MAX; + long long vll = LLONG_MAX; + int ret; + + field = getDecodedObject(field); + append = getDecodedObject(append); + + zl = o->ptr; + fptr = ziplistIndex(zl, ZIPLIST_HEAD); + if (fptr != NULL) { + fptr = ziplistFind(fptr, field->ptr, sdslen(field->ptr), 1); + if (fptr != NULL) { + /* Grab pointer to the value (fptr points to the field) */ + vptr = ziplistNext(zl, fptr); + redisAssert(vptr != NULL); + update = 1; + + /* Append to current string */ + ret = ziplistGet(vptr, &vstr, &vlen, &vll); + redisAssert(ret); + value = createStringObject((char *)vstr, (size_t)vlen); + value->ptr = sdscatlen(value->ptr, append->ptr, sdslen(append->ptr)); + + /* Delete value */ + zl = ziplistDelete(zl, &vptr); + + /* Insert new value */ + zl = ziplistInsert(zl, vptr, value->ptr, sdslen(value->ptr)); + totlen = sdslen(value->ptr); + decrRefCount(value); + } + } + + if (!update) { + /* Push new field/value pair onto the tail of the ziplist */ + value = append; + zl = ziplistPush(zl, field->ptr, sdslen(field->ptr), ZIPLIST_TAIL); + zl = ziplistPush(zl, value->ptr, sdslen(value->ptr), ZIPLIST_TAIL); + totlen = sdslen(value->ptr); + } + + o->ptr = zl; + decrRefCount(field); + decrRefCount(append); + + /* Check if the ziplist needs to be converted to a hash table */ + if (hashTypeLength(o) > server.hash_max_ziplist_entries || + totlen > server.hash_max_ziplist_value) + hashTypeConvert(o, REDIS_ENCODING_HT); + } else if (o->encoding == REDIS_ENCODING_HT) { + /* Append to current string */ + if ((value = hashTypeGetObject(o,field)) != NULL) { + value->ptr = sdscatlen(value->ptr,append->ptr,sdslen(append->ptr)); + } else { + value = append; + incrRefCount(value); + } + + if (dictReplace(o->ptr, field, value)) { /* Insert */ + incrRefCount(field); + } else { /* Update */ + update = 1; + } + totlen = sdslen(value->ptr); + } else { + redisPanic("Unknown hash encoding"); + } + return totlen; +} + /* Add an element, discard the old if the key already exists. * Return 0 on insert and 1 on update. * This function will take care of incrementing the reference count of the @@ -752,6 +834,34 @@ void hgetallCommand(redisClient *c) { genericHgetallCommand(c,REDIS_HASH_KEY|REDIS_HASH_VALUE); } +void genericHappendCommand(redisClient *c, int nx) { + size_t totlen; + robj *o; + + if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return; + hashTypeTryConversion(o,c->argv,2,3); + + if (!nx && !hashTypeExists(o, c->argv[2])) { + addReply(c, shared.czero); + return; + } else { + hashTypeTryObjectEncoding(o,&c->argv[2],NULL); + totlen = hashTypeAppend(o,c->argv[2],c->argv[3]); + addReplyLongLong(c, totlen); + signalModifiedKey(c->db,c->argv[1]); + notifyKeyspaceEvent(REDIS_NOTIFY_HASH,"happend",c->argv[1],c->db->id); + server.dirty++; + } +} + +void happendxCommand(redisClient *c) { + genericHappendCommand(c,0); +} + +void happendCommand(redisClient *c) { + genericHappendCommand(c,1); +} + void hexistsCommand(redisClient *c) { robj *o; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL || diff --git a/tests/unit/type/hash.tcl b/tests/unit/type/hash.tcl index fa52afd167a..e59b2d27c39 100644 --- a/tests/unit/type/hash.tcl +++ b/tests/unit/type/hash.tcl @@ -107,6 +107,78 @@ start_server {tags {"hash"}} { set _ $result } {foo} + test {HAPPEND target key missing - small hash} { + set rv {} + lappend rv [r happend smallhash __123123123__ foo] + lappend rv [r hget smallhash __123123123__] + r hdel smallhash __123123123__ + set _ $rv + } {3 foo} + + test {HAPPEND target key exists - small hash} { + set rv {} + r hset smallhash __123123123__ foo + lappend rv [r happend smallhash __123123123__ bar] + lappend rv [r hget smallhash __123123123__] + r hdel smallhash __123123123__ + set _ $rv + } {6 foobar} + + test {HAPPEND target key missing - big hash} { + set rv {} + lappend rv [r happend bighash __123123123__ foo] + lappend rv [r hget bighash __123123123__] + set _ $rv + } {3 foo} + + test {HAPPEND target key exists - big hash} { + set rv {} + r hset bighash __123123123__ foo + lappend rv [r happend bighash __123123123__ bar] + lappend rv [r hget bighash __123123123__] + r hdel bighash __123123123__ + set _ $rv + } {6 foobar} + + test {HAPPEND Is a ziplist encoded Hash promoted on big payload?} { + r hset myhash foo bar + assert_encoding ziplist myhash + r happend myhash foo [string repeat a 1024] + r debug object myhash + } {*hashtable*} + + test {HAPPENDX target key missing - small hash} { + set rv {} + lappend rv [r happendx smallhash __123123123__ foo] + lappend rv [r hget smallhash __123123123__] + set _ $rv + } {0 {}} + + test {HAPPENDX target key exists - small hash} { + set rv {} + r hset smallhash __123123123__ foo + lappend rv [r happendx smallhash __123123123__ bar] + lappend rv [r hget smallhash __123123123__] + r hdel smallhash __123123123__ + set _ $rv + } {6 foobar} + + test {HAPPENDX target key missing - big hash} { + set rv {} + lappend rv [r happendx bighash __123123123__ foo] + lappend rv [r hget bighash __123123123__] + set _ $rv + } {0 {}} + + test {HAPPENDX target key exists - big hash} { + set rv {} + r hset bighash __123123123__ foo + lappend rv [r happendx bighash __123123123__ bar] + lappend rv [r hget bighash __123123123__] + r hdel bighash __123123123__ + set _ $rv + } {6 foobar} + test {HMSET wrong number of args} { catch {r hmset smallhash key1 val1 key2} err format $err