Skip to content

Commit a422b24

Browse files
Java: Add hsetnx command. (Hash Command Group) (#162) (valkey-io#1211)
* Java: Add hsetnx command. (Hash Command Group) (#162) * Minor documentation update. * Convert booleans to Boolean and fix example Signed-off-by: Andrew Carbonetto <[email protected]> --------- Signed-off-by: Andrew Carbonetto <[email protected]> Co-authored-by: Andrew Carbonetto <[email protected]>
1 parent 5f0b8a3 commit a422b24

File tree

7 files changed

+109
-0
lines changed

7 files changed

+109
-0
lines changed

java/client/src/main/java/glide/api/BaseClient.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
1414
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
1515
import static redis_request.RedisRequestOuterClass.RequestType.GetString;
16+
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
1617
import static redis_request.RedisRequestOuterClass.RequestType.HashDel;
1718
import static redis_request.RedisRequestOuterClass.RequestType.HashExists;
1819
import static redis_request.RedisRequestOuterClass.RequestType.HashGet;
@@ -342,6 +343,13 @@ public CompletableFuture<Long> hset(
342343
return commandManager.submitNewCommand(HashSet, args, this::handleLongResponse);
343344
}
344345

346+
@Override
347+
public CompletableFuture<Boolean> hsetnx(
348+
@NonNull String key, @NonNull String field, @NonNull String value) {
349+
return commandManager.submitNewCommand(
350+
HSetNX, new String[] {key, field, value}, this::handleBooleanResponse);
351+
}
352+
345353
@Override
346354
public CompletableFuture<Long> hdel(@NonNull String key, @NonNull String[] fields) {
347355
String[] args = ArrayUtils.addFirst(fields, key);

java/client/src/main/java/glide/api/commands/HashBaseCommands.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,29 @@ public interface HashBaseCommands {
4747
*/
4848
CompletableFuture<Long> hset(String key, Map<String, String> fieldValueMap);
4949

50+
/**
51+
* Sets <code>field</code> in the hash stored at <code>key</code> to <code>value</code>, only if
52+
* <code>field</code> does not yet exist.<br>
53+
* If <code>key</code> does not exist, a new key holding a hash is created.<br>
54+
* If <code>field</code> already exists, this operation has no effect.
55+
*
56+
* @see <a href="https://redis.io/commands/hsetnx/">redis.io</a> for details.
57+
* @param key The key of the hash.
58+
* @param field The field to set the value for.
59+
* @param value The value to set.
60+
* @return <code>true</code> if the field was set, <code>false</code> if the field already existed
61+
* and was not set.
62+
* @example
63+
* <pre>{@code
64+
* Boolean payload1 = client.hsetnx("myHash", "field", "value").get();
65+
* assert payload1; // Indicates that the field "field" was set successfully in the hash "myHash".
66+
*
67+
* Boolean payload2 = client.hsetnx("myHash", "field", "newValue").get();
68+
* assert !payload2; // Indicates that the field "field" already existed in the hash "myHash" and was not set again.
69+
* }</pre>
70+
*/
71+
CompletableFuture<Boolean> hsetnx(String key, String field, String value);
72+
5073
/**
5174
* Removes the specified fields from the hash stored at <code>key</code>. Specified fields that do
5275
* not exist within this hash are ignored.

java/client/src/main/java/glide/api/models/BaseTransaction.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
2222
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
2323
import static redis_request.RedisRequestOuterClass.RequestType.GetString;
24+
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
2425
import static redis_request.RedisRequestOuterClass.RequestType.HashDel;
2526
import static redis_request.RedisRequestOuterClass.RequestType.HashExists;
2627
import static redis_request.RedisRequestOuterClass.RequestType.HashGet;
@@ -421,6 +422,26 @@ public T hset(@NonNull String key, @NonNull Map<String, String> fieldValueMap) {
421422
return getThis();
422423
}
423424

425+
/**
426+
* Sets <code>field</code> in the hash stored at <code>key</code> to <code>value</code>, only if
427+
* <code>field</code> does not yet exist.<br>
428+
* If <code>key</code> does not exist, a new key holding a hash is created.<br>
429+
* If <code>field</code> already exists, this operation has no effect.
430+
*
431+
* @see <a href="https://redis.io/commands/hsetnx/">redis.io</a> for details.
432+
* @param key The key of the hash.
433+
* @param field The field to set the value for.
434+
* @param value The value to set.
435+
* @return Command Response - <code>true</code> if the field was set, <code>false</code> if the
436+
* field already existed and was not set.
437+
*/
438+
public T hsetnx(@NonNull String key, @NonNull String field, @NonNull String value) {
439+
ArgsArray commandArgs = buildArgs(key, field, value);
440+
441+
protobufTransaction.addCommands(buildCommand(HSetNX, commandArgs));
442+
return getThis();
443+
}
444+
424445
/**
425446
* Removes the specified fields from the hash stored at <code>key</code>. Specified fields that do
426447
* not exist within this hash are ignored.

java/client/src/test/java/glide/api/RedisClientTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import static org.junit.jupiter.api.Assertions.assertNotNull;
1414
import static org.junit.jupiter.api.Assertions.assertNull;
1515
import static org.junit.jupiter.api.Assertions.assertThrows;
16+
import static org.junit.jupiter.api.Assertions.assertTrue;
1617
import static org.mockito.ArgumentMatchers.any;
1718
import static org.mockito.ArgumentMatchers.eq;
1819
import static org.mockito.Mockito.mock;
@@ -32,6 +33,7 @@
3233
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
3334
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
3435
import static redis_request.RedisRequestOuterClass.RequestType.GetString;
36+
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
3537
import static redis_request.RedisRequestOuterClass.RequestType.HashDel;
3638
import static redis_request.RedisRequestOuterClass.RequestType.HashExists;
3739
import static redis_request.RedisRequestOuterClass.RequestType.HashGet;
@@ -999,6 +1001,31 @@ public void hset_success() {
9991001
assertEquals(value, payload);
10001002
}
10011003

1004+
@SneakyThrows
1005+
@Test
1006+
public void hsetnx_success() {
1007+
// setup
1008+
String key = "testKey";
1009+
String field = "testField";
1010+
String value = "testValue";
1011+
String[] args = new String[] {key, field, value};
1012+
1013+
CompletableFuture<Boolean> testResponse = new CompletableFuture<>();
1014+
testResponse.complete(Boolean.TRUE);
1015+
1016+
// match on protobuf request
1017+
when(commandManager.<Boolean>submitNewCommand(eq(HSetNX), eq(args), any()))
1018+
.thenReturn(testResponse);
1019+
1020+
// exercise
1021+
CompletableFuture<Boolean> response = service.hsetnx(key, field, value);
1022+
Boolean payload = response.get();
1023+
1024+
// verify
1025+
assertEquals(testResponse, response);
1026+
assertTrue(payload);
1027+
}
1028+
10021029
@SneakyThrows
10031030
@Test
10041031
public void hdel_success() {
@@ -1010,6 +1037,8 @@ public void hdel_success() {
10101037

10111038
CompletableFuture testResponse = mock(CompletableFuture.class);
10121039
when(testResponse.get()).thenReturn(value);
1040+
1041+
// match on protobuf request
10131042
when(commandManager.<Long>submitNewCommand(eq(HashDel), eq(args), any()))
10141043
.thenReturn(testResponse);
10151044

java/client/src/test/java/glide/api/models/TransactionTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
2020
import static redis_request.RedisRequestOuterClass.RequestType.ExpireAt;
2121
import static redis_request.RedisRequestOuterClass.RequestType.GetString;
22+
import static redis_request.RedisRequestOuterClass.RequestType.HSetNX;
2223
import static redis_request.RedisRequestOuterClass.RequestType.HashDel;
2324
import static redis_request.RedisRequestOuterClass.RequestType.HashExists;
2425
import static redis_request.RedisRequestOuterClass.RequestType.HashGet;
@@ -164,6 +165,12 @@ public void transaction_builds_protobuf_request(BaseTransaction<?> transaction)
164165
HashSet,
165166
ArgsArray.newBuilder().addArgs("key").addArgs("field").addArgs("value").build()));
166167

168+
transaction.hsetnx("key", "field", "value");
169+
results.add(
170+
Pair.of(
171+
HSetNX,
172+
ArgsArray.newBuilder().addArgs("key").addArgs("field").addArgs("value").build()));
173+
167174
transaction.hget("key", "field");
168175
results.add(Pair.of(HashGet, ArgsArray.newBuilder().addArgs("key").addArgs("field").build()));
169176

java/integTest/src/test/java/glide/SharedCommandTests.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,25 @@ public void hset_hget_existing_fields_non_existing_fields(BaseClient client) {
455455
assertNull(client.hget(key, "non_existing_field").get());
456456
}
457457

458+
@SneakyThrows
459+
@ParameterizedTest
460+
@MethodSource("getClients")
461+
public void hsetnx(BaseClient client) {
462+
String key1 = UUID.randomUUID().toString();
463+
String key2 = UUID.randomUUID().toString();
464+
String field = UUID.randomUUID().toString();
465+
466+
assertTrue(client.hsetnx(key1, field, "value").get());
467+
assertFalse(client.hsetnx(key1, field, "newValue").get());
468+
assertEquals("value", client.hget(key1, field).get());
469+
470+
// Key exists, but it is not a hash
471+
assertEquals(OK, client.set(key2, "value").get());
472+
ExecutionException executionException =
473+
assertThrows(ExecutionException.class, () -> client.hsetnx(key2, field, "value").get());
474+
assertTrue(executionException.getCause() instanceof RequestException);
475+
}
476+
458477
@SneakyThrows
459478
@ParameterizedTest
460479
@MethodSource("getClients")

java/integTest/src/test/java/glide/TransactionTestUtilities.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public static BaseTransaction<?> transactionTest(BaseTransaction<?> baseTransact
6161
baseTransaction.hset(key4, Map.of(field1, value1, field2, value2));
6262
baseTransaction.hget(key4, field1);
6363
baseTransaction.hexists(key4, field2);
64+
baseTransaction.hsetnx(key4, field1, value1);
6465
baseTransaction.hmget(key4, new String[] {field1, "non_existing_field", field2});
6566
baseTransaction.hgetall(key4);
6667
baseTransaction.hdel(key4, new String[] {field1});
@@ -132,6 +133,7 @@ public static Object[] transactionTestResult() {
132133
2L,
133134
value1,
134135
true,
136+
Boolean.FALSE, // hsetnx(key4, field1, value1)
135137
new String[] {value1, null, value2},
136138
Map.of(field1, value1, field2, value2),
137139
1L,

0 commit comments

Comments
 (0)