Skip to content

Commit 56ab3c4

Browse files
roshkhatriranshid
andauthored
Adds HGETDEL Support to Valkey (#2851)
Fixes this: #2850 Adds support for HGETDEL to Valkey and aligns with Redis 8.0 feature. Maintains syntax compatibility Retrieves all the values, and null if fields dont exists and deletes once retrieved. ``` 127.0.0.1:6379> HGETDEL key FIELDS numfields field [ field ... ] ``` ``` 127.0.0.1:6379> HSET foo field1 bar1 field2 bar2 field3 bar3 (integer) 3 127.0.0.1:6379> HGETDEL foo FIELDS 1 field2 1) "bar2" 127.0.0.1:6379> HGETDEL foo FIELDS 1 field2 1) (nil) 127.0.0.1:6379> HGETALL foo 1) "field1" 2) "bar1" 3) "field3" 4) "bar3" 127.0.0.1:6379> HGETDEL foo FIELDS 2 field2 field3 1) (nil) 2) "bar3" 127.0.0.1:6379> HGETALL foo 1) "field1" 2) "bar1" 127.0.0.1:6379> HGETDEL foo FIELDS 3 field1 non-exist-field (error) ERR syntax error 127.0.0.1:6379> HGETDEL foo FIELDS 2 field1 non-exist-field 1) "bar1" 2) (nil) 127.0.0.1:6379> HGETALL foo (empty array) 127.0.0.1:6379> ``` --------- Signed-off-by: Roshan Khatri <[email protected]> Signed-off-by: Ran Shidlansik <[email protected]> Co-authored-by: Ran Shidlansik <[email protected]>
1 parent 9562bdc commit 56ab3c4

File tree

5 files changed

+242
-0
lines changed

5 files changed

+242
-0
lines changed

src/commands.def

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3823,6 +3823,37 @@ struct COMMAND_ARG HGETALL_Args[] = {
38233823
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
38243824
};
38253825

3826+
/********** HGETDEL ********************/
3827+
3828+
#ifndef SKIP_CMD_HISTORY_TABLE
3829+
/* HGETDEL history */
3830+
#define HGETDEL_History NULL
3831+
#endif
3832+
3833+
#ifndef SKIP_CMD_TIPS_TABLE
3834+
/* HGETDEL tips */
3835+
#define HGETDEL_Tips NULL
3836+
#endif
3837+
3838+
#ifndef SKIP_CMD_KEY_SPECS_TABLE
3839+
/* HGETDEL key specs */
3840+
keySpec HGETDEL_Keyspecs[1] = {
3841+
{NULL,CMD_KEY_RW|CMD_KEY_ACCESS|CMD_KEY_DELETE,KSPEC_BS_INDEX,.bs.index={1},KSPEC_FK_RANGE,.fk.range={0,1,0}}
3842+
};
3843+
#endif
3844+
3845+
/* HGETDEL fields argument table */
3846+
struct COMMAND_ARG HGETDEL_fields_Subargs[] = {
3847+
{MAKE_ARG("numfields",ARG_TYPE_INTEGER,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
3848+
{MAKE_ARG("field",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_MULTIPLE,0,NULL)},
3849+
};
3850+
3851+
/* HGETDEL argument table */
3852+
struct COMMAND_ARG HGETDEL_Args[] = {
3853+
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
3854+
{MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HGETDEL_fields_Subargs},
3855+
};
3856+
38263857
/********** HGETEX ********************/
38273858

38283859
#ifndef SKIP_CMD_HISTORY_TABLE
@@ -11800,6 +11831,7 @@ struct COMMAND_STRUCT serverCommandTable[] = {
1180011831
{MAKE_CMD("hexpiretime","Returns Unix timestamps in seconds since the epoch at which the given key's field(s) will expire","O(N) where N is the number of specified fields.","9.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HEXPIRETIME_History,0,HEXPIRETIME_Tips,0,hexpiretimeCommand,-5,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HEXPIRETIME_Keyspecs,1,NULL,2),.args=HEXPIRETIME_Args},
1180111832
{MAKE_CMD("hget","Returns the value of a field in a hash.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGET_History,0,HGET_Tips,0,hgetCommand,3,CMD_READONLY|CMD_FAST,ACL_CATEGORY_HASH,HGET_Keyspecs,1,NULL,2),.args=HGET_Args},
1180211833
{MAKE_CMD("hgetall","Returns all fields and values in a hash.","O(N) where N is the size of the hash.","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETALL_History,0,HGETALL_Tips,1,hgetallCommand,2,CMD_READONLY,ACL_CATEGORY_HASH,HGETALL_Keyspecs,1,NULL,1),.args=HGETALL_Args},
11834+
{MAKE_CMD("hgetdel","Returns the values of one or more fields and deletes them from a hash.","O(N) where N is the number of fields to be retrieved and deleted.","9.1.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETDEL_History,0,HGETDEL_Tips,0,hgetdelCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HGETDEL_Keyspecs,1,NULL,2),.args=HGETDEL_Args},
1180311835
{MAKE_CMD("hgetex","Get the value of one or more fields of a given hash key, and optionally set their expiration time or time-to-live (TTL).","O(N) where N is the number of specified fields.","9.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HGETEX_History,0,HGETEX_Tips,0,hgetexCommand,-5,CMD_WRITE|CMD_FAST,ACL_CATEGORY_HASH,HGETEX_Keyspecs,1,NULL,3),.args=HGETEX_Args},
1180411836
{MAKE_CMD("hincrby","Increments the integer value of a field in a hash by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.0.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBY_History,0,HINCRBY_Tips,0,hincrbyCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBY_Keyspecs,1,NULL,3),.args=HINCRBY_Args},
1180511837
{MAKE_CMD("hincrbyfloat","Increments the floating point value of a field by a number. Uses 0 as initial value if the field doesn't exist.","O(1)","2.6.0",CMD_DOC_NONE,NULL,NULL,"hash",COMMAND_GROUP_HASH,HINCRBYFLOAT_History,0,HINCRBYFLOAT_Tips,0,hincrbyfloatCommand,4,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_HASH,HINCRBYFLOAT_Keyspecs,1,NULL,3),.args=HINCRBYFLOAT_Args},

src/commands/hgetdel.json

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"HGETDEL": {
3+
"summary": "Returns the values of one or more fields and deletes them from a hash.",
4+
"complexity": "O(N) where N is the number of fields to be retrieved and deleted.",
5+
"group": "hash",
6+
"since": "9.1.0",
7+
"arity": -5,
8+
"function": "hgetdelCommand",
9+
"command_flags": [
10+
"WRITE",
11+
"FAST"
12+
],
13+
"acl_categories": [
14+
"HASH"
15+
],
16+
"key_specs": [
17+
{
18+
"flags": [
19+
"RW",
20+
"ACCESS",
21+
"DELETE"
22+
],
23+
"begin_search": {
24+
"index": {
25+
"pos": 1
26+
}
27+
},
28+
"find_keys": {
29+
"range": {
30+
"lastkey": 0,
31+
"step": 1,
32+
"limit": 0
33+
}
34+
}
35+
}
36+
],
37+
"reply_schema": {
38+
"description": "List of values associated with the given fields, in the same order as they are requested. Returns nil for fields that do not exist.",
39+
"type": "array",
40+
"minItems": 1,
41+
"items": {
42+
"oneOf": [
43+
{
44+
"type": "string"
45+
},
46+
{
47+
"type": "null"
48+
}
49+
]
50+
}
51+
},
52+
"arguments": [
53+
{
54+
"name": "key",
55+
"type": "key",
56+
"key_spec_index": 0
57+
},
58+
{
59+
"name": "fields",
60+
"token": "FIELDS",
61+
"type": "block",
62+
"arguments": [
63+
{
64+
"name": "numfields",
65+
"type": "integer",
66+
"key_spec_index": 0,
67+
"multiple": false,
68+
"minimum": 1
69+
},
70+
{
71+
"name": "field",
72+
"type": "string",
73+
"multiple": true
74+
}
75+
]
76+
}
77+
]
78+
}
79+
}

src/server.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3926,6 +3926,7 @@ void hsetnxCommand(client *c);
39263926
void hsetexCommand(client *c);
39273927
void hgetexCommand(client *c);
39283928
void hgetCommand(client *c);
3929+
void hgetdelCommand(client *c);
39293930
void hmgetCommand(client *c);
39303931
void hdelCommand(client *c);
39313932
void hlenCommand(client *c);

src/t_hash.c

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,57 @@ void hdelCommand(client *c) {
10531053
addReplyLongLong(c, deleted);
10541054
}
10551055

1056+
void hgetdelCommand(client *c) {
1057+
/* argv: [0]=HGETDEL, [1]=key, [2]=FIELDS, [3]=numfields, [4...]=fields */
1058+
int fields_index = 4;
1059+
int i, deleted = 0;
1060+
long long num_fields = 0;
1061+
bool keyremoved = false;
1062+
1063+
if (getLongLongFromObjectOrReply(c, c->argv[fields_index - 1], &num_fields, NULL) != C_OK) return;
1064+
1065+
/* Check that the parsed fields number matches the real provided number of fields */
1066+
if (!num_fields || num_fields != (c->argc - fields_index)) {
1067+
addReplyError(c, "numfields should be greater than 0 and match the provided number of fields");
1068+
return;
1069+
}
1070+
1071+
/* Don't abort when the key cannot be found. Non-existing keys are empty
1072+
* hashes, where HGETDEL should respond with a series of null bulks. */
1073+
robj *o = lookupKeyWrite(c->db, c->argv[1]);
1074+
if (checkType(c, o, OBJ_HASH)) return;
1075+
1076+
bool hash_volatile_items = hashTypeHasVolatileFields(o);
1077+
1078+
/* Reply with array of values and delete at the same time */
1079+
addReplyArrayLen(c, num_fields);
1080+
for (i = fields_index; i < c->argc; i++) {
1081+
addHashFieldToReply(c, o, c->argv[i]->ptr);
1082+
1083+
/* If hash doesn't exist, continue as already replied with NULL */
1084+
if (o == NULL) continue;
1085+
if (hashTypeDelete(o, c->argv[i]->ptr)) {
1086+
deleted++;
1087+
if (hashTypeLength(o) == 0) {
1088+
if (hash_volatile_items) dbUntrackKeyWithVolatileItems(c->db, o);
1089+
dbDelete(c->db, c->argv[1]);
1090+
keyremoved = true;
1091+
o = NULL;
1092+
}
1093+
}
1094+
}
1095+
1096+
if (deleted) {
1097+
if (!keyremoved && hash_volatile_items != hashTypeHasVolatileFields(o)) {
1098+
dbUpdateObjectWithVolatileItemsTracking(c->db, o);
1099+
}
1100+
signalModifiedKey(c, c->db, c->argv[1]);
1101+
notifyKeyspaceEvent(NOTIFY_HASH, "hdel", c->argv[1], c->db->id);
1102+
if (keyremoved) notifyKeyspaceEvent(NOTIFY_GENERIC, "del", c->argv[1], c->db->id);
1103+
server.dirty += deleted;
1104+
}
1105+
}
1106+
10561107
void hlenCommand(client *c) {
10571108
robj *o;
10581109

tests/unit/type/hash.tcl

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,85 @@ start_server {tags {"hash"}} {
439439
r hgetall htest
440440
} {}
441441

442+
test {HGETDEL - single field} {
443+
r del myhash
444+
r hset myhash field1 value1
445+
set rv {}
446+
lappend rv [r hgetdel myhash FIELDS 1 field1]
447+
lappend rv [r hexists myhash field1]
448+
lappend rv [r exists myhash]
449+
set _ $rv
450+
} {value1 0 0}
451+
452+
test {HGETDEL - multiple fields} {
453+
r del myhash
454+
r hmset myhash field1 value1 field2 value2 field3 value3
455+
set rv {}
456+
lappend rv [r hgetdel myhash FIELDS 2 field1 field3]
457+
lappend rv [r hexists myhash field1]
458+
lappend rv [r hexists myhash field2]
459+
lappend rv [r hexists myhash field3]
460+
lappend rv [r hget myhash field2]
461+
set _ $rv
462+
} {{value1 value3} 0 1 0 value2}
463+
464+
test {HGETDEL - non-existing field} {
465+
r del myhash
466+
r hset myhash field1 value1
467+
set rv {}
468+
lappend rv [r hgetdel myhash FIELDS 1 nonexisting]
469+
lappend rv [r hexists myhash field1]
470+
set _ $rv
471+
} {{{}} 1}
472+
473+
test {HGETDEL - non-existing key and hash after the key is deleted } {
474+
r del myhash
475+
r hset myhash field1 value1
476+
assert_equal {value1 {}} [r hgetdel myhash FIELDS 2 field1 field2]
477+
}
478+
479+
test {HGETDEL - non-existing key} {
480+
r del myhash
481+
assert_equal {{}} [r hgetdel myhash FIELDS 1 field1]
482+
}
483+
484+
test {HGETDEL - mix of existing and non-existing fields} {
485+
r del myhash
486+
r hmset myhash a 1 b 2 c 3
487+
set rv {}
488+
lappend rv [r hgetdel myhash FIELDS 3 a nonexist b]
489+
lappend rv [r hexists myhash a]
490+
lappend rv [r hexists myhash b]
491+
lappend rv [r hexists myhash c]
492+
set _ $rv
493+
} {{1 {} 2} 0 0 1}
494+
495+
test {HGETDEL - hash becomes empty after deletion} {
496+
r del myhash
497+
r hmset myhash a 1 b 2
498+
set rv {}
499+
lappend rv [r hgetdel myhash FIELDS 2 a b]
500+
lappend rv [r exists myhash]
501+
set _ $rv
502+
} {{1 2} 0}
503+
504+
test {HGETDEL - wrong type} {
505+
r del wrongtype
506+
r set wrongtype somevalue
507+
assert_error "*WRONGTYPE*" {r hgetdel wrongtype FIELDS 1 field1}
508+
}
509+
510+
test {HGETDEL - wrong number of arguments} {
511+
assert_error "*wrong number of arguments*" {r hgetdel myhash}
512+
}
513+
514+
test {HGETDEL - check for syntax and type errors} {
515+
assert_error "*value is not an integer or out of range" {r hgetdel myhash a b c}
516+
assert_error "*value is not an integer or out of range" {r hgetdel myhash FIELDS a b c}
517+
assert_error "*numfields should be greater than 0 and match the provided number of fields" {r hgetdel myhash FIELDS 2 a b c}
518+
assert_error "*numfields should be greater than 0 and match the provided number of fields" {r hgetdel myhash FIELDS 4 a b c}
519+
}
520+
442521
test {HDEL and return value} {
443522
set rv {}
444523
lappend rv [r hdel smallhash nokey]

0 commit comments

Comments
 (0)