Repository: redis/redis-py

Issue:
Add support for JSON.MERGE
As defined in https://github.com/RedisJSON/RedisJSON/blob/e4608065002bdf585ad54fbe142e08be18d666a8/commands.json#LL169C1-L169C1


Discussion:

Resolving patch:
diff --git a/redis/commands/json/__init__.py b/redis/commands/json/__init__.py
--- a/redis/commands/json/__init__.py
+++ b/redis/commands/json/__init__.py
@@ -38,6 +38,7 @@ def __init__(
             "JSON.GET": self._decode,
             "JSON.MGET": bulk_of_jsons(self._decode),
             "JSON.SET": lambda r: r and nativestr(r) == "OK",
+            "JSON.MERGE": lambda r: r and nativestr(r) == "OK",
             "JSON.NUMINCRBY": self._decode,
             "JSON.NUMMULTBY": self._decode,
             "JSON.TOGGLE": self._decode,
diff --git a/redis/commands/json/commands.py b/redis/commands/json/commands.py
--- a/redis/commands/json/commands.py
+++ b/redis/commands/json/commands.py
@@ -253,6 +253,28 @@ def set(
             pieces.append("XX")
         return self.execute_command("JSON.SET", *pieces)
 
+    def merge(
+        self,
+        name: str,
+        path: str,
+        obj: JsonType,
+        decode_keys: Optional[bool] = False,
+    ) -> Optional[str]:
+        """
+        Sets or updates the JSON value at a path..
+
+        ``decode_keys`` If set to True, the keys of ``obj`` will be decoded
+        with utf-8.
+
+        For more information see `JSON.MERGE <https://redis.io/commands/json.merge>`_.
+        """
+        if decode_keys:
+            obj = decode_dict_keys(obj)
+
+        pieces = [name, str(path), self._encode(obj)]
+
+        return self.execute_command("JSON.MERGE", *pieces)
+
     def set_file(
         self,
         name: str,


Test patch:
diff --git a/tests/test_asyncio/test_json.py b/tests/test_asyncio/test_json.py
--- a/tests/test_asyncio/test_json.py
+++ b/tests/test_asyncio/test_json.py
@@ -39,6 +39,41 @@ async def test_json_get_jset(modclient: redis.Redis):
     assert await modclient.exists("foo") == 0
 
 
+@pytest.mark.redismod
+@skip_ifmodversion_lt("2.6.0", "ReJSON")  # todo: update after the release
+async def test_json_merge(modclient: redis.Redis):
+    # Test with root path $
+    assert await modclient.json().set(
+        "person_data",
+        "$",
+        {"person1": {"personal_data": {"name": "John"}}},
+    )
+    assert await modclient.json().merge(
+        "person_data", "$", {"person1": {"personal_data": {"hobbies": "reading"}}}
+    )
+    assert await modclient.json().get("person_data") == {
+        "person1": {"personal_data": {"name": "John", "hobbies": "reading"}}
+    }
+
+    # Test with root path path $.person1.personal_data
+    assert await modclient.json().merge(
+        "person_data", "$.person1.personal_data", {"country": "Israel"}
+    )
+    assert await modclient.json().get("person_data") == {
+        "person1": {
+            "personal_data": {"name": "John", "hobbies": "reading", "country": "Israel"}
+        }
+    }
+
+    # Test with null value to delete a value
+    assert await modclient.json().merge(
+        "person_data", "$.person1.personal_data", {"name": None}
+    )
+    assert await modclient.json().get("person_data") == {
+        "person1": {"personal_data": {"country": "Israel", "hobbies": "reading"}}
+    }
+
+
 @pytest.mark.redismod
 async def test_nonascii_setgetdelete(modclient: redis.Redis):
     assert await modclient.json().set("notascii", Path.root_path(), "hyvää-élève")
diff --git a/tests/test_json.py b/tests/test_json.py
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -47,6 +47,39 @@ def test_json_get_jset(client):
     assert client.exists("foo") == 0
 
 
+@pytest.mark.redismod
+@skip_ifmodversion_lt("2.6.0", "ReJSON")  # todo: update after the release
+def test_json_merge(client):
+    # Test with root path $
+    assert client.json().set(
+        "person_data",
+        "$",
+        {"person1": {"personal_data": {"name": "John"}}},
+    )
+    assert client.json().merge(
+        "person_data", "$", {"person1": {"personal_data": {"hobbies": "reading"}}}
+    )
+    assert client.json().get("person_data") == {
+        "person1": {"personal_data": {"name": "John", "hobbies": "reading"}}
+    }
+
+    # Test with root path path $.person1.personal_data
+    assert client.json().merge(
+        "person_data", "$.person1.personal_data", {"country": "Israel"}
+    )
+    assert client.json().get("person_data") == {
+        "person1": {
+            "personal_data": {"name": "John", "hobbies": "reading", "country": "Israel"}
+        }
+    }
+
+    # Test with null value to delete a value
+    assert client.json().merge("person_data", "$.person1.personal_data", {"name": None})
+    assert client.json().get("person_data") == {
+        "person1": {"personal_data": {"country": "Israel", "hobbies": "reading"}}
+    }
+
+
 @pytest.mark.redismod
 def test_nonascii_setgetdelete(client):
     assert client.json().set("notascii", Path.root_path(), "hyvää-élève")