diff --git a/runtest-moduleapi b/runtest-moduleapi index 53656fad7..7c17501e0 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -32,6 +32,7 @@ $TCLSH tests/test_helper.tcl \ --single unit/moduleapi/getkeys \ --single unit/moduleapi/test_lazyfree \ --single unit/moduleapi/defrag \ +--single unit/moduleapi/hash \ --single unit/moduleapi/zset \ --single unit/moduleapi/stream \ "${@}" diff --git a/src/module.c b/src/module.c index e7f0cde26..f8d3e3170 100644 --- a/src/module.c +++ b/src/module.c @@ -2976,6 +2976,10 @@ int RM_ZsetRangePrev(RedisModuleKey *key) { * are created. * REDISMODULE_HASH_CFIELDS: The field names passed are null terminated C * strings instead of RedisModuleString objects. + * REDISMODULE_HASH_COUNT_ALL: Include the number of inserted fields in the + * returned number, in addition to the number of + * updated and deleted fields. (Added in Redis + * 6.2.) * * Unless NX is specified, the command overwrites the old field value with * the new one. @@ -2989,21 +2993,43 @@ int RM_ZsetRangePrev(RedisModuleKey *key) { * * Return value: * - * The number of fields updated (that may be less than the number of fields - * specified because of the XX or NX options). + * The number of fields existing in the hash prior to the call, which have been + * updated (its old value has been replaced by a new value) or deleted. If the + * flag REDISMODULE_HASH_COUNT_ALL is set, insterted fields not previously + * existing in the hash are also counted. * - * In the following case the return value is always zero: + * If the return value is zero, `errno` is set (since Redis 6.2) as follows: * - * * The key was not open for writing. - * * The key was associated with a non Hash value. + * - EINVAL if any unknown flags are set or if key is NULL. + * - ENOTSUP if the key is associated with a non Hash value. + * - EBADF if the key was not opened for writing. + * - ENOENT if no fields were counted as described under Return value above. + * This is not actually an error. The return value can be zero if all fields + * were just created and the COUNT_ALL flag was unset, or if changes were held + * back due to the NX and XX flags. + * + * NOTICE: The return value semantics of this function are very different + * between Redis 6.2 and older versions. Modules that use it should determine + * the Redis version and handle it accordingly. */ int RM_HashSet(RedisModuleKey *key, int flags, ...) { va_list ap; - if (!(key->mode & REDISMODULE_WRITE)) return 0; - if (key->value && key->value->type != OBJ_HASH) return 0; + if (!key || (flags & ~(REDISMODULE_HASH_NX | + REDISMODULE_HASH_XX | + REDISMODULE_HASH_CFIELDS | + REDISMODULE_HASH_COUNT_ALL))) { + errno = EINVAL; + return 0; + } else if (key->value && key->value->type != OBJ_HASH) { + errno = ENOTSUP; + return 0; + } else if (!(key->mode & REDISMODULE_WRITE)) { + errno = EBADF; + return 0; + } if (key->value == NULL) moduleCreateEmptyKey(key,REDISMODULE_KEYTYPE_HASH); - int updated = 0; + int count = 0; va_start(ap, flags); while(1) { RedisModuleString *field, *value; @@ -3031,7 +3057,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { /* Handle deletion if value is REDISMODULE_HASH_DELETE. */ if (value == REDISMODULE_HASH_DELETE) { - updated += hashTypeDelete(key->value, field->ptr); + count += hashTypeDelete(key->value, field->ptr); if (flags & REDISMODULE_HASH_CFIELDS) decrRefCount(field); continue; } @@ -3045,7 +3071,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { robj *argv[2] = {field,value}; hashTypeTryConversion(key->value,argv,0,1); - updated += hashTypeSet(key->value, field->ptr, value->ptr, low_flags); + int updated = hashTypeSet(key->value, field->ptr, value->ptr, low_flags); + count += (flags & REDISMODULE_HASH_COUNT_ALL) ? 1 : updated; /* If CFIELDS is active, SDS string ownership is now of hashTypeSet(), * however we still have to release the 'field' object shell. */ @@ -3056,7 +3083,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) { } va_end(ap); moduleDelKeyIfEmpty(key); - return updated; + if (count == 0) errno = ENOENT; + return count; } /* Get fields from an hash value. This function is called using a variable diff --git a/src/redismodule.h b/src/redismodule.h index 9d8c6c5ea..60a152452 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -68,6 +68,7 @@ #define REDISMODULE_HASH_XX (1<<1) #define REDISMODULE_HASH_CFIELDS (1<<2) #define REDISMODULE_HASH_EXISTS (1<<3) +#define REDISMODULE_HASH_COUNT_ALL (1<<4) /* StreamID type. */ typedef struct RedisModuleStreamID { diff --git a/tests/modules/Makefile b/tests/modules/Makefile index 8ea1d91a2..f56313964 100644 --- a/tests/modules/Makefile +++ b/tests/modules/Makefile @@ -35,6 +35,7 @@ TEST_MODULES = \ test_lazyfree.so \ timer.so \ defragtest.so \ + hash.so \ zset.so \ stream.so diff --git a/tests/modules/hash.c b/tests/modules/hash.c new file mode 100644 index 000000000..05ab03800 --- /dev/null +++ b/tests/modules/hash.c @@ -0,0 +1,90 @@ +#include "redismodule.h" +#include +#include +#include + +/* If a string is ":deleted:", the special value for deleted hash fields is + * returned; otherwise the input string is returned. */ +static RedisModuleString *value_or_delete(RedisModuleString *s) { + if (!strcasecmp(RedisModule_StringPtrLen(s, NULL), ":delete:")) + return REDISMODULE_HASH_DELETE; + else + return s; +} + +/* HASH.SET key flags field1 value1 [field2 value2 ..] + * + * Sets 1-4 fields. Returns the same as RedisModule_HashSet(). + * Flags is a string of "nxa" where n = NX, x = XX, a = COUNT_ALL. + * To delete a field, use the value ":delete:". + */ +int hash_set(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + if (argc < 5 || argc % 2 == 0 || argc > 11) + return RedisModule_WrongArity(ctx); + + RedisModule_AutoMemory(ctx); + RedisModuleKey *key = RedisModule_OpenKey(ctx, argv[1], REDISMODULE_WRITE); + + size_t flags_len; + const char *flags_str = RedisModule_StringPtrLen(argv[2], &flags_len); + int flags = REDISMODULE_HASH_NONE; + for (size_t i = 0; i < flags_len; i++) { + switch (flags_str[i]) { + case 'n': flags |= REDISMODULE_HASH_NX; break; + case 'x': flags |= REDISMODULE_HASH_XX; break; + case 'a': flags |= REDISMODULE_HASH_COUNT_ALL; break; + } + } + + /* Test some varargs. (In real-world, use a loop and set one at a time.) */ + int result; + errno = 0; + if (argc == 5) { + result = RedisModule_HashSet(key, flags, + argv[3], value_or_delete(argv[4]), + NULL); + } else if (argc == 7) { + result = RedisModule_HashSet(key, flags, + argv[3], value_or_delete(argv[4]), + argv[5], value_or_delete(argv[6]), + NULL); + } else if (argc == 9) { + result = RedisModule_HashSet(key, flags, + argv[3], value_or_delete(argv[4]), + argv[5], value_or_delete(argv[6]), + argv[7], value_or_delete(argv[8]), + NULL); + } else if (argc == 11) { + result = RedisModule_HashSet(key, flags, + argv[3], value_or_delete(argv[4]), + argv[5], value_or_delete(argv[6]), + argv[7], value_or_delete(argv[8]), + argv[9], value_or_delete(argv[10]), + NULL); + } else { + return RedisModule_ReplyWithError(ctx, "ERR too many fields"); + } + + /* Check errno */ + if (result == 0) { + if (errno == ENOTSUP) + return RedisModule_ReplyWithError(ctx, REDISMODULE_ERRORMSG_WRONGTYPE); + else + RedisModule_Assert(errno == ENOENT); + } + + return RedisModule_ReplyWithLongLong(ctx, result); +} + +int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + if (RedisModule_Init(ctx, "hash", 1, REDISMODULE_APIVER_1) == + REDISMODULE_OK && + RedisModule_CreateCommand(ctx, "hash.set", hash_set, "", + 1, 1, 1) == REDISMODULE_OK) { + return REDISMODULE_OK; + } else { + return REDISMODULE_ERR; + } +} diff --git a/tests/unit/moduleapi/hash.tcl b/tests/unit/moduleapi/hash.tcl new file mode 100644 index 000000000..89bb6c63a --- /dev/null +++ b/tests/unit/moduleapi/hash.tcl @@ -0,0 +1,23 @@ +set testmodule [file normalize tests/modules/hash.so] + +start_server {tags {"modules"}} { + r module load $testmodule + + test {Module hash set} { + r set k mystring + assert_error "WRONGTYPE*" {r hash.set k "" hello world} + r del k + # "" = count updates and deletes of existing fields only + assert_equal 0 [r hash.set k "" squirrel yes] + # "a" = COUNT_ALL = count inserted, modified and deleted fields + assert_equal 2 [r hash.set k "a" banana no sushi whynot] + # "n" = NX = only add fields not already existing in the hash + # "x" = XX = only replace the value for existing fields + assert_equal 0 [r hash.set k "n" squirrel hoho what nothing] + assert_equal 1 [r hash.set k "na" squirrel hoho something nice] + assert_equal 0 [r hash.set k "xa" new stuff not inserted] + assert_equal 1 [r hash.set k "x" squirrel ofcourse] + assert_equal 1 [r hash.set k "" sushi :delete: none :delete:] + r hgetall k + } {squirrel ofcourse banana no what nothing something nice} +}