Skip to content

Commit

Permalink
Java: Add BITOP command (#1458)
Browse files Browse the repository at this point in the history
* Java: Add `BITOP` command (#303)

* Incomplete implementation

* Needs more tests and javadocs

* Fixed errors from rebasing

* Added tests and javadocs

* Fixed incorrect comment

* Added tests

* Fixed tests

* Spotless

* Added to module-info

* Addressed PR comments

* Added TODO:
  • Loading branch information
GumpacG committed May 24, 2024
1 parent 6d75ec3 commit 7e1e04d
Show file tree
Hide file tree
Showing 12 changed files with 208 additions and 2 deletions.
1 change: 1 addition & 0 deletions glide-core/src/protobuf/redis_request.proto
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ enum RequestType {
GetBit = 145;
ZInter = 146;
BitPos = 147;
BitOp = 148;
FunctionLoad = 150;
}

Expand Down
3 changes: 3 additions & 0 deletions glide-core/src/request_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ pub enum RequestType {
GetBit = 145,
ZInter = 146,
BitPos = 147,
BitOp = 148,
FunctionLoad = 150,
}

Expand Down Expand Up @@ -319,6 +320,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::ZInter => RequestType::ZInter,
ProtobufRequestType::FunctionLoad => RequestType::FunctionLoad,
ProtobufRequestType::BitPos => RequestType::BitPos,
ProtobufRequestType::BitOp => RequestType::BitOp,
}
}
}
Expand Down Expand Up @@ -476,6 +478,7 @@ impl RequestType {
RequestType::ZInter => Some(cmd("ZINTER")),
RequestType::FunctionLoad => Some(get_two_word_command("FUNCTION", "LOAD")),
RequestType::BitPos => Some(cmd("BITPOS")),
RequestType::BitOp => Some(cmd("BITOP")),
}
}
}
12 changes: 12 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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.BitOp;
import static redis_request.RedisRequestOuterClass.RequestType.BitPos;
import static redis_request.RedisRequestOuterClass.RequestType.Decr;
import static redis_request.RedisRequestOuterClass.RequestType.DecrBy;
Expand Down Expand Up @@ -145,6 +146,7 @@
import glide.api.models.commands.WeightAggregateOptions.KeysOrWeightedKeys;
import glide.api.models.commands.ZAddOptions;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.geospatial.GeoAddOptions;
import glide.api.models.commands.geospatial.GeoUnit;
import glide.api.models.commands.geospatial.GeospatialData;
Expand Down Expand Up @@ -1442,4 +1444,14 @@ public CompletableFuture<Long> bitpos(
};
return commandManager.submitNewCommand(BitPos, arguments, this::handleLongResponse);
}

@Override
public CompletableFuture<Long> bitop(
@NonNull BitwiseOperation bitwiseOperation,
@NonNull String destination,
@NonNull String[] keys) {
String[] arguments =
concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys);
return commandManager.submitNewCommand(BitOp, arguments, this::handleLongResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package glide.api.commands;

import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import java.util.concurrent.CompletableFuture;

/**
Expand Down Expand Up @@ -213,4 +214,27 @@ public interface BitmapBaseCommands {
*/
CompletableFuture<Long> bitpos(
String key, long bit, long start, long end, BitmapIndexType offsetType);

/**
* Perform a bitwise operation between multiple keys (containing string values) and store the
* result in the <code>destination</code>.
*
* @apiNote When in cluster mode, <code>destination</code> and all <code>keys</code> must map to
* the same hash slot.
* @see <a href="https://redis.io/commands/bitop/">redis.io</a> for details.
* @param bitwiseOperation The bitwise operation to perform.
* @param destination The key that will store the resulting string.
* @param keys The list of keys to perform the bitwise operation on.
* @return The size of the string stored in <code>destination</code>.
* @example
* <pre>{@code
* client.set("key1", "A"); // "A" has binary value 01000001
* client.set("key2", "B"); // "B" has binary value 01000010
* Long payload = client.bitop(BitwiseOperation.AND, "destination", new String[] {key1, key2}).get();
* assert "@".equals(client.get("destination").get()); // "@" has binary value 01000000
* assert payload == 1L; // The size of the resulting string is 1.
* }</pre>
*/
CompletableFuture<Long> bitop(
BitwiseOperation bitwiseOperation, String destination, String[] keys);
}
23 changes: 23 additions & 0 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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.BitOp;
import static redis_request.RedisRequestOuterClass.RequestType.BitPos;
import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName;
import static redis_request.RedisRequestOuterClass.RequestType.ClientId;
Expand Down Expand Up @@ -167,6 +168,7 @@
import glide.api.models.commands.WeightAggregateOptions.WeightedKeys;
import glide.api.models.commands.ZAddOptions;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.function.FunctionLoadOptions;
import glide.api.models.commands.geospatial.GeoAddOptions;
import glide.api.models.commands.geospatial.GeoUnit;
Expand Down Expand Up @@ -3529,6 +3531,27 @@ public T bitpos(
return getThis();
}

