@@ -444,6 +444,70 @@ def test_index_max_hops_honored(engine, hop_kw):
444444 assert _sig (base ) == _sig (idx ), f"max_hops divergence { hop_kw } "
445445
446446
447+ @pytest .mark .parametrize ("engine" , ENGINES )
448+ @pytest .mark .parametrize ("max_hops" , [1 , 2 , 3 ])
449+ def test_index_bounded_range_min_one_hops_none (engine , max_hops ):
450+ """A bounded [1,max] hop is a depth union, not exactly max hops."""
451+ from graphistry .compute .gfql .index import index_trace
452+
453+ edf = pd .DataFrame ({"src" : [0 , 1 , 2 ], "dst" : [1 , 2 , 3 ]})
454+ g = graphistry .edges (edf , "src" , "dst" ).materialize_nodes ()
455+ seeds = pd .DataFrame ({"id" : [0 ]})
456+ kwargs = dict (hops = None , min_hops = 1 , max_hops = max_hops , direction = "forward" )
457+ base = g .hop (nodes = seeds , engine = engine , ** kwargs )
458+ with index_trace () as steps :
459+ idx = _force (g , engine ).hop (nodes = seeds , engine = engine , ** kwargs )
460+ assert _sig (base ) == _sig (idx ), "range-hop index must equal scan"
461+ assert any (step ["path" ] == "index" for step in steps ), steps
462+
463+
464+ def test_index_coverability_bounded_range_boundaries ():
465+ """Planner declines invalid direct-hop ranges before index traversal."""
466+ from graphistry .compute .gfql .index .api import _hop_is_index_coverable
467+
468+ common = dict (
469+ nodes = pd .DataFrame ({"id" : [0 ]}),
470+ to_fixed_point = False ,
471+ hops = None ,
472+ min_hops = 1 ,
473+ max_hops = 2 ,
474+ output_min_hops = None ,
475+ output_max_hops = None ,
476+ label_node_hops = None ,
477+ label_edge_hops = None ,
478+ label_seeds = False ,
479+ edge_match = None ,
480+ source_node_match = None ,
481+ destination_node_match = None ,
482+ source_node_query = None ,
483+ destination_node_query = None ,
484+ edge_query = None ,
485+ include_zero_hop_seed = False ,
486+ target_wave_front = None ,
487+ return_as_wave_front = False ,
488+ )
489+ assert not _hop_is_index_coverable (** dict (common , max_hops = 0 ))
490+ assert not _hop_is_index_coverable (** dict (common , min_hops = 0 ))
491+ assert _hop_is_index_coverable (** dict (common , max_hops = 1 ))
492+ assert _hop_is_index_coverable (** dict (common , hops = 5 , max_hops = 2 ))
493+ assert not _hop_is_index_coverable (** dict (common , max_hops = None ))
494+ assert not _hop_is_index_coverable (** dict (common , min_hops = 2 ))
495+ assert not _hop_is_index_coverable (** dict (common , min_hops = 3 , max_hops = 2 ))
496+ assert not _hop_is_index_coverable (** dict (common , nodes = None ))
497+
498+ def test_index_min_two_bounded_range_scans_pandas (graph ):
499+ """Unsupported [2,2] ranges scan without entering the indexed traversal."""
500+ from graphistry .compute .gfql .index import index_trace
501+
502+ seeds = pd .DataFrame ({"id" : [0 , 1 ]})
503+ kwargs = dict (hops = None , min_hops = 2 , max_hops = 2 , direction = "forward" )
504+ base = graph .hop (nodes = seeds , engine = "pandas" , ** kwargs )
505+ with index_trace () as steps :
506+ indexed = _force (graph , "pandas" ).hop (nodes = seeds , engine = "pandas" , ** kwargs )
507+ assert _sig (base ) == _sig (indexed )
508+ assert not any (step ["path" ] == "index" for step in steps ), steps
509+ assert any (step ["decision_reason" ] == "query not index-coverable" for step in steps ), steps
510+
447511@pytest .mark .parametrize ("engine" , ENGINES )
448512def test_index_duplicate_node_ids (engine ):
449513 # B2: duplicate node ids corrupted the unique-key node_id index. gfql_index_all
0 commit comments