From a2ea32e1991569ea44d106dfdaae420a441389d2 Mon Sep 17 00:00:00 2001 From: Shoham Elias <116083498+shohamazon@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:50:34 +0300 Subject: [PATCH] Python: adds JSON.CLEAR command (#2418) Signed-off-by: Shoham Elias --- CHANGELOG.md | 1 + .../async_commands/server_modules/json.py | 52 +++++++++++++++++++ .../tests/tests_server_modules/test_json.py | 47 +++++++++++++++++ 3 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee5dda24f..f8bc293a82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ #### Changes * Python: Add JSON.ARRLEN command ([#2403](https://github.com/valkey-io/valkey-glide/pull/2403)) +* Python: Add JSON.CLEAR command ([#2418](https://github.com/valkey-io/valkey-glide/pull/2418)) #### 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 d1709806bc..1864132451 100644 --- a/python/python/glide/async_commands/server_modules/json.py +++ b/python/python/glide/async_commands/server_modules/json.py @@ -193,6 +193,58 @@ async def arrlen( ) +async def clear( + client: TGlideClient, + key: TEncodable, + path: Optional[str] = None, +) -> int: + """ + 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: + 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. + + Returns: + 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. + + 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. + >>> 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", "$") + b'[{"obj":{},"arr":[],"str":"","bool":false,"int":0,"float":0.0,"nullVal":null}]' + >>> await json.clear(client, "doc", "$.*") + 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' + >>> 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]") + 0 # No elements cleared since specified slice has already been cleared. + >>> await json.get(client, "doc", "$..a") + b'[1,[5,0,0],true,"value",3.5,{"foo":"foo"}]' + + >>> await json.clear(client, "doc", "$..a") + 6 # All numeric, boolean, and string values across paths are cleared. + >>> await json.get(client, "doc", "$..a") + b'[0,[],false,"",0.0,{}]' + """ + args = ["JSON.CLEAR", key] + if path: + args.append(path) + + return cast(int, await client.custom_command(args)) + + async def delete( 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 a69c3010e2..d21b11686b 100644 --- a/python/python/tests/tests_server_modules/test_json.py +++ b/python/python/tests/tests_server_modules/test_json.py @@ -312,3 +312,50 @@ async def test_json_arrlen(self, glide_client: TGlideClient): assert await json.set(glide_client, key, "$", "[1, 2, 3, 4]") == OK assert await json.arrlen(glide_client, key) == 4 + + @pytest.mark.parametrize("cluster_mode", [True, False]) + @pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3]) + async def test_json_clear(self, glide_client: TGlideClient): + key = get_random_string(5) + + json_value = '{"obj":{"a":1, "b":2}, "arr":[1,2,3], "str": "foo", "bool": true, "int": 42, "float": 3.14, "nullVal": null}' + assert await json.set(glide_client, key, "$", json_value) == OK + + assert await json.clear(glide_client, key, "$.*") == 6 + result = await json.get(glide_client, key, "$") + assert ( + result + == b'[{"obj":{},"arr":[],"str":"","bool":false,"int":0,"float":0.0,"nullVal":null}]' + ) + assert await json.clear(glide_client, key, "$.*") == 0 + + assert await json.set(glide_client, key, "$", json_value) == OK + assert await json.clear(glide_client, key, "*") == 6 + + json_value = '{"a": 1, "b": {"a": [5, 6, 7], "b": {"a": true}}, "c": {"a": "value", "b": {"a": 3.5}}, "d": {"a": {"foo": "foo"}}, "nullVal": null}' + assert await json.set(glide_client, key, "$", json_value) == OK + + assert await json.clear(glide_client, key, "b.a[1:3]") == 2 + assert await json.clear(glide_client, key, "b.a[1:3]") == 0 + assert ( + await json.get(glide_client, key, "$..a") + == b'[1,[5,0,0],true,"value",3.5,{"foo":"foo"}]' + ) + assert await json.clear(glide_client, key, "..a") == 6 + assert await json.get(glide_client, key, "$..a") == b'[0,[],false,"",0.0,{}]' + + assert await json.clear(glide_client, key, "$..a") == 0 + + # Path doesn't exists + assert await json.clear(glide_client, key, "$.path") == 0 + assert await json.clear(glide_client, key, "path") == 0 + + # Key doesn't exists + with pytest.raises(RequestError): + await json.clear(glide_client, "non_existing_key") + + with pytest.raises(RequestError): + await json.clear(glide_client, "non_existing_key", "$") + + with pytest.raises(RequestError): + await json.clear(glide_client, "non_existing_key", ".")