fix(local): isolate payload values across wildcard targets (#1438)

* fix(local): isolate payload values across wildcard targets

* refactor: copy once per wildcard element, move the tests to local

---------

Co-authored-by: George Panchuk <george.panchuk@qdrant.tech>
This commit is contained in:
Haokai Ding
2026-09-15 22:40:19 +07:00
committed by GitHub
co-authored by George Panchuk
parent e603c2d5c9
commit 9c826ce9b3
3 changed files with 64 additions and 7 deletions
+11 -7
View File
@@ -1,3 +1,4 @@
from copy import deepcopy
from typing import Any, Type
from qdrant_client.local.json_path_parser import JsonPathItem, JsonPathItemType
@@ -279,18 +280,21 @@ class WildcardIndexSetter(_ListSetter):
k_list: list[JsonPathItem],
value: dict[str, Any],
) -> None:
if len(k_list) == 0:
for i, item in enumerate(data):
for i, item in enumerate(data):
# a wildcard is the one path item that writes the same value to several places,
# so each element takes its own copy. Sharing a single object would make a later
# write scoped to one element, e.g. set_payload(key="arr[0].x"), reach them all.
item_value = deepcopy(value)
if len(k_list) == 0:
if isinstance(item, dict):
data[i].update(value)
item.update(item_value)
else:
data[i] = value
else:
for i, item in enumerate(data):
data[i] = item_value
else:
cls.set(
item,
k_list.copy(),
value,
item_value,
data,
JsonPathItem(item_type=JsonPathItemType.INDEX, index=i),
)
@@ -0,0 +1,19 @@
from qdrant_client import QdrantClient, models
def test_set_payload_keeps_array_elements_independent() -> None:
client = QdrantClient(":memory:")
client.create_collection("test", vectors_config={})
client.upsert(
"test",
[models.PointStruct(id=1, vector={}, payload={"items": [{"id": 1}, {"id": 2}]})],
)
client.set_payload("test", payload={"metadata": {"color": "blue"}}, points=[1], key="items[]")
client.set_payload("test", payload={"color": "red"}, points=[1], key="items[0].metadata")
assert client.retrieve("test", [1])[0].payload == {
"items": [
{"id": 1, "metadata": {"color": "red"}},
{"id": 2, "metadata": {"color": "blue"}},
]
}
@@ -11,6 +11,40 @@ from qdrant_client.local.payload_value_extractor import value_by_key
from qdrant_client.local.payload_value_setter import delete_value_by_key, set_value_by_key
@pytest.mark.parametrize(
"payload,key,target_key,expected_colors",
[
({"items": [{}, {}]}, "items[]", "items[0].metadata", ["red", "blue"]),
({"items": [None, None]}, "items[]", "items[0].metadata", ["red", "blue"]),
(
{"items": [{}, {}]},
"items[].nested",
"items[0].nested.metadata",
["red", "blue"],
),
(
{"items": [[{}, {}], [{}, {}]]},
"items[][]",
"items[0][0].metadata",
["red", "blue", "blue", "blue"],
),
(
{"items": [{"sub": [{}, {}]}, {"sub": [{}]}]},
"items[].sub[]",
"items[0].sub[0].metadata",
["red", "blue", "blue"],
),
],
)
def test_wildcard_set_keeps_array_elements_independent(
payload: dict[str, Any], key: str, target_key: str, expected_colors: list[str]
) -> None:
set_value_by_key(payload, parse_json_path(key), {"metadata": {"color": "blue"}})
set_value_by_key(payload, parse_json_path(target_key), {"color": "red"})
assert value_by_key(payload, f"{key}.metadata.color") == expected_colors
def test_parse_json_path() -> None:
jp_key = "a"
keys = parse_json_path(jp_key)