From e07e739f70b18b167d12b550523bcebe9fc8bfcf Mon Sep 17 00:00:00 2001 From: Vardhman Gupta Date: Thu, 1 Oct 2026 16:16:23 +0530 Subject: [PATCH] fix(graph): preserve existing node ntype and props in add_node Closes #271. - Update GraphStore.add_node to use if_not_exists for ntype and props. - Prevent add_edge and link_docs placeholder node initialization from erasing pre-existing entity metadata. - Add unit tests verifying node metadata preservation across edge additions, doc linkages, and partial updates. --- src/dynavec/graph.py | 42 ++++++++++++++++++++++++---------- tests/test_graph_properties.py | 39 +++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/dynavec/graph.py b/src/dynavec/graph.py index ab289a8..37d5e86 100644 --- a/src/dynavec/graph.py +++ b/src/dynavec/graph.py @@ -188,20 +188,38 @@ def _node_pk(ns: str, entity_id: str) -> str: def add_node( self, ns: str, entity_id: str, ntype: str | None = None, props: Props | None = None ) -> None: + set_clauses = [ + "kind = if_not_exists(kind, :k)", + "ns = if_not_exists(ns, :ns)", + "entity_id = if_not_exists(entity_id, :eid)", + "edges = if_not_exists(edges, :empty_list)", + "docs = if_not_exists(docs, :empty_list)", + ] + values: dict[str, Any] = { + ":k": "node", + ":ns": ns, + ":eid": entity_id, + ":empty_list": [], + } + + if ntype is not None: + set_clauses.append("ntype = :t") + values[":t"] = ntype + else: + set_clauses.append("ntype = if_not_exists(ntype, :null)") + values[":null"] = None + + if props is not None: + set_clauses.append("props = :p") + values[":p"] = props + else: + set_clauses.append("props = if_not_exists(props, :empty_map)") + values[":empty_map"] = {} + self._table.update_item( Key={"pk": self._node_pk(ns, entity_id)}, - UpdateExpression=( - "SET kind = :k, ns = :ns, entity_id = :eid, ntype = :t, props = :p, " - "edges = if_not_exists(edges, :empty), docs = if_not_exists(docs, :empty)" - ), - ExpressionAttributeValues={ - ":k": "node", - ":ns": ns, - ":eid": entity_id, - ":t": ntype, - ":p": props or {}, - ":empty": [], - }, + UpdateExpression="SET " + ", ".join(set_clauses), + ExpressionAttributeValues=values, ) @retry() diff --git a/tests/test_graph_properties.py b/tests/test_graph_properties.py index bd34d52..18ab68f 100644 --- a/tests/test_graph_properties.py +++ b/tests/test_graph_properties.py @@ -127,3 +127,42 @@ def test_graph_add_edge_forwards_weight_props_and_bidirectional_values(): ("tenant", "source", "connects", "target", 3.5, props), ("tenant", "target", "connects", "source", 3.5, props), ] + + +def test_add_edge_and_link_docs_preserve_existing_node_metadata(store): + store.add_node("ns", "alice", ntype="Person", props={"role": "Admin", "dept": "Eng"}) + store.add_node("ns", "bob", ntype="Person", props={"role": "Dev", "dept": "Design"}) + + store.add_edge("ns", "alice", "manages", "bob", weight=1.0) + + alice_node = store.get_node("ns", "alice") + bob_node = store.get_node("ns", "bob") + + assert alice_node["ntype"] == "Person" + assert alice_node["props"] == {"role": "Admin", "dept": "Eng"} + assert len(alice_node["edges"]) == 1 + + assert bob_node["ntype"] == "Person" + assert bob_node["props"] == {"role": "Dev", "dept": "Design"} + + store.link_docs("ns", "alice", ["doc-1", "doc-2"]) + alice_node_after_link = store.get_node("ns", "alice") + + assert alice_node_after_link["ntype"] == "Person" + assert alice_node_after_link["props"] == {"role": "Admin", "dept": "Eng"} + assert alice_node_after_link["docs"] == ["doc-1", "doc-2"] + + +def test_add_node_partial_update_preserves_unspecified_attributes(store): + store.add_node("ns", "user", ntype="Person", props={"age": Decimal("30")}) + + store.add_node("ns", "user", props={"age": Decimal("31"), "city": "NYC"}) + node = store.get_node("ns", "user") + assert node["ntype"] == "Person" + assert node["props"] == {"age": Decimal("31"), "city": "NYC"} + + store.add_node("ns", "user", ntype="Admin") + node_after_ntype = store.get_node("ns", "user") + assert node_after_ntype["ntype"] == "Admin" + assert node_after_ntype["props"] == {"age": Decimal("31"), "city": "NYC"} +