Modules: In RM_HashSet, add COUNT_ALL flag and set errno (#8446)
The added flag affects the return value of RM_HashSet() to include the number of inserted fields, in addition to updated and deleted fields. errno is set on errors, tests are added and documentation updated.
This commit is contained in:
parent
05d2b269a9
commit
8a002f470a
@ -32,6 +32,7 @@ $TCLSH tests/test_helper.tcl \
|
|||||||
--single unit/moduleapi/getkeys \
|
--single unit/moduleapi/getkeys \
|
||||||
--single unit/moduleapi/test_lazyfree \
|
--single unit/moduleapi/test_lazyfree \
|
||||||
--single unit/moduleapi/defrag \
|
--single unit/moduleapi/defrag \
|
||||||
|
--single unit/moduleapi/hash \
|
||||||
--single unit/moduleapi/zset \
|
--single unit/moduleapi/zset \
|
||||||
--single unit/moduleapi/stream \
|
--single unit/moduleapi/stream \
|
||||||
"${@}"
|
"${@}"
|
||||||
|
50
src/module.c
50
src/module.c
@ -2976,6 +2976,10 @@ int RM_ZsetRangePrev(RedisModuleKey *key) {
|
|||||||
* are created.
|
* are created.
|
||||||
* REDISMODULE_HASH_CFIELDS: The field names passed are null terminated C
|
* REDISMODULE_HASH_CFIELDS: The field names passed are null terminated C
|
||||||
* strings instead of RedisModuleString objects.
|
* 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
|
* Unless NX is specified, the command overwrites the old field value with
|
||||||
* the new one.
|
* the new one.
|
||||||
@ -2989,21 +2993,43 @@ int RM_ZsetRangePrev(RedisModuleKey *key) {
|
|||||||
*
|
*
|
||||||
* Return value:
|
* Return value:
|
||||||
*
|
*
|
||||||
* The number of fields updated (that may be less than the number of fields
|
* The number of fields existing in the hash prior to the call, which have been
|
||||||
* specified because of the XX or NX options).
|
* 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.
|
* - EINVAL if any unknown flags are set or if key is NULL.
|
||||||
* * The key was associated with a non Hash value.
|
* - 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, ...) {
|
int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
||||||
va_list ap;
|
va_list ap;
|
||||||
if (!(key->mode & REDISMODULE_WRITE)) return 0;
|
if (!key || (flags & ~(REDISMODULE_HASH_NX |
|
||||||
if (key->value && key->value->type != OBJ_HASH) return 0;
|
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);
|
if (key->value == NULL) moduleCreateEmptyKey(key,REDISMODULE_KEYTYPE_HASH);
|
||||||
|
|
||||||
int updated = 0;
|
int count = 0;
|
||||||
va_start(ap, flags);
|
va_start(ap, flags);
|
||||||
while(1) {
|
while(1) {
|
||||||
RedisModuleString *field, *value;
|
RedisModuleString *field, *value;
|
||||||
@ -3031,7 +3057,7 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
|||||||
|
|
||||||
/* Handle deletion if value is REDISMODULE_HASH_DELETE. */
|
/* Handle deletion if value is REDISMODULE_HASH_DELETE. */
|
||||||
if (value == 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);
|
if (flags & REDISMODULE_HASH_CFIELDS) decrRefCount(field);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -3045,7 +3071,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
|||||||
|
|
||||||
robj *argv[2] = {field,value};
|
robj *argv[2] = {field,value};
|
||||||
hashTypeTryConversion(key->value,argv,0,1);
|
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(),
|
/* If CFIELDS is active, SDS string ownership is now of hashTypeSet(),
|
||||||
* however we still have to release the 'field' object shell. */
|
* however we still have to release the 'field' object shell. */
|
||||||
@ -3056,7 +3083,8 @@ int RM_HashSet(RedisModuleKey *key, int flags, ...) {
|
|||||||
}
|
}
|
||||||
va_end(ap);
|
va_end(ap);
|
||||||
moduleDelKeyIfEmpty(key);
|
moduleDelKeyIfEmpty(key);
|
||||||
return updated;
|
if (count == 0) errno = ENOENT;
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Get fields from an hash value. This function is called using a variable
|
/* Get fields from an hash value. This function is called using a variable
|
||||||
|
@ -68,6 +68,7 @@
|
|||||||
#define REDISMODULE_HASH_XX (1<<1)
|
#define REDISMODULE_HASH_XX (1<<1)
|
||||||
#define REDISMODULE_HASH_CFIELDS (1<<2)
|
#define REDISMODULE_HASH_CFIELDS (1<<2)
|
||||||
#define REDISMODULE_HASH_EXISTS (1<<3)
|
#define REDISMODULE_HASH_EXISTS (1<<3)
|
||||||
|
#define REDISMODULE_HASH_COUNT_ALL (1<<4)
|
||||||
|
|
||||||
/* StreamID type. */
|
/* StreamID type. */
|
||||||
typedef struct RedisModuleStreamID {
|
typedef struct RedisModuleStreamID {
|
||||||
|
@ -35,6 +35,7 @@ TEST_MODULES = \
|
|||||||
test_lazyfree.so \
|
test_lazyfree.so \
|
||||||
timer.so \
|
timer.so \
|
||||||
defragtest.so \
|
defragtest.so \
|
||||||
|
hash.so \
|
||||||
zset.so \
|
zset.so \
|
||||||
stream.so
|
stream.so
|
||||||
|
|
||||||
|
90
tests/modules/hash.c
Normal file
90
tests/modules/hash.c
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
23
tests/unit/moduleapi/hash.tcl
Normal file
23
tests/unit/moduleapi/hash.tcl
Normal file
@ -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}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user