Skip to content

Commit 0f8d26d

Browse files
authored
Merge pull request #1839 from graphistry/fix/gfql-index-bounded-range-hop
fix(gfql): index bounded range hops
2 parents 84e05d5 + 93eff17 commit 0f8d26d

2 files changed

Lines changed: 70 additions & 2 deletions

File tree

graphistry/compute/gfql/index/api.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,12 @@ def _hop_is_index_coverable(
452452
return False
453453
if label_seeds or include_zero_hop_seed:
454454
return False
455-
if not to_fixed_point and (not isinstance(hops, int) or hops < 1):
456-
return False
455+
effective_hops = max_hops if max_hops is not None else hops
456+
if not to_fixed_point:
457+
if not isinstance(effective_hops, int) or effective_hops < 1:
458+
return False
459+
if hops is None and min_hops not in (None, 1):
460+
return False
457461
return True
458462

459463

graphistry/tests/compute/gfql/index/test_index.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
448512
def test_index_duplicate_node_ids(engine):
449513
# B2: duplicate node ids corrupted the unique-key node_id index. gfql_index_all

0 commit comments

Comments
 (0)