Repository: redis/redis

Problem statement:
[NEW] Define NX in combination with GET on the SET command
**The problem/use-case that the feature addresses**

With Redis 6.2 the `SET` command allows the option flags `XX` and `GET` to be used together (getting the old value, but only writing the new one if the key already exists), but not `NX` and `GET`.
Peeking and creating a key - but not overwriting it if it exists - would be a handy command, e.g. for atomically setting an expiring "lock" value on a key if it doesn't exist.

**Description of the feature**

The command `SET key value NX GET` should behave as follows:
- if `key` exists and holds value `oldValue`
  - don't store `value` for `key`
  - reply with `oldValue`
- if `key` doesn't exist
  - store `value` for `key`
  - reply with `(nil)`

The client can determine if the value was set by checking the reply for `(nil)` (yes) or another value (no).

**Alternatives you've considered**

Other string commands don't seem to support this case either, so alternatives would be issuing multiple commands or scripting.

**Additional information**

The limitation is not explicitly mentioned in the command documentation and the "Return value" section is ambiguous about it. Another user has also tripped over it: https://github.com/redis/redis/issues/8838


Hints:
Makes sense to me.

Patch:
diff --git a/src/t_string.c b/src/t_string.c
index 9228c5ed040..a688e0110f8 100644
--- a/src/t_string.c
+++ b/src/t_string.c
@@ -94,17 +94,19 @@ void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire,
         }
     }
 
+    if (flags & OBJ_SET_GET) {
+        if (getGenericCommand(c) == C_ERR) return;
+    }
+
     if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
         (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
     {
-        addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
+        if (!(flags & OBJ_SET_GET)) {
+            addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
+        }
         return;
     }
 
-    if (flags & OBJ_SET_GET) {
-        if (getGenericCommand(c) == C_ERR) return;
-    }
-
     genericSetKey(c,c->db,key, val,flags & OBJ_KEEPTTL,1);
     server.dirty++;
     notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
@@ -177,7 +179,7 @@ int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj *
 
         if ((opt[0] == 'n' || opt[0] == 'N') &&
             (opt[1] == 'x' || opt[1] == 'X') && opt[2] == '\0' &&
-            !(*flags & OBJ_SET_XX) && !(*flags & OBJ_SET_GET) && (command_type == COMMAND_SET))
+            !(*flags & OBJ_SET_XX) && (command_type == COMMAND_SET))
         {
             *flags |= OBJ_SET_NX;
         } else if ((opt[0] == 'x' || opt[0] == 'X') &&
@@ -188,7 +190,7 @@ int parseExtendedStringArgumentsOrReply(client *c, int *flags, int *unit, robj *
         } else if ((opt[0] == 'g' || opt[0] == 'G') &&
                    (opt[1] == 'e' || opt[1] == 'E') &&
                    (opt[2] == 't' || opt[2] == 'T') && opt[3] == '\0' &&
-                   !(*flags & OBJ_SET_NX) && (command_type == COMMAND_SET))
+                   (command_type == COMMAND_SET))
         {
             *flags |= OBJ_SET_GET;
         } else if (!strcasecmp(opt, "KEEPTTL") && !(*flags & OBJ_PERSIST) &&


Test patch:
diff --git a/tests/unit/type/string.tcl b/tests/unit/type/string.tcl
index 43968b26b97..5dd9130c0ad 100644
--- a/tests/unit/type/string.tcl
+++ b/tests/unit/type/string.tcl
@@ -494,11 +494,35 @@ start_server {tags {"string"}} {
         list $old_value $new_value
     } {{} bar}
 
-    test {Extended SET GET with NX option should result in syntax err} {
-      catch {r set foo bar NX GET} err1
-      catch {r set foo bar NX GET} err2
-      list $err1 $err2
-    } {*syntax err* *syntax err*}
+    test {Extended SET GET option with XX} {
+        r del foo
+        r set foo bar
+        set old_value [r set foo baz GET XX]
+        set new_value [r get foo]
+        list $old_value $new_value
+    } {bar baz}
+
+    test {Extended SET GET option with XX and no previous value} {
+        r del foo
+        set old_value [r set foo bar GET XX]
+        set new_value [r get foo]
+        list $old_value $new_value
+    } {{} {}}
+
+    test {Extended SET GET option with NX} {
+        r del foo
+        set old_value [r set foo bar GET NX]
+        set new_value [r get foo]
+        list $old_value $new_value
+    } {{} bar}
+
+    test {Extended SET GET option with NX and previous value} {
+        r del foo
+        r set foo bar
+        set old_value [r set foo baz GET NX]
+        set new_value [r get foo]
+        list $old_value $new_value
+    } {bar bar}
 
     test {Extended SET GET with incorrect type should result in wrong type error} {
       r del foo