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 set(String key, String value, SetOptions options); @@ -235,8 +242,9 @@ 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();
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 at key, or null if 
      * key did not exist. An error is returned and SET aborted if the value stored
@@ -49,11 +53,71 @@ public enum ConditionalSet {
          * Only set the key if it does not already exist. Equivalent to NX 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 to IFEQ 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() {