diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f0a3e6e00..18bef9336c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * Node: Added `JSON.TYPE` ([#2510](https://github.com/valkey-io/valkey-glide/pull/2510)) * Java: Added `JSON.RESP` ([#2513](https://github.com/valkey-io/valkey-glide/pull/2513)) * Node: Added `FT.DROPINDEX` ([#2516](https://github.com/valkey-io/valkey-glide/pull/2516)) +* Python: Add `JSON.STRAPPEND` , `JSON.STRLEN` commands ([#2372](https://github.com/valkey-io/valkey-glide/pull/2372)) #### Breaking Changes diff --git a/python/python/glide/async_commands/server_modules/json.py b/python/python/glide/async_commands/server_modules/json.py index 7da1c6c4aa..e2bfa20ac2 100644 --- a/python/python/glide/async_commands/server_modules/json.py +++ b/python/python/glide/async_commands/server_modules/json.py @@ -1,18 +1,18 @@ # Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 -"""module for `RedisJSON` commands. +"""Glide module for `JSON` commands. Examples: - >>> from glide import json as redisJson - >>> import json + >>> from glide import json + >>> import json as jsonpy >>> value = {'a': 1.0, 'b': 2} - >>> json_str = json.dumps(value) # Convert Python dictionary to JSON string using json.dumps() - >>> await redisJson.set(client, "doc", "$", json_str) + >>> json_str = jsonpy.dumps(value) # Convert Python dictionary to JSON string using json.dumps() + >>> await json.set(client, "doc", "$", json_str) 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. - >>> json_get = await redisJson.get(client, "doc", "$") # Returns the value at path '$' in the JSON document stored at `doc` as JSON string. + >>> json_get = await json.get(client, "doc", "$") # Returns the value at path '$' in the JSON document stored at `doc` as JSON string. >>> print(json_get) b"[{\"a\":1.0,\"b\":2}]" - >>> json.loads(str(json_get)) + >>> jsonpy.loads(str(json_get)) [{"a": 1.0, "b" :2}] # JSON object retrieved from the key `doc` using json.loads() """ from typing import List, Optional, Union, cast @@ -64,27 +64,25 @@ async def set( """ Sets the JSON value at the specified `path` stored at `key`. - See https://valkey.io/commands/json.set/ for more details. - Args: - client (TGlideClient): The Redis client to execute the command. + client (TGlideClient): The client 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 Redis API. Defaults to None. + Equivalent to [`XX` | `NX`] in the RESP API. Defaults to None. Returns: Optional[TOK]: If the value is successfully set, returns OK. - If value isn't set because of `set_condition`, returns None. + If `value` isn't set because of `set_condition`, returns None. Examples: - >>> from glide import json as redisJson - >>> import json + >>> from glide import json + >>> import json as jsonpy >>> value = {'a': 1.0, 'b': 2} - >>> json_str = json.dumps(value) - >>> await redisJson.set(client, "doc", "$", json_str) + >>> json_str = jsonpy.dumps(value) + >>> await json.set(client, "doc", "$", json_str) 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. """ args = ["JSON.SET", key, path, value] @@ -99,33 +97,43 @@ async def get( key: TEncodable, paths: Optional[Union[TEncodable, List[TEncodable]]] = None, options: Optional[JsonGetOptions] = None, -) -> Optional[bytes]: +) -> TJsonResponse[Optional[bytes]]: """ Retrieves the JSON value at the specified `paths` stored at `key`. - See https://valkey.io/commands/json.get/ for more details. - Args: - client (TGlideClient): The Redis client to execute the command. + client (TGlideClient): The client 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 is root `$`. + 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`. Returns: - bytes: A bytes representation of the returned value. - If `key` doesn't exists, returns None. + 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`. Examples: - >>> from glide import json as redisJson - >>> import json - >>> json_str = await redisJson.get(client, "doc", "$") - >>> json.loads(str(json_str)) # Parse JSON string to Python data + >>> from glide import json, JsonGetOptions + >>> import as jsonpy + >>> json_str = await json.get(client, "doc", "$") + >>> jsonpy.loads(str(json_str)) # Parse JSON string to Python data [{"a": 1.0, "b" :2}] # JSON object retrieved from the key `doc` using json.loads() - >>> await redisJson.get(client, "doc", "$") + >>> await json.get(client, "doc", "$") b"[{\"a\":1.0,\"b\":2}]" # Returns the value at path '$' in the JSON document stored at `doc`. - >>> await redisJson.get(client, "doc", ["$.a", "$.b"], json.JsonGetOptions(indent=" ", newline="\n", space=" ")) + >>> await json.get(client, "doc", ["$.a", "$.b"], JsonGetOptions(indent=" ", newline="\n", space=" ")) b"{\n \"$.a\": [\n 1.0\n ],\n \"$.b\": [\n 2\n ]\n}" # Returns the values at paths '$.a' and '$.b' in the JSON document stored at `doc`, with specified formatting options. - >>> await redisJson.get(client, "doc", "$.non_existing_path") + >>> await json.get(client, "doc", "$.non_existing_path") b"[]" # Returns an empty array since the path '$.non_existing_path' does not exist in the JSON document stored at `doc`. """ args = ["JSON.GET", key] @@ -136,7 +144,7 @@ async def get( paths = [paths] args.extend(paths) - return cast(bytes, await client.custom_command(args)) + return cast(TJsonResponse[Optional[bytes]], await client.custom_command(args)) async def arrlen( @@ -167,7 +175,7 @@ async def arrlen( Examples: >>> from glide import json >>> await json.set(client, "doc", "$", '{"a": [1, 2, 3], "b": {"a": [1, 2], "c": {"a": 42}}}') - b'OK' # JSON is successfully set for doc + 'OK' # JSON is successfully set for doc >>> await json.arrlen(client, "doc", "$") [None] # No array at the root path. >>> await json.arrlen(client, "doc", "$.a") @@ -180,7 +188,7 @@ async def arrlen( None # Returns None because the key does not exist. >>> await json.set(client, "doc", "$", '[1, 2, 3, 4]') - b'OK' # JSON is successfully set for doc + 'OK' # JSON is successfully set for doc >>> await json.arrlen(client, "doc") 4 # Retrieves lengths of arrays in root. """ @@ -205,7 +213,7 @@ async def clear( Args: client (TGlideClient): The client to execute the command. key (TEncodable): The key of the JSON document. - path (Optional[str]): The JSON path to the arrays or objects to be cleared. Defaults to None. + path (Optional[str]): The path within the JSON document. Default to None. Returns: int: The number of containers cleared, numeric values zeroed, and booleans toggled to `false`, @@ -216,7 +224,7 @@ async def clear( Examples: >>> from glide import json >>> await json.set(client, "doc", "$", '{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14, "nullVal": null}') - b'OK' # JSON document is successfully set. + 'OK' # JSON document is successfully set. >>> await json.clear(client, "doc", "$.*") 6 # 6 values are cleared (arrays/objects/strings/numbers/booleans), but `null` remains as is. >>> await json.get(client, "doc", "$") @@ -225,7 +233,7 @@ async def clear( 0 # No further clearing needed since the containers are already empty and the values are defaults. >>> await json.set(client, "doc", "$", '{"a": 1, "b": {"a": [5, 6, 7], "b": {"a": true}}, "c": {"a": "value", "b": {"a": 3.5}}, "d": {"a": {"foo": "foo"}}, "nullVal": null}') - b'OK' + 'OK' >>> await json.clear(client, "doc", "b.a[1:3]") 2 # 2 elements (`6` and `7`) are cleared. >>> await json.clear(client, "doc", "b.a[1:3]") @@ -253,27 +261,25 @@ async def delete( """ Deletes the JSON value at the specified `path` within the JSON document stored at `key`. - See https://valkey.io/commands/json.del/ for more details. - Args: - client (TGlideClient): The Redis client to execute the command. + client (TGlideClient): The client to execute the command. key (TEncodable): The key of the JSON document. - path (Optional[TEncodable]): Represents the path within the JSON document where the value will be deleted. + path (Optional[TEncodable]): The path within the JSON document. If None, deletes the entire JSON document at `key`. Defaults to None. Returns: int: The number of elements removed. - If `key` or path doesn't exist, returns 0. + If `key` or `path` doesn't exist, returns 0. Examples: - >>> from glide import json as redisJson - >>> await redisJson.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') + >>> from glide import json + >>> await json.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. - >>> await redisJson.delete(client, "doc", "$..a") + >>> await json.delete(client, "doc", "$..a") 2 # Indicates successful deletion of the specific values in the key stored at `doc`. - >>> await redisJson.get(client, "doc", "$") + >>> await json.get(client, "doc", "$") "[{\"nested\":{\"b\":3}}]" # Returns the value at path '$' in the JSON document stored at `doc`. - >>> await redisJson.delete(client, "doc") + >>> await json.delete(client, "doc") 1 # Deletes the entire JSON document stored at `doc`. """ @@ -290,27 +296,25 @@ async def forget( """ Deletes the JSON value at the specified `path` within the JSON document stored at `key`. - See https://valkey.io/commands/json.forget/ for more details. - Args: - client (TGlideClient): The Redis client to execute the command. + client (TGlideClient): The client to execute the command. key (TEncodable): The key of the JSON document. - path (Optional[TEncodable]): Represents the path within the JSON document where the value will be deleted. + path (Optional[TEncodable]): The path within the JSON document. If None, deletes the entire JSON document at `key`. Defaults to None. Returns: int: The number of elements removed. - If `key` or path doesn't exist, returns 0. + If `key` or `path` doesn't exist, returns 0. Examples: - >>> from glide import json as redisJson - >>> await redisJson.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') + >>> from glide import json + >>> await json.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') 'OK' # Indicates successful setting of the value at path '$' in the key stored at `doc`. - >>> await redisJson.forget(client, "doc", "$..a") + >>> await json.forget(client, "doc", "$..a") 2 # Indicates successful deletion of the specific values in the key stored at `doc`. - >>> await redisJson.get(client, "doc", "$") + >>> await json.get(client, "doc", "$") "[{\"nested\":{\"b\":3}}]" # Returns the value at path '$' in the JSON document stored at `doc`. - >>> await redisJson.forget(client, "doc") + >>> await json.forget(client, "doc") 1 # Deletes the entire JSON document stored at `doc`. """ @@ -404,6 +408,103 @@ async def nummultby( return cast(Optional[bytes], await client.custom_command(args)) +async def strappend( + client: TGlideClient, + key: TEncodable, + value: TEncodable, + path: Optional[TEncodable] = None, +) -> TJsonResponse[int]: + """ + Appends the specified `value` to the string stored at the specified `path` within the JSON document stored at `key`. + + Args: + client (TGlideClient): The client 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. + + Returns: + 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`. + + Examples: + >>> from glide import json + >>> import json as jsonpy + >>> await json.set(client, "doc", "$", jsonpy.dumps({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})) + 'OK' + >>> await json.strappend(client, "doc", jsonpy.dumps("baz"), "$..a") + [6, 8, None] # The new length of the string values at path '$..a' in the key stored at `doc` after the append operation. + >>> await json.strappend(client, "doc", '"foo"', "nested.a") + 11 # The length of the string value after appending "foo" to the string at path 'nested.array' in the key stored at `doc`. + >>> jsonpy.loads(await json.get(client, jsonpy.dumps("doc"), "$")) + [{"a":"foobaz", "nested": {"a": "hellobazfoo"}, "nested2": {"a": 31}}] # The updated JSON value in the key stored at `doc`. + """ + + return cast( + TJsonResponse[int], + await client.custom_command( + ["JSON.STRAPPEND", key] + ([path, value] if path else [value]) + ), + ) + + +async def strlen( + client: TGlideClient, + key: TEncodable, + path: Optional[TEncodable] = None, +) -> TJsonResponse[Optional[int]]: + """ + Returns the length of the JSON string value stored at the specified `path` within the JSON document stored at `key`. + + Args: + client (TGlideClient): The client to execute the command. + key (TEncodable): The key of the JSON document. + path (Optional[TEncodable]): The path within the JSON document. Default to None. + + Returns: + 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`. + + Examples: + >>> from glide import json + >>> import jsonpy + >>> await json.set(client, "doc", "$", jsonpy.dumps({"a":"foo", "nested": {"a": "hello"}, "nested2": {"a": 31}})) + 'OK' + >>> await json.strlen(client, "doc", "$..a") + [3, 5, None] # The length of the string values at path '$..a' in the key stored at `doc`. + >>> await json.strlen(client, "doc", "nested.a") + 5 # The length of the JSON value at path 'nested.a' in the key stored at `doc`. + >>> await json.strlen(client, "doc", "$") + [None] # Returns an array with None since the value at root path does in the JSON document stored at `doc` is not a string. + >>> await json.strlen(client, "non_existing_key", ".") + None # `key` doesn't exist. + """ + + return cast( + TJsonResponse[Optional[int]], + await client.custom_command( + ["JSON.STRLEN", key, path] if path else ["JSON.STRLEN", key] + ), + ) + + async def toggle( client: TGlideClient, key: TEncodable, @@ -412,30 +513,33 @@ async def toggle( """ Toggles a Boolean value stored at the specified `path` within the JSON document stored at `key`. - See https://valkey.io/commands/json.toggle/ for more details. - Args: - client (TGlideClient): The Redis client to execute the command. + client (TGlideClient): The client to execute the command. key (TEncodable): The key of the JSON document. - path (TEncodable): The JSONPath to specify. + path (TEncodable): The path within the JSON document. Default to None. Returns: - 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. - 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. + 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`. Examples: - >>> from glide import json as redisJson - >>> import json - >>> await redisJson.set(client, "doc", "$", json.dumps({"bool": True, "nested": {"bool": False, "nested": {"bool": 10}}})) + >>> from glide import json + >>> import json as jsonpy + >>> await json.set(client, "doc", "$", jsonpy.dumps({"bool": True, "nested": {"bool": False, "nested": {"bool": 10}}})) 'OK' - >>> await redisJson.toggle(client, "doc", "$.bool") + >>> await json.toggle(client, "doc", "$.bool") [False, True, None] # Indicates successful toggling of the Boolean values at path '$.bool' in the key stored at `doc`. - >>> await redisJson.toggle(client, "doc", "bool") + >>> await json.toggle(client, "doc", "bool") True # Indicates successful toggling of the Boolean value at path 'bool' in the key stored at `doc`. - >>> json.loads(await redisJson.get(client, "doc", "$")) + >>> jsonpy.loads(await json.get(client, "doc", "$")) [{"bool": True, "nested": {"bool": True, "nested": {"bool": 10}}}] # The updated JSON value in the key stored at `doc`. """ @@ -456,8 +560,7 @@ async def type( Args: client (TGlideClient): The client to execute the command. key (TEncodable): The key of the JSON document. - path (Optional[TEncodable]): Represents the path within the JSON document where the type will be retrieved. - Defaults to None. + path (Optional[TEncodable]): The path within the JSON document. Default to None. Returns: Optional[Union[bytes, List[bytes]]]: @@ -473,6 +576,7 @@ async def type( Examples: >>> from glide import json >>> await json.set(client, "doc", "$", '{"a": 1, "nested": {"a": 2, "b": 3}}') + 'OK' >>> await json.type(client, "doc", "$.nested") [b'object'] # Indicates the type of the value at path '$.nested' in the key stored at `doc`. >>> await json.type(client, "doc", "$.nested.a") diff --git a/python/python/tests/tests_server_modules/test_json.py b/python/python/tests/tests_server_modules/test_json.py index 794e885cfe..982b563995 100644 --- a/python/python/tests/tests_server_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -144,42 +144,78 @@ async def test_json_get_formatting(self, glide_client: TGlideClient): @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_del(self, glide_client: TGlideClient): + async def test_json_del(self, glide_client: TGlideClient): key = get_random_string(5) json_value = {"a": 1.0, "b": {"a": 1, "b": 2.5, "c": True}} assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + # Non-exiseting paths + assert await json.delete(glide_client, key, "$..path") == 0 + assert await json.delete(glide_client, key, "..path") == 0 + assert await json.delete(glide_client, key, "$..a") == 2 assert await json.get(glide_client, key, "$..a") == b"[]" + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + + assert await json.delete(glide_client, key, "..a") == 2 + with pytest.raises(RequestError): + assert await json.get(glide_client, key, "..a") + result = await json.get(glide_client, key, "$") assert isinstance(result, bytes) assert OuterJson.loads(result) == [{"b": {"b": 2.5, "c": True}}] assert await json.delete(glide_client, key, "$") == 1 + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.delete(glide_client, key, ".") == 1 + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.delete(glide_client, key) == 1 assert await json.delete(glide_client, key) == 0 assert await json.get(glide_client, key, "$") == None + # Non-existing keys + assert await json.delete(glide_client, "non_existing_key", "$") == 0 + assert await json.delete(glide_client, "non_existing_key", ".") == 0 + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) - async def test_forget(self, glide_client: TGlideClient): + async def test_json_forget(self, glide_client: TGlideClient): key = get_random_string(5) json_value = {"a": 1.0, "b": {"a": 1, "b": 2.5, "c": True}} assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + # Non-existing paths + assert await json.forget(glide_client, key, "$..path") == 0 + assert await json.forget(glide_client, key, "..path") == 0 + assert await json.forget(glide_client, key, "$..a") == 2 assert await json.get(glide_client, key, "$..a") == b"[]" + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + + assert await json.forget(glide_client, key, "..a") == 2 + with pytest.raises(RequestError): + assert await json.get(glide_client, key, "..a") + result = await json.get(glide_client, key, "$") assert isinstance(result, bytes) assert OuterJson.loads(result) == [{"b": {"b": 2.5, "c": True}}] assert await json.forget(glide_client, key, "$") == 1 + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.forget(glide_client, key, ".") == 1 + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + assert await json.forget(glide_client, key) == 1 assert await json.forget(glide_client, key) == 0 assert await json.get(glide_client, key, "$") == None + # Non-existing keys + assert await json.forget(glide_client, "non_existing_key", "$") == 0 + assert await json.forget(glide_client, "non_existing_key", ".") == 0 + @pytest.mark.parametrize("cluster_mode", [True, False]) @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) async def test_json_toggle(self, glide_client: TGlideClient): @@ -189,11 +225,15 @@ async def test_json_toggle(self, glide_client: TGlideClient): assert await json.toggle(glide_client, key, "$..bool") == [False, True, None] assert await json.toggle(glide_client, key, "bool") is True + assert await json.toggle(glide_client, key, "$.not_existing") == [] assert await json.toggle(glide_client, key, "$.nested") == [None] with pytest.raises(RequestError): assert await json.toggle(glide_client, key, "nested") + with pytest.raises(RequestError): + assert await json.toggle(glide_client, key, ".not_existing") + with pytest.raises(RequestError): assert await json.toggle(glide_client, "non_exiting_key", "$") @@ -470,7 +510,7 @@ async def test_json_numincrby(self, glide_client: TGlideClient): assert result == b"76" # Check if the rest of the key1 path matches were updated and not only the last value - result = await json.get(glide_client, key, "$..key1") + result = await json.get(glide_client, key, "$..key1") # type: ignore assert ( result == b"[0,[16,17],76]" ) # First is 0 as 0 + 0 = 0, Second doesn't change as its an array type (non-numeric), third is 76 as 0 + 76 = 0 @@ -610,7 +650,7 @@ async def test_json_nummultby(self, glide_client: TGlideClient): assert result == b"1380" # Expect the last updated key1 value multiplied by 2 # Check if the rest of the key1 path matches were updated and not only the last value - result = await json.get(glide_client, key, "$..key1") + result = await json.get(glide_client, key, "$..key1") # type: ignore assert result == b"[-16500,[140,175],1380]" # Check for non-existent path in legacy @@ -624,3 +664,71 @@ async def test_json_nummultby(self, glide_client: TGlideClient): # Check for Overflow in legacy with pytest.raises(RequestError): await json.nummultby(glide_client, key, ".key9", 1.7976931348623157e308) + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_strlen(self, glide_client: TGlideClient): + key = get_random_string(10) + json_value = {"a": "foo", "nested": {"a": "hello"}, "nested2": {"a": 31}} + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + + assert await json.strlen(glide_client, key, "$..a") == [3, 5, None] + assert await json.strlen(glide_client, key, "a") == 3 + + assert await json.strlen(glide_client, key, "$.nested") == [None] + with pytest.raises(RequestError): + assert await json.strlen(glide_client, key, "nested") + + with pytest.raises(RequestError): + assert await json.strlen(glide_client, key) + + assert await json.strlen(glide_client, key, "$.non_existing_path") == [] + with pytest.raises(RequestError): + await json.strlen(glide_client, key, ".non_existing_path") + + assert await json.strlen(glide_client, "non_exiting_key", ".") is None + assert await json.strlen(glide_client, "non_exiting_key", "$") is None + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_strappend(self, glide_client: TGlideClient): + key = get_random_string(10) + json_value = {"a": "foo", "nested": {"a": "hello"}, "nested2": {"a": 31}} + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + + assert await json.strappend(glide_client, key, '"bar"', "$..a") == [6, 8, None] + assert await json.strappend(glide_client, key, OuterJson.dumps("foo"), "a") == 9 + + json_str = await json.get(glide_client, key, ".") + assert isinstance(json_str, bytes) + assert OuterJson.loads(json_str) == { + "a": "foobarfoo", + "nested": {"a": "hellobar"}, + "nested2": {"a": 31}, + } + + assert await json.strappend( + glide_client, key, OuterJson.dumps("bar"), "$.nested" + ) == [None] + + with pytest.raises(RequestError): + await json.strappend(glide_client, key, OuterJson.dumps("bar"), ".nested") + + with pytest.raises(RequestError): + await json.strappend(glide_client, key, OuterJson.dumps("bar")) + + assert ( + await json.strappend( + glide_client, key, OuterJson.dumps("try"), "$.non_existing_path" + ) + == [] + ) + with pytest.raises(RequestError): + await json.strappend( + glide_client, key, OuterJson.dumps("try"), "non_existing_path" + ) + + with pytest.raises(RequestError): + await json.strappend( + glide_client, "non_exiting_key", OuterJson.dumps("try") + )