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

LCS + mset + msetnx #1698

Closed
wants to merge 13 commits into from
28 changes: 25 additions & 3 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 glide.utils.ArrayTransformUtils.castMapOfArrays;
import static glide.utils.ArrayTransformUtils.concatenateArrays;
import static glide.utils.ArrayTransformUtils.convertMapToKeyValueStringArray;
import static glide.utils.ArrayTransformUtils.convertMapToKeyValueGlideStringArray;
import static glide.utils.ArrayTransformUtils.convertMapToValueKeyStringArray;
import static glide.utils.ArrayTransformUtils.mapGeoDataToArray;
import static redis_request.RedisRequestOuterClass.RequestType.Append;
Expand Down Expand Up @@ -632,9 +633,24 @@ public CompletableFuture<GlideString[]> mget(@NonNull GlideString[] keys) {
}

@Override
public CompletableFuture<String> mset(@NonNull Map<String, String> keyValueMap) {
String[] args = convertMapToKeyValueStringArray(keyValueMap);
return commandManager.submitNewCommand(MSet, args, this::handleStringResponse);
public <ArgType> CompletableFuture<String> mset(@NonNull Map<ArgType, ArgType> keyValueMap) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Better to duplicate code - as I understand, all APIs with String would be removed later

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since the functions signature differ only in template input Java doesn't allow such overload. I.E this kind of overloading isn't permitted:
someType foo(Map<String,String> ...);
someType foo(Map<GlideString,GlideString> ...);

That's why i had to resort to templating.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, Java's "type erasure" thingi

if (keyValueMap.isEmpty()) {
throw new IllegalArgumentException("empty map");
}

Comment on lines +752 to +755
Copy link
Collaborator

Choose a reason for hiding this comment

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

You can omit this. We have tests where intentionally send to redis an empty map

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure I follow. This isn't for testing purpose, if we remove this and get an empty map with mset we'll recieve
NoSuchElementException so isn't his necessary?

Copy link
Collaborator

Choose a reason for hiding this comment

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

if map is empty - submit command with 0 args. But track arg type to avoid issues.

Copy link
Contributor

Choose a reason for hiding this comment

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

@Yury-Fridlyand so basically you want the below code to be enclosed within:

if (!keyValueMap.isEmpty()) {
  ArgType firstValue = keyValueMap.keySet().iterator().next();
  ..
} else {
  // submit empty array
  return commandManager.submitNewCommand(MSet, new String[] {}, this::handleStringResponse);
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

yes

ArgType firstValue = keyValueMap.keySet().iterator().next();

Object[] args;
if (firstValue instanceof String) {
args = convertMapToKeyValueStringArray((Map<String, ?>) keyValueMap);
return commandManager.submitNewCommand(MSet, (String[]) args, this::handleStringResponse);
} else if (firstValue instanceof GlideString) {
args = convertMapToKeyValueGlideStringArray((Map<GlideString, GlideString>) keyValueMap);
return commandManager.submitNewCommand(
MSet, (GlideString[]) args, this::handleStringResponse);
} else {
throw new IllegalArgumentException("Expected String or GlideString");
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you decide to not duplicate - add a test for this case

}
}

@Override
Expand Down Expand Up @@ -2648,6 +2664,12 @@ public CompletableFuture<String> lcs(@NonNull String key1, @NonNull String key2)
return commandManager.submitNewCommand(LCS, arguments, this::handleStringResponse);
}

@Override
public CompletableFuture<GlideString> lcs(@NonNull GlideString key1, @NonNull GlideString key2) {
GlideString[] arguments = new GlideString[] {key1, key2};
return commandManager.submitNewCommand(LCS, arguments, this::handleGlideStringResponse);
}

@Override
public CompletableFuture<Long> lcsLen(@NonNull String key1, @NonNull String key2) {
String[] arguments = new String[] {key1, key2, LEN_REDIS_API};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public interface StringBaseCommands {
* assert result.equals("OK"));
* }</pre>
*/
CompletableFuture<String> mset(Map<String, String> keyValueMap);
<ArgType> CompletableFuture<String> mset(Map<ArgType , ArgType> keyValueMap);
Copy link
Collaborator

@Yury-Fridlyand Yury-Fridlyand Jun 28, 2024

Choose a reason for hiding this comment

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

If you decide to not duplicate - describe what is ArgType in the doc.
But this will be inconsistend with all other APIs

Copy link
Contributor

@eifrah-aws eifrah-aws Jul 4, 2024

Choose a reason for hiding this comment

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

IIUC, we can't really duplicate these functions. Java's type erasure does not differentiate between:

void foo(Map<GlideString, GlideString> m)

and

void foo(Map<String, String> m)

