7676 | rel_undirected_simple
7777 | rel_bidirectional_simple
7878
79- rel_forward: "-" "[" variable? rel_types? properties? "]" "->"
80- rel_reverse: "<-" "[" variable? rel_types? properties? "]" "-"
81- rel_undirected: "-" "[" variable? rel_types? properties? "]" "-"
79+ rel_forward: "-" "[" variable? rel_types? rel_range? properties? "]" "->"
80+ rel_reverse: "<-" "[" variable? rel_types? rel_range? properties? "]" "-"
81+ rel_undirected: "-" "[" variable? rel_types? rel_range? properties? "]" "-"
8282rel_forward_simple: REL_FWD_SIMPLE
8383rel_reverse_simple: REL_REV_SIMPLE
8484rel_undirected_simple: REL_UNDIR_SIMPLE
8585rel_bidirectional_simple: REL_BIDIR_SIMPLE
8686
8787rel_types: ":" LABEL_NAME ("|" ":"? LABEL_NAME)*
88+ rel_range: "*" INT ".." INT -> rel_range_bounded
89+ | "*" INT -> rel_range_exact
90+ | "*" -> rel_range_fixed
8891
8992variable: NAME
9093
@@ -389,46 +392,13 @@ def _to_unsupported(message: str, *, line: Optional[int] = None, column: Optiona
389392 )
390393
391394
392- _VARIABLE_REL_PATTERN_RE = re .compile (
393- r"(?:<-\s*\[[^\]\n]*\*[^\]\n]*\]\s*-)|(?:-\s*\[[^\]\n]*\*[^\]\n]*\]\s*->)|(?:-\s*\[[^\]\n]*\*[^\]\n]*\]\s*-)"
394- )
395-
396-
397395def _line_and_column_from_offset (source : str , offset : int ) -> Tuple [int , int ]:
398396 line = source .count ("\n " , 0 , offset ) + 1
399397 last_newline = source .rfind ("\n " , 0 , offset )
400398 column = offset + 1 if last_newline < 0 else offset - last_newline
401399 return line , column
402400
403401
404- def _find_variable_length_relationship_pattern (source : str ) -> Optional [Tuple [str , int , int ]]:
405- in_single_quote = False
406- escape = False
407- segment_start = 0
408- for idx , ch in enumerate (source ):
409- if in_single_quote :
410- if escape :
411- escape = False
412- elif ch == "\\ " :
413- escape = True
414- elif ch == "'" :
415- in_single_quote = False
416- segment_start = idx + 1
417- continue
418- if ch == "'" :
419- match = _VARIABLE_REL_PATTERN_RE .search (source , segment_start , idx )
420- if match is not None :
421- line , column = _line_and_column_from_offset (source , match .start ())
422- return match .group (0 ), line , column
423- in_single_quote = True
424- continue
425- match = _VARIABLE_REL_PATTERN_RE .search (source , segment_start )
426- if match is None :
427- return None
428- line , column = _line_and_column_from_offset (source , match .start ())
429- return match .group (0 ), line , column
430-
431-
432402def _build_transformer (source : str ) -> _TransformerLike :
433403 _ , Transformer , _ , v_args = _lark_imports ()
434404 op_map = {
@@ -523,9 +493,16 @@ def _relationship(
523493 variable : Optional [str ] = None
524494 rel_types : Tuple [str , ...] = ()
525495 properties : Tuple [PropertyEntry , ...] = ()
496+ min_hops : Optional [int ] = None
497+ max_hops : Optional [int ] = None
498+ to_fixed_point = False
526499 for item in items :
527500 if isinstance (item , str ):
528501 variable = item
502+ elif isinstance (item , dict ):
503+ min_hops = cast (Optional [int ], item .get ("min_hops" ))
504+ max_hops = cast (Optional [int ], item .get ("max_hops" ))
505+ to_fixed_point = bool (item .get ("to_fixed_point" , False ))
529506 elif isinstance (item , tuple ) and all (isinstance (v , str ) for v in item ):
530507 rel_types = cast (Tuple [str , ...], item )
531508 elif isinstance (item , tuple ):
@@ -536,8 +513,50 @@ def _relationship(
536513 types = rel_types ,
537514 properties = properties ,
538515 span = _span_from_meta (meta ),
516+ min_hops = min_hops ,
517+ max_hops = max_hops ,
518+ to_fixed_point = to_fixed_point ,
539519 )
540520
521+ def _rel_hops (self , meta : Any , token : Any ) -> int :
522+ try :
523+ value = int (str (token ))
524+ except Exception as exc :
525+ raise _to_syntax_error ("Invalid relationship range bound" , line = meta .line , column = meta .column ) from exc
526+ if value <= 0 :
527+ raise _to_unsupported (
528+ "Cypher zero-hop relationship ranges are not yet supported in the current GFQL Cypher compiler" ,
529+ line = meta .line ,
530+ column = meta .column ,
531+ field = "match" ,
532+ value = self ._slice (_span_from_meta (meta )),
533+ )
534+ return value
535+
536+ def rel_range_exact (self , meta : Any , items : Sequence [Any ]) -> dict [str , Any ]:
537+ if len (items ) != 1 :
538+ raise _to_syntax_error ("Invalid relationship range" , line = meta .line , column = meta .column )
539+ hops = self ._rel_hops (meta , items [0 ])
540+ return {"min_hops" : hops , "max_hops" : hops , "to_fixed_point" : False }
541+
542+ def rel_range_bounded (self , meta : Any , items : Sequence [Any ]) -> dict [str , Any ]:
543+ if len (items ) != 2 :
544+ raise _to_syntax_error ("Invalid relationship range" , line = meta .line , column = meta .column )
545+ min_hops = self ._rel_hops (meta , items [0 ])
546+ max_hops = self ._rel_hops (meta , items [1 ])
547+ if min_hops > max_hops :
548+ raise _to_unsupported (
549+ "Cypher relationship ranges require lower bound <= upper bound" ,
550+ line = meta .line ,
551+ column = meta .column ,
552+ field = "match" ,
553+ value = self ._slice (_span_from_meta (meta )),
554+ )
555+ return {"min_hops" : min_hops , "max_hops" : max_hops , "to_fixed_point" : False }
556+
557+ def rel_range_fixed (self , meta : Any , _items : Sequence [Any ]) -> dict [str , Any ]:
558+ return {"min_hops" : None , "max_hops" : None , "to_fixed_point" : True }
559+
541560 def rel_forward (self , meta : Any , items : Sequence [Any ]) -> RelationshipPattern :
542561 return self ._relationship (meta , items , direction = "forward" )
543562
@@ -1261,16 +1280,6 @@ def parse_cypher(query: str) -> Union[CypherQuery, CypherUnionQuery]:
12611280 """
12621281 if not isinstance (query , str ) or query .strip () == "" :
12631282 raise _to_syntax_error ("Cypher query must be a non-empty string" )
1264- variable_length_pattern = _find_variable_length_relationship_pattern (query )
1265- if variable_length_pattern is not None :
1266- pattern_text , line , column = variable_length_pattern
1267- raise _to_unsupported (
1268- "Cypher variable-length relationship patterns are not yet supported in the current GFQL Cypher compiler" ,
1269- line = line ,
1270- column = column ,
1271- field = "match" ,
1272- value = pattern_text ,
1273- )
12741283
12751284 parser = _parser ()
12761285 transformer = _build_transformer (query )
0 commit comments