Skip to content

Commit 84e05d5

Browse files
authored
Merge pull request #1838 from graphistry/fix/gfql-index-engine-mismatch-diagnostic
fix(gfql): explain engine-mismatched index scans
2 parents 3f7f792 + 5be6aac commit 84e05d5

2 files changed

Lines changed: 95 additions & 2 deletions

File tree

graphistry/compute/gfql/index/api.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,38 @@ def _trace_active() -> bool:
8181
return _get_trace_steps() is not None
8282

8383

84+
def _engine_mismatch_reason(
85+
registry: GfqlIndexRegistry, direction: HopDirection, engine: Engine
86+
) -> Optional[str]:
87+
"""Describe a trace-only adjacency-index engine mismatch, if present.
88+
89+
Indexes intentionally remain engine-specific: falling back to the scan is the
90+
correct execution behavior. This helper makes that otherwise silent decline
91+
visible to ``gfql_explain`` without changing planner policy.
92+
"""
93+
needed: Sequence[AdjacencyIndexKind]
94+
if direction == "forward":
95+
needed = (EDGE_OUT_ADJ,)
96+
elif direction == "reverse":
97+
needed = (EDGE_IN_ADJ,)
98+
else:
99+
needed = (EDGE_OUT_ADJ, EDGE_IN_ADJ)
100+
mismatches = [
101+
(kind, idx)
102+
for kind in needed
103+
for idx in (registry.get(kind),)
104+
if isinstance(idx, AdjacencyIndex) and idx.engine != engine
105+
]
106+
if not mismatches:
107+
return None
108+
kinds = ", ".join(kind for kind, _ in mismatches)
109+
engines = ", ".join(sorted({idx.engine.value for _, idx in mismatches}))
110+
return (
111+
f"resident {kinds} index engine={engines}, requested engine={engine.value} "
112+
"-> scan"
113+
)
114+
115+
84116
def _record_indexed_traversal(
85117
*,
86118
seam: str,
@@ -598,7 +630,7 @@ def _bail(reason: str) -> Optional[Plottable]:
598630
"path": "index" if result is not None else "scan",
599631
"decision_reason": (
600632
"frontier below cost gate -> index" if result is not None
601-
else "index path not applicable -> scan"
633+
else _engine_mismatch_reason(registry, direction, engine) or "index path not applicable -> scan"
602634
),
603635
}))
604636
return result

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
import pytest
1212

1313
import graphistry
14-
from graphistry.compute.ast import n, e_forward
14+
from graphistry.compute.ast import n, e_forward, e_reverse
15+
from graphistry.compute.gfql.index.api import _engine_mismatch_reason
1516
from graphistry.compute.gfql.index import (
1617
CreateIndex, DropIndex, ShowIndexes, index_op_from_json, parse_index_ddl,
1718
get_registry,
@@ -1180,3 +1181,63 @@ def spy(series, rows, eng):
11801181
monkeypatch.setenv("GFQL_INDEX_CANDIDATE_EDGE_MASK", "0")
11811182
gi.hop(nodes=seeds, **kwargs)
11821183
assert not seen, "OFF side still gathered candidate rows — the switch does nothing"
1184+
1185+
1186+
@pytest.mark.parametrize(
1187+
"index_kinds, edge, expected_kinds",
1188+
[
1189+
(("edge_out_adj",), e_forward, "edge_out_adj"),
1190+
(("edge_in_adj",), e_reverse, "edge_in_adj"),
1191+
],
1192+
)
1193+
@pytest.mark.parametrize(
1194+
"resident_engine, requested_engine",
1195+
[("pandas", "polars"), ("polars", "pandas")],
1196+
)
1197+
def test_explain_reports_bidirectional_engine_mismatch(
1198+
graph, index_kinds, edge, expected_kinds, resident_engine, requested_engine
1199+
):
1200+
"""#1767: a valid foreign-engine index must explain its scan, not fail silently."""
1201+
pytest.importorskip("polars")
1202+
gi = graph
1203+
for index_kind in index_kinds:
1204+
gi = gi.create_index(index_kind, engine=resident_engine)
1205+
shown = gi.show_indexes()
1206+
assert all(bool(valid) for valid in shown["valid"].tolist())
1207+
1208+
report = gi.gfql_explain(
1209+
[n({"id": 0}), edge(hops=1)],
1210+
index_policy="use",
1211+
engine=requested_engine,
1212+
)
1213+
1214+
assert report["used_index"] is False, report
1215+
assert report["decision_reason"] == (
1216+
f"resident {expected_kinds} index "
1217+
f"engine={resident_engine}, requested engine={requested_engine} -> scan"
1218+
), report
1219+
1220+
1221+
@pytest.mark.parametrize(
1222+
"resident_engine, requested_engine",
1223+
[("pandas", "polars"), ("polars", "pandas")],
1224+
)
1225+
def test_engine_mismatch_reason_reports_all_undirected_indexes(
1226+
graph, resident_engine, requested_engine
1227+
):
1228+
"""The undirected diagnostic reports every valid resident foreign-engine index."""
1229+
from graphistry.Engine import Engine
1230+
1231+
pytest.importorskip("polars")
1232+
gi = graph
1233+
for index_kind in ("edge_out_adj", "edge_in_adj"):
1234+
gi = gi.create_index(index_kind, engine=resident_engine)
1235+
shown = gi.show_indexes()
1236+
assert all(bool(valid) for valid in shown["valid"].tolist())
1237+
1238+
reason = _engine_mismatch_reason(get_registry(gi), "undirected", Engine(requested_engine))
1239+
1240+
assert reason == (
1241+
"resident edge_out_adj, edge_in_adj index "
1242+
f"engine={resident_engine}, requested engine={requested_engine} -> scan"
1243+
), reason

0 commit comments

Comments
 (0)