From f8b396e9aa5942625bd568759078223caecdaa41 Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 12:17:10 +0200 Subject: [PATCH 1/8] Add initial impl of CAS/CAD --- .../core/AbstractRedisAsyncCommands.java | 35 +++++ .../core/AbstractRedisReactiveCommands.java | 35 +++++ .../io/lettuce/core/RedisCommandBuilder.java | 92 ++++++++++++ .../java/io/lettuce/core/ValueCondition.java | 132 ++++++++++++++++++ .../core/api/async/RedisKeyAsyncCommands.java | 36 ++++- .../api/async/RedisStringAsyncCommands.java | 57 +++++++- .../reactive/RedisKeyReactiveCommands.java | 26 +++- .../reactive/RedisStringReactiveCommands.java | 59 +++++++- .../core/api/sync/RedisKeyCommands.java | 24 +++- .../core/api/sync/RedisStringCommands.java | 55 +++++++- .../async/NodeSelectionKeyAsyncCommands.java | 24 +++- .../NodeSelectionStringAsyncCommands.java | 55 +++++++- .../api/sync/NodeSelectionKeyCommands.java | 24 +++- .../api/sync/NodeSelectionStringCommands.java | 55 +++++++- .../lettuce/core/protocol/CommandKeyword.java | 2 +- .../io/lettuce/core/protocol/CommandType.java | 4 +- .../core/protocol/ReadOnlyCommands.java | 2 +- .../coroutines/RedisKeyCoroutinesCommands.kt | 38 ++++- .../RedisKeyCoroutinesCommandsImpl.kt | 6 + .../RedisStringCoroutinesCommands.kt | 59 +++++++- .../RedisStringCoroutinesCommandsImpl.kt | 23 +++ .../io/lettuce/core/api/RedisKeyCommands.java | 35 ++++- .../lettuce/core/api/RedisStringCommands.java | 61 +++++++- 23 files changed, 893 insertions(+), 46 deletions(-) create mode 100644 src/main/java/io/lettuce/core/ValueCondition.java diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index 68245ab529..f1452671f3 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -741,6 +741,16 @@ public RedisFuture del(Iterable keys) { return dispatch(commandBuilder.del(keys)); } + @Override + public RedisFuture delex(K key) { + return dispatch(commandBuilder.delex(key)); + } + + @Override + public RedisFuture delex(K key, ValueCondition condition) { + return dispatch(commandBuilder.delex(key, condition)); + } + @Override public String digest(String script) { return digest(encodeScript(script)); @@ -1265,6 +1275,11 @@ public RedisFuture get(K key) { return dispatch(commandBuilder.get(key)); } + @Override + public RedisFuture digestKey(K key) { + return dispatch(commandBuilder.digestKey(key)); + } + public StatefulConnection getConnection() { return connection; } @@ -2675,6 +2690,26 @@ public RedisFuture set(K key, V value, SetArgs setArgs) { return dispatch(commandBuilder.set(key, value, setArgs)); } + @Override + public RedisFuture set(K key, V value, ValueCondition condition) { + return dispatch(commandBuilder.set(key, value, condition)); + } + + @Override + public RedisFuture set(K key, V value, SetArgs setArgs, ValueCondition condition) { + return dispatch(commandBuilder.set(key, value, setArgs, condition)); + } + + @Override + public RedisFuture setGet(K key, V value, ValueCondition condition) { + return dispatch(commandBuilder.setGet(key, value, condition)); + } + + @Override + public RedisFuture setGet(K key, V value, SetArgs setArgs, ValueCondition condition) { + return dispatch(commandBuilder.setGet(key, value, setArgs, condition)); + } + @Override public RedisFuture setGet(K key, V value) { return dispatch(commandBuilder.setGet(key, value)); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index b0374474a7..43963f5ad1 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -811,6 +811,16 @@ public Mono del(Iterable keys) { return createMono(() -> commandBuilder.del(keys)); } + @Override + public Mono delex(K key) { + return createMono(() -> commandBuilder.delex(key)); + } + + @Override + public Mono delex(K key, ValueCondition condition) { + return createMono(() -> commandBuilder.delex(key, condition)); + } + @Override public String digest(String script) { return digest(encodeScript(script)); @@ -1326,6 +1336,11 @@ public Mono get(K key) { return createMono(() -> commandBuilder.get(key)); } + @Override + public Mono digestKey(K key) { + return createMono(() -> commandBuilder.digestKey(key)); + } + public StatefulConnection getConnection() { return connection; } @@ -2758,6 +2773,26 @@ public Mono set(K key, V value, SetArgs setArgs) { return createMono(() -> commandBuilder.set(key, value, setArgs)); } + @Override + public Mono set(K key, V value, ValueCondition condition) { + return createMono(() -> commandBuilder.set(key, value, condition)); + } + + @Override + public Mono set(K key, V value, SetArgs setArgs, ValueCondition condition) { + return createMono(() -> commandBuilder.set(key, value, setArgs, condition)); + } + + @Override + public Mono setGet(K key, V value, ValueCondition condition) { + return createMono(() -> commandBuilder.setGet(key, value, condition)); + } + + @Override + public Mono setGet(K key, V value, SetArgs setArgs, ValueCondition condition) { + return createMono(() -> commandBuilder.setGet(key, value, setArgs, condition)); + } + @Override public Mono setGet(K key, V value) { return createMono(() -> commandBuilder.setGet(key, value)); diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 7a4d44ec30..8ec63544a2 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -940,6 +940,22 @@ Command del(Iterable keys) { return createCommand(DEL, new IntegerOutput<>(codec), args); } + Command delex(K key) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + return createCommand(DELEX, new IntegerOutput<>(codec), args); + } + + Command delex(K key, ValueCondition condition) { + notNullKey(key); + LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + buildConditionArgs(args, condition); + return createCommand(DELEX, new IntegerOutput<>(codec), args); + } + Command discard() { return createCommand(DISCARD, new StatusOutput<>(codec)); } @@ -2702,6 +2718,55 @@ Command set(K key, V value, SetArgs setArgs) { return createCommand(SET, new StatusOutput<>(codec), args); } + Command digestKey(K key) { + notNullKey(key); + + CommandArgs args = new CommandArgs<>(codec).addKey(key); + return createCommand(DIGEST, new StatusOutput<>(codec), args); + } + + Command set(K key, V value, ValueCondition condition) { + notNullKey(key); + LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); + buildConditionArgs(args, condition); + return createCommand(SET, new StatusOutput<>(codec), args); + } + + Command set(K key, V value, SetArgs setArgs, ValueCondition condition) { + notNullKey(key); + LettuceAssert.notNull(setArgs, "SetArgs " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); + setArgs.build(args); + buildConditionArgs(args, condition); + return createCommand(SET, new StatusOutput<>(codec), args); + } + + Command setGet(K key, V value, ValueCondition condition) { + notNullKey(key); + LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); + buildConditionArgs(args, condition); + args.add(GET); + return createCommand(SET, new ValueOutput<>(codec), args); + } + + Command setGet(K key, V value, SetArgs setArgs, ValueCondition condition) { + notNullKey(key); + LettuceAssert.notNull(setArgs, "SetArgs " + MUST_NOT_BE_NULL); + LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); + setArgs.build(args); + buildConditionArgs(args, condition); + args.add(GET); + return createCommand(SET, new ValueOutput<>(codec), args); + } + Command setGet(K key, V value) { return setGet(key, value, new SetArgs()); } @@ -4564,6 +4629,33 @@ Command zscanStreaming(ScoredValueStreamingChannel ch return zscanStreaming(channel, key, ScanCursor.INITIAL, scanArgs); } + private void buildConditionArgs(CommandArgs args, ValueCondition condition) { + switch (condition.kind()) { + case EXISTS: + args.add(XX); + break; + case NOT_EXISTS: + args.add(NX); + break; + case EQUAL: + args.add(IFEQ).addValue(condition.value()); + break; + case NOT_EQUAL: + args.add(IFNE).addValue(condition.value()); + break; + case DIGEST_EQUAL: + args.add(IFDEQ).add(condition.digestHex()); + break; + case DIGEST_NOT_EQUAL: + args.add(IFDNE).add(condition.digestHex()); + break; + case ALWAYS: + default: + // no additional args + break; + } + } + Command zscanStreaming(ScoredValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs) { notNullKey(key); diff --git a/src/main/java/io/lettuce/core/ValueCondition.java b/src/main/java/io/lettuce/core/ValueCondition.java new file mode 100644 index 0000000000..b367f421a0 --- /dev/null +++ b/src/main/java/io/lettuce/core/ValueCondition.java @@ -0,0 +1,132 @@ +/* + * Copyright 2017-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + * + * This file contains contributions from third-party contributors + * licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.lettuce.core; + +/** + * A compare condition to be used with commands that support conditional value checks (e.g. SET with IFEQ/IFNE/IFDEQ/IFDNE and + * DELEX). This abstraction lets callers express value-based or digest-based comparisons without exploding method overloads. + * + *

+ * Digest-based comparisons use a 64-bit XXH3 digest represented as a 16-character lower-case hexadecimal string. + *

+ * + * @param value type used for value-based comparisons + * @since 8.x + */ +public final class ValueCondition { + + /** + * The kind of condition represented by this instance. + */ + public enum Kind { + /** unconditional */ + ALWAYS, + /** key must exist */ + EXISTS, + /** key must not exist */ + NOT_EXISTS, + /** current value must equal provided value */ + EQUAL, + /** current value must not equal provided value */ + NOT_EQUAL, + /** current value's digest must equal provided digest */ + DIGEST_EQUAL, + /** current value's digest must not equal provided digest */ + DIGEST_NOT_EQUAL + } + + private final Kind kind; + + private final V value; // used for EQUAL/NOT_EQUAL + + private final String digestHex; // used for DIGEST_EQUAL/DIGEST_NOT_EQUAL + + private ValueCondition(Kind kind, V value, String digestHex) { + this.kind = kind; + this.value = value; + this.digestHex = digestHex; + } + + /** A condition that always applies (no comparison). */ + public static ValueCondition always() { + return new ValueCondition<>(Kind.ALWAYS, null, null); + + } + + /** A condition that requires the key to exist (equivalent to XX in SET). */ + public static ValueCondition exists() { + return new ValueCondition<>(Kind.EXISTS, null, null); + } + + /** A condition that requires the key to not exist (equivalent to NX in SET). */ + public static ValueCondition notExists() { + return new ValueCondition<>(Kind.NOT_EXISTS, null, null); + } + + /** A value-based comparison: set/delete only if the current value equals {@code value}. */ + public static ValueCondition equal(V value) { + if (value == null) + throw new IllegalArgumentException("value must not be null"); + return new ValueCondition<>(Kind.EQUAL, value, null); + } + + /** A value-based comparison: set/delete only if the current value does not equal {@code value}. */ + public static ValueCondition notEqual(V value) { + if (value == null) + throw new IllegalArgumentException("value must not be null"); + return new ValueCondition<>(Kind.NOT_EQUAL, value, null); + } + + /** A digest-based comparison: set/delete only if the current value's digest equals {@code hex16Digest}. */ + public static ValueCondition digestEqualHex(String hex16Digest) { + if (hex16Digest == null) + throw new IllegalArgumentException("digest must not be null"); + return new ValueCondition<>(Kind.DIGEST_EQUAL, null, hex16Digest); + } + + /** A digest-based comparison: set/delete only if the current value's digest does not equal {@code hex16Digest}. */ + public static ValueCondition digestNotEqualHex(String hex16Digest) { + if (hex16Digest == null) + throw new IllegalArgumentException("digest must not be null"); + return new ValueCondition<>(Kind.DIGEST_NOT_EQUAL, null, hex16Digest); + } + + /** The kind of this condition. */ + public Kind kind() { + return kind; + } + + /** The value for value-based comparisons, or {@code null}. */ + public V value() { + return value; + } + + /** The 16-character lower-case hex digest for digest-based comparisons, or {@code null}. */ + public String digestHex() { + return digestHex; + } + + @Override + public String toString() { + return "ValueCondition{" + "kind=" + kind + (value != null ? ", value=" + value : "") + + (digestHex != null ? ", digestHex='" + digestHex + '\'' : "") + '}'; + } + +} diff --git a/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java index e5eab6dfbd..c66ddd68e6 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java @@ -19,12 +19,23 @@ */ package io.lettuce.core.api.async; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; import java.util.List; +import java.util.Date; +import java.time.Instant; +import java.time.Duration; -import io.lettuce.core.*; +import io.lettuce.core.CopyArgs; +import io.lettuce.core.ExpireArgs; +import io.lettuce.core.KeyScanArgs; +import io.lettuce.core.KeyScanCursor; +import io.lettuce.core.MigrateArgs; +import io.lettuce.core.RestoreArgs; +import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; +import io.lettuce.core.SortArgs; +import io.lettuce.core.StreamScanCursor; +import io.lettuce.core.ValueCondition; +import io.lettuce.core.RedisFuture; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -68,6 +79,23 @@ public interface RedisKeyAsyncCommands { */ RedisFuture del(K... keys); + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + RedisFuture delex(K key); + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be {@code null}. + * @return Long integer-reply the number of keys that were removed. + */ + RedisFuture delex(K key, ValueCondition condition); + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java index bcacfa731e..e6a2677c5e 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java @@ -19,18 +19,19 @@ */ package io.lettuce.core.api.async; -import java.util.List; import java.util.Map; +import java.util.List; import io.lettuce.core.BitFieldArgs; import io.lettuce.core.GetExArgs; import io.lettuce.core.KeyValue; -import io.lettuce.core.RedisFuture; -import io.lettuce.core.SetArgs; import io.lettuce.core.LcsArgs; +import io.lettuce.core.SetArgs; import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.RedisFuture; /** * Asynchronous executed commands for Strings. @@ -256,6 +257,14 @@ public interface RedisStringAsyncCommands { */ RedisFuture get(K key); + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. + */ + RedisFuture digestKey(K key); + /** * Returns the bit value at offset in the string value stored at key. * @@ -373,6 +382,48 @@ public interface RedisStringAsyncCommands { */ RedisFuture set(K key, V value); + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + RedisFuture set(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + RedisFuture set(K key, V value, SetArgs setArgs, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + RedisFuture setGet(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + RedisFuture setGet(K key, V value, SetArgs setArgs, ValueCondition condition); + /** * Set the string value of a key. * diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java index 8fad8dde43..8f335b1c04 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java @@ -19,12 +19,10 @@ */ package io.lettuce.core.api.reactive; -import java.time.Duration; -import java.time.Instant; import java.util.Date; +import java.time.Instant; +import java.time.Duration; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import io.lettuce.core.CopyArgs; import io.lettuce.core.ExpireArgs; import io.lettuce.core.KeyScanArgs; @@ -35,8 +33,11 @@ import io.lettuce.core.ScanCursor; import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Reactive executed commands for Keys (Key manipulation/querying). @@ -78,6 +79,23 @@ public interface RedisKeyReactiveCommands { */ Mono del(K... keys); + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + Mono delex(K key); + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be {@code null}. + * @return Long integer-reply the number of keys that were removed. + */ + Mono delex(K key, ValueCondition condition); + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java index 60e2d5294c..81a5b58443 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java @@ -21,17 +21,18 @@ import java.util.Map; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import io.lettuce.core.BitFieldArgs; import io.lettuce.core.GetExArgs; import io.lettuce.core.KeyValue; +import io.lettuce.core.LcsArgs; import io.lettuce.core.SetArgs; import io.lettuce.core.StrAlgoArgs; -import io.lettuce.core.LcsArgs; import io.lettuce.core.StringMatchResult; -import io.lettuce.core.Value; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.Value; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Reactive executed commands for Strings. @@ -257,6 +258,14 @@ public interface RedisStringReactiveCommands { */ Mono get(K key); + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. + */ + Mono digestKey(K key); + /** * Returns the bit value at offset in the string value stored at key. * @@ -377,6 +386,48 @@ public interface RedisStringReactiveCommands { */ Mono set(K key, V value); + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + Mono set(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + Mono set(K key, V value, SetArgs setArgs, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + Mono setGet(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + Mono setGet(K key, V value, SetArgs setArgs, ValueCondition condition); + /** * Set the string value of a key. * diff --git a/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java index 777dc966f5..f9a5a3b980 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java @@ -19,10 +19,10 @@ */ package io.lettuce.core.api.sync; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; import java.util.List; +import java.util.Date; +import java.time.Instant; +import java.time.Duration; import io.lettuce.core.CopyArgs; import io.lettuce.core.ExpireArgs; @@ -34,6 +34,7 @@ import io.lettuce.core.ScanCursor; import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -77,6 +78,23 @@ public interface RedisKeyCommands { */ Long del(K... keys); + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + Long delex(K key); + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be {@code null}. + * @return Long integer-reply the number of keys that were removed. + */ + Long delex(K key, ValueCondition condition); + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java index bae38567b8..651215bc91 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java @@ -19,16 +19,17 @@ */ package io.lettuce.core.api.sync; -import java.util.List; import java.util.Map; +import java.util.List; import io.lettuce.core.BitFieldArgs; import io.lettuce.core.GetExArgs; import io.lettuce.core.KeyValue; +import io.lettuce.core.LcsArgs; import io.lettuce.core.SetArgs; import io.lettuce.core.StrAlgoArgs; -import io.lettuce.core.LcsArgs; import io.lettuce.core.StringMatchResult; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyValueStreamingChannel; /** @@ -255,6 +256,14 @@ public interface RedisStringCommands { */ V get(K key); + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. + */ + String digestKey(K key); + /** * Returns the bit value at offset in the string value stored at key. * @@ -372,6 +381,48 @@ public interface RedisStringCommands { */ String set(K key, V value); + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + String set(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + String set(K key, V value, SetArgs setArgs, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + V setGet(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + V setGet(K key, V value, SetArgs setArgs, ValueCondition condition); + /** * Set the string value of a key. * diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java index 7233e5ae9d..c224ad93c1 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java @@ -19,10 +19,10 @@ */ package io.lettuce.core.cluster.api.async; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; import java.util.List; +import java.util.Date; +import java.time.Instant; +import java.time.Duration; import io.lettuce.core.CopyArgs; import io.lettuce.core.ExpireArgs; @@ -34,6 +34,7 @@ import io.lettuce.core.ScanCursor; import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -77,6 +78,23 @@ public interface NodeSelectionKeyAsyncCommands { */ AsyncExecutions del(K... keys); + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + AsyncExecutions delex(K key); + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be {@code null}. + * @return Long integer-reply the number of keys that were removed. + */ + AsyncExecutions delex(K key, ValueCondition condition); + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java index 44672f3810..a6dd197ba9 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java @@ -19,16 +19,17 @@ */ package io.lettuce.core.cluster.api.async; -import java.util.List; import java.util.Map; +import java.util.List; import io.lettuce.core.BitFieldArgs; import io.lettuce.core.GetExArgs; import io.lettuce.core.KeyValue; +import io.lettuce.core.LcsArgs; import io.lettuce.core.SetArgs; import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; -import io.lettuce.core.LcsArgs; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyValueStreamingChannel; /** @@ -255,6 +256,14 @@ public interface NodeSelectionStringAsyncCommands { */ AsyncExecutions get(K key); + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. + */ + AsyncExecutions digestKey(K key); + /** * Returns the bit value at offset in the string value stored at key. * @@ -372,6 +381,48 @@ public interface NodeSelectionStringAsyncCommands { */ AsyncExecutions set(K key, V value); + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + AsyncExecutions set(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + AsyncExecutions set(K key, V value, SetArgs setArgs, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + AsyncExecutions setGet(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + AsyncExecutions setGet(K key, V value, SetArgs setArgs, ValueCondition condition); + /** * Set the string value of a key. * diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java index 663a8aeadb..6e6b7c8c45 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java @@ -19,10 +19,10 @@ */ package io.lettuce.core.cluster.api.sync; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; import java.util.List; +import java.util.Date; +import java.time.Instant; +import java.time.Duration; import io.lettuce.core.CopyArgs; import io.lettuce.core.ExpireArgs; @@ -34,6 +34,7 @@ import io.lettuce.core.ScanCursor; import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -77,6 +78,23 @@ public interface NodeSelectionKeyCommands { */ Executions del(K... keys); + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + Executions delex(K key); + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be {@code null}. + * @return Long integer-reply the number of keys that were removed. + */ + Executions delex(K key, ValueCondition condition); + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java index d52a332d55..d9136b99eb 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java @@ -19,16 +19,17 @@ */ package io.lettuce.core.cluster.api.sync; -import java.util.List; import java.util.Map; +import java.util.List; import io.lettuce.core.BitFieldArgs; import io.lettuce.core.GetExArgs; import io.lettuce.core.KeyValue; +import io.lettuce.core.LcsArgs; import io.lettuce.core.SetArgs; import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; -import io.lettuce.core.LcsArgs; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyValueStreamingChannel; /** @@ -255,6 +256,14 @@ public interface NodeSelectionStringCommands { */ Executions get(K key); + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. + */ + Executions digestKey(K key); + /** * Returns the bit value at offset in the string value stored at key. * @@ -372,6 +381,48 @@ public interface NodeSelectionStringCommands { */ Executions set(K key, V value); + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + Executions set(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + Executions set(K key, V value, SetArgs setArgs, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + Executions setGet(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + Executions setGet(K key, V value, SetArgs setArgs, ValueCondition condition); + /** * Set the string value of a key. * diff --git a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java index b25e15f785..028fc98503 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandKeyword.java +++ b/src/main/java/io/lettuce/core/protocol/CommandKeyword.java @@ -58,7 +58,7 @@ CAS, EF, ELE, SETATTR, M, NOQUANT, BIN, Q8, FILTER, FILTER_EF( NOSTEM, PHONETIC, WEIGHT, SEPARATOR, CASESENSITIVE, WITHSUFFIXTRIE, INDEXEMPTY, INDEXMISSING, DD, SORTBY, WITHCOUNT, SUMMARIZE, FRAGS, HIGHLIGHT, TAGS, DIALECT, PARAMS, TIMEOUT, SLOP, EXPLAINSCORE, PAYLOAD, - SCORER, EXPANDER, INORDER, RETURN, INFIELDS, INKEYS, WITHSORTKEYS, WITHPAYLOADS, NOSTOPWORDS, VERBATIM, NOCONTENT, FLAT, SPHERICAL, HNSW, DIM, DISTANCE_METRIC, FLOAT32, FLOAT64, L2, COSINE, IP, WITHCURSOR, MAXIDLE, ADDSCORES, GROUPBY, APPLY, READ, DEL, TERMS, DISTANCE; + SCORER, EXPANDER, INORDER, RETURN, INFIELDS, INKEYS, WITHSORTKEYS, WITHPAYLOADS, NOSTOPWORDS, VERBATIM, NOCONTENT, FLAT, SPHERICAL, HNSW, DIM, DISTANCE_METRIC, FLOAT32, FLOAT64, L2, COSINE, IP, WITHCURSOR, MAXIDLE, ADDSCORES, GROUPBY, APPLY, READ, DEL, TERMS, DISTANCE, IFEQ, IFNE, IFDEQ, IFDNE; public final byte[] bytes; diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java index a6be8b4ee7..d24bfb8d74 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandType.java +++ b/src/main/java/io/lettuce/core/protocol/CommandType.java @@ -46,11 +46,11 @@ public enum CommandType implements ProtocolKeyword { // Keys - COPY, DEL, DUMP, EXISTS, HEXPIRE, EXPIRE, HEXPIREAT, EXPIREAT, HEXPIRETIME, EXPIRETIME, KEYS, MIGRATE, MOVE, OBJECT, HPERSIST, PERSIST, PEXPIRE, HPEXPIRE, PEXPIREAT, HPEXPIREAT, PEXPIRETIME, HPEXPIRETIME, PTTL, HPTTL, RANDOMKEY, RENAME, RENAMENX, RESTORE, TOUCH, TTL, HTTL, TYPE, SCAN, UNLINK, + COPY, DEL, DELEX, DUMP, EXISTS, HEXPIRE, EXPIRE, HEXPIREAT, EXPIREAT, HEXPIRETIME, EXPIRETIME, KEYS, MIGRATE, MOVE, OBJECT, HPERSIST, PERSIST, PEXPIRE, HPEXPIRE, PEXPIREAT, HPEXPIREAT, PEXPIRETIME, HPEXPIRETIME, PTTL, HPTTL, RANDOMKEY, RENAME, RENAMENX, RESTORE, TOUCH, TTL, HTTL, TYPE, SCAN, UNLINK, // String - APPEND, GET, GETDEL, GETEX, GETRANGE, GETSET, MGET, MSET, MSETNX, SET, SETEX, PSETEX, SETNX, SETRANGE, STRLEN, STRALGO, LCS, + APPEND, GET, GETDEL, GETEX, GETRANGE, GETSET, DIGEST, MGET, MSET, MSETNX, SET, SETEX, PSETEX, SETNX, SETRANGE, STRLEN, STRALGO, LCS, // Numeric diff --git a/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java b/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java index 46d2a6650c..62515e8ada 100644 --- a/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java +++ b/src/main/java/io/lettuce/core/protocol/ReadOnlyCommands.java @@ -72,7 +72,7 @@ public static ReadOnlyPredicate asPredicate() { enum CommandName { ASKING, BITCOUNT, BITPOS, CLIENT, COMMAND, DUMP, ECHO, EVAL_RO, EVALSHA_RO, EXISTS, FCALL_RO, // - GEODIST, GEOPOS, GEORADIUS_RO, GEORADIUSBYMEMBER_RO, GEOSEARCH, GEOHASH, GET, GETBIT, // + GEODIST, GEOPOS, GEORADIUS_RO, GEORADIUSBYMEMBER_RO, GEOSEARCH, GEOHASH, GET, DIGEST, GETBIT, // GETRANGE, HEXISTS, HGET, HGETALL, HKEYS, HLEN, HMGET, HRANDFIELD, HSCAN, HSTRLEN, // HVALS, INFO, KEYS, LINDEX, LLEN, LPOS, LRANGE, SORT_RO, MGET, PFCOUNT, PTTL, // RANDOMKEY, READWRITE, SCAN, SCARD, SCRIPT, // diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt index acd55b4d40..8346e10079 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-Present, Redis Ltd. and Contributors + * Copyright 2017-Present, Redis Ltd. and Contributors * All rights reserved. * * Licensed under the MIT License. @@ -20,11 +20,22 @@ package io.lettuce.core.api.coroutines -import io.lettuce.core.* -import kotlinx.coroutines.flow.Flow -import java.time.Duration +import java.util.Date import java.time.Instant -import java.util.* +import java.time.Duration + +import io.lettuce.core.CopyArgs +import io.lettuce.core.ExpireArgs +import io.lettuce.core.KeyScanArgs +import io.lettuce.core.KeyScanCursor +import io.lettuce.core.MigrateArgs +import io.lettuce.core.RestoreArgs +import io.lettuce.core.ScanArgs +import io.lettuce.core.ScanCursor +import io.lettuce.core.SortArgs +import io.lettuce.core.ValueCondition +import io.lettuce.core.ExperimentalLettuceCoroutinesApi +import kotlinx.coroutines.flow.Flow /** * Coroutine executed commands for Keys (Key manipulation/querying). @@ -67,6 +78,23 @@ interface RedisKeyCoroutinesCommands { */ suspend fun del(vararg keys: K): Long? + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + suspend fun delex(key: K): Long? + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be `null`. + * @return Long integer-reply the number of keys that were removed. + */ + suspend fun delex(key: K, condition: ValueCondition): Long? + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt index 5195e705e0..62feab2046 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt @@ -52,6 +52,12 @@ internal class RedisKeyCoroutinesCommandsImpl(internal val ops override suspend fun unlink(vararg keys: K): Long? = ops.unlink(*keys).awaitFirstOrNull() + override suspend fun delex(key: K): Long? = + ops.delex(key).awaitFirstOrNull() + + override suspend fun delex(key: K, condition: ValueCondition): Long? = + ops.delex(key, condition).awaitFirstOrNull() + override suspend fun dump(key: K): ByteArray? = ops.dump(key).awaitFirstOrNull() override suspend fun exists(vararg keys: K): Long? = diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt index f6500da836..1911043327 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt @@ -20,7 +20,14 @@ package io.lettuce.core.api.coroutines -import io.lettuce.core.* +import io.lettuce.core.BitFieldArgs +import io.lettuce.core.GetExArgs +import io.lettuce.core.KeyValue +import io.lettuce.core.LcsArgs +import io.lettuce.core.SetArgs +import io.lettuce.core.StringMatchResult +import io.lettuce.core.ValueCondition +import io.lettuce.core.ExperimentalLettuceCoroutinesApi import kotlinx.coroutines.flow.Flow /** @@ -201,6 +208,14 @@ interface RedisStringCoroutinesCommands { */ suspend fun get(key: K): V? + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or `null` when `key` does not exist. + */ + suspend fun digestKey(key: K): String? + /** * Returns the bit value at offset in the string value stored at key. * @@ -309,6 +324,48 @@ interface RedisStringCoroutinesCommands { */ suspend fun set(key: K, value: V): String? + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be `null`. + * @return String simple-string-reply `OK` if `SET` was executed; `null` if the operation was aborted. + */ + suspend fun set(key: K, value: V, condition: ValueCondition): String? + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be `null`. + * @return String simple-string-reply `OK` if `SET` was executed; `null` if the operation was aborted. + */ + suspend fun set(key: K, value: V, setArgs: SetArgs, condition: ValueCondition): String? + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be `null`. + * @return V bulk-string-reply the previous value if the key existed, or `null` when `key` did not exist. + */ + suspend fun setGet(key: K, value: V, condition: ValueCondition): V? + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be `null`. + * @return V bulk-string-reply the previous value if the key existed, or `null` when `key` did not exist. + */ + suspend fun setGet(key: K, value: V, setArgs: SetArgs, condition: ValueCondition): V? + /** * Set the string value of a key. * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommandsImpl.kt index 8bfbb56d4d..45c0507150 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommandsImpl.kt @@ -70,6 +70,9 @@ internal class RedisStringCoroutinesCommandsImpl(internal val override suspend fun get(key: K): V? = ops.get(key).awaitFirstOrNull() + override suspend fun digestKey(key: K): String? = + ops.digestKey(key).awaitFirstOrNull() + override suspend fun getbit(key: K, offset: Long): Long? = ops.getbit(key, offset).awaitFirstOrNull() @@ -98,6 +101,26 @@ internal class RedisStringCoroutinesCommandsImpl(internal val override suspend fun msetnx(map: Map): Boolean? = ops.msetnx(map).awaitFirstOrNull() + override suspend fun set(key: K, value: V, condition: ValueCondition): String? = + ops.set(key, value, condition).awaitFirstOrNull() + + override suspend fun set( + key: K, + value: V, + setArgs: SetArgs, + condition: ValueCondition + ): String? = ops.set(key, value, setArgs, condition).awaitFirstOrNull() + + override suspend fun setGet(key: K, value: V, condition: ValueCondition): V? = + ops.setGet(key, value, condition).awaitFirstOrNull() + + override suspend fun setGet( + key: K, + value: V, + setArgs: SetArgs, + condition: ValueCondition + ): V? = ops.setGet(key, value, setArgs, condition).awaitFirstOrNull() + override suspend fun set(key: K, value: V): String? = ops.set(key, value).awaitFirstOrNull() override suspend fun set(key: K, value: V, setArgs: SetArgs): String? = ops.set(key, value, setArgs).awaitFirstOrNull() diff --git a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java index 81217833fb..cb8706e872 100644 --- a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java @@ -19,12 +19,22 @@ */ package io.lettuce.core.api; -import java.time.Duration; -import java.time.Instant; -import java.util.Date; import java.util.List; +import java.util.Date; +import java.time.Instant; +import java.time.Duration; -import io.lettuce.core.*; +import io.lettuce.core.CopyArgs; +import io.lettuce.core.ExpireArgs; +import io.lettuce.core.KeyScanArgs; +import io.lettuce.core.KeyScanCursor; +import io.lettuce.core.MigrateArgs; +import io.lettuce.core.RestoreArgs; +import io.lettuce.core.ScanArgs; +import io.lettuce.core.ScanCursor; +import io.lettuce.core.SortArgs; +import io.lettuce.core.StreamScanCursor; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -67,6 +77,23 @@ public interface RedisKeyCommands { */ Long del(K... keys); + /** + * Delete the specified key conditionally. + * + * @param key the key. + * @return Long integer-reply the number of keys that were removed. + */ + Long delex(K key); + + /** + * Delete the specified key if the compare condition matches. + * + * @param key the key. + * @param condition the compare condition, must not be {@code null}. + * @return Long integer-reply the number of keys that were removed. + */ + Long delex(K key, ValueCondition condition); + /** * Unlink one or more keys (non blocking DEL). * diff --git a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java index d4530aa886..ab88715adf 100644 --- a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java @@ -19,10 +19,17 @@ */ package io.lettuce.core.api; -import java.util.List; import java.util.Map; +import java.util.List; -import io.lettuce.core.*; +import io.lettuce.core.BitFieldArgs; +import io.lettuce.core.GetExArgs; +import io.lettuce.core.KeyValue; +import io.lettuce.core.LcsArgs; +import io.lettuce.core.SetArgs; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; +import io.lettuce.core.ValueCondition; import io.lettuce.core.output.KeyValueStreamingChannel; /** @@ -248,6 +255,14 @@ public interface RedisStringCommands { */ V get(K key); + /** + * Return the XXH3 64-bit digest of the string value stored at a key as a 16-character hex string. + * + * @param key the key. + * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. + */ + String digestKey(K key); + /** * Returns the bit value at offset in the string value stored at key. * @@ -365,6 +380,48 @@ public interface RedisStringCommands { */ String set(K key, V value); + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + String set(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition. + * + * @param key the key. + * @param value the value. + * @param setArgs the setArgs. + * @param condition the compare condition, must not be {@code null}. + * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. + */ + String set(K key, V value, SetArgs setArgs, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + V setGet(K key, V value, ValueCondition condition); + + /** + * Set the string value of a key with a compare condition and return its old value. + * + * @param key the key. + * @param value the value. + * @param setArgs the command arguments. + * @param condition the compare condition, must not be {@code null}. + * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. + */ + V setGet(K key, V value, SetArgs setArgs, ValueCondition condition); + /** * Set the string value of a key. * From 2f1c65285c01c690536f802f70fa30d8982c1ab4 Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 12:20:36 +0200 Subject: [PATCH 2/8] Add tests --- .../KeyClusterCommandIntegrationTests.java | 52 ++++++ .../commands/KeyCommandIntegrationTests.java | 55 +++++- .../StringCommandIntegrationTests.java | 164 +++++++++++++++++- 3 files changed, 263 insertions(+), 8 deletions(-) diff --git a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java index 757df6ea79..78dbab3272 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java @@ -16,6 +16,11 @@ import io.lettuce.core.cluster.ClusterTestUtil; import io.lettuce.core.cluster.api.StatefulRedisClusterConnection; import io.lettuce.test.LettuceExtension; +import io.lettuce.core.ValueCondition; + +import io.lettuce.test.condition.RedisConditions; +import org.junit.jupiter.api.Assumptions; + import io.lettuce.test.condition.EnabledOnCommand; /** @@ -90,6 +95,53 @@ void unlink() { assertThat(redis.unlink(key, "a", "b")).isEqualTo(3); assertThat(redis.exists(key)).isEqualTo(0); + + } + + @Test + @EnabledOnCommand("DELEX") + void delex_unconditional_and_digest_guarded_cluster() { + String k = "k:delex"; + // unconditional delete + + redis.set(k, "v"); + assertThat(redis.delex(k)).isEqualTo(1); + assertThat(redis.exists(k)).isEqualTo(0); + + // digest-guarded delete + redis.set(k, "bar"); + String d = redis.digestKey(k); + // wrong condition: digestNotEqualHex (should abort) + assertThat(redis.delex(k, ValueCondition. digestNotEqualHex(d))).isEqualTo(0); + assertThat(redis.exists(k)).isEqualTo(1); + // right condition: digestEqualHex (should delete) + assertThat(redis.delex(k, ValueCondition. digestEqualHex(d))).isEqualTo(1); + assertThat(redis.exists(k)).isEqualTo(0); + } + + @Test + @EnabledOnCommand("DELEX") + void delex_missing_cluster_returns_0() { + + String k = "k:missing-cluster"; + assertThat(redis.delex(k)).isEqualTo(0); + } + + @Test + @EnabledOnCommand("DELEX") + void delex_value_equal_notEqual_cluster() { + + String k = "k:delex-eq-cluster"; + redis.set(k, "v1"); + // wrong equality -> abort + assertThat(redis.delex(k, ValueCondition.equal("nope"))).isEqualTo(0); + // correct equality -> delete + assertThat(redis.delex(k, ValueCondition.equal("v1"))).isEqualTo(1); + // not-equal that fails (after deletion, recreate) + redis.set(k, "v2"); + assertThat(redis.delex(k, ValueCondition.notEqual("v2"))).isEqualTo(0); + // not-equal that succeeds + assertThat(redis.delex(k, ValueCondition.notEqual("other"))).isEqualTo(1); } } diff --git a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java index 2e1ba647c3..dc5fda3df8 100644 --- a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java @@ -33,10 +33,12 @@ import javax.inject.Inject; -import org.junit.Ignore; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; +import io.lettuce.test.condition.RedisConditions; +import org.junit.jupiter.api.Assumptions; + import io.lettuce.core.CopyArgs; import io.lettuce.core.ExpireArgs; import io.lettuce.core.KeyScanArgs; @@ -46,6 +48,8 @@ import io.lettuce.core.ScanCursor; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.TestSupport; +import io.lettuce.core.ValueCondition; + import io.lettuce.core.api.sync.RedisCommands; import io.lettuce.test.LettuceExtension; import io.lettuce.test.ListStreamingAdapter; @@ -612,4 +616,53 @@ void setup100KeyValues(Set expect) { } } + @Test + @EnabledOnCommand("DELEX") + void delex_unconditional_and_digest_guarded() { + String k = "k:delex"; + // unconditional delete + + redis.set(k, value); + assertThat(redis.delex(k)).isEqualTo(1); + assertThat(redis.exists(k)).isEqualTo(0); + + // digest-guarded delete + redis.set(k, "bar"); + String d = redis.digestKey(k); // new DIGEST command + // wrong condition: digestNotEqualHex (should abort) + assertThat(redis.delex(k, ValueCondition. digestNotEqualHex(d))).isEqualTo(0); + assertThat(redis.exists(k)).isEqualTo(1); + // right condition: digestEqualHex (should delete) + assertThat(redis.delex(k, ValueCondition. digestEqualHex(d))).isEqualTo(1); + assertThat(redis.exists(k)).isEqualTo(0); + } + + @Test + @EnabledOnCommand("DELEX") + void delex_with_value_equal_notEqual() { + + String k = "k:delex-eq"; + redis.set(k, "v1"); + // wrong equality -> abort + assertThat(redis.delex(k, ValueCondition.equal("nope"))).isEqualTo(0); + // correct equality -> delete + assertThat(redis.delex(k, ValueCondition.equal("v1"))).isEqualTo(1); + // not-equal that fails (after deletion, recreate) + redis.set(k, "v2"); + assertThat(redis.delex(k, ValueCondition.notEqual("v2"))).isEqualTo(0); + // not-equal that succeeds + assertThat(redis.delex(k, ValueCondition.notEqual("other"))).isEqualTo(1); + } + + @Test + @EnabledOnCommand("DELEX") + void delex_on_missing_returns_0_for_any_condition() { + String k = "k:missing"; + assertThat(redis.delex(k)).isEqualTo(0); + + assertThat(redis.delex(k, ValueCondition.exists())).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.equal("x"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition. digestEqualHex("0000000000000000"))).isEqualTo(0); + } + } diff --git a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java index a90c3c27ad..590315e69a 100644 --- a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java @@ -25,13 +25,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.lang.reflect.Proxy; import java.time.Duration; import java.time.Instant; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import io.lettuce.test.condition.RedisConditions; + import javax.inject.Inject; import org.junit.jupiter.api.Assumptions; @@ -42,13 +43,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import io.lettuce.core.*; -import io.lettuce.core.api.StatefulConnection; -import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; -import io.lettuce.core.dynamic.Commands; -import io.lettuce.core.dynamic.RedisCommandFactory; -import io.lettuce.core.dynamic.annotation.Command; -import io.lettuce.core.dynamic.annotation.Param; import io.lettuce.test.KeyValueStreamingAdapter; import io.lettuce.test.LettuceExtension; import io.lettuce.test.condition.EnabledOnCommand; @@ -487,4 +482,159 @@ void lcsMinMatchLenIdxMatchLen() { assertThat(matchResult.getMatchString()).isNullOrEmpty(); } + @Test + @EnabledOnCommand("DIGEST") + void digestKey_returnsHex_and_changesOnValueChange() { + + assertThat(redis.digestKey("d")).isNull(); + redis.set("d", "v1"); + String d1 = redis.digestKey("d"); + assertThat(d1).isNotNull().matches("[0-9a-f]{16}"); + assertThat(redis.digestKey("d")).isEqualTo(d1); + redis.set("d", "v2"); + assertThat(redis.digestKey("d")).isNotEqualTo(d1); + } + + @Test + @EnabledOnCommand("DELEX") + void set_with_ValueCondition_exists_notExists() { + + String k = "k:nx-xx"; + // key does not exist: EXISTS/XX should abort + assertThat(redis.set(k, "v", ValueCondition. exists())).isNull(); + // NOT_EXISTS/NX should succeed + assertThat(redis.set(k, "v", ValueCondition. notExists())).isEqualTo("OK"); + // NOT_EXISTS/NX should now abort + assertThat(redis.set(k, "v2", ValueCondition. notExists())).isNull(); + // EXISTS/XX should succeed + assertThat(redis.set(k, "v2", ValueCondition. exists())).isEqualTo("OK"); + assertThat(redis.get(k)).isEqualTo("v2"); + } + + @Test + @EnabledOnCommand("DELEX") + void set_with_ValueCondition_equal_notEqual() { + + String k = "k:ifeq-ifne"; + redis.set(k, "v1"); + // wrong equality value -> abort + assertThat(redis.set(k, "v2", ValueCondition.equal("nope"))).isNull(); + assertThat(redis.get(k)).isEqualTo("v1"); + // correct equality value -> success + assertThat(redis.set(k, "v2", ValueCondition.equal("v1"))).isEqualTo("OK"); + assertThat(redis.get(k)).isEqualTo("v2"); + // not-equal that fails (current is v2) + assertThat(redis.set(k, "v3", ValueCondition.notEqual("v2"))).isNull(); + assertThat(redis.get(k)).isEqualTo("v2"); + // not-equal that succeeds + assertThat(redis.set(k, "v3", ValueCondition.notEqual("other"))).isEqualTo("OK"); + assertThat(redis.get(k)).isEqualTo("v3"); + } + + @Test + @EnabledOnCommand("DELEX") + void setget_with_ValueCondition_returnsPreviousValue() { + + String k = "k:setget"; + redis.set(k, "A"); + // condition mismatch -> server returns previous value, value unchanged + assertThat(redis.setGet(k, "B", ValueCondition.equal("X"))).isEqualTo("A"); + assertThat(redis.get(k)).isEqualTo("A"); + // condition match -> returns previous and updates + assertThat(redis.setGet(k, "C", ValueCondition.equal("A"))).isEqualTo("A"); + assertThat(redis.get(k)).isEqualTo("C"); + } + + @Test + @EnabledOnCommand("DIGEST") + void set_with_digest_conditions() { + + String k = "k:digest"; + redis.set(k, "C"); + String d = redis.digestKey(k); + assertThat(d).isNotNull(); + // digest equal -> success + assertThat(redis.set(k, "D", ValueCondition. digestEqualHex(d))).isEqualTo("OK"); + // reusing old digest equal -> abort + assertThat(redis.set(k, "E", ValueCondition. digestEqualHex(d))).isNull(); + // digest not equal (against old digest) -> success + assertThat(redis.set(k, "F", ValueCondition. digestNotEqualHex(d))).isEqualTo("OK"); + } + + @Test + @EnabledOnCommand("DIGEST") + void set_with_digestNotEqual_fails_when_equal() { + String k = "k:dig-not-eq-fail"; + + redis.set(k, "X"); + String d = redis.digestKey(k); + assertThat(redis.set(k, "Y", ValueCondition. digestNotEqualHex(d))).isNull(); + assertThat(redis.get(k)).isEqualTo("X"); + } + + @Test + @EnabledOnCommand("DIGEST") + void setget_with_digest_conditions() { + String k = "k:setget-dig"; + + redis.set(k, "A"); + String dA = redis.digestKey(k); + // match -> returns previous and updates + assertThat(redis.setGet(k, "B", ValueCondition. digestEqualHex(dA))).isEqualTo("A"); + assertThat(redis.get(k)).isEqualTo("B"); + // mismatch (using old digest) -> returns previous and does not update + assertThat(redis.setGet(k, "C", ValueCondition. digestEqualHex(dA))).isEqualTo("B"); + assertThat(redis.get(k)).isEqualTo("B"); + } + + @Test + @EnabledOnCommand("DIGEST") + void digestKey_emptyValue() { + String k = "k:empty"; + redis.set(k, ""); + String d = redis.digestKey(k); + + assertThat(d).isNotNull().matches("[0-9a-f]{16}"); + assertThat(redis.digestKey(k)).isEqualTo(d); + } + + @Test + void setget_with_exists_notExists_conditions() { + Assumptions.assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.3.224")); + + String k1 = "k:setget-nx"; + // NX when missing -> perform set, returns previous (null) + assertThat(redis.setGet(k1, "A", ValueCondition.notExists())).isNull(); + assertThat(redis.get(k1)).isEqualTo("A"); + // NX when present -> no set, returns previous + assertThat(redis.setGet(k1, "B", ValueCondition.notExists())).isEqualTo("A"); + assertThat(redis.get(k1)).isEqualTo("A"); + + String k2 = "k:setget-xx"; + // XX when missing -> no-op, returns previous (null) + redis.del(k2); + assertThat(redis.setGet(k2, "X", ValueCondition.exists())).isNull(); + assertThat(redis.get(k2)).isNull(); + // XX when present -> set, returns previous + redis.set(k2, "Y"); + assertThat(redis.setGet(k2, "Z", ValueCondition.exists())).isEqualTo("Y"); + assertThat(redis.get(k2)).isEqualTo("Z"); + } + + @Test + void set_with_SetArgs_and_ValueCondition_combination() { + String k = "k:set-args-cond"; + // NX + EX should set with TTL + Assumptions.assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.3.224")); + + assertThat(redis.set(k, "v1", ex(100), ValueCondition.notExists())).isEqualTo("OK"); + assertThat(redis.ttl(k)).isGreaterThan(0); + // NX when present should abort and keep value/ttl + Long ttlBefore = redis.ttl(k); + assertThat(redis.set(k, "v2", ex(100), ValueCondition.notExists())).isNull(); + assertThat(redis.get(k)).isEqualTo("v1"); + assertThat(redis.ttl(k)).isGreaterThan(0); + assertThat(redis.ttl(k)).isLessThanOrEqualTo(ttlBefore); + } + } From 5c8ad5e3283136bf69957613332b240a42b00e93 Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 12:42:33 +0200 Subject: [PATCH 3/8] Remove unconditional delex - use del instead --- .../lettuce/core/AbstractRedisAsyncCommands.java | 5 ----- .../core/AbstractRedisReactiveCommands.java | 5 ----- .../io/lettuce/core/RedisCommandBuilder.java | 7 ------- .../core/api/async/RedisKeyAsyncCommands.java | 8 -------- .../api/reactive/RedisKeyReactiveCommands.java | 8 -------- .../lettuce/core/api/sync/RedisKeyCommands.java | 8 -------- .../api/async/NodeSelectionKeyAsyncCommands.java | 16 +++------------- .../api/sync/NodeSelectionKeyCommands.java | 8 -------- .../api/coroutines/RedisKeyCoroutinesCommands.kt | 8 -------- .../coroutines/RedisKeyCoroutinesCommandsImpl.kt | 3 --- .../io/lettuce/core/api/RedisKeyCommands.java | 8 -------- .../java/io/lettuce/apigenerator/Constants.java | 6 +----- .../KeyClusterCommandIntegrationTests.java | 5 ++--- .../commands/KeyCommandIntegrationTests.java | 4 ++-- 14 files changed, 8 insertions(+), 91 deletions(-) diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index f1452671f3..40d523973d 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -741,11 +741,6 @@ public RedisFuture del(Iterable keys) { return dispatch(commandBuilder.del(keys)); } - @Override - public RedisFuture delex(K key) { - return dispatch(commandBuilder.delex(key)); - } - @Override public RedisFuture delex(K key, ValueCondition condition) { return dispatch(commandBuilder.delex(key, condition)); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index 43963f5ad1..b21a81fd84 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -811,11 +811,6 @@ public Mono del(Iterable keys) { return createMono(() -> commandBuilder.del(keys)); } - @Override - public Mono delex(K key) { - return createMono(() -> commandBuilder.delex(key)); - } - @Override public Mono delex(K key, ValueCondition condition) { return createMono(() -> commandBuilder.delex(key, condition)); diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 8ec63544a2..8ed7506444 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -940,13 +940,6 @@ Command del(Iterable keys) { return createCommand(DEL, new IntegerOutput<>(codec), args); } - Command delex(K key) { - notNullKey(key); - - CommandArgs args = new CommandArgs<>(codec).addKey(key); - return createCommand(DELEX, new IntegerOutput<>(codec), args); - } - Command delex(K key, ValueCondition condition) { notNullKey(key); LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); diff --git a/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java index c66ddd68e6..f82b2219e1 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java @@ -79,14 +79,6 @@ public interface RedisKeyAsyncCommands { */ RedisFuture del(K... keys); - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - RedisFuture delex(K key); - /** * Delete the specified key if the compare condition matches. * diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java index 8f335b1c04..ecc0a55da7 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java @@ -79,14 +79,6 @@ public interface RedisKeyReactiveCommands { */ Mono del(K... keys); - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - Mono delex(K key); - /** * Delete the specified key if the compare condition matches. * diff --git a/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java index f9a5a3b980..c74ae69ef2 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java @@ -78,14 +78,6 @@ public interface RedisKeyCommands { */ Long del(K... keys); - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - Long delex(K key); - /** * Delete the specified key if the compare condition matches. * diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java index c224ad93c1..0fc25bcac8 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java @@ -19,14 +19,12 @@ */ package io.lettuce.core.cluster.api.async; -import java.util.List; -import java.util.Date; -import java.time.Instant; import java.time.Duration; - +import java.time.Instant; +import java.util.Date; +import java.util.List; import io.lettuce.core.CopyArgs; import io.lettuce.core.ExpireArgs; -import io.lettuce.core.KeyScanArgs; import io.lettuce.core.KeyScanCursor; import io.lettuce.core.MigrateArgs; import io.lettuce.core.RestoreArgs; @@ -78,14 +76,6 @@ public interface NodeSelectionKeyAsyncCommands { */ AsyncExecutions del(K... keys); - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - AsyncExecutions delex(K key); - /** * Delete the specified key if the compare condition matches. * diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java index 6e6b7c8c45..f1eaaa89cd 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java @@ -78,14 +78,6 @@ public interface NodeSelectionKeyCommands { */ Executions del(K... keys); - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - Executions delex(K key); - /** * Delete the specified key if the compare condition matches. * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt index 8346e10079..5b7d1c4350 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt @@ -78,14 +78,6 @@ interface RedisKeyCoroutinesCommands { */ suspend fun del(vararg keys: K): Long? - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - suspend fun delex(key: K): Long? - /** * Delete the specified key if the compare condition matches. * diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt index 62feab2046..3c056d1adf 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommandsImpl.kt @@ -52,9 +52,6 @@ internal class RedisKeyCoroutinesCommandsImpl(internal val ops override suspend fun unlink(vararg keys: K): Long? = ops.unlink(*keys).awaitFirstOrNull() - override suspend fun delex(key: K): Long? = - ops.delex(key).awaitFirstOrNull() - override suspend fun delex(key: K, condition: ValueCondition): Long? = ops.delex(key, condition).awaitFirstOrNull() diff --git a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java index cb8706e872..6ffba2d0f5 100644 --- a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java @@ -77,14 +77,6 @@ public interface RedisKeyCommands { */ Long del(K... keys); - /** - * Delete the specified key conditionally. - * - * @param key the key. - * @return Long integer-reply the number of keys that were removed. - */ - Long delex(K key); - /** * Delete the specified key if the compare condition matches. * diff --git a/src/test/java/io/lettuce/apigenerator/Constants.java b/src/test/java/io/lettuce/apigenerator/Constants.java index 7cee3129ac..2d457f912d 100644 --- a/src/test/java/io/lettuce/apigenerator/Constants.java +++ b/src/test/java/io/lettuce/apigenerator/Constants.java @@ -27,11 +27,7 @@ */ class Constants { - public static final String[] TEMPLATE_NAMES = { "BaseRedisCommands", "RedisAclCommands", "RedisFunctionCommands", - "RedisGeoCommands", "RedisHashCommands", "RedisHLLCommands", "RedisKeyCommands", "RedisListCommands", - "RedisScriptingCommands", "RedisSentinelCommands", "RedisServerCommands", "RedisSetCommands", - "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands", - "RedisJsonCommands", "RedisVectorSetCommands", "RediSearchCommands" }; + public static final String[] TEMPLATE_NAMES = { "RedisKeyCommands" }; public static final File TEMPLATES = new File("src/main/templates"); diff --git a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java index 78dbab3272..1110c6b3a4 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java @@ -95,7 +95,6 @@ void unlink() { assertThat(redis.unlink(key, "a", "b")).isEqualTo(3); assertThat(redis.exists(key)).isEqualTo(0); - } @Test @@ -105,7 +104,7 @@ void delex_unconditional_and_digest_guarded_cluster() { // unconditional delete redis.set(k, "v"); - assertThat(redis.delex(k)).isEqualTo(1); + assertThat(redis.del(k)).isEqualTo(1); assertThat(redis.exists(k)).isEqualTo(0); // digest-guarded delete @@ -124,7 +123,7 @@ void delex_unconditional_and_digest_guarded_cluster() { void delex_missing_cluster_returns_0() { String k = "k:missing-cluster"; - assertThat(redis.delex(k)).isEqualTo(0); + assertThat(redis.del(k)).isEqualTo(0); } @Test diff --git a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java index dc5fda3df8..4a4f382098 100644 --- a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java @@ -623,7 +623,7 @@ void delex_unconditional_and_digest_guarded() { // unconditional delete redis.set(k, value); - assertThat(redis.delex(k)).isEqualTo(1); + assertThat(redis.del(k)).isEqualTo(1); assertThat(redis.exists(k)).isEqualTo(0); // digest-guarded delete @@ -658,7 +658,7 @@ void delex_with_value_equal_notEqual() { @EnabledOnCommand("DELEX") void delex_on_missing_returns_0_for_any_condition() { String k = "k:missing"; - assertThat(redis.delex(k)).isEqualTo(0); + assertThat(redis.del(k)).isEqualTo(0); assertThat(redis.delex(k, ValueCondition.exists())).isEqualTo(0); assertThat(redis.delex(k, ValueCondition.equal("x"))).isEqualTo(0); From 9dcad7393f0c53ee7ffd8fc1d2e972d3e3330c2a Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 13:27:03 +0200 Subject: [PATCH 4/8] Fix failing test --- .../lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java index f34205de3d..28bc753066 100644 --- a/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java +++ b/src/test/java/io/lettuce/core/cluster/ClusterReadOnlyCommandsUnitTests.java @@ -20,7 +20,7 @@ class ClusterReadOnlyCommandsUnitTests { @Test void testCount() { - assertThat(ClusterReadOnlyCommands.getReadOnlyCommands()).hasSize(100); + assertThat(ClusterReadOnlyCommands.getReadOnlyCommands()).hasSize(101); } @Test From 29b48eb3ffe00fec1c7f572e8bc774c9f6f2fab8 Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 13:51:49 +0200 Subject: [PATCH 5/8] Mark new API as experimental --- src/main/java/io/lettuce/core/ValueCondition.java | 4 +++- .../io/lettuce/core/api/async/RedisKeyAsyncCommands.java | 2 ++ .../io/lettuce/core/api/async/RedisStringAsyncCommands.java | 6 ++++++ .../lettuce/core/api/reactive/RedisKeyReactiveCommands.java | 2 ++ .../core/api/reactive/RedisStringReactiveCommands.java | 6 ++++++ .../java/io/lettuce/core/api/sync/RedisKeyCommands.java | 2 ++ .../java/io/lettuce/core/api/sync/RedisStringCommands.java | 6 ++++++ .../cluster/api/async/NodeSelectionKeyAsyncCommands.java | 3 +++ .../core/cluster/api/sync/NodeSelectionKeyCommands.java | 2 ++ .../core/api/coroutines/RedisKeyCoroutinesCommands.kt | 2 ++ .../core/api/coroutines/RedisStringCoroutinesCommands.kt | 6 ++++++ .../templates/io/lettuce/core/api/RedisKeyCommands.java | 2 ++ .../templates/io/lettuce/core/api/RedisStringCommands.java | 6 ++++++ 13 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/lettuce/core/ValueCondition.java b/src/main/java/io/lettuce/core/ValueCondition.java index b367f421a0..369750ec03 100644 --- a/src/main/java/io/lettuce/core/ValueCondition.java +++ b/src/main/java/io/lettuce/core/ValueCondition.java @@ -19,6 +19,8 @@ */ package io.lettuce.core; +import io.lettuce.core.annotations.Experimental; + /** * A compare condition to be used with commands that support conditional value checks (e.g. SET with IFEQ/IFNE/IFDEQ/IFDNE and * DELEX). This abstraction lets callers express value-based or digest-based comparisons without exploding method overloads. @@ -28,8 +30,8 @@ *

* * @param value type used for value-based comparisons - * @since 8.x */ +@Experimental public final class ValueCondition { /** diff --git a/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java index f82b2219e1..fd6bb1f3d1 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisKeyAsyncCommands.java @@ -35,6 +35,7 @@ import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.RedisFuture; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -86,6 +87,7 @@ public interface RedisKeyAsyncCommands { * @param condition the compare condition, must not be {@code null}. * @return Long integer-reply the number of keys that were removed. */ + @Experimental RedisFuture delex(K key, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java index e6a2677c5e..bc864994c8 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java @@ -30,6 +30,7 @@ import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyValueStreamingChannel; import io.lettuce.core.RedisFuture; @@ -263,6 +264,7 @@ public interface RedisStringAsyncCommands { * @param key the key. * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. */ + @Experimental RedisFuture digestKey(K key); /** @@ -390,6 +392,7 @@ public interface RedisStringAsyncCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental RedisFuture set(K key, V value, ValueCondition condition); /** @@ -401,6 +404,7 @@ public interface RedisStringAsyncCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental RedisFuture set(K key, V value, SetArgs setArgs, ValueCondition condition); /** @@ -411,6 +415,7 @@ public interface RedisStringAsyncCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental RedisFuture setGet(K key, V value, ValueCondition condition); /** @@ -422,6 +427,7 @@ public interface RedisStringAsyncCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental RedisFuture setGet(K key, V value, SetArgs setArgs, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java index ecc0a55da7..c9aa02e921 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisKeyReactiveCommands.java @@ -34,6 +34,7 @@ import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; import reactor.core.publisher.Flux; @@ -86,6 +87,7 @@ public interface RedisKeyReactiveCommands { * @param condition the compare condition, must not be {@code null}. * @return Long integer-reply the number of keys that were removed. */ + @Experimental Mono delex(K key, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java index 81a5b58443..64fdc1afe2 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java @@ -29,6 +29,7 @@ import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyValueStreamingChannel; import io.lettuce.core.Value; import reactor.core.publisher.Flux; @@ -264,6 +265,7 @@ public interface RedisStringReactiveCommands { * @param key the key. * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. */ + @Experimental Mono digestKey(K key); /** @@ -394,6 +396,7 @@ public interface RedisStringReactiveCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental Mono set(K key, V value, ValueCondition condition); /** @@ -405,6 +408,7 @@ public interface RedisStringReactiveCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental Mono set(K key, V value, SetArgs setArgs, ValueCondition condition); /** @@ -415,6 +419,7 @@ public interface RedisStringReactiveCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental Mono setGet(K key, V value, ValueCondition condition); /** @@ -426,6 +431,7 @@ public interface RedisStringReactiveCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental Mono setGet(K key, V value, SetArgs setArgs, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java index c74ae69ef2..3ad08ab476 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisKeyCommands.java @@ -35,6 +35,7 @@ import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -85,6 +86,7 @@ public interface RedisKeyCommands { * @param condition the compare condition, must not be {@code null}. * @return Long integer-reply the number of keys that were removed. */ + @Experimental Long delex(K key, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java index 651215bc91..c00a5d3d27 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java @@ -30,6 +30,7 @@ import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyValueStreamingChannel; /** @@ -262,6 +263,7 @@ public interface RedisStringCommands { * @param key the key. * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. */ + @Experimental String digestKey(K key); /** @@ -389,6 +391,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental String set(K key, V value, ValueCondition condition); /** @@ -400,6 +403,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental String set(K key, V value, SetArgs setArgs, ValueCondition condition); /** @@ -410,6 +414,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental V setGet(K key, V value, ValueCondition condition); /** @@ -421,6 +426,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental V setGet(K key, V value, SetArgs setArgs, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java index 0fc25bcac8..810fcbe8a2 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionKeyAsyncCommands.java @@ -33,6 +33,8 @@ import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.ValueCondition; +import io.lettuce.core.KeyScanArgs; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -83,6 +85,7 @@ public interface NodeSelectionKeyAsyncCommands { * @param condition the compare condition, must not be {@code null}. * @return Long integer-reply the number of keys that were removed. */ + @Experimental AsyncExecutions delex(K key, ValueCondition condition); /** diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java index f1eaaa89cd..5b0c318f1d 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionKeyCommands.java @@ -35,6 +35,7 @@ import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -85,6 +86,7 @@ public interface NodeSelectionKeyCommands { * @param condition the compare condition, must not be {@code null}. * @return Long integer-reply the number of keys that were removed. */ + @Experimental Executions delex(K key, ValueCondition condition); /** diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt index 5b7d1c4350..4d9efde582 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisKeyCoroutinesCommands.kt @@ -35,6 +35,7 @@ import io.lettuce.core.ScanCursor import io.lettuce.core.SortArgs import io.lettuce.core.ValueCondition import io.lettuce.core.ExperimentalLettuceCoroutinesApi +import io.lettuce.core.annotations.Experimental import kotlinx.coroutines.flow.Flow /** @@ -85,6 +86,7 @@ interface RedisKeyCoroutinesCommands { * @param condition the compare condition, must not be `null`. * @return Long integer-reply the number of keys that were removed. */ + @Experimental suspend fun delex(key: K, condition: ValueCondition): Long? /** diff --git a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt index 1911043327..6d72ed0dfc 100644 --- a/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt +++ b/src/main/kotlin/io/lettuce/core/api/coroutines/RedisStringCoroutinesCommands.kt @@ -28,6 +28,7 @@ import io.lettuce.core.SetArgs import io.lettuce.core.StringMatchResult import io.lettuce.core.ValueCondition import io.lettuce.core.ExperimentalLettuceCoroutinesApi +import io.lettuce.core.annotations.Experimental import kotlinx.coroutines.flow.Flow /** @@ -214,6 +215,7 @@ interface RedisStringCoroutinesCommands { * @param key the key. * @return String bulk-string-reply the hex digest of the key's value, or `null` when `key` does not exist. */ + @Experimental suspend fun digestKey(key: K): String? /** @@ -332,6 +334,7 @@ interface RedisStringCoroutinesCommands { * @param condition the compare condition, must not be `null`. * @return String simple-string-reply `OK` if `SET` was executed; `null` if the operation was aborted. */ + @Experimental suspend fun set(key: K, value: V, condition: ValueCondition): String? /** @@ -343,6 +346,7 @@ interface RedisStringCoroutinesCommands { * @param condition the compare condition, must not be `null`. * @return String simple-string-reply `OK` if `SET` was executed; `null` if the operation was aborted. */ + @Experimental suspend fun set(key: K, value: V, setArgs: SetArgs, condition: ValueCondition): String? /** @@ -353,6 +357,7 @@ interface RedisStringCoroutinesCommands { * @param condition the compare condition, must not be `null`. * @return V bulk-string-reply the previous value if the key existed, or `null` when `key` did not exist. */ + @Experimental suspend fun setGet(key: K, value: V, condition: ValueCondition): V? /** @@ -364,6 +369,7 @@ interface RedisStringCoroutinesCommands { * @param condition the compare condition, must not be `null`. * @return V bulk-string-reply the previous value if the key existed, or `null` when `key` did not exist. */ + @Experimental suspend fun setGet(key: K, value: V, setArgs: SetArgs, condition: ValueCondition): V? /** diff --git a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java index 6ffba2d0f5..398a6bad39 100644 --- a/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisKeyCommands.java @@ -35,6 +35,7 @@ import io.lettuce.core.SortArgs; import io.lettuce.core.StreamScanCursor; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyStreamingChannel; import io.lettuce.core.output.ValueStreamingChannel; @@ -84,6 +85,7 @@ public interface RedisKeyCommands { * @param condition the compare condition, must not be {@code null}. * @return Long integer-reply the number of keys that were removed. */ + @Experimental Long delex(K key, ValueCondition condition); /** diff --git a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java index ab88715adf..191fff6c6c 100644 --- a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java @@ -30,6 +30,7 @@ import io.lettuce.core.StrAlgoArgs; import io.lettuce.core.StringMatchResult; import io.lettuce.core.ValueCondition; +import io.lettuce.core.annotations.Experimental; import io.lettuce.core.output.KeyValueStreamingChannel; /** @@ -261,6 +262,7 @@ public interface RedisStringCommands { * @param key the key. * @return String bulk-string-reply the hex digest of the key's value, or {@code null} when {@code key} does not exist. */ + @Experimental String digestKey(K key); /** @@ -388,6 +390,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental String set(K key, V value, ValueCondition condition); /** @@ -399,6 +402,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return String simple-string-reply {@code OK} if {@code SET} was executed; {@code null} if the operation was aborted. */ + @Experimental String set(K key, V value, SetArgs setArgs, ValueCondition condition); /** @@ -409,6 +413,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental V setGet(K key, V value, ValueCondition condition); /** @@ -420,6 +425,7 @@ public interface RedisStringCommands { * @param condition the compare condition, must not be {@code null}. * @return V bulk-string-reply the previous value if the key existed, or {@code null} when {@code key} did not exist. */ + @Experimental V setGet(K key, V value, SetArgs setArgs, ValueCondition condition); /** From b921774ed3477dd47a44cc65c1b6fc927098ca5a Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 15:56:58 +0200 Subject: [PATCH 6/8] Refactor ValueCondition --- .../io/lettuce/core/RedisCommandBuilder.java | 37 +------- .../java/io/lettuce/core/ValueCondition.java | 93 ++++++++++++------- .../KeyClusterCommandIntegrationTests.java | 12 +-- .../commands/KeyCommandIntegrationTests.java | 17 ++-- .../StringCommandIntegrationTests.java | 58 ++++++------ .../MasterReplicaIntegrationTests.java | 25 +++++ 6 files changed, 134 insertions(+), 108 deletions(-) diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 8ed7506444..8a572442de 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -945,7 +945,7 @@ Command delex(K key, ValueCondition condition) { LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); CommandArgs args = new CommandArgs<>(codec).addKey(key); - buildConditionArgs(args, condition); + condition.build(args); return createCommand(DELEX, new IntegerOutput<>(codec), args); } @@ -2723,7 +2723,7 @@ Command set(K key, V value, ValueCondition condition) { LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); - buildConditionArgs(args, condition); + condition.build(args); return createCommand(SET, new StatusOutput<>(codec), args); } @@ -2734,7 +2734,7 @@ Command set(K key, V value, SetArgs setArgs, ValueCondition con CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); setArgs.build(args); - buildConditionArgs(args, condition); + condition.build(args); return createCommand(SET, new StatusOutput<>(codec), args); } @@ -2743,7 +2743,7 @@ Command setGet(K key, V value, ValueCondition condition) { LettuceAssert.notNull(condition, "ValueCondition " + MUST_NOT_BE_NULL); CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); - buildConditionArgs(args, condition); + condition.build(args); args.add(GET); return createCommand(SET, new ValueOutput<>(codec), args); } @@ -2755,7 +2755,7 @@ Command setGet(K key, V value, SetArgs setArgs, ValueCondition condi CommandArgs args = new CommandArgs<>(codec).addKey(key).addValue(value); setArgs.build(args); - buildConditionArgs(args, condition); + condition.build(args); args.add(GET); return createCommand(SET, new ValueOutput<>(codec), args); } @@ -4622,33 +4622,6 @@ Command zscanStreaming(ScoredValueStreamingChannel ch return zscanStreaming(channel, key, ScanCursor.INITIAL, scanArgs); } - private void buildConditionArgs(CommandArgs args, ValueCondition condition) { - switch (condition.kind()) { - case EXISTS: - args.add(XX); - break; - case NOT_EXISTS: - args.add(NX); - break; - case EQUAL: - args.add(IFEQ).addValue(condition.value()); - break; - case NOT_EQUAL: - args.add(IFNE).addValue(condition.value()); - break; - case DIGEST_EQUAL: - args.add(IFDEQ).add(condition.digestHex()); - break; - case DIGEST_NOT_EQUAL: - args.add(IFDNE).add(condition.digestHex()); - break; - case ALWAYS: - default: - // no additional args - break; - } - } - Command zscanStreaming(ScoredValueStreamingChannel channel, K key, ScanCursor scanCursor, ScanArgs scanArgs) { notNullKey(key); diff --git a/src/main/java/io/lettuce/core/ValueCondition.java b/src/main/java/io/lettuce/core/ValueCondition.java index 369750ec03..47b719ebf3 100644 --- a/src/main/java/io/lettuce/core/ValueCondition.java +++ b/src/main/java/io/lettuce/core/ValueCondition.java @@ -20,6 +20,8 @@ package io.lettuce.core; import io.lettuce.core.annotations.Experimental; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandKeyword; /** * A compare condition to be used with commands that support conditional value checks (e.g. SET with IFEQ/IFNE/IFDEQ/IFDNE and @@ -38,20 +40,39 @@ public final class ValueCondition { * The kind of condition represented by this instance. */ public enum Kind { - /** unconditional */ - ALWAYS, - /** key must exist */ - EXISTS, - /** key must not exist */ - NOT_EXISTS, + /** current value must equal provided value */ - EQUAL, + EQUAL(CommandKeyword.IFEQ, Param.VALUE), /** current value must not equal provided value */ - NOT_EQUAL, + NOT_EQUAL(CommandKeyword.IFNE, Param.VALUE), /** current value's digest must equal provided digest */ - DIGEST_EQUAL, + DIGEST_EQUAL(CommandKeyword.IFDEQ, Param.DIGEST), /** current value's digest must not equal provided digest */ - DIGEST_NOT_EQUAL + DIGEST_NOT_EQUAL(CommandKeyword.IFDNE, Param.DIGEST); + + private final CommandKeyword keyword; + + private final Param param; + + Kind(CommandKeyword keyword, Param param) { + this.keyword = keyword; + this.param = param; + } + + /** The protocol keyword to emit for this condition. */ + public CommandKeyword keyword() { + return keyword; + } + + /** Indicates whether this condition uses a value or a digest parameter. */ + public Param param() { + return param; + } + } + + /** Parameter kind for condition arguments. */ + enum Param { + VALUE, DIGEST } private final Kind kind; @@ -66,45 +87,53 @@ private ValueCondition(Kind kind, V value, String digestHex) { this.digestHex = digestHex; } - /** A condition that always applies (no comparison). */ - public static ValueCondition always() { - return new ValueCondition<>(Kind.ALWAYS, null, null); - - } - - /** A condition that requires the key to exist (equivalent to XX in SET). */ - public static ValueCondition exists() { - return new ValueCondition<>(Kind.EXISTS, null, null); - } - - /** A condition that requires the key to not exist (equivalent to NX in SET). */ - public static ValueCondition notExists() { - return new ValueCondition<>(Kind.NOT_EXISTS, null, null); + /** + * Append this condition's protocol arguments to the given args. + */ + public void build(CommandArgs args) { + args.add(kind.keyword()); + switch (kind.param()) { + case VALUE: + args.addValue(value); + break; + case DIGEST: + args.add(digestHex); + break; + default: + break; + } } - /** A value-based comparison: set/delete only if the current value equals {@code value}. */ - public static ValueCondition equal(V value) { + // Factory methods for creating value- and digest-based conditions + /** Create a value-based equality condition; succeeds only if the current value equals the given value. */ + public static ValueCondition valueEq(V value) { if (value == null) throw new IllegalArgumentException("value must not be null"); return new ValueCondition<>(Kind.EQUAL, value, null); } - /** A value-based comparison: set/delete only if the current value does not equal {@code value}. */ - public static ValueCondition notEqual(V value) { + /** Create a value-based inequality condition; succeeds only if the current value does not equal the given value. */ + public static ValueCondition valueNe(V value) { if (value == null) throw new IllegalArgumentException("value must not be null"); return new ValueCondition<>(Kind.NOT_EQUAL, value, null); } - /** A digest-based comparison: set/delete only if the current value's digest equals {@code hex16Digest}. */ - public static ValueCondition digestEqualHex(String hex16Digest) { + /** + * Create a digest-based equality condition; succeeds only if the current value's digest matches the given 16-character + * lower-case hex digest. + */ + public static ValueCondition digestEq(String hex16Digest) { if (hex16Digest == null) throw new IllegalArgumentException("digest must not be null"); return new ValueCondition<>(Kind.DIGEST_EQUAL, null, hex16Digest); } - /** A digest-based comparison: set/delete only if the current value's digest does not equal {@code hex16Digest}. */ - public static ValueCondition digestNotEqualHex(String hex16Digest) { + /** + * Create a digest-based inequality condition; succeeds only if the current value's digest does not match the given + * 16-character lower-case hex digest. + */ + public static ValueCondition digestNe(String hex16Digest) { if (hex16Digest == null) throw new IllegalArgumentException("digest must not be null"); return new ValueCondition<>(Kind.DIGEST_NOT_EQUAL, null, hex16Digest); diff --git a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java index 1110c6b3a4..c80148c5bb 100644 --- a/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/cluster/commands/KeyClusterCommandIntegrationTests.java @@ -111,10 +111,10 @@ void delex_unconditional_and_digest_guarded_cluster() { redis.set(k, "bar"); String d = redis.digestKey(k); // wrong condition: digestNotEqualHex (should abort) - assertThat(redis.delex(k, ValueCondition. digestNotEqualHex(d))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.digestNe(d))).isEqualTo(0); assertThat(redis.exists(k)).isEqualTo(1); // right condition: digestEqualHex (should delete) - assertThat(redis.delex(k, ValueCondition. digestEqualHex(d))).isEqualTo(1); + assertThat(redis.delex(k, ValueCondition.digestEq(d))).isEqualTo(1); assertThat(redis.exists(k)).isEqualTo(0); } @@ -133,14 +133,14 @@ void delex_value_equal_notEqual_cluster() { String k = "k:delex-eq-cluster"; redis.set(k, "v1"); // wrong equality -> abort - assertThat(redis.delex(k, ValueCondition.equal("nope"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.valueEq("nope"))).isEqualTo(0); // correct equality -> delete - assertThat(redis.delex(k, ValueCondition.equal("v1"))).isEqualTo(1); + assertThat(redis.delex(k, ValueCondition.valueEq("v1"))).isEqualTo(1); // not-equal that fails (after deletion, recreate) redis.set(k, "v2"); - assertThat(redis.delex(k, ValueCondition.notEqual("v2"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.valueNe("v2"))).isEqualTo(0); // not-equal that succeeds - assertThat(redis.delex(k, ValueCondition.notEqual("other"))).isEqualTo(1); + assertThat(redis.delex(k, ValueCondition.valueNe("other"))).isEqualTo(1); } } diff --git a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java index 4a4f382098..377b0acca7 100644 --- a/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/KeyCommandIntegrationTests.java @@ -630,10 +630,10 @@ void delex_unconditional_and_digest_guarded() { redis.set(k, "bar"); String d = redis.digestKey(k); // new DIGEST command // wrong condition: digestNotEqualHex (should abort) - assertThat(redis.delex(k, ValueCondition. digestNotEqualHex(d))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.digestNe(d))).isEqualTo(0); assertThat(redis.exists(k)).isEqualTo(1); // right condition: digestEqualHex (should delete) - assertThat(redis.delex(k, ValueCondition. digestEqualHex(d))).isEqualTo(1); + assertThat(redis.delex(k, ValueCondition.digestEq(d))).isEqualTo(1); assertThat(redis.exists(k)).isEqualTo(0); } @@ -644,14 +644,14 @@ void delex_with_value_equal_notEqual() { String k = "k:delex-eq"; redis.set(k, "v1"); // wrong equality -> abort - assertThat(redis.delex(k, ValueCondition.equal("nope"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.valueEq("nope"))).isEqualTo(0); // correct equality -> delete - assertThat(redis.delex(k, ValueCondition.equal("v1"))).isEqualTo(1); + assertThat(redis.delex(k, ValueCondition.valueEq("v1"))).isEqualTo(1); // not-equal that fails (after deletion, recreate) redis.set(k, "v2"); - assertThat(redis.delex(k, ValueCondition.notEqual("v2"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.valueNe("v2"))).isEqualTo(0); // not-equal that succeeds - assertThat(redis.delex(k, ValueCondition.notEqual("other"))).isEqualTo(1); + assertThat(redis.delex(k, ValueCondition.valueNe("other"))).isEqualTo(1); } @Test @@ -660,9 +660,8 @@ void delex_on_missing_returns_0_for_any_condition() { String k = "k:missing"; assertThat(redis.del(k)).isEqualTo(0); - assertThat(redis.delex(k, ValueCondition.exists())).isEqualTo(0); - assertThat(redis.delex(k, ValueCondition.equal("x"))).isEqualTo(0); - assertThat(redis.delex(k, ValueCondition. digestEqualHex("0000000000000000"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.valueEq("x"))).isEqualTo(0); + assertThat(redis.delex(k, ValueCondition.digestEq("0000000000000000"))).isEqualTo(0); } } diff --git a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java index 590315e69a..d8d186a15f 100644 --- a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java @@ -497,17 +497,17 @@ void digestKey_returnsHex_and_changesOnValueChange() { @Test @EnabledOnCommand("DELEX") - void set_with_ValueCondition_exists_notExists() { + void set_with_SetArgs_nx_xx() { String k = "k:nx-xx"; - // key does not exist: EXISTS/XX should abort - assertThat(redis.set(k, "v", ValueCondition. exists())).isNull(); - // NOT_EXISTS/NX should succeed - assertThat(redis.set(k, "v", ValueCondition. notExists())).isEqualTo("OK"); - // NOT_EXISTS/NX should now abort - assertThat(redis.set(k, "v2", ValueCondition. notExists())).isNull(); - // EXISTS/XX should succeed - assertThat(redis.set(k, "v2", ValueCondition. exists())).isEqualTo("OK"); + // key does not exist: XX should abort + assertThat(redis.set(k, "v", xx())).isNull(); + // NX should succeed + assertThat(redis.set(k, "v", nx())).isEqualTo("OK"); + // NX should now abort + assertThat(redis.set(k, "v2", nx())).isNull(); + // XX should succeed + assertThat(redis.set(k, "v2", xx())).isEqualTo("OK"); assertThat(redis.get(k)).isEqualTo("v2"); } @@ -518,16 +518,16 @@ void set_with_ValueCondition_equal_notEqual() { String k = "k:ifeq-ifne"; redis.set(k, "v1"); // wrong equality value -> abort - assertThat(redis.set(k, "v2", ValueCondition.equal("nope"))).isNull(); + assertThat(redis.set(k, "v2", ValueCondition.valueEq("nope"))).isNull(); assertThat(redis.get(k)).isEqualTo("v1"); // correct equality value -> success - assertThat(redis.set(k, "v2", ValueCondition.equal("v1"))).isEqualTo("OK"); + assertThat(redis.set(k, "v2", ValueCondition.valueEq("v1"))).isEqualTo("OK"); assertThat(redis.get(k)).isEqualTo("v2"); // not-equal that fails (current is v2) - assertThat(redis.set(k, "v3", ValueCondition.notEqual("v2"))).isNull(); + assertThat(redis.set(k, "v3", ValueCondition.valueNe("v2"))).isNull(); assertThat(redis.get(k)).isEqualTo("v2"); // not-equal that succeeds - assertThat(redis.set(k, "v3", ValueCondition.notEqual("other"))).isEqualTo("OK"); + assertThat(redis.set(k, "v3", ValueCondition.valueNe("other"))).isEqualTo("OK"); assertThat(redis.get(k)).isEqualTo("v3"); } @@ -538,10 +538,10 @@ void setget_with_ValueCondition_returnsPreviousValue() { String k = "k:setget"; redis.set(k, "A"); // condition mismatch -> server returns previous value, value unchanged - assertThat(redis.setGet(k, "B", ValueCondition.equal("X"))).isEqualTo("A"); + assertThat(redis.setGet(k, "B", ValueCondition.valueEq("X"))).isEqualTo("A"); assertThat(redis.get(k)).isEqualTo("A"); // condition match -> returns previous and updates - assertThat(redis.setGet(k, "C", ValueCondition.equal("A"))).isEqualTo("A"); + assertThat(redis.setGet(k, "C", ValueCondition.valueEq("A"))).isEqualTo("A"); assertThat(redis.get(k)).isEqualTo("C"); } @@ -554,11 +554,11 @@ void set_with_digest_conditions() { String d = redis.digestKey(k); assertThat(d).isNotNull(); // digest equal -> success - assertThat(redis.set(k, "D", ValueCondition. digestEqualHex(d))).isEqualTo("OK"); + assertThat(redis.set(k, "D", ValueCondition.digestEq(d))).isEqualTo("OK"); // reusing old digest equal -> abort - assertThat(redis.set(k, "E", ValueCondition. digestEqualHex(d))).isNull(); + assertThat(redis.set(k, "E", ValueCondition.digestEq(d))).isNull(); // digest not equal (against old digest) -> success - assertThat(redis.set(k, "F", ValueCondition. digestNotEqualHex(d))).isEqualTo("OK"); + assertThat(redis.set(k, "F", ValueCondition.digestNe(d))).isEqualTo("OK"); } @Test @@ -568,7 +568,7 @@ void set_with_digestNotEqual_fails_when_equal() { redis.set(k, "X"); String d = redis.digestKey(k); - assertThat(redis.set(k, "Y", ValueCondition. digestNotEqualHex(d))).isNull(); + assertThat(redis.set(k, "Y", ValueCondition.digestNe(d))).isNull(); assertThat(redis.get(k)).isEqualTo("X"); } @@ -580,10 +580,10 @@ void setget_with_digest_conditions() { redis.set(k, "A"); String dA = redis.digestKey(k); // match -> returns previous and updates - assertThat(redis.setGet(k, "B", ValueCondition. digestEqualHex(dA))).isEqualTo("A"); + assertThat(redis.setGet(k, "B", ValueCondition.digestEq(dA))).isEqualTo("A"); assertThat(redis.get(k)).isEqualTo("B"); // mismatch (using old digest) -> returns previous and does not update - assertThat(redis.setGet(k, "C", ValueCondition. digestEqualHex(dA))).isEqualTo("B"); + assertThat(redis.setGet(k, "C", ValueCondition.digestEq(dA))).isEqualTo("B"); assertThat(redis.get(k)).isEqualTo("B"); } @@ -599,39 +599,39 @@ void digestKey_emptyValue() { } @Test - void setget_with_exists_notExists_conditions() { + void setget_with_SetArgs_nx_xx() { Assumptions.assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.3.224")); String k1 = "k:setget-nx"; // NX when missing -> perform set, returns previous (null) - assertThat(redis.setGet(k1, "A", ValueCondition.notExists())).isNull(); + assertThat(redis.setGet(k1, "A", nx())).isNull(); assertThat(redis.get(k1)).isEqualTo("A"); // NX when present -> no set, returns previous - assertThat(redis.setGet(k1, "B", ValueCondition.notExists())).isEqualTo("A"); + assertThat(redis.setGet(k1, "B", nx())).isEqualTo("A"); assertThat(redis.get(k1)).isEqualTo("A"); String k2 = "k:setget-xx"; // XX when missing -> no-op, returns previous (null) redis.del(k2); - assertThat(redis.setGet(k2, "X", ValueCondition.exists())).isNull(); + assertThat(redis.setGet(k2, "X", xx())).isNull(); assertThat(redis.get(k2)).isNull(); // XX when present -> set, returns previous redis.set(k2, "Y"); - assertThat(redis.setGet(k2, "Z", ValueCondition.exists())).isEqualTo("Y"); + assertThat(redis.setGet(k2, "Z", xx())).isEqualTo("Y"); assertThat(redis.get(k2)).isEqualTo("Z"); } @Test - void set_with_SetArgs_and_ValueCondition_combination() { + void set_with_SetArgs_nx_and_expiration() { String k = "k:set-args-cond"; // NX + EX should set with TTL Assumptions.assumeTrue(RedisConditions.of(redis).hasVersionGreaterOrEqualsTo("8.3.224")); - assertThat(redis.set(k, "v1", ex(100), ValueCondition.notExists())).isEqualTo("OK"); + assertThat(redis.set(k, "v1", ex(100).nx())).isEqualTo("OK"); assertThat(redis.ttl(k)).isGreaterThan(0); // NX when present should abort and keep value/ttl Long ttlBefore = redis.ttl(k); - assertThat(redis.set(k, "v2", ex(100), ValueCondition.notExists())).isNull(); + assertThat(redis.set(k, "v2", ex(100).nx())).isNull(); assertThat(redis.get(k)).isEqualTo("v1"); assertThat(redis.ttl(k)).isGreaterThan(0); assertThat(redis.ttl(k)).isLessThanOrEqualTo(ttlBefore); diff --git a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java index d5aa5b82bb..2c7dc1c744 100644 --- a/src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java +++ b/src/test/java/io/lettuce/core/masterreplica/MasterReplicaIntegrationTests.java @@ -152,6 +152,31 @@ void testConnectToReplica() { sync.set(key, value); } + @Test + @EnabledOnCommand("DIGEST") + void digestKey_reads_from_replica() { + + RedisCommands commands = connection.sync(); + String k = "k:digest-replica"; + String v = "replica-value"; + + // Write to upstream and wait for replica to catch up + commands.set(k, v); + commands.waitForReplication(1, 1000); + + // Compute digest on upstream + connection.setReadFrom(ReadFrom.UPSTREAM); + String digestUpstream = commands.digestKey(k); + + // Read digest from replica (read-only) + connection.setReadFrom(ReadFrom.REPLICA); + String digestReplica = commands.digestKey(k); + + assertThat(digestReplica).isNotNull(); + assertThat(digestReplica).hasSize(16); + assertThat(digestReplica).isEqualTo(digestUpstream); + } + @Test void noReplicaForRead() { From 893d3f12950094e489a789f6105aeb134a556cc5 Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 16:06:29 +0200 Subject: [PATCH 7/8] Format --- src/main/java/io/lettuce/core/ValueCondition.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/io/lettuce/core/ValueCondition.java b/src/main/java/io/lettuce/core/ValueCondition.java index 47b719ebf3..c9f7d6c0b2 100644 --- a/src/main/java/io/lettuce/core/ValueCondition.java +++ b/src/main/java/io/lettuce/core/ValueCondition.java @@ -68,6 +68,7 @@ public CommandKeyword keyword() { public Param param() { return param; } + } /** Parameter kind for condition arguments. */ From 77cd5549cb703fac7e2adcfc9a679f429373c72b Mon Sep 17 00:00:00 2001 From: "aleksandar.todorov" Date: Thu, 6 Nov 2025 16:29:21 +0200 Subject: [PATCH 8/8] Revert unintentional changes --- src/test/java/io/lettuce/apigenerator/Constants.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/io/lettuce/apigenerator/Constants.java b/src/test/java/io/lettuce/apigenerator/Constants.java index 2d457f912d..7cee3129ac 100644 --- a/src/test/java/io/lettuce/apigenerator/Constants.java +++ b/src/test/java/io/lettuce/apigenerator/Constants.java @@ -27,7 +27,11 @@ */ class Constants { - public static final String[] TEMPLATE_NAMES = { "RedisKeyCommands" }; + public static final String[] TEMPLATE_NAMES = { "BaseRedisCommands", "RedisAclCommands", "RedisFunctionCommands", + "RedisGeoCommands", "RedisHashCommands", "RedisHLLCommands", "RedisKeyCommands", "RedisListCommands", + "RedisScriptingCommands", "RedisSentinelCommands", "RedisServerCommands", "RedisSetCommands", + "RedisSortedSetCommands", "RedisStreamCommands", "RedisStringCommands", "RedisTransactionalCommands", + "RedisJsonCommands", "RedisVectorSetCommands", "RediSearchCommands" }; public static final File TEMPLATES = new File("src/main/templates");