diff --git a/CHANGELOG.md b/CHANGELOG.md index 339684f4ad..3cf1a3a7ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Python: Add JSON.CLEAR command ([#2418](https://github.com/valkey-io/valkey-glide/pull/2418)) * Python: Add JSON.TYPE command ([#2409](https://github.com/valkey-io/valkey-glide/pull/2409)) * Python: Add JSON.NUMINCRBY command ([#2448](https://github.com/valkey-io/valkey-glide/pull/2448)) +* Python: Add JSON.NUMMULTBY command ([#2458](https://github.com/valkey-io/valkey-glide/pull/2458)) * Java: Added `FT.CREATE` ([#2414](https://github.com/valkey-io/valkey-glide/pull/2414)) * Java: Added `FT.DROPINDEX` ([#2440](https://github.com/valkey-io/valkey-glide/pull/2440)) * Core: Update routing for commands from server modules ([#2461](https://github.com/valkey-io/valkey-glide/pull/2461)) diff --git a/python/python/glide/async_commands/server_modules/json.py b/python/python/glide/async_commands/server_modules/json.py index 31ee9fe10e..7da1c6c4aa 100644 --- a/python/python/glide/async_commands/server_modules/json.py +++ b/python/python/glide/async_commands/server_modules/json.py @@ -362,6 +362,48 @@ async def numincrby( return cast(Optional[bytes], await client.custom_command(args)) +async def nummultby( + client: TGlideClient, + key: TEncodable, + path: TEncodable, + number: Union[int, float], +) -> Optional[bytes]: + """ + Multiplies the JSON value(s) at the specified `path` by `number` 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 (TEncodable): The path within the JSON document. + number (Union[int, float]): The number to multiply by. + + Returns: + Optional[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. + + Examples: + >>> from glide import json + >>> await json.set(client, "doc", "$", '{"a": [], "b": [1], "c": [1, 2], "d": [1, 2, 3]}') + 'OK' + >>> await json.nummultby(client, "doc", "$.d[*]", 2) + b'[2,4,6]' # Multiplies each element in the `d` array by 2. + >>> await json.nummultby(client, "doc", ".c[1]", 2) + b'4' # Multiplies the second element in the `c` array by 2. + """ + args = ["JSON.NUMMULTBY", key, path, str(number)] + + return cast(Optional[bytes], await client.custom_command(args)) + + async def toggle( client: TGlideClient, key: TEncodable, diff --git a/python/python/tests/tests_server_modules/test_json.py b/python/python/tests/tests_server_modules/test_json.py index 18d7ff525b..794e885cfe 100644 --- a/python/python/tests/tests_server_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -486,3 +486,141 @@ async def test_json_numincrby(self, glide_client: TGlideClient): # Check for Overflow in legacy with pytest.raises(RequestError): await json.numincrby(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_nummultby(self, glide_client: TGlideClient): + key = get_random_string(10) + + json_value = { + "key1": 1, + "key2": 3.5, + "key3": {"nested_key": {"key1": [4, 5]}}, + "key4": [1, 2, 3], + "key5": 0, + "key6": "hello", + "key7": None, + "key8": {"nested_key": {"key1": 69}}, + "key9": 3.5953862697246314e307, + } + + # Set the initial JSON document at the key + assert await json.set(glide_client, key, "$", OuterJson.dumps(json_value)) == OK + + # Test JSONPath + # Multiply integer value (key1) by 5 + result = await json.nummultby(glide_client, key, "$.key1", 5) + assert result == b"[5]" # Expect 1 * 5 = 5 + + # Multiply float value (key2) by 2.5 + result = await json.nummultby(glide_client, key, "$.key2", 2.5) + assert result == b"[8.75]" # Expect 3.5 * 2.5 = 8.75 + + # Multiply nested object (key3.nested_key.key1[1]) by 7 + result = await json.nummultby(glide_client, key, "$.key3.nested_key.key1[1]", 7) + assert result == b"[35]" # Expect 5 * 7 = 35 + + # Multiply array element (key4[1]) by 1 + result = await json.nummultby(glide_client, key, "$.key4[1]", 1) + assert result == b"[2]" # Expect 2 * 1 = 2 + + # Multiply zero value (key5) by 10.23 (float number) + result = await json.nummultby(glide_client, key, "$.key5", 10.23) + assert result == b"[0]" # Expect 0 * 10.23 = 0 + + # Multiply a string value (key6) by a number + result = await json.nummultby(glide_client, key, "$.key6", 99) + assert result == b"[null]" # Expect null + + # Multiply a None value (key7) by a number + result = await json.nummultby(glide_client, key, "$.key7", 51) + assert result == b"[null]" # Expect null + + # Check multiplication for all numbers in the document using JSON Path + # key1: 5 * 5 = 25 + # key2: 8.75 * 5 = 43.75 + # key3.nested_key.key1[0]: 4 * 5 = 20 + # key3.nested_key.key1[1]: 35 * 5 = 175 + # key4[0]: 1 * 5 = 5 + # key4[1]: 2 * 5 = 10 + # key4[2]: 3 * 5 = 15 + # key5: 0 * 5 = 0 + # key8.nested_key.key1: 69 * 5 = 345 + # key9: 3.5953862697246314e307 * 5 = 1.7976931348623157e308 + result = await json.nummultby(glide_client, key, "$..*", 5) + assert ( + result + == b"[25,43.75,null,null,0,null,null,null,1.7976931348623157e+308,null,null,20,175,5,10,15,null,345]" + ) + + # Check for multiple path matches in JSONPath + # key1: 25 * 2 = 50 + # key8.nested_key.key1: 345 * 2 = 690 + result = await json.nummultby(glide_client, key, "$..key1", 2) + assert result == b"[50,null,690]" # After previous multiplications + + # Check for non-existent path in JSONPath + result = await json.nummultby(glide_client, key, "$.key10", 51) + assert result == b"[]" # Expect Empty Array + + # Check for non-existent key in JSONPath + with pytest.raises(RequestError): + await json.nummultby(glide_client, "non_existent_key", "$.key10", 51) + + # Check for Overflow in JSONPath + with pytest.raises(RequestError): + await json.nummultby(glide_client, key, "$.key9", 1.7976931348623157e308) + + # Multiply integer value (key1) by -12 + result = await json.nummultby(glide_client, key, "$.key1", -12) + assert result == b"[-600]" # Expect 50 * -12 = -600 + + # Multiply integer value (key1) by -0.5 + result = await json.nummultby(glide_client, key, "$.key1", -0.5) + assert result == b"[300]" # Expect -600 * -0.5 = 300 + + # Test Legacy Path + # Multiply int value (key1) by 5 (integer) + result = await json.nummultby(glide_client, key, "key1", 5) + assert result == b"1500" # Expect 300 * 5 = -1500 + + # Multiply int value (key1) by -5.5 (float number) + result = await json.nummultby(glide_client, key, "key1", -5.5) + assert result == b"-8250" # Expect -150 * -5.5 = -8250 + + # Multiply int float (key2) by 2.5 (a float number) + result = await json.nummultby(glide_client, key, "key2", 2.5) + assert result == b"109.375" # Expect 43.75 * 2.5 = 109.375 + + # Multiply nested value (key3.nested_key.key1[0]) by 7 + result = await json.nummultby(glide_client, key, "key3.nested_key.key1[0]", 7) + assert result == b"140" # Expect 20 * 7 = 140 + + # Multiply array element (key4[1]) by 1 + result = await json.nummultby(glide_client, key, "key4[1]", 1) + assert result == b"10" # Expect 10 * 1 = 10 + + # Multiply a float value (key5) by 10.2 (a float number) + result = await json.nummultby(glide_client, key, "key5", 10.2) + assert result == b"0" # Expect 0 * 10.2 = 0 + + # Check for multiple path matches in legacy and assure that the result of the last updated value is returned + # last updated value is key8.nested_key.key1: 690 * 2 = 1380 + result = await json.nummultby(glide_client, key, "..key1", 2) + 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") + assert result == b"[-16500,[140,175],1380]" + + # Check for non-existent path in legacy + with pytest.raises(RequestError): + await json.nummultby(glide_client, key, ".key10", 51) + + # Check for non-existent key in legacy + with pytest.raises(RequestError): + await json.nummultby(glide_client, "non_existent_key", ".key10", 51) + + # Check for Overflow in legacy + with pytest.raises(RequestError): + await json.nummultby(glide_client, key, ".key9", 1.7976931348623157e308)