Skip to content

Commit f045c77

Browse files
committed
fix(adapters): use unconditional CREATE for edges; fix get_rel_map
LangChain adapter: - Remove WHERE NOT (pattern) guard — CoordiNode returns 0 rows silently instead of raising, so the guarded CREATE never executed - Remove try/except fallback (no longer needed) - Skip SET r += $props when props is empty — SET r += {} unsupported LlamaIndex adapter: - upsert_relations: same fix — skip SET r += $props when props is empty - get_rel_map: replace [r*1..N] variable-length path with single-hop [r]; variable-length paths don't serialize correctly in RETURN position - Adjust ignore_rels filter from NONE(rel IN r ...) to r.__type__ NOT IN (compatible with single-hop [r] pattern)
1 parent 13487c9 commit f045c77

4 files changed

Lines changed: 52 additions & 49 deletions

File tree

langchain-coordinode/langchain_coordinode/graph.py

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -113,30 +113,29 @@ def add_graph_documents(
113113
params={"name": node.id, "props": props},
114114
)
115115

116-
# ── Create relationships (idempotent: skip if already exists)
116+
# ── Create relationships ─────────────────────────────────────
117117
for rel in doc.relationships:
118118
src_label = _cypher_ident(rel.source.type or "Entity")
119119
dst_label = _cypher_ident(rel.target.type or "Entity")
120120
rel_type = _cypher_ident(rel.type)
121121
props = dict(rel.properties or {})
122-
# CoordiNode does not yet support MERGE for edges; use CREATE
123-
# guarded by NOT EXISTS to avoid duplicates on repeated calls.
124-
try:
122+
# CoordiNode does not support MERGE for edges or WHERE NOT
123+
# (pattern) guards — use unconditional CREATE. SET r += $props
124+
# is skipped when props is empty because SET r += {} is not
125+
# supported by all server versions.
126+
if props:
125127
self._client.cypher(
126128
f"MATCH (src:{src_label} {{name: $src}}) "
127129
f"MATCH (dst:{dst_label} {{name: $dst}}) "
128-
f"WHERE NOT (src)-[:{rel_type}]->(dst) "
129130
f"CREATE (src)-[r:{rel_type}]->(dst) SET r += $props",
130131
params={"src": rel.source.id, "dst": rel.target.id, "props": props},
131132
)
132-
except Exception: # noqa: BLE001
133-
# WHERE NOT EXISTS guard may not be supported on all server
134-
# versions; fall back to unconditional CREATE
133+
else:
135134
self._client.cypher(
136135
f"MATCH (src:{src_label} {{name: $src}}) "
137136
f"MATCH (dst:{dst_label} {{name: $dst}}) "
138-
f"CREATE (src)-[r:{rel_type}]->(dst) SET r += $props",
139-
params={"src": rel.source.id, "dst": rel.target.id, "props": props},
137+
f"CREATE (src)-[r:{rel_type}]->(dst)",
138+
params={"src": rel.source.id, "dst": rel.target.id},
140139
)
141140

142141
# ── Optionally link source document ───────────────────────────
@@ -148,21 +147,12 @@ def add_graph_documents(
148147
)
149148
for node in doc.nodes:
150149
label = _cypher_ident(node.type or "Entity")
151-
try:
152-
self._client.cypher(
153-
f"MATCH (d:__Document__ {{id: $doc_id}}) "
154-
f"MATCH (n:{label} {{name: $name}}) "
155-
f"WHERE NOT (d)-[:MENTIONS]->(n) "
156-
f"CREATE (d)-[:MENTIONS]->(n)",
157-
params={"doc_id": src_id, "name": node.id},
158-
)
159-
except Exception: # noqa: BLE001
160-
self._client.cypher(
161-
f"MATCH (d:__Document__ {{id: $doc_id}}) "
162-
f"MATCH (n:{label} {{name: $name}}) "
163-
f"CREATE (d)-[:MENTIONS]->(n)",
164-
params={"doc_id": src_id, "name": node.id},
165-
)
150+
self._client.cypher(
151+
f"MATCH (d:__Document__ {{id: $doc_id}}) "
152+
f"MATCH (n:{label} {{name: $name}}) "
153+
f"CREATE (d)-[:MENTIONS]->(n)",
154+
params={"doc_id": src_id, "name": node.id},
155+
)
166156

167157
# Invalidate cached schema so next access reflects new data
168158
self._schema = None

llama-index-coordinode/llama_index/graph_stores/coordinode/base.py

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -172,16 +172,19 @@ def get_rel_map(
172172
node_ids = [n.id for n in graph_nodes]
173173
ignored = list(ignore_rels) if ignore_rels else []
174174

175-
# Push ignore_rels filter into Cypher so LIMIT applies after filtering;
176-
# a Python-side filter after LIMIT would silently truncate valid results.
177175
params: dict[str, object] = {"ids": node_ids}
178176
ignore_clause = ""
179177
if ignored:
180-
ignore_clause = " AND NONE(rel IN r WHERE type(rel) IN $ignored_rels)"
178+
# Single-hop [r]: filter with r.__type__ NOT IN $ignored_rels.
179+
ignore_clause = " AND NOT r.__type__ IN $ignored_rels"
181180
params["ignored_rels"] = ignored
182181

182+
# CoordiNode does not support variable-length path [r*1..N] in RETURN
183+
# position (result serialisation is undefined for path lists). Use a
184+
# single-hop pattern; multi-hop traversal is a future enhancement.
185+
_ = depth # depth parameter reserved; currently single-hop only
183186
cypher = (
184-
f"MATCH (n)-[r*1..{depth}]->(m) "
187+
f"MATCH (n)-[r]->(m) "
185188
f"WHERE n.id IN $ids{ignore_clause} "
186189
f"RETURN n, r, m, n.id AS _src_id, m.id AS _dst_id "
187190
f"LIMIT {limit}"
@@ -194,12 +197,11 @@ def get_rel_map(
194197
dst_data = row.get("m", {})
195198
src_id = str(row.get("_src_id", ""))
196199
dst_id = str(row.get("_dst_id", ""))
197-
# Variable-length path [r*1..N] returns a list of relationship dicts.
198-
rels = row.get("r", [])
199-
if isinstance(rels, list) and rels:
200-
first_rel = rels[0]
201-
# CoordiNode: use __type__ key instead of "type" — type() returns null
202-
rel_label = first_rel.get("__type__") or first_rel.get("type", "RELATED") if isinstance(first_rel, dict) else str(first_rel)
200+
# Single-hop [r] returns the relationship as a dict.
201+
# CoordiNode: use __type__ key — type() returns null.
202+
r_val = row.get("r", {})
203+
if isinstance(r_val, dict):
204+
rel_label = r_val.get("__type__") or r_val.get("type") or "RELATED"
203205
else:
204206
rel_label = "RELATED"
205207
src = _node_result_to_labelled(src_id, src_data)
@@ -223,20 +225,25 @@ def upsert_relations(self, relations: list[Relation]) -> None:
223225
props = rel.properties or {}
224226
label = _cypher_ident(rel.label)
225227
# CoordiNode does not yet support MERGE for edge patterns; use CREATE.
228+
# SET r += $props is skipped when props is empty — SET r += {} is
229+
# not supported by all server versions.
226230
# Note: repeated calls will create duplicate edges until MERGE for
227231
# edges is implemented server-side.
228-
cypher = (
229-
f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) "
230-
f"CREATE (src)-[r:{label}]->(dst) SET r += $props"
231-
)
232-
self._client.cypher(
233-
cypher,
234-
params={
235-
"src_id": rel.source_id,
236-
"dst_id": rel.target_id,
237-
"props": props,
238-
},
239-
)
232+
if props:
233+
cypher = (
234+
f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) "
235+
f"CREATE (src)-[r:{label}]->(dst) SET r += $props"
236+
)
237+
self._client.cypher(
238+
cypher,
239+
params={"src_id": rel.source_id, "dst_id": rel.target_id, "props": props},
240+
)
241+
else:
242+
cypher = f"MATCH (src {{id: $src_id}}) MATCH (dst {{id: $dst_id}}) CREATE (src)-[r:{label}]->(dst)"
243+
self._client.cypher(
244+
cypher,
245+
params={"src_id": rel.source_id, "dst_id": rel.target_id},
246+
)
240247

241248
def delete(
242249
self,

tests/integration/adapters/test_langchain.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def graph():
2727

2828
# ── Basic connectivity ────────────────────────────────────────────────────────
2929

30+
3031
def test_connect(graph):
3132
assert graph is not None
3233

@@ -47,6 +48,7 @@ def test_refresh_schema_does_not_raise(graph):
4748

4849
# ── Cypher query ──────────────────────────────────────────────────────────────
4950

51+
5052
def test_query_returns_list(graph):
5153
result = graph.query("RETURN 1 AS n")
5254
assert isinstance(result, list)
@@ -61,6 +63,7 @@ def test_query_count(graph):
6163

6264
# ── add_graph_documents ───────────────────────────────────────────────────────
6365

66+
6467
@pytest.fixture
6568
def unique_tag():
6669
return uuid.uuid4().hex[:8]
@@ -95,8 +98,7 @@ def test_add_graph_documents_creates_relationship(graph, unique_tag):
9598

9699
# Verify the relationship was created, not just the source node.
97100
result = graph.query(
98-
"MATCH (a:LCPerson2 {name: $src})-[r:LC_RESEARCHES]->(b:LCConcept {name: $dst}) "
99-
"RETURN count(r) AS cnt",
101+
"MATCH (a:LCPerson2 {name: $src})-[r:LC_RESEARCHES]->(b:LCConcept {name: $dst}) RETURN count(r) AS cnt",
100102
params={"src": f"Charlie-{unique_tag}", "dst": f"GraphRAG-{unique_tag}"},
101103
)
102104
assert result[0]["cnt"] >= 1, f"relationship not found: {result}"

tests/integration/adapters/test_llama_index.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def tag():
3030

3131
# ── Basic connectivity ────────────────────────────────────────────────────────
3232

33+
3334
def test_connect(store):
3435
assert store is not None
3536

@@ -47,6 +48,7 @@ def test_structured_query_literal(store):
4748

4849
# ── Node operations ───────────────────────────────────────────────────────────
4950

51+
5052
def test_upsert_and_get_nodes(store, tag):
5153
nodes = [
5254
EntityNode(label="LITestPerson", name=f"Alice-{tag}", properties={"role": "researcher"}),
@@ -80,6 +82,7 @@ def test_get_by_id(store, tag):
8082

8183
# ── Relation operations ───────────────────────────────────────────────────────
8284

85+
8386
def test_upsert_and_get_triplets(store, tag):
8487
src = EntityNode(label="LIRelPerson", name=f"Src-{tag}")
8588
dst = EntityNode(label="LIRelConcept", name=f"Dst-{tag}")
@@ -121,6 +124,7 @@ def test_get_rel_map(store, tag):
121124

122125
# ── Delete ────────────────────────────────────────────────────────────────────
123126

127+
124128
def test_delete_by_id(store, tag):
125129
node = EntityNode(label="LIDelete", name=f"Del-{tag}")
126130
store.upsert_nodes([node])

0 commit comments

Comments
 (0)