/**
* Perform a bitwise operation between multiple keys (containing string values) and store the
* result in the <code>destination</code>.
*
* @see <a href="https://redis.io/commands/bitop/">redis.io</a> for details.
* @param bitwiseOperation The bitwise operation to perform.
* @param destination The key that will store the resulting string.
* @param keys The list of keys to perform the bitwise operation on.
* @return Command Response - The size of the string stored in <code>destination</code>.
*/
public T bitop(
@NonNull BitwiseOperation bitwiseOperation,
@NonNull String destination,
@NonNull String[] keys) {
ArgsArray commandArgs =
buildArgs(concatenateArrays(new String[] {bitwiseOperation.toString(), destination}, keys));

protobufTransaction.addCommands(buildCommand(BitOp, commandArgs));
return getThis();
}

/** Build protobuf {@link Command} object for given command and arguments. */
protected Command buildCommand(RequestType requestType) {
return buildCommand(requestType, buildArgs());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.models.commands.bitmap;

import glide.api.commands.BitmapBaseCommands;

/**
* Defines bitwise operation for {@link BitmapBaseCommands#bitop(BitwiseOperation, String,
* String[])}. Specifies bitwise operation to perform between keys.
*
* @see <a href="https://redis.io/commands/bitop/">redis.io</a>
*/
public enum BitwiseOperation {
AND,
OR,
XOR,
NOT
}
1 change: 1 addition & 0 deletions java/client/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
exports glide.api.models.commands.stream;
exports glide.api.models.configuration;
exports glide.api.models.exceptions;
exports glide.api.models.commands.bitmap;
exports glide.api.models.commands.geospatial;
exports glide.api.models.commands.function;

Expand Down
27 changes: 27 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
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.BitOp;
import static redis_request.RedisRequestOuterClass.RequestType.BitPos;
import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName;
import static redis_request.RedisRequestOuterClass.RequestType.ClientId;
Expand Down Expand Up @@ -182,6 +183,7 @@
import glide.api.models.commands.WeightAggregateOptions.WeightedKeys;
import glide.api.models.commands.ZAddOptions;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.function.FunctionLoadOptions;
import glide.api.models.commands.geospatial.GeoAddOptions;
import glide.api.models.commands.geospatial.GeoUnit;
Expand Down Expand Up @@ -4874,4 +4876,29 @@ public void bitpos_with_start_and_end_and_type_returns_success() {
assertEquals(testResponse, response);
assertEquals(bitPosition, payload);
}

@SneakyThrows
@Test
public void bitop_returns_success() {
// setup
String destination = "destination";
String[] keys = new String[] {"key1", "key2"};
Long result = 6L;
BitwiseOperation bitwiseAnd = BitwiseOperation.AND;
String[] arguments = concatenateArrays(new String[] {bitwiseAnd.toString(), destination}, keys);
CompletableFuture<Long> testResponse = new CompletableFuture<>();
testResponse.complete(result);

// match on protobuf request
when(commandManager.<Long>submitNewCommand(eq(BitOp), eq(arguments), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<Long> response = service.bitop(bitwiseAnd, destination, keys);
Long payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(result, payload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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.BitOp;
import static redis_request.RedisRequestOuterClass.RequestType.BitPos;
import static redis_request.RedisRequestOuterClass.RequestType.ClientGetName;
import static redis_request.RedisRequestOuterClass.RequestType.ClientId;
Expand Down Expand Up @@ -166,6 +167,7 @@
import glide.api.models.commands.WeightAggregateOptions.WeightedKeys;
import glide.api.models.commands.ZAddOptions;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.geospatial.GeoAddOptions;
import glide.api.models.commands.geospatial.GeoUnit;
import glide.api.models.commands.geospatial.GeospatialData;
Expand Down Expand Up @@ -823,6 +825,9 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)),
transaction.bitpos("key", 1, 8, 10, BitmapIndexType.BIT);
results.add(Pair.of(BitPos, buildArgs("key", "1", "8", "10", BitmapIndexType.BIT.toString())));

transaction.bitop(BitwiseOperation.AND, "destination", new String[] {"key"});
results.add(Pair.of(BitOp, buildArgs(BitwiseOperation.AND.toString(), "destination", "key")));

var protobufTransaction = transaction.getProtobufTransaction().build();

for (int idx = 0; idx < protobufTransaction.getCommandsCount(); idx++) {
Expand Down
79 changes: 79 additions & 0 deletions java/integTest/src/test/java/glide/SharedCommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import glide.api.models.commands.WeightAggregateOptions.WeightedKeys;
import glide.api.models.commands.ZAddOptions;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.geospatial.GeoAddOptions;
import glide.api.models.commands.geospatial.GeoUnit;
import glide.api.models.commands.geospatial.GeospatialData;
Expand Down Expand Up @@ -3982,4 +3983,82 @@ public void bitpos(BaseClient client) {
assertTrue(executionException.getCause() instanceof RequestException);
}
}

@SneakyThrows
@ParameterizedTest(autoCloseArguments = false)
@MethodSource("getClients")
public void bitop(BaseClient client) {
String key1 = "{key}-1".concat(UUID.randomUUID().toString());
String key2 = "{key}-2".concat(UUID.randomUUID().toString());
String emptyKey1 = "{key}-3".concat(UUID.randomUUID().toString());
String emptyKey2 = "{key}-4".concat(UUID.randomUUID().toString());
String destination = "{key}-5".concat(UUID.randomUUID().toString());
String[] keys = new String[] {key1, key2};
String[] emptyKeys = new String[] {emptyKey1, emptyKey2};
String value1 = "foobar";
String value2 = "abcdef";

assertEquals(OK, client.set(key1, value1).get());
assertEquals(OK, client.set(key2, value2).get());
assertEquals(6L, client.bitop(BitwiseOperation.AND, destination, keys).get());
assertEquals("`bc`ab", client.get(destination).get());
assertEquals(6L, client.bitop(BitwiseOperation.OR, destination, keys).get());
assertEquals("goofev", client.get(destination).get());

// Reset values for simplicity of results in XOR
assertEquals(OK, client.set(key1, "a").get());
assertEquals(OK, client.set(key2, "b").get());
assertEquals(1L, client.bitop(BitwiseOperation.XOR, destination, keys).get());
assertEquals("\u0003", client.get(destination).get());

// Test single source key
assertEquals(1L, client.bitop(BitwiseOperation.AND, destination, new String[] {key1}).get());
assertEquals("a", client.get(destination).get());
assertEquals(1L, client.bitop(BitwiseOperation.OR, destination, new String[] {key1}).get());
assertEquals("a", client.get(destination).get());
assertEquals(1L, client.bitop(BitwiseOperation.XOR, destination, new String[] {key1}).get());
assertEquals("a", client.get(destination).get());
assertEquals(1L, client.bitop(BitwiseOperation.NOT, destination, new String[] {key1}).get());
// First bit is flipped to 1 and throws 'utf-8' codec can't decode byte 0x9e in position 0:
// invalid start byte
// TODO: update once fix is implemented for https://github.com/aws/glide-for-redis/issues/1447
ExecutionException executionException =
assertThrows(ExecutionException.class, () -> client.get(destination).get());
assertTrue(executionException.getCause() instanceof RuntimeException);
assertEquals(0, client.setbit(key1, 0, 1).get());
assertEquals(1L, client.bitop(BitwiseOperation.NOT, destination, new String[] {key1}).get());
assertEquals("\u001e", client.get(destination).get());

// Returns null when all keys hold empty strings
assertEquals(0L, client.bitop(BitwiseOperation.AND, destination, emptyKeys).get());
assertEquals(null, client.get(destination).get());
assertEquals(0L, client.bitop(BitwiseOperation.OR, destination, emptyKeys).get());
assertEquals(null, client.get(destination).get());
assertEquals(0L, client.bitop(BitwiseOperation.XOR, destination, emptyKeys).get());
assertEquals(null, client.get(destination).get());
assertEquals(
0L, client.bitop(BitwiseOperation.NOT, destination, new String[] {emptyKey1}).get());
assertEquals(null, client.get(destination).get());

// Exception thrown due to the key holding a value with the wrong type
assertEquals(1, client.sadd(emptyKey1, new String[] {value1}).get());
executionException =
assertThrows(
ExecutionException.class,
() -> client.bitop(BitwiseOperation.AND, destination, new String[] {emptyKey1}).get());

// Source keys is an empty list
executionException =
assertThrows(
ExecutionException.class,
() -> client.bitop(BitwiseOperation.OR, destination, new String[] {}).get());
assertTrue(executionException.getCause() instanceof RequestException);

// NOT with more than one source key
executionException =
assertThrows(
ExecutionException.class,
() -> client.bitop(BitwiseOperation.NOT, destination, new String[] {key1, key2}).get());
assertTrue(executionException.getCause() instanceof RequestException);
}
}
11 changes: 10 additions & 1 deletion java/integTest/src/test/java/glide/TransactionTestUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import glide.api.models.commands.WeightAggregateOptions.Aggregate;
import glide.api.models.commands.WeightAggregateOptions.KeyArray;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.geospatial.GeoUnit;
import glide.api.models.commands.geospatial.GeospatialData;
import glide.api.models.commands.stream.StreamAddOptions;
Expand Down Expand Up @@ -557,6 +558,8 @@ private static Object[] scriptingAndFunctionsCommands(BaseTransaction<?> transac
private static Object[] bitmapCommands(BaseTransaction<?> transaction) {
String key1 = "{bitmapKey}-1" + UUID.randomUUID();
String key2 = "{bitmapKey}-2" + UUID.randomUUID();
String key3 = "{bitmapKey}-3" + UUID.randomUUID();
String key4 = "{bitmapKey}-4" + UUID.randomUUID();

transaction
.set(key1, "foobar")
Expand All @@ -567,7 +570,10 @@ private static Object[] bitmapCommands(BaseTransaction<?> transaction) {
.getbit(key1, 1)
.bitpos(key1, 1)
.bitpos(key1, 1, 3)
.bitpos(key1, 1, 3, 5);
.bitpos(key1, 1, 3, 5)
.set(key3, "abcdef")
.bitop(BitwiseOperation.AND, key4, new String[] {key1, key3})
.get(key4);

if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) {
transaction
Expand All @@ -586,6 +592,9 @@ private static Object[] bitmapCommands(BaseTransaction<?> transaction) {
1L, // bitpos(key, 1)
25L, // bitpos(key, 1, 3)
25L, // bitpos(key, 1, 3, 5)
OK, // set(key3, "abcdef")
6L, // bitop(BitwiseOperation.AND, key4, new String[] {key1, key3})
"`bc`ab", // get(key4)
};

if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) {
Expand Down
7 changes: 6 additions & 1 deletion java/integTest/src/test/java/glide/cluster/CommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import glide.api.models.commands.InfoOptions;
import glide.api.models.commands.RangeOptions.RangeByIndex;
import glide.api.models.commands.WeightAggregateOptions.KeyArray;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.configuration.NodeAddress;
import glide.api.models.configuration.RedisClusterClientConfiguration;
import glide.api.models.configuration.RequestRoutingConfiguration.Route;
Expand Down Expand Up @@ -691,7 +692,11 @@ public static Stream<Arguments> callCrossSlotCommandsWhichShouldFail() {
Arguments.of(
"zmpop", "7.0.0", clusterClient.zmpop(new String[] {"abc", "zxy", "lkn"}, MAX)),
Arguments.of(
"bzmpop", "7.0.0", clusterClient.bzmpop(new String[] {"abc", "zxy", "lkn"}, MAX, .1)));
"bzmpop", "7.0.0", clusterClient.bzmpop(new String[] {"abc", "zxy", "lkn"}, MAX, .1)),
Arguments.of(
"bitop",
null,
clusterClient.bitop(BitwiseOperation.OR, "abc", new String[] {"zxy", "lkn"})));
}

@SneakyThrows
Expand Down

0 comments on commit 7e1e04d

Please sign in to comment.