diff --git a/glide-core/src/protobuf/redis_request.proto b/glide-core/src/protobuf/redis_request.proto index 886ab27d21..002902ab6c 100644 --- a/glide-core/src/protobuf/redis_request.proto +++ b/glide-core/src/protobuf/redis_request.proto @@ -202,6 +202,8 @@ enum RequestType { BLMove = 169; GetDel = 170; SRandMember = 171; + BitField = 172; + BitFieldReadOnly = 173; SInterCard = 175; } diff --git a/glide-core/src/request_type.rs b/glide-core/src/request_type.rs index eb2292098f..99173cc8c6 100644 --- a/glide-core/src/request_type.rs +++ b/glide-core/src/request_type.rs @@ -172,6 +172,8 @@ pub enum RequestType { BLMove = 169, GetDel = 170, SRandMember = 171, + BitField = 172, + BitFieldReadOnly = 173, SInterCard = 175, } @@ -348,6 +350,8 @@ impl From<::protobuf::EnumOrUnknown> for RequestType { ProtobufRequestType::BLMove => RequestType::BLMove, ProtobufRequestType::GetDel => RequestType::GetDel, ProtobufRequestType::SRandMember => RequestType::SRandMember, + ProtobufRequestType::BitField => RequestType::BitField, + ProtobufRequestType::BitFieldReadOnly => RequestType::BitFieldReadOnly, ProtobufRequestType::SInterCard => RequestType::SInterCard, } } @@ -520,6 +524,8 @@ impl RequestType { RequestType::BLMove => Some(cmd("BLMOVE")), RequestType::GetDel => Some(cmd("GETDEL")), RequestType::SRandMember => Some(cmd("SRANDMEMBER")), + RequestType::BitField => Some(cmd("BITFIELD")), + RequestType::BitFieldReadOnly => Some(cmd("BITFIELD_RO")), RequestType::SInterCard => Some(cmd("SINTERCARD")), } } diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index f5fac1da76..fbf897eb28 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -1,6 +1,9 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.ffi.resolvers.SocketListenerResolver.getSocket; import static glide.utils.ArrayTransformUtils.castArray; import static glide.utils.ArrayTransformUtils.castArrayofArrays; @@ -18,6 +21,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitField; +import static redis_request.RedisRequestOuterClass.RequestType.BitFieldReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.Decr; @@ -1637,4 +1642,22 @@ public CompletableFuture srandmember(@NonNull String key, long count) return commandManager.submitNewCommand( SRandMember, arguments, response -> castArray(handleArrayResponse(response), String.class)); } + + @Override + public CompletableFuture bitfield( + @NonNull String key, @NonNull BitFieldSubCommands[] subCommands) { + String[] arguments = ArrayUtils.addFirst(createBitFieldArgs(subCommands), key); + return commandManager.submitNewCommand( + BitField, arguments, response -> castArray(handleArrayResponse(response), Long.class)); + } + + @Override + public CompletableFuture bitfieldReadOnly( + @NonNull String key, @NonNull BitFieldReadOnlySubCommands[] subCommands) { + String[] arguments = ArrayUtils.addFirst(createBitFieldArgs(subCommands), key); + return commandManager.submitNewCommand( + BitFieldReadOnly, + arguments, + response -> castArray(handleArrayResponse(response), Long.class)); + } } diff --git a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java index 0b15ad79f3..a22ad2ab3f 100644 --- a/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/BitmapBaseCommands.java @@ -1,6 +1,15 @@ /** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.api.commands; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; + +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import java.util.concurrent.CompletableFuture; @@ -237,4 +246,69 @@ CompletableFuture bitpos( */ CompletableFuture bitop( BitwiseOperation bitwiseOperation, String destination, String[] keys); + + /** + * Reads or modifies the array of bits representing the string that is held at key + * based on the specified subCommands. + * + * @see redis.io for details. + * @param key The key of the string. + * @param subCommands The subCommands to be performed on the binary value of the string at + * key, which could be any of the following: + *
    + *
  • {@link BitFieldGet}. + *
  • {@link BitFieldSet}. + *
  • {@link BitFieldIncrby}. + *
  • {@link BitFieldOverflow}. + *
+ * + * @return An array of results from the executed subcommands. + *
    + *
  • {@link BitFieldGet} returns the value in {@link Offset} or {@link OffsetMultiplier}. + *
  • {@link BitFieldSet} returns the old value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldIncrby} returns the new value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldOverflow} determines the behaviour of SET and + * INCRBY when an overflow occurs. OVERFLOW does not return a value + * and does not contribute a value to the array response. + *
+ * + * @example + *
{@code
+     * client.set("sampleKey", "A"); // "A" has binary value 01000001
+     * BitFieldSubCommands[] subcommands = new BitFieldSubCommands[] {
+     *      new BitFieldSet(new UnsignedEncoding(2), new Offset(1), 3), // Sets the new binary value to 01100001
+     *      new BitFieldGet(new UnsignedEncoding(2), new Offset(1)) // Gets value from 0(11)00001
+     * };
+     * Long[] payload = client.bitfield("sampleKey", subcommands).get();
+     * assertArrayEquals(payload, new Long[] {2L, 3L});
+     * }
+ */ + CompletableFuture bitfield(String key, BitFieldSubCommands[] subCommands); + + /** + * Reads the array of bits representing the string that is held at key based on the + * specified subCommands. + * + * @since Redis 6.0 and above + * @see redis.io for details. + * @param key The key of the string. + * @param subCommands The GET subCommands to be performed. + * @return An array of results from the GET subcommands. + * @example + *
{@code
+     * client.set("sampleKey", "A"); // "A" has binary value 01000001
+     * Long[] payload =
+     *      client.
+     *          bitfieldReadOnly(
+     *              "sampleKey",
+     *              new BitFieldReadOnlySubCommands[] {
+     *                  new BitFieldGet(new UnsignedEncoding(2), new Offset(1))
+     *              })
+     *          .get();
+     * assertArrayEquals(payload, new Long[] {2L}); // Value is from 0(10)00001
+     * }
+ */ + CompletableFuture bitfieldReadOnly(String key, BitFieldReadOnlySubCommands[] subCommands); } diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 35ca15389f..8aa25bceb8 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -9,6 +9,7 @@ import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API; import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API; import static glide.api.models.commands.RangeOptions.createZRangeArgs; +import static glide.api.models.commands.bitmap.BitFieldOptions.createBitFieldArgs; import static glide.api.models.commands.function.FunctionLoadOptions.REPLACE; import static glide.utils.ArrayTransformUtils.concatenateArrays; import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray; @@ -23,6 +24,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitField; +import static redis_request.RedisRequestOuterClass.RequestType.BitFieldReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; @@ -185,6 +188,14 @@ import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; @@ -3971,6 +3982,56 @@ public T srandmember(@NonNull String key, long count) { return getThis(); } + /** + * Reads or modifies the array of bits representing the string that is held at key + * based on the specified subCommands. + * + * @see redis.io for details. + * @param key The key of the string. + * @param subCommands The subCommands to be performed on the binary value of the string at + * key, which could be any of the following: + *
    + *
  • {@link BitFieldGet}. + *
  • {@link BitFieldSet}. + *
  • {@link BitFieldIncrby}. + *
  • {@link BitFieldOverflow}. + *
+ * + * @return Command Response - An array of results from the executed subcommands. + *
    + *
  • {@link BitFieldGet} returns the value in {@link Offset} or {@link OffsetMultiplier}. + *
  • {@link BitFieldSet} returns the old value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldIncrby} returns the new value in {@link Offset} or {@link + * OffsetMultiplier}. + *
  • {@link BitFieldOverflow} determines the behaviour of SET and + * INCRBY when an overflow occurs. OVERFLOW does not return a value + * and does not contribute a value to the array response. + *
+ */ + public T bitfield(@NonNull String key, @NonNull BitFieldSubCommands[] subCommands) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(createBitFieldArgs(subCommands), key)); + protobufTransaction.addCommands(buildCommand(BitField, commandArgs)); + return getThis(); + } + + /** + * Reads the array of bits representing the string that is held at key based on the + * specified subCommands. + * + * @since Redis 6.0 and above + * @see redis.io for details. + * @param key The key of the string. + * @param subCommands The GET subCommands to be performed. + * @return Command Response - An array of results from the GET subcommands. + */ + public T bitfieldReadOnly( + @NonNull String key, @NonNull BitFieldReadOnlySubCommands[] subCommands) { + ArgsArray commandArgs = buildArgs(ArrayUtils.addFirst(createBitFieldArgs(subCommands), key)); + protobufTransaction.addCommands(buildCommand(BitFieldReadOnly, commandArgs)); + return getThis(); + } + /** Build protobuf {@link Command} object for given command and arguments. */ protected Command buildCommand(RequestType requestType) { return buildCommand(requestType, buildArgs()); diff --git a/java/client/src/main/java/glide/api/models/commands/bitmap/BitFieldOptions.java b/java/client/src/main/java/glide/api/models/commands/bitmap/BitFieldOptions.java new file mode 100644 index 0000000000..743ef17b15 --- /dev/null +++ b/java/client/src/main/java/glide/api/models/commands/bitmap/BitFieldOptions.java @@ -0,0 +1,235 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.models.commands.bitmap; + +import static glide.utils.ArrayTransformUtils.concatenateArrays; + +import glide.api.commands.BitmapBaseCommands; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Subcommand arguments for {@link BitmapBaseCommands#bitfield(String, BitFieldSubCommands[])} and + * {@link BitmapBaseCommands#bitfieldReadOnly(String, BitFieldReadOnlySubCommands[])}. Specifies + * subcommands, bit-encoding type, and offset type. + * + * @see redis.io and redis.io + */ +public class BitFieldOptions { + /** GET subcommand string to include in the BITFIELD command. */ + public static final String GET_COMMAND_STRING = "GET"; + + /** SET subcommand string to include in the BITFIELD command. */ + public static final String SET_COMMAND_STRING = "SET"; + + /** INCRBY subcommand string to include in the BITFIELD command. */ + public static final String INCRBY_COMMAND_STRING = "INCRBY"; + + /** OVERFLOW subcommand string to include in the BITFIELD command. */ + public static final String OVERFLOW_COMMAND_STRING = "OVERFLOW"; + + /** Prefix specifying that the encoding is unsigned. */ + public static final String UNSIGNED_ENCODING_PREFIX = "u"; + + /** Prefix specifying that the encoding is signed. */ + public static final String SIGNED_ENCODING_PREFIX = "i"; + + /** Prefix specifying that the offset uses an encoding multiplier. */ + public static final String OFFSET_MULTIPLIER_PREFIX = "#"; + + /** Subcommands for bitfield and bitfield_ro. */ + public interface BitFieldSubCommands { + /** + * Creates the subcommand arguments. + * + * @return a String array with subcommands and their arguments. + */ + String[] toArgs(); + } + + /** Subcommands for bitfieldReadOnly. */ + public interface BitFieldReadOnlySubCommands extends BitFieldSubCommands {} + + /** + * GET subcommand for getting the value in the binary representation of the string + * stored in key based on {@link BitEncoding} and {@link Offset}. + */ + @RequiredArgsConstructor + public static final class BitFieldGet implements BitFieldReadOnlySubCommands { + /** Specifies if the bit encoding is signed or unsigned. */ + private final BitEncoding encoding; + + /** Specifies if the offset uses an encoding multiplier. */ + private final BitOffset offset; + + public String[] toArgs() { + return new String[] {GET_COMMAND_STRING, encoding.getEncoding(), offset.getOffset()}; + } + } + + /** + * SET subcommand for setting the bits in the binary representation of the string + * stored in key based on {@link BitEncoding} and {@link Offset}. + */ + @RequiredArgsConstructor + public static final class BitFieldSet implements BitFieldSubCommands { + /** Specifies if the bit encoding is signed or unsigned. */ + private final BitEncoding encoding; + + /** Specifies if the offset uses an encoding multiplier. */ + private final BitOffset offset; + + /** Value to set the bits in the binary value to. */ + private final long value; + + public String[] toArgs() { + return new String[] { + SET_COMMAND_STRING, encoding.getEncoding(), offset.getOffset(), Long.toString(value) + }; + } + } + + /** + * INCRBY subcommand for increasing or decreasing the bits in the binary + * representation of the string stored in key based on {@link BitEncoding} and {@link + * Offset}. + */ + @RequiredArgsConstructor + public static final class BitFieldIncrby implements BitFieldSubCommands { + /** Specifies if the bit encoding is signed or unsigned. */ + private final BitEncoding encoding; + + /** Specifies if the offset uses an encoding multiplier. */ + private final BitOffset offset; + + /** Value to increment the bits in the binary value by. */ + private final long increment; + + public String[] toArgs() { + return new String[] { + INCRBY_COMMAND_STRING, encoding.getEncoding(), offset.getOffset(), Long.toString(increment) + }; + } + } + + /** + * OVERFLOW subcommand that determines the result of the SET or + * INCRBY commands when an under or overflow occurs. + */ + @RequiredArgsConstructor + public static final class BitFieldOverflow implements BitFieldSubCommands { + /** Overflow behaviour. */ + private final BitOverflowControl overflowControl; + + public String[] toArgs() { + return new String[] {OVERFLOW_COMMAND_STRING, overflowControl.toString()}; + } + + /** Supported bit overflow controls */ + public enum BitOverflowControl { + /** + * Performs modulo when overflow occurs with unsigned encoding. When overflows occur with + * signed encoding, the value restarts at the most negative value. When underflows occur with + * signed encoding, the value restarts at the most positive value. + */ + WRAP, + /** + * Underflows remain set to the minimum value and overflows remain set to the maximum value. + */ + SAT, + /** Returns null when overflows occur. */ + FAIL + } + } + + /** Specifies if the argument is a signed or unsigned encoding */ + private interface BitEncoding { + String getEncoding(); + } + + /** Specifies that the argument is a signed encoding. Must be less than 65 bits long. */ + public static final class SignedEncoding implements BitEncoding { + @Getter private final String encoding; + + /** + * Constructor that prepends the number with "i" to specify that it is in signed encoding. + * + * @param encodingLength bit size of encoding. + */ + public SignedEncoding(long encodingLength) { + encoding = SIGNED_ENCODING_PREFIX.concat(Long.toString(encodingLength)); + } + } + + /** Specifies that the argument is an unsigned encoding. Must be less than 64 bits long. */ + public static final class UnsignedEncoding implements BitEncoding { + @Getter private final String encoding; + + /** + * Constructor that prepends the number with "u" to specify that it is in unsigned encoding. + * + * @param encodingLength bit size of encoding. + */ + public UnsignedEncoding(long encodingLength) { + encoding = UNSIGNED_ENCODING_PREFIX.concat(Long.toString(encodingLength)); + } + } + + /** Offset in the array of bits. */ + private interface BitOffset { + String getOffset(); + } + + /** + * Offset in the array of bits. Must be greater than or equal to 0. If we have the binary 01101001 + * with offset of 1 for unsigned encoding of size 4, then the value is 13 from 0(1101)001. + */ + public static final class Offset implements BitOffset { + @Getter private final String offset; + + /** + * Constructor for Offset. + * + * @param offset element offset in the array of bits. + */ + public Offset(long offset) { + this.offset = Long.toString(offset); + } + } + + /** + * Offset in the array of bits multiplied by the encoding value. Must be greater than or equal to + * 0. If we have the binary 01101001 with offset multiplier of 1 for unsigned encoding of size 4, + * then the value is 9 from 0110(1001). + */ + public static final class OffsetMultiplier implements BitOffset { + @Getter private final String offset; + + /** + * Constructor for the offset multiplier. + * + * @param offset element offset multiplied by the encoding value in the array of bits. + */ + public OffsetMultiplier(long offset) { + this.offset = OFFSET_MULTIPLIER_PREFIX.concat(Long.toString(offset)); + } + } + + /** + * Creates the arguments to be used in {@link BitmapBaseCommands#bitfield(String, + * BitFieldSubCommands[])} and {@link BitmapBaseCommands#bitfieldReadOnly(String, + * BitFieldReadOnlySubCommands[])}. + * + * @param subCommands commands that holds arguments to be included in the argument String array. + * @return a String array that holds the sub commands and their arguments. + */ + public static String[] createBitFieldArgs(BitFieldSubCommands[] subCommands) { + String[] arguments = {}; + + for (int i = 0; i < subCommands.length; i++) { + arguments = concatenateArrays(arguments, subCommands[i].toArgs()); + } + + return arguments; + } +} diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index a1186cf666..74f27d6f63 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -14,6 +14,11 @@ import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_DOES_NOT_EXIST; import static glide.api.models.commands.SetOptions.ConditionalSet.ONLY_IF_EXISTS; import static glide.api.models.commands.SetOptions.RETURN_OLD_VALUE; +import static glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl.SAT; +import static glide.api.models.commands.bitmap.BitFieldOptions.GET_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.INCRBY_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.OVERFLOW_COMMAND_STRING; +import static glide.api.models.commands.bitmap.BitFieldOptions.SET_COMMAND_STRING; import static glide.api.models.commands.geospatial.GeoAddOptions.CHANGED_REDIS_API; import static glide.api.models.commands.stream.StreamAddOptions.NO_MAKE_STREAM_REDIS_API; import static glide.api.models.commands.stream.StreamRange.MAXIMUM_RANGE_REDIS_API; @@ -46,6 +51,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitField; +import static redis_request.RedisRequestOuterClass.RequestType.BitFieldReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; @@ -201,6 +208,15 @@ import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.function.FunctionLoadOptions; @@ -5399,4 +5415,93 @@ public void srandmember_with_count_returns_success() { assertEquals(testResponse, response); assertArrayEquals(value, payload); } + + @SneakyThrows + @Test + public void bitfieldReadOnly_returns_success() { + // setup + String key = "testKey"; + Long[] result = new Long[] {7L, 8L}; + Offset offset = new Offset(1); + OffsetMultiplier offsetMultiplier = new OffsetMultiplier(8); + BitFieldGet subcommand1 = new BitFieldGet(new UnsignedEncoding(4), offset); + BitFieldGet subcommand2 = new BitFieldGet(new SignedEncoding(5), offsetMultiplier); + String[] args = { + key, + BitFieldOptions.GET_COMMAND_STRING, + BitFieldOptions.UNSIGNED_ENCODING_PREFIX.concat("4"), + offset.getOffset(), + BitFieldOptions.GET_COMMAND_STRING, + BitFieldOptions.SIGNED_ENCODING_PREFIX.concat("5"), + offsetMultiplier.getOffset() + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitFieldReadOnly), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.bitfieldReadOnly(key, new BitFieldReadOnlySubCommands[] {subcommand1, subcommand2}); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } + + @SneakyThrows + @Test + public void bitfield_returns_success() { + // setup + String key = "testKey"; + Long[] result = new Long[] {7L, 8L, 9L}; + UnsignedEncoding u2 = new UnsignedEncoding(2); + SignedEncoding i8 = new SignedEncoding(8); + Offset offset = new Offset(1); + OffsetMultiplier offsetMultiplier = new OffsetMultiplier(8); + long setValue = 2; + long incrbyValue = 5; + String[] args = + new String[] { + key, + SET_COMMAND_STRING, + u2.getEncoding(), + offset.getOffset(), + Long.toString(setValue), + GET_COMMAND_STRING, + i8.getEncoding(), + offsetMultiplier.getOffset(), + OVERFLOW_COMMAND_STRING, + SAT.toString(), + INCRBY_COMMAND_STRING, + u2.getEncoding(), + offset.getOffset(), + Long.toString(incrbyValue) + }; + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(result); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(BitField), eq(args), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = + service.bitfield( + key, + new BitFieldSubCommands[] { + new BitFieldSet(u2, offset, setValue), + new BitFieldGet(i8, offsetMultiplier), + new BitFieldOptions.BitFieldOverflow(SAT), + new BitFieldOptions.BitFieldIncrby(u2, offset, incrbyValue), + }); + Long[] payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(result, payload); + } } diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index 59dac89633..2337d75153 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -36,6 +36,8 @@ import static redis_request.RedisRequestOuterClass.RequestType.BZPopMax; import static redis_request.RedisRequestOuterClass.RequestType.BZPopMin; import static redis_request.RedisRequestOuterClass.RequestType.BitCount; +import static redis_request.RedisRequestOuterClass.RequestType.BitField; +import static redis_request.RedisRequestOuterClass.RequestType.BitFieldReadOnly; import static redis_request.RedisRequestOuterClass.RequestType.BitOp; import static redis_request.RedisRequestOuterClass.RequestType.BitPos; import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName; @@ -185,6 +187,15 @@ import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; @@ -884,6 +895,32 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.srandmember("key", 1); results.add(Pair.of(SRandMember, buildArgs("key", "1"))); + transaction.bitfieldReadOnly( + "key", + new BitFieldReadOnlySubCommands[] {new BitFieldGet(new SignedEncoding(5), new Offset(3))}); + results.add( + Pair.of( + BitFieldReadOnly, + buildArgs( + "key", + BitFieldOptions.GET_COMMAND_STRING, + BitFieldOptions.SIGNED_ENCODING_PREFIX.concat("5"), + "3"))); + transaction.bitfield( + "key", + new BitFieldSubCommands[] { + new BitFieldSet(new UnsignedEncoding(10), new OffsetMultiplier(3), 4) + }); + results.add( + Pair.of( + BitField, + buildArgs( + "key", + BitFieldOptions.SET_COMMAND_STRING, + BitFieldOptions.UNSIGNED_ENCODING_PREFIX.concat("10"), + BitFieldOptions.OFFSET_MULTIPLIER_PREFIX.concat("3"), + "4"))); + var protobufTransaction = transaction.getProtobufTransaction().build(); for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) { diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 99f0bc15cf..0ce4331d2a 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -48,6 +48,17 @@ import glide.api.models.commands.WeightAggregateOptions.KeyArray; import glide.api.models.commands.WeightAggregateOptions.WeightedKeys; import glide.api.models.commands.ZAddOptions; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldIncrby; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldOverflow.BitOverflowControl; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoAddOptions; @@ -4574,4 +4585,281 @@ public void srandmember(BaseClient client) { assertThrows(ExecutionException.class, () -> client.srandmember(nonSetKey, count).get()); assertInstanceOf(RequestException.class, executionExceptionWithCount.getCause()); } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitfieldReadOnly(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + BitFieldGet unsignedOffsetGet = new BitFieldGet(new UnsignedEncoding(2), new Offset(1)); + String emptyKey = UUID.randomUUID().toString(); + String foobar = "foobar"; + + client.set(key1, foobar); + assertArrayEquals( + new Long[] {3L, -2L, 118L, 111L}, + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + // Get value in: 0(11)00110 01101111 01101111 01100010 01100001 01110010 00010100 + unsignedOffsetGet, + // Get value in: 01100(110) 01101111 01101111 01100010 01100001 01110010 00010100 + new BitFieldGet(new SignedEncoding(3), new Offset(5)), + // Get value in: 01100110 01101111 01101(111 0110)0010 01100001 01110010 00010100 + new BitFieldGet(new UnsignedEncoding(7), new OffsetMultiplier(3)), + // Get value in: 01100110 01101111 (01101111) 01100010 01100001 01110010 00010100 + new BitFieldGet(new SignedEncoding(8), new OffsetMultiplier(2)) + }) + .get()); + assertArrayEquals( + new Long[] {0L}, + client + .bitfieldReadOnly(emptyKey, new BitFieldReadOnlySubCommands[] {unsignedOffsetGet}) + .get()); + + // Empty subcommands return an empty array + assertArrayEquals( + new Long[] {}, client.bitfieldReadOnly(key2, new BitFieldReadOnlySubCommands[] {}).get()); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(key2, new String[] {foobar}).get()); + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly(key2, new BitFieldReadOnlySubCommands[] {unsignedOffsetGet}) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Offset must be >= 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(5), new Offset(-1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be > 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(-1), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be < 64 for unsigned bit encoding + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new UnsignedEncoding(64), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Encoding must be < 65 for signed bit encoding + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfieldReadOnly( + key1, + new BitFieldReadOnlySubCommands[] { + new BitFieldGet(new SignedEncoding(65), new Offset(1)) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } + + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void bitfield(BaseClient client) { + String key1 = UUID.randomUUID().toString(); + String key2 = UUID.randomUUID().toString(); + String setKey = UUID.randomUUID().toString(); + String foobar = "foobar"; + UnsignedEncoding u2 = new UnsignedEncoding(2); + UnsignedEncoding u7 = new UnsignedEncoding(7); + SignedEncoding i3 = new SignedEncoding(3); + SignedEncoding i8 = new SignedEncoding(8); + Offset offset1 = new Offset(1); + Offset offset5 = new Offset(5); + OffsetMultiplier offsetMultiplier4 = new OffsetMultiplier(4); + OffsetMultiplier offsetMultiplier8 = new OffsetMultiplier(8); + BitFieldSet overflowSet = new BitFieldSet(u2, offset1, -10); + BitFieldGet overflowGet = new BitFieldGet(u2, offset1); + + client.set(key1, foobar); // binary value: 01100110 01101111 01101111 01100010 01100001 01110010 + + // SET tests + assertArrayEquals( + new Long[] {3L, -2L, 19L, 0L, 2L, 3L, 18L, 20L}, + client + .bitfield( + key1, + new BitFieldSubCommands[] { + // binary value becomes: 0(10)00110 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(u2, offset1, 2), + // binary value becomes: 01000(011) 01101111 01101111 01100010 01100001 01110010 + new BitFieldSet(i3, offset5, 3), + // binary value becomes: 01000011 01101111 01101111 0110(0010 010)00001 01110010 + new BitFieldSet(u7, offsetMultiplier4, 18), + // binary value becomes: 01000011 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 (00010100) + new BitFieldSet(i8, offsetMultiplier8, 20), + new BitFieldGet(u2, offset1), + new BitFieldGet(i3, offset5), + new BitFieldGet(u7, offsetMultiplier4), + new BitFieldGet(i8, offsetMultiplier8) + }) + .get()); + + // INCRBY tests + assertArrayEquals( + new Long[] {3L, -3L, 15L, 30L}, + client + .bitfield( + key1, + new BitFieldSubCommands[] { + // binary value becomes: 0(11)00011 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(u2, offset1, 1), + // binary value becomes: 01100(101) 01101111 01101111 01100010 01000001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(i3, offset5, 2), + // binary value becomes: 01100101 01101111 01101111 0110(0001 111)00001 01110010 + // 00000000 00000000 00010100 + new BitFieldIncrby(u7, offsetMultiplier4, -3), + // binary value becomes: 01100101 01101111 01101111 01100001 11100001 01110010 + // 00000000 00000000 (00011110) + new BitFieldIncrby(i8, offsetMultiplier8, 10) + }) + .get()); + + // OVERFLOW WRAP is used by default if no OVERFLOW is specified + assertArrayEquals( + new Long[] {0L, 2L, 2L}, + client + .bitfield( + key2, + new BitFieldSubCommands[] { + overflowSet, + new BitFieldOverflow(BitOverflowControl.WRAP), + overflowSet, + overflowGet + }) + .get()); + + // OVERFLOW affects only SET or INCRBY after OVERFLOW subcommand + assertArrayEquals( + new Long[] {2L, 2L, 3L, null}, + client + .bitfield( + key2, + new BitFieldSubCommands[] { + overflowSet, + new BitFieldOverflow(BitOverflowControl.SAT), + overflowSet, + overflowGet, + new BitFieldOverflow(BitOverflowControl.FAIL), + overflowSet + }) + .get()); + + // Empty subcommands return an empty array + assertArrayEquals(new Long[] {}, client.bitfield(key2, new BitFieldSubCommands[] {}).get()); + + // Exceptions + // Encoding must be > 0 + ExecutionException executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldSet(new UnsignedEncoding(-1), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Offset must be > 0 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldIncrby(new UnsignedEncoding(5), new Offset(-1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Unsigned bit encoding must be < 64 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldIncrby(new UnsignedEncoding(64), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Signed bit encoding must be < 65 + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + key1, + new BitFieldSubCommands[] { + new BitFieldSet(new SignedEncoding(65), new Offset(1), 1) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + + // Exception thrown due to the key holding a value with the wrong type + assertEquals(1, client.sadd(setKey, new String[] {foobar}).get()); + executionException = + assertThrows( + ExecutionException.class, + () -> + client + .bitfield( + setKey, + new BitFieldSubCommands[] { + new BitFieldSet(new SignedEncoding(3), new Offset(1), 2) + }) + .get()); + assertTrue(executionException.getCause() instanceof RequestException); + } } diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index fd8008eab1..29ce7f73c2 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -20,6 +20,14 @@ import glide.api.models.commands.SetOptions; import glide.api.models.commands.WeightAggregateOptions.Aggregate; import glide.api.models.commands.WeightAggregateOptions.KeyArray; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldGet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldReadOnlySubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSet; +import glide.api.models.commands.bitmap.BitFieldOptions.BitFieldSubCommands; +import glide.api.models.commands.bitmap.BitFieldOptions.Offset; +import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier; +import glide.api.models.commands.bitmap.BitFieldOptions.SignedEncoding; +import glide.api.models.commands.bitmap.BitFieldOptions.UnsignedEncoding; import glide.api.models.commands.bitmap.BitmapIndexType; import glide.api.models.commands.bitmap.BitwiseOperation; import glide.api.models.commands.geospatial.GeoUnit; @@ -656,6 +664,8 @@ private static Object[] bitmapCommands(BaseTransaction transaction) { String key2 = "{bitmapKey}-2" + UUID.randomUUID(); String key3 = "{bitmapKey}-3" + UUID.randomUUID(); String key4 = "{bitmapKey}-4" + UUID.randomUUID(); + BitFieldGet bitFieldGet = new BitFieldGet(new SignedEncoding(5), new Offset(3)); + BitFieldSet bitFieldSet = new BitFieldSet(new UnsignedEncoding(10), new OffsetMultiplier(3), 4); transaction .set(key1, "foobar") @@ -669,12 +679,15 @@ private static Object[] bitmapCommands(BaseTransaction transaction) { .bitpos(key1, 1, 3, 5) .set(key3, "abcdef") .bitop(BitwiseOperation.AND, key4, new String[] {key1, key3}) - .get(key4); + .get(key4) + .bitfieldReadOnly(key1, new BitFieldReadOnlySubCommands[] {bitFieldGet}) + .bitfield(key1, new BitFieldSubCommands[] {bitFieldSet}); if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction - .bitcount(key1, 5, 30, BitmapIndexType.BIT) - .bitpos(key1, 1, 44, 50, BitmapIndexType.BIT); + .set(key3, "foobar") + .bitcount(key3, 5, 30, BitmapIndexType.BIT) + .bitpos(key3, 1, 44, 50, BitmapIndexType.BIT); } var expectedResults = @@ -691,12 +704,15 @@ private static Object[] bitmapCommands(BaseTransaction transaction) { OK, // set(key3, "abcdef") 6L, // bitop(BitwiseOperation.AND, key4, new String[] {key1, key3}) "`bc`ab", // get(key4) + new Long[] {6L}, // bitfieldReadOnly(key1, BitFieldReadOnlySubCommands) + new Long[] {609L}, // bitfield(key1, BitFieldSubCommands) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { return concatenateArrays( expectedResults, new Object[] { + OK, // set(key1, "foobar") 17L, // bitcount(key, 5, 30, BitmapIndexType.BIT) 46L, // bitpos(key, 1, 44, 50, BitmapIndexType.BIT) });