diff --git a/pom.xml b/pom.xml index 9ae293e949..5ae78cd78b 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,10 @@ 5.13.4 + + + + integration,scenario false @@ -349,7 +353,7 @@ ${maven.surefire.version} ${skipUnitTests} - @{argLine} ${JVM_OPTS} + ${argLine} ${JVM_OPTS} ${redis-hosts} @@ -368,7 +372,7 @@ maven-failsafe-plugin ${maven.surefire.version} - @{failsafeSuffixArgLine} ${JVM_OPTS} + ${failsafeSuffixArgLine} ${JVM_OPTS} ${redis-hosts} @@ -394,7 +398,7 @@ ${skipIntegrationTests} - @{failsafeTaggedArgLine} ${JVM_OPTS} + ${failsafeTaggedArgLine} ${JVM_OPTS} integration scenario,unit @@ -411,7 +415,7 @@ ${skipIntegrationTests} - @{failsafeSuffixArgLine} ${JVM_OPTS} + ${failsafeSuffixArgLine} ${JVM_OPTS} scenario,unit **/*IT.java @@ -429,7 +433,7 @@ ${skipScenarioTests} - @{failsafeScenarioArgLine} ${JVM_OPTS} + ${failsafeScenarioArgLine} ${JVM_OPTS} scenario integration,unit @@ -638,7 +642,7 @@ maven-surefire-plugin ${maven.surefire.version} - @{argLine} ${JVM_OPTS} + ${argLine} ${JVM_OPTS} diff --git a/src/main/java/redis/clients/jedis/CommandObjects.java b/src/main/java/redis/clients/jedis/CommandObjects.java index 1d0173ffff..f6c81beae6 100644 --- a/src/main/java/redis/clients/jedis/CommandObjects.java +++ b/src/main/java/redis/clients/jedis/CommandObjects.java @@ -32,6 +32,7 @@ import redis.clients.jedis.timeseries.*; import redis.clients.jedis.timeseries.TimeSeriesProtocol.*; import redis.clients.jedis.util.KeyValue; +import redis.clients.jedis.conditions.ValueCondition; public class CommandObjects { @@ -49,7 +50,7 @@ protected RedisProtocol getProtocol() { protected volatile CommandKeyArgumentPreProcessor keyPreProcessor = null; private JedisBroadcastAndRoundRobinConfig broadcastAndRoundRobinConfig = null; - private Lock mapperLock = new ReentrantLock(true); + private Lock mapperLock = new ReentrantLock(true); private volatile JsonObjectMapper jsonObjectMapper; private final AtomicInteger searchDialect = new AtomicInteger(SearchProtocol.DEFAULT_DIALECT); @@ -346,6 +347,18 @@ public final CommandObject del(byte[]... keys) { return new CommandObject<>(commandArguments(DEL).keys((Object[]) keys), BuilderFactory.LONG); } + public final CommandObject delex(String key, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.DELEX).key(key); + cond.addTo(ca); + return new CommandObject<>(ca, BuilderFactory.LONG); + } + + public final CommandObject delex(byte[] key, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.DELEX).key(key); + cond.addTo(ca); + return new CommandObject<>(ca, BuilderFactory.LONG); + } + public final CommandObject unlink(String key) { return new CommandObject<>(commandArguments(UNLINK).key(key), BuilderFactory.LONG); } @@ -454,14 +467,74 @@ public final CommandObject set(byte[] key, byte[] value) { return new CommandObject<>(commandArguments(Command.SET).key(key).add(value), BuilderFactory.STRING); } + public final CommandObject set(String key, String value, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + return new CommandObject<>(ca, BuilderFactory.STRING); + } + + public final CommandObject setGet(String key, String value, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + ca.add(Keyword.GET); + return new CommandObject<>(ca, BuilderFactory.STRING); + } + + public final CommandObject set(byte[] key, byte[] value, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + return new CommandObject<>(ca, BuilderFactory.STRING); + } + + public final CommandObject setGet(byte[] key, byte[] value, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + ca.add(Keyword.GET); + return new CommandObject<>(ca, BuilderFactory.BINARY); + } + public final CommandObject set(byte[] key, byte[] value, SetParams params) { return new CommandObject<>(commandArguments(Command.SET).key(key).add(value).addParams(params), BuilderFactory.STRING); } + public final CommandObject set(String key, String value, SetParams params, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + ca.addParams(params); + return new CommandObject<>(ca, BuilderFactory.STRING); + } + + public final CommandObject setGet(String key, String value, SetParams params, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + ca.addParams(params); + ca.add(Keyword.GET); + return new CommandObject<>(ca, BuilderFactory.STRING); + } + + public final CommandObject set(byte[] key, byte[] value, SetParams params, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + ca.addParams(params); + return new CommandObject<>(ca, BuilderFactory.STRING); + } + + public final CommandObject setGet(byte[] key, byte[] value, SetParams params, ValueCondition cond) { + CommandArguments ca = commandArguments(Command.SET).key(key).add(value); + cond.addTo(ca); + ca.addParams(params); + ca.add(Keyword.GET); + return new CommandObject<>(ca, BuilderFactory.BINARY); + } + public final CommandObject get(String key) { return new CommandObject<>(commandArguments(Command.GET).key(key), BuilderFactory.STRING); } + public final CommandObject digestKey(String key) { + return new CommandObject<>(commandArguments(Command.DIGEST).key(key), BuilderFactory.STRING); + } + public final CommandObject setGet(String key, String value) { return new CommandObject<>(commandArguments(Command.SET).key(key).add(value).add(Keyword.GET), BuilderFactory.STRING); } @@ -479,6 +552,10 @@ public final CommandObject getEx(String key, GetExParams params) { return new CommandObject<>(commandArguments(Command.GETEX).key(key).addParams(params), BuilderFactory.STRING); } + public final CommandObject digestKey(byte[] key) { + return new CommandObject<>(commandArguments(Command.DIGEST).key(key), BuilderFactory.BINARY); + } + public final CommandObject get(byte[] key) { return new CommandObject<>(commandArguments(Command.GET).key(key), BuilderFactory.BINARY); } @@ -2977,7 +3054,7 @@ public final CommandObject> xreadGroup(byte[] groupName, byte[] con } public final CommandObject>>> xreadGroupBinary( - byte[] groupName, byte[] consumer, XReadGroupParams xReadGroupParams, + byte[] groupName, byte[] consumer, XReadGroupParams xReadGroupParams, Map.Entry... streams) { CommandArguments args = commandArguments(XREADGROUP) .add(GROUP).add(groupName).add(consumer) @@ -2992,7 +3069,7 @@ public final CommandObject>>> xre } public final CommandObject>> xreadGroupBinaryAsMap( - byte[] groupName, byte[] consumer, XReadGroupParams xReadGroupParams, + byte[] groupName, byte[] consumer, XReadGroupParams xReadGroupParams, Map.Entry... streams) { CommandArguments args = commandArguments(XREADGROUP) .add(GROUP).add(groupName).add(consumer) diff --git a/src/main/java/redis/clients/jedis/Jedis.java b/src/main/java/redis/clients/jedis/Jedis.java index 1dc47145e3..842a814835 100644 --- a/src/main/java/redis/clients/jedis/Jedis.java +++ b/src/main/java/redis/clients/jedis/Jedis.java @@ -21,8 +21,10 @@ import javax.net.ssl.SSLSocketFactory; import redis.clients.jedis.Protocol.*; +import redis.clients.jedis.annots.Experimental; import redis.clients.jedis.args.*; import redis.clients.jedis.commands.*; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.exceptions.InvalidURIException; import redis.clients.jedis.exceptions.JedisConnectionException; import redis.clients.jedis.exceptions.JedisException; @@ -490,6 +492,24 @@ public String set(final byte[] key, final byte[] value, final SetParams params) return connection.executeCommand(commandObjects.set(key, value, params)); } + @Override + public String set(final byte[] key, final byte[] value, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.set(key, value, condition)); + } + + @Override + public String set(final byte[] key, final byte[] value, final SetParams params, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.set(key, value, params, condition)); + } + + @Override + public byte[] setGet(final byte[] key, final byte[] value, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.setGet(key, value, condition)); + } + /** * Get the value of the specified key. If the key does not exist the special value 'nil' is * returned. If the value stored at key is not a string an error is returned because GET can only @@ -505,6 +525,12 @@ public byte[] get(final byte[] key) { return connection.executeCommand(commandObjects.get(key)); } + @Override + public byte[] digestKey(final byte[] key) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.digestKey(key)); + } + @Override public byte[] setGet(final byte[] key, final byte[] value) { checkIsInMultiOrPipeline(); @@ -517,6 +543,12 @@ public byte[] setGet(final byte[] key, final byte[] value, final SetParams param return connection.executeCommand(commandObjects.setGet(key, value, params)); } + @Override + public byte[] setGet(final byte[] key, final byte[] value, final SetParams params, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.setGet(key, value, params, condition)); + } + /** * Get the value of key and delete the key. This command is similar to GET, except for the fact * that it also deletes the key on success (if and only if the key's value type is a string). @@ -573,6 +605,12 @@ public long del(final byte[]... keys) { checkIsInMultiOrPipeline(); return connection.executeCommand(commandObjects.del(keys)); } + @Override + public long delex(final byte[] key, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.delex(key, condition)); + } + @Override public long del(final byte[] key) { @@ -5100,6 +5138,24 @@ public String set(final String key, final String value, final SetParams params) return connection.executeCommand(commandObjects.set(key, value, params)); } + @Override + public String set(final String key, final String value, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.set(key, value, condition)); + } + + @Override + public String set(final String key, final String value, final SetParams params, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.set(key, value, params, condition)); + } + + @Override + public String setGet(final String key, final String value, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.setGet(key, value, condition)); + } + /** * Get the value of the specified key. If the key does not exist the special value 'nil' is * returned. If the value stored at key is not a string an error is returned because GET can only @@ -5121,6 +5177,12 @@ public String setGet(final String key, final String value) { return connection.executeCommand(commandObjects.setGet(key, value)); } + @Override + public String setGet(final String key, final String value, final SetParams params, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.setGet(key, value, params, condition)); + } + @Override public String setGet(final String key, final String value, final SetParams params) { checkIsInMultiOrPipeline(); @@ -5147,6 +5209,18 @@ public String getEx(String key, GetExParams params) { return connection.executeCommand(commandObjects.getEx(key, params)); } + @Override + public String digestKey(final String key) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.digestKey(key)); + } + + @Override + public long delex(final String key, final ValueCondition condition) { + checkIsInMultiOrPipeline(); + return connection.executeCommand(commandObjects.delex(key, condition)); + } + /** * Test if the specified keys exist. The command returns the number of keys exist. * Time complexity: O(N) diff --git a/src/main/java/redis/clients/jedis/Protocol.java b/src/main/java/redis/clients/jedis/Protocol.java index ccc185188a..3c87af866c 100644 --- a/src/main/java/redis/clients/jedis/Protocol.java +++ b/src/main/java/redis/clients/jedis/Protocol.java @@ -283,7 +283,7 @@ public static final byte[] toByteArray(final double value) { public static enum Command implements ProtocolCommand { - PING, AUTH, HELLO, SET, GET, GETDEL, GETEX, EXISTS, DEL, UNLINK, TYPE, FLUSHDB, FLUSHALL, MOVE, + PING, AUTH, HELLO, SET, GET, GETDEL, GETEX, DIGEST, EXISTS, DEL, DELEX, UNLINK, TYPE, FLUSHDB, FLUSHALL, MOVE, KEYS, RANDOMKEY, RENAME, RENAMENX, DUMP, RESTORE, DBSIZE, SELECT, SWAPDB, MIGRATE, ECHO, // EXPIRE, EXPIREAT, EXPIRETIME, PEXPIRE, PEXPIREAT, PEXPIRETIME, TTL, PTTL, // <-- key expiration MULTI, DISCARD, EXEC, WATCH, UNWATCH, SORT, SORT_RO, INFO, SHUTDOWN, MONITOR, CONFIG, LCS, // @@ -334,7 +334,7 @@ public static enum Keyword implements Rawable { STREAMS, CREATE, MKSTREAM, SETID, DESTROY, DELCONSUMER, MAXLEN, GROUP, IDLE, TIME, BLOCK, NOACK, RETRYCOUNT, STREAM, GROUPS, CONSUMERS, JUSTID, WITHVALUES, NOMKSTREAM, MINID, CREATECONSUMER, SETUSER, GETUSER, DELUSER, WHOAMI, USERS, CAT, GENPASS, LOG, SAVE, DRYRUN, COPY, AUTH, AUTH2, - NX, XX, EX, PX, EXAT, PXAT, ABSTTL, KEEPTTL, INCR, LT, GT, CH, INFO, PAUSE, UNPAUSE, UNBLOCK, + NX, XX, IFEQ, IFNE, IFDEQ, IFDNE, EX, PX, EXAT, PXAT, ABSTTL, KEEPTTL, INCR, LT, GT, CH, INFO, PAUSE, UNPAUSE, UNBLOCK, REV, WITHCOORD, WITHDIST, WITHHASH, ANY, FROMMEMBER, FROMLONLAT, BYRADIUS, BYBOX, BYLEX, BYSCORE, STOREDIST, TO, FORCE, TIMEOUT, DB, UNLOAD, ABORT, IDX, MINMATCHLEN, WITHMATCHLEN, FULL, DELETE, LIBRARYNAME, WITHCODE, DESCRIPTION, GETKEYS, GETKEYSANDFLAGS, DOCS, FILTERBY, DUMP, diff --git a/src/main/java/redis/clients/jedis/UnifiedJedis.java b/src/main/java/redis/clients/jedis/UnifiedJedis.java index 3839c38ea2..db784a714c 100644 --- a/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -17,6 +17,7 @@ import redis.clients.jedis.commands.SampleBinaryKeyedCommands; import redis.clients.jedis.commands.SampleKeyedCommands; import redis.clients.jedis.commands.RedisModuleCommands; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.csc.Cache; import redis.clients.jedis.csc.CacheConfig; import redis.clients.jedis.csc.CacheConnection; @@ -26,7 +27,6 @@ import redis.clients.jedis.json.JsonSetParams; import redis.clients.jedis.json.Path; import redis.clients.jedis.json.Path2; -import redis.clients.jedis.mcf.MultiDbCommandExecutor; import redis.clients.jedis.params.VAddParams; import redis.clients.jedis.params.VSimParams; import redis.clients.jedis.resps.RawVector; @@ -585,6 +585,11 @@ public long del(String key) { return executeCommand(commandObjects.del(key)); } + @Override + public long delex(String key, ValueCondition condition) { + return executeCommand(commandObjects.delex(key, condition)); + } + @Override public long del(String... keys) { return executeCommand(commandObjects.del(keys)); @@ -595,6 +600,11 @@ public long unlink(String key) { return executeCommand(commandObjects.unlink(key)); } + @Override + public long delex(byte[] key, ValueCondition condition) { + return executeCommand(commandObjects.delex(key, condition)); + } + @Override public long unlink(String... keys) { return executeCommand(commandObjects.unlink(keys)); @@ -760,6 +770,31 @@ public String get(String key) { return executeCommand(commandObjects.get(key)); } + @Override + public String digestKey(String key) { + return executeCommand(commandObjects.digestKey(key)); + } + + @Override + public String set(String key, String value, ValueCondition condition) { + return executeCommand(commandObjects.set(key, value, condition)); + } + + @Override + public String set(String key, String value, SetParams params, ValueCondition condition) { + return executeCommand(commandObjects.set(key, value, params, condition)); + } + + @Override + public String setGet(String key, String value, SetParams params, ValueCondition condition) { + return executeCommand(commandObjects.setGet(key, value, params, condition)); + } + + @Override + public String setGet(String key, String value, ValueCondition condition) { + return executeCommand(commandObjects.setGet(key, value, condition)); + } + @Override public String setGet(String key, String value) { return executeCommand(commandObjects.setGet(key, value)); @@ -790,11 +825,31 @@ public String set(byte[] key, byte[] value, SetParams params) { return executeCommand(commandObjects.set(key, value, params)); } + @Override + public String set(byte[] key, byte[] value, SetParams params, ValueCondition condition) { + return executeCommand(commandObjects.set(key, value, params, condition)); + } + @Override public byte[] get(byte[] key) { return executeCommand(commandObjects.get(key)); } + @Override + public String set(byte[] key, byte[] value, ValueCondition condition) { + return executeCommand(commandObjects.set(key, value, condition)); + } + + @Override + public byte[] setGet(byte[] key, byte[] value, ValueCondition condition) { + return executeCommand(commandObjects.setGet(key, value, condition)); + } + + @Override + public byte[] digestKey(byte[] key) { + return executeCommand(commandObjects.digestKey(key)); + } + @Override public byte[] setGet(byte[] key, byte[] value) { return executeCommand(commandObjects.setGet(key, value)); @@ -810,6 +865,12 @@ public byte[] getDel(byte[] key) { return executeCommand(commandObjects.getDel(key)); } + + @Override + public byte[] setGet(byte[] key, byte[] value, SetParams params, ValueCondition condition) { + return executeCommand(commandObjects.setGet(key, value, params, condition)); + } + @Override public byte[] getEx(byte[] key, GetExParams params) { return executeCommand(commandObjects.getEx(key, params)); diff --git a/src/main/java/redis/clients/jedis/commands/KeyBinaryCommands.java b/src/main/java/redis/clients/jedis/commands/KeyBinaryCommands.java index 7593a42944..6b64a00668 100644 --- a/src/main/java/redis/clients/jedis/commands/KeyBinaryCommands.java +++ b/src/main/java/redis/clients/jedis/commands/KeyBinaryCommands.java @@ -6,6 +6,8 @@ import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.params.MigrateParams; import redis.clients.jedis.params.RestoreParams; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.params.SortingParams; import redis.clients.jedis.resps.ScanResult; @@ -60,6 +62,13 @@ public interface KeyBinaryCommands { long del(byte[] key); + + /** + * Experimental: Compare-and-delete guarded by value/digest condition. + */ + @Experimental + long delex(byte[] key, ValueCondition condition); + long del(byte[]... keys); long unlink(byte[] key); diff --git a/src/main/java/redis/clients/jedis/commands/KeyCommands.java b/src/main/java/redis/clients/jedis/commands/KeyCommands.java index e64f4fc3d7..2d6032484e 100644 --- a/src/main/java/redis/clients/jedis/commands/KeyCommands.java +++ b/src/main/java/redis/clients/jedis/commands/KeyCommands.java @@ -6,6 +6,8 @@ import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.params.MigrateParams; import redis.clients.jedis.params.RestoreParams; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.params.SortingParams; import redis.clients.jedis.resps.ScanResult; @@ -418,6 +420,17 @@ public interface KeyCommands { */ long del(String... keys); + /** + * Compare-and-delete: delete key if optional value/digest condition matches. + * @return 1 if the key was deleted, 0 otherwise + */ + + /** + * Experimental: Compare-and-delete guarded by value/digest condition. + */ + @Experimental + long delex(String key, ValueCondition condition); + /** * Unlink Command * This command is very similar to {@link KeyCommands#del(String) DEL}: it removes the specified key. diff --git a/src/main/java/redis/clients/jedis/commands/StringBinaryCommands.java b/src/main/java/redis/clients/jedis/commands/StringBinaryCommands.java index 0d087bc1b3..8f7e6c49c8 100644 --- a/src/main/java/redis/clients/jedis/commands/StringBinaryCommands.java +++ b/src/main/java/redis/clients/jedis/commands/StringBinaryCommands.java @@ -7,8 +7,36 @@ import redis.clients.jedis.params.LCSParams; import redis.clients.jedis.resps.LCSMatchResult; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.conditions.ValueCondition; + public interface StringBinaryCommands extends BitBinaryCommands { + /** + * Experimental: SET with compare-and-* condition. + */ + @Experimental + String set(byte[] key, byte[] value, ValueCondition condition); + + /** + * Experimental: SET+GET with compare-and-* condition. + */ + @Experimental + byte[] setGet(byte[] key, byte[] value, ValueCondition condition); + + /** + * Experimental: SET with SetParams and compare-and-* condition. + */ + @Experimental + String set(byte[] key, byte[] value, SetParams params, ValueCondition condition); + + /** + * Experimental: SET+GET with SetParams and compare-and-* condition. + */ + @Experimental + byte[] setGet(byte[] key, byte[] value, SetParams params, ValueCondition condition); + + String set(byte[] key, byte[] value); String set(byte[] key, byte[] value, SetParams params); @@ -23,6 +51,10 @@ public interface StringBinaryCommands extends BitBinaryCommands { byte[] getEx(byte[] key, GetExParams params); + /** Returns the 64-bit XXH3 digest hex (ASCII bytes) of the string value stored at key, or null if missing. */ + @Experimental + byte[] digestKey(byte[] key); + long setrange(byte[] key, long offset, byte[] value); byte[] getrange(byte[] key, long startOffset, long endOffset); diff --git a/src/main/java/redis/clients/jedis/commands/StringCommands.java b/src/main/java/redis/clients/jedis/commands/StringCommands.java index c4ea21fea7..81792e8322 100644 --- a/src/main/java/redis/clients/jedis/commands/StringCommands.java +++ b/src/main/java/redis/clients/jedis/commands/StringCommands.java @@ -7,6 +7,9 @@ import redis.clients.jedis.params.LCSParams; import redis.clients.jedis.resps.LCSMatchResult; +import redis.clients.jedis.annots.Experimental; +import redis.clients.jedis.conditions.ValueCondition; + public interface StringCommands extends BitCommands { /** @@ -34,6 +37,12 @@ public interface StringCommands extends BitCommands { */ String set(String key, String value, SetParams params); + /** + * Experimental: SET with compare-and-* condition. + */ + @Experimental + String set(String key, String value, ValueCondition condition); + /** * Get Command * Get the value of the specified key. If the key does not exist the special value 'nil' is @@ -50,6 +59,24 @@ public interface StringCommands extends BitCommands { String setGet(String key, String value, SetParams params); + /** + * Experimental: SET+GET with compare-and-* condition. + */ + @Experimental + String setGet(String key, String value, ValueCondition condition); + + /** + * Experimental: SET with SetParams and compare-and-* condition. + */ + @Experimental + String set(String key, String value, SetParams params, ValueCondition condition); + + /** + * Experimental: SET+GET with SetParams and compare-and-* condition. + */ + @Experimental + String setGet(String key, String value, SetParams params, ValueCondition condition); + /** * GetDel Command * Get the value of key and delete the key. This command is similar to GET, except for the fact @@ -78,6 +105,13 @@ public interface StringCommands extends BitCommands { */ String getEx(String key, GetExParams params); + /** + * Compute and return the 64-bit XXH3 digest hex of the string value stored at key. + * Returns null if key does not exist. + */ + @Experimental + String digestKey(String key); + /** * SetRange Command * GETRANGE overwrite part of the string stored at key, starting at the specified offset, for the entire diff --git a/src/main/java/redis/clients/jedis/conditions/ValueCondition.java b/src/main/java/redis/clients/jedis/conditions/ValueCondition.java new file mode 100644 index 0000000000..ac0427daa4 --- /dev/null +++ b/src/main/java/redis/clients/jedis/conditions/ValueCondition.java @@ -0,0 +1,53 @@ +package redis.clients.jedis.conditions; + +import redis.clients.jedis.CommandArguments; +import redis.clients.jedis.Protocol.Keyword; +import redis.clients.jedis.annots.Experimental; + +/** + * Experimental condition object for optimistic concurrency with SET/DELEX. + * Encapsulates either a value-based or digest-based condition with equality or inequality. + */ +@Experimental +public final class ValueCondition { + + public enum Kind { VALUE, DIGEST } + public enum Mode { EQ, NE } + + private final Kind kind; + private final Mode mode; + private final Object payload; // String or byte[] + + private ValueCondition(Kind kind, Mode mode, Object payload) { + if (!(payload instanceof String) && !(payload instanceof byte[])) { + throw new IllegalArgumentException("payload must be String or byte[]"); + } + this.kind = kind; + this.mode = mode; + this.payload = payload; + } + + // Factory methods: value-based + public static ValueCondition valueEq(String value) { return new ValueCondition(Kind.VALUE, Mode.EQ, value); } + public static ValueCondition valueNe(String value) { return new ValueCondition(Kind.VALUE, Mode.NE, value); } + public static ValueCondition valueEq(byte[] value) { return new ValueCondition(Kind.VALUE, Mode.EQ, value); } + public static ValueCondition valueNe(byte[] value) { return new ValueCondition(Kind.VALUE, Mode.NE, value); } + + // Factory methods: digest-based + public static ValueCondition digestEq(String hex16) { return new ValueCondition(Kind.DIGEST, Mode.EQ, hex16); } + public static ValueCondition digestNe(String hex16) { return new ValueCondition(Kind.DIGEST, Mode.NE, hex16); } + public static ValueCondition digestEq(byte[] digest) { return new ValueCondition(Kind.DIGEST, Mode.EQ, digest); } + public static ValueCondition digestNe(byte[] digest) { return new ValueCondition(Kind.DIGEST, Mode.NE, digest); } + + /** + * Append this condition to the command arguments by emitting the appropriate keyword and payload. + */ + public void addTo(CommandArguments args) { + if (kind == Kind.VALUE) { + args.add(mode == Mode.EQ ? Keyword.IFEQ : Keyword.IFNE).add(payload); + } else { // DIGEST + args.add(mode == Mode.EQ ? Keyword.IFDEQ : Keyword.IFDNE).add(payload); + } + } +} + diff --git a/src/test/java/redis/clients/jedis/commands/jedis/AllKindOfValuesCommandsTest.java b/src/test/java/redis/clients/jedis/commands/jedis/AllKindOfValuesCommandsTest.java index 9bbf4d323e..2faadf18f9 100644 --- a/src/test/java/redis/clients/jedis/commands/jedis/AllKindOfValuesCommandsTest.java +++ b/src/test/java/redis/clients/jedis/commands/jedis/AllKindOfValuesCommandsTest.java @@ -35,6 +35,7 @@ import redis.clients.jedis.*; import redis.clients.jedis.args.ExpiryOption; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; import redis.clients.jedis.args.FlushMode; @@ -1178,4 +1179,25 @@ public void reset() { jedis.auth(endpoint.getPassword()); assertEquals("1", jedis.get(counter)); } + + @Test + @SinceRedisVersion("8.3.224") + public void delexBasicAndConditions() { + jedis.set("dk", "v"); + assertEquals(1L, jedis.del("dk")); + assertFalse(jedis.exists("dk")); + + jedis.set("dk", "v1"); + assertEquals(0L, jedis.delex("dk", ValueCondition.valueEq("nope"))); + assertEquals(1L, jedis.delex("dk", ValueCondition.valueEq("v1"))); + + jedis.set("dk2", "x"); + assertEquals(0L, jedis.delex("dk2", ValueCondition.valueNe("x"))); + jedis.set("dk3", "y"); + assertEquals(1L, jedis.delex("dk3", ValueCondition.valueNe("z"))); + + jedis.del("missing"); + assertEquals(0L, jedis.del("missing")); + assertEquals(0L, jedis.delex("missing", ValueCondition.valueNe("anything"))); + } } diff --git a/src/test/java/redis/clients/jedis/commands/jedis/StringValuesCommandsTest.java b/src/test/java/redis/clients/jedis/commands/jedis/StringValuesCommandsTest.java index ce73ad5a6e..7076cce20d 100644 --- a/src/test/java/redis/clients/jedis/commands/jedis/StringValuesCommandsTest.java +++ b/src/test/java/redis/clients/jedis/commands/jedis/StringValuesCommandsTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.MethodSource; import redis.clients.jedis.RedisProtocol; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.LCSParams; import redis.clients.jedis.resps.LCSMatchResult; import redis.clients.jedis.exceptions.JedisDataException; @@ -40,6 +41,44 @@ public void setAndGet() { assertNull(jedis.get("bar")); } + @Test + @SinceRedisVersion("8.3.224") + public void digestSimple() { + assertNull(jedis.digestKey("missing")); + jedis.set("foo", "bar"); + String hex = jedis.digestKey("foo"); + org.junit.jupiter.api.Assertions.assertNotNull(hex); + assertTrue(hex.matches("(?i)[0-9a-f]{16}")); + } + + @Test + @SinceRedisVersion("8.3.224") + public void setWithIFConditionsValue() { + jedis.del("k"); + + // IFEQ on existing value: success + jedis.set("k", "v1"); + assertEquals("OK", jedis.set("k", "v2", ValueCondition.valueEq("v1"))); + assertEquals("v2", jedis.get("k")); + + // IFEQ no match: no change + assertNull(jedis.set("k", "v3", ValueCondition.valueEq("nope"))); + assertEquals("v2", jedis.get("k")); + + // IFNE on missing key: treated as true -> creates key + jedis.del("k"); + assertEquals("OK", jedis.set("k", "vx", ValueCondition.valueNe("anything"))); + assertEquals("vx", jedis.get("k")); + + // IFEQ on missing key: false -> no create + jedis.del("k"); + assertNull(jedis.set("k", "vy", ValueCondition.valueEq("anything"))); + assertNull(jedis.get("k")); + } + + + + @Test public void getSet() { String value = jedis.getSet("foo", "bar"); @@ -226,6 +265,39 @@ public void substr() { } @Test + @SinceRedisVersion("8.3.224") + public void digestBasic() { + jedis.del("dg"); + assertNull(jedis.digestKey("dg")); + jedis.set("dg", "val"); + String hex = jedis.digestKey("dg"); + assertTrue(hex != null && (hex.length() == 16)); + } + + @Test + @SinceRedisVersion("8.3.224") + public void setWithIfConditions() { + jedis.set("kif", "v1"); + + // IFEQ matches -> set + assertEquals("OK", jedis.set("kif", "v2", ValueCondition.valueEq("v1"))); + assertEquals("v2", jedis.get("kif")); + + // IFEQ fails -> no set + assertNull(jedis.set("kif", "v3", ValueCondition.valueEq("nope"))); + assertEquals("v2", jedis.get("kif")); + + // IFNE matches -> set + assertEquals("OK", jedis.set("kif", "v4", ValueCondition.valueNe("nope"))); + assertEquals("v4", jedis.get("kif")); + + // Missing key semantics + jedis.del("kif_missing"); + assertNull(jedis.set("kif_missing", "x", ValueCondition.valueEq("anything"))); + assertEquals("OK", jedis.set("kif_missing", "x", ValueCondition.valueNe("anything"))); + } + + public void strlen() { String str = "This is a string"; jedis.set("s", str); diff --git a/src/test/java/redis/clients/jedis/commands/unified/AllKindOfValuesCommandsTestBase.java b/src/test/java/redis/clients/jedis/commands/unified/AllKindOfValuesCommandsTestBase.java index f1891ac893..55ecb8a9cb 100644 --- a/src/test/java/redis/clients/jedis/commands/unified/AllKindOfValuesCommandsTestBase.java +++ b/src/test/java/redis/clients/jedis/commands/unified/AllKindOfValuesCommandsTestBase.java @@ -42,6 +42,7 @@ import redis.clients.jedis.ScanIteration; import redis.clients.jedis.StreamEntryID; import redis.clients.jedis.args.ExpiryOption; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; import redis.clients.jedis.params.RestoreParams; @@ -968,4 +969,30 @@ public void scanIteration() { } assertEquals(allIn, allScan); } + + @Test + @SinceRedisVersion("8.3.224") + public void delexBasicAndConditions() { + // basic + jedis.set("dk", "v"); + assertEquals(1L, jedis.del("dk")); + assertFalse(jedis.exists("dk")); + + // IFEQ match + jedis.set("dk", "v1"); + assertEquals(0L, jedis.delex("dk", ValueCondition.valueEq("nope"))); + assertEquals(1L, jedis.delex("dk", ValueCondition.valueEq("v1"))); + + // IFNE non-match + jedis.set("dk2", "x"); + assertEquals(0L, jedis.delex("dk2", ValueCondition.valueNe("x"))); + jedis.set("dk3", "y"); + assertEquals(1L, jedis.delex("dk3", ValueCondition.valueNe("z"))); + + // Missing key: regardless of condition, deletion count should be 0 + jedis.del("missing"); + assertEquals(0L, jedis.del("missing")); + assertEquals(0L, jedis.delex("missing", ValueCondition.valueNe("anything"))); + } + } diff --git a/src/test/java/redis/clients/jedis/commands/unified/StringValuesCommandsTestBase.java b/src/test/java/redis/clients/jedis/commands/unified/StringValuesCommandsTestBase.java index 60fbbbd595..60b686abd1 100644 --- a/src/test/java/redis/clients/jedis/commands/unified/StringValuesCommandsTestBase.java +++ b/src/test/java/redis/clients/jedis/commands/unified/StringValuesCommandsTestBase.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; import static redis.clients.jedis.params.SetParams.setParams; import java.util.ArrayList; @@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Tag; import redis.clients.jedis.RedisProtocol; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.LCSParams; import redis.clients.jedis.resps.LCSMatchResult; import redis.clients.jedis.exceptions.JedisDataException; @@ -281,4 +283,161 @@ public void lcs() { assertEquals(0, stringMatchResult.getMatches().size()); } + @Test + @SinceRedisVersion("8.3.224") + public void digestBasic() { + jedis.del("dg"); + assertNull(jedis.digestKey("dg")); + jedis.set("dg", "val"); + String hex = jedis.digestKey("dg"); + assertTrue(hex != null && (hex.length() == 16)); + } + + @Test + @SinceRedisVersion("8.3.224") + public void setWithIfConditions() { + jedis.set("kif", "v1"); + + // IFEQ matches -> set + assertEquals("OK", jedis.set("kif", "v2", ValueCondition.valueEq("v1"))); + assertEquals("v2", jedis.get("kif")); + + // IFEQ fails -> no set + assertNull(jedis.set("kif", "v3", ValueCondition.valueEq("nope"))); + assertEquals("v2", jedis.get("kif")); + + // IFNE matches -> set + assertEquals("OK", jedis.set("kif", "v4", ValueCondition.valueNe("nope"))); + assertEquals("v4", jedis.get("kif")); + + // Missing key semantics + jedis.del("kif_missing"); + assertNull(jedis.set("kif_missing", "x", ValueCondition.valueEq("anything"))); // missing + IFEQ should fail + assertEquals("OK", jedis.set("kif_missing", "x", ValueCondition.valueNe("anything"))); // missing + IFNE should pass + } + + + @Test + @SinceRedisVersion("8.3.224") + public void setGetWithIFConditions() { + jedis.del("sgk"); + // Missing + IFNE should set and return previous (null) + assertNull(jedis.setGet("sgk", "v1", ValueCondition.valueNe("x"))); + assertEquals("v1", jedis.get("sgk")); + + // IFEQ matches -> returns old value and sets + assertEquals("v1", jedis.setGet("sgk", "v2", ValueCondition.valueEq("v1"))); + assertEquals("v2", jedis.get("sgk")); + + // IFEQ fails -> returns old value and does not set + assertEquals("v2", jedis.setGet("sgk", "v3", ValueCondition.valueEq("nope"))); + assertEquals("v2", jedis.get("sgk")); + } + + @Test + @SinceRedisVersion("8.3.224") + public void setWithIFDigestConditions() { + jedis.set("dk", "abc"); + String dig = jedis.digestKey("dk"); + + // IFDEQ matches -> set + assertEquals("OK", jedis.set("dk", "def", ValueCondition.digestEq(dig))); + String newDig = jedis.digestKey("dk"); + assertTrue(newDig != null && newDig.length() == 16); + + // IFDEQ fails -> no set + assertNull(jedis.set("dk", "ghi", ValueCondition.digestEq(dig))); + assertEquals("def", jedis.get("dk")); + + // IFDNE equal digest -> fail (no set) + assertNull(jedis.set("dk", "ghi", ValueCondition.digestNe(newDig))); + assertEquals("def", jedis.get("dk")); + + // Missing key semantics + jedis.del("dm"); + assertNull(jedis.set("dm", "x", ValueCondition.digestEq("0000000000000000"))); + jedis.del("dm"); + assertEquals("OK", jedis.set("dm", "x", ValueCondition.digestNe("0000000000000000"))); + } + + @Test + @SinceRedisVersion("8.3.224") + public void casCadEndToEndExample() { + final String k = "cas:ex"; + jedis.del(k); + + // 1) Create initial value + assertEquals("OK", jedis.set(k, "v1")); + assertEquals("v1", jedis.get(k)); + + // 2) Read digest and use it to CAS to v2 + String d1 = jedis.digestKey(k); + assertTrue(d1 != null && d1.length() == 16); + + // Wrong digest must not set + assertNull(jedis.set(k, "bad", ValueCondition.digestEq("0000000000000000"))); + assertEquals("v1", jedis.get(k)); + + // Correct digest sets the new value + assertEquals("OK", jedis.set(k, "v2", ValueCondition.digestEq(d1))); + assertEquals("v2", jedis.get(k)); + + // 3) Delete using DELEX guarded by the latest digest + String d2 = jedis.digestKey(k); + assertEquals(0L, jedis.delex(k, ValueCondition.digestEq("0000000000000000"))); + assertEquals(1L, jedis.delex(k, ValueCondition.digestEq(d2))); + assertFalse(jedis.exists(k)); + } + @Test + @SinceRedisVersion("8.3.224") + public void casCadEndToEndExample_Experimental() { + final String k = "cas:ex2"; + jedis.del(k); + + assertEquals("OK", jedis.set(k, "v1")); + + String d1 = jedis.digestKey(k); + ValueCondition cond1 = ValueCondition.digestEq(d1); + assertEquals("OK", jedis.set(k, "v2", cond1)); + assertEquals("v2", jedis.get(k)); + + String d2 = jedis.digestKey(k); + ValueCondition cond2 = ValueCondition.digestEq(d2); + assertEquals(1L, jedis.delex(k, cond2)); + assertFalse(jedis.exists(k)); + } + + @Test + @SinceRedisVersion("8.3.224") + public void setWithParamsAndIFCondition() { + jedis.del("comb1"); + // missing key: NX + IFNE should set + assertEquals("OK", jedis.set("comb1", "v1", setParams().nx(), ValueCondition.valueNe("x"))); + assertEquals("v1", jedis.get("comb1")); + + // existing key: XX + IFEQ should set + assertEquals("OK", jedis.set("comb1", "v2", setParams().xx(), ValueCondition.valueEq("v1"))); + assertEquals("v2", jedis.get("comb1")); + + // existing key: XX + wrong IFEQ should not set + assertNull(jedis.set("comb1", "no", setParams().xx(), ValueCondition.valueEq("nope"))); + assertEquals("v2", jedis.get("comb1")); + } + + @Test + @SinceRedisVersion("8.3.224") + public void setGetWithParamsAndIFCondition() { + jedis.set("comb2", "v1"); + + // existing key: XX + IFEQ should set and return previous + String prev = jedis.setGet("comb2", "v2", setParams().xx(), ValueCondition.valueEq("v1")); + assertEquals("v1", prev); + assertEquals("v2", jedis.get("comb2")); + + // failing condition: returns current and does not set + prev = jedis.setGet("comb2", "no", setParams().xx(), ValueCondition.valueEq("nope")); + assertEquals("v2", prev); + assertEquals("v2", jedis.get("comb2")); + } + } diff --git a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisStringCommandsTest.java b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisStringCommandsTest.java index d12e9e6224..e1f6781655 100644 --- a/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisStringCommandsTest.java +++ b/src/test/java/redis/clients/jedis/mocked/unified/UnifiedJedisStringCommandsTest.java @@ -4,11 +4,13 @@ import static org.hamcrest.Matchers.equalTo; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.times; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.Test; +import redis.clients.jedis.conditions.ValueCondition; import redis.clients.jedis.params.GetExParams; import redis.clients.jedis.params.LCSParams; import redis.clients.jedis.params.SetParams; @@ -866,4 +868,121 @@ public void testSubstrBinary() { verify(commandObjects).substr(key, start, end); } + @Test + public void testDigest() { + String key = "key"; + String expectedHex = "0123456789abcdef"; + + when(commandObjects.digestKey(key)).thenReturn(stringCommandObject); + when(commandExecutor.executeCommand(stringCommandObject)).thenReturn(expectedHex); + + String result = jedis.digestKey(key); + + assertThat(result, equalTo(expectedHex)); + + verify(commandExecutor).executeCommand(stringCommandObject); + verify(commandObjects).digestKey(key); + } + + @Test + public void testDigestBinary() { + byte[] key = "key".getBytes(); + byte[] expectedHex = "fedcba9876543210".getBytes(); + + when(commandObjects.digestKey(key)).thenReturn(bytesCommandObject); + when(commandExecutor.executeCommand(bytesCommandObject)).thenReturn(expectedHex); + + byte[] result = jedis.digestKey(key); + + assertThat(result, equalTo(expectedHex)); + + verify(commandExecutor).executeCommand(bytesCommandObject); + verify(commandObjects).digestKey(key); + } + + @Test + public void testDelex() { + String key = "dkp"; + ValueCondition cond = ValueCondition.valueEq("v1"); + long expected = 1L; + + when(commandObjects.delex(key, cond)).thenReturn(longCommandObject); + when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expected); + + long result = jedis.delex(key, cond); + + assertThat(result, equalTo(expected)); + + verify(commandExecutor).executeCommand(longCommandObject); + verify(commandObjects).delex(key, cond); + } + + @Test + public void testDelexBinary() { + byte[] key = "dkp".getBytes(); + ValueCondition cond = ValueCondition.digestEq("0123456789abcdef"); + long expected = 0L; + + when(commandObjects.delex(key, cond)).thenReturn(longCommandObject); + when(commandExecutor.executeCommand(longCommandObject)).thenReturn(expected); + + long result = jedis.delex(key, cond); + + assertThat(result, equalTo(expected)); + + verify(commandExecutor).executeCommand(longCommandObject); + verify(commandObjects).delex(key, cond); + } + + @Test + public void testSetWithParamsIFConditions() { + String key = "k"; + String value = "v"; + ValueCondition c1 = ValueCondition.valueEq("v"); + ValueCondition c2 = ValueCondition.valueNe("x"); + ValueCondition c3 = ValueCondition.digestEq("0123456789abcdef"); + ValueCondition c4 = ValueCondition.digestNe("0123456789abcdef"); + + when(commandObjects.set(key, value, c1)).thenReturn(stringCommandObject); + when(commandExecutor.executeCommand(stringCommandObject)).thenReturn("OK"); + assertThat(jedis.set(key, value, c1), equalTo("OK")); + verify(commandExecutor, times(1)).executeCommand(stringCommandObject); + verify(commandObjects).set(key, value, c1); + + when(commandObjects.set(key, value, c2)).thenReturn(stringCommandObject); + when(commandExecutor.executeCommand(stringCommandObject)).thenReturn("OK"); + assertThat(jedis.set(key, value, c2), equalTo("OK")); + verify(commandExecutor, times(2)).executeCommand(stringCommandObject); + verify(commandObjects).set(key, value, c2); + + when(commandObjects.set(key, value, c3)).thenReturn(stringCommandObject); + when(commandExecutor.executeCommand(stringCommandObject)).thenReturn("OK"); + assertThat(jedis.set(key, value, c3), equalTo("OK")); + verify(commandExecutor, times(3)).executeCommand(stringCommandObject); + verify(commandObjects).set(key, value, c3); + + when(commandObjects.set(key, value, c4)).thenReturn(stringCommandObject); + when(commandExecutor.executeCommand(stringCommandObject)).thenReturn("OK"); + assertThat(jedis.set(key, value, c4), equalTo("OK")); + verify(commandExecutor, times(4)).executeCommand(stringCommandObject); + verify(commandObjects).set(key, value, c4); + } + + @Test + public void testSetGetWithParamsIFConditions() { + String key = "kg"; + String value = "v"; + ValueCondition cond = ValueCondition.valueEq("v"); + + when(commandObjects.setGet(key, value, cond)).thenReturn(stringCommandObject); + when(commandExecutor.executeCommand(stringCommandObject)).thenReturn("old"); + + String result = jedis.setGet(key, value, cond); + + assertThat(result, equalTo("old")); + + verify(commandExecutor).executeCommand(stringCommandObject); + verify(commandObjects).setGet(key, value, cond); + } + }