Skip to content

Commit 76c6a4e

Browse files
authored
Java: JSON.ARRINDEX (#2546)
* add JSON.ARRINDEX to Java commands --------- Signed-off-by: Chloe <[email protected]> Signed-off-by: Chloe Yip <[email protected]>
1 parent 7cb459d commit 76c6a4e

File tree

4 files changed

+313
-0
lines changed

4 files changed

+313
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* Node: Added `FT.AGGREGATE` ([#2554](https://github.com/valkey-io/valkey-glide/pull/2554))
3232
* Java: Added `JSON.DEBUG` ([#2520](https://github.com/valkey-io/valkey-glide/pull/2520))
3333
* Java: Added `JSON.ARRINSERT` and `JSON.ARRLEN` ([#2476](https://github.com/valkey-io/valkey-glide/pull/2476))
34+
* Java: Added `JSON.ARRINDEX` ([#2546](https://github.com/valkey-io/valkey-glide/pull/2546))
3435
* Java: Added `JSON.ARRPOP` ([#2486](https://github.com/valkey-io/valkey-glide/pull/2486))
3536
* Java: Added `JSON.OBJLEN` and `JSON.OBJKEYS` ([#2492](https://github.com/valkey-io/valkey-glide/pull/2492))
3637
* Java: Added `JSON.DEL` and `JSON.FORGET` ([#2490](https://github.com/valkey-io/valkey-glide/pull/2490))

java/client/src/main/java/glide/api/commands/servermodules/Json.java

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import glide.api.models.ClusterValue;
1111
import glide.api.models.GlideString;
1212
import glide.api.models.commands.ConditionalChange;
13+
import glide.api.models.commands.json.JsonArrindexOptions;
1314
import glide.api.models.commands.json.JsonGetOptions;
1415
import glide.api.models.commands.json.JsonGetOptionsBinary;
1516
import glide.utils.ArgsBuilder;
@@ -26,6 +27,7 @@ public class Json {
2627
private static final String JSON_NUMMULTBY = JSON_PREFIX + "NUMMULTBY";
2728
private static final String JSON_ARRAPPEND = JSON_PREFIX + "ARRAPPEND";
2829
private static final String JSON_ARRINSERT = JSON_PREFIX + "ARRINSERT";
30+
private static final String JSON_ARRINDEX = JSON_PREFIX + "ARRINDEX";
2931
private static final String JSON_ARRLEN = JSON_PREFIX + "ARRLEN";
3032
private static final String[] JSON_DEBUG_MEMORY = new String[] {JSON_PREFIX + "DEBUG", "MEMORY"};
3133
private static final String[] JSON_DEBUG_FIELDS = new String[] {JSON_PREFIX + "DEBUG", "FIELDS"};
@@ -604,6 +606,171 @@ public static CompletableFuture<Object> arrinsert(
604606
.toArray());
605607
}
606608

609+
/**
610+
* Searches for the first occurrence of a <code>scalar</code> JSON value in the arrays at the
611+
* path.
612+
*
613+
* @param client The client to execute the command.
614+
* @param key The key of the JSON document.
615+
* @param path The path within the JSON document.
616+
* @param scalar The scalar value to search for.
617+
* @return
618+
* <ul>
619+
* <li>For JSONPath (<code>path</code> starts with <code>$</code>): Returns an array with a
620+
* list of integers for every possible path, indicating the index of the matching
621+
* element. The value is <code>-1</code> if not found. If a value is not an array, its
622+
* corresponding return value is <code>null</code>.
623+
* <li>For legacy path (path doesn't start with <code>$</code>): Returns an integer
624+
* representing the index of matching element, or <code>-1</code> if not found. If the
625+
* value at the <code>path</code> is not an array, an error is raised.
626+
* </ul>
627+
*
628+
* @example
629+
* <pre>{@code
630+
* Json.set(client, key, "$", "{\"a\": [\"value\", 3], \"b\": {\"a\": [3, [\"value\", false], 5]}}").get();
631+
* var result = Json.arrindex(client, key, "$..a", "3").get();
632+
* assert Arrays.equals((Object[]) result, new Object[] {1L, 0L});
633+
*
634+
* result = Json.arrindex(client, key, "$..a", "\"value\"").get();
635+
* assert Arrays.equals((Object[]) result, new Object[] {0L, -1L});
636+
* }</pre>
637+
*/
638+
public static CompletableFuture<Object> arrindex(
639+
@NonNull BaseClient client,
640+
@NonNull String key,
641+
@NonNull String path,
642+
@NonNull String scalar) {
643+
return arrindex(client, gs(key), gs(path), gs(scalar));
644+
}
645+
646+
/**
647+
* Searches for the first occurrence of a <code>scalar</code> JSON value in the arrays at the
648+
* path.
649+
*
650+
* @param client The client to execute the command.
651+
* @param key The key of the JSON document.
652+
* @param path The path within the JSON document.
653+
* @param scalar The scalar value to search for.
654+
* @return
655+
* <ul>
656+
* <li>For JSONPath (<code>path</code> starts with <code>$</code>): Returns an array with a
657+
* list of integers for every possible path, indicating the index of the matching
658+
* element. The value is <code>-1</code> if not found. If a value is not an array, its
659+
* corresponding return value is <code>null</code>.
660+
* <li>For legacy path (path doesn't start with <code>$</code>): Returns an integer
661+
* representing the index of matching element, or <code>-1</code> if not found. If the
662+
* value at the <code>path</code> is not an array, an error is raised.
663+
* </ul>
664+
*
665+
* @example
666+
* <pre>{@code
667+
* Json.set(client, key, "$", "{\"a\": [\"value\", 3], \"b\": {\"a\": [3, [\"value\", false], 5]}}").get();
668+
* var result = Json.arrindex(client, gs(key), gs("$..a"), gs("3")).get();
669+
* assert Arrays.equals((Object[]) result, new Object[] {1L, 0L});
670+
*
671+
* // Searches for the first occurrence of null in the arrays
672+
* result = Json.arrindex(client, gs(key), gs("$..a"), gs("null")).get();
673+
* assert Arrays.equals((Object[]) result, new Object[] {-1L, -1L});
674+
* }</pre>
675+
*/
676+
public static CompletableFuture<Object> arrindex(
677+
@NonNull BaseClient client,
678+
@NonNull GlideString key,
679+
@NonNull GlideString path,
680+
@NonNull GlideString scalar) {
681+
return executeCommand(client, new GlideString[] {gs(JSON_ARRINDEX), key, path, scalar});
682+
}
683+
684+
/**
685+
* Searches for the first occurrence of a <code>scalar</code> JSON value in the arrays at the
686+
* path.
687+
*
688+
* @param client The client to execute the command.
689+
* @param key The key of the JSON document.
690+
* @param path The path within the JSON document.
691+
* @param scalar The scalar value to search for.
692+
* @param options The additional options for the command. See <code>JsonArrindexOptions</code>.
693+
* @return
694+
* <ul>
695+
* <li>For JSONPath (<code>path</code> starts with <code>$</code>): Returns an array with a
696+
* list of integers for every possible path, indicating the index of the matching
697+
* element. The value is <code>-1</code> if not found. If a value is not an array, its
698+
* corresponding return value is <code>null</code>.
699+
* <li>For legacy path (path doesn't start with <code>$</code>): Returns an integer
700+
* representing the index of matching element, or <code>-1</code> if not found. If the
701+
* value at the <code>path</code> is not an array, an error is raised.
702+
* </ul>
703+
*
704+
* @example
705+
* <pre>{@code
706+
* Json.set(client, key, "$", "{\"a\": [\"value\", 3], \"b\": {\"a\": [3, [\"value\", false], 5]}}").get();
707+
* var result = Json.arrindex(client, key, ".a", "3", new JsonArrindexOptions(0L)).get();
708+
* assert Arrays.equals(1L, result);
709+
* }</pre>
710+
*/
711+
public static CompletableFuture<Object> arrindex(
712+
@NonNull BaseClient client,
713+
@NonNull String key,
714+
@NonNull String path,
715+
@NonNull String scalar,
716+
@NonNull JsonArrindexOptions options) {
717+
718+
return executeCommand(
719+
client,
720+
new ArgsBuilder()
721+
.add(JSON_ARRINDEX)
722+
.add(key)
723+
.add(path)
724+
.add(scalar)
725+
.add(options.toArgs())
726+
.toArray());
727+
}
728+
729+
/**
730+
* Searches for the first occurrence of a <code>scalar</code> JSON value in the arrays at the
731+
* path.
732+
*
733+
* @param client The client to execute the command.
734+
* @param key The key of the JSON document.
735+
* @param path The path within the JSON document.
736+
* @param scalar The scalar value to search for.
737+
* @param options The additional options for the command. See <code>JsonArrindexOptions</code>.
738+
* @return
739+
* <ul>
740+
* <li>For JSONPath (<code>path</code> starts with <code>$</code>): Returns an array with a
741+
* list of integers for every possible path, indicating the index of the matching
742+
* element. The value is <code>-1</code> if not found. If a value is not an array, its
743+
* corresponding return value is <code>null</code>..
744+
* <li>For legacy path (path doesn't start with <code>$</code>): Returns an integer
745+
* representing the index of matching element, or <code>-1</code> if not found. If the
746+
* value at the <code>path</code> is not an array, an error is raised.
747+
* </ul>
748+
*
749+
* @example
750+
* <pre>{@code
751+
* Json.set(client, key, "$", "{\"a\": [\"value\", 3], \"b\": {\"a\": [3, [\"value\", false], 5]}}").get();
752+
* var result = Json.arrindex(client, gs(key), gs(".a"), gs("3"), new JsonArrindexOptions(0L)).get();
753+
* assert Arrays.equals(1L, result);
754+
* }</pre>
755+
*/
756+
public static CompletableFuture<Object> arrindex(
757+
@NonNull BaseClient client,
758+
@NonNull GlideString key,
759+
@NonNull GlideString path,
760+
@NonNull GlideString scalar,
761+
@NonNull JsonArrindexOptions options) {
762+
763+
return executeCommand(
764+
client,
765+
new ArgsBuilder()
766+
.add(JSON_ARRINDEX)
767+
.add(key)
768+
.add(path)
769+
.add(scalar)
770+
.add(options.toArgs())
771+
.toArray());
772+
}
773+
607774
/**
608775
* Retrieves the length of the array at the specified <code>path</code> within the JSON document
609776
* stored at <code>key</code>.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */
2+
package glide.api.models.commands.json;
3+
4+
import glide.api.commands.servermodules.Json;
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
/** Additional parameters for {@link Json#arrindex} command. */
9+
public final class JsonArrindexOptions {
10+
11+
/** The start index, inclusive. Default to <code>0</code>. */
12+
private Long start;
13+
14+
/** The end index, exclusive. */
15+
private Long end;
16+
17+
/**
18+
* Search using a start index (is inclusive). Defaults to <code>0</code> if not provided. Indices
19+
* that exceed the array bounds are automatically adjusted to the nearest valid position.
20+
*/
21+
public JsonArrindexOptions(Long start) {
22+
this.start = start;
23+
}
24+
25+
/**
26+
* Search using a start index (is inclusive) and end index (is exclusive). If <code>start</code>
27+
* is greater than <code>end</code>, the command returns <code>-1</code> to indicate that the
28+
* value was not found. Indices that exceed the array bounds are automatically adjusted to the
29+
* nearest valid position.
30+
*/
31+
public JsonArrindexOptions(Long start, Long end) {
32+
this.start = start;
33+
this.end = end;
34+
}
35+
36+
/**
37+
* Converts JsonArrindexOptions into a String[].
38+
*
39+
* @return String[]
40+
*/
41+
public String[] toArgs() {
42+
List<String> args = new ArrayList<>();
43+
44+
if (start != null) {
45+
args.add(start.toString());
46+
47+
if (end != null) {
48+
args.add(end.toString());
49+
}
50+
}
51+
52+
return args.toArray(new String[0]);
53+
}
54+
}

java/integTest/src/test/java/glide/modules/JsonTests.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import glide.api.models.commands.ConditionalChange;
2121
import glide.api.models.commands.FlushMode;
2222
import glide.api.models.commands.InfoOptions.Section;
23+
import glide.api.models.commands.json.JsonArrindexOptions;
2324
import glide.api.models.commands.json.JsonGetOptions;
2425
import java.util.UUID;
2526
import java.util.concurrent.ExecutionException;
@@ -206,6 +207,96 @@ public void arrappend() {
206207
() -> Json.arrappend(client, "non_existing_key", ".b", new String[] {"\"six\""}).get());
207208
}
208209

210+
@Test
211+
@SneakyThrows
212+
public void arrindex() {
213+
String key1 = UUID.randomUUID().toString();
214+
String key2 = UUID.randomUUID().toString();
215+
String key3 = UUID.randomUUID().toString();
216+
217+
String doc1 =
218+
"{\"a\": [1, 3, true], \"b\": {\"a\": [3, 4, [\"value\", 3, false], 5], \"c\": {\"a\":"
219+
+ " 42}}}";
220+
221+
String doc2 =
222+
"{\"a\": [1, 3, true, \"foo\", \"meow\", \"m\", \"foo\", \"lol\", false], \"b\": {\"a\":"
223+
+ " [3, 4, [\"value\", 3, false], 5], \"c\": {\"a\": 42}, \"empty\": []}}";
224+
225+
String doc3 = "{\"a\": 123123}";
226+
227+
assertEquals("OK", Json.set(client, key1, "$", doc1).get());
228+
assertArrayEquals(
229+
new Object[] {2L, -1L, null}, (Object[]) Json.arrindex(client, key1, "$..a", "true").get());
230+
231+
assertArrayEquals(
232+
new Object[] {1L, 0L, null},
233+
(Object[]) Json.arrindex(client, gs(key1), gs("$..a"), gs("3")).get());
234+
235+
assertEquals("OK", Json.set(client, key2, "$", doc2).get());
236+
237+
assertArrayEquals(
238+
new Object[] {6L, -1L, null},
239+
(Object[])
240+
Json.arrindex(client, key2, "$..a", "\"foo\"", new JsonArrindexOptions(6L, 8L)).get());
241+
242+
assertArrayEquals(
243+
new Object[] {-1L, -1L, null},
244+
(Object[])
245+
Json.arrindex(client, key2, "$..a", "null", new JsonArrindexOptions(6L, 8L)).get());
246+
assertArrayEquals(
247+
new Object[] {-1L, -1L, null},
248+
(Object[])
249+
Json.arrindex(client, gs(key2), gs("$..a"), gs("null"), new JsonArrindexOptions(6L, 8L))
250+
.get());
251+
252+
assertArrayEquals(
253+
new Object[] {6L, -1L, null},
254+
(Object[])
255+
Json.arrindex(
256+
client, gs(key2), gs("$..a"), gs("\"foo\""), new JsonArrindexOptions(6L, 8L))
257+
.get());
258+
259+
assertArrayEquals(
260+
new Object[] {6L, -1L, null},
261+
(Object[])
262+
Json.arrindex(client, key2, "$..a", "\"foo\"", new JsonArrindexOptions(6L)).get());
263+
264+
// value doesn't exist
265+
assertArrayEquals(
266+
new Object[] {null},
267+
(Object[])
268+
Json.arrindex(client, key1, "$..b", "true", new JsonArrindexOptions(1L, 3L)).get());
269+
270+
// with legacy path
271+
assertEquals(2L, Json.arrindex(client, key1, ".a", "true").get());
272+
273+
// element doesn't exist
274+
assertEquals(-1L, Json.arrindex(client, key1, ".a", "\"nonexistent-element\"").get());
275+
276+
// empty array
277+
assertThrows(
278+
ExecutionException.class,
279+
() -> Json.arrindex(client, key1, ".empty", "\"nonexistent-element\"").get());
280+
281+
assertEquals("OK", Json.set(client, key3, "$", doc3).get());
282+
283+
// wrong type error
284+
assertThrows(ExecutionException.class, () -> Json.arrindex(client, key3, ".a", "42").get());
285+
286+
// JsonScalar is null
287+
assertThrows(ExecutionException.class, () -> Json.arrindex(client, key3, ".a", "null").get());
288+
289+
// start index is larger than the end index
290+
assertEquals(
291+
-1L, Json.arrindex(client, key2, ".a", "false", new JsonArrindexOptions(4L, 2L)).get());
292+
293+
// end index is larger than the length of the array
294+
assertEquals(
295+
8L,
296+
Json.arrindex(client, key2, ".a", "false", new JsonArrindexOptions(0L, 12378798798721L))
297+
.get());
298+
}
299+
209300
@Test
210301
@SneakyThrows
211302
public void arrinsert() {

0 commit comments

Comments
 (0)