Skip to content

Commit 9e0dba3

Browse files
lmeyerovclaude
andcommitted
test(gfql): pin #1767 cliff, LazyFrame/cudf AUTO routing, and pandas-oracle parity
- test_auto_engine_gfql_serves_polars_index_1767_cliff: polars frames + explicit gfql_index_all(engine='polars') + g.gfql() with NO engine now serves path=index on engine=polars per index_trace() -- the #1767 engine-mismatch cliff can no longer regress silently. - test_auto_engine_hop_residual_still_scans_1767: the documented scope boundary as executable documentation -- direct g.hop() with no engine still resolves AUTO to pandas and declines with decision_code engine_mismatch (hop/chain/index-build AUTO are follow-ups). - TestAutoEngineLazyFrames: LazyFrame edges+nodes with engine unset route native and return EAGER polars equal to explicit engine='polars'. - TestAutoEngineCudfUntouched: AUTO on cudf frames never enters the polars guard; legacy CUDF resolution unchanged (importorskip-gated). - TestAutoEnginePandasOracleParity: AUTO-on-polars-frames answers equal the pandas-engine oracle in value for a filter+traverse, an aggregate, and a WHERE-bearing shape (normalized frame comparison). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
1 parent 98f8e20 commit 9e0dba3

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,3 +1399,65 @@ def test_engine_mismatch_reason_reports_all_undirected_indexes(
13991399
"resident edge_out_adj, edge_in_adj index "
14001400
f"engine={resident_engine}, requested engine={requested_engine} -> scan"
14011401
), reason
1402+
1403+
1404+
# ---------------------------------------------------------------------------
1405+
# 1743: engine=auto on polars-frame graphs routes to the native polars engine,
1406+
# so a polars-engine index built explicitly is actually consulted through
1407+
# g.gfql(...) with NO engine argument. Regression pin for the #1767 cliff:
1408+
# before the AUTO route, resolve_engine(AUTO) mapped polars frames to pandas,
1409+
# so the resident polars index was engine-mismatched and every query scanned.
1410+
# ---------------------------------------------------------------------------
1411+
1412+
def _polars_indexed_graph():
1413+
pl = pytest.importorskip("polars")
1414+
nodes = pd.DataFrame({
1415+
"id": np.arange(1, 13, dtype=np.int64),
1416+
"public": np.arange(100, 112, dtype=np.int64),
1417+
})
1418+
edges = pd.DataFrame({
1419+
"src": [1, 1, 1, 2, 2, 3, 4, 5, 6, 8],
1420+
"dst": [2, 2, 3, 4, 5, 5, 6, 6, 7, 1],
1421+
"type": ["A", "A", "A", "B", "B", "B", "C", "C", "D", "REV"],
1422+
})
1423+
g = graphistry.nodes(pl.from_pandas(nodes), "id").edges(
1424+
pl.from_pandas(edges), "src", "dst"
1425+
)
1426+
return g.gfql_index_all(engine="polars")
1427+
1428+
1429+
def test_auto_engine_gfql_serves_polars_index_1767_cliff():
1430+
"""#1767 cliff pin: polars frames + explicit polars index + gfql with NO engine
1431+
argument must serve path=index on engine=polars (AUTO routes native, so the
1432+
resident index is no longer engine-mismatched into a silent scan)."""
1433+
from graphistry.compute.gfql.index import index_trace
1434+
1435+
gi = _polars_indexed_graph()
1436+
q = ("MATCH (source {public:100})-[:A]->(destination) "
1437+
"RETURN destination.public AS destination ORDER BY destination")
1438+
with index_trace() as steps:
1439+
out = gi.gfql(q) # no engine argument: default AUTO, default index policy
1440+
assert "polars" in type(out._nodes).__module__, "AUTO must answer in polars frames"
1441+
served = [s for s in steps if s.get("served") is True]
1442+
assert served, ("no index-served step; AUTO fell off the polars route", steps)
1443+
for s in served:
1444+
assert s.get("path") == "index", steps
1445+
assert s.get("engine") == "polars", steps
1446+
assert out._nodes["destination"].to_list() == [101, 102]
1447+
1448+
1449+
def test_auto_engine_hop_residual_still_scans_1767():
1450+
"""Documented residual (#1743 scope boundary, executable): direct g.hop() with no
1451+
engine on the same polars-frame graph still resolves AUTO to pandas, so the
1452+
resident polars index declines with an engine mismatch and the hop scans."""
1453+
from graphistry.compute.gfql.index import index_trace
1454+
1455+
gi = _polars_indexed_graph()
1456+
seeds = pd.DataFrame({"id": [1]})
1457+
with index_trace() as steps:
1458+
out = gi.hop(nodes=seeds, hops=1, direction="forward") # no engine argument
1459+
assert "pandas" in type(out._nodes).__module__, "hop AUTO still bridges to pandas"
1460+
assert not any(s.get("path") == "index" for s in steps), steps
1461+
mismatch = [s for s in steps if s.get("decision_code") == "engine_mismatch"]
1462+
assert mismatch, ("expected an engine_mismatch decline", steps)
1463+
assert "requested engine=pandas" in mismatch[-1]["decision_reason"], steps

