Skip to content

Commit 664cc8e

Browse files
authored
Merge pull request #2507 from valkey-io/python/json.mget
Python - Implement JSON.MGET command
2 parents d052c4c + 2f9e62f commit 664cc8e

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* Python: Add commands FT.ALIASADD, FT.ALIASDEL, FT.ALIASUPDATE([#2471](https://github.com/valkey-io/valkey-glide/pull/2471))
1313
* Python: Python FT.DROPINDEX command ([#2437](https://github.com/valkey-io/valkey-glide/pull/2437))
1414
* Python: Python: Added FT.CREATE command([#2413](https://github.com/valkey-io/valkey-glide/pull/2413))
15+
* Python: Add JSON.MGET command ([#2507](https://github.com/valkey-io/valkey-glide/pull/2507))
1516
* Python: Add JSON.ARRLEN command ([#2403](https://github.com/valkey-io/valkey-glide/pull/2403))
1617
* Python: Add JSON.CLEAR command ([#2418](https://github.com/valkey-io/valkey-glide/pull/2418))
1718
* Python: Add JSON.TYPE command ([#2409](https://github.com/valkey-io/valkey-glide/pull/2409))

python/python/glide/async_commands/server_modules/json.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,53 @@ async def get(
204204
return cast(TJsonResponse[Optional[bytes]], await client.custom_command(args))
205205

206206

207+
async def mget(
208+
client: TGlideClient,
209+
keys: List[TEncodable],
210+
path: TEncodable,
211+
) -> List[Optional[bytes]]:
212+
"""
213+
Retrieves the JSON values at the specified `path` stored at multiple `keys`.
214+
215+
Note:
216+
In cluster mode, if keys in `keys` map to different hash slots, the command
217+
will be split across these slots and executed separately for each. This means the command
218+
is atomic only at the slot level. If one or more slot-specific requests fail, the entire
219+
call will return the first encountered error, even though some requests may have succeeded
220+
while others did not. If this behavior impacts your application logic, consider splitting
221+
the request into sub-requests per slot to ensure atomicity.
222+
223+
Args:
224+
client (TGlideClient): The client to execute the command.
225+
keys (List[TEncodable]): A list of keys for the JSON documents.
226+
path (TEncodable): The path within the JSON documents.
227+
228+
Returns:
229+
List[Optional[bytes]]:
230+
For JSONPath (`path` starts with `$`):
231+
Returns a list of byte representations of the values found at the given path for each key.
232+
If `path` does not exist within the key, the entry will be an empty array.
233+
For legacy path (`path` doesn't starts with `$`):
234+
Returns a list of byte representations of the values found at the given path for each key.
235+
If `path` does not exist within the key, the entry will be None.
236+
If a key doesn't exist, the corresponding list element will be None.
237+
238+
239+
Examples:
240+
>>> from glide import json as glideJson
241+
>>> import json
242+
>>> json_strs = await glideJson.mget(client, ["doc1", "doc2"], "$")
243+
>>> [json.loads(js) for js in json_strs] # Parse JSON strings to Python data
244+
[[{"a": 1.0, "b": 2}], [{"a": 2.0, "b": {"a": 3.0, "b" : 4.0}}]] # JSON objects retrieved from keys `doc1` and `doc2`
245+
>>> await glideJson.mget(client, ["doc1", "doc2"], "$.a")
246+
[b"[1.0]", b"[2.0]"] # Returns values at path '$.a' for the JSON documents stored at `doc1` and `doc2`.
247+
>>> await glideJson.mget(client, ["doc1"], "$.non_existing_path")
248+
[None] # Returns an empty array since the path '$.non_existing_path' does not exist in the JSON document stored at `doc1`.
249+
"""
250+
args = ["JSON.MGET"] + keys + [path]
251+
return cast(TJsonResponse[Optional[bytes]], await client.custom_command(args))
252+
253+
207254
async def arrappend(
208255
client: TGlideClient,
209256
key: TEncodable,

python/python/tests/tests_server_modules/test_json.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,149 @@ async def test_json_get_formatting(self, glide_client: TGlideClient):
162162
expected_result = b'[\n~{\n~~"a":*1.0,\n~~"b":*2,\n~~"c":*{\n~~~"d":*3,\n~~~"e":*4\n~~}\n~}\n]'
163163
assert result == expected_result
164164

165+
@pytest.mark.parametrize("cluster_mode", [True, False])
166+
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
167+
async def test_json_mget(self, glide_client: TGlideClient):
168+
key1 = get_random_string(5)
169+
key2 = get_random_string(5)
170+
171+
json1_value = {"a": 1.0, "b": {"a": 1, "b": 2.5, "c": True}}
172+
json2_value = {"a": 3.0, "b": {"a": 1, "b": 4}}
173+
174+
assert (
175+
await json.set(glide_client, key1, "$", OuterJson.dumps(json1_value)) == OK
176+
)
177+
assert (
178+
await json.set(glide_client, key2, "$", OuterJson.dumps(json2_value)) == OK
179+
)
180+
181+
# Test with root JSONPath
182+
result = await json.mget(
183+
glide_client,
184+
[key1, key2],
185+
"$",
186+
)
187+
expected_result = [
188+
b'[{"a":1.0,"b":{"a":1,"b":2.5,"c":true}}]',
189+
b'[{"a":3.0,"b":{"a":1,"b":4}}]',
190+
]
191+
assert result == expected_result
192+
193+
# Retrieves the full JSON objects from multiple keys.
194+
result = await json.mget(
195+
glide_client,
196+
[key1, key2],
197+
".",
198+
)
199+
expected_result = [
200+
b'{"a":1.0,"b":{"a":1,"b":2.5,"c":true}}',
201+
b'{"a":3.0,"b":{"a":1,"b":4}}',
202+
]
203+
assert result == expected_result
204+
205+
result = await json.mget(
206+
glide_client,
207+
[key1, key2],
208+
"$.a",
209+
)
210+
expected_result = [b"[1.0]", b"[3.0]"]
211+
assert result == expected_result
212+
213+
# Retrieves the value of the 'b' field for multiple keys.
214+
result = await json.mget(
215+
glide_client,
216+
[key1, key2],
217+
"$.b",
218+
)
219+
expected_result = [b'[{"a":1,"b":2.5,"c":true}]', b'[{"a":1,"b":4}]']
220+
assert result == expected_result
221+
222+
# Retrieves all values of 'b' fields using recursive path for multiple keys
223+
result = await json.mget(
224+
glide_client,
225+
[key1, key2],
226+
"$..b",
227+
)
228+
expected_result = [b'[{"a":1,"b":2.5,"c":true},2.5]', b'[{"a":1,"b":4},4]']
229+
assert result == expected_result
230+
231+
# retrieves the value of the nested 'b.b' field for multiple keys
232+
result = await json.mget(
233+
glide_client,
234+
[key1, key2],
235+
".b.b",
236+
)
237+
expected_result = [b"2.5", b"4"]
238+
assert result == expected_result
239+
240+
# JSONPath that exists in only one of the keys
241+
result = await json.mget(
242+
glide_client,
243+
[key1, key2],
244+
"$.b.c",
245+
)
246+
expected_result = [b"[true]", b"[]"]
247+
assert result == expected_result
248+
249+
# Legacy path that exists in only one of the keys
250+
result = await json.mget(
251+
glide_client,
252+
[key1, key2],
253+
".b.c",
254+
)
255+
expected_result = [b"true", None]
256+
assert result == expected_result
257+
258+
# JSONPath doesn't exist
259+
result = await json.mget(
260+
glide_client,
261+
[key1, key2],
262+
"$non_existing_path",
263+
)
264+
expected_result = [b"[]", b"[]"]
265+
assert result == expected_result
266+
267+
# Legacy path doesn't exist
268+
result = await json.mget(
269+
glide_client,
270+
[key1, key2],
271+
".non_existing_path",
272+
)
273+
assert result == [None, None]
274+
275+
# JSONPath one key doesn't exist
276+
result = await json.mget(
277+
glide_client,
278+
[key1, "{non_existing_key}"],
279+
"$.a",
280+
)
281+
assert result == [b"[1.0]", None]
282+
283+
# Legacy path one key doesn't exist
284+
result = await json.mget(
285+
glide_client,
286+
[key1, "{non_existing_key}"],
287+
".a",
288+
)
289+
assert result == [b"1.0", None]
290+
291+
# Both keys don't exist
292+
result = await json.mget(
293+
glide_client,
294+
["{non_existing_key}1", "{non_existing_key}2"],
295+
"$a",
296+
)
297+
assert result == [None, None]
298+
299+
# Test with only one key
300+
result = await json.mget(
301+
glide_client,
302+
[key1],
303+
"$.a",
304+
)
305+
expected_result = [b"[1.0]"]
306+
assert result == expected_result
307+
165308
@pytest.mark.parametrize("cluster_mode", [True, False])
166309
@pytest.mark.parametrize("protocol", [ProtocolVersion.RESP2, ProtocolVersion.RESP3])
167310
async def test_json_del(self, glide_client: TGlideClient):

0 commit comments

Comments
 (0)