summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xruntest-moduleapi1
-rw-r--r--src/module.c50
-rw-r--r--src/redismodule.h1
-rw-r--r--tests/modules/Makefile1
-rw-r--r--tests/modules/hash.c90
-rw-r--r--tests/unit/moduleapi/hash.tcl23
6 files changed, 155 insertions, 11 deletions
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 <strings.h>
+#include <errno.h>
+#include <stdlib.h>
+
+/* 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}
+}