diff --git a/CHANGELOG.md b/CHANGELOG.md
index d794567e5c..5301843e06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,7 @@
* Java: Shadow `protobuf` dependency ([#2931](https://github.com/valkey-io/valkey-glide/pull/2931))
* Java: Add `RESP2` support ([#2383](https://github.com/valkey-io/valkey-glide/pull/2383))
* Node, Python: Add `IFEQ` option ([#2909](https://github.com/valkey-io/valkey-glide/pull/2909), [#2962](https://github.com/valkey-io/valkey-glide/pull/2962))
+* Java: Add `IFEQ` option ([#2978](https://github.com/valkey-io/valkey-glide/pull/2978))
#### Breaking Changes
diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java
index 20f13c30f2..0931cc42b3 100644
--- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java
+++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java
@@ -215,14 +215,21 @@ public interface StringBaseCommands {
* @param options The Set options.
* @return If the value is successfully set, return "OK"
. If value isn't set because
* of {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST}
- * conditions, return null
. If {@link SetOptionsBuilder#returnOldValue(boolean)}
- * is set, return the old value as a String
.
+ * or {@link ConditionalSet#ONLY_IF_EQUAL} conditions, return null
. If {@link
+ * SetOptionsBuilder#returnOldValue(boolean)} is set, return the old value as a String
+ *
.
* @example
*
{@code - * SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).expiry(Seconds(5L)).build(); + * SetOptions options = SetOptions.builder().conditionalSetOnlyIfExists().expiry(Seconds(5L)).build(); * String value = client.set("key", "value", options).get(); * assert value.equals("OK"); * }+ *
{@code + * client.set("key", "value").get(); + * SetOptions options = SetOptions.builder().conditionalSetIfEqualTo("value").build(); + * String value = client.set("key", "newValue", options).get(); + * assert value.equals("OK"); + * }*/ CompletableFuture
"OK"
. If value isn't set because
* of {@link ConditionalSet#ONLY_IF_EXISTS} or {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST}
- * conditions, return null
. If {@link SetOptionsBuilder#returnOldValue(boolean)}
- * is set, return the old value as a String
.
+ * or {@link ConditionalSet#ONLY_IF_EQUAL} conditions, return null
. If {@link
+ * SetOptionsBuilder#returnOldValue(boolean)} is set, return the old value as a String
+ *
.
* @example
* {@code * SetOptions options = SetOptions.builder().conditionalSet(ONLY_IF_EXISTS).expiry(Seconds(5L)).build(); diff --git a/java/client/src/main/java/glide/api/models/commands/SetOptions.java b/java/client/src/main/java/glide/api/models/commands/SetOptions.java index ccee370ff2..ff348c85a6 100644 --- a/java/client/src/main/java/glide/api/models/commands/SetOptions.java +++ b/java/client/src/main/java/glide/api/models/commands/SetOptions.java @@ -13,6 +13,7 @@ import java.util.List; import lombok.Builder; import lombok.Getter; +import lombok.NonNull; import lombok.RequiredArgsConstructor; /** @@ -29,6 +30,9 @@ public final class SetOptions { */ private final ConditionalSet conditionalSet; + /** Value to compare when {@link ConditionalSet#ONLY_IF_EQUAL} is set. */ + private final String comparisonValue; + /** * Set command to return the old string stored atkey
, ornull
if* key
did not exist. An error is returned andSET
aborted if the value stored @@ -49,11 +53,71 @@ public enum ConditionalSet { * Only set the key if it does not already exist. Equivalent toNX
in the Valkey * API. */ - ONLY_IF_DOES_NOT_EXIST("NX"); + ONLY_IF_DOES_NOT_EXIST("NX"), + /** + * Only set the key if the current value of key equals the {@link SetOptions#comparisonValue}. + * Equivalent toIFEQ comparison-value
in the Valkey API. + */ + ONLY_IF_EQUAL("IFEQ"); private final String valkeyApi; } + /** + * Builder class for {@link SetOptions}. + * + *Provides methods to set conditions under which a value should be set. + * + *
Note: Calling any of these methods will override the existing values of {@code + * conditionalSet} and {@code comparisonValue}, if they are already set. + */ + public static class SetOptionsBuilder { + /** + * Sets the condition to {@link ConditionalSet#ONLY_IF_EXISTS} for setting the value. + * + *
This method overrides any previously set {@code conditionalSet} and {@code + * comparisonValue}. + * + * @return This builder instance + */ + public SetOptionsBuilder conditionalSetOnlyIfExists() { + this.conditionalSet = ConditionalSet.ONLY_IF_EXISTS; + this.comparisonValue = null; + return this; + } + + /** + * Sets the condition to {@link ConditionalSet#ONLY_IF_DOES_NOT_EXIST} for setting the value. + * + *
This method overrides any previously set {@code conditionalSet} and {@code + * comparisonValue}. + * + * @return This builder instance + */ + public SetOptionsBuilder conditionalSetOnlyIfNotExist() { + this.conditionalSet = ConditionalSet.ONLY_IF_DOES_NOT_EXIST; + this.comparisonValue = null; + return this; + } + + /** + * Sets the condition to {@link ConditionalSet#ONLY_IF_EQUAL} for setting the value. The key + * will be set if the provided comparison value matches the existing value. + * + *
This method overrides any previously set {@code conditionalSet} and {@code + * comparisonValue}. + * + * @since Valkey 8.1 and above. + * @param value the value to compare + * @return this builder instance + */ + public SetOptionsBuilder conditionalSetOnlyIfEqualTo(@NonNull String value) { + this.conditionalSet = ConditionalSet.ONLY_IF_EQUAL; + this.comparisonValue = value; + return this; + } + } + /** Configuration of value lifetime. */ public static final class Expiry { @@ -151,6 +215,9 @@ public String[] toArgs() { List
optionArgs = new ArrayList<>(); if (conditionalSet != null) { optionArgs.add(conditionalSet.valkeyApi); + if (conditionalSet == ConditionalSet.ONLY_IF_EQUAL) { + optionArgs.add(comparisonValue); + } } if (returnOldValue) { diff --git a/java/client/src/test/java/glide/api/GlideClientTest.java b/java/client/src/test/java/glide/api/GlideClientTest.java index 4b55459328..a4c74e4a86 100644 --- a/java/client/src/test/java/glide/api/GlideClientTest.java +++ b/java/client/src/test/java/glide/api/GlideClientTest.java @@ -223,6 +223,7 @@ import static glide.api.models.commands.LInsertOptions.InsertPosition.BEFORE; import static glide.api.models.commands.ScoreFilter.MAX; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; +import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EQUAL; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; import static glide.api.models.commands.SortBaseOptions.ALPHA_COMMAND_STRING; @@ -953,6 +954,100 @@ public void set_with_SetOptions_OnlyIfDoesNotExist_returns_success() { assertEquals(value, response.get()); } + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfEqual_success() { + // setup + String key = "key"; + String value = "value"; + String newValue = "newValue"; + + // Set `key` to `value` initially + CompletableFuture initialSetResponse = new CompletableFuture<>(); + initialSetResponse.complete("OK"); + String[] initialArguments = new String[] {key, value}; + when(commandManager. submitNewCommand(eq(pSet), eq(initialArguments), any())) + .thenReturn(initialSetResponse); + + CompletableFuture initialResponse = service.set(key, value); + assertNotNull(initialResponse); + assertEquals("OK", initialResponse.get()); + + // Set `key` to `newValue` with the correct condition + SetOptions setOptions = + SetOptions.builder() + .conditionalSetOnlyIfEqualTo(value) // Key must currently have `value` + .expiry(Expiry.UnixSeconds(60L)) + .build(); + String[] correctConditionArguments = + new String[] {key, newValue, ONLY_IF_EQUAL.getValkeyApi(), value, "EXAT", "60"}; + CompletableFuture correctSetResponse = new CompletableFuture<>(); + correctSetResponse.complete("OK"); + when(commandManager. submitNewCommand(eq(pSet), eq(correctConditionArguments), any())) + .thenReturn(correctSetResponse); + + CompletableFuture correctResponse = service.set(key, newValue, setOptions); + assertNotNull(correctResponse); + assertEquals("OK", correctResponse.get()); + + // Verify that the key is now set to `newValue` + CompletableFuture fetchValueResponse = new CompletableFuture<>(); + fetchValueResponse.complete(newValue); + when(commandManager. submitNewCommand(eq(Get), eq(new String[] {key}), any())) + .thenReturn(fetchValueResponse); + + CompletableFuture finalValue = service.get(key); + assertEquals(newValue, finalValue.get()); + } + + @SneakyThrows + @Test + public void set_with_SetOptions_OnlyIfEqual_fails() { + // Key-Value setup + String key = "key"; + String value = "value"; + String newValue = "newValue"; + + // Set `key` to `value` initially + CompletableFuture initialSetResponse = new CompletableFuture<>(); + initialSetResponse.complete("OK"); + String[] initialArguments = new String[] {key, value}; + when(commandManager. submitNewCommand(eq(pSet), eq(initialArguments), any())) + .thenReturn(initialSetResponse); + + CompletableFuture initialResponse = service.set(key, value); + assertNotNull(initialResponse); + assertEquals("OK", initialResponse.get()); + + // Attempt to set `key` to `newValue` with the wrong condition + SetOptions wrongConditionOptions = + SetOptions.builder() + .conditionalSetOnlyIfEqualTo(newValue) // Incorrect: current value of key is `value` + .expiry(Expiry.UnixSeconds(60L)) + .build(); + + String[] wrongConditionArguments = + new String[] {key, newValue, ONLY_IF_EQUAL.getValkeyApi(), newValue, "EXAT", "60"}; + + CompletableFuture failedSetResponse = new CompletableFuture<>(); + failedSetResponse.complete(null); + when(commandManager. submitNewCommand(eq(pSet), eq(wrongConditionArguments), any())) + .thenReturn(failedSetResponse); + + CompletableFuture failedResponse = service.set(key, newValue, wrongConditionOptions); + assertNotNull(failedResponse); + assertNull(failedResponse.get()); // Ensure the set operation failed + + // Verify that the key remains set to `value` + CompletableFuture fetchValueResponse = new CompletableFuture<>(); + fetchValueResponse.complete(value); + when(commandManager. submitNewCommand(eq(Get), eq(new String[] {key}), any())) + .thenReturn(fetchValueResponse); + + CompletableFuture finalValue = service.get(key); + assertEquals(value, finalValue.get()); + } + @SneakyThrows @Test public void exists_returns_long_success() {