Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions graphistry/compute/gfql_unified.py
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,24 @@ def gfql(self: Plottable,
:returns: Resulting Plottable
:rtype: Plottable
"""
# engine inference: resolve_engine(AUTO) maps polars frames to PANDAS (polars predates
# Engine.POLARS there), silently bridging polars-frame graphs onto the generic pandas path
# (~13x slower on cypher point queries, pandas frames out). Route AUTO to the native polars
# engine instead; an honest NIE (unsupported shape) falls back to the legacy AUTO path, which
# is allowed here because the user did not pin an engine.
if (
(engine == EngineAbstract.AUTO or engine == EngineAbstract.AUTO.value)
and is_polars_df(self._edges) and (self._nodes is None or is_polars_df(self._nodes))
):
try:
return gfql(
self, query, engine=Engine.POLARS.value, output=output, policy=policy,
where=where, language=language, params=params, validate=validate,
shortest_path_backend=shortest_path_backend,
)
except NotImplementedError:
logger.debug('AUTO polars-native attempt declined; falling back to generic path')

context = ExecutionContext()

if policy and context.policy_depth >= 1:
Expand Down
8 changes: 4 additions & 4 deletions graphistry/tests/compute/gfql/cypher/test_lowering.py
Original file line number Diff line number Diff line change
Expand Up @@ -15997,7 +15997,7 @@ def test_connected_join_polars_nullable_columns_match_master(predicate: str, exp
query = f"MATCH (p)-[]->(x), (p)-[]->(y) WHERE {predicate} RETURN count(p) AS n"

out = g.gfql(query)._nodes
records = out.to_dict(orient="records") if hasattr(out, "to_dict") else out.to_dicts()
records = out.to_dicts() if hasattr(out, "to_dicts") else out.to_dict(orient="records")
assert records == expected


Expand Down Expand Up @@ -16027,7 +16027,7 @@ def test_arrow_table_nodes_match_master_results() -> None:
def run(predicate: str) -> Any:
query = f"MATCH (p)-[]->(x), (p)-[]->(y) WHERE {predicate} RETURN count(p) AS n"
out = g.gfql(query)._nodes
return out.to_dict(orient="records") if hasattr(out, "to_dict") else out.to_dicts()
return out.to_dicts() if hasattr(out, "to_dicts") else out.to_dict(orient="records")

assert run("p.name = 5") == []
assert run("p.name = 'ann'") == [{"n": 4}]
Expand Down Expand Up @@ -16084,7 +16084,7 @@ def test_connected_join_polars_decimal_matches_master(predicate: str, expected:
query = f"MATCH (p)-->(a), (p)-->(b) WHERE {predicate} RETURN count(p) AS n"

result = g.gfql(query)._nodes
records = result.to_dict(orient="records") if hasattr(result, "to_dict") else result.to_dicts()
records = result.to_dicts() if hasattr(result, "to_dicts") else result.to_dict(orient="records")
assert records == expected


Expand All @@ -16099,7 +16099,7 @@ def test_connected_join_polars_backed_graph_matches_pandas_results() -> None:

g_polars = graphistry.nodes(pl.from_pandas(nodes), "id").edges(pl.from_pandas(edges), "s", "d")
result = g_polars.gfql(query)._nodes
records = result.to_dict(orient="records") if hasattr(result, "to_dict") else result.to_dicts()
records = result.to_dicts() if hasattr(result, "to_dicts") else result.to_dict(orient="records")
assert records == []


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,33 @@ def test_bool_modulo_declines_like_pandas():
# bool + int still computes in parity (not over-declined)
got = g.gfql("MATCH (n) RETURN n.flag + 2 AS r", engine="polars")._nodes["r"].to_list()
assert got == [3, 2, 3]


class TestAutoEngineRoutesPolarsNative:
"""engine=auto on polars-frame graphs must run the native polars path (frames in = frames
out), not the silent pandas bridge (~13x on point queries); polars-NIE shapes still answer
via the legacy AUTO fallback since the user did not pin an engine."""

def _graph(self):
nodes = pl.DataFrame({"id": [0, 1, 2, 3], "label__Person": [True] * 4})
edges = pl.DataFrame({"s": [0, 1, 2], "d": [1, 2, 3], "type": ["KNOWS"] * 3})
return graphistry.nodes(nodes, "id").edges(edges, "s", "d")

def test_auto_returns_polars_frames_and_matches_explicit(self):
g = self._graph()
q = "MATCH (a:Person {id: 0})-[:KNOWS]->(b) RETURN b.id AS bid ORDER BY bid"
r_auto = g.gfql(q)._nodes
r_expl = g.gfql(q, engine="polars")._nodes
assert "polars" in type(r_auto).__module__, "auto must not bridge polars graphs to pandas"
from polars.testing import assert_frame_equal
assert_frame_equal(r_auto, r_expl)

def test_auto_falls_back_on_polars_nie(self):
g = self._graph()
q = ("MATCH p = shortestPath((a:Person {id: 0})-[:KNOWS*]-(b:Person {id: 3})) "
"RETURN length(p) AS l")
with pytest.raises(NotImplementedError):
g.gfql(q, engine="polars") # pinned engine: honest NIE
out = g.gfql(q)._nodes # auto: answers via fallback
rows = out.to_dict("records") if hasattr(out, "to_dict") else out.to_pandas().to_dict("records")
assert rows == [{"l": 3}]
Loading