|
5 | 5 | importorskip so GPU lanes run only where available. |
6 | 6 | """ |
7 | 7 | import importlib |
| 8 | +from copy import copy |
8 | 9 |
|
9 | 10 | import numpy as np |
10 | 11 | import pandas as pd |
11 | 12 | import pytest |
12 | 13 |
|
13 | 14 | import graphistry |
14 | 15 | from graphistry.compute.ast import n, e_forward, e_reverse |
15 | | -from graphistry.compute.gfql.index.api import _engine_mismatch_reason |
| 16 | +from graphistry.compute.gfql.index.api import _engine_mismatch_reason, maybe_index_hop |
16 | 17 | from graphistry.compute.gfql.index import ( |
17 | 18 | CreateIndex, DropIndex, ShowIndexes, index_op_from_json, parse_index_ddl, |
18 | 19 | get_registry, |
@@ -242,6 +243,74 @@ def test_index_policy_force_and_explain(graph, engine): |
242 | 243 | assert rep_force["error"] is None |
243 | 244 |
|
244 | 245 |
|
| 246 | +def test_maybe_index_hop_reports_no_resident_index(graph): |
| 247 | + """A direct planner caller gets a named scan decline when no index is resident.""" |
| 248 | + from graphistry.Engine import Engine |
| 249 | + from graphistry.compute.gfql.index import index_trace |
| 250 | + |
| 251 | + with index_trace() as steps: |
| 252 | + result = maybe_index_hop( |
| 253 | + graph, |
| 254 | + Engine.PANDAS, |
| 255 | + nodes=pd.DataFrame({"id": [0]}), |
| 256 | + hops=1, |
| 257 | + direction="forward", |
| 258 | + return_as_wave_front=False, |
| 259 | + policy="use", |
| 260 | + ) |
| 261 | + |
| 262 | + assert result is None |
| 263 | + assert steps[-1]["decision_code"] == "no_resident_index" |
| 264 | + |
| 265 | + |
| 266 | +def test_maybe_index_hop_reports_missing_graph_columns(graph): |
| 267 | + """A resident index cannot make an unbound graph use a different query path.""" |
| 268 | + from graphistry.Engine import Engine |
| 269 | + from graphistry.compute.gfql.index import index_trace |
| 270 | + |
| 271 | + unbound = copy(graph.gfql_index_all(engine="pandas")) |
| 272 | + unbound._nodes = None |
| 273 | + with index_trace() as steps: |
| 274 | + result = maybe_index_hop( |
| 275 | + unbound, |
| 276 | + Engine.PANDAS, |
| 277 | + nodes=pd.DataFrame({"id": [0]}), |
| 278 | + hops=1, |
| 279 | + direction="forward", |
| 280 | + return_as_wave_front=False, |
| 281 | + policy="use", |
| 282 | + ) |
| 283 | + |
| 284 | + assert result is None |
| 285 | + assert steps[-1]["decision_code"] == "missing_graph_columns" |
| 286 | + |
| 287 | + |
| 288 | +def test_maybe_index_hop_reports_auto_build_decline_with_scan_parity(graph): |
| 289 | + """An unselective auto request declines the build and preserves scan results.""" |
| 290 | + from graphistry.Engine import Engine |
| 291 | + from graphistry.compute.gfql.index import index_trace |
| 292 | + |
| 293 | + seeds = graph._nodes.copy() |
| 294 | + with index_trace() as steps: |
| 295 | + result = maybe_index_hop( |
| 296 | + graph, |
| 297 | + Engine.PANDAS, |
| 298 | + nodes=seeds, |
| 299 | + hops=1, |
| 300 | + direction="forward", |
| 301 | + return_as_wave_front=False, |
| 302 | + policy="auto", |
| 303 | + ) |
| 304 | + |
| 305 | + auto_graph = graph.bind() |
| 306 | + auto_graph._gfql_index_policy = "auto" |
| 307 | + auto_result = auto_graph.hop(nodes=seeds, engine="pandas", hops=1) |
| 308 | + scan_result = graph.hop(nodes=seeds, engine="pandas", hops=1) |
| 309 | + assert result is None |
| 310 | + assert steps[-1]["decision_code"] == "index_build_declined" |
| 311 | + assert _sig(auto_result) == _sig(scan_result) |
| 312 | + |
| 313 | + |
245 | 314 | @pytest.mark.parametrize("engine", ENGINES) |
246 | 315 | def test_explain_exposes_planner_diagnostics(graph, engine): |
247 | 316 | """LP1: gfql_explain surfaces the planner's cost signal — seed cardinality, the |
|
0 commit comments