From 3f2995c88e21f836c5a5bf06efc022b40dd1d924 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 10:19:19 -0500 Subject: [PATCH 01/12] Polish "Add HGETDEL, HGETEX and HSETEX hash commands". This commit polishes the previous commit with the following: - Convert space indents into tab indents Signed-off-by: Chris Bono --- .../DefaultStringRedisConnection.java | 24 +- .../connection/DefaultedRedisConnection.java | 40 +- .../connection/ReactiveHashCommands.java | 502 +++++++++--------- .../redis/connection/RedisHashCommands.java | 158 +++--- .../connection/StringRedisConnection.java | 74 +-- .../lettuce/LettuceReactiveHashCommands.java | 65 +-- .../data/redis/core/BoundHashOperations.java | 46 +- .../redis/core/DefaultHashOperations.java | 33 +- .../core/DefaultReactiveHashOperations.java | 68 +-- .../data/redis/core/HashOperations.java | 46 +- .../redis/core/ReactiveHashOperations.java | 74 +-- .../data/redis/core/RedisCommand.java | 2 +- .../connection/RedisConnectionUnitTests.java | 18 +- 13 files changed, 577 insertions(+), 573 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 6a7ad98f41..0cc7c5c827 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -1617,20 +1617,20 @@ public List hVals(String key) { return convertAndReturn(delegate.hVals(serialize(key)), byteListToStringList); } - @Override - public List hGetDel(String key, String... fields) { - return convertAndReturn(delegate.hGetDel(serialize(key), serializeMulti(fields)), byteListToStringList); - } + @Override + public List hGetDel(String key, String... fields) { + return convertAndReturn(delegate.hGetDel(serialize(key), serializeMulti(fields)), byteListToStringList); + } - @Override - public List hGetEx(String key, Expiration expiration, String... fields) { - return convertAndReturn(delegate.hGetEx(serialize(key), expiration, serializeMulti(fields)), byteListToStringList); - } + @Override + public List hGetEx(String key, Expiration expiration, String... fields) { + return convertAndReturn(delegate.hGetEx(serialize(key), expiration, serializeMulti(fields)), byteListToStringList); + } - @Override - public Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, Expiration expiration) { - return convertAndReturn(delegate.hSetEx(serialize(key), serialize(hashes), condition, expiration), Converters.identityConverter()); - } + @Override + public Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, Expiration expiration) { + return convertAndReturn(delegate.hSetEx(serialize(key), serialize(hashes), condition, expiration), Converters.identityConverter()); + } @Override public Long incr(String key) { diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 5a5e1daa57..706db37a39 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1602,26 +1602,26 @@ default List hpTtl(byte[] key, byte[]... fields) { return hashCommands().hpTtl(key, fields); } - /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ - @Override - @Deprecated - default List hGetDel(byte[] key, byte[]... fields) { - return hashCommands().hGetDel(key, fields); - } - - /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ - @Override - @Deprecated - default List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { - return hashCommands().hGetEx(key, expiration, fields); - } - - /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ - @Override - @Deprecated - default Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { - return hashCommands().hSetEx(key, hashes, condition, expiration); - } + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hGetDel(byte[] key, byte[]... fields) { + return hashCommands().hGetDel(key, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { + return hashCommands().hGetEx(key, expiration, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { + return hashCommands().hSetEx(key, hashes, condition, expiration); + } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index 47898878f0..fed5e2d53a 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -1257,254 +1257,256 @@ default Flux hpTtl(ByteBuffer key, List fields) { Flux> hpTtl(Publisher commands); - /** - * {@literal HGETDEL} {@link Command}. - * - * @author Viktoriya Kutsarova - * @see Redis Documentation: HGETDEL - */ - class HGetDelCommand extends HashFieldsCommand { - - private HGetDelCommand(@Nullable ByteBuffer key, List fields) { - super(key, fields); - } - - /** - * Creates a new {@link HGetDelCommand} given a {@link ByteBuffer field name}. - * - * @param field must not be {@literal null}. - * @return a new {@link HGetDelCommand} for a {@link ByteBuffer field name}. - */ - public static HGetDelCommand field(ByteBuffer field) { - - Assert.notNull(field, "Field must not be null"); - - return new HGetDelCommand(null, Collections.singletonList(field)); - } - - /** - * Creates a new {@link HGetDelCommand} given a {@link Collection} of field names. - * - * @param fields must not be {@literal null}. - * @return a new {@link HGetDelCommand} for a {@link Collection} of field names. - */ - public static HGetDelCommand fields(Collection fields) { - - Assert.notNull(fields, "Fields must not be null"); - - return new HGetDelCommand(null, new ArrayList<>(fields)); - } - - /** - * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. - * - * @param key must not be {@literal null}. - * @return a new {@link HGetDelCommand} with {@literal key} applied. - */ - public HGetDelCommand from(ByteBuffer key) { - - Assert.notNull(key, "Key must not be null"); - - return new HGetDelCommand(key, getFields()); - } - } - - - /** - * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the - * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETDEL - */ - default Mono> hGetDel(ByteBuffer key, Collection fields) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - return hGetDel(Mono.just(HGetDelCommand.fields(fields).from(key))).next().map(MultiValueResponse::getOutput); - } - - /** - * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the - * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param commands must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETDEL - */ - Flux> hGetDel(Publisher commands); - - class HGetExCommand extends HashFieldsCommand { - - private final Expiration expiration; - - private HGetExCommand(@Nullable ByteBuffer key, List fields, Expiration expiration) { - - super(key, fields); - - this.expiration = expiration; - } - - /** - * Creates a new {@link HGetExCommand}. - * - * @param fields the {@code fields} names to apply expiration to - * @param expiration the {@link Expiration} to apply to the given {@literal fields}. - * @return new instance of {@link HGetExCommand}. - */ - public static HGetExCommand expire(List fields, Expiration expiration) { - return new HGetExCommand(null, fields, expiration); - } - - /** - * @param key the {@literal key} from which to expire the {@literal fields} from. - * @return new instance of {@link HashExpireCommand}. - */ - public HGetExCommand from(ByteBuffer key) { - return new HGetExCommand(key, getFields(), expiration); - } - - /** - * Creates a new {@link HGetExCommand}. - * - * @param fields the {@code fields} names to apply expiration to - * @return new instance of {@link HGetExCommand}. - */ - public HGetExCommand fields(Collection fields) { - return new HGetExCommand(getKey(), new ArrayList<>(fields), expiration); - } - - public Expiration getExpiration() { - return expiration; - } - } - - /** - * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or - * time-to-live (TTL) for given {@literal fields}. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETEX - */ - default Mono> hGetEx(ByteBuffer key, Expiration expiration, List fields) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - return hGetEx(Mono.just(HGetExCommand.expire(fields, expiration).from(key))).next().map(MultiValueResponse::getOutput); - } - - /** - * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or - * time-to-live (TTL) for given {@literal fields}. - * - * @param commands must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HGETEX - */ - Flux> hGetEx(Publisher commands); - - /** - * {@literal HSETEX} {@link Command}. - * - * @author Viktoriya Kutsarova - * @see Redis Documentation: HSETEX - */ - class HSetExCommand extends KeyCommand { - - private final Map fieldValueMap; - private final RedisHashCommands.HashFieldSetOption condition; - private final Expiration expiration; - - private HSetExCommand(@Nullable ByteBuffer key, Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - super(key); - this.fieldValueMap = fieldValueMap; - this.condition = condition; - this.expiration = expiration; - } - - /** - * Creates a new {@link HSetExCommand} for setting field-value pairs with condition and expiration. - * - * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. - * @param condition the condition for setting fields; must not be {@literal null}. - * @param expiration the expiration to apply; must not be {@literal null}. - * @return new instance of {@link HSetExCommand}. - */ - public static HSetExCommand setWithConditionAndExpiration(Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - return new HSetExCommand(null, fieldValueMap, condition, expiration); - } - - /** - * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. - * - * @param key must not be {@literal null}. - * @return a new {@link HSetExCommand} with {@literal key} applied. - */ - public HSetExCommand from(ByteBuffer key) { - Assert.notNull(key, "Key must not be null"); - return new HSetExCommand(key, fieldValueMap, condition, expiration); - } - - /** - * @return the field-value map. - */ - public Map getFieldValueMap() { - return fieldValueMap; - } - - /** - * @return the condition for setting fields. - */ - public RedisHashCommands.HashFieldSetOption getCondition() { - return condition; - } - - /** - * @return the expiration to apply. - */ - public Expiration getExpiration() { - return expiration; - } - } - - /** - * Set field-value pairs in hash at {@literal key} with condition and expiration. - * - * @param key must not be {@literal null}. - * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. - * @param condition the condition for setting fields; must not be {@literal null}. - * @param expiration the expiration to apply; must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - */ - default Mono hSetEx(ByteBuffer key, Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fieldValueMap, "Field-value map must not be null"); - Assert.notNull(condition, "Condition must not be null"); - Assert.notNull(expiration, "Expiration must not be null"); - - return hSetEx(Mono.just(HSetExCommand.setWithConditionAndExpiration(fieldValueMap, condition, expiration).from(key))) - .next().map(CommandResponse::getOutput); - } - - /** - * Set field-value pairs in hash at {@literal key} with condition and expiration. - * - * @param commands must not be {@literal null}. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - */ - Flux> hSetEx(Publisher commands); + /** + * {@literal HGETDEL} {@link Command}. + * + * @author Viktoriya Kutsarova + * @see Redis Documentation: HGETDEL + */ + class HGetDelCommand extends HashFieldsCommand { + + private HGetDelCommand(@Nullable ByteBuffer key, List fields) { + super(key, fields); + } + + /** + * Creates a new {@link HGetDelCommand} given a {@link ByteBuffer field name}. + * + * @param field must not be {@literal null}. + * @return a new {@link HGetDelCommand} for a {@link ByteBuffer field name}. + */ + public static HGetDelCommand field(ByteBuffer field) { + + Assert.notNull(field, "Field must not be null"); + + return new HGetDelCommand(null, Collections.singletonList(field)); + } + + /** + * Creates a new {@link HGetDelCommand} given a {@link Collection} of field names. + * + * @param fields must not be {@literal null}. + * @return a new {@link HGetDelCommand} for a {@link Collection} of field names. + */ + public static HGetDelCommand fields(Collection fields) { + + Assert.notNull(fields, "Fields must not be null"); + + return new HGetDelCommand(null, new ArrayList<>(fields)); + } + + /** + * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param key must not be {@literal null}. + * @return a new {@link HGetDelCommand} with {@literal key} applied. + */ + public HGetDelCommand from(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null"); + + return new HGetDelCommand(key, getFields()); + } + } + + + /** + * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the + * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HGETDEL + */ + default Mono> hGetDel(ByteBuffer key, Collection fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + return hGetDel(Mono.just(HGetDelCommand.fields(fields).from(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get and delete the value of one or more {@literal fields} from hash at {@literal key}. Values are returned in the + * order of the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HGETDEL + */ + Flux> hGetDel(Publisher commands); + + class HGetExCommand extends HashFieldsCommand { + + private final Expiration expiration; + + private HGetExCommand(@Nullable ByteBuffer key, List fields, Expiration expiration) { + + super(key, fields); + + this.expiration = expiration; + } + + /** + * Creates a new {@link HGetExCommand}. + * + * @param fields the {@code fields} names to apply expiration to + * @param expiration the {@link Expiration} to apply to the given {@literal fields}. + * @return new instance of {@link HGetExCommand}. + */ + public static HGetExCommand expire(List fields, Expiration expiration) { + return new HGetExCommand(null, fields, expiration); + } + + /** + * @param key the {@literal key} from which to expire the {@literal fields} from. + * @return new instance of {@link HashExpireCommand}. + */ + public HGetExCommand from(ByteBuffer key) { + return new HGetExCommand(key, getFields(), expiration); + } + + /** + * Creates a new {@link HGetExCommand}. + * + * @param fields the {@code fields} names to apply expiration to + * @return new instance of {@link HGetExCommand}. + */ + public HGetExCommand fields(Collection fields) { + return new HGetExCommand(getKey(), new ArrayList<>(fields), expiration); + } + + public Expiration getExpiration() { + return expiration; + } + } + + /** + * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or + * time-to-live (TTL) for given {@literal fields}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HGETEX + */ + default Mono> hGetEx(ByteBuffer key, Expiration expiration, List fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + return hGetEx(Mono.just(HGetExCommand.expire(fields, expiration).from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get the value of one or more {@literal fields} from hash at {@literal key} and optionally set expiration time or + * time-to-live (TTL) for given {@literal fields}. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HGETEX + */ + Flux> hGetEx(Publisher commands); + + /** + * {@literal HSETEX} {@link Command}. + * + * @author Viktoriya Kutsarova + * @see Redis Documentation: HSETEX + */ + class HSetExCommand extends KeyCommand { + + private final Map fieldValueMap; + private final RedisHashCommands.HashFieldSetOption condition; + private final Expiration expiration; + + private HSetExCommand(@Nullable ByteBuffer key, Map fieldValueMap, + RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + super(key); + this.fieldValueMap = fieldValueMap; + this.condition = condition; + this.expiration = expiration; + } + + /** + * Creates a new {@link HSetExCommand} for setting field-value pairs with condition and expiration. + * + * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. + * @param condition the condition for setting fields; must not be {@literal null}. + * @param expiration the expiration to apply; must not be {@literal null}. + * @return new instance of {@link HSetExCommand}. + */ + public static HSetExCommand setWithConditionAndExpiration(Map fieldValueMap, + RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + return new HSetExCommand(null, fieldValueMap, condition, expiration); + } + + /** + * Applies the hash {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param key must not be {@literal null}. + * @return a new {@link HSetExCommand} with {@literal key} applied. + */ + public HSetExCommand from(ByteBuffer key) { + Assert.notNull(key, "Key must not be null"); + return new HSetExCommand(key, fieldValueMap, condition, expiration); + } + + /** + * @return the field-value map. + */ + public Map getFieldValueMap() { + return fieldValueMap; + } + + /** + * @return the condition for setting fields. + */ + public RedisHashCommands.HashFieldSetOption getCondition() { + return condition; + } + + /** + * @return the expiration to apply. + */ + public Expiration getExpiration() { + return expiration; + } + } + + /** + * Set field-value pairs in hash at {@literal key} with condition and expiration. + * + * @param key must not be {@literal null}. + * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. + * @param condition the condition for setting fields; must not be {@literal null}. + * @param expiration the expiration to apply; must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + */ + default Mono hSetEx(ByteBuffer key, Map fieldValueMap, + RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fieldValueMap, "Field-value map must not be null"); + Assert.notNull(condition, "Condition must not be null"); + Assert.notNull(expiration, "Expiration must not be null"); + + return hSetEx(Mono.just(HSetExCommand.setWithConditionAndExpiration(fieldValueMap, condition, expiration) + .from(key))) + .next().map(CommandResponse::getOutput); + } + + /** + * Set field-value pairs in hash at {@literal key} with condition and expiration. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + */ + Flux> hSetEx(Publisher commands); } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 80146c9fb0..d9e3e6464e 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -544,86 +544,86 @@ default List hExpireAt(byte @NonNull [] key, long unixTime, byte @NonNull */ List<@NonNull Long> hpTtl(byte @NonNull [] key, byte @NonNull [] @NonNull... fields); - /** - * Get and delete the value of one or more {@code fields} from hash at {@code key}. Values are returned in the order of - * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return list of values for deleted {@code fields} ({@literal null} for fields that does not exist) or an + /** + * Get and delete the value of one or more {@code fields} from hash at {@code key}. Values are returned in the order of + * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return list of values for deleted {@code fields} ({@literal null} for fields that does not exist) or an * empty {@link List} if key does not exist or {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HGETDEL - */ - List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull... fields); - - /** - * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or - * time-to-live (TTL) for given {@code fields}. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. + * @see Redis Documentation: HGETDEL + */ + List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull ... fields); + + /** + * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or + * time-to-live (TTL) for given {@code fields}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. * @return list of values for given {@code fields} or an empty {@link List} if key does not * exist or {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HGETEX - */ - List hGetEx(byte @NonNull [] key, Expiration expiration, - byte @NonNull [] @NonNull... fields); - - /** - * Set field-value pairs in hash at {@literal key} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param hashes the field-value pairs to set; must not be {@literal null}. - * @param hashFieldSetOption the optional condition for setting fields. - * @param expiration the optional expiration to apply. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - */ - Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption hashFieldSetOption, - Expiration expiration); - - /** - * {@code HSETEX} command arguments for {@code FNX}, {@code FXX}. - * - * @author Viktoriya Kutsarova - */ - enum HashFieldSetOption { - - /** - * Do not set any additional command argument. - */ - UPSERT, - - /** - * {@code FNX} - */ - IF_NONE_EXIST, - - /** - * {@code FXX} - */ - IF_ALL_EXIST; - - /** - * Do not set any additional command argument. - */ - public static HashFieldSetOption upsert() { - return UPSERT; - } - - /** - * {@code FNX} - */ - public static HashFieldSetOption ifNoneExist() { - return IF_NONE_EXIST; - } - - /** - * {@code FXX} - */ - public static HashFieldSetOption ifAllExist() { - return IF_ALL_EXIST; - } - } + * @see Redis Documentation: HGETEX + */ + List hGetEx(byte @NonNull [] key, Expiration expiration, + byte @NonNull [] @NonNull ... fields); + + /** + * Set field-value pairs in hash at {@literal key} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param hashes the field-value pairs to set; must not be {@literal null}. + * @param hashFieldSetOption the optional condition for setting fields. + * @param expiration the optional expiration to apply. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + */ + Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption hashFieldSetOption, + Expiration expiration); + + /** + * {@code HSETEX} command arguments for {@code FNX}, {@code FXX}. + * + * @author Viktoriya Kutsarova + */ + enum HashFieldSetOption { + + /** + * Do not set any additional command argument. + */ + UPSERT, + + /** + * {@code FNX} + */ + IF_NONE_EXIST, + + /** + * {@code FXX} + */ + IF_ALL_EXIST; + + /** + * Do not set any additional command argument. + */ + public static HashFieldSetOption upsert() { + return UPSERT; + } + + /** + * {@code FNX} + */ + public static HashFieldSetOption ifNoneExist() { + return IF_NONE_EXIST; + } + + /** + * {@code FXX} + */ + public static HashFieldSetOption ifAllExist() { + return IF_ALL_EXIST; + } + } } diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 3069f0c587..f2ed6e7d1b 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -2564,43 +2564,43 @@ List hpExpireAt(@NonNull String key, long unixTimeInMillis, ExpirationOpti */ List hpTtl(@NonNull String key, @NonNull String @NonNull... fields); - /** - * Get and delete the value of one or more {@code fields} from hash at {@code key}. When the last field is deleted, - * the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HMGET - * @see RedisHashCommands#hMGet(byte[], byte[]...) - */ - List hGetDel(@NonNull String key, @NonNull String @NonNull... fields); - - /** - * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or - * time-to-live (TTL) for given {@code fields}. - * - * @param key must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HGETEX - * @see RedisHashCommands#hGetEx(byte[], Expiration, byte[]...) - */ - List hGetEx(@NonNull String key, Expiration expiration, @NonNull String @NonNull... fields); - - /** - * Set field-value pairs in hash at {@literal key} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param hashes the field-value pairs to set; must not be {@literal null}. - * @param condition the optional condition for setting fields. - * @param expiration the optional expiration to apply. - * @return never {@literal null}. - * @see Redis Documentation: HSETEX - * @see RedisHashCommands#hSetEx(byte[], Map, HashFieldSetOption, Expiration) - */ - Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, - Expiration expiration); + /** + * Get and delete the value of one or more {@code fields} from hash at {@code key}. When the last field is deleted, + * the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HMGET + * @see RedisHashCommands#hMGet(byte[], byte[]...) + */ + List hGetDel(@NonNull String key, @NonNull String @NonNull ... fields); + + /** + * Get the value of one or more {@code fields} from hash at {@code key} and optionally set expiration time or + * time-to-live (TTL) for given {@code fields}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return empty {@link List} if key does not exist. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HGETEX + * @see RedisHashCommands#hGetEx(byte[], Expiration, byte[]...) + */ + List hGetEx(@NonNull String key, Expiration expiration, @NonNull String @NonNull ... fields); + + /** + * Set field-value pairs in hash at {@literal key} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param hashes the field-value pairs to set; must not be {@literal null}. + * @param condition the optional condition for setting fields. + * @param expiration the optional expiration to apply. + * @return never {@literal null}. + * @see Redis Documentation: HSETEX + * @see RedisHashCommands#hSetEx(byte[], Map, HashFieldSetOption, Expiration) + */ + Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, + Expiration expiration); // ------------------------------------------------------------------------- // Methods dealing with HyperLogLog diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index ac9f0491d0..ae77ebbd84 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -356,50 +356,51 @@ public Flux> hpTtl(Publisher> hGetDel(Publisher commands) { + @Override + public Flux> hGetDel(Publisher commands) { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getFields(), "Fields must not be null"); + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); - return cmd.hgetdel(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)).collectList() - .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) - .collect(Collectors.toList()))); - })); - } + return cmd.hgetdel(command.getKey(), command.getFields().toArray(ByteBuffer[]::new)).collectList() + .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) + .collect(Collectors.toList()))); + })); + } - @Override - public Flux> hGetEx(Publisher commands) { + @Override + public Flux> hGetEx(Publisher commands) { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getFields(), "Fields must not be null"); + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFields(), "Fields must not be null"); - return cmd.hgetex(command.getKey(), LettuceConverters.toHGetExArgs(command.getExpiration()), command.getFields().toArray(ByteBuffer[]::new)).collectList() - .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) - .collect(Collectors.toList()))); - })); - } + return cmd.hgetex(command.getKey(), LettuceConverters.toHGetExArgs(command.getExpiration()), command.getFields() + .toArray(ByteBuffer[]::new)).collectList() + .map(value -> new MultiValueResponse<>(command, value.stream().map(v -> v.getValueOrElse(null)) + .collect(Collectors.toList()))); + })); + } - @Override - public Flux> hSetEx(Publisher commands) { - return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + @Override + public Flux> hSetEx(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { - Assert.notNull(command.getKey(), "Key must not be null"); - Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null"); + Assert.notNull(command.getKey(), "Key must not be null"); + Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null"); - Map entries = command.getFieldValueMap(); + Map entries = command.getFieldValueMap(); - return cmd.hsetex(command.getKey(), - LettuceConverters.toHSetExArgs(command.getCondition(), command.getExpiration()), entries) - .map(LettuceConverters.longToBooleanConverter()::convert) - .map(value -> new BooleanResponse<>(command, value)); + return cmd.hsetex(command.getKey(), + LettuceConverters.toHSetExArgs(command.getCondition(), command.getExpiration()), entries) + .map(LettuceConverters.longToBooleanConverter()::convert) + .map(value -> new BooleanResponse<>(command, value)); - })); - } + })); + } private static Map.Entry toEntry(KeyValue kv) { diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 9d44c8c2ba..8a874bd847 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -256,29 +256,29 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * @return {@literal null} when used in pipeline / transaction. * @since 3.1 */ - List getAndDelete(@NonNull Collection<@NonNull HK> hashFields); + List getAndDelete(@NonNull Collection<@NonNull HK> hashFields); - /** - * Get and optionally expire the value for given {@code hashFields} from the hash at the bound key. Values are in the order of the - * requested hash fields. Absent field values are represented using {@literal null} in the resulting {@link List}. - * - * @param expiration is optional. - * @param hashFields must not be {@literal null}. - * @return never {@literal null}. - * @since 4.0 - */ - List getAndExpire(Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); + /** + * Get and optionally expire the value for given {@code hashFields} from the hash at the bound key. Values are in the order of the + * requested hash fields. Absent field values are represented using {@literal null} in the resulting {@link List}. + * + * @param expiration is optional. + * @param hashFields must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + */ + List getAndExpire(Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); - /** - * Set the value of one or more fields using data provided in {@code m} at the bound key, and optionally set their - * expiration time or time-to-live (TTL). The {@code condition} determines whether the fields are set. - * - * @param m must not be {@literal null}. - * @param condition is optional. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if - * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the - * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields - * unconditionally. - * @param expiration is optional. - */ - void putAndExpire(Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); + /** + * Set the value of one or more fields using data provided in {@code m} at the bound key, and optionally set their + * expiration time or time-to-live (TTL). The {@code condition} determines whether the fields are set. + * + * @param m must not be {@literal null}. + * @param condition is optional. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if + * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the + * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields + * unconditionally. + * @param expiration is optional. + */ + void putAndExpire(Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index cf8ea5a0e1..a505adf523 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -215,8 +215,8 @@ public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fi return deserializeHashValues(rawValues); } - @Override - public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, + @Override + public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, @NonNull Collection<@NonNull HK> fields) { if (fields.isEmpty()) { @@ -230,28 +230,29 @@ public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } - List rawValues = execute(connection -> connection.hashCommands().hGetEx(rawKey, expiration, rawHashKeys)); + List rawValues = execute(connection -> connection.hashCommands() + .hGetEx(rawKey, expiration, rawHashKeys)); return deserializeHashValues(rawValues); } - @Override - public Boolean putAndExpire(@NonNull K key, @NonNull Map m, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - if (m.isEmpty()) { - return false; - } + @Override + public Boolean putAndExpire(@NonNull K key, @NonNull Map m, + RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + if (m.isEmpty()) { + return false; + } - byte[] rawKey = rawKey(key); + byte[] rawKey = rawKey(key); - Map hashes = new LinkedHashMap<>(m.size()); + Map hashes = new LinkedHashMap<>(m.size()); - for (Map.Entry entry : m.entrySet()) { - hashes.put(rawHashKey(entry.getKey()), rawHashValue(entry.getValue())); - } + for (Map.Entry entry : m.entrySet()) { + hashes.put(rawHashKey(entry.getKey()), rawHashValue(entry.getValue())); + } - return execute(connection -> connection.hashCommands().hSetEx(rawKey, hashes, condition, expiration)); - } + return execute(connection -> connection.hashCommands().hSetEx(rawKey, hashes, condition, expiration)); + } @Override public void put(@NonNull K key, @NonNull HK hashKey, HV value) { diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java index 3dc149940e..8847ed0222 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -112,40 +112,40 @@ public Mono> multiGet(H key, Collection hashKeys) { .flatMap(hks -> hashCommands.hMGet(rawKey(key), hks)).map(this::deserializeHashValues)); } - @Override - public Mono> getAndDelete(H key, Collection hashKeys) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashKeys, "Hash keys must not be null"); - Assert.notEmpty(hashKeys, "Hash keys must not be empty"); - - return createMono(hashCommands -> Flux.fromIterable(hashKeys) // - .map(this::rawHashKey) // - .collectList() // - .flatMap(hks -> hashCommands.hGetDel(rawKey(key), hks)).map(this::deserializeHashValues)); - } - - @Override - public Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(map, "Map must not be null"); - - return createMono(hashCommands -> Flux.fromIterable(() -> map.entrySet().iterator()) // - .collectMap(entry -> rawHashKey(entry.getKey()), entry -> rawHashValue(entry.getValue())) // - .flatMap(serialized -> hashCommands.hSetEx(rawKey(key), serialized, condition, expiration))); - } - - @Override - public Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashKeys, "Hash keys must not be null"); - Assert.notEmpty(hashKeys, "Hash keys must not be empty"); - - return createMono(hashCommands -> Flux.fromIterable(hashKeys) // - .map(this::rawHashKey) // - .collectList() // - .flatMap(hks -> hashCommands.hGetEx(rawKey(key), expiration, hks)).map(this::deserializeHashValues)); - } + @Override + public Mono> getAndDelete(H key, Collection hashKeys) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashKeys, "Hash keys must not be null"); + Assert.notEmpty(hashKeys, "Hash keys must not be empty"); + + return createMono(hashCommands -> Flux.fromIterable(hashKeys) // + .map(this::rawHashKey) // + .collectList() // + .flatMap(hks -> hashCommands.hGetDel(rawKey(key), hks)).map(this::deserializeHashValues)); + } + + @Override + public Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(map, "Map must not be null"); + + return createMono(hashCommands -> Flux.fromIterable(() -> map.entrySet().iterator()) // + .collectMap(entry -> rawHashKey(entry.getKey()), entry -> rawHashValue(entry.getValue())) // + .flatMap(serialized -> hashCommands.hSetEx(rawKey(key), serialized, condition, expiration))); + } + + @Override + public Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashKeys, "Hash keys must not be null"); + Assert.notEmpty(hashKeys, "Hash keys must not be empty"); + + return createMono(hashCommands -> Flux.fromIterable(hashKeys) // + .map(this::rawHashKey) // + .collectList() // + .flatMap(hks -> hashCommands.hGetEx(rawKey(key), expiration, hks)).map(this::deserializeHashValues)); + } @Override public Mono increment(H key, HK hashKey, long delta) { diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index 61e1648f58..579d13c2ee 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -92,30 +92,30 @@ public interface HashOperations { */ List getAndDelete(@NonNull H key, @NonNull Collection<@NonNull HK> hashKeys); - /** - * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of - * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * - * @param key must not be {@literal null}. - * @param expiration is optional. - * @param hashKeys must not be {@literal null}. - * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. - * @since 4.0 - */ - List getAndExpire(@NonNull H key, Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); + /** + * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of + * the requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * + * @param key must not be {@literal null}. + * @param expiration is optional. + * @param hashKeys must not be {@literal null}. + * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. + * @since 4.0 + */ + List getAndExpire(@NonNull H key, Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); - /** - * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param m must not be {@literal null}. - * @param condition is optional. - * @param expiration is optional. - * @return whether all fields were set or {@literal null} when used in pipeline / transaction. - * @since 4.0 - */ - Boolean putAndExpire(@NonNull H key, @NonNull Map m, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration); + /** + * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param m must not be {@literal null}. + * @param condition is optional. + * @param expiration is optional. + * @return whether all fields were set or {@literal null} when used in pipeline / transaction. + * @since 4.0 + */ + Boolean putAndExpire(@NonNull H key, @NonNull Map m, + RedisHashCommands.HashFieldSetOption condition, Expiration expiration); /** * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java index b7c69c2396..12130e048c 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -83,43 +83,43 @@ public interface ReactiveHashOperations { * @return */ Mono> multiGet(H key, Collection hashKeys); - - /** - * Get and remove the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the - * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * When the last field is deleted, the key will also be deleted. - * - * @param key must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return never {@literal null}. - * @since 4.0 - */ - Mono> getAndDelete(H key, Collection hashKeys); - - /** - * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. - * - * @param key must not be {@literal null}. - * @param map must not be {@literal null}. - * @param condition is optional. - * @param expiration is optional. - * @return never {@literal null}. - * @since 4.0 - */ - Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, - Expiration expiration); - - /** - * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the - * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. - * - * @param key must not be {@literal null}. - * @param expiration is optional. - * @param hashKeys must not be {@literal null}. - * @return never {@literal null}. - * @since 4.0 - */ - Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys); + + /** + * Get and remove the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the + * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * When the last field is deleted, the key will also be deleted. + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + */ + Mono> getAndDelete(H key, Collection hashKeys); + + /** + * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. + * + * @param key must not be {@literal null}. + * @param map must not be {@literal null}. + * @param condition is optional. + * @param expiration is optional. + * @return never {@literal null}. + * @since 4.0 + */ + Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, + Expiration expiration); + + /** + * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the + * requested keys. Absent field values are represented using {@literal null} in the resulting {@link List}. + * + * @param key must not be {@literal null}. + * @param expiration is optional. + * @param hashKeys must not be {@literal null}. + * @return never {@literal null}. + * @since 4.0 + */ + Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys); /** * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index c0bc3f8a4d..99a7aeb8cf 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -147,7 +147,7 @@ public enum RedisCommand { HMSET("w", 3), // HPOP("rw", 3), HSET("w", 3, 3), // - HSETEX("w", 3), // + HSETEX("w", 3), // HSETNX("w", 3, 3), // HVALS("r", 1, 1), // HEXPIRE("w", 5), // diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index aea8fdc6bd..754f17e889 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -445,17 +445,17 @@ public List hMGet(byte[] key, byte[]... fields) { return delegate.hMGet(key, fields); } - public List hGetDel(byte[] key, byte[]... fields) { - return delegate.hGetDel(key, fields); - } + public List hGetDel(byte[] key, byte[]... fields) { + return delegate.hGetDel(key, fields); + } - public List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { - return delegate.hGetEx(key, expiration, fields); - } + public List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { + return delegate.hGetEx(key, expiration, fields); + } - public Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { - return delegate.hSetEx(key, hashes, condition, expiration); - } + public Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { + return delegate.hSetEx(key, hashes, condition, expiration); + } public Long zRem(byte[] key, byte[]... values) { return delegate.zRem(key, values); From 3d2b787a0eb97ac7fbca9658e741606df9f510a1 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 11:54:30 -0500 Subject: [PATCH 02/12] - Reorder commands in LettuceConnection Signed-off-by: Chris Bono --- .../data/redis/connection/lettuce/LettuceConnection.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index bdeea2dd6f..5520535e0d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -1243,8 +1243,8 @@ static class TypeHints { COMMAND_OUTPUT_TYPE_MAPPING.put(ZRANGEBYSCORE, ValueListOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANGE, ValueListOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(ZREVRANGEBYSCORE, ValueListOutput.class); - COMMAND_OUTPUT_TYPE_MAPPING.put(HGETDEL, ValueListOutput.class); - COMMAND_OUTPUT_TYPE_MAPPING.put(HGETEX, ValueListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HGETDEL, ValueListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HGETEX, ValueListOutput.class); // BOOLEAN COMMAND_OUTPUT_TYPE_MAPPING.put(EXISTS, BooleanOutput.class); From 38ade5ff425ccdb67ac06d17afc6e2514a52f767 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 11:57:29 -0500 Subject: [PATCH 03/12] - Remove magic UTF characters from LettuceConverters Signed-off-by: Chris Bono --- .../data/redis/connection/lettuce/LettuceConverters.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 98c0969738..c18661d93b 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -656,9 +656,9 @@ static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { * *

Condition mapping:

*
    - *
  • {@code IF_NONE_EXIST}  {@code FNX}
  • - *
  • {@code IF_ALL_EXIST}  {@code FXX}
  • - *
  • {@code UPSERT}  no condition flag
  • + *
  • {@code IF_NONE_EXIST} {@code FNX}
  • + *
  • {@code IF_ALL_EXIST} {@code FXX}
  • + *
  • {@code UPSERT} no condition flag
  • *
* *

Expiration mapping:

From f73833c472da4e8201d9a7fc342544a9de8f3db3 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 12:03:49 -0500 Subject: [PATCH 04/12] - Remove invisible UTF characters from LettuceConverters Signed-off-by: Chris Bono --- .../data/redis/connection/lettuce/LettuceConverters.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index c18661d93b..29e064a808 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -663,10 +663,10 @@ static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { * *

Expiration mapping:

*
    - *
  • {@link Expiration#keepTtl()}  {@code KEEPTTL}
  • - *
  • Unix timestamp  {@code EXAT}/{@code PXAT} depending on time unit
  • - *
  • Relative expiration  {@code EX}/{@code PX} depending on time unit
  • - *
  • {@code null} expiration  no TTL argument
  • + *
  • {@link Expiration#keepTtl()} {@code KEEPTTL}
  • + *
  • Unix timestamp {@code EXAT}/{@code PXAT} depending on time unit
  • + *
  • Relative expiration {@code EX}/{@code PX} depending on time unit
  • + *
  • {@code null} expiration no TTL argument
  • *
* * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. From 090a4790df7ae9373edf7d07c5941ab830897813 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 12:13:42 -0500 Subject: [PATCH 05/12] - Add missing `@since` tags Signed-off-by: Chris Bono --- .../data/redis/connection/ReactiveHashCommands.java | 3 +++ .../data/redis/connection/RedisHashCommands.java | 1 + .../data/redis/connection/lettuce/LettuceConnection.java | 2 +- .../data/redis/core/BoundHashOperations.java | 3 ++- .../data/redis/core/DefaultHashOperations.java | 9 +++------ 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index fed5e2d53a..94b22c9cfc 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -1261,6 +1261,7 @@ default Flux hpTtl(ByteBuffer key, List fields) { * {@literal HGETDEL} {@link Command}. * * @author Viktoriya Kutsarova + * @since 4.0 * @see Redis Documentation: HGETDEL */ class HGetDelCommand extends HashFieldsCommand { @@ -1318,6 +1319,7 @@ public HGetDelCommand from(ByteBuffer key) { * @param key must not be {@literal null}. * @param fields must not be {@literal null}. * @return never {@literal null}. + * @since 4.0 * @see Redis Documentation: HGETDEL */ default Mono> hGetDel(ByteBuffer key, Collection fields) { @@ -1335,6 +1337,7 @@ default Mono> hGetDel(ByteBuffer key, Collection fi * * @param commands must not be {@literal null}. * @return never {@literal null}. + * @since 4.0 * @see Redis Documentation: HGETDEL */ Flux> hGetDel(Publisher commands); diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index d9e3e6464e..8d8335ebad 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -587,6 +587,7 @@ Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFi * {@code HSETEX} command arguments for {@code FNX}, {@code FXX}. * * @author Viktoriya Kutsarova + * @since 4.0 */ enum HashFieldSetOption { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index 5520535e0d..2a367f45e7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -1246,7 +1246,7 @@ static class TypeHints { COMMAND_OUTPUT_TYPE_MAPPING.put(HGETDEL, ValueListOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(HGETEX, ValueListOutput.class); - // BOOLEAN + // BOOLEAN COMMAND_OUTPUT_TYPE_MAPPING.put(EXISTS, BooleanOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(EXPIRE, BooleanOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(EXPIREAT, BooleanOutput.class); diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 8a874bd847..ebf9514f99 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -254,7 +254,7 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * * @param hashFields must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. - * @since 3.1 + * @since 4.0 */ List getAndDelete(@NonNull Collection<@NonNull HK> hashFields); @@ -279,6 +279,7 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields * unconditionally. * @param expiration is optional. + * @since 4.0 */ void putAndExpire(Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index a505adf523..dd7808273e 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -186,8 +186,7 @@ public List multiGet(@NonNull K key, @NonNull Collection<@NonNull HK> fields byte[][] rawHashKeys = new byte[fields.size()][]; int counter = 0; - for (@NonNull - HK hashKey : fields) { + for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } @@ -206,8 +205,7 @@ public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fi byte[] rawKey = rawKey(key); byte[][] rawHashKeys = new byte[fields.size()][]; int counter = 0; - for (@NonNull - HK hashKey : fields) { + for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } List rawValues = execute(connection -> connection.hashCommands().hGetDel(rawKey, rawHashKeys)); @@ -226,8 +224,7 @@ public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, byte[] rawKey = rawKey(key); byte[][] rawHashKeys = new byte[fields.size()][]; int counter = 0; - for (@NonNull - HK hashKey : fields) { + for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } List rawValues = execute(connection -> connection.hashCommands() From d6d7d240dea6649c20ca7dd1cd21dd337aa99229 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 12:27:30 -0500 Subject: [PATCH 06/12] - Add `@see` for new commands in interfaces Signed-off-by: Chris Bono --- .../data/redis/core/BoundHashOperations.java | 9 ++++++--- .../springframework/data/redis/core/HashOperations.java | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index ebf9514f99..701b169d78 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -255,6 +255,7 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * @param hashFields must not be {@literal null}. * @return {@literal null} when used in pipeline / transaction. * @since 4.0 + * @see Redis Documentation: HGETDEL */ List getAndDelete(@NonNull Collection<@NonNull HK> hashFields); @@ -266,6 +267,7 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * @param hashFields must not be {@literal null}. * @return never {@literal null}. * @since 4.0 + * @see Redis Documentation: HSETEX */ List getAndExpire(Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); @@ -275,11 +277,12 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * * @param m must not be {@literal null}. * @param condition is optional. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if - * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the - * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields - * unconditionally. + * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the + * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields + * unconditionally. * @param expiration is optional. * @since 4.0 + * @see Redis Documentation: HSETEX */ void putAndExpire(Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); } diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index 579d13c2ee..e2c68f3fb1 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -89,6 +89,7 @@ public interface HashOperations { * @param hashKeys must not be {@literal null}. * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. * @since 4.0 + * @see Redis Documentation: HGETDEL */ List getAndDelete(@NonNull H key, @NonNull Collection<@NonNull HK> hashKeys); @@ -101,6 +102,7 @@ public interface HashOperations { * @param hashKeys must not be {@literal null}. * @return list of values for the given fields or {@literal null} when used in pipeline / transaction. * @since 4.0 + * @see Redis Documentation: HGETEX */ List getAndExpire(@NonNull H key, Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); @@ -113,6 +115,7 @@ public interface HashOperations { * @param expiration is optional. * @return whether all fields were set or {@literal null} when used in pipeline / transaction. * @since 4.0 + *@see Redis Documentation: HSETEX */ Boolean putAndExpire(@NonNull H key, @NonNull Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); From 748e3441a2bba495b48e8940346f9ed27487d869 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 6 Oct 2025 12:31:19 -0500 Subject: [PATCH 07/12] - Replace Intellij `@NotNull` with Jspecify `@NonNull` Signed-off-by: Chris Bono --- .../redis/connection/DefaultStringRedisConnection.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 0cc7c5c827..bc81fb3988 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -23,7 +23,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.jetbrains.annotations.NotNull; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; @@ -2611,17 +2610,17 @@ public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { } @Override - public List hGetDel(@NotNull byte[] key, @NotNull byte[]... fields) { + public List hGetDel(@NonNull byte[] key, @NonNull byte[]... fields) { return convertAndReturn(delegate.hGetDel(key, fields), Converters.identityConverter()); } @Override - public List hGetEx(@NotNull byte[] key, Expiration expiration, @NotNull byte[]... fields) { + public List hGetEx(@NonNull byte[] key, Expiration expiration, @NonNull byte[]... fields) { return convertAndReturn(delegate.hGetEx(key, expiration, fields), Converters.identityConverter()); } @Override - public Boolean hSetEx(@NotNull byte[] key, @NonNull Map hashes, HashFieldSetOption condition, Expiration expiration) { + public Boolean hSetEx(@NonNull byte[] key, @NonNull Map hashes, HashFieldSetOption condition, Expiration expiration) { return convertAndReturn(delegate.hSetEx(key, hashes, condition, expiration), Converters.identityConverter()); } From 424026b66601bb19082513a32a56b3efafc71f78 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 7 Oct 2025 15:38:25 -0500 Subject: [PATCH 08/12] Add unit tests for the new converter methods Signed-off-by: Chris Bono --- .../DefaultStringRedisConnection.java | 29 +-- .../connection/DefaultedRedisConnection.java | 6 +- .../connection/ReactiveHashCommands.java | 37 ++-- .../redis/connection/RedisHashCommands.java | 11 +- .../connection/StringRedisConnection.java | 4 +- .../jedis/JedisClusterHashCommands.java | 66 +++--- .../connection/jedis/JedisConverters.java | 193 +++++++++--------- .../connection/jedis/JedisHashCommands.java | 45 ++-- .../connection/lettuce/LettuceConverters.java | 172 ++++++++-------- .../lettuce/LettuceHashCommands.java | 47 ++--- .../lettuce/LettuceReactiveHashCommands.java | 1 + .../data/redis/core/BoundHashOperations.java | 9 +- .../redis/core/DefaultHashOperations.java | 15 +- .../core/DefaultReactiveHashOperations.java | 10 +- .../data/redis/core/HashOperations.java | 6 +- .../redis/core/ReactiveHashOperations.java | 9 +- .../jedis/JedisConvertersUnitTests.java | 119 ++++++++++- .../lettuce/LettuceConvertersUnitTests.java | 122 +++++++++++ 18 files changed, 579 insertions(+), 322 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index bc81fb3988..8d8249e6b4 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -2609,20 +2609,21 @@ public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { return this.delegate.hTtl(key, timeUnit, fields); } - @Override - public List hGetDel(@NonNull byte[] key, @NonNull byte[]... fields) { - return convertAndReturn(delegate.hGetDel(key, fields), Converters.identityConverter()); - } - - @Override - public List hGetEx(@NonNull byte[] key, Expiration expiration, @NonNull byte[]... fields) { - return convertAndReturn(delegate.hGetEx(key, expiration, fields), Converters.identityConverter()); - } - - @Override - public Boolean hSetEx(@NonNull byte[] key, @NonNull Map hashes, HashFieldSetOption condition, Expiration expiration) { - return convertAndReturn(delegate.hSetEx(key, hashes, condition, expiration), Converters.identityConverter()); - } + @Override + public List hGetDel(@NonNull byte[] key, @NonNull byte[]... fields) { + return convertAndReturn(delegate.hGetDel(key, fields), Converters.identityConverter()); + } + + @Override + public List hGetEx(@NonNull byte[] key, @Nullable Expiration expiration, @NonNull byte[]... fields) { + return convertAndReturn(delegate.hGetEx(key, expiration, fields), Converters.identityConverter()); + } + + @Override + public Boolean hSetEx(@NonNull byte[] key, @NonNull Map hashes, @NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + return convertAndReturn(delegate.hSetEx(key, hashes, condition, expiration), Converters.identityConverter()); + } public @Nullable List applyExpiration(String key, org.springframework.data.redis.core.types.Expiration expiration, diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 706db37a39..ab0b6a78e3 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Circle; @@ -1612,14 +1613,15 @@ default List hGetDel(byte[] key, byte[]... fields) { /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated - default List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { + default List hGetEx(byte[] key, @Nullable Expiration expiration, byte[]... fields) { return hashCommands().hGetEx(key, expiration, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated - default Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { + default Boolean hSetEx(byte[] key, Map hashes, @NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { return hashCommands().hSetEx(key, hashes, condition, expiration); } diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index 94b22c9cfc..1ca9f031de 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -1342,11 +1343,18 @@ default Mono> hGetDel(ByteBuffer key, Collection fi */ Flux> hGetDel(Publisher commands); + /** + * {@literal HGETEX} {@link Command}. + * + * @author Viktoriya Kutsarova + * @see Redis Documentation: HGETEX + * @since 4.0 + */ class HGetExCommand extends HashFieldsCommand { - private final Expiration expiration; + private final @Nullable Expiration expiration; - private HGetExCommand(@Nullable ByteBuffer key, List fields, Expiration expiration) { + private HGetExCommand(@Nullable ByteBuffer key, List fields, @Nullable Expiration expiration) { super(key, fields); @@ -1357,10 +1365,10 @@ private HGetExCommand(@Nullable ByteBuffer key, List fields, Expirat * Creates a new {@link HGetExCommand}. * * @param fields the {@code fields} names to apply expiration to - * @param expiration the {@link Expiration} to apply to the given {@literal fields}. + * @param expiration the optional {@link Expiration} to apply to the given {@literal fields}. * @return new instance of {@link HGetExCommand}. */ - public static HGetExCommand expire(List fields, Expiration expiration) { + public static HGetExCommand expire(List fields, @Nullable Expiration expiration) { return new HGetExCommand(null, fields, expiration); } @@ -1382,7 +1390,7 @@ public HGetExCommand fields(Collection fields) { return new HGetExCommand(getKey(), new ArrayList<>(fields), expiration); } - public Expiration getExpiration() { + public @Nullable Expiration getExpiration() { return expiration; } } @@ -1392,11 +1400,12 @@ public Expiration getExpiration() { * time-to-live (TTL) for given {@literal fields}. * * @param key must not be {@literal null}. + * @param expiration the optional expiration to set. * @param fields must not be {@literal null}. * @return never {@literal null}. * @see Redis Documentation: HGETEX */ - default Mono> hGetEx(ByteBuffer key, Expiration expiration, List fields) { + default Mono> hGetEx(ByteBuffer key, @Nullable Expiration expiration, List fields) { Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -1419,16 +1428,17 @@ default Mono> hGetEx(ByteBuffer key, Expiration expiration, Lis * {@literal HSETEX} {@link Command}. * * @author Viktoriya Kutsarova + * @since 4.0 * @see Redis Documentation: HSETEX */ class HSetExCommand extends KeyCommand { private final Map fieldValueMap; private final RedisHashCommands.HashFieldSetOption condition; - private final Expiration expiration; + private final @Nullable Expiration expiration; private HSetExCommand(@Nullable ByteBuffer key, Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + RedisHashCommands.HashFieldSetOption condition, @Nullable Expiration expiration) { super(key); this.fieldValueMap = fieldValueMap; this.condition = condition; @@ -1440,11 +1450,11 @@ private HSetExCommand(@Nullable ByteBuffer key, Map fiel * * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. * @param condition the condition for setting fields; must not be {@literal null}. - * @param expiration the expiration to apply; must not be {@literal null}. + * @param expiration the optional expiration to apply. * @return new instance of {@link HSetExCommand}. */ public static HSetExCommand setWithConditionAndExpiration(Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + RedisHashCommands.HashFieldSetOption condition, @Nullable Expiration expiration) { return new HSetExCommand(null, fieldValueMap, condition, expiration); } @@ -1476,7 +1486,7 @@ public RedisHashCommands.HashFieldSetOption getCondition() { /** * @return the expiration to apply. */ - public Expiration getExpiration() { + public @Nullable Expiration getExpiration() { return expiration; } } @@ -1487,17 +1497,16 @@ public Expiration getExpiration() { * @param key must not be {@literal null}. * @param fieldValueMap the field-value pairs to set; must not be {@literal null}. * @param condition the condition for setting fields; must not be {@literal null}. - * @param expiration the expiration to apply; must not be {@literal null}. + * @param expiration the optional expiration to apply * @return never {@literal null}. * @see Redis Documentation: HSETEX */ default Mono hSetEx(ByteBuffer key, Map fieldValueMap, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { Assert.notNull(key, "Key must not be null"); Assert.notNull(fieldValueMap, "Field-value map must not be null"); Assert.notNull(condition, "Condition must not be null"); - Assert.notNull(expiration, "Expiration must not be null"); return hSetEx(Mono.just(HSetExCommand.setWithConditionAndExpiration(fieldValueMap, condition, expiration) .from(key))) diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 8d8335ebad..46b97e3829 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -23,6 +23,8 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; @@ -562,12 +564,13 @@ default List hExpireAt(byte @NonNull [] key, long unixTime, byte @NonNull * time-to-live (TTL) for given {@code fields}. * * @param key must not be {@literal null}. + * @param expiration the optional expiration to apply. * @param fields must not be {@literal null}. * @return list of values for given {@code fields} or an empty {@link List} if key does not * exist or {@literal null} when used in pipeline / transaction. * @see Redis Documentation: HGETEX */ - List hGetEx(byte @NonNull [] key, Expiration expiration, + List hGetEx(byte @NonNull [] key, @Nullable Expiration expiration, byte @NonNull [] @NonNull ... fields); /** @@ -575,13 +578,13 @@ List hGetEx(byte @NonNull [] key, Expiration expiration, * * @param key must not be {@literal null}. * @param hashes the field-value pairs to set; must not be {@literal null}. - * @param hashFieldSetOption the optional condition for setting fields. + * @param hashFieldSetOption the condition for setting fields; must not be {@literal null}. * @param expiration the optional expiration to apply. * @return never {@literal null}. * @see Redis Documentation: HSETEX */ - Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption hashFieldSetOption, - Expiration expiration); + Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, + @NonNull HashFieldSetOption hashFieldSetOption, @Nullable Expiration expiration); /** * {@code HSETEX} command arguments for {@code FNX}, {@code FXX}. diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index f2ed6e7d1b..bf60a4f8cf 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -2599,8 +2599,8 @@ List hpExpireAt(@NonNull String key, long unixTimeInMillis, ExpirationOpti * @see Redis Documentation: HSETEX * @see RedisHashCommands#hSetEx(byte[], Map, HashFieldSetOption, Expiration) */ - Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, HashFieldSetOption condition, - Expiration expiration); + Boolean hSetEx(@NonNull String key, @NonNull Map<@NonNull String, String> hashes, + @NonNull HashFieldSetOption condition, @Nullable Expiration expiration); // ------------------------------------------------------------------------- // Methods dealing with HyperLogLog diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java index 582fb2a9cf..e37dab9cdc 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java @@ -15,7 +15,6 @@ */ package org.springframework.data.redis.connection.jedis; -import org.springframework.data.redis.core.types.Expiration; import redis.clients.jedis.args.ExpiryOption; import redis.clients.jedis.params.ScanParams; import redis.clients.jedis.resps.ScanResult; @@ -27,6 +26,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.ExpirationOptions; @@ -35,6 +35,7 @@ import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.Assert; /** @@ -415,44 +416,47 @@ public List hpTtl(byte[] key, byte[]... fields) { } } - @Override - public List hGetDel(byte[] key, byte[]... fields) { + @Override + public List hGetDel(byte[] key, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - try { - return connection.getCluster().hgetdel(key, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } + try { + return connection.getCluster().hgetdel(key, fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } - @Override - public List hGetEx(byte[] key, Expiration expiration, byte[]... fields) { + @Override + public List hGetEx(byte[] key, @Nullable Expiration expiration, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - try { - return connection.getCluster().hgetex(key, JedisConverters.toHGetExParams(expiration), fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } + try { + return connection.getCluster().hgetex(key, JedisConverters.toHGetExParams(expiration), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } - @Override - public Boolean hSetEx(byte[] key, Map hashes, HashFieldSetOption condition, Expiration expiration) { + @Override + public Boolean hSetEx(byte[] key, Map hashes, @NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashes, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashes, "Fields must not be null"); + Assert.notNull(condition, "Condition must not be null"); - try { - return JedisConverters.toBoolean(connection.getCluster().hsetex(key, JedisConverters.toHSetExParams(condition, expiration), hashes)); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } + try { + return JedisConverters.toBoolean( + connection.getCluster().hsetex(key, JedisConverters.toHSetExParams(condition, expiration), hashes)); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } @Nullable @Override diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index 1be5c57bab..0a2f933292 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -45,8 +45,8 @@ import java.util.Set; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Sort; import org.springframework.data.geo.Distance; @@ -401,105 +401,6 @@ static GetExParams toGetExParams(Expiration expiration, GetExParams params) { : params.ex(expiration.getConverted(TimeUnit.SECONDS)); } - /** - * Converts a given {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to the according - * {@code HSETEX} command argument. - *
- *
{@link RedisHashCommands.HashFieldSetOption#ifNoneExist()}
- *
{@code FNX}
- *
{@link RedisHashCommands.HashFieldSetOption#ifAllExist()}
- *
{@code FXX}
- *
{@link RedisHashCommands.HashFieldSetOption#upsert()}
- *
no condition flag
- *
- *
- *
{@link TimeUnit#MILLISECONDS}
- *
{@code PX|PXAT}
- *
{@link TimeUnit#SECONDS}
- *
{@code EX|EXAT}
- *
- * - * @param condition can be {@literal null}. - * @param expiration can be {@literal null}. - * @since 4.0 - */ - static HSetExParams toHSetExParams(RedisHashCommands.@Nullable HashFieldSetOption condition, @Nullable Expiration expiration) { - return toHSetExParams(condition, expiration, new HSetExParams()); - } - - static HSetExParams toHSetExParams(RedisHashCommands.@Nullable HashFieldSetOption condition, @Nullable Expiration expiration, HSetExParams params) { - - if (condition == null && expiration == null) { - return params; - } - - if (condition != null) { - if (condition.equals(RedisHashCommands.HashFieldSetOption.ifNoneExist())) { - params.fnx(); - } else if (condition.equals(RedisHashCommands.HashFieldSetOption.ifAllExist())) { - params.fxx(); - } - } - - if (expiration == null) { - return params; - } - - if (expiration.isKeepTtl()) { - return params.keepTtl(); - } - - if (expiration.isPersistent()) { - return params; - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - return expiration.isUnixTimestamp() ? params.pxAt(expiration.getExpirationTime()) - : params.px(expiration.getExpirationTime()); - } - - return expiration.isUnixTimestamp() ? params.exAt(expiration.getConverted(TimeUnit.SECONDS)) - : params.ex(expiration.getConverted(TimeUnit.SECONDS)); - } - - /** - * Converts a given {@link Expiration} to the according {@code HGETEX} command argument depending on - * {@link Expiration#isUnixTimestamp()}. - *
- *
{@link TimeUnit#MILLISECONDS}
- *
{@code PX|PXAT}
- *
{@link TimeUnit#SECONDS}
- *
{@code EX|EXAT}
- *
- * - * @param expiration must not be {@literal null}. - * @since 4.0 - */ - static HGetExParams toHGetExParams(Expiration expiration) { - return toHGetExParams(expiration, new HGetExParams()); - } - - static HGetExParams toHGetExParams(Expiration expiration, HGetExParams params) { - - if (expiration == null) { - return params; - } - - if (expiration.isPersistent()) { - return params.persist(); - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return params.pxAt(expiration.getExpirationTime()); - } - return params.px(expiration.getExpirationTime()); - } - - return expiration.isUnixTimestamp() ? params.exAt(expiration.getConverted(TimeUnit.SECONDS)) - : params.ex(expiration.getConverted(TimeUnit.SECONDS)); - } - /** * Converts a given {@link SetOption} to the according {@code SET} command argument.
*
@@ -543,6 +444,98 @@ public static SetParams toSetCommandNxXxArgument(SetOption option, SetParams par }; } + /** + * Converts a given {@link Expiration} to the according {@code HGETEX} command argument depending on + * {@link Expiration#isUnixTimestamp()}. + *
+ *
{@link TimeUnit#MILLISECONDS}
+ *
{@code PX|PXAT}
+ *
{@link TimeUnit#SECONDS}
+ *
{@code EX|EXAT}
+ *
+ * + * @param expiration can be {@literal null}. + * @since 4.0 + */ + static HGetExParams toHGetExParams(@Nullable Expiration expiration) { + + HGetExParams params = new HGetExParams(); + + if (expiration == null) { + return params; + } + + if (expiration.isPersistent()) { + return params.persist(); + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + if (expiration.isUnixTimestamp()) { + return params.pxAt(expiration.getExpirationTime()); + } + return params.px(expiration.getExpirationTime()); + } + + return expiration.isUnixTimestamp() ? + params.exAt(expiration.getConverted(TimeUnit.SECONDS)) : + params.ex(expiration.getConverted(TimeUnit.SECONDS)); + } + + /** + * Converts a given {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to the according + * {@code HSETEX} command argument. + *
+ *
{@link RedisHashCommands.HashFieldSetOption#ifNoneExist()}
+ *
{@code FNX}
+ *
{@link RedisHashCommands.HashFieldSetOption#ifAllExist()}
+ *
{@code FXX}
+ *
{@link RedisHashCommands.HashFieldSetOption#upsert()}
+ *
no condition flag
+ *
+ *
+ *
{@link TimeUnit#MILLISECONDS}
+ *
{@code PX|PXAT}
+ *
{@link TimeUnit#SECONDS}
+ *
{@code EX|EXAT}
+ *
+ * + * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. + * @param expiration can be {@literal null} to omit TTL. + * @return never {@literal null}. + * @since 4.0 + */ + static HSetExParams toHSetExParams(RedisHashCommands.@NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + + HSetExParams params = new HSetExParams(); + + switch (condition) { + case IF_NONE_EXIST -> params.fnx(); + case IF_ALL_EXIST -> params.fxx(); + } + ; + + if (expiration == null || expiration.isPersistent()) { + return params; + } + + if (expiration.isKeepTtl()) { + return params.keepTtl(); + } + + // PX | PXAT + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + return expiration.isUnixTimestamp() ? + params.pxAt(expiration.getExpirationTime()) : + params.px(expiration.getExpirationTime()); + } + + // EX | EXAT + return expiration.isUnixTimestamp() ? + params.exAt(expiration.getConverted(TimeUnit.SECONDS)) : + params.ex(expiration.getConverted(TimeUnit.SECONDS)); + } + private static byte[] boundaryToBytes(org.springframework.data.domain.Range.Bound boundary, byte[] inclPrefix, byte[] exclPrefix) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java index 11581a741d..449b3e4de1 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java @@ -333,35 +333,38 @@ protected void doClose() { return connection.invoke().just(Jedis::hpttl, PipelineBinaryCommands::hpttl, key, fields); } - @Override - public List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull... fields) { + @Override + public List hGetDel(byte @NonNull [] key, byte @NonNull [] @NonNull ... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().just(Jedis::hgetdel, PipelineBinaryCommands::hgetdel, key, fields); - } + return connection.invoke().just(Jedis::hgetdel, PipelineBinaryCommands::hgetdel, key, fields); + } - @Override - public List hGetEx(byte @NonNull [] key, Expiration expiration, byte @NonNull [] @NonNull... fields) { + @Override + public List hGetEx(byte @NonNull [] key, @Nullable Expiration expiration, + byte @NonNull [] @NonNull ... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().just(Jedis::hgetex, PipelineBinaryCommands::hgetex, key, JedisConverters.toHGetExParams(expiration), fields); - } + return connection.invoke() + .just(Jedis::hgetex, PipelineBinaryCommands::hgetex, key, JedisConverters.toHGetExParams(expiration), fields); + } - @Override - public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption condition, - Expiration expiration) { + @Override + public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, + @NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(condition, "Condition must not be null"); - return connection.invoke().from(Jedis::hsetex, PipelineBinaryCommands::hsetex, key, - JedisConverters.toHSetExParams(condition, expiration), hashes) - .get(Converters::toBoolean); - } + return connection.invoke() + .from(Jedis::hsetex, PipelineBinaryCommands::hsetex, key, JedisConverters.toHSetExParams(condition, expiration), + hashes).get(Converters::toBoolean); + } @Nullable @Override diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 29e064a808..5cea9c0b81 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; @@ -622,93 +623,90 @@ static GetExArgs toGetExArgs(@Nullable Expiration expiration) { : args.ex(expiration.getConverted(TimeUnit.SECONDS)); } - /** - * Convert {@link Expiration} to {@link HGetExArgs}. - * - * @param expiration can be {@literal null}. - * @since 4.0 - */ - static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { - - HGetExArgs args = new HGetExArgs(); - - if (expiration == null) { - return args; - } - - if (expiration.isPersistent()) { - return args.persist(); - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); - } - return args.px(Duration.ofMillis(expiration.getExpirationTime())); - } - - return expiration.isUnixTimestamp() ? args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) - : args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); - } - - /** - * Convert {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to {@link HSetExArgs} for the Redis {@code HSETEX} command. - * - *

Condition mapping:

- *
    - *
  • {@code IF_NONE_EXIST} {@code FNX}
  • - *
  • {@code IF_ALL_EXIST} {@code FXX}
  • - *
  • {@code UPSERT} no condition flag
  • - *
- * - *

Expiration mapping:

- *
    - *
  • {@link Expiration#keepTtl()} {@code KEEPTTL}
  • - *
  • Unix timestamp {@code EXAT}/{@code PXAT} depending on time unit
  • - *
  • Relative expiration {@code EX}/{@code PX} depending on time unit
  • - *
  • {@code null} expiration no TTL argument
  • - *
- * - * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. - * @param expiration can be {@literal null} to omit TTL. - * @return never {@literal null}. - * @since 4.0 - */ - static HSetExArgs toHSetExArgs(RedisHashCommands.HashFieldSetOption condition, @Nullable Expiration expiration) { - - HSetExArgs args = new HSetExArgs(); - - if (condition == null && expiration == null) { - return args; - } - - if (condition != null ) { - if (condition.equals(RedisHashCommands.HashFieldSetOption.ifNoneExist())) { - args.fnx(); - } - if (condition.equals(RedisHashCommands.HashFieldSetOption.ifAllExist())) { - args.fxx(); - } - } - - if (expiration == null) { - return args; - } - - if (expiration.isKeepTtl()) { - return args.keepttl(); - } - - if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); - } - return args.px(Duration.ofMillis(expiration.getExpirationTime())); - } - - return expiration.isUnixTimestamp() ? args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) - : args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); - } + /** + * Convert {@link Expiration} to {@link HGetExArgs}. + * + * @param expiration can be {@literal null}. + * @since 4.0 + */ + static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { + + HGetExArgs args = new HGetExArgs(); + + if (expiration == null) { + return args; + } + + if (expiration.isPersistent()) { + return args.persist(); + } + + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + if (expiration.isUnixTimestamp()) { + return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); + } + return args.px(Duration.ofMillis(expiration.getExpirationTime())); + } + + return expiration.isUnixTimestamp() ? + args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) : + args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); + } + + /** + * Convert {@link RedisHashCommands.HashFieldSetOption} and {@link Expiration} to {@link HSetExArgs} for the Redis + * {@code HSETEX} command. + *

Condition mapping:

+ *
    + *
  • {@code IF_NONE_EXIST} {@code FNX}
  • + *
  • {@code IF_ALL_EXIST} {@code FXX}
  • + *
  • {@code UPSERT} no condition flag
  • + *
+ *

Expiration mapping:

+ *
    + *
  • {@link Expiration#keepTtl()} {@code KEEPTTL}
  • + *
  • Unix timestamp {@code EXAT}/{@code PXAT} depending on time unit
  • + *
  • Relative expiration {@code EX}/{@code PX} depending on time unit
  • + *
  • {@code null} expiration no TTL argument
  • + *
+ * + * @param condition must not be {@literal null}; use {@code UPSERT} to omit FNX/FXX. + * @param expiration can be {@literal null} to omit TTL. + * @return never {@literal null}. + * @since 4.0 + */ + static HSetExArgs toHSetExArgs(RedisHashCommands.@NonNull HashFieldSetOption condition, + @Nullable Expiration expiration) { + + HSetExArgs args = new HSetExArgs(); + + switch (condition) { + case IF_NONE_EXIST -> args.fnx(); + case IF_ALL_EXIST -> args.fxx(); + } + ; + + if (expiration == null || expiration.isPersistent()) { + return args; + } + + if (expiration.isKeepTtl()) { + return args.keepttl(); + } + + // PX | PXAT + if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { + if (expiration.isUnixTimestamp()) { + return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); + } + return args.px(Duration.ofMillis(expiration.getExpirationTime())); + } + + // EX | EXAT + return expiration.isUnixTimestamp() ? + args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) : + args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); + } @SuppressWarnings("NullAway") static Converter, Long> toTimeConverter(TimeUnit timeUnit) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java index 7b71e76dd4..25164bdc2e 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java @@ -266,38 +266,39 @@ public List hpTtl(byte @NonNull [] key, byte @NonNull [] @NonNull... field return connection.invoke().fromMany(RedisHashAsyncCommands::hpttl, key, fields).toList(); } - @Override - public List hGetDel(byte @NonNull [] key, byte @NonNull []... fields) { + @Override + public List hGetDel(byte @NonNull [] key, byte @NonNull []... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().fromMany(RedisHashAsyncCommands::hgetdel, key, fields) - .toList(source -> source.getValueOrElse(null)); - } + return connection.invoke().fromMany(RedisHashAsyncCommands::hgetdel, key, fields) + .toList(source -> source.getValueOrElse(null)); + } - @Override - public List hGetEx(byte @NonNull [] key, Expiration expiration, byte @NonNull []... fields) { + @Override + public List hGetEx(byte @NonNull [] key, Expiration expiration, byte @NonNull []... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - return connection.invoke().fromMany(RedisHashAsyncCommands::hgetex, key, - LettuceConverters.toHGetExArgs(expiration), fields) - .toList(source -> source.getValueOrElse(null)); - } + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hgetex, key, LettuceConverters.toHGetExArgs(expiration), fields) + .toList(source -> source.getValueOrElse(null)); + } @Override - public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, HashFieldSetOption condition, - Expiration expiration) { + public Boolean hSetEx(byte @NonNull [] key, @NonNull Map hashes, + @NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(key, "Key must not be null"); + Assert.notNull(hashes, "Hashes must not be null"); + Assert.notNull(condition, "Condition must not be null"); - return connection.invoke().from(RedisHashAsyncCommands::hsetex, key, - LettuceConverters.toHSetExArgs(condition, expiration), hashes) - .get(LettuceConverters.longToBooleanConverter()); - } + return connection.invoke() + .from(RedisHashAsyncCommands::hsetex, key, LettuceConverters.toHSetExArgs(condition, expiration), + hashes).get(LettuceConverters.longToBooleanConverter()); + } /** * @param key diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index ae77ebbd84..712050ced7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -391,6 +391,7 @@ public Flux> hSetEx(Publisher comm Assert.notNull(command.getKey(), "Key must not be null"); Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null"); + Assert.notNull(command.getCondition(), "Condition must not be null"); Map entries = command.getFieldValueMap(); diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 701b169d78..560eacaf7c 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -23,6 +23,8 @@ import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.core.types.Expiration; @@ -269,14 +271,14 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * @since 4.0 * @see Redis Documentation: HSETEX */ - List getAndExpire(Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); + List getAndExpire(@Nullable Expiration expiration, @NonNull Collection<@NonNull HK> hashFields); /** * Set the value of one or more fields using data provided in {@code m} at the bound key, and optionally set their * expiration time or time-to-live (TTL). The {@code condition} determines whether the fields are set. * * @param m must not be {@literal null}. - * @param condition is optional. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if + * @param condition must not be {@literal null}. Use {@link RedisHashCommands.HashFieldSetOption#IF_NONE_EXIST} (FNX) to only set the fields if * none of them already exist, {@link RedisHashCommands.HashFieldSetOption#IF_ALL_EXIST} (FXX) to only set the * fields if all of them already exist, or {@link RedisHashCommands.HashFieldSetOption#UPSERT} to set the fields * unconditionally. @@ -284,5 +286,6 @@ default BoundHashFieldExpirationOperations hashExpiration(@NonNull Collectio * @since 4.0 * @see Redis Documentation: HSETEX */ - void putAndExpire(Map m, RedisHashCommands.HashFieldSetOption condition, Expiration expiration); + void putAndExpire(Map m, RedisHashCommands.@NonNull HashFieldSetOption condition, + @Nullable Expiration expiration); } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index dd7808273e..77c78c02e7 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -195,8 +195,8 @@ public List multiGet(@NonNull K key, @NonNull Collection<@NonNull HK> fields return deserializeHashValues(rawValues); } - @Override - public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fields) { + @Override + public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fields) { if (fields.isEmpty()) { return Collections.emptyList(); @@ -204,17 +204,17 @@ public List getAndDelete(@NonNull K key, @NonNull Collection<@NonNull HK> fi byte[] rawKey = rawKey(key); byte[][] rawHashKeys = new byte[fields.size()][]; - int counter = 0; + int counter = 0; for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } - List rawValues = execute(connection -> connection.hashCommands().hGetDel(rawKey, rawHashKeys)); + List rawValues = execute(connection -> connection.hashCommands().hGetDel(rawKey, rawHashKeys)); return deserializeHashValues(rawValues); } @Override - public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, + public List getAndExpire(@NonNull K key, @Nullable Expiration expiration, @NonNull Collection<@NonNull HK> fields) { if (fields.isEmpty()) { @@ -227,15 +227,14 @@ public List getAndExpire(@NonNull K key, @NonNull Expiration expiration, for (@NonNull HK hashKey : fields) { rawHashKeys[counter++] = rawHashKey(hashKey); } - List rawValues = execute(connection -> connection.hashCommands() - .hGetEx(rawKey, expiration, rawHashKeys)); + List rawValues = execute(connection -> connection.hashCommands().hGetEx(rawKey, expiration, rawHashKeys)); return deserializeHashValues(rawValues); } @Override public Boolean putAndExpire(@NonNull K key, @NonNull Map m, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { if (m.isEmpty()) { return false; } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java index 8847ed0222..4b3c8f1e58 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -15,7 +15,6 @@ */ package org.springframework.data.redis.core; -import org.springframework.data.redis.connection.RedisHashCommands; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,7 +27,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Function; - +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -36,6 +35,7 @@ import org.springframework.data.redis.connection.ReactiveHashCommands; import org.springframework.data.redis.connection.ReactiveHashCommands.HashExpireCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.Expirations; @@ -125,9 +125,11 @@ public Mono> getAndDelete(H key, Collection hashKeys) { } @Override - public Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, Expiration expiration) { + public Mono putAndExpire(H key, Map map, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration) { Assert.notNull(key, "Key must not be null"); Assert.notNull(map, "Map must not be null"); + Assert.notNull(condition, "Condition must not be null"); return createMono(hashCommands -> Flux.fromIterable(() -> map.entrySet().iterator()) // .collectMap(entry -> rawHashKey(entry.getKey()), entry -> rawHashValue(entry.getValue())) // @@ -135,7 +137,7 @@ public Mono putAndExpire(H key, Map map, Re } @Override - public Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys) { + public Mono> getAndExpire(H key, @Nullable Expiration expiration, Collection hashKeys) { Assert.notNull(key, "Key must not be null"); Assert.notNull(hashKeys, "Hash keys must not be null"); diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index e2c68f3fb1..43d925400a 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -104,21 +104,21 @@ public interface HashOperations { * @since 4.0 * @see Redis Documentation: HGETEX */ - List getAndExpire(@NonNull H key, Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); + List getAndExpire(@NonNull H key, @Nullable Expiration expiration, @NonNull Collection<@NonNull HK> hashKeys); /** * Set multiple hash fields to multiple values using data provided in {@code m} with optional condition and expiration. * * @param key must not be {@literal null}. * @param m must not be {@literal null}. - * @param condition is optional. + * @param condition must not be {@literal}. * @param expiration is optional. * @return whether all fields were set or {@literal null} when used in pipeline / transaction. * @since 4.0 *@see Redis Documentation: HSETEX */ Boolean putAndExpire(@NonNull H key, @NonNull Map m, - RedisHashCommands.HashFieldSetOption condition, Expiration expiration); + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration); /** * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java index 12130e048c..2c27b04352 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import org.jspecify.annotations.NonNull; import org.springframework.data.redis.connection.RedisHashCommands; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -101,13 +102,13 @@ public interface ReactiveHashOperations { * * @param key must not be {@literal null}. * @param map must not be {@literal null}. - * @param condition is optional. + * @param condition must not be {@literal null}. * @param expiration is optional. * @return never {@literal null}. * @since 4.0 */ - Mono putAndExpire(H key, Map map, RedisHashCommands.HashFieldSetOption condition, - Expiration expiration); + Mono putAndExpire(H key, Map map, + RedisHashCommands.@NonNull HashFieldSetOption condition, @Nullable Expiration expiration); /** * Get and optionally expire the value for given {@code hashKeys} from hash at {@code key}. Values are in the order of the @@ -119,7 +120,7 @@ Mono putAndExpire(H key, Map map, RedisHash * @return never {@literal null}. * @since 4.0 */ - Mono> getAndExpire(H key, Expiration expiration, Collection hashKeys); + Mono> getAndExpire(H key, @Nullable Expiration expiration, Collection hashKeys); /** * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index 40eafd76c3..19468b6f41 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -26,6 +26,8 @@ import redis.clients.jedis.Protocol; import redis.clients.jedis.params.GetExParams; +import redis.clients.jedis.params.HGetExParams; +import redis.clients.jedis.params.HSetExParams; import redis.clients.jedis.params.SetParams; import java.util.Arrays; @@ -35,11 +37,11 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; - import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; @@ -405,4 +407,117 @@ private Map getRedisServerInfoMap(String name, int port) { map.put("parallel-syncs", "1"); return map; } + + /** + * Unit tests for {@link JedisConverters#toHGetExParams}. + */ + @Nested + class HGetExParamsTests { // GH-3211 + + @Test + void convertsNullExpirationToDefaultHGetEx() { + + HGetExParams params = JedisConverters.toHGetExParams(null); + assertThat(params).isEqualTo(new HGetExParams()); + } + + @Test + void convertsPersistentExpirationToNonExpiringHGetEx() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.persistent()); + assertThat(params).isEqualTo(new HGetExParams().persist()); + } + + @Test + void convertsExpirationWithMillisTimeUnitToHGetExPX() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30_000, TimeUnit.MILLISECONDS)); + assertThat(params).isEqualTo(new HGetExParams().px(30_000)); + } + + @Test + void convertsExpirationWithMillisUnixTsToHGetExPXAT() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS)); + assertThat(params).isEqualTo(new HGetExParams().pxAt(10_000)); + } + + @Test + void convertsExpirationWithNonMillisTimeUnitToHGetExEX() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30, TimeUnit.SECONDS)); + assertThat(params).isEqualTo(new HGetExParams().ex(30)); + } + + @Test + void convertsExpirationWithNonMillisUnixTsToHGetExEXAT() { + + HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(10, TimeUnit.SECONDS)); + assertThat(params).isEqualTo(new HGetExParams().exAt(10)); + } + } + + /** + * Unit tests for {@link JedisConverters#toHSetExParams}. + */ + @Nested + class HSetExArgsTests { // GH-3211 + + @Test + void convertsNullExpirationAndUpsertConditionToDefaultHSetEx() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThat(params).isEqualTo(new HSetExParams()); + } + + @Test + void convertsNoneExistConditionToHSetExFNX() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.ifNoneExist(), null); + assertThat(params).isEqualTo(new HSetExParams().fnx()); + } + + @Test + void convertsAllExistConditionToHSetExFXX() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.ifAllExist(), null); + assertThat(params).isEqualTo(new HSetExParams().fxx()); + } + + @Test + void convertsExpirationWithTtlToHSetExWithKeepTtl() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.keepTtl()); + assertThat(params).isEqualTo(new HSetExParams().keepttl()); + } + + @Test + void convertsExpirationWithMillisTimeUnitToHSetExPX() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.from(30_000, TimeUnit.MILLISECONDS)); + assertThat(params).isEqualTo(new HSetExParams().px(30_000)); + } + + @Test + void convertsExpirationWithMillisUnixTsToHSetExPXAT() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS)); + assertThat(params).isEqualTo(new HSetExParams().pxAt(10_000)); + } + + @Test + void convertsExpirationWithNonMillisTimeUnitToHSetExEX() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.from(30, TimeUnit.SECONDS)); + assertThat(params).isEqualTo(new HSetExParams().ex(30)); + } + + @Test + void convertsExpirationWithNonMillisUnixTsToHSetExEXAT() { + + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.unixTimestamp(10, TimeUnit.SECONDS)); + assertThat(params).isEqualTo(new HSetExParams().exAt(10)); + } + + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index 74cb969dc6..4dcea051a7 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -21,22 +21,28 @@ import static org.springframework.test.util.ReflectionTestUtils.*; import io.lettuce.core.GetExArgs; +import io.lettuce.core.HGetExArgs; +import io.lettuce.core.HSetExArgs; import io.lettuce.core.Limit; import io.lettuce.core.RedisURI; import io.lettuce.core.SetArgs; import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisHashCommands; import org.springframework.data.redis.connection.RedisPassword; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; @@ -329,4 +335,120 @@ void sentinelConfigurationShouldNotSetSentinelAuthIfUsernameIsPresentWithNoPassw assertThat(sentinel.getPassword()).isNull(); }); } + + + /** + * Unit tests for {@link LettuceConverters#toHGetExArgs}. + */ + @Nested + class HGetExArgsTests { // GH-3211 + + @Test + void convertsNullExpirationToDefaultHGetEx() { + + assertThatCommandArgument(LettuceConverters.toHGetExArgs(null)) + .isEqualTo(new HGetExArgs()); + } + + @Test + void convertsPersistentExpirationToNonExpiringHGetEx() { + + assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.persistent())) + .isEqualTo(new HGetExArgs().persist()); + } + + @Test + void convertsExpirationWithMillisTimeUnitToHGetExPX() { + + assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.from(30_000, TimeUnit.MILLISECONDS))) + .isEqualTo(new HGetExArgs().px(Duration.ofMillis(30_000))); + } + + @Test + void convertsExpirationWithMillisUnixTsToHGetExPXAT() { + + assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS))) + .isEqualTo(new HGetExArgs().pxAt(Instant.ofEpochSecond(10_000))); + } + + @Test + void convertsExpirationWithNonMillisTimeUnitToHGetExEX() { + + assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.from(30, TimeUnit.SECONDS))) + .isEqualTo(new HGetExArgs().ex(Duration.ofMillis(30_000))); + } + + @Test + void convertsExpirationWithNonMillisUnixTsToHGetExEXAT() { + + assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(10, TimeUnit.SECONDS))) + .isEqualTo(new HGetExArgs().exAt(Instant.ofEpochSecond(10))); + } + } + + /** + * Unit tests for {@link LettuceConverters#toHSetExArgs}. + */ + @Nested + class HSetExArgsTests { // GH-3211 + + @Test + void convertsNullExpirationAndUpsertConditionToDefaultHSetEx() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)) + .isEqualTo(new HSetExArgs()); + } + + @Test + void convertsNoneExistConditionToHSetExFNX() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs( + RedisHashCommands.HashFieldSetOption.ifNoneExist(), null)) + .isEqualTo(new HSetExArgs().fnx()); + } + + @Test + void convertsAllExistConditionToHSetExFXX() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs( + RedisHashCommands.HashFieldSetOption.ifAllExist(), null)) + .isEqualTo(new HSetExArgs().fxx()); + + } + + @Test + void convertsExpirationWithTtlToHSetExWithKeepTtl() { + assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl())) + .isEqualTo(new HSetExArgs().keepttl()); + + } + + @Test + void convertsExpirationWithMillisTimeUnitToHSetExPX() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30_000, TimeUnit.MILLISECONDS))) + .isEqualTo(new HSetExArgs().px(Duration.ofMillis(30_000))); + } + + @Test + void convertsExpirationWithMillisUnixTsToHSetExPXAT() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS))) + .isEqualTo(new HSetExArgs().pxAt(Instant.ofEpochSecond(10_000))); + } + + @Test + void convertsExpirationWithNonMillisTimeUnitToHSetExEX() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30, TimeUnit.SECONDS))) + .isEqualTo(new HSetExArgs().ex(Duration.ofMillis(30_000))); + } + + @Test + void convertsExpirationWithNonMillisUnixTsToHSetExEXAT() { + + assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.unixTimestamp(10, TimeUnit.SECONDS))) + .isEqualTo(new HGetExArgs().exAt(Instant.ofEpochSecond(10))); + } + } } From cdb4b9345b707890817c971d5cfd52733c2103f5 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sat, 11 Oct 2025 23:02:15 -0500 Subject: [PATCH 09/12] Fix conversion bug in converters and make unit tests consistent. Signed-off-by: Chris Bono --- .../connection/jedis/JedisConverters.java | 16 +- .../connection/lettuce/LettuceConverters.java | 23 +-- .../jedis/JedisConvertersUnitTests.java | 185 ++++++++++-------- .../lettuce/LettuceConvertersUnitTests.java | 172 ++++++++-------- 4 files changed, 207 insertions(+), 189 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index 0a2f933292..249802553c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -470,15 +470,14 @@ static HGetExParams toHGetExParams(@Nullable Expiration expiration) { } if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return params.pxAt(expiration.getExpirationTime()); - } - return params.px(expiration.getExpirationTime()); + return expiration.isUnixTimestamp() ? + params.pxAt(expiration.getExpirationTime()) : + params.px(expiration.getExpirationTime()); } return expiration.isUnixTimestamp() ? - params.exAt(expiration.getConverted(TimeUnit.SECONDS)) : - params.ex(expiration.getConverted(TimeUnit.SECONDS)); + params.exAt(expiration.getExpirationTimeInSeconds()) : + params.ex(expiration.getExpirationTimeInSeconds()); } /** @@ -513,7 +512,6 @@ static HSetExParams toHSetExParams(RedisHashCommands.@NonNull HashFieldSetOption case IF_NONE_EXIST -> params.fnx(); case IF_ALL_EXIST -> params.fxx(); } - ; if (expiration == null || expiration.isPersistent()) { return params; @@ -532,8 +530,8 @@ static HSetExParams toHSetExParams(RedisHashCommands.@NonNull HashFieldSetOption // EX | EXAT return expiration.isUnixTimestamp() ? - params.exAt(expiration.getConverted(TimeUnit.SECONDS)) : - params.ex(expiration.getConverted(TimeUnit.SECONDS)); + params.exAt(expiration.getExpirationTimeInSeconds()) : + params.ex(expiration.getExpirationTimeInSeconds()); } private static byte[] boundaryToBytes(org.springframework.data.domain.Range.Bound boundary, byte[] inclPrefix, diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 5cea9c0b81..aee9852a18 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -642,15 +642,14 @@ static HGetExArgs toHGetExArgs(@Nullable Expiration expiration) { } if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); - } - return args.px(Duration.ofMillis(expiration.getExpirationTime())); + return expiration.isUnixTimestamp() ? + args.pxAt(Instant.ofEpochMilli(expiration.getExpirationTime())) : + args.px(Duration.ofMillis(expiration.getExpirationTime())); } return expiration.isUnixTimestamp() ? - args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) : - args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); + args.exAt(Instant.ofEpochSecond(expiration.getExpirationTimeInSeconds())) : + args.ex(Duration.ofSeconds(expiration.getExpirationTimeInSeconds())); } /** @@ -684,7 +683,6 @@ static HSetExArgs toHSetExArgs(RedisHashCommands.@NonNull HashFieldSetOption con case IF_NONE_EXIST -> args.fnx(); case IF_ALL_EXIST -> args.fxx(); } - ; if (expiration == null || expiration.isPersistent()) { return args; @@ -696,16 +694,15 @@ static HSetExArgs toHSetExArgs(RedisHashCommands.@NonNull HashFieldSetOption con // PX | PXAT if (expiration.getTimeUnit() == TimeUnit.MILLISECONDS) { - if (expiration.isUnixTimestamp()) { - return args.pxAt(Instant.ofEpochSecond(expiration.getExpirationTime())); - } - return args.px(Duration.ofMillis(expiration.getExpirationTime())); + return expiration.isUnixTimestamp() ? + args.pxAt(Instant.ofEpochMilli(expiration.getExpirationTime())) : + args.px(Duration.ofMillis(expiration.getExpirationTime())); } // EX | EXAT return expiration.isUnixTimestamp() ? - args.exAt(Instant.ofEpochSecond(expiration.getConverted(TimeUnit.SECONDS))) : - args.ex(Duration.ofSeconds(expiration.getConverted(TimeUnit.SECONDS))); + args.exAt(Instant.ofEpochSecond(expiration.getExpirationTimeInSeconds())) : + args.ex(Duration.ofSeconds(expiration.getExpirationTimeInSeconds())); } @SuppressWarnings("NullAway") diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index 19468b6f41..a288f1dc43 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -30,6 +30,8 @@ import redis.clients.jedis.params.HSetExParams; import redis.clients.jedis.params.SetParams; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.Date; @@ -38,7 +40,6 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.RedisHashCommands; @@ -408,116 +409,138 @@ private Map getRedisServerInfoMap(String name, int port) { return map; } - /** - * Unit tests for {@link JedisConverters#toHGetExParams}. - */ - @Nested - class HGetExParamsTests { // GH-3211 + @Test // GH-3211 + void toHGetExArgsShouldNotSetAnyFieldsForNullExpiration() { - @Test - void convertsNullExpirationToDefaultHGetEx() { + assertThatParamsHasExpiration(JedisConverters.toHGetExParams(null), + null, null); + } - HGetExParams params = JedisConverters.toHGetExParams(null); - assertThat(params).isEqualTo(new HGetExParams()); - } + @Test // GH-3211 + void toHGetExArgsShouldSetPersistForNonExpiringExpiration() { - @Test - void convertsPersistentExpirationToNonExpiringHGetEx() { + assertThatParamsHasExpiration(JedisConverters.toHGetExParams(Expiration.persistent()), + Protocol.Keyword.PERSIST, null); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.persistent()); - assertThat(params).isEqualTo(new HGetExParams().persist()); - } + @Test // GH-3211 + void toHGetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { - @Test - void convertsExpirationWithMillisTimeUnitToHGetExPX() { + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30_000, TimeUnit.MILLISECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.PX, 30_000L); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30_000, TimeUnit.MILLISECONDS)); - assertThat(params).isEqualTo(new HGetExParams().px(30_000)); - } + @Test // GH-3211 + void toHGetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { - @Test - void convertsExpirationWithMillisUnixTsToHGetExPXAT() { + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.PXAT, fourHoursFromNowMillis); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS)); - assertThat(params).isEqualTo(new HGetExParams().pxAt(10_000)); - } + @Test // GH-3211 + void toHGetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { - @Test - void convertsExpirationWithNonMillisTimeUnitToHGetExEX() { + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30, TimeUnit.SECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.EX, 30L); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30, TimeUnit.SECONDS)); - assertThat(params).isEqualTo(new HGetExParams().ex(30)); - } + @Test // GH-3211 + void toHGetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { - @Test - void convertsExpirationWithNonMillisUnixTsToHGetExEXAT() { + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.EXAT, fourHoursFromNowSecs); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(10, TimeUnit.SECONDS)); - assertThat(params).isEqualTo(new HGetExParams().exAt(10)); - } + private void assertThatParamsHasExpiration(HGetExParams params, Protocol.Keyword expirationType, Long expirationValue) { + assertThat(params).extracting("expiration", "expirationValue") + .containsExactly(expirationType, expirationValue); } - /** - * Unit tests for {@link JedisConverters#toHSetExParams}. - */ - @Nested - class HSetExArgsTests { // GH-3211 + @Test // GH-3211 + void toHSetExArgsShouldSetFnxForNoneExistCondition() { - @Test - void convertsNullExpirationAndUpsertConditionToDefaultHSetEx() { + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null); + assertThatParamsHasExistance(params, Protocol.Keyword.FNX); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); - assertThat(params).isEqualTo(new HSetExParams()); - } + @Test // GH-3211 + void toHSetExArgsShouldSetFxxForAllExistCondition() { - @Test - void convertsNoneExistConditionToHSetExFNX() { + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null); + assertThatParamsHasExistance(params, Protocol.Keyword.FXX); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.ifNoneExist(), null); - assertThat(params).isEqualTo(new HSetExParams().fnx()); - } + @Test // GH-3211 + void toHSetExArgsShouldNotSetFnxNorFxxForUpsertCondition() { - @Test - void convertsAllExistConditionToHSetExFXX() { + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThatParamsHasExistance(params, null); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.ifAllExist(), null); - assertThat(params).isEqualTo(new HSetExParams().fxx()); - } + @Test // GH-3211 + void toHSetExArgsShouldNotSetAnyTimeFieldsForNullExpiration() { - @Test - void convertsExpirationWithTtlToHSetExWithKeepTtl() { + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThatParamsHasExpiration(params, null, null); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.keepTtl()); - assertThat(params).isEqualTo(new HSetExParams().keepttl()); - } + @Test // GH-3211 + void toHSetExArgsShouldNotSetAnyTimeFieldsForNonExpiringExpiration() { - @Test - void convertsExpirationWithMillisTimeUnitToHSetExPX() { + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.persistent()); + assertThatParamsHasExpiration(params, null, null); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.from(30_000, TimeUnit.MILLISECONDS)); - assertThat(params).isEqualTo(new HSetExParams().px(30_000)); - } + @Test // GH-3211 + void toHSetExArgsShouldSetKeepTtlForKeepTtlExpiration() { - @Test - void convertsExpirationWithMillisUnixTsToHSetExPXAT() { + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl()); + assertThatParamsHasExpiration(params, Protocol.Keyword.KEEPTTL, null); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS)); - assertThat(params).isEqualTo(new HSetExParams().pxAt(10_000)); - } + @Test // GH-3211 + void toHSetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { - @Test - void convertsExpirationWithNonMillisTimeUnitToHSetExEX() { + Expiration expiration = Expiration.from(30_000, TimeUnit.MILLISECONDS); + assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.PX, 30_000L); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.from(30, TimeUnit.SECONDS)); - assertThat(params).isEqualTo(new HSetExParams().ex(30)); - } + @Test // GH-3211 + void toHSetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { - @Test - void convertsExpirationWithNonMillisUnixTsToHSetExEXAT() { + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); + assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.PXAT, fourHoursFromNowMillis); + } - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.upsert(), Expiration.unixTimestamp(10, TimeUnit.SECONDS)); - assertThat(params).isEqualTo(new HSetExParams().exAt(10)); - } + @Test // GH-3211 + void toHSetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { + + Expiration expiration = Expiration.from(30, TimeUnit.SECONDS); + assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.EX, 30L); + } + + @Test // GH-3211 + void toHSetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); + assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.EXAT, fourHoursFromNowSecs); } + + private void assertThatParamsHasExistance(HSetExParams params, Protocol.Keyword existance) { + assertThat(params).extracting("existance").isEqualTo(existance); + } + + private void assertThatParamsHasExpiration(HSetExParams params, Protocol.Keyword expirationType, Long expirationValue) { + assertThat(params).extracting("expiration", "expirationValue") + .containsExactly(expirationType, expirationValue); + } + } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index 4dcea051a7..d47f409994 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -21,23 +21,20 @@ import static org.springframework.test.util.ReflectionTestUtils.*; import io.lettuce.core.GetExArgs; -import io.lettuce.core.HGetExArgs; -import io.lettuce.core.HSetExArgs; import io.lettuce.core.Limit; import io.lettuce.core.RedisURI; import io.lettuce.core.SetArgs; import io.lettuce.core.cluster.models.partitions.Partitions; import io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag; -import java.time.Duration; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; @@ -336,119 +333,122 @@ void sentinelConfigurationShouldNotSetSentinelAuthIfUsernameIsPresentWithNoPassw }); } + @Test // GH-3211 + void toHGetExArgsShouldNotSetAnyFieldsForNullExpiration() { - /** - * Unit tests for {@link LettuceConverters#toHGetExArgs}. - */ - @Nested - class HGetExArgsTests { // GH-3211 + assertThat(LettuceConverters.toHGetExArgs(null)) + .extracting("ex", "exAt", "px", "pxAt", "persist") + .containsExactly(null, null, null, null, Boolean.FALSE); + } - @Test - void convertsNullExpirationToDefaultHGetEx() { + @Test // GH-3211 + void toHGetExArgsShouldSetPersistForNonExpiringExpiration() { - assertThatCommandArgument(LettuceConverters.toHGetExArgs(null)) - .isEqualTo(new HGetExArgs()); - } + assertThat(LettuceConverters.toHGetExArgs(Expiration.persistent())) + .extracting("persist").isEqualTo(Boolean.TRUE); + } - @Test - void convertsPersistentExpirationToNonExpiringHGetEx() { + @Test // GH-3211 + void toHGetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { - assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.persistent())) - .isEqualTo(new HGetExArgs().persist()); - } + assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30_000, TimeUnit.MILLISECONDS))) + .extracting("px").isEqualTo(30_000L); + } - @Test - void convertsExpirationWithMillisTimeUnitToHGetExPX() { + @Test // GH-3211 + void toHGetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { - assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.from(30_000, TimeUnit.MILLISECONDS))) - .isEqualTo(new HGetExArgs().px(Duration.ofMillis(30_000))); - } + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + assertThat(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS))) + .extracting("pxAt").isEqualTo(fourHoursFromNowMillis); + } - @Test - void convertsExpirationWithMillisUnixTsToHGetExPXAT() { + @Test // GH-3211 + void toHGetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { - assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS))) - .isEqualTo(new HGetExArgs().pxAt(Instant.ofEpochSecond(10_000))); - } + assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30, TimeUnit.SECONDS))) + .extracting("ex").isEqualTo(30L); + } - @Test - void convertsExpirationWithNonMillisTimeUnitToHGetExEX() { + @Test // GH-3211 + void toHGetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { - assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.from(30, TimeUnit.SECONDS))) - .isEqualTo(new HGetExArgs().ex(Duration.ofMillis(30_000))); - } + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + assertThat(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS))) + .extracting("exAt").isEqualTo(fourHoursFromNowSecs); + } - @Test - void convertsExpirationWithNonMillisUnixTsToHGetExEXAT() { + @Test // GH-3211 + void toHSetExArgsShouldSetFnxForNoneExistCondition() { - assertThatCommandArgument(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(10, TimeUnit.SECONDS))) - .isEqualTo(new HGetExArgs().exAt(Instant.ofEpochSecond(10))); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null)) + .extracting("fnx").isEqualTo(Boolean.TRUE); } - /** - * Unit tests for {@link LettuceConverters#toHSetExArgs}. - */ - @Nested - class HSetExArgsTests { // GH-3211 + @Test // GH-3211 + void toHSetExArgsShouldSetFxxForAllExistCondition() { - @Test - void convertsNullExpirationAndUpsertConditionToDefaultHSetEx() { + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null)) + .extracting("fxx").isEqualTo(Boolean.TRUE); + } - assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)) - .isEqualTo(new HSetExArgs()); - } + @Test // GH-3211 + void toHSetExArgsShouldNotSetFnxNorFxxForUpsertCondition() { - @Test - void convertsNoneExistConditionToHSetExFNX() { + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)) + .extracting("fnx", "fxx").containsExactly(Boolean.FALSE, Boolean.FALSE); + } - assertThatCommandArgument(LettuceConverters.toHSetExArgs( - RedisHashCommands.HashFieldSetOption.ifNoneExist(), null)) - .isEqualTo(new HSetExArgs().fnx()); - } + @Test // GH-3211 + void toHSetExArgsShouldNotSetAnyTimeFieldsForNullExpiration() { - @Test - void convertsAllExistConditionToHSetExFXX() { + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)) + .extracting("ex", "exAt", "px", "pxAt").containsExactly(null, null, null, null); + } - assertThatCommandArgument(LettuceConverters.toHSetExArgs( - RedisHashCommands.HashFieldSetOption.ifAllExist(), null)) - .isEqualTo(new HSetExArgs().fxx()); + @Test // GH-3211 + void toHSetExArgsShouldNotSetAnyTimeFieldsForNonExpiringExpiration() { - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.persistent())) + .extracting("ex", "exAt", "px", "pxAt").containsExactly(null, null, null, null); + } - @Test - void convertsExpirationWithTtlToHSetExWithKeepTtl() { - assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl())) - .isEqualTo(new HSetExArgs().keepttl()); + @Test // GH-3211 + void toHSetExArgsShouldSetKeepTtlForKeepTtlExpiration() { - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl())) + .extracting("keepttl").isEqualTo(Boolean.TRUE); + } - @Test - void convertsExpirationWithMillisTimeUnitToHSetExPX() { + @Test // GH-3211 + void toHSetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { - assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30_000, TimeUnit.MILLISECONDS))) - .isEqualTo(new HSetExArgs().px(Duration.ofMillis(30_000))); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30_000, TimeUnit.MILLISECONDS))) + .extracting("px").isEqualTo(30_000L); + } - @Test - void convertsExpirationWithMillisUnixTsToHSetExPXAT() { + @Test // GH-3211 + void toHSetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { - assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.unixTimestamp(10_000, TimeUnit.MILLISECONDS))) - .isEqualTo(new HSetExArgs().pxAt(Instant.ofEpochSecond(10_000))); - } + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)) + .extracting("pxAt").isEqualTo(fourHoursFromNowMillis); + } - @Test - void convertsExpirationWithNonMillisTimeUnitToHSetExEX() { + @Test // GH-3211 + void toHSetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { - assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30, TimeUnit.SECONDS))) - .isEqualTo(new HSetExArgs().ex(Duration.ofMillis(30_000))); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30, TimeUnit.SECONDS))) + .extracting("ex").isEqualTo(30L); + } - @Test - void convertsExpirationWithNonMillisUnixTsToHSetExEXAT() { + @Test // GH-3211 + void toHSetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { - assertThatCommandArgument(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.unixTimestamp(10, TimeUnit.SECONDS))) - .isEqualTo(new HGetExArgs().exAt(Instant.ofEpochSecond(10))); - } + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)) + .extracting("exAt").isEqualTo(fourHoursFromNowSecs); } } From 48c621320bc60065c7d694b995dce6480448d6b0 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sun, 12 Oct 2025 15:01:32 -0500 Subject: [PATCH 10/12] Add tests for new ops to DefaultStringRedisConnectionTests Adds unit tests to verify that the underlying connection API gets invoked as expected for the newly added HGETDEL, HGETEX, and HSETEX methods. Signed-off-by: Chris Bono --- .../DefaultStringRedisConnectionTests.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java index 5e9c1b38e6..0c14f0d90c 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java @@ -29,6 +29,7 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -61,6 +62,7 @@ import org.springframework.data.redis.connection.zset.DefaultTuple; import org.springframework.data.redis.connection.zset.Tuple; import org.springframework.data.redis.connection.zset.Weights; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.serializer.StringRedisSerializer; /** @@ -541,6 +543,66 @@ public void testHVals() { verifyResults(Collections.singletonList(stringList)); } + @Test // GH-3211 + public void hGetDelBytes() { + doReturn(Collections.singletonList(barBytes)).when(nativeConnection).hGetDel(fooBytes, barBytes); + List deleted = connection.hGetDel(fooBytes, barBytes); + assertThat(deleted).containsExactly(barBytes); + } + + @Test // GH-3211 + public void hGetDel() { + doReturn(Collections.singletonList(barBytes)).when(nativeConnection).hGetDel(fooBytes, barBytes); + List deleted = connection.hGetDel(foo, bar); + assertThat(deleted).containsExactly(bar); + } + + @Test // GH-3211 + public void hGetExBytes() { + Expiration expiration = mock(); + doReturn(bytesList).when(nativeConnection).hGetEx(fooBytes, expiration, barBytes); + List values = connection.hGetEx(fooBytes, expiration, barBytes); + assertThat(values).containsExactly(barBytes); + } + + @Test // GH-3211 + public void hGetEx() { + Expiration expiration = mock(); + doReturn(bytesList).when(nativeConnection).hGetEx(fooBytes, expiration, barBytes); + List values = connection.hGetEx(foo, expiration, bar); + assertThat(values).containsExactly(bar); + } + + @Test // GH-3211 + public void hSetExBytes() { + Expiration expiration = mock(); + RedisHashCommands.HashFieldSetOption setOption = mock(); + Map fieldMap = Map.of(barBytes, bar2Bytes); + doReturn(Boolean.TRUE).when(nativeConnection).hSetEx(fooBytes, fieldMap, setOption, expiration); + assertThat(connection.hSetEx(fooBytes, fieldMap, setOption, expiration)).isTrue(); + } + + @Test // GH-3211 + public void hSetEx() { + Expiration expiration = mock(); + RedisHashCommands.HashFieldSetOption setOption = mock(); + Map stringMap = Map.of(bar, bar2); + doReturn(Boolean.TRUE).when(nativeConnection).hSetEx( + eq(fooBytes), + argThat(fieldMap -> isFieldMap(fieldMap, stringMap)), + eq(setOption), eq(expiration)); + assertThat(connection.hSetEx(foo, stringMap, setOption, expiration)).isTrue(); + } + + private boolean isFieldMap(Map fieldMap, Map stringMap) { + Map fieldMapAsStringMap = fieldMap.entrySet().stream() + .collect(Collectors.toMap( + entry -> new String(entry.getKey()), + entry -> new String(entry.getValue()) + )); + return fieldMapAsStringMap.equals(stringMap); + } + @Test public void testIncrBytes() { doReturn(2L).when(nativeConnection).incr(fooBytes); From c64d52981d5ddfee1ec86d70b17c0634b092b124 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sun, 12 Oct 2025 15:34:36 -0500 Subject: [PATCH 11/12] Simplify AbstractConnectionIntegrationTests for HGETDEL/HGETEX/HSETEX Signed-off-by: Chris Bono --- .../AbstractConnectionIntegrationTests.java | 214 ++++-------------- 1 file changed, 41 insertions(+), 173 deletions(-) diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index 056f443a93..272ceefca2 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -3727,156 +3727,68 @@ public void hTtlReturnsMinusTwoWhenFieldOrKeyMissing() { @Test // GH-3211 @EnabledOnCommand("HGETDEL") - public void hGetDelReturnsValueAndDeletesField() { + public void hGetDelWorksAsExpected() { - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetdel", "field-2", "value-2")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - actual.add(connection.hExists("hash-hgetdel", "field-2")); + connection.hSet("hash-hgetdel", "field-1", "value-1"); + connection.hSet("hash-hgetdel", "field-2", "value-2"); + connection.hSet("hash-hgetdel", "field-3", "value-3"); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, List.of("value-1"), Boolean.FALSE, Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelReturnsNullWhenFieldDoesNotExist() { - - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hGetDel("hash-hgetdel", "missing-field")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - - verifyResults(Arrays.asList(Boolean.TRUE, Arrays.asList((Object) null), Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelReturnsNullWhenKeyDoesNotExist() { - - actual.add(connection.hGetDel("missing-hash", "field-1")); - - verifyResults(Arrays.asList(Arrays.asList((Object) null))); - } + // hgetdel first 2 fields + assertThat(connection.hGetDel("hash-hgetdel", "field-1", "field-2")) + .containsExactly("value-1", "value-2"); + assertThat(connection.hExists("hash-hgetdel", "field-1")).isFalse(); + assertThat(connection.hExists("hash-hgetdel", "field-2")).isFalse(); - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelMultipleFieldsReturnsValuesAndDeletesFields() { + // hgetdel non-existent field returns null + assertThat(connection.hGetDel("hash-hgetdel", "field-1")) + .containsExactly(null); - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetdel", "field-2", "value-2")); - actual.add(connection.hSet("hash-hgetdel", "field-3", "value-3")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1", "field-2")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - actual.add(connection.hExists("hash-hgetdel", "field-2")); - actual.add(connection.hExists("hash-hgetdel", "field-3")); + // hgetdel last field + assertThat(connection.hGetDel("hash-hgetdel", "field-3")) + .containsExactly("value-3"); + assertThat(connection.hExists("hash-hgetdel", "field-3")).isFalse(); + assertThat(connection.exists("hash-hgetdel")).isFalse(); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, - Arrays.asList("value-1", "value-2"), - Boolean.FALSE, Boolean.FALSE, Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelMultipleFieldsWithNonExistentFields() { - - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1", "missing-field")); - actual.add(connection.hExists("hash-hgetdel", "field-1")); - - verifyResults(Arrays.asList(Boolean.TRUE, - Arrays.asList("value-1", null), - Boolean.FALSE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETDEL") - public void hGetDelDeletesKeyWhenAllFieldsRemoved() { - - actual.add(connection.hSet("hash-hgetdel", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetdel", "field-2", "value-2")); - actual.add(connection.hGetDel("hash-hgetdel", "field-1", "field-2")); - actual.add(connection.exists("hash-hgetdel")); - - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, - Arrays.asList("value-1", "value-2"), - Boolean.FALSE)); + // hgetdel non-existent hash returns null + assertThat(connection.hGetDel("hash-hgetdel", "field-1")) + .containsExactly(null); } @Test // GH-3211 @EnabledOnCommand("HGETEX") - public void hGetExReturnsValueAndSetsExpiration() { - - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetex", "field-2", "value-2")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(60), "field-1")); - actual.add(connection.hExists("hash-hgetex", "field-1")); - actual.add(connection.hExists("hash-hgetex", "field-2")); - - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, List.of("value-1"), Boolean.TRUE, Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExReturnsNullWhenFieldDoesNotExist() { - - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(60), "missing-field")); - actual.add(connection.hExists("hash-hgetex", "field-1")); - - verifyResults(Arrays.asList(Boolean.TRUE, Arrays.asList((Object) null), Boolean.TRUE)); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExReturnsNullWhenKeyDoesNotExist() { - - actual.add(connection.hGetEx("missing-hash", Expiration.seconds(60), "field-1")); - - verifyResults(Arrays.asList(Arrays.asList((Object) null))); - } - - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExMultipleFieldsReturnsValuesAndSetsExpiration() { + @LongRunningTest + public void hGetExWorksAsExpected() { - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hSet("hash-hgetex", "field-2", "value-2")); - actual.add(connection.hSet("hash-hgetex", "field-3", "value-3")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(120), "field-1", "field-2")); - actual.add(connection.hExists("hash-hgetex", "field-1")); - actual.add(connection.hExists("hash-hgetex", "field-2")); - actual.add(connection.hExists("hash-hgetex", "field-3")); + connection.hSet("hash-hgetex", "field-1", "value-1"); + connection.hSet("hash-hgetex", "field-2", "value-2"); + connection.hSet("hash-hgetex", "field-3", "value-3"); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, - Arrays.asList("value-1", "value-2"), - Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)); - } + assertThat(connection.hGetEx("hash-hgetex", Expiration.seconds(2), "field-1", "field-2")) + .containsExactly("value-1", "value-2"); - @Test // GH-3211 - @EnabledOnCommand("HGETEX") - public void hGetExMultipleFieldsWithNonExistentFields() { + // non-existent field returns null + assertThat(connection.hGetEx("hash-hgetex", null, "no-such-field")).containsExactly(null); - actual.add(connection.hSet("hash-hgetex", "field-1", "value-1")); - actual.add(connection.hGetEx("hash-hgetex", Expiration.seconds(60), "field-1", "missing-field")); - actual.add(connection.hExists("hash-hgetex", "field-1")); + // non-existent hash returns null + assertThat(connection.hGetEx("no-such-key", null, "field-1")).containsExactly(null); - verifyResults(Arrays.asList(Boolean.TRUE, - Arrays.asList("value-1", null), - Boolean.TRUE)); + await().atMost(Duration.ofMillis(3000L)).until(() -> + !connection.hExists("hash-getex", "field-1") && !connection.hExists("hash-getex", "field-2")); } @Test // GH-3211 @EnabledOnCommand("HSETEX") - public void hSetExUpsertConditionSetsFieldsWithExpiration() { + @LongRunningTest + public void hSetExWorksAsExpected() { Map fieldMap = Map.of("field-1", "value-1", "field-2", "value-2"); - actual.add(connection.hSetEx("hash-hsetex", fieldMap, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.seconds(60))); - actual.add(connection.hExists("hash-hsetex", "field-1")); - actual.add(connection.hExists("hash-hsetex", "field-2")); - actual.add(connection.hGet("hash-hsetex", "field-1")); - actual.add(connection.hGet("hash-hsetex", "field-2")); + assertThat(connection.hSetEx("hash-hsetex", fieldMap, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.seconds(2))) + .isTrue(); + assertThat(connection.hGet("hash-hsetex", "field-1")).isEqualTo("value-1"); + assertThat(connection.hGet("hash-hsetex", "field-2")).isEqualTo("value-2"); - verifyResults(Arrays.asList(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE, "value-1", "value-2")); + await().atMost(Duration.ofMillis(3000L)).until(() -> + !connection.hExists("hash-getex", "field-1") && !connection.hExists("hash-getex", "field-2")); } @Test // GH-3211 @@ -3933,50 +3845,6 @@ public void hSetExIfAllExistConditionFailsWhenSomeFieldsMissing() { verifyResults(Arrays.asList(Boolean.TRUE, Boolean.FALSE, "existing-value", Boolean.FALSE)); } - @Test // GH-3211 - @EnabledOnCommand("HSETEX") - public void hSetExWithDifferentExpirationPolicies() { - - // Test with seconds expiration - Map fieldMap1 = Map.of("field-1", "value-1"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap1, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.seconds(60))); - actual.add(connection.hExists("hash-hsetex-exp", "field-1")); - actual.add(connection.hGet("hash-hsetex-exp", "field-1")); - - // Test with milliseconds expiration - Map fieldMap2 = Map.of("field-2", "value-2"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap2, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.milliseconds(120000))); - actual.add(connection.hExists("hash-hsetex-exp", "field-2")); - actual.add(connection.hGet("hash-hsetex-exp", "field-2")); - - // Test with Duration expiration - Map fieldMap3 = Map.of("field-3", "value-3"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap3, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.from(Duration.ofMinutes(3)))); - actual.add(connection.hExists("hash-hsetex-exp", "field-3")); - actual.add(connection.hGet("hash-hsetex-exp", "field-3")); - - // Test with unix timestamp expiration (5 minutes from now) - long futureTimestamp = System.currentTimeMillis() / 1000 + 300; // 5 minutes from now - Map fieldMap4 = Map.of("field-4", "value-4"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap4, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.unixTimestamp(futureTimestamp, TimeUnit.SECONDS))); - actual.add(connection.hExists("hash-hsetex-exp", "field-4")); - actual.add(connection.hGet("hash-hsetex-exp", "field-4")); - - // Test with keepTtl expiration - Map fieldMap5 = Map.of("field-5", "value-5"); - actual.add(connection.hSetEx("hash-hsetex-exp", fieldMap5, RedisHashCommands.HashFieldSetOption.upsert(), Expiration.keepTtl())); - actual.add(connection.hExists("hash-hsetex-exp", "field-5")); - actual.add(connection.hGet("hash-hsetex-exp", "field-5")); - - verifyResults(Arrays.asList( - Boolean.TRUE, Boolean.TRUE, "value-1", // seconds - Boolean.TRUE, Boolean.TRUE, "value-2", // milliseconds - Boolean.TRUE, Boolean.TRUE, "value-3", // Duration - Boolean.TRUE, Boolean.TRUE, "value-4", // unix timestamp - Boolean.TRUE, Boolean.TRUE, "value-5" // keepTtl - )); - } - @Test // DATAREDIS-694 void touchReturnsNrOfKeysTouched() { From e35b336e728e7106018781db5faf7073dbcae6e9 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Sun, 12 Oct 2025 16:01:28 -0500 Subject: [PATCH 12/12] Use `@Nested` for converter unit tests Shows the simplification added when using `@Nested` for a group of API unit tests. Signed-off-by: Chris Bono --- .../jedis/JedisConvertersUnitTests.java | 211 ++++++++++-------- .../lettuce/LettuceConvertersUnitTests.java | 179 ++++++++------- 2 files changed, 207 insertions(+), 183 deletions(-) diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index a288f1dc43..02c86c79f2 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -40,6 +40,7 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.RedisHashCommands; @@ -409,138 +410,152 @@ private Map getRedisServerInfoMap(String name, int port) { return map; } - @Test // GH-3211 - void toHGetExArgsShouldNotSetAnyFieldsForNullExpiration() { + @Nested + class ToHGetExParamsShould { - assertThatParamsHasExpiration(JedisConverters.toHGetExParams(null), - null, null); - } + @Test + void notSetAnyFieldsForNullExpiration() { - @Test // GH-3211 - void toHGetExArgsShouldSetPersistForNonExpiringExpiration() { + assertThatParamsHasExpiration(JedisConverters.toHGetExParams(null), null, null); + } - assertThatParamsHasExpiration(JedisConverters.toHGetExParams(Expiration.persistent()), - Protocol.Keyword.PERSIST, null); - } + @Test + void setPersistForNonExpiringExpiration() { - @Test // GH-3211 - void toHGetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { + assertThatParamsHasExpiration(JedisConverters.toHGetExParams(Expiration.persistent()), Protocol.Keyword.PERSIST, + null); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30_000, TimeUnit.MILLISECONDS)); - assertThatParamsHasExpiration(params, Protocol.Keyword.PX, 30_000L); - } + @Test + void setPxForExpirationWithMillisTimeUnit() { - @Test // GH-3211 - void toHGetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30_000, TimeUnit.MILLISECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.PX, 30_000L); + } - long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); - HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS)); - assertThatParamsHasExpiration(params, Protocol.Keyword.PXAT, fourHoursFromNowMillis); - } + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { - @Test // GH-3211 - void toHGetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + HGetExParams params = JedisConverters.toHGetExParams( + Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.PXAT, fourHoursFromNowMillis); + } - HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30, TimeUnit.SECONDS)); - assertThatParamsHasExpiration(params, Protocol.Keyword.EX, 30L); - } + @Test + void setExForExpirationWithNonMillisTimeUnit() { - @Test // GH-3211 - void toHGetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { + HGetExParams params = JedisConverters.toHGetExParams(Expiration.from(30, TimeUnit.SECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.EX, 30L); + } - long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); - HGetExParams params = JedisConverters.toHGetExParams(Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS)); - assertThatParamsHasExpiration(params, Protocol.Keyword.EXAT, fourHoursFromNowSecs); - } + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { + + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + HGetExParams params = JedisConverters.toHGetExParams( + Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS)); + assertThatParamsHasExpiration(params, Protocol.Keyword.EXAT, fourHoursFromNowSecs); + } - private void assertThatParamsHasExpiration(HGetExParams params, Protocol.Keyword expirationType, Long expirationValue) { - assertThat(params).extracting("expiration", "expirationValue") - .containsExactly(expirationType, expirationValue); + private void assertThatParamsHasExpiration(HGetExParams params, Protocol.Keyword expirationType, + Long expirationValue) { + assertThat(params).extracting("expiration", "expirationValue").containsExactly(expirationType, expirationValue); + } } + + @Nested + class ToHSetExParamsShould { - @Test // GH-3211 - void toHSetExArgsShouldSetFnxForNoneExistCondition() { + @Test + void setFnxForNoneExistCondition() { - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null); - assertThatParamsHasExistance(params, Protocol.Keyword.FNX); - } + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null); + assertThatParamsHasExistance(params, Protocol.Keyword.FNX); + } - @Test // GH-3211 - void toHSetExArgsShouldSetFxxForAllExistCondition() { + @Test + void setFxxForAllExistCondition() { - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null); - assertThatParamsHasExistance(params, Protocol.Keyword.FXX); - } + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null); + assertThatParamsHasExistance(params, Protocol.Keyword.FXX); + } - @Test // GH-3211 - void toHSetExArgsShouldNotSetFnxNorFxxForUpsertCondition() { + @Test + void notSetFnxNorFxxForUpsertCondition() { - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); - assertThatParamsHasExistance(params, null); - } + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThatParamsHasExistance(params, null); + } - @Test // GH-3211 - void toHSetExArgsShouldNotSetAnyTimeFieldsForNullExpiration() { + @Test + void notSetAnyTimeFieldsForNullExpiration() { - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); - assertThatParamsHasExpiration(params, null, null); - } + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, null); + assertThatParamsHasExpiration(params, null, null); + } - @Test // GH-3211 - void toHSetExArgsShouldNotSetAnyTimeFieldsForNonExpiringExpiration() { + @Test + void notSetAnyTimeFieldsForNonExpiringExpiration() { - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.persistent()); - assertThatParamsHasExpiration(params, null, null); - } + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.persistent()); + assertThatParamsHasExpiration(params, null, null); + } - @Test // GH-3211 - void toHSetExArgsShouldSetKeepTtlForKeepTtlExpiration() { + @Test + void setKeepTtlForKeepTtlExpiration() { - HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl()); - assertThatParamsHasExpiration(params, Protocol.Keyword.KEEPTTL, null); - } + HSetExParams params = JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.keepTtl()); + assertThatParamsHasExpiration(params, Protocol.Keyword.KEEPTTL, null); + } - @Test // GH-3211 - void toHSetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { + @Test + void setPxForExpirationWithMillisTimeUnit() { - Expiration expiration = Expiration.from(30_000, TimeUnit.MILLISECONDS); - assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), - Protocol.Keyword.PX, 30_000L); - } + Expiration expiration = Expiration.from(30_000, TimeUnit.MILLISECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), Protocol.Keyword.PX, + 30_000L); + } - @Test // GH-3211 - void toHSetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { - long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); - Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); - assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), - Protocol.Keyword.PXAT, fourHoursFromNowMillis); - } + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.PXAT, fourHoursFromNowMillis); + } - @Test // GH-3211 - void toHSetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { + @Test + void setExForExpirationWithNonMillisTimeUnit() { - Expiration expiration = Expiration.from(30, TimeUnit.SECONDS); - assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), - Protocol.Keyword.EX, 30L); - } + Expiration expiration = Expiration.from(30, TimeUnit.SECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), Protocol.Keyword.EX, + 30L); + } - @Test // GH-3211 - void toHSetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { - long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); - Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); - assertThatParamsHasExpiration(JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), - Protocol.Keyword.EXAT, fourHoursFromNowSecs); - } + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); + assertThatParamsHasExpiration( + JedisConverters.toHSetExParams(RedisHashCommands.HashFieldSetOption.UPSERT, expiration), + Protocol.Keyword.EXAT, fourHoursFromNowSecs); + } - private void assertThatParamsHasExistance(HSetExParams params, Protocol.Keyword existance) { - assertThat(params).extracting("existance").isEqualTo(existance); - } + private void assertThatParamsHasExistance(HSetExParams params, Protocol.Keyword existance) { + assertThat(params).extracting("existance").isEqualTo(existance); + } - private void assertThatParamsHasExpiration(HSetExParams params, Protocol.Keyword expirationType, Long expirationValue) { - assertThat(params).extracting("expiration", "expirationValue") - .containsExactly(expirationType, expirationValue); + private void assertThatParamsHasExpiration(HSetExParams params, Protocol.Keyword expirationType, + Long expirationValue) { + assertThat(params).extracting("expiration", "expirationValue").containsExactly(expirationType, expirationValue); + } } - } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index d47f409994..f7826377db 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; @@ -333,122 +334,130 @@ void sentinelConfigurationShouldNotSetSentinelAuthIfUsernameIsPresentWithNoPassw }); } - @Test // GH-3211 - void toHGetExArgsShouldNotSetAnyFieldsForNullExpiration() { + @Nested // GH-3211 + class ToHGetExArgsShould { - assertThat(LettuceConverters.toHGetExArgs(null)) - .extracting("ex", "exAt", "px", "pxAt", "persist") - .containsExactly(null, null, null, null, Boolean.FALSE); - } + @Test + void notSetAnyFieldsForNullExpiration() { - @Test // GH-3211 - void toHGetExArgsShouldSetPersistForNonExpiringExpiration() { + assertThat(LettuceConverters.toHGetExArgs(null)).extracting("ex", "exAt", "px", "pxAt", "persist") + .containsExactly(null, null, null, null, Boolean.FALSE); + } - assertThat(LettuceConverters.toHGetExArgs(Expiration.persistent())) - .extracting("persist").isEqualTo(Boolean.TRUE); - } + @Test + void setPersistForNonExpiringExpiration() { - @Test // GH-3211 - void toHGetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { + assertThat(LettuceConverters.toHGetExArgs(Expiration.persistent())).extracting("persist").isEqualTo(Boolean.TRUE); + } - assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30_000, TimeUnit.MILLISECONDS))) - .extracting("px").isEqualTo(30_000L); - } + @Test + void setPxForExpirationWithMillisTimeUnit() { - @Test // GH-3211 - void toHGetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { + assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30_000, TimeUnit.MILLISECONDS))).extracting("px") + .isEqualTo(30_000L); + } - long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); - assertThat(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS))) - .extracting("pxAt").isEqualTo(fourHoursFromNowMillis); - } + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { - @Test // GH-3211 - void toHGetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + assertThat(LettuceConverters.toHGetExArgs( + Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS))).extracting("pxAt") + .isEqualTo(fourHoursFromNowMillis); + } - assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30, TimeUnit.SECONDS))) - .extracting("ex").isEqualTo(30L); - } + @Test + void setExForExpirationWithNonMillisTimeUnit() { + + assertThat(LettuceConverters.toHGetExArgs(Expiration.from(30, TimeUnit.SECONDS))).extracting("ex").isEqualTo(30L); + } - @Test // GH-3211 - void toHGetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { - long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); - assertThat(LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS))) - .extracting("exAt").isEqualTo(fourHoursFromNowSecs); + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + assertThat( + LettuceConverters.toHGetExArgs(Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS))).extracting( + "exAt").isEqualTo(fourHoursFromNowSecs); + } } + + @Nested + class ToHSetExArgsShould { - @Test // GH-3211 - void toHSetExArgsShouldSetFnxForNoneExistCondition() { + @Test + void setFnxForNoneExistCondition() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null)) - .extracting("fnx").isEqualTo(Boolean.TRUE); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_NONE_EXIST, null)).extracting( + "fnx").isEqualTo(Boolean.TRUE); + } - @Test // GH-3211 - void toHSetExArgsShouldSetFxxForAllExistCondition() { + @Test + void setFxxForAllExistCondition() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null)) - .extracting("fxx").isEqualTo(Boolean.TRUE); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.IF_ALL_EXIST, null)).extracting( + "fxx").isEqualTo(Boolean.TRUE); + } - @Test // GH-3211 - void toHSetExArgsShouldNotSetFnxNorFxxForUpsertCondition() { + @Test + void notSetFnxNorFxxForUpsertCondition() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)) - .extracting("fnx", "fxx").containsExactly(Boolean.FALSE, Boolean.FALSE); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)).extracting("fnx", + "fxx").containsExactly(Boolean.FALSE, Boolean.FALSE); + } - @Test // GH-3211 - void toHSetExArgsShouldNotSetAnyTimeFieldsForNullExpiration() { + @Test + void notSetAnyTimeFieldsForNullExpiration() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)) - .extracting("ex", "exAt", "px", "pxAt").containsExactly(null, null, null, null); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, null)).extracting("ex", + "exAt", "px", "pxAt").containsExactly(null, null, null, null); + } - @Test // GH-3211 - void toHSetExArgsShouldNotSetAnyTimeFieldsForNonExpiringExpiration() { + @Test + void notSetAnyTimeFieldsForNonExpiringExpiration() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.persistent())) - .extracting("ex", "exAt", "px", "pxAt").containsExactly(null, null, null, null); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.persistent())).extracting("ex", "exAt", "px", "pxAt").containsExactly(null, null, null, null); + } - @Test // GH-3211 - void toHSetExArgsShouldSetKeepTtlForKeepTtlExpiration() { + @Test + void setKeepTtlForKeepTtlExpiration() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl())) - .extracting("keepttl").isEqualTo(Boolean.TRUE); - } + assertThat( + LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.keepTtl())).extracting( + "keepttl").isEqualTo(Boolean.TRUE); + } - @Test // GH-3211 - void toHSetExArgsShouldSetPxForExpirationWithMillisTimeUnit() { + @Test + void setPxForExpirationWithMillisTimeUnit() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30_000, TimeUnit.MILLISECONDS))) - .extracting("px").isEqualTo(30_000L); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.from(30_000, TimeUnit.MILLISECONDS))).extracting("px").isEqualTo(30_000L); + } - @Test // GH-3211 - void toHSetExArgsShouldSetPxAtForExpirationWithMillisUnixTimestamp() { + @Test + void setPxAtForExpirationWithMillisUnixTimestamp() { - long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); - Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)) - .extracting("pxAt").isEqualTo(fourHoursFromNowMillis); - } + long fourHoursFromNowMillis = Instant.now().plus(4L, ChronoUnit.HOURS).toEpochMilli(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowMillis, TimeUnit.MILLISECONDS); + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)).extracting( + "pxAt").isEqualTo(fourHoursFromNowMillis); + } - @Test // GH-3211 - void toHSetExArgsShouldSetExForExpirationWithNonMillisTimeUnit() { + @Test + void setExForExpirationWithNonMillisTimeUnit() { - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, Expiration.from(30, TimeUnit.SECONDS))) - .extracting("ex").isEqualTo(30L); - } + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, + Expiration.from(30, TimeUnit.SECONDS))).extracting("ex").isEqualTo(30L); + } - @Test // GH-3211 - void toHSetExArgsShouldSetExAtForExpirationWithNonMillisUnixTimestamp() { + @Test + void setExAtForExpirationWithNonMillisUnixTimestamp() { - long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); - Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); - assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)) - .extracting("exAt").isEqualTo(fourHoursFromNowSecs); + long fourHoursFromNowSecs = Instant.now().plus(4L, ChronoUnit.HOURS).getEpochSecond(); + Expiration expiration = Expiration.unixTimestamp(fourHoursFromNowSecs, TimeUnit.SECONDS); + assertThat(LettuceConverters.toHSetExArgs(RedisHashCommands.HashFieldSetOption.UPSERT, expiration)).extracting( + "exAt").isEqualTo(fourHoursFromNowSecs); + } } }