Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: Add SMOVE command. #1249

Merged
merged 13 commits into from
Apr 16, 2024
11 changes: 10 additions & 1 deletion glide-core/src/client/value_conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,14 @@ fn convert_array_to_map(
pub(crate) fn expected_type_for_cmd(cmd: &Cmd) -> Option<ExpectedReturnType> {
let command = cmd.command()?;

// TODO use enum to avoid mistakes
match command.as_slice() {
b"HGETALL" | b"XREAD" | b"CONFIG GET" | b"FT.CONFIG GET" | b"HELLO" => {
Some(ExpectedReturnType::Map)
}
b"INCRBYFLOAT" | b"HINCRBYFLOAT" => Some(ExpectedReturnType::Double),
b"HEXISTS" | b"HSETNX" | b"EXPIRE" | b"EXPIREAT" | b"PEXPIRE" | b"PEXPIREAT"
| b"SISMEMBER" | b"PERSIST" => Some(ExpectedReturnType::Boolean),
| b"SISMEMBER" | b"PERSIST" | b"SMOVE" => Some(ExpectedReturnType::Boolean),
b"SMEMBERS" => Some(ExpectedReturnType::Set),
b"ZSCORE" => Some(ExpectedReturnType::DoubleOrNull),
b"ZPOPMIN" | b"ZPOPMAX" => Some(ExpectedReturnType::MapOfStringToDouble),
Expand Down Expand Up @@ -318,6 +319,14 @@ mod tests {
assert!(expected_type_for_cmd(redis::cmd("ZREVRANK").arg("key").arg("member")).is_none());
}

#[test]
fn convert_smove_to_bool() {
assert!(matches!(
expected_type_for_cmd(redis::cmd("SMOVE").arg("key1").arg("key2").arg("elem")),
Some(ExpectedReturnType::Boolean)
));
}

#[test]
fn test_convert_to_map_of_string_to_double() {
assert_eq!(
Expand Down
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 @@ -143,6 +143,7 @@ enum RequestType {
Blpop = 100;
RPushX = 102;
LPushX = 103;
SMove = 106;
}

message Command {
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 @@ -111,6 +111,7 @@ pub enum RequestType {
Blpop = 100,
RPushX = 102,
LPushX = 103,
SMove = 109,
}

fn get_two_word_command(first: &str, second: &str) -> Cmd {
Expand Down Expand Up @@ -224,6 +225,7 @@ impl From<::protobuf::EnumOrUnknown<ProtobufRequestType>> for RequestType {
ProtobufRequestType::RPushX => RequestType::RPushX,
ProtobufRequestType::LPushX => RequestType::LPushX,
ProtobufRequestType::Blpop => RequestType::Blpop,
ProtobufRequestType::SMove => RequestType::SMove,
ProtobufRequestType::Spop => RequestType::Spop,
}
}
Expand Down Expand Up @@ -334,6 +336,7 @@ impl RequestType {
RequestType::RPushX => Some(cmd("RPUSHX")),
RequestType::LPushX => Some(cmd("LPUSHX")),
RequestType::Blpop => Some(cmd("BLPOP")),
RequestType::SMove => Some(cmd("SMOVE")),
RequestType::Spop => Some(cmd("SPOP")),
}
}
Expand Down
8 changes: 8 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.SCard;
import static redis_request.RedisRequestOuterClass.RequestType.SIsMember;
import static redis_request.RedisRequestOuterClass.RequestType.SMembers;
import static redis_request.RedisRequestOuterClass.RequestType.SMove;
import static redis_request.RedisRequestOuterClass.RequestType.SRem;
import static redis_request.RedisRequestOuterClass.RequestType.SetString;
import static redis_request.RedisRequestOuterClass.RequestType.Strlen;
Expand Down Expand Up @@ -512,6 +513,13 @@ public CompletableFuture<Long> scard(@NonNull String key) {
return commandManager.submitNewCommand(SCard, new String[] {key}, this::handleLongResponse);
}

@Override
public CompletableFuture<Boolean> smove(
@NonNull String source, @NonNull String destination, @NonNull String member) {
return commandManager.submitNewCommand(
SMove, new String[] {source, destination, member}, this::handleBooleanResponse);
}

@Override
public CompletableFuture<Long> exists(@NonNull String[] keys) {
return commandManager.submitNewCommand(Exists, keys, this::handleLongResponse);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ public interface SetBaseCommands {
*/
CompletableFuture<Long> scard(String key);

/**
* Moves <code>member</code> from the set at <code>source</code> to the set at <code>destination
* </code>, removing it from the source set. Creates a new destination set if needed. The
* operation is atomic.
*
* @see <a href="https://redis.io/commands/smove/">redis.io</a> for details.
* @param source The key of the set to remove the element from.
* @param destination The key of the set to add the element to.
* @param member The set element to move.
* @return <code>true</code> on success, or <code>false</code> if the <code>source</code> set does
* not exist or the element is not a member of the source set.
* @example
* <pre>{@code
* boolean moved = client.smove("set1", "set2", "element").get();
* assert moved;
* }</pre>
*/
CompletableFuture<Boolean> smove(String source, String destination, String member);

/**
* Returns if <code>member</code> is a member of the set stored at <code>key</code>.
*
Expand Down
19 changes: 19 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 @@ -62,6 +62,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.SCard;
import static redis_request.RedisRequestOuterClass.RequestType.SIsMember;
import static redis_request.RedisRequestOuterClass.RequestType.SMembers;
import static redis_request.RedisRequestOuterClass.RequestType.SMove;
import static redis_request.RedisRequestOuterClass.RequestType.SRem;
import static redis_request.RedisRequestOuterClass.RequestType.SetString;
import static redis_request.RedisRequestOuterClass.RequestType.Strlen;
Expand Down Expand Up @@ -878,6 +879,24 @@ public T scard(@NonNull String key) {
return getThis();
}

/**
* Moves <code>member</code> from the set at <code>source</code> to the set at <code>destination
* </code>, removing it from the source set. Creates a new destination set if needed. The
* operation is atomic.
*
* @see <a href="https://redis.io/commands/smove/">redis.io</a> for details.
* @param source The key of the set to remove the element from.
* @param destination The key of the set to add the element to.
* @param member The set element to move.
* @return Command response - <code>true</code> on success, or <code>false</code> if the <code>
* source</code> set does not exist or the element is not a member of the source set.
*/
public T smove(@NonNull String source, @NonNull String destination, @NonNull String member) {
ArgsArray commandArgs = buildArgs(source, destination, member);
protobufTransaction.addCommands(buildCommand(SMove, commandArgs));
return getThis();
}

/**
* Reads the configuration parameters of a running Redis server.
*
Expand Down
25 changes: 25 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.SCard;
import static redis_request.RedisRequestOuterClass.RequestType.SIsMember;
import static redis_request.RedisRequestOuterClass.RequestType.SMembers;
import static redis_request.RedisRequestOuterClass.RequestType.SMove;
import static redis_request.RedisRequestOuterClass.RequestType.SRem;
import static redis_request.RedisRequestOuterClass.RequestType.Select;
import static redis_request.RedisRequestOuterClass.RequestType.SetString;
Expand Down Expand Up @@ -1615,6 +1616,30 @@ public void scard_returns_success() {
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void smove_returns_success() {
// setup
String source = "src";
String destination = "dst";
String member = "elem";
String[] arguments = {source, destination, member};

CompletableFuture<Boolean> testResponse = new CompletableFuture<>();
testResponse.complete(true);

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

// exercise
CompletableFuture<Boolean> response = service.smove(source, destination, member);

// verify
assertEquals(testResponse, response);
assertTrue(response.get());
}

@SneakyThrows
@Test
public void zadd_noOptions_returns_success() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.SCard;
import static redis_request.RedisRequestOuterClass.RequestType.SIsMember;
import static redis_request.RedisRequestOuterClass.RequestType.SMembers;
import static redis_request.RedisRequestOuterClass.RequestType.SMove;
import static redis_request.RedisRequestOuterClass.RequestType.SRem;
import static redis_request.RedisRequestOuterClass.RequestType.SetString;
import static redis_request.RedisRequestOuterClass.RequestType.Strlen;
Expand Down Expand Up @@ -269,6 +270,11 @@ public void transaction_builds_protobuf_request(BaseTransaction<?> transaction)
transaction.scard("key");
results.add(Pair.of(SCard, ArgsArray.newBuilder().addArgs("key").build()));

transaction.smove("key1", "key2", "elem");
results.add(
Pair.of(
SMove, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").addArgs("elem").build()));

transaction.exists(new String[] {"key1", "key2"});
results.add(Pair.of(Exists, ArgsArray.newBuilder().addArgs("key1").addArgs("key2").build()));

Expand Down
46 changes: 46 additions & 0 deletions java/integTest/src/test/java/glide/SharedCommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,52 @@ public void sadd_srem_scard_smembers_key_with_non_set_value(BaseClient client) {
assertTrue(e.getCause() instanceof RequestException);
}

@SneakyThrows
@ParameterizedTest
@MethodSource("getClients")
public void smove(BaseClient client) {
String setKey1 = "{key}" + UUID.randomUUID();
String setKey2 = "{key}" + UUID.randomUUID();
String setKey3 = "{key}" + UUID.randomUUID();
String nonSetKey = "{key}" + UUID.randomUUID();

assertEquals(3, client.sadd(setKey1, new String[] {"1", "2", "3"}).get());
assertEquals(2, client.sadd(setKey2, new String[] {"2", "3"}).get());
// move an elem
assertTrue(client.smove(setKey1, setKey2, "1").get());
assertEquals(Set.of("2", "3"), client.smembers(setKey1).get());
assertEquals(Set.of("1", "2", "3"), client.smembers(setKey2).get());
// move an elem which preset at destination
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt that you need this test case. // move an elem which is preset at the destination (you are testing how sets actually work here)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but it does not hurt.
It also shows use cases of the command. I like when IT could be interpreted as examples.

assertTrue(client.smove(setKey2, setKey1, "2").get());
assertEquals(Set.of("2", "3"), client.smembers(setKey1).get());
assertEquals(Set.of("1", "3"), client.smembers(setKey2).get());
// move from missing key
assertFalse(client.smove(setKey3, setKey1, "4").get());
assertEquals(Set.of("2", "3"), client.smembers(setKey1).get());
// move to a new set
assertTrue(client.smove(setKey1, setKey3, "2").get());
assertEquals(Set.of("3"), client.smembers(setKey1).get());
assertEquals(Set.of("2"), client.smembers(setKey3).get());
// move missing element
assertFalse(client.smove(setKey1, setKey3, "42").get());
assertEquals(Set.of("3"), client.smembers(setKey1).get());
assertEquals(Set.of("2"), client.smembers(setKey3).get());
// move missing element to missing key
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
assertFalse(client.smove(setKey1, nonSetKey, "42").get());
assertEquals(Set.of("3"), client.smembers(setKey1).get());
assertEquals("none", client.type(nonSetKey).get());

// Key exists, but it is not a set
assertEquals(OK, client.set(nonSetKey, "bar").get());
ExecutionException executionException =
assertThrows(ExecutionException.class, () -> client.smove(nonSetKey, setKey1, "_").get());
assertTrue(executionException.getCause() instanceof RequestException);

executionException =
assertThrows(ExecutionException.class, () -> client.smove(setKey1, nonSetKey, "_").get());
assertTrue(executionException.getCause() instanceof RequestException);
}

@SneakyThrows
@ParameterizedTest
@MethodSource("getClients")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class TransactionTestUtilities {
private static final String key6 = "{key}" + UUID.randomUUID();
private static final String listKey3 = "{key}:listKey3-" + UUID.randomUUID();
private static final String key7 = "{key}" + UUID.randomUUID();
private static final String setKey2 = "{key}" + UUID.randomUUID();
private static final String key8 = "{key}" + UUID.randomUUID();
private static final String key9 = "{key}" + UUID.randomUUID();
private static final String hllKey1 = "{key}:hllKey1-" + UUID.randomUUID();
Expand Down Expand Up @@ -94,6 +95,7 @@ public static BaseTransaction<?> transactionTest(BaseTransaction<?> baseTransact
baseTransaction.scard(key7);
baseTransaction.sismember(key7, "baz");
baseTransaction.smembers(key7);
baseTransaction.smove(key7, setKey2, "baz");

baseTransaction.zadd(key8, Map.of("one", 1.0, "two", 2.0, "three", 3.0));
baseTransaction.zrank(key8, "one");
Expand Down Expand Up @@ -185,6 +187,7 @@ public static Object[] transactionTestResult() {
1L,
true, // sismember(key7, "baz")
Set.of("baz"),
true, // smove(key7, setKey2, "baz")
3L,
0L, // zrank(key8, "one")
4.0,
Expand Down
Loading