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 HRANDFIELD command. #1455

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions java/client/src/main/java/glide/api/BaseClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.HKeys;
import static redis_request.RedisRequestOuterClass.RequestType.HLen;
import static redis_request.RedisRequestOuterClass.RequestType.HMGet;
import static redis_request.RedisRequestOuterClass.RequestType.HRandField;
import static redis_request.RedisRequestOuterClass.RequestType.HSet;
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
import static redis_request.RedisRequestOuterClass.RequestType.HVals;
Expand Down Expand Up @@ -526,6 +527,29 @@ public CompletableFuture<String[]> hkeys(@NonNull String key) {
response -> castArray(handleArrayResponse(response), String.class));
}

@Override
public CompletableFuture<String> hrandfield(@NonNull String key) {
return commandManager.submitNewCommand(
HRandField, new String[] {key}, this::handleStringOrNullResponse);
}

@Override
public CompletableFuture<String[]> hrandfieldWithCount(@NonNull String key, long count) {
return commandManager.submitNewCommand(
HRandField,
new String[] {key, Long.toString(count)},
response -> castArray(handleArrayResponse(response), String.class));
}

@Override
public CompletableFuture<String[][]> hrandfieldWithCountWithValues(
@NonNull String key, long count) {
return commandManager.submitNewCommand(
HRandField,
new String[] {key, Long.toString(count), WITH_VALUES_REDIS_API},
response -> castArrayofArrays(handleArrayResponse(response), String.class));
}

