Summary (native-polars feature; effort L, multi-day)
On polars, any Cypher query that crosses an edge (a traversal) NIEs: polars row pipeline does not yet support op 'rows' when rows carries binding_ops. This blocks graph-benchmark q1, q2, q8, q9 (and every multi-hop/edge query) on polars/polars-gpu. Single-MATCH-node queries already run natively on polars.
Enforcement sites
graphistry/compute/gfql/lazy/engine/polars/chain.py:301-305 (_run_calls_polars) and 253-257 (_call_native_on_polars force-declines rows when it carries binding_ops/alias_endpoints).
graphistry/compute/gfql/row/pipeline.py:4712 whitelist _POLARS_NATIVE_ROW_PIPELINE_CALLS = {"rows","skip","limit","distinct","drop_cols"}; force-decline at 4735-4744.
Why it's pandas/cuDF-bound
The traversal itself already runs on polars (each ASTNode/ASTEdge.execute(engine=POLARS) uses the polars hop engine). What is pandas/cuDF-API-bound is the connected-binding state-threading + materialization (the joins/merges):
_gfql_connected_bindings_state (pipeline.py:3347), _gfql_connected_bindings_row_frame_from_state (3576)
_gfql_cartesian_node_bindings_row_table (3788), _gfql_multihop_binding_rows (3251, ~93 lines)
_gfql_disambiguate_has_edge_destination_nodes (3232) + _gfql_has_edge_destination_label_col (3221)
_gfql_shortest_path_scalar_bindings_row_table (3721) + _gfql_shortest_path_scalar_native (3630)
_gfql_bindings_row_table alias_endpoints variant (3849); EdgeSemantics.orient_edges (same_path/edge_semantics.py:36)
Idiom counts in the 3180–3860 range: .merge×7, .iloc×8, .drop(columns=)×11, .rename(columns=)×7, .drop_duplicates×5, .isin×3, .assign×4, .duplicated×2, .groupby().min().reset_index(), null-handling .astype(object).where(...).
Effort: L (~6–10 methods, ~400–600 lines), multi-day
Idiom mapping is mostly mechanical (.merge→.join, .isin→.is_in, .rename(columns=dict)→.rename(dict), .drop(columns=)→.drop, .drop_duplicates(subset=)→.unique(subset=), .groupby().min().reset_index()→.group_by().agg(), .iloc[0:0]→.clear()). The finicky parts:
.astype(object).where(~null_mask, None) null-normalization has no polars analog (polars nulls are typed); touches the shortest-path scalar path.
maintain_order / first-occurrence semantics for parity with pandas.
orient_edges must be polars-aware.
MUST land with a parity gate: compare polars results to pandas across many random graphs + the q1/q2/q8/q9 shapes before trusting any number. The repo forbids a polars→pandas→polars arrow bridge (chain.py:269,297,304, plans/gfql-polars-engine NO-CHEATING rule) — do NOT take that shortcut.
Extends #1665 (polars NIE surfaces). Depends on nothing, but pairs with the count(*) broadcast bug (separate P0 issue) — both are needed for correct polars analytical numbers.
Summary (native-polars feature; effort L, multi-day)
On polars, any Cypher query that crosses an edge (a traversal) NIEs:
polars row pipeline does not yet support op 'rows'whenrowscarriesbinding_ops. This blocks graph-benchmark q1, q2, q8, q9 (and every multi-hop/edge query) on polars/polars-gpu. Single-MATCH-node queries already run natively on polars.Enforcement sites
graphistry/compute/gfql/lazy/engine/polars/chain.py:301-305(_run_calls_polars) and253-257(_call_native_on_polarsforce-declinesrowswhen it carriesbinding_ops/alias_endpoints).graphistry/compute/gfql/row/pipeline.py:4712whitelist_POLARS_NATIVE_ROW_PIPELINE_CALLS = {"rows","skip","limit","distinct","drop_cols"}; force-decline at4735-4744.Why it's pandas/cuDF-bound
The traversal itself already runs on polars (each
ASTNode/ASTEdge.execute(engine=POLARS)uses the polars hop engine). What is pandas/cuDF-API-bound is the connected-binding state-threading + materialization (the joins/merges):_gfql_connected_bindings_state(pipeline.py:3347),_gfql_connected_bindings_row_frame_from_state(3576)_gfql_cartesian_node_bindings_row_table(3788),_gfql_multihop_binding_rows(3251, ~93 lines)_gfql_disambiguate_has_edge_destination_nodes(3232) +_gfql_has_edge_destination_label_col(3221)_gfql_shortest_path_scalar_bindings_row_table(3721) +_gfql_shortest_path_scalar_native(3630)_gfql_bindings_row_tablealias_endpoints variant (3849);EdgeSemantics.orient_edges(same_path/edge_semantics.py:36)Idiom counts in the 3180–3860 range:
.merge×7,.iloc×8,.drop(columns=)×11,.rename(columns=)×7,.drop_duplicates×5,.isin×3,.assign×4,.duplicated×2,.groupby().min().reset_index(), null-handling.astype(object).where(...).Effort: L (~6–10 methods, ~400–600 lines), multi-day
Idiom mapping is mostly mechanical (
.merge→.join,.isin→.is_in,.rename(columns=dict)→.rename(dict),.drop(columns=)→.drop,.drop_duplicates(subset=)→.unique(subset=),.groupby().min().reset_index()→.group_by().agg(),.iloc[0:0]→.clear()). The finicky parts:.astype(object).where(~null_mask, None)null-normalization has no polars analog (polars nulls are typed); touches the shortest-path scalar path.maintain_order/ first-occurrence semantics for parity with pandas.orient_edgesmust be polars-aware.MUST land with a parity gate: compare polars results to pandas across many random graphs + the q1/q2/q8/q9 shapes before trusting any number. The repo forbids a polars→pandas→polars arrow bridge (
chain.py:269,297,304,plans/gfql-polars-engineNO-CHEATING rule) — do NOT take that shortcut.Extends #1665 (polars NIE surfaces). Depends on nothing, but pairs with the count(*) broadcast bug (separate P0 issue) — both are needed for correct polars analytical numbers.