@@ -114,6 +114,36 @@ def test_lower_match_clause_relationship_direction(query: str, edge_type: type)
114114 assert isinstance (ops [1 ], edge_type )
115115
116116
117+ @pytest .mark .parametrize (
118+ "query,edge_type,min_hops,max_hops,to_fixed_point,edge_match" ,
119+ [
120+ ("MATCH (a)-[*2]->(b) RETURN b" , ASTEdgeForward , 2 , 2 , False , None ),
121+ ("MATCH (a)<-[*3]-(b) RETURN b" , ASTEdgeReverse , 3 , 3 , False , None ),
122+ ("MATCH (a)-[:R*1..4]-(b) RETURN b" , ASTEdgeUndirected , 1 , 4 , False , {"type" : "R" }),
123+ ("MATCH (a)-[*]->(b) RETURN b" , ASTEdgeForward , None , None , True , None ),
124+ ],
125+ )
126+ def test_lower_match_clause_variable_length_relationships (
127+ query : str ,
128+ edge_type : type ,
129+ min_hops : int | None ,
130+ max_hops : int | None ,
131+ to_fixed_point : bool ,
132+ edge_match : dict [str , object ] | None ,
133+ ) -> None :
134+ parsed = _parse_query (query )
135+ assert parsed .match is not None
136+
137+ ops = lower_match_clause (parsed .match )
138+
139+ assert isinstance (ops [1 ], edge_type )
140+ edge = ops [1 ]
141+ assert edge .min_hops == min_hops
142+ assert edge .max_hops == max_hops
143+ assert edge .to_fixed_point is to_fixed_point
144+ assert edge .edge_match == edge_match
145+
146+
117147def test_lower_match_clause_relationship_type_alternation_uses_is_in_predicate () -> None :
118148 parsed = _parse_query ("MATCH (n)-[r:KNOWS|HATES]->(x) RETURN r" )
119149 assert parsed .match is not None
@@ -2331,31 +2361,89 @@ def test_string_cypher_failfast_rejects_graph_backed_unwind_after_with_as_valida
23312361 assert "UNWIND after WITH/RETURN" in exc_info .value .message
23322362
23332363
2334- @pytest .mark .parametrize (
2335- "query" ,
2336- [
2337- "MATCH (a)-[*]->(b) RETURN a, b" ,
2338- "MATCH (a)-[*2]->(b) RETURN a, b" ,
2339- "MATCH (a)-[*1..3]->(b) RETURN a, b" ,
2340- ],
2341- )
2342- def test_string_cypher_failfast_rejects_variable_length_relationship_patterns_as_validation_error (query : str ) -> None :
2364+ def test_string_cypher_executes_exact_multihop_relationship_pattern () -> None :
23432365 graph = _mk_graph (
23442366 pd .DataFrame ({"id" : ["a" , "b" , "c" , "d" , "e" , "f" ]}),
23452367 pd .DataFrame (
23462368 {
2347- "s" : ["a" , "c" , "d" , "e" ],
2348- "d" : ["b" , "d" , "e" , "f" ],
2349- "type" : ["R" , "R" , "R" , "R" ],
2369+ "s" : ["a" , "b" , " c" , "d" , "e" ],
2370+ "d" : ["b" , "c" , " d" , "e" , "f" ],
2371+ "type" : ["R" , "R" , "R" , "R" , "R" ],
23502372 }
23512373 ),
23522374 )
23532375
2354- with pytest .raises (GFQLValidationError ) as exc_info :
2355- graph .gfql (query )
2376+ result = graph .gfql ("MATCH (a {id: 'a'})-[*2]->(b) RETURN b.id AS id ORDER BY id" )
23562377
2357- assert exc_info .value .code == ErrorCode .E108
2358- assert "variable-length relationship patterns" in exc_info .value .message
2378+ assert result ._nodes .to_dict (orient = "records" ) == [{"id" : "c" }]
2379+
2380+
2381+ def test_string_cypher_executes_bounded_multihop_relationship_pattern () -> None :
2382+ graph = _mk_graph (
2383+ pd .DataFrame ({"id" : ["a" , "b" , "c" , "d" , "e" ]}),
2384+ pd .DataFrame (
2385+ {
2386+ "s" : ["a" , "b" , "c" , "a" ],
2387+ "d" : ["b" , "c" , "d" , "e" ],
2388+ "type" : ["R" , "R" , "R" , "S" ],
2389+ }
2390+ ),
2391+ )
2392+
2393+ result = graph .gfql ("MATCH (a {id: 'a'})-[:R*1..3]->(b) RETURN b.id AS id ORDER BY id" )
2394+
2395+ assert result ._nodes .to_dict (orient = "records" ) == [{"id" : "b" }, {"id" : "c" }, {"id" : "d" }]
2396+
2397+
2398+ def test_string_cypher_executes_fixed_point_relationship_pattern () -> None :
2399+ graph = _mk_graph (
2400+ pd .DataFrame ({"id" : ["a" , "b" , "c" , "d" , "e" ]}),
2401+ pd .DataFrame (
2402+ {
2403+ "s" : ["a" , "b" , "c" , "a" ],
2404+ "d" : ["b" , "c" , "d" , "e" ],
2405+ "type" : ["R" , "R" , "R" , "S" ],
2406+ }
2407+ ),
2408+ )
2409+
2410+ result = graph .gfql ("MATCH (a {id: 'a'})-[*]->(b) RETURN b.id AS id ORDER BY id" )
2411+
2412+ assert result ._nodes .to_dict (orient = "records" ) == [{"id" : "b" }, {"id" : "c" }, {"id" : "d" }, {"id" : "e" }]
2413+
2414+
2415+ def test_string_cypher_executes_reverse_multihop_relationship_pattern () -> None :
2416+ graph = _mk_graph (
2417+ pd .DataFrame ({"id" : ["a" , "b" , "c" , "d" ]}),
2418+ pd .DataFrame (
2419+ {
2420+ "s" : ["a" , "b" , "c" ],
2421+ "d" : ["b" , "c" , "d" ],
2422+ "type" : ["R" , "R" , "R" ],
2423+ }
2424+ ),
2425+ )
2426+
2427+ result = graph .gfql ("MATCH (a {id: 'c'})<-[*2]-(b) RETURN b.id AS id ORDER BY id" )
2428+
2429+ assert result ._nodes .to_dict (orient = "records" ) == [{"id" : "a" }]
2430+
2431+
2432+ def test_string_cypher_executes_undirected_multihop_relationship_pattern () -> None :
2433+ graph = _mk_graph (
2434+ pd .DataFrame ({"id" : ["a" , "b" , "c" , "d" ]}),
2435+ pd .DataFrame (
2436+ {
2437+ "s" : ["a" , "b" , "c" ],
2438+ "d" : ["b" , "c" , "d" ],
2439+ "type" : ["R" , "R" , "R" ],
2440+ }
2441+ ),
2442+ )
2443+
2444+ result = graph .gfql ("MATCH (a {id: 'a'})-[:R*1..2]-(b) RETURN b.id AS id ORDER BY id" )
2445+
2446+ assert result ._nodes .to_dict (orient = "records" ) == [{"id" : "b" }, {"id" : "c" }]
23592447
23602448
23612449@pytest .mark .parametrize (
0 commit comments