@Override
public CompletableFuture<Long> lpush(@NonNull String key, @NonNull String[] elements) {
String[] arguments = ArrayUtils.addFirst(elements, key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* @see <a href="https://redis.io/commands/?group=hash">Hash Commands</a>
*/
public interface HashBaseCommands {
/** Redis API keyword used to query hash members with their values. */
String WITH_VALUES_REDIS_API = "WITHVALUES";

/**
* Retrieves the value associated with <code>field</code> in the hash stored at <code>key</code>.
Expand Down Expand Up @@ -234,4 +236,62 @@ public interface HashBaseCommands {
* }</pre>
*/
CompletableFuture<String[]> hkeys(String key);

/**
* Returns a random field name from the hash value stored at <code>key</code>.
*
* @since Redis 6.2 and above.
* @see <a href="https://redis.io/commands/hrandfield/">redis.io</a> for details.
* @param key The key of the hash.
* @return A random field name from the hash stored at <code>key</code>, or <code>null</code> when
* the key does not exist.
* @example
* <pre>{@code
* String field = client.hrandfield("my_hash").get();
* System.out.printf("A random field from the hash is '%s'", field);
* }</pre>
*/
CompletableFuture<String> hrandfield(String key);

/**
* Retrieves up to <code>count</code> random field names from the hash value stored at <code>key
* </code>.
*
* @since Redis 6.2 and above.
* @see <a href="https://redis.io/commands/hrandfield/">redis.io</a> for details.
* @param key The key of the hash.
* @param count The number of field names to return.<br>
* If <code>count</code> is positive, returns unique elements.<br>
* If negative, allows for duplicates.
* @return An <code>array</code> of random field names from the hash stored at <code>key</code>,
* or an <code>empty array</code> when the key does not exist.
* @example
* <pre>{@code
* String[] fields = client.hrandfieldWithCount("my_hash", 10).get();
* System.out.printf("Random fields from the hash are '%s'", String.join(", ", fields));
* }</pre>
*/
CompletableFuture<String[]> hrandfieldWithCount(String key, long count);

/**
* Retrieves up to <code>count</code> random field names along with their values from the hash
* value stored at <code>key</code>.
*
* @since Redis 6.2 and above.
* @see <a href="https://redis.io/commands/hrandfield/">redis.io</a> for details.
* @param key The key of the hash.
* @param count The number of field names to return.<br>
* If <code>count</code> is positive, returns unique elements.<br>
* If negative, allows for duplicates.
* @return A 2D <code>array</code> of <code>[fieldName, value]</code> <code>arrays</code>, where
* <code>fieldName</code> is a random field name from the hash and <code>value</code> is the
* associated value of the field name.<br>
* If the hash does not exist or is empty, the response will be an empty <code>array</code>.
* @example
* <pre>{@code
* String[][] fields = client.hrandfieldWithCountWithValues("my_hash", 1).get();
* System.out.printf("A random field from the hash is '%s' and the value is '%s'", fields[0][0], fields[0][1]);
* }</pre>
*/
CompletableFuture<String[][]> hrandfieldWithCountWithValues(String key, long count);
}
55 changes: 55 additions & 0 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.models;

import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API;
import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.COUNT_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API;
Expand Down Expand Up @@ -46,6 +47,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.HKeys;
import static redis_request.RedisRequestOuterClass.RequestType.HLen;
import static redis_request.RedisRequestOuterClass.RequestType.HMGet;
import static redis_request.RedisRequestOuterClass.RequestType.HRandField;
import static redis_request.RedisRequestOuterClass.RequestType.HSet;
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
import static redis_request.RedisRequestOuterClass.RequestType.HVals;
Expand Down Expand Up @@ -698,6 +700,59 @@ public T hkeys(@NonNull String key) {
return getThis();
}

/**
* Returns a random field name from the hash value stored at <code>key</code>.
*
* @since Redis 6.2 and above.
* @see <a href="https://redis.io/commands/hrandfield/">redis.io</a> for details.
* @param key The key of the hash.
* @return Command Response - A random field name from the hash stored at <code>key</code>, or
* <code>null</code> when the key does not exist.
*/
public T hrandfield(@NonNull String key) {
protobufTransaction.addCommands(buildCommand(HRandField, buildArgs(key)));
return getThis();
}

/**
* Retrieves up to <code>count</code> random field names from the hash value stored at <code>key
* </code>.
*
* @since Redis 6.2 and above.
* @see <a href="https://redis.io/commands/hrandfield/">redis.io</a> for details.
* @param key The key of the hash.
* @param count The number of field names to return.<br>
* If <code>count</code> is positive, returns unique elements.<br>
* If negative, allows for duplicates.
* @return Command Response - An <code>array</code> of random field names from the hash stored at
* <code>key</code>, or an <code>empty array</code> when the key does not exist.
*/
public T hrandfieldWithCount(@NonNull String key, long count) {
protobufTransaction.addCommands(buildCommand(HRandField, buildArgs(key, Long.toString(count))));
return getThis();
}

/**
* Retrieves up to <code>count</code> random field names along with their values from the hash
* value stored at <code>key</code>.
*
* @since Redis 6.2 and above.
* @see <a href="https://redis.io/commands/hrandfield/">redis.io</a> for details.
* @param key The key of the hash.
* @param count The number of field names to return.<br>
* If <code>count</code> is positive, returns unique elements.<br>
* If negative, allows for duplicates.
* @return Command Response - A 2D <code>array</code> of <code>[fieldName, value]</code> <code>
* arrays</code>, where <code>fieldName</code> is a random field name from the hash and <code>
* value</code> is the associated value of the field name.<br>
* If the hash does not exist or is empty, the response will be an empty <code>array</code>.
*/
public T hrandfieldWithCountWithValues(@NonNull String key, long count) {
ArgsArray commandArgs = buildArgs(key, Long.toString(count), WITH_VALUES_REDIS_API);
protobufTransaction.addCommands(buildCommand(HRandField, commandArgs));
return getThis();
}

/**
* Inserts all the specified values at the head of the list stored at <code>key</code>. <code>
* elements</code> are inserted one after the other to the head of the list, from the leftmost
Expand Down
74 changes: 74 additions & 0 deletions java/client/src/test/java/glide/api/RedisClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package glide.api;

import static glide.api.BaseClient.OK;
import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API;
import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API;
Expand Down Expand Up @@ -67,6 +68,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.HKeys;
import static redis_request.RedisRequestOuterClass.RequestType.HLen;
import static redis_request.RedisRequestOuterClass.RequestType.HMGet;
import static redis_request.RedisRequestOuterClass.RequestType.HRandField;
import static redis_request.RedisRequestOuterClass.RequestType.HSet;
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
import static redis_request.RedisRequestOuterClass.RequestType.HVals;
Expand Down Expand Up @@ -1412,6 +1414,78 @@ public void hkeys_returns_success() {
assertEquals(values, payload);
}

@SneakyThrows
@Test
public void hrandfield_returns_success() {
// setup
String key = "testKey";
String[] args = {key};
String field = "field";

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

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

// exercise
CompletableFuture<String> response = service.hrandfield(key);
String payload = response.get();

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

@SneakyThrows
@Test
public void hrandfieldWithCount_returns_success() {
// setup
String key = "testKey";
String[] args = {key, "2"};
String[] fields = new String[] {"field_1", "field_2"};

CompletableFuture<String[]> testResponse = new CompletableFuture<>();
testResponse.complete(fields);

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

// exercise
CompletableFuture<String[]> response = service.hrandfieldWithCount(key, 2);
String[] payload = response.get();

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

@SneakyThrows
@Test
public void hrandfieldWithCountWithValues_returns_success() {
// setup
String key = "testKey";
String[] args = {key, "2", WITH_VALUES_REDIS_API};
String[][] fields = new String[][] {{"field_1", "value_1"}, {"field_2", "value_2"}};

CompletableFuture<String[][]> testResponse = new CompletableFuture<>();
testResponse.complete(fields);

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

// exercise
CompletableFuture<String[][]> response = service.hrandfieldWithCountWithValues(key, 2);
String[][] payload = response.get();

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

@SneakyThrows
@Test
public void lpush_returns_success() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */
package glide.api.models;

import static glide.api.commands.HashBaseCommands.WITH_VALUES_REDIS_API;
import static glide.api.commands.ServerManagementCommands.VERSION_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.WITH_SCORES_REDIS_API;
import static glide.api.commands.SortedSetBaseCommands.WITH_SCORE_REDIS_API;
Expand Down Expand Up @@ -57,6 +58,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.HKeys;
import static redis_request.RedisRequestOuterClass.RequestType.HLen;
import static redis_request.RedisRequestOuterClass.RequestType.HMGet;
import static redis_request.RedisRequestOuterClass.RequestType.HRandField;
import static redis_request.RedisRequestOuterClass.RequestType.HSet;
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
import static redis_request.RedisRequestOuterClass.RequestType.HVals;
Expand Down Expand Up @@ -277,6 +279,14 @@ public void transaction_builds_protobuf_request(BaseTransaction<?> transaction)
transaction.hkeys("key");
results.add(Pair.of(HKeys, buildArgs("key")));

transaction
.hrandfield("key")
.hrandfieldWithCount("key", 2)
.hrandfieldWithCountWithValues("key", 3);
results.add(Pair.of(HRandField, buildArgs("key")));
results.add(Pair.of(HRandField, buildArgs("key", "2")));
results.add(Pair.of(HRandField, buildArgs("key", "3", WITH_VALUES_REDIS_API)));

transaction.lpush("key", new String[] {"element1", "element2"});
results.add(Pair.of(LPush, buildArgs("key", "element1", "element2")));

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

@SneakyThrows
@ParameterizedTest(autoCloseArguments = false)
@MethodSource("getClients")
public void hrandfield(BaseClient client) {
String key1 = UUID.randomUUID().toString();
String key2 = UUID.randomUUID().toString();

// key does not exist
assertNull(client.hrandfield(key1).get());
assertEquals(0, client.hrandfieldWithCount(key1, 5).get().length);
assertEquals(0, client.hrandfieldWithCountWithValues(key1, 5).get().length);

var data = Map.of("f 1", "v 1", "f 2", "v 2", "f 3", "v 3");
assertEquals(3, client.hset(key1, data).get());

// random key
assertTrue(data.containsKey(client.hrandfield(key1).get()));

// WithCount - positive count
var keys = client.hrandfieldWithCount(key1, 5).get();
assertEquals(data.keySet().size(), keys.length);
assertEquals(data.keySet(), Set.of(keys));

// WithCount - negative count
keys = client.hrandfieldWithCount(key1, -5).get();
assertEquals(5, keys.length);
Arrays.stream(keys).forEach(key -> assertTrue(data.containsKey(key)));

// WithCountWithValues - positive count
var keysWithValues = client.hrandfieldWithCountWithValues(key1, 5).get();
assertEquals(data.keySet().size(), keysWithValues.length);
for (var pair : keysWithValues) {
assertEquals(data.get(pair[0]), pair[1]);
}

// WithCountWithValues - negative count
keysWithValues = client.hrandfieldWithCountWithValues(key1, -5).get();
assertEquals(5, keysWithValues.length);
for (var pair : keysWithValues) {
assertEquals(data.get(pair[0]), pair[1]);
}

// Key exists, but it is not a List
assertEquals(OK, client.set(key2, "value").get());
Exception executionException =
assertThrows(ExecutionException.class, () -> client.hrandfield(key2).get());
assertInstanceOf(RequestException.class, executionException.getCause());
executionException =
assertThrows(ExecutionException.class, () -> client.hrandfieldWithCount(key2, 2).get());
assertInstanceOf(RequestException.class, executionException.getCause());
executionException =
assertThrows(
ExecutionException.class, () -> client.hrandfieldWithCountWithValues(key2, 3).get());
assertInstanceOf(RequestException.class, executionException.getCause());
}

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