Skip to content

GFQL/polars: native rows(binding_ops) — traversal queries NIE (blocks polars for edge/multi-hop Cypher) #1709

Description

@lmeyerov

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:

  1. .astype(object).where(~null_mask, None) null-normalization has no polars analog (polars nulls are typed); touches the shortest-path scalar path.
  2. maintain_order / first-occurrence semantics for parity with pandas.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions