1515from llama_index .core .vector_stores .types import VectorStoreQuery
1616
1717
18+ def _cypher_ident (value : str ) -> str :
19+ """Backtick-escape a Cypher identifier (label, rel-type, property key).
20+
21+ Doubles any embedded backticks per the OpenCypher spec so that arbitrary
22+ strings can be used safely as identifiers without Cypher injection.
23+ """
24+ return f"`{ value .replace ('`' , '``' )} `"
25+
26+
1827class CoordinodePropertyGraphStore (PropertyGraphStore ):
1928 """LlamaIndex ``PropertyGraphStore`` backed by CoordiNode.
2029
@@ -60,17 +69,21 @@ def get(
6069 nodes : list [LabelledNode ] = []
6170
6271 if ids :
63- for node_id in ids :
64- result = self ._client .get_node (node_id )
65- if result is not None :
66- nodes .append (_node_result_to_labelled (node_id , result ))
72+ # Query by the stored n.id property (string adapter ID), not by the
73+ # graph-internal integer node ID that get_node() expects.
74+ cypher = "MATCH (n) WHERE n.id IN $ids RETURN n, n.id AS _nid LIMIT 1000"
75+ result = self ._client .cypher (cypher , params = {"ids" : ids })
76+ for row in result :
77+ node_data = row .get ("n" , {})
78+ node_id = str (row .get ("_nid" , "" ))
79+ nodes .append (_node_result_to_labelled (node_id , node_data ))
6780 elif properties :
68- where_clauses = " AND " .join (f"n.{ k } = ${ k } " for k in properties )
69- cypher = f"MATCH (n) WHERE { where_clauses } RETURN n, id(n) AS _id LIMIT 1000"
81+ where_clauses = " AND " .join (f"n.{ _cypher_ident ( k ) } = ${ k } " for k in properties )
82+ cypher = f"MATCH (n) WHERE { where_clauses } RETURN n, n.id AS _nid LIMIT 1000"
7083 result = self ._client .cypher (cypher , params = properties )
7184 for row in result :
7285 node_data = row .get ("n" , {})
73- node_id = str (row .get ("_id " , "" ))
86+ node_id = str (row .get ("_nid " , "" ))
7487 nodes .append (_node_result_to_labelled (node_id , node_data ))
7588
7689 return nodes
@@ -86,12 +99,14 @@ def get_triplets(
8699 conditions : list [str ] = []
87100 params : dict [str , Any ] = {}
88101
102+ if properties or ids :
103+ raise NotImplementedError ("get_triplets() does not yet support filtering by properties or ids" )
89104 if entity_names :
90105 conditions .append ("(n.name IN $entity_names OR m.name IN $entity_names)" )
91106 params ["entity_names" ] = entity_names
92107 if relation_names :
93- rel_filter = "|" . join ( relation_names )
94- # Inline into pattern — CoordiNode supports dynamic type lists
108+ # Escape each type name to prevent Cypher injection
109+ rel_filter = "|" . join ( _cypher_ident ( t ) for t in relation_names )
95110 rel_pattern = f"[r:{ rel_filter } ]"
96111 else :
97112 rel_pattern = "[r]"
@@ -129,17 +144,16 @@ def get_rel_map(
129144 if not graph_nodes :
130145 return []
131146
132- ids = [n .id for n in graph_nodes ]
133- # ignore_rels: OpenCypher doesn't support dynamic type exclusion in patterns;
134- # would require WHERE NOT type(r) IN $ignore_rels — added when needed.
147+ node_ids = [n .id for n in graph_nodes ]
148+ ignored = set (ignore_rels ) if ignore_rels else set ()
135149
136150 cypher = (
137151 f"MATCH (n)-[r*1..{ depth } ]->(m) "
138152 f"WHERE id(n) IN $ids "
139153 f"RETURN n, r, m, id(n) AS _src_id, id(m) AS _dst_id "
140154 f"LIMIT { limit } "
141155 )
142- result = self ._client .cypher (cypher , params = {"ids" : ids })
156+ result = self ._client .cypher (cypher , params = {"ids" : node_ids })
143157
144158 triplets : list [list [LabelledNode ]] = []
145159 for row in result :
@@ -149,6 +163,10 @@ def get_rel_map(
149163 dst_id = str (row .get ("_dst_id" , "" ))
150164 # Variable-length path [r*1..N] returns a list of relationship dicts.
151165 rels = row .get ("r" , [])
166+ # Skip paths that contain any ignored relationship type.
167+ if ignored and isinstance (rels , list ):
168+ if any (isinstance (r , dict ) and r .get ("type" ) in ignored for r in rels ):
169+ continue
152170 if isinstance (rels , list ) and rels :
153171 first_rel = rels [0 ]
154172 rel_label = first_rel .get ("type" , "RELATED" ) if isinstance (first_rel , dict ) else str (first_rel )
@@ -174,7 +192,8 @@ def upsert_relations(self, relations: list[Relation]) -> None:
174192 for rel in relations :
175193 props = rel .properties or {}
176194 cypher = (
177- f"MATCH (src {{id: $src_id}}), (dst {{id: $dst_id}}) MERGE (src)-[r:{ rel .label } ]->(dst) SET r += $props"
195+ f"MATCH (src {{id: $src_id}}), (dst {{id: $dst_id}}) "
196+ f"MERGE (src)-[r:{ _cypher_ident (rel .label )} ]->(dst) SET r += $props"
178197 )
179198 self ._client .cypher (
180199 cypher ,
@@ -193,6 +212,8 @@ def delete(
193212 ids : list [str ] | None = None ,
194213 ) -> None :
195214 """Delete nodes and/or relations matching given criteria."""
215+ if relation_names or properties :
216+ raise NotImplementedError ("delete() does not yet support filtering by relation_names or properties" )
196217 if ids :
197218 cypher = "MATCH (n) WHERE id(n) IN $ids DETACH DELETE n"
198219 self ._client .cypher (cypher , params = {"ids" : ids })
0 commit comments