diff --git a/src/commands.def b/src/commands.def index c115c5e7093..89e8c4c5646 100644 --- a/src/commands.def +++ b/src/commands.def @@ -4314,6 +4314,8 @@ keySpec HSETEX_Keyspecs[1] = { struct COMMAND_ARG HSETEX_fields_condition_Subargs[] = { {MAKE_ARG("fnx",ARG_TYPE_PURE_TOKEN,-1,"FNX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, {MAKE_ARG("fxx",ARG_TYPE_PURE_TOKEN,-1,"FXX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, +{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,NULL,CMD_ARG_NONE,0,NULL)}, }; /* HSETEX expiration argument table */ @@ -4340,7 +4342,7 @@ struct COMMAND_ARG HSETEX_fields_Subargs[] = { /* HSETEX argument table */ struct COMMAND_ARG HSETEX_Args[] = { {MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)}, -{MAKE_ARG("fields-condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,2,NULL),.subargs=HSETEX_fields_condition_Subargs}, +{MAKE_ARG("fields-condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,4,NULL),.subargs=HSETEX_fields_condition_Subargs}, {MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=HSETEX_expiration_Subargs}, {MAKE_ARG("fields",ARG_TYPE_BLOCK,-1,"FIELDS",NULL,NULL,CMD_ARG_NONE,2,NULL),.subargs=HSETEX_fields_Subargs}, }; diff --git a/src/commands/hsetex.json b/src/commands/hsetex.json index 7e1df6ead0e..3936625ab92 100644 --- a/src/commands/hsetex.json +++ b/src/commands/hsetex.json @@ -66,6 +66,16 @@ "name": "fxx", "type": "pure-token", "token": "FXX" + }, + { + "name": "nx", + "type": "pure-token", + "token": "NX" + }, + { + "name": "xx", + "type": "pure-token", + "token": "XX" } ] }, diff --git a/src/server.c b/src/server.c index da367429d5e..8097cf71907 100644 --- a/src/server.c +++ b/src/server.c @@ -7445,12 +7445,12 @@ int parseExtendedCommandArgumentsOrReply(client *c, int *flags, int *unit, robj /* clang-format off */ if ((opt[0] == 'n' || opt[0] == 'N') && (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' && - !(*flags & ARGS_SET_XX || *flags & ARGS_SET_IFEQ) && (command_type == COMMAND_SET)) + !(*flags & ARGS_SET_XX || *flags & ARGS_SET_IFEQ) && (command_type == COMMAND_SET || command_type == COMMAND_HSET)) { *flags |= ARGS_SET_NX; } else if ((opt[0] == 'x' || opt[0] == 'X') && (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' && - !(*flags & ARGS_SET_NX || *flags & ARGS_SET_IFEQ) && (command_type == COMMAND_SET)) + !(*flags & ARGS_SET_NX || *flags & ARGS_SET_IFEQ) && (command_type == COMMAND_SET || command_type == COMMAND_HSET)) { *flags |= ARGS_SET_XX; } else if ((opt[0] == 'f' || opt[0] == 'F') && diff --git a/src/t_hash.c b/src/t_hash.c index 733dc297d49..fa1c6df8f01 100644 --- a/src/t_hash.c +++ b/src/t_hash.c @@ -1209,6 +1209,17 @@ void hsetexCommand(client *c) { if (checkType(c, o, OBJ_HASH)) return; + /* Check NX/XX key-level conditions before creating a new object */ + if ((flags & ARGS_SET_NX) && o != NULL) { + addReply(c, shared.czero); // NX fails if key exists + return; + } + + if ((flags & ARGS_SET_XX) && o == NULL) { + addReply(c, shared.czero); // XX fails if key does not exist + return; + } + if (o == NULL) { o = createHashObject(); dbAdd(c->db, c->argv[1], &o); diff --git a/tests/unit/hashexpire.tcl b/tests/unit/hashexpire.tcl index 500bf32ea8b..6c273c999eb 100644 --- a/tests/unit/hashexpire.tcl +++ b/tests/unit/hashexpire.tcl @@ -627,7 +627,42 @@ start_server {tags {"hashexpire"}} { assert_error {ERR numfields should be greater than 0 and match the provided number of fields} {r HSETEX myhash PX 100 FIELDS 1 field1 val1 extra} } + ## NX/XX key-level tests + test {HSETEX NX - non-existing key creates the key} { + r FLUSHALL + set res [r HSETEX myhash NX FIELDS 2 f1 v1 f2 v2] + assert_equal 1 $res + assert_equal v1 [r HGET myhash f1] + assert_equal v2 [r HGET myhash f2] + } + + test {HSETEX NX - existing key blocked} { + r FLUSHALL + r HSET myhash f1 v1 + set res [r HSETEX myhash NX FIELDS 2 f1 new1 f2 new2] + assert_equal 0 $res + assert_equal v1 [r HGET myhash f1] + assert_equal 0 [r HEXISTS myhash f2] + } + + test {HSETEX XX - existing key updates fields} { + r FLUSHALL + r HSET myhash f1 v1 f2 v2 + set res [r HSETEX myhash XX FIELDS 2 f1 new1 f2 new2] + assert_equal 1 $res + assert_equal new1 [r HGET myhash f1] + assert_equal new2 [r HGET myhash f2] + } + + test {HSETEX XX - non-existing key blocked} { + r FLUSHALL + set res [r HSETEX myhash XX FIELDS 2 f1 v1 f2 v2] + assert_equal 0 $res + assert_equal 0 [r HEXISTS myhash f1] + assert_equal 0 [r HEXISTS myhash f2] + } + ## FNX/FXX # hsetex throws ERR *, it shouldn't