Skip to content

Commit

Permalink
Set Command with IFEQ Support (valkey-io#1324)
Browse files Browse the repository at this point in the history
This PR allows the Valkey users to perform conditional updates where the
SET command is completed if the given comparison-value matches the key’s
current value.

Syntax:

```
SET key value IFEQ comparison-value
```

Behavior:

If the values match, the SET completes as expected. If they do not
match, the command returns a (nil), except if the GET argument is also
given (see below).

Behavior with Additional Flags:

1. ```SET key value IFEQ comparison-value GET``` returns the existing
value, regardless of whether it matches comparison-value or not. The
conditional set operation is performed if the given comparison value
matches the existing value. To check if the SET succeeded, the caller
needs to check if the returned string matches the comparison-value.
2. ```SET key value IFEQ comparison-value XX``` is a syntax error.
3.  ```SET key value IFEQ comparison-value NX``` is a syntax error.

Closes: valkey-io#1215

---------

Signed-off-by: Sarthak Aggarwal <[email protected]>
  • Loading branch information
sarthakaggarwal97 authored and vudiep411 committed Dec 15, 2024
1 parent 38c1603 commit 4fe5edf
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 41 deletions.
10 changes: 6 additions & 4 deletions src/commands.def
Original file line number Diff line number Diff line change
Expand Up @@ -10632,6 +10632,7 @@ commandHistory SET_History[] = {
{"6.0.0","Added the `KEEPTTL` option."},
{"6.2.0","Added the `GET`, `EXAT` and `PXAT` option."},
{"7.0.0","Allowed the `NX` and `GET` options to be used together."},
{"8.1.0","Added the `IFEQ` option."},
};
#endif

Expand All @@ -10649,8 +10650,9 @@ keySpec SET_Keyspecs[1] = {

/* SET condition argument table */
struct COMMAND_ARG SET_condition_Subargs[] = {
{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)},
{MAKE_ARG("nx",ARG_TYPE_PURE_TOKEN,-1,"NX",NULL,"2.6.12",CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("xx",ARG_TYPE_PURE_TOKEN,-1,"XX",NULL,"2.6.12",CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("comparison-value",ARG_TYPE_STRING,-1,"IFEQ","Sets the key's value only if the current value matches the specified comparison value.","8.1.0",CMD_ARG_NONE,0,NULL)},
};

/* SET expiration argument table */
Expand All @@ -10666,7 +10668,7 @@ struct COMMAND_ARG SET_expiration_Subargs[] = {
struct COMMAND_ARG SET_Args[] = {
{MAKE_ARG("key",ARG_TYPE_KEY,0,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("value",ARG_TYPE_STRING,-1,NULL,NULL,NULL,CMD_ARG_NONE,0,NULL)},
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,"2.6.12",CMD_ARG_OPTIONAL,2,NULL),.subargs=SET_condition_Subargs},
{MAKE_ARG("condition",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,3,NULL),.subargs=SET_condition_Subargs},
{MAKE_ARG("get",ARG_TYPE_PURE_TOKEN,-1,"GET",NULL,"6.2.0",CMD_ARG_OPTIONAL,0,NULL)},
{MAKE_ARG("expiration",ARG_TYPE_ONEOF,-1,NULL,NULL,NULL,CMD_ARG_OPTIONAL,5,NULL),.subargs=SET_expiration_Subargs},
};
Expand Down Expand Up @@ -11139,7 +11141,7 @@ struct COMMAND_STRUCT serverCommandTable[] = {
{MAKE_CMD("mset","Atomically creates or modifies the string values of one or more keys.","O(N) where N is the number of keys to set.","1.0.1",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MSET_History,0,MSET_Tips,2,msetCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,MSET_Keyspecs,1,NULL,1),.args=MSET_Args},
{MAKE_CMD("msetnx","Atomically modifies the string values of one or more keys only when all keys don't exist.","O(N) where N is the number of keys to set.","1.0.1",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,MSETNX_History,0,MSETNX_Tips,0,msetnxCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,MSETNX_Keyspecs,1,NULL,1),.args=MSETNX_Args},
{MAKE_CMD("psetex","Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn't exist.","O(1)","2.6.0",CMD_DOC_DEPRECATED,"`SET` with the `PX` argument","2.6.12","string",COMMAND_GROUP_STRING,PSETEX_History,0,PSETEX_Tips,0,psetexCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,PSETEX_Keyspecs,1,NULL,3),.args=PSETEX_Args},
{MAKE_CMD("set","Sets the string value of a key, ignoring its type. The key is created if it doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SET_History,4,SET_Tips,0,setCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SET_Keyspecs,1,setGetKeys,5),.args=SET_Args},
{MAKE_CMD("set","Sets the string value of a key, ignoring its type. The key is created if it doesn't exist.","O(1)","1.0.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SET_History,5,SET_Tips,0,setCommand,-3,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SET_Keyspecs,1,setGetKeys,5),.args=SET_Args},
{MAKE_CMD("setex","Sets the string value and expiration time of a key. Creates the key if it doesn't exist.","O(1)","2.0.0",CMD_DOC_DEPRECATED,"`SET` with the `EX` argument","2.6.12","string",COMMAND_GROUP_STRING,SETEX_History,0,SETEX_Tips,0,setexCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SETEX_Keyspecs,1,NULL,3),.args=SETEX_Args},
{MAKE_CMD("setnx","Set the string value of a key only when the key doesn't exist.","O(1)","1.0.0",CMD_DOC_DEPRECATED,"`SET` with the `NX` argument","2.6.12","string",COMMAND_GROUP_STRING,SETNX_History,0,SETNX_Tips,0,setnxCommand,3,CMD_WRITE|CMD_DENYOOM|CMD_FAST,ACL_CATEGORY_STRING,SETNX_Keyspecs,1,NULL,2),.args=SETNX_Args},
{MAKE_CMD("setrange","Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist.","O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument.","2.2.0",CMD_DOC_NONE,NULL,NULL,"string",COMMAND_GROUP_STRING,SETRANGE_History,0,SETRANGE_Tips,0,setrangeCommand,4,CMD_WRITE|CMD_DENYOOM,ACL_CATEGORY_STRING,SETRANGE_Keyspecs,1,NULL,3),.args=SETRANGE_Args},
Expand Down
25 changes: 22 additions & 3 deletions src/commands/set.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
[
"7.0.0",
"Allowed the `NX` and `GET` options to be used together."
],
[
"8.1.0",
"Added the `IFEQ` option."
]
],
"command_flags": [
Expand Down Expand Up @@ -89,17 +93,32 @@
"name": "condition",
"type": "oneof",
"optional": true,
"since": "2.6.12",
"arguments": [
{
"name": "nx",
"type": "pure-token",
"token": "NX"
"token": "NX",
"since": "2.6.12"
},
{
"name": "xx",
"type": "pure-token",
"token": "XX"
"token": "XX",
"since": "2.6.12"
},
{
"name": "comparison-value",
"type": "string",
"token": "IFEQ",
"since": "8.1.0",
"summary": "Sets the key's value only if the current value matches the specified comparison value.",
"arguments": [
{
"name": "comparison-value",
"type": "string",
"summary": "The value to compare with the current key's value before setting."
}
]
}
]
},
Expand Down
79 changes: 56 additions & 23 deletions src/t_string.c
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,16 @@ static int checkStringLength(client *c, long long size, long long append) {
* If abort_reply is NULL, "$-1" is used. */

#define OBJ_NO_FLAGS 0
#define OBJ_SET_NX (1 << 0) /* Set if key not exists. */
#define OBJ_SET_XX (1 << 1) /* Set if key exists. */
#define OBJ_EX (1 << 2) /* Set if time in seconds is given */
#define OBJ_PX (1 << 3) /* Set if time in ms in given */
#define OBJ_KEEPTTL (1 << 4) /* Set and keep the ttl */
#define OBJ_SET_GET (1 << 5) /* Set if want to get key before set */
#define OBJ_EXAT (1 << 6) /* Set if timestamp in second is given */
#define OBJ_PXAT (1 << 7) /* Set if timestamp in ms is given */
#define OBJ_PERSIST (1 << 8) /* Set if we need to remove the ttl */
#define OBJ_SET_NX (1 << 0) /* Set if key not exists. */
#define OBJ_SET_XX (1 << 1) /* Set if key exists. */
#define OBJ_EX (1 << 2) /* Set if time in seconds is given */
#define OBJ_PX (1 << 3) /* Set if time in ms in given */
#define OBJ_KEEPTTL (1 << 4) /* Set and keep the ttl */
#define OBJ_SET_GET (1 << 5) /* Set if want to get key before set */
#define OBJ_EXAT (1 << 6) /* Set if timestamp in second is given */
#define OBJ_PXAT (1 << 7) /* Set if timestamp in ms is given */
#define OBJ_PERSIST (1 << 8) /* Set if we need to remove the ttl */
#define OBJ_SET_IFEQ (1 << 9) /* Set if we need compare and set */

/* Forward declaration */
static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int unit, long long *milliseconds);
Expand All @@ -87,7 +88,8 @@ void setGenericCommand(client *c,
robj *expire,
int unit,
robj *ok_reply,
robj *abort_reply) {
robj *abort_reply,
robj *comparison) {
long long milliseconds = 0; /* initialized to avoid any harmness warning */
int found = 0;
int setkey_flags = 0;
Expand All @@ -100,7 +102,27 @@ void setGenericCommand(client *c,
if (getGenericCommand(c) == C_ERR) return;
}

found = (lookupKeyWrite(c->db, key) != NULL);
robj *existing_value = lookupKeyWrite(c->db, key);
found = existing_value != NULL;

/* Handle the IFEQ conditional check */
if (flags & OBJ_SET_IFEQ && found) {
if (!(flags & OBJ_SET_GET) && checkType(c, existing_value, OBJ_STRING)) {
return;
}

if (compareStringObjects(existing_value, comparison) != 0) {
if (!(flags & OBJ_SET_GET)) {
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
}
return;
}
} else if (flags & OBJ_SET_IFEQ && !found) {
if (!(flags & OBJ_SET_GET)) {
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
}
return;
}

if ((flags & OBJ_SET_NX && found) || (flags & OBJ_SET_XX && !found)) {
if (!(flags & OBJ_SET_GET)) {
Expand Down Expand Up @@ -208,7 +230,7 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
* string arguments used in SET and GET command.
*
* Get specific commands - PERSIST/DEL
* Set specific commands - XX/NX/GET
* Set specific commands - XX/NX/GET/IFEQ
* Common commands - EX/EXAT/PX/PXAT/KEEPTTL
*
* Function takes pointers to client, flags, unit, pointer to pointer of expire obj if needed
Expand All @@ -219,7 +241,7 @@ static int getExpireMillisecondsOrReply(client *c, robj *expire, int flags, int
* Input flags are updated upon parsing the arguments. Unit and expire are updated if there are any
* EX/EXAT/PX/PXAT arguments. Unit is updated to millisecond if PX/PXAT is set.
*/
int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj **expire, int command_type) {
int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj **expire, robj **compare_val, int command_type) {
int j = command_type == COMMAND_GET ? 2 : 3;
for (; j < c->argc; j++) {
char *opt = c->argv[j]->ptr;
Expand All @@ -228,14 +250,23 @@ int parseExtendedStringArgumentsOrReply(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 & OBJ_SET_XX) && (command_type == COMMAND_SET))
!(*flags & OBJ_SET_XX || *flags & OBJ_SET_IFEQ) && (command_type == COMMAND_SET))
{
*flags |= OBJ_SET_NX;
} else if ((opt[0] == 'x' || opt[0] == 'X') &&
(opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
!(*flags & OBJ_SET_NX) && (command_type == COMMAND_SET))
!(*flags & OBJ_SET_NX || *flags & OBJ_SET_IFEQ) && (command_type == COMMAND_SET))
{
*flags |= OBJ_SET_XX;
} else if ((opt[0] == 'i' || opt[0] == 'I') &&
(opt[1] == 'f' || opt[1] == 'F') &&
(opt[2] == 'e' || opt[2] == 'E') &&
(opt[3] == 'q' || opt[3] == 'Q') && opt[4] == '\0' &&
next && !(*flags & OBJ_SET_NX || *flags & OBJ_SET_XX || *flags & OBJ_SET_IFEQ) && (command_type == COMMAND_SET))
{
*flags |= OBJ_SET_IFEQ;
*compare_val = next;
j++;
} else if ((opt[0] == 'g' || opt[0] == 'G') &&
(opt[1] == 'e' || opt[1] == 'E') &&
(opt[2] == 't' || opt[2] == 'T') && opt[3] == '\0' &&
Expand Down Expand Up @@ -304,34 +335,36 @@ int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj *
return C_OK;
}

/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]
* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
/* SET key value [NX | XX | IFEQ comparison-value] [GET]
* [EX seconds | PX milliseconds |
* EXAT seconds-timestamp | PXAT milliseconds-timestamp | KEEPTTL] */
void setCommand(client *c) {
robj *expire = NULL;
robj *comparison = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;

if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, COMMAND_SET) != C_OK) {
if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, &comparison, COMMAND_SET) != C_OK) {
return;
}

c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c, flags, c->argv[1], c->argv[2], expire, unit, NULL, NULL);
setGenericCommand(c, flags, c->argv[1], c->argv[2], expire, unit, NULL, NULL, comparison);
}

void setnxCommand(client *c) {
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c, OBJ_SET_NX, c->argv[1], c->argv[2], NULL, 0, shared.cone, shared.czero);
setGenericCommand(c, OBJ_SET_NX, c->argv[1], c->argv[2], NULL, 0, shared.cone, shared.czero, NULL);
}

void setexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c, OBJ_EX, c->argv[1], c->argv[3], c->argv[2], UNIT_SECONDS, NULL, NULL);
setGenericCommand(c, OBJ_EX, c->argv[1], c->argv[3], c->argv[2], UNIT_SECONDS, NULL, NULL, NULL);
}

void psetexCommand(client *c) {
c->argv[3] = tryObjectEncoding(c->argv[3]);
setGenericCommand(c, OBJ_PX, c->argv[1], c->argv[3], c->argv[2], UNIT_MILLISECONDS, NULL, NULL);
setGenericCommand(c, OBJ_PX, c->argv[1], c->argv[3], c->argv[2], UNIT_MILLISECONDS, NULL, NULL, NULL);
}

int getGenericCommand(client *c) {
Expand Down Expand Up @@ -377,7 +410,7 @@ void getexCommand(client *c) {
int unit = UNIT_SECONDS;
int flags = OBJ_NO_FLAGS;

if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, COMMAND_GET) != C_OK) {
if (parseExtendedStringArgumentsOrReply(c, &flags, &unit, &expire, NULL, COMMAND_GET) != C_OK) {
return;
}

Expand Down
22 changes: 11 additions & 11 deletions tests/assets/test_cli_hint_suite.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,17 @@
"ZRANGE k 1 2 WITHSCORES " "[BYSCORE|BYLEX] [REV] [LIMIT offset count]"

# Optional one-of args with parameters: SET key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]
"SET key value " "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX " "seconds [NX|XX] [GET]"
"SET key value EX 23 " "[NX|XX] [GET]"
"SET key value EXAT" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EXAT " "unix-time-seconds [NX|XX] [GET]"
"SET key value PX" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PX " "milliseconds [NX|XX] [GET]"
"SET key value PXAT" "[NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PXAT " "unix-time-milliseconds [NX|XX] [GET]"
"SET key value KEEPTTL " "[NX|XX] [GET]"
"SET key value " "[NX|XX|IFEQ comparison-value] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX" "[NX|XX|IFEQ comparison-value] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EX " "seconds [NX|XX|IFEQ comparison-value] [GET]"
"SET key value EX 23 " "[NX|XX|IFEQ comparison-value] [GET]"
"SET key value EXAT" "[NX|XX|IFEQ comparison-value] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value EXAT " "unix-time-seconds [NX|XX|IFEQ comparison-value] [GET]"
"SET key value PX" "[NX|XX|IFEQ comparison-value] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PX " "milliseconds [NX|XX|IFEQ comparison-value] [GET]"
"SET key value PXAT" "[NX|XX|IFEQ comparison-value] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"
"SET key value PXAT " "unix-time-milliseconds [NX|XX|IFEQ comparison-value] [GET]"
"SET key value KEEPTTL " "[NX|XX|IFEQ comparison-value] [GET]"
"SET key value XX " "[GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]"

# If an input word can't be matched, stop hinting.
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/type/string.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,56 @@ if {[string match {*jemalloc*} [s mem_allocator]]} {
set err1
} {*WRONGTYPE*}

test "SET with IFEQ conditional" {
r del foo

r set foo "initial_value"

assert_equal {OK} [r set foo "new_value" ifeq "initial_value"]
assert_equal "new_value" [r get foo]

assert_equal {} [r set foo "should_not_set" ifeq "wrong_value"]
assert_equal "new_value" [r get foo]
}

test "SET with IFEQ conditional - non-string current value" {
r del foo

r sadd foo "some_set_value"
assert_error {WRONGTYPE Operation against a key holding the wrong kind of value} {r set foo "new_value" ifeq "some_set_value"}
}


test "SET with IFEQ conditional - with get" {
r del foo

assert_equal {} [r set foo "new_value" ifeq "initial_value" get]
assert_equal {} [r get foo]

r set foo "initial_value"

assert_equal "initial_value" [r set foo "new_value" ifeq "initial_value" get]
assert_equal "new_value" [r get foo]
}

test "SET with IFEQ conditional - non string current value with get" {
r del foo

r sadd foo "some_set_value"

assert_error {WRONGTYPE Operation against a key holding the wrong kind of value} {r set foo "new_value" ifeq "initial_value" get}
}

test "SET with IFEQ conditional - with xx" {
r del foo
assert_error {ERR syntax error} {r set foo "new_value" ifeq "initial_value" xx}
}

test "SET with IFEQ conditional - with nx" {
r del foo
assert_error {ERR syntax error} {r set foo "new_value" ifeq "initial_value" nx}
}

test {Extended SET EX option} {
r del foo
r set foo bar ex 10
Expand Down

0 comments on commit 4fe5edf

Please sign in to comment.