graphistry/tests/compute/gfql/test_engine_polars_cypher_conformance.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,65 @@ def test_policy_hooks_fire_once_on_nie_shape(self):
602602
g.gfql(q, policy=pol)
603603
assert seen.count("precompile") == 1, seen
604604
assert seen.count("preload") == 1, seen
605+
606+
607+
class TestAutoEngineLazyFrames:
608+
"""LazyFrame is the other member of the PolarsFrame union `is_polars_df` admits, so
609+
the AUTO route must take it too — and hand back EAGER polars, same as explicit
610+
engine='polars' does."""
611+
612+
def test_lazyframe_auto_routes_native_and_matches_explicit(self):
613+
nodes = pl.DataFrame({"id": [0, 1, 2, 3], "label__Person": [True] * 4}).lazy()
614+
edges = pl.DataFrame({"s": [0, 1, 2], "d": [1, 2, 3], "type": ["KNOWS"] * 3}).lazy()
615+
g = graphistry.nodes(nodes, "id").edges(edges, "s", "d")
616+
q = "MATCH (a:Person {id: 0})-[:KNOWS]->(b) RETURN b.id AS bid ORDER BY bid"
617+
r_auto = g.gfql(q)._nodes
618+
r_expl = g.gfql(q, engine="polars")._nodes
619+
assert isinstance(r_auto, pl.DataFrame), type(r_auto) # eager out, not Lazy
620+
assert isinstance(r_expl, pl.DataFrame), type(r_expl)
621+
from polars.testing import assert_frame_equal
622+
assert_frame_equal(r_auto, r_expl)
623+
assert r_auto["bid"].to_list() == [1]
624+
625+
626+
class TestAutoEngineCudfUntouched:
627+
"""cuDF frames never enter the polars-AUTO guard (`is_polars_df` is a polars module
628+
check), so the legacy AUTO->CUDF resolution is byte-for-byte what it was. Skips
629+
without cudf/GPU; runs on GPU lanes."""
630+
631+
def test_auto_on_cudf_frames_stays_on_legacy_cudf_path(self):
632+
cudf = pytest.importorskip("cudf")
633+
nodes = cudf.DataFrame({"id": [0, 1, 2, 3], "label__Person": [True] * 4})
634+
edges = cudf.DataFrame({"s": [0, 1, 2], "d": [1, 2, 3], "type": ["KNOWS"] * 3})
635+
g = graphistry.nodes(nodes, "id").edges(edges, "s", "d")
636+
out = g.gfql("MATCH (a:Person {id: 0})-[:KNOWS]->(b) RETURN b.id AS bid")._nodes
637+
assert "cudf" in type(out).__module__, type(out) # not polars, not pandas
638+
639+
640+
class TestAutoEnginePandasOracleParity:
641+
"""Routed AUTO answers must equal the pandas oracle in VALUE, not just shape: the
642+
same query on pandas frames via engine='pandas' is the reference for each routed
643+
query shape (filter+traverse, aggregate, WHERE-bearing)."""
644+
645+
ROUTED_SHAPES = [
646+
# filter + traverse (connected join, multi-alias projection)
647+
"MATCH (a {kind: 'alpha'})-[]->(b) RETURN a.id AS aid, b.id AS bid ORDER BY aid, bid",
648+
# aggregate
649+
"MATCH (n) RETURN n.kind AS kind, count(n) AS c ORDER BY kind",
650+
# WHERE-bearing (predicate + arithmetic projection)
651+
"MATCH (a)-[]->(b) WHERE b.val > 50 AND b.flag "
652+
"RETURN b.id AS bid, b.val + b.score AS v ORDER BY bid, v",
653+
]
654+
655+
@pytest.mark.parametrize("query", ROUTED_SHAPES)
656+
def test_auto_polars_matches_pandas_oracle(self, query):
657+
g_pl = graphistry.nodes(pl.from_pandas(BASE._nodes), "id").edges(
658+
pl.from_pandas(BASE._edges), "s", "d"
659+
)
660+
got = g_pl.gfql(query)._nodes # AUTO: routes native polars
661+
assert "polars" in type(got).__module__, "shape did not route; oracle test is vacuous"
662+
oracle = BASE.gfql(query, engine="pandas")._nodes
663+
a = _normalize_nulls(_round_floats(_to_pd(got).reset_index(drop=True)))
664+
b = _normalize_nulls(_round_floats(oracle.reset_index(drop=True)))
665+
assert list(a.columns) == list(b.columns), (list(a.columns), list(b.columns))
666+
pd.testing.assert_frame_equal(a.astype(str), b.astype(str), check_dtype=False)

0 commit comments

Comments
 (0)