Both have the same signature of void foo(Map)
The options are:

  • Add custom method name msetBinary or
  • Generalize the method (we could also be using Map<?, ?>

With the second approach, the user gets the "feel" of overloading: he can still call the method with either Map<String, String> or Map<GlideString, GlideString> (anything else will throw)


/**
* Sets multiple keys to values if the key does not exist. The operation is atomic, and if one or
Expand Down Expand Up @@ -536,6 +536,28 @@ public interface StringBaseCommands {
*/
CompletableFuture<String> lcs(String key1, String key2);

/**
* Returns the longest common subsequence between strings stored at <code>key1</code> and <code>
* key2</code>.
*
* @since Redis 7.0 and above.
* @apiNote When in cluster mode, <code>key1</code> and <code>key2</code> must map to the same
* hash slot.
* @see <a href="https://valkey.io/commands/lcs/">valkey.io</a> for details.
* @param key1 The key that stores the first string.
* @param key2 The key that stores the second string.
* @return A <code>String</code> containing the longest common subsequence between the 2 strings.
* An empty <code>String</code> is returned if the keys do not exist or have no common
* subsequences.
* @example
* <pre>{@code
* // testKey1 = abcd, testKey2 = axcd
* GlideString result = client.lcs(gs("testKey1"), gs("testKey2")).get();
* assert result.equals("acd");
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved
* }</pre>
*/
CompletableFuture<GlideString> lcs(GlideString key1, GlideString key2);

/**
* Returns the length of the longest common subsequence between strings stored at <code>key1
* </code> and <code>key2</code>.
Expand Down
15 changes: 14 additions & 1 deletion java/client/src/main/java/glide/utils/ArrayTransformUtils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.utils;

import glide.api.models.GlideString;
import glide.api.commands.GeospatialIndicesBaseCommands;
import glide.api.models.commands.geospatial.GeospatialData;
import java.lang.reflect.Array;
Expand All @@ -26,6 +26,19 @@ public static String[] convertMapToKeyValueStringArray(Map<String, ?> args) {
.toArray(String[]::new);
}

/**
* Converts a map of string keys and values of any type in to an array of strings with alternating
* keys and values.
*
* @param args Map of glide string keys to values of gs type to convert.
* @return Array of GlideStrings [key1, value1, key2, value2, ...].
*/
public static GlideString[] convertMapToKeyValueGlideStringArray(Map<GlideString, GlideString> args) {
return args.entrySet().stream()
.flatMap(entry -> Stream.of(entry.getKey(), entry.getValue()))
.toArray(GlideString[]::new);
}

/**
* Converts a map of string keys and values of any type into an array of strings with alternating
* values and keys.
Expand Down
50 changes: 50 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,31 @@ public void mset_returns_success() {
assertEquals(OK, payload);
}

@SneakyThrows
@Test
public void mset_returns_success_binary() {
// setup
Map<GlideString, GlideString> keyValueMap = new LinkedHashMap<>();
keyValueMap.put(gs("key1"), gs("value1"));
keyValueMap.put(gs("key2"), gs("value2"));
GlideString[] args = {gs("key1"), gs("value1"), gs("key2"), gs("value2")};

CompletableFuture<String> testResponse = new CompletableFuture<>();
testResponse.complete(OK);

// match on protobuf request
when(commandManager.<String>submitNewCommand(eq(MSet), eq(args), any()))
.thenReturn(testResponse);

// exercise
CompletableFuture<String> response = service.mset(keyValueMap);
String payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(OK, payload);
}

@SneakyThrows
@Test
public void msetnx_returns_success() {
Expand Down Expand Up @@ -8487,6 +8512,31 @@ public void lcs() {
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void lcs_binary() {
// setup
GlideString key1 = gs("testKey1");
GlideString key2 = gs("testKey2");
GlideString[] arguments = new GlideString[] {key1, key2};
GlideString value = gs("foo");

CompletableFuture<GlideString> testResponse = new CompletableFuture<>();
testResponse.complete(value);

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

// exercise
CompletableFuture<GlideString> response = service.lcs(key1, key2);
GlideString payload = response.get();

// verify
assertEquals(testResponse, response);
assertEquals(value, payload);
}

@SneakyThrows
@Test
public void lcs_with_len_option() {
Expand Down
32 changes: 31 additions & 1 deletion java/integTest/src/test/java/glide/SharedCommandTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ public void mset_mget_binary(BaseClient client) {
String key2 = UUID.randomUUID().toString();
String key3 = UUID.randomUUID().toString();
String value = UUID.randomUUID().toString();
Map<String, String> keyValueMap = Map.of(key1, value, key2, value, key3, value);
Map<GlideString, GlideString> keyValueMap = Map.of(gs(key1), gs(value), gs(key2), gs(value), gs(key3), gs(value));
Yury-Fridlyand marked this conversation as resolved.
Show resolved Hide resolved

assertEquals(OK, client.mset(keyValueMap).get());
assertArrayEquals(
Expand Down Expand Up @@ -6690,6 +6690,36 @@ public void lcs(BaseClient client) {
assertInstanceOf(RequestException.class, executionException.getCause());
}

@SneakyThrows
@ParameterizedTest(autoCloseArguments = false)
@MethodSource("getClients")
public void lcs_binary(BaseClient client) {
assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"), "This feature added in redis 7.0.0");
// setup
GlideString key1 = gs("{key}-1" + UUID.randomUUID());
GlideString key2 = gs("{key}-2" + UUID.randomUUID());
GlideString key3 = gs("{key}-3" + UUID.randomUUID());
GlideString nonStringKey = gs("{key}-4" + UUID.randomUUID());

// keys does not exist or is empty
assertEquals(gs(""), client.lcs(key1, key2).get());

// setting string values
client.set(key1, gs("abcd"));
client.set(key2, gs("bcde"));
client.set(key3, gs("wxyz"));

// getting the lcs
assertEquals(gs(""), client.lcs(key1, key3).get());
assertEquals(gs("bcd"), client.lcs(key1, key2).get());

// non set keys are used
client.sadd(nonStringKey, new GlideString[] {gs("setmember")}).get();
ExecutionException executionException =
assertThrows(ExecutionException.class, () -> client.lcs(nonStringKey, key1).get());
assertInstanceOf(RequestException.class, executionException.getCause());
}

@SneakyThrows
@ParameterizedTest(autoCloseArguments = false)
@MethodSource("getClients")
Expand Down
Loading