@@ -10,7 +10,7 @@ def _parse_value(value_str: str) -> Any:
1010 if not value_str :
1111 return None
1212
13- # Parse object
13+ # Parse map
1414 if value_str .startswith ("{" ) and value_str .endswith ("}" ):
1515 inner = value_str [1 :- 1 ].strip ()
1616 result = {}
@@ -43,6 +43,7 @@ def _parse_value(value_str: str) -> Any:
4343 k , v = segment .split (":" , 1 )
4444 k = k .strip ().strip ("'\" " )
4545 result [k ] = _parse_value (v )
46+
4647 return result
4748
4849 # Parse list
@@ -68,6 +69,7 @@ def _parse_value(value_str: str) -> Any:
6869 in_string = None
6970 if inner [start_idx :]:
7071 items .append (_parse_value (inner [start_idx :]))
72+
7173 return items
7274
7375 # Parse boolean, float, int, or string
@@ -81,12 +83,91 @@ def _parse_value(value_str: str) -> Any:
8183 return False
8284 if value_str .lower () == "null" :
8385 return None
86+
8487 return value_str .strip ("'\" " )
8588
8689
90+ def _parse_prop_str (
91+ query : str , prop_str : str , prop_start : int , top_level_keys : set [str ]
92+ ) -> tuple [dict [str , Any ], dict [str , Any ]]:
93+ top_level : dict [str , Any ] = {}
94+ props : dict [str , Any ] = {}
95+ depth = 0
96+ in_string = None
97+ start_idx = 0
98+ for i , ch in enumerate (prop_str ):
99+ if in_string is None :
100+ if ch in ["'" , '"' ]:
101+ in_string = ch
102+ elif ch in ["{" , "[" ]:
103+ depth += 1
104+ elif ch in ["}" , "]" ]:
105+ depth -= 1
106+ elif ch == "," and depth == 0 :
107+ pair = prop_str [start_idx :i ].strip ()
108+ if ":" not in pair :
109+ snippet = _get_snippet (query , prop_start + start_idx )
110+ raise ValueError (f"Property syntax error near: `{ snippet } `." )
111+ k , v = pair .split (":" , 1 )
112+ k = k .strip ().strip ("'\" " )
113+
114+ if k in top_level_keys :
115+ top_level [k ] = _parse_value (v )
116+ else :
117+ props [k ] = _parse_value (v )
118+
119+ start_idx = i + 1
120+ else :
121+ if ch == in_string :
122+ in_string = None
123+
124+ if prop_str [start_idx :]:
125+ pair = prop_str [start_idx :].strip ()
126+ if ":" not in pair :
127+ snippet = _get_snippet (query , prop_start + start_idx )
128+ raise ValueError (f"Property syntax error near: `{ snippet } `." )
129+ k , v = pair .split (":" , 1 )
130+ k = k .strip ().strip ("'\" " )
131+
132+ if k in top_level_keys :
133+ top_level [k ] = _parse_value (v )
134+ else :
135+ props [k ] = _parse_value (v )
136+
137+ return top_level , props
138+
139+
140+ def _parse_labels_and_props (
141+ query : str , s : str , top_level_keys : set [str ]
142+ ) -> tuple [Optional [str ], dict [str , Any ], dict [str , Any ]]:
143+ prop_match = re .search (r"\{(.*)\}" , s )
144+ prop_str = ""
145+ if prop_match :
146+ prop_str = prop_match .group (1 )
147+ prop_start = query .index (prop_str , query .index (s ))
148+ s = s [: prop_match .start ()].strip ()
149+ alias_labels = re .split (r"[:&]" , s )
150+ raw_alias = alias_labels [0 ].strip ()
151+ final_alias = raw_alias if raw_alias else None
152+
153+ if prop_str :
154+ top_level , props = _parse_prop_str (query , prop_str , prop_start , top_level_keys )
155+ else :
156+ top_level = {}
157+ props = {}
158+
159+ label_list = [lbl .strip () for lbl in alias_labels [1 :]]
160+ if "labels" in props :
161+ props ["__labels" ] = props ["labels" ]
162+ props ["labels" ] = sorted (label_list )
163+
164+ return final_alias , top_level , props
165+
166+
87167def _get_snippet (q : str , idx : int , context : int = 15 ) -> str :
88168 start = max (0 , idx - context )
89169 end = min (len (q ), idx + context )
170+
90171 return q [start :end ].replace ("\n " , " " )
91172
92173
@@ -129,81 +210,6 @@ def from_gql_create(
129210 if not re .match (r"(?i)^create\b" , query ):
130211 raise ValueError ("Query must begin with 'CREATE' (case insensitive)." )
131212
132- def parse_prop_str (
133- prop_str : str , prop_start : int , top_level_keys : set [str ]
134- ) -> tuple [dict [str , Any ], dict [str , Any ]]:
135- top_level : dict [str , Any ] = {}
136- props : dict [str , Any ] = {}
137- depth = 0
138- in_string = None
139- start_idx = 0
140- for i , ch in enumerate (prop_str ):
141- if in_string is None :
142- if ch in ["'" , '"' ]:
143- in_string = ch
144- elif ch in ["{" , "[" ]:
145- depth += 1
146- elif ch in ["}" , "]" ]:
147- depth -= 1
148- elif ch == "," and depth == 0 :
149- pair = prop_str [start_idx :i ].strip ()
150- if ":" not in pair :
151- snippet = _get_snippet (query , prop_start + start_idx )
152- raise ValueError (f"Property syntax error near: `{ snippet } `." )
153- k , v = pair .split (":" , 1 )
154- k = k .strip ().strip ("'\" " )
155-
156- if k in top_level_keys :
157- top_level [k ] = _parse_value (v )
158- else :
159- props [k ] = _parse_value (v )
160-
161- start_idx = i + 1
162- else :
163- if ch == in_string :
164- in_string = None
165-
166- if prop_str [start_idx :]:
167- pair = prop_str [start_idx :].strip ()
168- if ":" not in pair :
169- snippet = _get_snippet (query , prop_start + start_idx )
170- raise ValueError (f"Property syntax error near: `{ snippet } `." )
171- k , v = pair .split (":" , 1 )
172- k = k .strip ().strip ("'\" " )
173-
174- if k in top_level_keys :
175- top_level [k ] = _parse_value (v )
176- else :
177- props [k ] = _parse_value (v )
178-
179- return top_level , props
180-
181- def parse_labels_and_props (
182- s : str , top_level_keys : set [str ]
183- ) -> tuple [Optional [str ], dict [str , Any ], dict [str , Any ]]:
184- prop_match = re .search (r"\{(.*)\}" , s )
185- prop_str = ""
186- if prop_match :
187- prop_str = prop_match .group (1 )
188- prop_start = query .index (prop_str , query .index (s ))
189- s = s [: prop_match .start ()].strip ()
190- alias_labels = re .split (r"[:&]" , s )
191- raw_alias = alias_labels [0 ].strip ()
192- final_alias = raw_alias if raw_alias else None
193-
194- if prop_str :
195- top_level , props = parse_prop_str (prop_str , prop_start , top_level_keys )
196- else :
197- top_level = {}
198- props = {}
199-
200- label_list = [lbl .strip () for lbl in alias_labels [1 :]]
201- if "labels" in props :
202- props ["__labels" ] = props ["labels" ]
203- props ["labels" ] = sorted (label_list )
204-
205- return final_alias , top_level , props
206-
207213 nodes = []
208214 relationships = []
209215 alias_to_id = {}
@@ -259,7 +265,7 @@ def parse_labels_and_props(
259265 node_m = node_pattern .match (part )
260266 if node_m :
261267 alias_labels_props = node_m .group (1 ).strip ()
262- alias , top_level , props = parse_labels_and_props ( alias_labels_props , node_top_level_keys )
268+ alias , top_level , props = _parse_labels_and_props ( query , alias_labels_props , node_top_level_keys )
263269 if not alias :
264270 alias = f"_anon_{ anonymous_count } "
265271 anonymous_count += 1
@@ -273,12 +279,12 @@ def parse_labels_and_props(
273279 rel_type = rel_m .group (2 ).replace (":" , "" ).strip ()
274280 right_node = rel_m .group (4 ).strip ()
275281
276- left_alias , _ , _ = parse_labels_and_props ( left_node , empty_set )
282+ left_alias , _ , _ = _parse_labels_and_props ( query , left_node , empty_set )
277283 if not left_alias or left_alias not in alias_to_id :
278284 snippet = _get_snippet (query , query .index (left_node ))
279285 raise ValueError (f"Relationship references unknown node alias: '{ left_alias } ' near: `{ snippet } `." )
280286
281- right_alias , _ , _ = parse_labels_and_props ( right_node , empty_set )
287+ right_alias , _ , _ = _parse_labels_and_props ( query , right_node , empty_set )
282288 if not right_alias or right_alias not in alias_to_id :
283289 snippet = _get_snippet (query , query .index (right_node ))
284290 raise ValueError (f"Relationship references unknown node alias: '{ right_alias } ' near: `{ snippet } `." )
@@ -288,7 +294,7 @@ def parse_labels_and_props(
288294 if rel_props_str :
289295 inner_str = rel_props_str .strip ("{}" ).strip ()
290296 prop_start = query .index (inner_str , query .index (inner_str ))
291- top_level , props = parse_prop_str ( inner_str , prop_start , rel_top_level_keys )
297+ top_level , props = _parse_prop_str ( query , inner_str , prop_start , rel_top_level_keys )
292298 else :
293299 top_level = {}
294300 props = {}
0 commit comments