diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b85345f69..3c7585a2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes - +* Java, Node, Python: Add transaction commands for JSON module ([#2862](https://github.com/valkey-io/valkey-glide/pull/2862)) * Go: Add HINCRBY command ([#2847](https://github.com/valkey-io/valkey-glide/pull/2847)) * Go: Add HINCRBYFLOAT command ([#2846](https://github.com/valkey-io/valkey-glide/pull/2846)) * Go: Add SUNIONSTORE command ([#2805](https://github.com/valkey-io/valkey-glide/pull/2805)) diff --git a/glide-core/redis-rs/redis/src/cluster_routing.rs b/glide-core/redis-rs/redis/src/cluster_routing.rs index 011f5e08e6..fe03d1e41a 100644 --- a/glide-core/redis-rs/redis/src/cluster_routing.rs +++ b/glide-core/redis-rs/redis/src/cluster_routing.rs @@ -672,7 +672,8 @@ fn base_routing(cmd: &[u8]) -> RouteBy { | b"OBJECT ENCODING" | b"OBJECT FREQ" | b"OBJECT IDLETIME" - | b"OBJECT REFCOUNT" => RouteBy::SecondArg, + | b"OBJECT REFCOUNT" + | b"JSON.DEBUG" => RouteBy::SecondArg, b"LMPOP" | b"SINTERCARD" | b"ZDIFF" | b"ZINTER" | b"ZINTERCARD" | b"ZMPOP" | b"ZUNION" => { RouteBy::SecondArgAfterKeyCount diff --git a/java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java b/java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java new file mode 100644 index 0000000000..32f19b45c1 --- /dev/null +++ b/java/client/src/main/java/glide/api/commands/servermodules/MultiJson.java @@ -0,0 +1,1205 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api.commands.servermodules; + +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; + +import glide.api.models.BaseTransaction; +import glide.api.models.Transaction; +import glide.api.models.commands.ConditionalChange; +import glide.api.models.commands.json.JsonArrindexOptions; +import glide.api.models.commands.json.JsonGetOptions; +import lombok.NonNull; + +/** + * Transaction implementation for JSON module. Transactions allow the execution of a group of + * commands in a single step. See {@link Transaction}. + * + * @example + *
{@code
+ * Transaction transaction = new Transaction();
+ * MultiJson.set(transaction, "doc", ".", "{\"a\": 1.0, \"b\": 2}");
+ * MultiJson.get(transaction, "doc");
+ * Object[] result = client.exec(transaction).get();
+ * assert result[0].equals("OK"); // result of MultiJson.set()
+ * assert result[1].equals("{\"a\": 1.0, \"b\": 2}"); // result of MultiJson.get()
+ * }
+ */ +public class MultiJson { + + private static final String JSON_PREFIX = "JSON."; + private static final String JSON_SET = JSON_PREFIX + "SET"; + private static final String JSON_GET = JSON_PREFIX + "GET"; + private static final String JSON_MGET = JSON_PREFIX + "MGET"; + private static final String JSON_NUMINCRBY = JSON_PREFIX + "NUMINCRBY"; + private static final String JSON_NUMMULTBY = JSON_PREFIX + "NUMMULTBY"; + private static final String JSON_ARRAPPEND = JSON_PREFIX + "ARRAPPEND"; + private static final String JSON_ARRINSERT = JSON_PREFIX + "ARRINSERT"; + private static final String JSON_ARRINDEX = JSON_PREFIX + "ARRINDEX"; + private static final String JSON_ARRLEN = JSON_PREFIX + "ARRLEN"; + private static final String[] JSON_DEBUG_MEMORY = new String[] {JSON_PREFIX + "DEBUG", "MEMORY"}; + private static final String[] JSON_DEBUG_FIELDS = new String[] {JSON_PREFIX + "DEBUG", "FIELDS"}; + private static final String JSON_ARRPOP = JSON_PREFIX + "ARRPOP"; + private static final String JSON_ARRTRIM = JSON_PREFIX + "ARRTRIM"; + private static final String JSON_OBJLEN = JSON_PREFIX + "OBJLEN"; + private static final String JSON_OBJKEYS = JSON_PREFIX + "OBJKEYS"; + private static final String JSON_DEL = JSON_PREFIX + "DEL"; + private static final String JSON_FORGET = JSON_PREFIX + "FORGET"; + private static final String JSON_TOGGLE = JSON_PREFIX + "TOGGLE"; + private static final String JSON_STRAPPEND = JSON_PREFIX + "STRAPPEND"; + private static final String JSON_STRLEN = JSON_PREFIX + "STRLEN"; + private static final String JSON_CLEAR = JSON_PREFIX + "CLEAR"; + private static final String JSON_RESP = JSON_PREFIX + "RESP"; + private static final String JSON_TYPE = JSON_PREFIX + "TYPE"; + + private MultiJson() {} + + /** + * Sets the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be set. The key + * will be modified only if value is added as the last child in the specified + * path, or if the specified path acts as the parent of a new child + * being added. + * @param value The value to set at the specific path, in JSON formatted string. + * @return Command Response - A simple "OK" response if the value is successfully + * set. + */ + public static > BaseTransaction set( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType value) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(value); + return transaction.customCommand( + newArgsBuilder().add(JSON_SET).add(key).add(path).add(value).toArray()); + } + + /** + * Sets the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be set. The key + * will be modified only if value is added as the last child in the specified + * path, or if the specified path acts as the parent of a new child + * being added. + * @param value The value to set at the specific path, in JSON formatted string. + * @param setCondition Set the value only if the given condition is met (within the key or path). + * @return Command Response - A simple "OK" response if the value is successfully + * set. If value isn't set because of setCondition, returns null. + */ + public static > BaseTransaction set( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType value, + @NonNull ConditionalChange setCondition) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(value); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_SET) + .add(key) + .add(path) + .add(value) + .add(setCondition.getValkeyApi()) + .toArray()); + } + + /** + * Retrieves the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns a string representation of the JSON document. If key + * doesn't exist, returns null. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_GET).add(key).toArray()); + } + + /** + * Retrieves the JSON value at the specified paths stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param paths List of paths within the JSON document. + * @return Command Response - + *
    + *
  • If one path is given: + *
      + *
    • For JSONPath (path starts with $): Returns a stringified JSON list + * replies for every possible path, or a string representation of an empty array, + * if path doesn't exist. If key doesn't exist, returns null + * . + *
    • For legacy path (path doesn't start with $): Returns a string + * representation of the value in paths. If paths + * doesn't exist, an error is raised. If key doesn't exist, returns + * null. + *
    + *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, + * and it's corresponding value, is the value as if the path was executed in the command + * as a single path. + *
+ * In case of multiple paths, and paths are a mix of both JSONPath and legacy + * path, the command behaves as if all are JSONPath paths. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType[] paths) { + checkTypeOrThrow(key); + checkTypeOrThrow(paths); + return transaction.customCommand(newArgsBuilder().add(JSON_GET).add(key).add(paths).toArray()); + } + + /** + * Retrieves the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param options Options for formatting the byte representation of the JSON data. See + * JsonGetOptions. + * @return Command Response - Returns a string representation of the JSON document. If key + * doesn't exist, returns null. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull JsonGetOptions options) { + checkTypeOrThrow(key); + return transaction.customCommand( + newArgsBuilder().add(JSON_GET).add(key).add(options.toArgs()).toArray()); + } + + /** + * Retrieves the JSON value at the specified path stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param paths List of paths within the JSON document. + * @param options Options for formatting the byte representation of the JSON data. See + * JsonGetOptions. + * @return Command Response - + *
    + *
  • If one path is given: + *
      + *
    • For JSONPath (path starts with $): Returns a stringified JSON list + * replies for every possible path, or a string representation of an empty array, + * if path doesn't exist. If key doesn't exist, returns null + * . + *
    • For legacy path (path doesn't start with $): Returns a string + * representation of the value in paths. If paths + * doesn't exist, an error is raised. If key doesn't exist, returns + * null. + *
    + *
  • If multiple paths are given: Returns a stringified JSON, in which each path is a key, + * and it's corresponding value, is the value as if the path was executed in the command + * as a single path. + *
+ * In case of multiple paths, and paths are a mix of both JSONPath and legacy + * path, the command behaves as if all are JSONPath paths. + */ + public static > BaseTransaction get( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType[] paths, + @NonNull JsonGetOptions options) { + checkTypeOrThrow(key); + checkTypeOrThrow(paths); + return transaction.customCommand( + newArgsBuilder().add(JSON_GET).add(key).add(options.toArgs()).add(paths).toArray()); + } + + /** + * Retrieves the JSON values at the specified path stored at multiple keys + * . + * + * @apiNote When using ClusterTransaction, all keys in the transaction must be mapped to the same + * slot. + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param keys The keys of the JSON documents. + * @param path The path within the JSON documents. + * @return Command Response -An array with requested values for each key. + *
    + *
  • For JSONPath (path starts with $): Returns a stringified JSON list + * replies for every possible path, or a string representation of an empty array, if + * path doesn't exist. + *
  • For legacy path (path doesn't start with $): Returns a string + * representation of the value in path. If path doesn't exist, + * the corresponding array element will be null. + *
+ * If a key doesn't exist, the corresponding array element will be null + * . + */ + public static > BaseTransaction mget( + @NonNull BaseTransaction transaction, @NonNull ArgType[] keys, @NonNull ArgType path) { + checkTypeOrThrow(keys); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_MGET).add(keys).add(path).toArray()); + } + + /** + * Appends one or more values to the JSON array at the specified path + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the values + * will be appended. + * @param values The JSON values to be appended to the array.
+ * JSON string values must be wrapped with quotes. For example, to append "foo", + * pass "\"foo\"". + * @return Command Response - + *
    + *
  • For JSONPath (path starts with $):
    + * Returns a list of integers for every possible path, indicating the new length of the + * array after appending values, or null for JSON values + * matching the path that are not an array. If path does not exist, an + * empty array will be returned. + *
  • For legacy path (path doesn't start with $):
    + * Returns the new length of the array after appending values to the array + * at path. If multiple paths are matched, returns the last updated array. + * If the JSON value at path is not an array or if path + * doesn't exist, an error is raised. If key doesn't exist, an error is + * raised. + */ + public static > BaseTransaction arrappend( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType[] values) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(values); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRAPPEND).add(key).add(path).add(values).toArray()); + } + + /** + * Inserts one or more values into the array at the specified path within the JSON + * document stored at key, before the given index. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param index The array index before which values are inserted. + * @param values The JSON values to be inserted into the array.
    + * JSON string values must be wrapped with quotes. For example, to insert "foo", + * pass "\"foo\"". + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of integers for every possible path, + * indicating the new length of the array, or null for JSON values matching + * the path that are not an array. If path does not exist, an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the new length of the array. If multiple paths are + * matched, returns the length of the first modified array. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If the index is out of bounds or key doesn't exist, an error is raised. + */ + public static > BaseTransaction arrinsert( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + int index, + @NonNull ArgType[] values) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(values); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_ARRINSERT) + .add(key) + .add(path) + .add(Integer.toString(index)) + .add(values) + .toArray()); + } + + /** + * Searches for the first occurrence of a scalar JSON value in the arrays at the + * path. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param scalar The scalar value to search for. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns an array with a + * list of integers for every possible path, indicating the index of the matching + * element. The value is -1 if not found. If a value is not an array, its + * corresponding return value is null. + *
    • For legacy path (path doesn't start with $): Returns an integer + * representing the index of matching element, or -1 if not found. If the + * value at the path is not an array, an error is raised. + *
    + */ + public static > BaseTransaction arrindex( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType scalar) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(scalar); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRINDEX).add(key).add(path).add(scalar).toArray()); + } + + /** + * Searches for the first occurrence of a scalar JSON value in the arrays at the + * path. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param scalar The scalar value to search for. + * @param options The additional options for the command. See JsonArrindexOptions. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns an array with a + * list of integers for every possible path, indicating the index of the matching + * element. The value is -1 if not found. If a value is not an array, its + * corresponding return value is null. + *
    • For legacy path (path doesn't start with $): Returns an integer + * representing the index of matching element, or -1 if not found. If the + * value at the path is not an array, an error is raised. + *
    + */ + public static > BaseTransaction arrindex( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + @NonNull ArgType scalar, + @NonNull JsonArrindexOptions options) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + checkTypeOrThrow(scalar); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_ARRINDEX) + .add(key) + .add(path) + .add(scalar) + .add(options.toArgs()) + .toArray()); + } + + /** + * Retrieves the length of the array at the specified path within the JSON document + * stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of integers for every possible path, + * indicating the length of the array, or null for JSON values matching the + * path that are not an array. If path does not exist, an empty array will + * be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the length of the array. If multiple paths are + * matched, returns the length of the first matching array. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction arrlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRLEN).add(key).add(path).toArray()); + } + + /** + * Retrieves the length of the array at the root of the JSON document stored at key. + *
    + * Equivalent to {@link #arrlen(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The array length stored at the root of the document. If document + * root is not an array, an error is raised.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction arrlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_ARRLEN).add(key).toArray()); + } + + /** + * Reports memory usage in bytes of a JSON object at the specified path within the + * JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of numbers for every possible path, + * indicating the memory usage. If path does not exist, an empty array will + * be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the memory usage. If multiple paths are matched, + * returns the data of the first matching object. If path doesn't exist, an + * error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction debugMemory( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_DEBUG_MEMORY).add(key).add(path).toArray()); + } + + /** + * Reports memory usage in bytes of a JSON object at the specified path within the + * JSON document stored at key.
    + * Equivalent to {@link #debugMemory(BaseTransaction, ArgType, ArgType)} with path + * set to "..". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The total memory usage in bytes of the entire JSON document.
    + * If key doesn't exist, returns null. + * @example + *
    {@code
    +     * Json.set(client, "doc", "$", "[1, 2.3, \"foo\", true, null, {}, [], {\"a\":1, \"b\":2}, [1, 2, 3]]").get();
    +     * var res = Json.debugMemory(client, "doc").get();
    +     * assert res == 258L;
    +     * }
    + */ + public static > BaseTransaction debugMemory( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_DEBUG_MEMORY).add(key).toArray()); + } + + /** + * Reports the number of fields at the specified path within the JSON document stored + * at key.
    + * Each non-container JSON value counts as one field. Objects and arrays recursively count one + * field for each of their containing JSON values. Each container value, except the root + * container, counts as one additional field. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of numbers for every possible path, + * indicating the number of fields. If path does not exist, an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the number of fields. If multiple paths are matched, + * returns the data of the first matching object. If path doesn't exist, an + * error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction debugFields( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_DEBUG_FIELDS).add(key).add(path).toArray()); + } + + /** + * Reports the number of fields at the specified path within the JSON document stored + * at key.
    + * Each non-container JSON value counts as one field. Objects and arrays recursively count one + * field for each of their containing JSON values. Each container value, except the root + * container, counts as one additional field.
    + * Equivalent to {@link #debugFields(BaseTransaction, ArgType, ArgType)} with path + * set to "..". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The total number of fields in the entire JSON document.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction debugFields( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_DEBUG_FIELDS).add(key).toArray()); + } + + /** + * Pops the last element from the array stored in the root of the JSON document stored at + * key. Equivalent to {@link #arrpop(BaseTransaction, ArgType, ArgType)} with + * path set to ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns a string representing the popped JSON value, or null + * if the array at document root is empty.
    + * If the JSON value at document root is not an array or if key doesn't exist, an + * error is raised. + */ + public static > BaseTransaction arrpop( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_ARRPOP).add(key).toArray()); + } + + /** + * Pops the last element from the array located at path in the JSON document stored + * at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an array with a strings for every possible path, representing the popped JSON + * values, or null for JSON values matching the path that are not an array + * or an empty array. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representing the popped JSON value, or null if the + * array at path is empty. If multiple paths are matched, the value from + * the first matching array that is not empty is returned. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction arrpop( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRPOP).add(key).add(path).toArray()); + } + + /** + * Pops an element from the array located at path in the JSON document stored at + * key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param index The index of the element to pop. Out of boundary indexes are rounded to their + * respective array boundaries. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an array with a strings for every possible path, representing the popped JSON + * values, or null for JSON values matching the path that are not an array + * or an empty array. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representing the popped JSON value, or null if the + * array at path is empty. If multiple paths are matched, the value from + * the first matching array that is not empty is returned. If path doesn't + * exist or the value at path is not an array, an error is raised. + *
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction arrpop( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + long index) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_ARRPOP).add(key).add(path).add(Long.toString(index)).toArray()); + } + + /** + * Trims an array at the specified path within the JSON document stored at key + * so that it becomes a subarray [start, end], both inclusive. + *
    + * If start < 0, it is treated as 0.
    + * If end >= size (size of the array), it is treated as size -1.
    + * If start >= size or start > end, the array is emptied + * and 0 is return.
    + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param start The index of the first element to keep, inclusive. + * @param end The index of the last element to keep, inclusive. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of integers for every possible path, + * indicating the new length of the array, or null for JSON values matching + * the path that are not an array. If the array is empty, its corresponding return value + * is 0. If path doesn't exist, an empty array will be return. If an index + * argument is out of bounds, an error is raised. + *
    • For legacy path (path doesn't start with $):
      + * Returns an integer representing the new length of the array. If the array is empty, + * its corresponding return value is 0. If multiple paths match, the length of the first + * trimmed array match is returned. If path doesn't exist, or the value at + * path is not an array, an error is raised. If an index argument is out of + * bounds, an error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction arrtrim( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + int start, + int end) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder() + .add(JSON_ARRTRIM) + .add(key) + .add(path) + .add(Integer.toString(start)) + .add(Integer.toString(end)) + .toArray()); + } + + /** + * Increments or decrements the JSON value(s) at the specified path by number + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param number The number to increment or decrement by. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a string representation of an array of strings, indicating the new values + * after incrementing for each matched path.
      + * If a value is not a number, its corresponding return value will be null. + *
      + * If path doesn't exist, a byte string representation of an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representation of the resulting value after the increment or + * decrement.
      + * If multiple paths match, the result of the last updated value is returned.
      + * If the value at the path is not a number or path doesn't + * exist, an error is raised. + *
    + * If key does not exist, an error is raised.
    + * If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + public static > BaseTransaction numincrby( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + Number number) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_NUMINCRBY).add(key).add(path).add(number.toString()).toArray()); + } + + /** + * Multiplies the JSON value(s) at the specified path by number within + * the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @param number The number to multiply by. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a string representation of an array of strings, indicating the new values + * after multiplication for each matched path.
      + * If a value is not a number, its corresponding return value will be null. + *
      + * If path doesn't exist, a byte string representation of an empty array + * will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns a string representation of the resulting value after multiplication.
      + * If multiple paths match, the result of the last updated value is returned.
      + * If the value at the path is not a number or path doesn't + * exist, an error is raised. + *
    + * If key does not exist, an error is raised.
    + * If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + public static > BaseTransaction nummultby( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType path, + Number number) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_NUMMULTBY).add(key).add(path).add(number.toString()).toArray()); + } + + /** + * Retrieves the number of key-value pairs in the object values at the specified path + * within the JSON document stored at key.
    + * Equivalent to {@link #objlen(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The object length stored at the root of the document. If document + * root is not an object, an error is raised.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_OBJLEN).add(key).toArray()); + } + + /** + * Retrieves the number of key-value pairs in the object values at the specified path + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[] with a list of long integers for every possible + * path, indicating the number of key-value pairs for each matching object, or + * null + * for JSON values matching the path that are not an object. If path + * does not exist, an empty array will be returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns the number of key-value pairs for the object value matching the path. If + * multiple paths are matched, returns the length of the first matching object. If + * path doesn't exist or the value at path is not an array, an + * error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_OBJLEN).add(key).add(path).toArray()); + } + + /** + * Retrieves the key names in the object values at the specified path within the JSON + * document stored at key.
    + * Equivalent to {@link #objkeys(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The object length stored at the root of the document. If document + * root is not an object, an error is raised.
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objkeys( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_OBJKEYS).add(key).toArray()); + } + + /** + * Retrieves the key names in the object values at the specified path within the JSON + * document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns an Object[][] with each nested array containing key names for + * each matching object for every possible path, indicating the list of object keys for + * each matching object, or null for JSON values matching the path that are + * not an object. If path does not exist, an empty sub-array will be + * returned. + *
    • For legacy path (path doesn't start with $):
      + * Returns an array of object keys for the object value matching the path. If multiple + * paths are matched, returns the length of the first matching object. If path + * doesn't exist or the value at path is not an array, an error is + * raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction objkeys( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_OBJKEYS).add(key).add(path).toArray()); + } + + /** + * Deletes the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The number of elements deleted. 0 if the key does not exist. + */ + public static > BaseTransaction del( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_DEL).add(key).toArray()); + } + + /** + * Deletes the JSON value at the specified path within the JSON document stored at + * key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be deleted. + * @return Command Response - The number of elements deleted. 0 if the key does not exist, or if + * the JSON path is invalid or does not exist. + */ + public static > BaseTransaction del( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_DEL).add(key).add(path).toArray()); + } + + /** + * Deletes the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - The number of elements deleted. 0 if the key does not exist. + */ + public static > BaseTransaction forget( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_FORGET).add(key).toArray()); + } + + /** + * Deletes the JSON value at the specified path within the JSON document stored at + * key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the value will be deleted. + * @return Command Response - The number of elements deleted. 0 if the key does not exist, or if + * the JSON path is invalid or does not exist. + */ + public static > BaseTransaction forget( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_FORGET).add(key).add(path).toArray()); + } + + /** + * Toggles a Boolean value stored at the root within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the toggled boolean value at the root of the document, or + * null for JSON values matching the root that are not boolean. If key + * doesn't exist, returns null. + */ + public static > BaseTransaction toggle( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_TOGGLE).add(key).toArray()); + } + + /** + * Toggles a Boolean value stored at the specified path within the JSON document + * stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a Boolean[] with the toggled boolean value for every possible + * path, or null for JSON values matching the path that are not boolean. + *
    • For legacy path (path doesn't start with $):
      + * Returns the value of the toggled boolean in path. If path + * doesn't exist or the value at path isn't a boolean, an error is raised. + *
    + * If key doesn't exist, returns null. + */ + public static > BaseTransaction toggle( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_TOGGLE).add(key).add(path).toArray()); + } + + /** + * Appends the specified value to the string stored at the specified path + * within the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param value The value to append to the string. Must be wrapped with single quotes. For + * example, to append "foo", pass '"foo"'. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a list of integer replies for every possible path, indicating the length of + * the resulting string after appending value, or null for + * JSON values matching the path that are not string.
      + * If key doesn't exist, an error is raised. + *
    • For legacy path (path doesn't start with $):
      + * Returns the length of the resulting string after appending value to the + * string at path.
      + * If multiple paths match, the length of the last updated string is returned.
      + * If the JSON value at path is not a string of if path + * doesn't exist, an error is raised.
      + * If key doesn't exist, an error is raised. + *
    + */ + public static > BaseTransaction strappend( + @NonNull BaseTransaction transaction, + @NonNull ArgType key, + @NonNull ArgType value, + @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(value); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_STRAPPEND).add(key).add(path).add(value).toArray()); + } + + /** + * Appends the specified value to the string stored at the root within the JSON + * document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param value The value to append to the string. Must be wrapped with single quotes. For + * example, to append "foo", pass '"foo"'. + * @return Command Response - Returns the length of the resulting string after appending + * value to the string at the root.
    + * If the JSON value at root is not a string, an error is raised.
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction strappend( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType value) { + checkTypeOrThrow(key); + checkTypeOrThrow(value); + return transaction.customCommand( + newArgsBuilder().add(JSON_STRAPPEND).add(key).add(value).toArray()); + } + + /** + * Returns the length of the JSON string value stored at the specified path within + * the JSON document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $):
      + * Returns a list of integer replies for every possible path, indicating the length of + * the JSON string value, or null for JSON values matching the path that + * are not string. + *
    • For legacy path (path doesn't start with $):
      + * Returns the length of the JSON value at path or null if + * key doesn't exist.
      + * If multiple paths match, the length of the first matched string is returned.
      + * If the JSON value at path is not a string of if path + * doesn't exist, an error is raised. If key doesn't exist, null + * is returned. + *
    + */ + public static > BaseTransaction strlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand( + newArgsBuilder().add(JSON_STRLEN).add(key).add(path).toArray()); + } + + /** + * Returns the length of the JSON string value stored at the root within the JSON document stored + * at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the length of the JSON value at the root.
    + * If the JSON value is not a string, an error is raised.
    + * If key doesn't exist, null is returned. + */ + public static > BaseTransaction strlen( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_STRLEN).add(key).toArray()); + } + + /** + * Clears an array and an object at the root of the JSON document stored at key.
    + * Equivalent to {@link #clear(BaseTransaction, ArgType, ArgType)} with path set to + * + * ".". + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - 1 if the document wasn't empty or 0 if it + * was.
    + * If key doesn't exist, an error is raised. + */ + public static > BaseTransaction clear( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_CLEAR).add(key).toArray()); + } + + /** + * Clears arrays and objects at the specified path within the JSON document stored at + * key.
    + * Numeric values are set to 0, boolean values are set to false, and + * string values are converted to empty strings. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - The number of containers cleared.
    + * If path doesn't exist, or the value at path is already cleared + * (e.g., an empty array, object, or string), 0 is returned. If key doesn't + * exist, an error is raised. + */ + public static > BaseTransaction clear( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_CLEAR).add(key).add(path).toArray()); + } + + /** + * Retrieves the JSON document stored at key. The returning result is in the Valkey + * or Redis OSS Serialization Protocol (RESP). + * + *
      + *
    • JSON null is mapped to the RESP Null Bulk String. + *
    • JSON Booleans are mapped to RESP Simple string. + *
    • JSON integers are mapped to RESP Integers. + *
    • JSON doubles are mapped to RESP Bulk Strings. + *
    • JSON strings are mapped to RESP Bulk Strings. + *
    • JSON arrays are represented as RESP arrays, where the first element is the simple string + * [, followed by the array's elements. + *
    • JSON objects are represented as RESP object, where the first element is the simple string + * {, followed by key-value pairs, each of which is a RESP bulk string. + *
    + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the JSON document in its RESP form. If key + * doesn't exist, null is returned. + */ + public static > BaseTransaction resp( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_RESP).add(key).toArray()); + } + + /** + * Retrieve the JSON value at the specified path within the JSON document stored at + * key. The returning result is in the Valkey or Redis OSS Serialization Protocol + * (RESP). + * + *
      + *
    • JSON null is mapped to the RESP Null Bulk String. + *
    • JSON Booleans are mapped to RESP Simple string. + *
    • JSON integers are mapped to RESP Integers. + *
    • JSON doubles are mapped to RESP Bulk Strings. + *
    • JSON strings are mapped to RESP Bulk Strings. + *
    • JSON arrays are represented as RESP arrays, where the first element is the simple string + * [, followed by the array's elements. + *
    • JSON objects are represented as RESP object, where the first element is the simple string + * {, followed by key-value pairs, each of which is a RESP bulk string. + *
    + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path The path within the JSON document. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns a list of + * replies for every possible path, indicating the RESP form of the JSON value. If + * path doesn't exist, returns an empty list. + *
    • For legacy path (path doesn't starts with $): Returns a + * single reply for the JSON value at the specified path, in its RESP form. If multiple + * paths match, the value of the first JSON value match is returned. If path + * doesn't exist, an error is raised. + *
    + * If key doesn't exist, null is returned. + */ + public static > BaseTransaction resp( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_RESP).add(key).add(path).toArray()); + } + + /** + * Retrieves the type of the JSON value at the root of the JSON document stored at key + * . + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @return Command Response - Returns the type of the JSON value at root. If key + * doesn't exist, + * null is returned. + */ + public static > BaseTransaction type( + @NonNull BaseTransaction transaction, @NonNull ArgType key) { + checkTypeOrThrow(key); + return transaction.customCommand(newArgsBuilder().add(JSON_TYPE).add(key).toArray()); + } + + /** + * Retrieves the type of the JSON value at the specified path within the JSON + * document stored at key. + * + * @param transaction The Valkey GLIDE client to execute the command in transaction. + * @param key The key of the JSON document. + * @param path Represents the path within the JSON document where the type will be retrieved. + * @return Command Response - + *
      + *
    • For JSONPath (path starts with $): Returns a list of string + * replies for every possible path, indicating the type of the JSON value. If `path` + * doesn't exist, an empty array will be returned. + *
    • For legacy path (path doesn't starts with $): Returns the + * type of the JSON value at `path`. If multiple paths match, the type of the first JSON + * value match is returned. If `path` doesn't exist, null will be returned. + *
    + * If key doesn't exist, null is returned. + */ + public static > BaseTransaction type( + @NonNull BaseTransaction transaction, @NonNull ArgType key, @NonNull ArgType path) { + checkTypeOrThrow(key); + checkTypeOrThrow(path); + return transaction.customCommand(newArgsBuilder().add(JSON_TYPE).add(key).add(path).toArray()); + } +} diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index 3914b05049..bfdd81efc0 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -215,6 +215,8 @@ import static glide.api.models.commands.stream.StreamReadOptions.READ_COUNT_VALKEY_API; import static glide.api.models.commands.stream.XInfoStreamOptions.COUNT; import static glide.api.models.commands.stream.XInfoStreamOptions.FULL; +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; import static glide.utils.ArrayTransformUtils.flattenAllKeysFollowedByAllValues; import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArray; import static glide.utils.ArrayTransformUtils.flattenMapToGlideStringArrayValueFirst; @@ -7267,35 +7269,6 @@ protected ArgsArray emptyArgs() { return commandArgs.build(); } - protected ArgsBuilder newArgsBuilder() { - return new ArgsBuilder(); - } - - protected void checkTypeOrThrow(ArgType arg) { - if ((arg instanceof String) || (arg instanceof GlideString)) { - return; - } - throw new IllegalArgumentException("Expected String or GlideString"); - } - - protected void checkTypeOrThrow(ArgType[] args) { - if (args.length == 0) { - // nothing to check here - return; - } - checkTypeOrThrow(args[0]); - } - - protected void checkTypeOrThrow(Map argsMap) { - if (argsMap.isEmpty()) { - // nothing to check here - return; - } - - var arg = argsMap.keySet().iterator().next(); - checkTypeOrThrow(arg); - } - /** Helper function for creating generic type ("ArgType") array */ @SafeVarargs protected final ArgType[] createArray(ArgType... args) { diff --git a/java/client/src/main/java/glide/api/models/ClusterTransaction.java b/java/client/src/main/java/glide/api/models/ClusterTransaction.java index 6252d69d36..667c8e2785 100644 --- a/java/client/src/main/java/glide/api/models/ClusterTransaction.java +++ b/java/client/src/main/java/glide/api/models/ClusterTransaction.java @@ -4,6 +4,8 @@ import static command_request.CommandRequestOuterClass.RequestType.PubSubShardChannels; import static command_request.CommandRequestOuterClass.RequestType.PubSubShardNumSub; import static command_request.CommandRequestOuterClass.RequestType.SPublish; +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; import glide.api.GlideClusterClient; import lombok.NonNull; diff --git a/java/client/src/main/java/glide/api/models/Transaction.java b/java/client/src/main/java/glide/api/models/Transaction.java index ed69907b2b..ac7bf6e09f 100644 --- a/java/client/src/main/java/glide/api/models/Transaction.java +++ b/java/client/src/main/java/glide/api/models/Transaction.java @@ -7,6 +7,8 @@ import static command_request.CommandRequestOuterClass.RequestType.Select; import static glide.api.commands.GenericBaseCommands.REPLACE_VALKEY_API; import static glide.api.commands.GenericCommands.DB_VALKEY_API; +import static glide.utils.ArgsBuilder.checkTypeOrThrow; +import static glide.utils.ArgsBuilder.newArgsBuilder; import glide.api.GlideClient; import glide.api.models.commands.scan.ScanOptions; diff --git a/java/client/src/main/java/glide/utils/ArgsBuilder.java b/java/client/src/main/java/glide/utils/ArgsBuilder.java index 066d75a707..c6873f70fb 100644 --- a/java/client/src/main/java/glide/utils/ArgsBuilder.java +++ b/java/client/src/main/java/glide/utils/ArgsBuilder.java @@ -3,6 +3,7 @@ import glide.api.models.GlideString; import java.util.ArrayList; +import java.util.Map; /** * Helper class for collecting arbitrary type of arguments and stores them as an array of @@ -63,4 +64,33 @@ public ArgsBuilder add(int[] args) { public GlideString[] toArray() { return argumentsList.toArray(new GlideString[0]); } + + public static void checkTypeOrThrow(ArgType arg) { + if ((arg instanceof String) || (arg instanceof GlideString)) { + return; + } + throw new IllegalArgumentException("Expected String or GlideString"); + } + + public static void checkTypeOrThrow(ArgType[] args) { + if (args.length == 0) { + // nothing to check here + return; + } + checkTypeOrThrow(args[0]); + } + + public static void checkTypeOrThrow(Map argsMap) { + if (argsMap.isEmpty()) { + // nothing to check here + return; + } + + var arg = argsMap.keySet().iterator().next(); + checkTypeOrThrow(arg); + } + + public static ArgsBuilder newArgsBuilder() { + return new ArgsBuilder(); + } } diff --git a/java/integTest/src/test/java/glide/modules/JsonTests.java b/java/integTest/src/test/java/glide/modules/JsonTests.java index 747a6078b6..21d051f12f 100644 --- a/java/integTest/src/test/java/glide/modules/JsonTests.java +++ b/java/integTest/src/test/java/glide/modules/JsonTests.java @@ -1,6 +1,7 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package glide.modules; +import static glide.TestUtilities.assertDeepEquals; import static glide.TestUtilities.commonClusterClientConfig; import static glide.api.BaseClient.OK; import static glide.api.models.GlideString.gs; @@ -16,12 +17,15 @@ import com.google.gson.JsonParser; import glide.api.GlideClusterClient; import glide.api.commands.servermodules.Json; +import glide.api.commands.servermodules.MultiJson; +import glide.api.models.ClusterTransaction; import glide.api.models.GlideString; import glide.api.models.commands.ConditionalChange; import glide.api.models.commands.FlushMode; import glide.api.models.commands.InfoOptions.Section; import glide.api.models.commands.json.JsonArrindexOptions; import glide.api.models.commands.json.JsonGetOptions; +import java.util.ArrayList; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -1225,4 +1229,201 @@ public void json_type() { // Check for all types in the JSON document using legacy path assertEquals("string", Json.type(client, key, "[*]").get()); } + + @SneakyThrows + @Test + public void transaction_tests() { + + ClusterTransaction transaction = new ClusterTransaction(); + ArrayList expectedResult = new ArrayList<>(); + + String key1 = "{key}-1" + UUID.randomUUID(); + String key2 = "{key}-2" + UUID.randomUUID(); + String key3 = "{key}-3" + UUID.randomUUID(); + String key4 = "{key}-4" + UUID.randomUUID(); + String key5 = "{key}-5" + UUID.randomUUID(); + String key6 = "{key}-6" + UUID.randomUUID(); + + MultiJson.set(transaction, key1, "$", "{\"a\": \"one\", \"b\": [\"one\", \"two\"]}"); + expectedResult.add(OK); + + MultiJson.set( + transaction, + key1, + "$", + "{\"a\": \"one\", \"b\": [\"one\", \"two\"]}", + ConditionalChange.ONLY_IF_DOES_NOT_EXIST); + expectedResult.add(null); + + MultiJson.get(transaction, key1); + expectedResult.add("{\"a\":\"one\",\"b\":[\"one\",\"two\"]}"); + + MultiJson.get(transaction, key1, new String[] {"$.a", "$.b"}); + expectedResult.add("{\"$.a\":[\"one\"],\"$.b\":[[\"one\",\"two\"]]}"); + + MultiJson.get(transaction, key1, JsonGetOptions.builder().space(" ").build()); + expectedResult.add("{\"a\": \"one\",\"b\": [\"one\",\"two\"]}"); + + MultiJson.get( + transaction, + key1, + new String[] {"$.a", "$.b"}, + JsonGetOptions.builder().space(" ").build()); + expectedResult.add("{\"$.a\": [\"one\"],\"$.b\": [[\"one\",\"two\"]]}"); + + MultiJson.arrappend( + transaction, key1, "$.b", new String[] {"\"3\"", "\"4\"", "\"5\"", "\"6\""}); + expectedResult.add(new Object[] {6L}); + + MultiJson.arrindex(transaction, key1, "$..b", "\"one\""); + expectedResult.add(new Object[] {0L}); + + MultiJson.arrindex(transaction, key1, "$..b", "\"one\"", new JsonArrindexOptions(0L)); + expectedResult.add(new Object[] {0L}); + + MultiJson.arrinsert(transaction, key1, "$..b", 4, new String[] {"\"7\""}); + expectedResult.add(new Object[] {7L}); + + MultiJson.arrlen(transaction, key1, "$..b"); + expectedResult.add(new Object[] {7L}); + + MultiJson.arrpop(transaction, key1, "$..b", 6L); + expectedResult.add(new Object[] {"\"6\""}); + + MultiJson.arrpop(transaction, key1, "$..b"); + expectedResult.add(new Object[] {"\"5\""}); + + MultiJson.arrtrim(transaction, key1, "$..b", 2, 3); + expectedResult.add(new Object[] {2L}); + + MultiJson.objlen(transaction, key1); + expectedResult.add(2L); + + MultiJson.objlen(transaction, key1, "$..b"); + expectedResult.add(new Object[] {null}); + + MultiJson.objkeys(transaction, key1, ".."); + expectedResult.add(new Object[] {"a", "b"}); + + MultiJson.objkeys(transaction, key1); + expectedResult.add(new Object[] {"a", "b"}); + + MultiJson.del(transaction, key1); + expectedResult.add(1L); + + MultiJson.set( + transaction, + key1, + "$", + "{\"c\": [1, 2], \"d\": true, \"e\": [\"hello\", \"clouds\"], \"f\": {\"a\": \"hello\"}}"); + expectedResult.add(OK); + + MultiJson.del(transaction, key1, "$"); + expectedResult.add(1L); + + MultiJson.set( + transaction, + key1, + "$", + "{\"c\": [1, 2], \"d\": true, \"e\": [\"hello\", \"clouds\"], \"f\": {\"a\": \"hello\"}}"); + expectedResult.add(OK); + + MultiJson.numincrby(transaction, key1, "$.c[*]", 10.0); + expectedResult.add("[11,12]"); + + MultiJson.nummultby(transaction, key1, "$.c[*]", 10.0); + expectedResult.add("[110,120]"); + + MultiJson.strappend(transaction, key1, "\"bar\"", "$..a"); + expectedResult.add(new Object[] {8L}); + + MultiJson.strlen(transaction, key1, "$..a"); + expectedResult.add(new Object[] {8L}); + + MultiJson.type(transaction, key1, "$..a"); + expectedResult.add(new Object[] {"string"}); + + MultiJson.toggle(transaction, key1, "..d"); + expectedResult.add(false); + + MultiJson.resp(transaction, key1, "$..a"); + expectedResult.add(new Object[] {"hellobar"}); + + MultiJson.del(transaction, key1, "$..a"); + expectedResult.add(1L); + + // then delete the entire key + MultiJson.del(transaction, key1, "$"); + expectedResult.add(1L); + + // 2nd key + MultiJson.set(transaction, key2, "$", "[1, 2, true, null, \"tree\", \"tree2\" ]"); + expectedResult.add(OK); + + MultiJson.arrlen(transaction, key2); + expectedResult.add(6L); + + MultiJson.arrpop(transaction, key2); + expectedResult.add("\"tree2\""); + + MultiJson.debugFields(transaction, key2); + expectedResult.add(5L); + + MultiJson.debugFields(transaction, key2, "$"); + expectedResult.add(new Object[] {5L}); + + // 3rd key + MultiJson.set(transaction, key3, "$", "\"abc\""); + expectedResult.add(OK); + + MultiJson.strappend(transaction, key3, "\"bar\""); + expectedResult.add(6L); + + MultiJson.strlen(transaction, key3); + expectedResult.add(6L); + + MultiJson.type(transaction, key3); + expectedResult.add("string"); + + MultiJson.resp(transaction, key3); + expectedResult.add("abcbar"); + + // 4th key + MultiJson.set(transaction, key4, "$", "true"); + expectedResult.add(OK); + + MultiJson.toggle(transaction, key4); + expectedResult.add(false); + + MultiJson.debugMemory(transaction, key4); + expectedResult.add(24L); + + MultiJson.debugMemory(transaction, key4, "$"); + expectedResult.add(new Object[] {16L}); + + MultiJson.clear(transaction, key2, "$.a"); + expectedResult.add(0L); + + MultiJson.clear(transaction, key2); + expectedResult.add(1L); + + MultiJson.forget(transaction, key3); + expectedResult.add(1L); + + MultiJson.forget(transaction, key4, "$"); + expectedResult.add(1L); + + // mget, key5 and key6 + MultiJson.set(transaction, key5, "$", "{\"a\": 1, \"b\": [\"one\", \"two\"]}"); + expectedResult.add(OK); + + MultiJson.set(transaction, key6, "$", "{\"a\": 1, \"c\": false}"); + expectedResult.add(OK); + + MultiJson.mget(transaction, new String[] {key5, key6}, "$.c"); + expectedResult.add(new String[] {"[]", "[false]"}); + + Object[] results = client.exec(transaction).get(); + assertDeepEquals(expectedResult.toArray(), results); + } } diff --git a/node/src/server-modules/GlideJson.ts b/node/src/server-modules/GlideJson.ts index 23d667292e..4b9d1a2ded 100644 --- a/node/src/server-modules/GlideJson.ts +++ b/node/src/server-modules/GlideJson.ts @@ -2,6 +2,7 @@ * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +import { ClusterTransaction, Transaction } from "src/Transaction"; import { BaseClient, DecoderOption, GlideString } from "../BaseClient"; import { ConditionalChange } from "../Commands"; import { GlideClient } from "../GlideClient"; @@ -263,7 +264,7 @@ export class GlideJson { * await GlideJson.set(client, "doc", "$", '[[], ["a"], ["a", "b"]]'); * const result = await GlideJson.arrinsert(client, "doc", "$[*]", 0, ['"c"', '{"key": "value"}', "true", "null", '["bar"]']); * console.log(result); // Output: [5, 6, 7] - * const doc = await json.get(client, "doc"); + * const doc = await GlideJson.get(client, "doc"); * console.log(doc); // Output: '[["c",{"key":"value"},true,null,["bar"]],["c",{"key":"value"},true,null,["bar"],"a"],["c",{"key":"value"},true,null,["bar"],"a","b"]]' * ``` * @example @@ -271,7 +272,7 @@ export class GlideJson { * await GlideJson.set(client, "doc", "$", '[[], ["a"], ["a", "b"]]'); * const result = await GlideJson.arrinsert(client, "doc", ".", 0, ['"c"']) * console.log(result); // Output: 4 - * const doc = await json.get(client, "doc"); + * const doc = await GlideJson.get(client, "doc"); * console.log(doc); // Output: '[\"c\",[],[\"a\"],[\"a\",\"b\"]]' * ``` */ @@ -721,13 +722,13 @@ export class GlideJson { /** * Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. * The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). - * JSON null is mapped to the RESP Null Bulk String. - * JSON Booleans are mapped to RESP Simple string. - * JSON integers are mapped to RESP Integers. - * JSON doubles are mapped to RESP Bulk Strings. - * JSON strings are mapped to RESP Bulk Strings. - * JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. - * JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. + * - JSON null is mapped to the RESP Null Bulk String. + * - JSON Booleans are mapped to RESP Simple string. + * - JSON integers are mapped to RESP Integers. + * - JSON doubles are mapped to RESP Bulk Strings. + * - JSON strings are mapped to RESP Bulk Strings. + * - JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. + * - JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. * * @param client - The client to execute the command. * @param key - The key of the JSON document. @@ -974,7 +975,7 @@ export class GlideJson { * ```typescript * console.log(await GlideJson.set(client, "doc", "$", '[1, 2.3, "foo", true, null, {}, [], {a:1, b:2}, [1, 2, 3]]')); * // Output: 'OK' - Indicates successful setting of the value at path '$' in the key stored at `doc`. - * console.log(await GlideJson.debugMemory(client, "doc", {path: "$[*]"}); + * console.log(await GlideJson.debugFields(client, "doc", {path: "$[*]"}); * // Output: [1, 1, 1, 1, 1, 0, 0, 2, 3] * ``` */ @@ -1157,3 +1158,773 @@ export class GlideJson { return _executeCommand(client, args, options); } } + +/** + * Transaction implementation for JSON module. Transactions allow the execution of a group of + * commands in a single step. See {@link Transaction} and {@link ClusterTransaction}. + * + * @example + * ```typescript + * const transaction = new Transaction(); + * GlideMultiJson.set(transaction, "doc", ".", '{"a": 1.0, "b": 2}'); + * GlideMultiJson.get(transaction, "doc"); + * const result = await client.exec(transaction); + * + * console.log(result[0]); // Output: 'OK' - result of GlideMultiJson.set() + * console.log(result[1]); // Output: '{"a": 1.0, "b": 2}' - result of GlideMultiJson.get() + * ``` + */ +export class GlideMultiJson { + /** + * Sets the JSON value at the specified `path` stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - Represents the path within the JSON document where the value will be set. + * The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added. + * @param value - The value to set at the specific path, in JSON formatted bytes or str. + * @param options - (Optional) Additional parameters: + * - (Optional) `conditionalChange` - Set the value only if the given condition is met (within the key or path). + * Equivalent to [`XX` | `NX`] in the module API. + * + * Command Response - If the value is successfully set, returns `"OK"`. + * If `value` isn't set because of `conditionalChange`, returns `null`. + */ + static set( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + value: GlideString, + options?: { conditionalChange: ConditionalChange }, + ): Transaction | ClusterTransaction { + const args: GlideString[] = ["JSON.SET", key, path, value]; + + if (options?.conditionalChange !== undefined) { + args.push(options.conditionalChange); + } + + return transaction.customCommand(args); + } + + /** + * Retrieves the JSON value at the specified `paths` stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) Options for formatting the byte representation of the JSON data. See {@link JsonGetOptions}. + * + * Command Response - + * - If one path is given: + * - For JSONPath (path starts with `$`): + * - Returns a stringified JSON list of bytes replies for every possible path, + * or a byte string representation of an empty array, if path doesn't exist. + * If `key` doesn't exist, returns `null`. + * - For legacy path (path doesn't start with `$`): + * Returns a byte string representation of the value in `path`. + * If `path` doesn't exist, an error is raised. + * If `key` doesn't exist, returns `null`. + * - If multiple paths are given: + * Returns a stringified JSON object in bytes, in which each path is a key, and it's corresponding value, is the value as if the path was executed in the command as a single path. + * In case of multiple paths, and `paths` are a mix of both JSONPath and legacy path, the command behaves as if all are JSONPath paths. + */ + static get( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: JsonGetOptions, + ): Transaction | ClusterTransaction { + const args = ["JSON.GET", key]; + + if (options) { + const optionArgs = _jsonGetOptionsToArgs(options); + args.push(...optionArgs); + } + + return transaction.customCommand(args); + } + + /** + * Retrieves the JSON values at the specified `path` stored at multiple `keys`. + * + * @remarks When in cluster mode, all keys in the transaction must be mapped to the same slot. + * + * @param client - The client to execute the command. + * @param keys - The keys of the JSON documents. + * @param path - The path within the JSON documents. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns a stringified JSON list replies for every possible path, or a string representation + * of an empty array, if path doesn't exist. + * - For legacy path (path doesn't start with `$`): + * Returns a string representation of the value in `path`. If `path` doesn't exist, + * the corresponding array element will be `null`. + * - If a `key` doesn't exist, the corresponding array element will be `null`. + */ + static mget( + transaction: Transaction | ClusterTransaction, + keys: GlideString[], + path: GlideString, + ): Transaction | ClusterTransaction { + const args = ["JSON.MGET", ...keys, path]; + return transaction.customCommand(args); + } + + /** + * Inserts one or more values into the array at the specified `path` within the JSON + * document stored at `key`, before the given `index`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param index - The array index before which values are inserted. + * @param values - The JSON values to be inserted into the array. + * JSON string values must be wrapped with quotes. For example, to insert `"foo"`, pass `"\"foo\""`. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the new length of the array, or `null` for JSON values matching + * the path that are not an array. If `path` does not exist, an empty array + * will be returned. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the new length of the array. If multiple paths are + * matched, returns the length of the first modified array. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrinsert( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + index: number, + values: GlideString[], + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRINSERT", key, path, index.toString(), ...values]; + + return transaction.customCommand(args); + } + + /** + * Pops an element from the array located at `path` in the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) See {@link JsonArrPopOptions}. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a strings for every possible path, representing the popped JSON + * values, or `null` for JSON values matching the path that are not an array + * or an empty array. + * - For legacy path (path doesn't start with `$`): + * Returns a string representing the popped JSON value, or `null` if the + * array at `path` is empty. If multiple paths are matched, the value from + * the first matching array that is not empty is returned. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrpop( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: JsonArrPopOptions, + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRPOP", key]; + if (options?.path) args.push(options?.path); + if (options && "index" in options && options.index) + args.push(options?.index.toString()); + + return transaction.customCommand(args); + } + + /** + * Retrieves the length of the array at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document. Defaults to the root (`"."`) if not specified. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the length of the array, or `null` for JSON values matching + * the path that are not an array. If `path` does not exist, an empty array + * will be returned. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the length of the array. If multiple paths are + * matched, returns the length of the first matching array. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrlen( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRLEN", key]; + if (options?.path) args.push(options?.path); + + return transaction.customCommand(args); + } + + /** + * Trims an array at the specified `path` within the JSON document stored at `key` so that it becomes a subarray [start, end], both inclusive. + * If `start` < 0, it is treated as 0. + * If `end` >= size (size of the array), it is treated as size-1. + * If `start` >= size or `start` > `end`, the array is emptied and 0 is returned. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param start - The start index, inclusive. + * @param end - The end index, inclusive. + * + * Command Response - + * - For JSONPath (`path` starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the new length of the array, + * or `null` for JSON values matching the path that are not an array. + * - If the array is empty, its corresponding return value is 0. + * - If `path` doesn't exist, an empty array will be returned. + * - If an index argument is out of bounds, an error is raised. + * - For legacy path (`path` doesn't start with `$`): + * - Returns an integer representing the new length of the array. + * - If the array is empty, its corresponding return value is 0. + * - If multiple paths match, the length of the first trimmed array match is returned. + * - If `path` doesn't exist, or the value at `path` is not an array, an error is raised. + * - If an index argument is out of bounds, an error is raised. + */ + static arrtrim( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + start: number, + end: number, + ): Transaction | ClusterTransaction { + const args: GlideString[] = [ + "JSON.ARRTRIM", + key, + path, + start.toString(), + end.toString(), + ]; + return transaction.customCommand(args); + } + + /** + * Searches for the first occurrence of a `scalar` JSON value in the arrays at the `path`. + * Out of range errors are treated by rounding the index to the array's `start` and `end. + * If `start` > `end`, return `-1` (not found). + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param scalar - The scalar value to search for. + * @param options - (Optional) Additional parameters: + * - (Optional) `start`: The start index, inclusive. Default to 0 if not provided. + * - (Optional) `end`: The end index, exclusive. Default to 0 if not provided. + * 0 or -1 means the last element is included. + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the index of the matching element. The value is `-1` if not found. + * If a value is not an array, its corresponding return value is `null`. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the index of matching element, or `-1` if + * not found. If the value at the `path` is not an array, an error is raised. + */ + static arrindex( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + scalar: GlideString | number | boolean | null, + options?: { start: number; end?: number }, + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRINDEX", key, path]; + + if (typeof scalar === `number`) { + args.push(scalar.toString()); + } else if (typeof scalar === `boolean`) { + args.push(scalar ? `true` : `false`); + } else if (scalar !== null) { + args.push(scalar); + } else { + args.push(`null`); + } + + if (options?.start !== undefined) args.push(options?.start.toString()); + if (options?.end !== undefined) args.push(options?.end.toString()); + + return transaction.customCommand(args); + } + + /** + * Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document. Defaults to the root (`"."`) if not specified. + * + * Command Response - For JSONPath (`path` starts with `$`), returns a list of boolean replies for every possible path, with the toggled boolean value, + * or `null` for JSON values matching the path that are not boolean. + * - For legacy path (`path` doesn't starts with `$`), returns the value of the toggled boolean in `path`. + * - Note that when sending legacy path syntax, If `path` doesn't exist or the value at `path` isn't a boolean, an error is raised. + */ + static toggle( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.TOGGLE", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Deletes the JSON value at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: If `null`, deletes the entire JSON document at `key`. + * + * Command Response - The number of elements removed. If `key` or `path` doesn't exist, returns 0. + */ + static del( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.DEL", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Deletes the JSON value at the specified `path` within the JSON document stored at `key`. This command is + * an alias of {@link del}. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: If `null`, deletes the entire JSON document at `key`. + * + * Command Response - The number of elements removed. If `key` or `path` doesn't exist, returns 0. + */ + static forget( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.FORGET", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Reports the type of values at the given path. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: Defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of strings that represents the type of value at each path. + * The type is one of "null", "boolean", "string", "number", "integer", "object" and "array". + * - If a path does not exist, its corresponding return value is `null`. + * - Empty array if the document key does not exist. + * - For legacy path (path doesn't start with `$`): + * - String that represents the type of the value. + * - `null` if the document key does not exist. + * - `null` if the JSON path is invalid or does not exist. + */ + static type( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.TYPE", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Clears arrays or objects at the specified JSON path in the document stored at `key`. + * Numeric values are set to `0`, boolean values are set to `false`, and string values are converted to empty strings. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The JSON path to the arrays or objects to be cleared. Defaults to root if not provided. + * + * Command Response - The number of containers cleared, numeric values zeroed, and booleans toggled to `false`, + * and string values converted to empty strings. + * If `path` doesn't exist, or the value at `path` is already empty (e.g., an empty array, object, or string), `0` is returned. + * If `key doesn't exist, an error is raised. + */ + static clear( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.CLEAR", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. + * The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP). + * - JSON null is mapped to the RESP Null Bulk String. + * - JSON Booleans are mapped to RESP Simple string. + * - JSON integers are mapped to RESP Integers. + * - JSON doubles are mapped to RESP Bulk Strings. + * - JSON strings are mapped to RESP Bulk Strings. + * - JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements. + * - JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of replies for every possible path, indicating the RESP form of the JSON value. + * If `path` doesn't exist, returns an empty array. + * - For legacy path (path doesn't start with `$`): + * - Returns a single reply for the JSON value at the specified `path`, in its RESP form. + * If multiple paths match, the value of the first JSON value match is returned. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + */ + static resp( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.RESP", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Returns the length of the JSON string value stored at the specified `path` within + * the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, Defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the length of + * the JSON string value, or null for JSON values matching the path that + * are not string. + * - For legacy path (path doesn't start with `$`): + * - Returns the length of the JSON value at `path` or `null` if `key` doesn't exist. + * - If multiple paths match, the length of the first matched string is returned. + * - If the JSON value at`path` is not a string or if `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + */ + static strlen( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.STRLEN", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Appends the specified `value` to the string stored at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param value - The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, defaults to root (`"."`) if not provided. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the length of the resulting string after appending `value`, + * or None for JSON values matching the path that are not string. + * - If `key` doesn't exist, an error is raised. + * - For legacy path (path doesn't start with `$`): + * - Returns the length of the resulting string after appending `value` to the string at `path`. + * - If multiple paths match, the length of the last updated string is returned. + * - If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, an error is raised. + */ + static strappend( + transaction: Transaction | ClusterTransaction, + key: GlideString, + value: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.STRAPPEND", key]; + + if (options) { + args.push(options.path); + } + + args.push(value); + + return transaction.customCommand(args); + } + + /** + * Appends one or more `values` to the JSON array at the specified `path` within the JSON + * document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param values - The JSON values to be appended to the array. + * JSON string values must be wrapped with quotes. For example, to append `"foo"`, pass `"\"foo\""`. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * Returns an array with a list of integers for every possible path, + * indicating the new length of the array, or `null` for JSON values matching + * the path that are not an array. If `path` does not exist, an empty array + * will be returned. + * - For legacy path (path doesn't start with `$`): + * Returns an integer representing the new length of the array. If multiple paths are + * matched, returns the length of the first modified array. If `path` doesn't + * exist or the value at `path` is not an array, an error is raised. + * - If the index is out of bounds or `key` doesn't exist, an error is raised. + */ + static arrappend( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + values: GlideString[], + ): Transaction | ClusterTransaction { + const args = ["JSON.ARRAPPEND", key, path, ...values]; + return transaction.customCommand(args); + } + + /** + * Reports memory usage in bytes of a JSON object at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param value - The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, returns total memory usage if no path is given. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of numbers for every possible path, indicating the memory usage. + * If `path` does not exist, an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns an integer representing the memory usage. If multiple paths are matched, + * returns the data of the first matching object. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, returns `null`. + */ + static debugMemory( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.DEBUG", "MEMORY", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Reports the number of fields at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param value - The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, returns total number of fields if no path is given. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns an array of numbers for every possible path, indicating the number of fields. + * If `path` does not exist, an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns an integer representing the memory usage. If multiple paths are matched, + * returns the data of the first matching object. If `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, returns `null`. + */ + static debugFields( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.DEBUG", "FIELDS", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Increments or decrements the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param num - The number to increment or decrement by. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a string representation of an array of strings, indicating the new values after incrementing for each matched `path`. + * If a value is not a number, its corresponding return value will be `null`. + * If `path` doesn't exist, a byte string representation of an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns a string representation of the resulting value after the increment or decrement. + * If multiple paths match, the result of the last updated value is returned. + * If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + * - If `key` does not exist, an error is raised. + * - If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + static numincrby( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + num: number, + ): Transaction | ClusterTransaction { + const args = ["JSON.NUMINCRBY", key, path, num.toString()]; + return transaction.customCommand(args); + } + + /** + * Multiplies the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param path - The path within the JSON document. + * @param num - The number to multiply by. + * + * Command Response - + * - For JSONPath (path starts with `$`): + * - Returns a GlideString representation of an array of strings, indicating the new values after multiplication for each matched `path`. + * If a value is not a number, its corresponding return value will be `null`. + * If `path` doesn't exist, a byte string representation of an empty array will be returned. + * - For legacy path (path doesn't start with `$`): + * - Returns a GlideString representation of the resulting value after multiplication. + * If multiple paths match, the result of the last updated value is returned. + * If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + * - If `key` does not exist, an error is raised. + * - If the result is out of the range of 64-bit IEEE double, an error is raised. + */ + static nummultby( + transaction: Transaction | ClusterTransaction, + key: GlideString, + path: GlideString, + num: number, + ): Transaction | ClusterTransaction { + const args = ["JSON.NUMMULTBY", key, path, num.toString()]; + return transaction.customCommand(args); + } + + /** + * Retrieves the number of key-value pairs in the object stored at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document, Defaults to root (`"."`) if not provided. + * + * Command Response - ReturnTypeJson: + * - For JSONPath (`path` starts with `$`): + * - Returns a list of integer replies for every possible path, indicating the length of the object, + * or `null` for JSON values matching the path that are not an object. + * - If `path` doesn't exist, an empty array will be returned. + * - For legacy path (`path` doesn't starts with `$`): + * - Returns the length of the object at `path`. + * - If multiple paths match, the length of the first object match is returned. + * - If the JSON value at `path` is not an object or if `path` doesn't exist, an error is raised. + * - If `key` doesn't exist, `null` is returned. + */ + static objlen( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.OBJLEN", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } + + /** + * Retrieves key names in the object values at the specified `path` within the JSON document stored at `key`. + * + * @param transaction - A transaction to add commands to. + * @param key - The key of the JSON document. + * @param options - (Optional) Additional parameters: + * - (Optional) `path`: The path within the JSON document where the key names will be retrieved. Defaults to root (`"."`) if not provided. + * + * Command Response - ReturnTypeJson: + * - For JSONPath (`path` starts with `$`): + * - Returns a list of arrays containing key names for each matching object. + * - If a value matching the path is not an object, an empty array is returned. + * - If `path` doesn't exist, an empty array is returned. + * - For legacy path (`path` starts with `.`): + * - Returns a list of key names for the object value matching the path. + * - If multiple objects match the path, the key names of the first object is returned. + * - If a value matching the path is not an object, an error is raised. + * - If `path` doesn't exist, `null` is returned. + * - If `key` doesn't exist, `null` is returned. + */ + static objkeys( + transaction: Transaction | ClusterTransaction, + key: GlideString, + options?: { path: GlideString }, + ): Transaction | ClusterTransaction { + const args = ["JSON.OBJKEYS", key]; + + if (options) { + args.push(options.path); + } + + return transaction.customCommand(args); + } +} diff --git a/node/tests/ServerModules.test.ts b/node/tests/ServerModules.test.ts index df16ce89e7..96ac19cea3 100644 --- a/node/tests/ServerModules.test.ts +++ b/node/tests/ServerModules.test.ts @@ -11,6 +11,7 @@ import { } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + ClusterTransaction, ConditionalChange, convertGlideRecordToRecord, Decoder, @@ -36,6 +37,9 @@ import { getClientConfigurationOption, getServerVersion, parseEndpoints, + transactionMultiJson, + transactionMultiJsonForArrCommands, + validateTransactionResponse, } from "./TestUtilities"; const TIMEOUT = 50000; @@ -1034,158 +1038,148 @@ describe("Server Module Tests", () => { ).toEqual("integer"); }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.clear tests", - async () => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = { - obj: { a: 1, b: 2 }, - arr: [1, 2, 3], - str: "foo", - bool: true, - int: 42, - float: 3.14, - nullVal: null, - }; + it("json.clear tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + obj: { a: 1, b: 2 }, + arr: [1, 2, 3], + str: "foo", + bool: true, + int: 42, + float: 3.14, + nullVal: null, + }; - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.clear(client, key, { path: "$.*" }), - ).toBe(6); + expect( + await GlideJson.clear(client, key, { path: "$.*" }), + ).toBe(6); - const result = await GlideJson.get(client, key, { - path: ["$"], - }); + const result = await GlideJson.get(client, key, { + path: ["$"], + }); - expect(JSON.parse(result as string)).toEqual([ - { - obj: {}, - arr: [], - str: "", - bool: false, - int: 0, - float: 0.0, - nullVal: null, - }, - ]); + expect(JSON.parse(result as string)).toEqual([ + { + obj: {}, + arr: [], + str: "", + bool: false, + int: 0, + float: 0.0, + nullVal: null, + }, + ]); - expect( - await GlideJson.clear(client, key, { path: "$.*" }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { path: "$.*" }), + ).toBe(0); - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.clear(client, key, { path: "*" }), - ).toBe(6); + expect(await GlideJson.clear(client, key, { path: "*" })).toBe( + 6, + ); - const jsonValue2 = { - a: 1, - b: { a: [5, 6, 7], b: { a: true } }, - c: { a: "value", b: { a: 3.5 } }, - d: { a: { foo: "foo" } }, - nullVal: null, - }; - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue2), - ), - ).toBe("OK"); + const jsonValue2 = { + a: 1, + b: { a: [5, 6, 7], b: { a: true } }, + c: { a: "value", b: { a: 3.5 } }, + d: { a: { foo: "foo" } }, + nullVal: null, + }; + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue2), + ), + ).toBe("OK"); - expect( - await GlideJson.clear(client, key, { - path: "b.a[1:3]", - }), - ).toBe(2); + expect( + await GlideJson.clear(client, key, { + path: "b.a[1:3]", + }), + ).toBe(2); - expect( - await GlideJson.clear(client, key, { - path: "b.a[1:3]", - }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { + path: "b.a[1:3]", + }), + ).toBe(0); - expect( - JSON.parse( - (await GlideJson.get(client, key, { - path: ["$..a"], - })) as string, - ), - ).toEqual([ - 1, - [5, 0, 0], - true, - "value", - 3.5, - { foo: "foo" }, - ]); - - expect( - await GlideJson.clear(client, key, { path: "..a" }), - ).toBe(6); - - expect( - JSON.parse( - (await GlideJson.get(client, key, { - path: ["$..a"], - })) as string, - ), - ).toEqual([0, [], false, "", 0.0, {}]); + expect( + JSON.parse( + (await GlideJson.get(client, key, { + path: ["$..a"], + })) as string, + ), + ).toEqual([1, [5, 0, 0], true, "value", 3.5, { foo: "foo" }]); - expect( - await GlideJson.clear(client, key, { path: "$..a" }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { path: "..a" }), + ).toBe(6); - // Path doesn't exist - expect( - await GlideJson.clear(client, key, { path: "$.path" }), - ).toBe(0); + expect( + JSON.parse( + (await GlideJson.get(client, key, { + path: ["$..a"], + })) as string, + ), + ).toEqual([0, [], false, "", 0.0, {}]); - expect( - await GlideJson.clear(client, key, { path: "path" }), - ).toBe(0); + expect( + await GlideJson.clear(client, key, { path: "$..a" }), + ).toBe(0); - // Key doesn't exist - await expect( - GlideJson.clear(client, "non_existing_key"), - ).rejects.toThrow(RequestError); + // Path doesn't exist + expect( + await GlideJson.clear(client, key, { path: "$.path" }), + ).toBe(0); - await expect( - GlideJson.clear(client, "non_existing_key", { - path: "$", - }), - ).rejects.toThrow(RequestError); + expect( + await GlideJson.clear(client, key, { path: "path" }), + ).toBe(0); - await expect( - GlideJson.clear(client, "non_existing_key", { - path: ".", - }), - ).rejects.toThrow(RequestError); - }, - ); + // Key doesn't exist + await expect( + GlideJson.clear(client, "non_existing_key"), + ).rejects.toThrow(RequestError); + + await expect( + GlideJson.clear(client, "non_existing_key", { + path: "$", + }), + ).rejects.toThrow(RequestError); + + await expect( + GlideJson.clear(client, "non_existing_key", { + path: ".", + }), + ).rejects.toThrow(RequestError); + }); it("json.resp tests", async () => { client = await GlideClusterClient.createClient( @@ -2068,269 +2062,290 @@ describe("Server Module Tests", () => { ).toBe("0"); // 0 * 10.2 = 0 }); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.debug tests", - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = - '{ "key1": 1, "key2": 3.5, "key3": {"nested_key": {"key1": [4, 5]}}, "key4":' + - ' [1, 2, 3], "key5": 0, "key6": "hello", "key7": null, "key8":' + - ' {"nested_key": {"key1": 3.5953862697246314e307}}, "key9":' + - ' 3.5953862697246314e307, "key10": true }'; - // setup - expect( - await GlideJson.set(client, key, "$", jsonValue), - ).toBe("OK"); - - expect( - await GlideJson.debugFields(client, key, { - path: "$.key1", - }), - ).toEqual([1]); + it("json.debug tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = + '{ "key1": 1, "key2": 3.5, "key3": {"nested_key": {"key1": [4, 5]}}, "key4":' + + ' [1, 2, 3], "key5": 0, "key6": "hello", "key7": null, "key8":' + + ' {"nested_key": {"key1": 3.5953862697246314e307}}, "key9":' + + ' 3.5953862697246314e307, "key10": true }'; + // setup + expect(await GlideJson.set(client, key, "$", jsonValue)).toBe( + "OK", + ); - expect( - await GlideJson.debugFields(client, key, { - path: "$.key3.nested_key.key1", - }), - ).toEqual([2]); + expect( + await GlideJson.debugFields(client, key, { + path: "$.key1", + }), + ).toEqual([1]); - expect( - await GlideJson.debugMemory(client, key, { - path: "$.key4[2]", - }), - ).toEqual([16]); + expect( + await GlideJson.debugFields(client, key, { + path: "$.key3.nested_key.key1", + }), + ).toEqual([2]); - expect( - await GlideJson.debugMemory(client, key, { - path: ".key6", - }), - ).toEqual(16); + expect( + await GlideJson.debugMemory(client, key, { + path: "$.key4[2]", + }), + ).toEqual([16]); - expect(await GlideJson.debugMemory(client, key)).toEqual( - 504, - ); + expect( + await GlideJson.debugMemory(client, key, { + path: ".key6", + }), + ).toEqual(16); - expect(await GlideJson.debugFields(client, key)).toEqual( - 19, - ); + expect(await GlideJson.debugMemory(client, key)).toEqual(504); - // testing binary input - expect( - await GlideJson.debugMemory(client, Buffer.from(key)), - ).toEqual(504); + expect(await GlideJson.debugFields(client, key)).toEqual(19); - expect( - await GlideJson.debugFields(client, Buffer.from(key)), - ).toEqual(19); - }, - ); + // testing binary input + expect( + await GlideJson.debugMemory(client, Buffer.from(key)), + ).toEqual(504); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.objlen tests", - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = { - a: 1.0, - b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, - }; - - // setup - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.debugFields(client, Buffer.from(key)), + ).toEqual(19); + }); + + it("json.objlen tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + a: 1.0, + b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, + }; - expect( - await GlideJson.objlen(client, key, { path: "$" }), - ).toEqual([2]); + // setup + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.objlen(client, key, { path: "." }), - ).toEqual(2); + expect( + await GlideJson.objlen(client, key, { path: "$" }), + ).toEqual([2]); - expect( - await GlideJson.objlen(client, key, { path: "$.." }), - ).toEqual([2, 3, 2]); + expect( + await GlideJson.objlen(client, key, { path: "." }), + ).toEqual(2); - expect( - await GlideJson.objlen(client, key, { path: ".." }), - ).toEqual(2); + expect( + await GlideJson.objlen(client, key, { path: "$.." }), + ).toEqual([2, 3, 2]); - expect( - await GlideJson.objlen(client, key, { path: "$..b" }), - ).toEqual([3, null]); + expect( + await GlideJson.objlen(client, key, { path: ".." }), + ).toEqual(2); - expect( - await GlideJson.objlen(client, key, { path: "..b" }), - ).toEqual(3); + expect( + await GlideJson.objlen(client, key, { path: "$..b" }), + ).toEqual([3, null]); - expect( - await GlideJson.objlen(client, Buffer.from(key), { - path: Buffer.from("..a"), - }), - ).toEqual(2); + expect( + await GlideJson.objlen(client, key, { path: "..b" }), + ).toEqual(3); - expect(await GlideJson.objlen(client, key)).toEqual(2); + expect( + await GlideJson.objlen(client, Buffer.from(key), { + path: Buffer.from("..a"), + }), + ).toEqual(2); - // path doesn't exist - expect( - await GlideJson.objlen(client, key, { - path: "$.non_existing_path", - }), - ).toEqual([]); + expect(await GlideJson.objlen(client, key)).toEqual(2); - await expect( - GlideJson.objlen(client, key, { - path: "non_existing_path", - }), - ).rejects.toThrow(RequestError); + // path doesn't exist + expect( + await GlideJson.objlen(client, key, { + path: "$.non_existing_path", + }), + ).toEqual([]); - // Value at path isnt an object - expect( - await GlideJson.objlen(client, key, { - path: "$.non_existing_path", - }), - ).toEqual([]); + await expect( + GlideJson.objlen(client, key, { + path: "non_existing_path", + }), + ).rejects.toThrow(RequestError); - await expect( - GlideJson.objlen(client, key, { path: ".a" }), - ).rejects.toThrow(RequestError); + // Value at path isnt an object + expect( + await GlideJson.objlen(client, key, { + path: "$.non_existing_path", + }), + ).toEqual([]); - // Non-existing key - expect( - await GlideJson.objlen(client, "non_existing_key", { - path: "$", - }), - ).toBeNull(); + await expect( + GlideJson.objlen(client, key, { path: ".a" }), + ).rejects.toThrow(RequestError); - expect( - await GlideJson.objlen(client, "non_existing_key", { - path: ".", - }), - ).toBeNull(); + // Non-existing key + expect( + await GlideJson.objlen(client, "non_existing_key", { + path: "$", + }), + ).toBeNull(); - expect( - await GlideJson.set( - client, - key, - "$", - '{"a": 1, "b": 2, "c":3, "d":4}', - ), - ).toBe("OK"); - expect(await GlideJson.objlen(client, key)).toEqual(4); - }, - ); + expect( + await GlideJson.objlen(client, "non_existing_key", { + path: ".", + }), + ).toBeNull(); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "json.objkeys tests", - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - const key = uuidv4(); - const jsonValue = { - a: 1.0, - b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, - }; - - // setup - expect( - await GlideJson.set( - client, - key, - "$", - JSON.stringify(jsonValue), - ), - ).toBe("OK"); + expect( + await GlideJson.set( + client, + key, + "$", + '{"a": 1, "b": 2, "c":3, "d":4}', + ), + ).toBe("OK"); + expect(await GlideJson.objlen(client, key)).toEqual(4); + }); + + it("json.objkeys tests", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const key = uuidv4(); + const jsonValue = { + a: 1.0, + b: { a: { x: 1, y: 2 }, b: 2.5, c: true }, + }; - expect( - await GlideJson.objkeys(client, key, { path: "$" }), - ).toEqual([["a", "b"]]); + // setup + expect( + await GlideJson.set( + client, + key, + "$", + JSON.stringify(jsonValue), + ), + ).toBe("OK"); - expect( - await GlideJson.objkeys(client, key, { - path: ".", - decoder: Decoder.Bytes, - }), - ).toEqual([Buffer.from("a"), Buffer.from("b")]); + expect( + await GlideJson.objkeys(client, key, { path: "$" }), + ).toEqual([["a", "b"]]); - expect( - await GlideJson.objkeys(client, Buffer.from(key), { - path: Buffer.from("$.."), - }), - ).toEqual([ - ["a", "b"], - ["a", "b", "c"], - ["x", "y"], - ]); - - expect( - await GlideJson.objkeys(client, key, { path: ".." }), - ).toEqual(["a", "b"]); - - expect( - await GlideJson.objkeys(client, key, { path: "$..b" }), - ).toEqual([["a", "b", "c"], []]); - - expect( - await GlideJson.objkeys(client, key, { path: "..b" }), - ).toEqual(["a", "b", "c"]); - - // path doesn't exist - expect( - await GlideJson.objkeys(client, key, { - path: "$.non_existing_path", - }), - ).toEqual([]); + expect( + await GlideJson.objkeys(client, key, { + path: ".", + decoder: Decoder.Bytes, + }), + ).toEqual([Buffer.from("a"), Buffer.from("b")]); - expect( - await GlideJson.objkeys(client, key, { - path: "non_existing_path", - }), - ).toBeNull(); + expect( + await GlideJson.objkeys(client, Buffer.from(key), { + path: Buffer.from("$.."), + }), + ).toEqual([ + ["a", "b"], + ["a", "b", "c"], + ["x", "y"], + ]); - // Value at path isnt an object - expect( - await GlideJson.objkeys(client, key, { path: "$.a" }), - ).toEqual([[]]); + expect( + await GlideJson.objkeys(client, key, { path: ".." }), + ).toEqual(["a", "b"]); - await expect( - GlideJson.objkeys(client, key, { path: ".a" }), - ).rejects.toThrow(RequestError); + expect( + await GlideJson.objkeys(client, key, { path: "$..b" }), + ).toEqual([["a", "b", "c"], []]); - // Non-existing key - expect( - await GlideJson.objkeys(client, "non_existing_key", { - path: "$", - }), - ).toBeNull(); + expect( + await GlideJson.objkeys(client, key, { path: "..b" }), + ).toEqual(["a", "b", "c"]); - expect( - await GlideJson.objkeys(client, "non_existing_key", { - path: ".", - }), - ).toBeNull(); - }, - ); + // path doesn't exist + expect( + await GlideJson.objkeys(client, key, { + path: "$.non_existing_path", + }), + ).toEqual([]); + + expect( + await GlideJson.objkeys(client, key, { + path: "non_existing_path", + }), + ).toBeNull(); + + // Value at path isnt an object + expect( + await GlideJson.objkeys(client, key, { path: "$.a" }), + ).toEqual([[]]); + + await expect( + GlideJson.objkeys(client, key, { path: ".a" }), + ).rejects.toThrow(RequestError); + + // Non-existing key + expect( + await GlideJson.objkeys(client, "non_existing_key", { + path: "$", + }), + ).toBeNull(); + + expect( + await GlideJson.objkeys(client, "non_existing_key", { + path: ".", + }), + ).toBeNull(); + }); + + it("can send GlideMultiJson transactions for ARR commands", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const clusterTransaction = new ClusterTransaction(); + const expectedRes = + await transactionMultiJsonForArrCommands( + clusterTransaction, + ); + const result = await client.exec(clusterTransaction); + + validateTransactionResponse(result, expectedRes); + client.close(); + }); + + it("can send GlideMultiJson transactions general commands", async () => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const clusterTransaction = new ClusterTransaction(); + const expectedRes = + await transactionMultiJson(clusterTransaction); + const result = await client.exec(clusterTransaction); + + validateTransactionResponse(result, expectedRes); + client.close(); + }); }, ); diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a58abacb6c..234e82f259 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -23,6 +23,7 @@ import { GeospatialData, GlideClient, GlideClusterClient, + GlideMultiJson, GlideReturnType, GlideString, InfBoundary, @@ -1883,6 +1884,188 @@ export async function transactionTest( return responseData; } +/** + * Populates a transaction with JSON commands to test. + * @param baseTransaction - A transaction. + * @returns Array of tuples, where first element is a test name/description, second - expected return value. + */ +export async function transactionMultiJsonForArrCommands( + baseTransaction: ClusterTransaction, +): Promise<[string, GlideReturnType][]> { + const responseData: [string, GlideReturnType][] = []; + const key = "{key}:1" + uuidv4(); + const jsonValue = { a: 1.0, b: 2 }; + + // JSON.SET + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue)); + responseData.push(['set(key, "{ a: 1.0, b: 2 }")', "OK"]); + + // JSON.CLEAR + GlideMultiJson.clear(baseTransaction, key, { path: "$" }); + responseData.push(['clear(key, "bar")', 1]); + + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue)); + responseData.push(['set(key, "$", "{ "a": 1, b: ["one", "two"] }")', "OK"]); + + // JSON.GET + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push(['get(key, {path: "."})', JSON.stringify(jsonValue)]); + + const jsonValue2 = { a: 1.0, b: [1, 2] }; + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue2)); + responseData.push(['set(key, "$", "{ "a": 1, b: ["1", "2"] }")', "OK"]); + + // JSON.ARRAPPEND + GlideMultiJson.arrappend(baseTransaction, key, "$.b", ["3", "4"]); + responseData.push(['arrappend(key, "$.b", [\'"3"\', \'"4"\'])', [4]]); + + // JSON.GET to check JSON.ARRAPPEND was successful. + const jsonValueAfterAppend = { a: 1.0, b: [1, 2, 3, 4] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterAppend), + ]); + + // JSON.ARRINDEX + GlideMultiJson.arrindex(baseTransaction, key, "$.b", "2"); + responseData.push(['arrindex(key, "$.b", "1")', [1]]); + + // JSON.ARRINSERT + GlideMultiJson.arrinsert(baseTransaction, key, "$.b", 2, ["5"]); + responseData.push(['arrinsert(key, "$.b", 4, [\'"5"\'])', [5]]); + + // JSON.GET to check JSON.ARRINSERT was successful. + const jsonValueAfterArrInsert = { a: 1.0, b: [1, 2, 5, 3, 4] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterArrInsert), + ]); + + // JSON.ARRLEN + GlideMultiJson.arrlen(baseTransaction, key, { path: "$.b" }); + responseData.push(['arrlen(key, "$.b")', [5]]); + + // JSON.ARRPOP + GlideMultiJson.arrpop(baseTransaction, key, { + path: "$.b", + index: 2, + }); + responseData.push(['arrpop(key, {path: "$.b", index: 4})', ["5"]]); + + // JSON.GET to check JSON.ARRPOP was successful. + const jsonValueAfterArrpop = { a: 1.0, b: [1, 2, 3, 4] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterArrpop), + ]); + + // JSON.ARRTRIM + GlideMultiJson.arrtrim(baseTransaction, key, "$.b", 1, 2); + responseData.push(['arrtrim(key, "$.b", 2, 3)', [2]]); + + // JSON.GET to check JSON.ARRTRIM was successful. + const jsonValueAfterArrTrim = { a: 1.0, b: [2, 3] }; + GlideMultiJson.get(baseTransaction, key, { path: "." }); + responseData.push([ + 'get(key, {path: "."})', + JSON.stringify(jsonValueAfterArrTrim), + ]); + return responseData; +} + +export async function transactionMultiJson( + baseTransaction: ClusterTransaction, +): Promise<[string, GlideReturnType][]> { + const responseData: [string, GlideReturnType][] = []; + const key = "{key}:1" + uuidv4(); + const jsonValue = { a: [1, 2], b: [3, 4], c: "c", d: true }; + + // JSON.SET to create a key for testing commands. + GlideMultiJson.set(baseTransaction, key, "$", JSON.stringify(jsonValue)); + responseData.push(['set(key, "$")', "OK"]); + + // JSON.DEBUG MEMORY + GlideMultiJson.debugMemory(baseTransaction, key, { path: "$.a" }); + responseData.push(['debugMemory(key, "{ path: "$.a" }")', [48]]); + + // JSON.DEBUG FIELDS + GlideMultiJson.debugFields(baseTransaction, key, { path: "$.a" }); + responseData.push(['debugFields(key, "{ path: "$.a" }")', [2]]); + + // JSON.OBJLEN + GlideMultiJson.objlen(baseTransaction, key, { path: "." }); + responseData.push(["objlen(key)", 4]); + + // JSON.OBJKEY + GlideMultiJson.objkeys(baseTransaction, key, { path: "." }); + responseData.push(['objkeys(key, "$.")', ["a", "b", "c", "d"]]); + + // JSON.NUMINCRBY + GlideMultiJson.numincrby(baseTransaction, key, "$.a[*]", 10.0); + responseData.push(['numincrby(key, "$.a[*]", 10.0)', "[11,12]"]); + + // JSON.NUMMULTBY + GlideMultiJson.nummultby(baseTransaction, key, "$.a[*]", 10.0); + responseData.push(['nummultby(key, "$.a[*]", 10.0)', "[110,120]"]); + + // // JSON.STRAPPEND + GlideMultiJson.strappend(baseTransaction, key, '"-test"', { path: "$.c" }); + responseData.push(['strappend(key, \'"-test"\', "$.c")', [6]]); + + // // JSON.STRLEN + GlideMultiJson.strlen(baseTransaction, key, { path: "$.c" }); + responseData.push(['strlen(key, "$.c")', [6]]); + + // JSON.TYPE + GlideMultiJson.type(baseTransaction, key, { path: "$.a" }); + responseData.push(['type(key, "$.a")', ["array"]]); + + // JSON.MGET + const key2 = "{key}:2" + uuidv4(); + const key3 = "{key}:3" + uuidv4(); + const jsonValue2 = { b: [3, 4], c: "c", d: true }; + GlideMultiJson.set(baseTransaction, key2, "$", JSON.stringify(jsonValue2)); + responseData.push(['set(key2, "$")', "OK"]); + + GlideMultiJson.mget(baseTransaction, [key, key2, key3], "$.a"); + responseData.push([ + 'json.mget([key, key2, key3], "$.a")', + ["[[110,120]]", "[]", null], + ]); + + // JSON.TOGGLE + GlideMultiJson.toggle(baseTransaction, key, { path: "$.d" }); + responseData.push(['toggle(key2, "$.d")', [false]]); + + // JSON.RESP + GlideMultiJson.resp(baseTransaction, key, { path: "$" }); + responseData.push([ + 'resp(key, "$")', + [ + [ + "{", + ["a", ["[", 110, 120]], + ["b", ["[", 3, 4]], + ["c", "c-test"], + ["d", "false"], + ], + ], + ]); + + // JSON.DEL + GlideMultiJson.del(baseTransaction, key, { path: "$.d" }); + responseData.push(['del(key, { path: "$.d" })', 1]); + + // JSON.FORGET + GlideMultiJson.forget(baseTransaction, key, { path: "$.c" }); + responseData.push(['forget(key, {path: "$.c" })', 1]); + + return responseData; +} + /** * This function gets server version using info command in glide client. * diff --git a/python/python/glide/__init__.py b/python/python/glide/__init__.py index f2ecc3da4e..4a7ca8328e 100644 --- a/python/python/glide/__init__.py +++ b/python/python/glide/__init__.py @@ -32,7 +32,7 @@ InsertPosition, UpdateOptions, ) -from glide.async_commands.server_modules import ft, glide_json +from glide.async_commands.server_modules import ft, glide_json, json_transaction from glide.async_commands.server_modules.ft_options.ft_aggregate_options import ( FtAggregateApply, FtAggregateClause, @@ -271,6 +271,7 @@ "PubSubMsg", # Json "glide_json", + "json_transaction", "JsonGetOptions", "JsonArrIndexOptions", "JsonArrPopOptions", diff --git a/python/python/glide/async_commands/server_modules/json_transaction.py b/python/python/glide/async_commands/server_modules/json_transaction.py new file mode 100644 index 0000000000..ad0cc91158 --- /dev/null +++ b/python/python/glide/async_commands/server_modules/json_transaction.py @@ -0,0 +1,789 @@ +# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 +"""Glide module for `JSON` commands in transaction. + + Examples: + >>> import json + >>> from glide import json_transaction + >>> transaction = ClusterTransaction() + >>> value = {'a': 1.0, 'b': 2} + >>> json_str = json.dumps(value) # Convert Python dictionary to JSON string using json.dumps() + >>> json_transaction.set(transaction, "doc", "$", json_str) + >>> json_transaction.get(transaction, "doc", "$") # Returns the value at path '$' in the JSON document stored at `doc` as JSON string. + >>> result = await glide_client.exec(transaction) + >>> print result[0] # set result + 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. + >>> print result[1] # get result + b"[{\"a\":1.0,\"b\":2}]" + >>> print json.loads(str(result[1])) + [{"a": 1.0, "b": 2}] # JSON object retrieved from the key `doc` using json.loads() + """ + +from typing import List, Optional, Union, cast + +from glide.async_commands.core import ConditionalChange +from glide.async_commands.server_modules.glide_json import ( + JsonArrIndexOptions, + JsonArrPopOptions, + JsonGetOptions, +) +from glide.async_commands.transaction import TTransaction +from glide.constants import TEncodable +from glide.protobuf.command_request_pb2 import RequestType + + +def set( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + value: TEncodable, + set_condition: Optional[ConditionalChange] = None, +) -> TTransaction: + """ + Sets the JSON value at the specified `path` stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): Represents the path within the JSON document where the value will be set. + The key will be modified only if `value` is added as the last child in the specified `path`, or if the specified `path` acts as the parent of a new child being added. + value (TEncodable): The value to set at the specific path, in JSON formatted bytes or str. + set_condition (Optional[ConditionalChange]): Set the value only if the given condition is met (within the key or path). + Equivalent to [`XX` | `NX`] in the RESP API. Defaults to None. + + Command response: + Optional[TOK]: If the value is successfully set, returns OK. + If `value` isn't set because of `set_condition`, returns None. + """ + args = ["JSON.SET", key, path, value] + if set_condition: + args.append(set_condition.value) + + return transaction.custom_command(args) + + +def get( + transaction: TTransaction, + key: TEncodable, + paths: Optional[Union[TEncodable, List[TEncodable]]] = None, + options: Optional[JsonGetOptions] = None, +) -> TTransaction: + """ + Retrieves the JSON value at the specified `paths` stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + paths (Optional[Union[TEncodable, List[TEncodable]]]): The path or list of paths within the JSON document. Default to None. + options (Optional[JsonGetOptions]): Options for formatting the byte representation of the JSON data. See `JsonGetOptions`. + + Command response: + TJsonResponse[Optional[bytes]]: + If one path is given: + For JSONPath (path starts with `$`): + Returns a stringified JSON list of bytes replies for every possible path, + or a byte string representation of an empty array, if path doesn't exists. + If `key` doesn't exist, returns None. + For legacy path (path doesn't start with `$`): + Returns a byte string representation of the value in `path`. + If `path` doesn't exist, an error is raised. + If `key` doesn't exist, returns None. + If multiple paths are given: + Returns a stringified JSON object in bytes, in which each path is a key, and it's corresponding value, is the value as if the path was executed in the command as a single path. + In case of multiple paths, and `paths` are a mix of both JSONPath and legacy path, the command behaves as if all are JSONPath paths. + For more information about the returned type, see `TJsonResponse`. + """ + args = ["JSON.GET", key] + if options: + args.extend(options.get_options()) + if paths: + if isinstance(paths, (str, bytes)): + paths = [paths] + args.extend(paths) + + return transaction.custom_command(args) + + +def mget( + transaction: TTransaction, + keys: List[TEncodable], + path: TEncodable, +) -> TTransaction: + """ + Retrieves the JSON values at the specified `path` stored at multiple `keys`. + + Note: + When in cluster mode, all keys in the transaction must be mapped to the same slot. + + Args: + transaction (TTransaction): The transaction to execute the command. + keys (List[TEncodable]): A list of keys for the JSON documents. + path (TEncodable): The path within the JSON documents. + + Command response: + List[Optional[bytes]]: + For JSONPath (`path` starts with `$`): + Returns a list of byte representations of the values found at the given path for each key. + If `path` does not exist within the key, the entry will be an empty array. + For legacy path (`path` doesn't starts with `$`): + Returns a list of byte representations of the values found at the given path for each key. + If `path` does not exist within the key, the entry will be None. + If a key doesn't exist, the corresponding list element will be None. + """ + args = ["JSON.MGET"] + keys + [path] + return transaction.custom_command(args) + + +def arrappend( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + values: List[TEncodable], +) -> TTransaction: + """ + Appends one or more `values` to the JSON array at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): Represents the path within the JSON document where the `values` will be appended. + values (TEncodable): The values to append to the JSON array at the specified path. + JSON string values must be wrapped with quotes. For example, to append `"foo"`, pass `"\"foo\""`. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the new length of the array after appending `values`, + or None for JSON values matching the path that are not an array. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns the length of the array after appending `values` to the array at `path`. + If multiple paths match, the length of the first updated array is returned. + If the JSON value at `path` is not a array or if `path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + For more information about the returned type, see `TJsonResponse`. + """ + args = ["JSON.ARRAPPEND", key, path] + values + return transaction.custom_command(args) + + +def arrindex( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + value: TEncodable, + options: Optional[JsonArrIndexOptions] = None, +) -> TTransaction: + """ + Searches for the first occurrence of a scalar JSON value (i.e., a value that is neither an object nor an array) within arrays at the specified `path` in the JSON document stored at `key`. + + If specified, `options.start` and `options.end` define an inclusive-to-exclusive search range within the array. + (Where `options.start` is inclusive and `options.end` is exclusive). + + Out-of-range indices adjust to the nearest valid position, and negative values count from the end (e.g., `-1` is the last element, `-2` the second last). + + Setting `options.end` to `0` behaves like `-1`, extending the range to the array's end (inclusive). + + If `options.start` exceeds `options.end`, `-1` is returned, indicating that the value was not found. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + value (TEncodable): The value to search for within the arrays. + options (Optional[JsonArrIndexOptions]): Options specifying an inclusive `start` index and an optional exclusive `end` index for a range-limited search. + Defaults to the full array if not provided. See `JsonArrIndexOptions`. + + Command response: + Optional[Union[int, List[int]]]: + For JSONPath (`path` starts with `$`): + Returns an array of integers for every possible path, indicating of the first occurrence of `value` within the array, + or None for JSON values matching the path that are not an array. + A returned value of `-1` indicates that the value was not found in that particular array. + If `path` does not exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns an integer representing the index of the first occurrence of `value` within the array at the specified path. + A returned value of `-1` indicates that the value was not found in that particular array. + If multiple paths match, the index of the value from the first matching array is returned. + If the JSON value at the `path` is not an array or if `path` does not exist, an error is raised. + If `key` does not exist, an error is raised. + """ + args = ["JSON.ARRINDEX", key, path, value] + + if options: + args.extend(options.to_args()) + + return transaction.custom_command(args) + + +def arrinsert( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + index: int, + values: List[TEncodable], +) -> TTransaction: + """ + Inserts one or more values into the array at the specified `path` within the JSON document stored at `key`, before the given `index`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + index (int): The array index before which values are inserted. + values (List[TEncodable]): The JSON values to be inserted into the array, in JSON formatted bytes or str. + Json string values must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with '$'): + Returns a list of integer replies for every possible path, indicating the new length of the array, + or None for JSON values matching the path that are not an array. + If `path` does not exist, an empty array will be returned. + For legacy path (`path` doesn't start with '$'): + Returns an integer representing the new length of the array. + If multiple paths are matched, returns the length of the first modified array. + If `path` doesn't exist or the value at `path` is not an array, an error is raised. + If the index is out of bounds, an error is raised. + If `key` doesn't exist, an error is raised. + """ + args = ["JSON.ARRINSERT", key, path, str(index)] + values + return transaction.custom_command(args) + + +def arrlen( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves the length of the array at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to None. + + Command response: + Optional[TJsonResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the array, + or None for JSON values matching the path that are not an array. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns the length of the array at `path`. + If multiple paths match, the length of the first array match is returned. + If the JSON value at `path` is not a array or if `path` doesn't exist, an error is raised. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.ARRLEN", key] + if path: + args.append(path) + return transaction.custom_command(args) + + +def arrpop( + transaction: TTransaction, + key: TEncodable, + options: Optional[JsonArrPopOptions] = None, +) -> TTransaction: + """ + Pops an element from the array located at the specified path within the JSON document stored at `key`. + If `options.index` is provided, it pops the element at that index instead of the last element. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + options (Optional[JsonArrPopOptions]): Options including the path and optional index. See `JsonArrPopOptions`. Default to None. + If not specified, attempts to pop the last element from the root value if it's an array. + If the root value is not an array, an error will be raised. + + Command response: + Optional[TJsonResponse[bytes]]: + For JSONPath (`options.path` starts with `$`): + Returns a list of bytes string replies for every possible path, representing the popped JSON values, + or None for JSON values matching the path that are not an array or are an empty array. + If `options.path` doesn't exist, an empty list will be returned. + For legacy path (`options.path` doesn't starts with `$`): + Returns a bytes string representing the popped JSON value, or None if the array at `options.path` is empty. + If multiple paths match, the value from the first matching array that is not empty is returned. + If the JSON value at `options.path` is not a array or if `options.path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + """ + args = ["JSON.ARRPOP", key] + if options: + args.extend(options.to_args()) + + return transaction.custom_command(args) + + +def arrtrim( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + start: int, + end: int, +) -> TTransaction: + """ + Trims an array at the specified `path` within the JSON document stored at `key` so that it becomes a subarray [start, end], both inclusive. + If `start` < 0, it is treated as 0. + If `end` >= size (size of the array), it is treated as size-1. + If `start` >= size or `start` > `end`, the array is emptied and 0 is returned. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + start (int): The start index, inclusive. + end (int): The end index, inclusive. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with '$'): + Returns a list of integer replies for every possible path, indicating the new length of the array, or None for JSON values matching the path that are not an array. + If a value is an empty array, its corresponding return value is 0. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns an integer representing the new length of the array. + If the array is empty, returns 0. + If multiple paths match, the length of the first trimmed array match is returned. + If `path` doesn't exist, or the value at `path` is not an array, an error is raised. + If `key` doesn't exist, an error is raised. + """ + + return transaction.custom_command(["JSON.ARRTRIM", key, path, str(start), str(end)]) + + +def clear( + transaction: TTransaction, + key: TEncodable, + path: Optional[str] = None, +) -> TTransaction: + """ + Clears arrays or objects at the specified JSON path in the document stored at `key`. + Numeric values are set to `0`, and boolean values are set to `False`, and string values are converted to empty strings. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[str]): The path within the JSON document. Default to None. + + Command response: + int: The number of containers cleared, numeric values zeroed, and booleans toggled to `false`, + and string values converted to empty strings. + If `path` doesn't exist, or the value at `path` is already empty (e.g., an empty array, object, or string), 0 is returned. + If `key doesn't exist, an error is raised. + """ + args = ["JSON.CLEAR", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def debug_fields( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Returns the number of fields of the JSON value at the specified `path` within the JSON document stored at `key`. + - **Primitive Values**: Each non-container JSON value (e.g., strings, numbers, booleans, and null) counts as one field. + - **Arrays and Objects:**: Each item in an array and each key-value pair in an object is counted as one field. (Each top-level value counts as one field, regardless of it's type.) + - Their nested values are counted recursively and added to the total. + - **Example**: For the JSON `{"a": 1, "b": [2, 3, {"c": 4}]}`, the count would be: + - Top-level: 2 fields (`"a"` and `"b"`) + - Nested: 3 fields in the array (`2`, `3`, and `{"c": 4}`) plus 1 for the object (`"c"`) + - Total: 2 (top-level) + 3 (from array) + 1 (from nested object) = 6 fields. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to root if not provided. + + Command response: + Optional[TJsonUniversalResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns an array of integers, each indicating the number of fields for each matched `path`. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns an integer indicating the number of fields for each matched `path`. + If multiple paths match, number of fields of the first JSON value match is returned. + If `path` doesn't exist, an error is raised. + If `path` is not provided, it reports the total number of fields in the entire JSON document. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.DEBUG", "FIELDS", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def debug_memory( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Reports memory usage in bytes of a JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to None. + + Command response: + Optional[TJsonUniversalResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns an array of integers, indicating the memory usage in bytes of a JSON value for each matched `path`. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns an integer, indicating the memory usage in bytes for the JSON value in `path`. + If multiple paths match, the memory usage of the first JSON value match is returned. + If `path` doesn't exist, an error is raised. + If `path` is not provided, it reports the total memory usage in bytes in the entire JSON document. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.DEBUG", "MEMORY", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def delete( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Deletes the JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. + If None, deletes the entire JSON document at `key`. Defaults to None. + + Command response: + int: The number of elements removed. + If `key` or `path` doesn't exist, returns 0. + """ + + return transaction.custom_command(["JSON.DEL", key] + ([path] if path else [])) + + +def forget( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Deletes the JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. + If None, deletes the entire JSON document at `key`. Defaults to None. + + Command response: + int: The number of elements removed. + If `key` or `path` doesn't exist, returns 0. + """ + + return transaction.custom_command(["JSON.FORGET", key] + ([path] if path else [])) + + +def numincrby( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + number: Union[int, float], +) -> TTransaction: + """ + Increments or decrements the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + number (Union[int, float]): The number to increment or decrement by. + + Command response: + bytes: + For JSONPath (`path` starts with `$`): + Returns a bytes string representation of an array of bulk strings, indicating the new values after incrementing for each matched `path`. + If a value is not a number, its corresponding return value will be `null`. + If `path` doesn't exist, a byte string representation of an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns a bytes string representation of the resulting value after the increment or decrement. + If multiple paths match, the result of the last updated value is returned. + If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + If `key` does not exist, an error is raised. + If the result is out of the range of 64-bit IEEE double, an error is raised. + """ + args = ["JSON.NUMINCRBY", key, path, str(number)] + + return transaction.custom_command(args) + + +def nummultby( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, + number: Union[int, float], +) -> TTransaction: + """ + Multiplies the JSON value(s) at the specified `path` by `number` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. + number (Union[int, float]): The number to multiply by. + + Command response: + bytes: + For JSONPath (`path` starts with `$`): + Returns a bytes string representation of an array of bulk strings, indicating the new values after multiplication for each matched `path`. + If a value is not a number, its corresponding return value will be `null`. + If `path` doesn't exist, a byte string representation of an empty array will be returned. + For legacy path (`path` doesn't start with `$`): + Returns a bytes string representation of the resulting value after multiplication. + If multiple paths match, the result of the last updated value is returned. + If the value at the `path` is not a number or `path` doesn't exist, an error is raised. + If `key` does not exist, an error is raised. + If the result is out of the range of 64-bit IEEE double, an error is raised. + """ + args = ["JSON.NUMMULTBY", key, path, str(number)] + + return transaction.custom_command(args) + + +def objlen( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves the number of key-value pairs in the object stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Defaults to None. + + Command response: + Optional[TJsonResponse[int]]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the object, + or None for JSON values matching the path that are not an object. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns the length of the object at `path`. + If multiple paths match, the length of the first object match is returned. + If the JSON value at `path` is not an object or if `path` doesn't exist, an error is raised. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.OBJLEN", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def objkeys( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves key names in the object values at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): Represents the path within the JSON document where the key names will be retrieved. + Defaults to None. + + Command response: + Optional[TJsonUniversalResponse[List[bytes]]]: + For JSONPath (`path` starts with `$`): + Returns a list of arrays containing key names for each matching object. + If a value matching the path is not an object, an empty array is returned. + If `path` doesn't exist, an empty array is returned. + For legacy path (`path` starts with `.`): + Returns a list of key names for the object value matching the path. + If multiple objects match the path, the key names of the first object are returned. + If a value matching the path is not an object, an error is raised. + If `path` doesn't exist, None is returned. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.OBJKEYS", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def resp( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieve the JSON value at the specified `path` within the JSON document stored at `key`. + The returning result is in the Valkey or Redis OSS Serialization Protocol (RESP).\n + JSON null is mapped to the RESP Null Bulk String.\n + JSON Booleans are mapped to RESP Simple string.\n + JSON integers are mapped to RESP Integers.\n + JSON doubles are mapped to RESP Bulk Strings.\n + JSON strings are mapped to RESP Bulk Strings.\n + JSON arrays are represented as RESP arrays, where the first element is the simple string [, followed by the array's elements.\n + JSON objects are represented as RESP object, where the first element is the simple string {, followed by key-value pairs, each of which is a RESP bulk string.\n + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + TJsonUniversalResponse[Optional[Union[bytes, int, List[Optional[Union[bytes, int]]]]]] + For JSONPath ('path' starts with '$'): + Returns a list of replies for every possible path, indicating the RESP form of the JSON value. + If `path` doesn't exist, returns an empty list. + For legacy path (`path` doesn't starts with `$`): + Returns a single reply for the JSON value at the specified path, in its RESP form. + This can be a bytes object, an integer, None, or a list representing complex structures. + If multiple paths match, the value of the first JSON value match is returned. + If `path` doesn't exist, an error is raised. + If `key` doesn't exist, an None is returned. + """ + args = ["JSON.RESP", key] + if path: + args.append(path) + + return transaction.custom_command(args) + + +def strappend( + transaction: TTransaction, + key: TEncodable, + value: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Appends the specified `value` to the string stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + value (TEncodable): The value to append to the string. Must be wrapped with single quotes. For example, to append "foo", pass '"foo"'. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + TJsonResponse[int]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the resulting string after appending `value`, + or None for JSON values matching the path that are not string. + If `key` doesn't exist, an error is raised. + For legacy path (`path` doesn't start with `$`): + Returns the length of the resulting string after appending `value` to the string at `path`. + If multiple paths match, the length of the last updated string is returned. + If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + For more information about the returned type, see `TJsonResponse`. + """ + return transaction.custom_command( + ["JSON.STRAPPEND", key] + ([path, value] if path else [value]) + ) + + +def strlen( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Returns the length of the JSON string value stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + TJsonResponse[Optional[int]]: + For JSONPath (`path` starts with `$`): + Returns a list of integer replies for every possible path, indicating the length of the JSON string value, + or None for JSON values matching the path that are not string. + For legacy path (`path` doesn't start with `$`): + Returns the length of the JSON value at `path` or None if `key` doesn't exist. + If multiple paths match, the length of the first mached string is returned. + If the JSON value at `path` is not a string of if `path` doesn't exist, an error is raised. + If `key` doesn't exist, None is returned. + For more information about the returned type, see `TJsonResponse`. + """ + return transaction.custom_command( + ["JSON.STRLEN", key, path] if path else ["JSON.STRLEN", key] + ) + + +def toggle( + transaction: TTransaction, + key: TEncodable, + path: TEncodable, +) -> TTransaction: + """ + Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (TEncodable): The path within the JSON document. Default to None. + + Command response: + TJsonResponse[bool]: + For JSONPath (`path` starts with `$`): + Returns a list of boolean replies for every possible path, with the toggled boolean value, + or None for JSON values matching the path that are not boolean. + If `key` doesn't exist, an error is raised. + For legacy path (`path` doesn't start with `$`): + Returns the value of the toggled boolean in `path`. + If the JSON value at `path` is not a boolean of if `path` doesn't exist, an error is raised. + If `key` doesn't exist, an error is raised. + For more information about the returned type, see `TJsonResponse`. + """ + return transaction.custom_command(["JSON.TOGGLE", key, path]) + + +def type( + transaction: TTransaction, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TTransaction: + """ + Retrieves the type of the JSON value at the specified `path` within the JSON document stored at `key`. + + Args: + transaction (TTransaction): The transaction to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Command response: + Optional[TJsonUniversalResponse[bytes]]: + For JSONPath ('path' starts with '$'): + Returns a list of byte string replies for every possible path, indicating the type of the JSON value. + If `path` doesn't exist, an empty array will be returned. + For legacy path (`path` doesn't starts with `$`): + Returns the type of the JSON value at `path`. + If multiple paths match, the type of the first JSON value match is returned. + If `path` doesn't exist, None will be returned. + If `key` doesn't exist, None is returned. + """ + args = ["JSON.TYPE", key] + if path: + args.append(path) + + return transaction.custom_command(args) diff --git a/python/python/tests/tests_server_modules/test_json.py b/python/python/tests/tests_server_modules/test_json.py index 85657914de..0182943d82 100644 --- a/python/python/tests/tests_server_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -4,19 +4,26 @@ import json as OuterJson import random import typing +from typing import List import pytest from glide.async_commands.core import ConditionalChange, InfoSection from glide.async_commands.server_modules import glide_json as json +from glide.async_commands.server_modules import json_transaction from glide.async_commands.server_modules.glide_json import ( JsonArrIndexOptions, JsonArrPopOptions, JsonGetOptions, ) +from glide.async_commands.transaction import ( + BaseTransaction, + ClusterTransaction, + Transaction, +) from glide.config import ProtocolVersion from glide.constants import OK from glide.exceptions import RequestError -from glide.glide_client import TGlideClient +from glide.glide_client import GlideClusterClient, TGlideClient from tests.test_async_client import get_random_string, parse_info_response @@ -2097,3 +2104,128 @@ async def test_json_arrpop(self, glide_client: TGlideClient): assert await json.arrpop(glide_client, key2, JsonArrPopOptions("[*]")) == b'"a"' assert await json.get(glide_client, key2, ".") == b'[[],[],["a"],["a","b"]]' + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_transaction_array(self, glide_client: GlideClusterClient): + transaction = ClusterTransaction() + + key = get_random_string(5) + json_value1 = {"a": 1.0, "b": 2} + json_value2 = {"a": 1.0, "b": [1, 2]} + + # Test 'set', 'get', and 'clear' commands + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value1)) + json_transaction.clear(transaction, key, "$") + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value1)) + json_transaction.get(transaction, key, ".") + + # Test array related commands + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value2)) + json_transaction.arrappend(transaction, key, "$.b", ["3", "4"]) + json_transaction.arrindex(transaction, key, "$.b", "2") + json_transaction.arrinsert(transaction, key, "$.b", 2, ["5"]) + json_transaction.arrlen(transaction, key, "$.b") + json_transaction.arrpop( + transaction, key, JsonArrPopOptions(path="$.b", index=2) + ) + json_transaction.arrtrim(transaction, key, "$.b", 1, 2) + json_transaction.get(transaction, key, ".") + + result = await glide_client.exec(transaction) + assert isinstance(result, list) + + assert result[0] == "OK" # set + assert result[1] == 1 # clear + assert result[2] == "OK" # set + assert isinstance(result[3], bytes) + assert OuterJson.loads(result[3]) == json_value1 # get + + assert result[4] == "OK" # set + assert result[5] == [4] # arrappend + assert result[6] == [1] # arrindex + assert result[7] == [5] # arrinsert + assert result[8] == [5] # arrlen + assert result[9] == [b"5"] # arrpop + assert result[10] == [2] # arrtrim + assert isinstance(result[11], bytes) + assert OuterJson.loads(result[11]) == {"a": 1.0, "b": [2, 3]} # get + + @pytest.mark.parametrize("cluster_mode", [True]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_transaction(self, glide_client: GlideClusterClient): + transaction = ClusterTransaction() + + key = f"{{key}}-1{get_random_string(5)}" + key2 = f"{{key}}-2{get_random_string(5)}" + key3 = f"{{key}}-3{get_random_string(5)}" + json_value = {"a": [1, 2], "b": [3, 4], "c": "c", "d": True} + + json_transaction.set(transaction, key, "$", OuterJson.dumps(json_value)) + + # Test debug commands + json_transaction.debug_memory(transaction, key, "$.a") + json_transaction.debug_fields(transaction, key, "$.a") + + # Test obj commands + json_transaction.objlen(transaction, key, ".") + json_transaction.objkeys(transaction, key, ".") + + # Test num commands + json_transaction.numincrby(transaction, key, "$.a[*]", 10.0) + json_transaction.nummultby(transaction, key, "$.a[*]", 10.0) + + # Test str commands + json_transaction.strappend(transaction, key, '"-test"', "$.c") + json_transaction.strlen(transaction, key, "$.c") + + # Test type command + json_transaction.type(transaction, key, "$.a") + + # Test mget command + json_value2 = {"b": [3, 4], "c": "c", "d": True} + json_transaction.set(transaction, key2, "$", OuterJson.dumps(json_value2)) + json_transaction.mget(transaction, [key, key2, key3], "$.a") + + # Test toggle command + json_transaction.toggle(transaction, key, "$.d") + + # Test resp command + json_transaction.resp(transaction, key, "$") + + # Test del command + json_transaction.delete(transaction, key, "$.d") + + # Test forget command + json_transaction.forget(transaction, key, "$.c") + + result = await glide_client.exec(transaction) + assert isinstance(result, list) + + assert result[0] == "OK" # set + assert result[1] == [48] # debug_memory + assert result[2] == [2] # debug_field + + assert result[3] == 4 # objlen + assert result[4] == [b"a", b"b", b"c", b"d"] # objkeys + assert result[5] == b"[11,12]" # numincrby + assert result[6] == b"[110,120]" # nummultby + assert result[7] == [6] # strappend + assert result[8] == [6] # strlen + assert result[9] == [b"array"] # type + assert result[10] == "OK" # set + assert result[11] == [b"[[110,120]]", b"[]", None] # mget + assert result[12] == [False] # toggle + + assert result[13] == [ + [ + b"{", + [b"a", [b"[", 110, 120]], + [b"b", [b"[", 3, 4]], + [b"c", b"c-test"], + [b"d", b"false"], + ] + ] # resp + + assert result[14] == 1 # del + assert result[15] == 1 # forget