Repository navigation
GFQL/polars: Cypher RETURN count(*) returns constant 1 (silent wrong answer) #1707
Description
Activity
Fixed on branch `dev/gfql-cypher-std-conformance` (2026-07-06).
Fix: `select_polars` now routes through a new `_project_preserving_height()` in `graphistry/compute/gfql/lazy/engine/polars/row_pipeline.py`. Cypher `WITH`/`RETURN` is a map (preserves row cardinality); polars `DataFrame.select` of an all-scalar-literal projection collapses to 1 row (no column establishes height), so the synthetic `cypher_group=1` for keyless `count(*)` made the downstream `group_by.count` see 1 row. Now an all-scalar projection broadcasts to the frame height via `with_columns(...).select(names)`. Also fixes polars-gpu (shared code).
Verified: `count()`=6 (was 1), filtered `count()`=4 (was 1), `RETURN 1` preserves N rows, `RETURN DISTINCT 1`=1 row — all == pandas oracle.
Tests: +5 parity cases in `NATIVE_LOWERED` + 2 focused value-regression tests (assert absolute count, pandas-oracle-independent). Full sweep: 2177 passed.
- added 7 commits that reference this issue
on Jul 9, 2026 - added a commit that references this issue
on Jul 16, 2026 Verified fixed on master
e6625ed28(post correctness-stack merge) — recommending close after review, with one adjacent-defect note.With edges bound (the filed configuration), polars
RETURN count(*)no longer returns constant 1:pandas MATCH (a) RETURN count(*) exp=4 got=4 rows=1 OK pandas MATCH (a) WHERE a.age>=30 count(*) exp=2 got=2 rows=1 OK polars ... exp=4 got=4 rows=1 OK polars ... exp=2 got=2 rows=1 OK cudf ... exp=4 got=4 rows=1 OK cudf ... exp=2 got=2 rows=1 OK(Expected values hand-computed from the 4-node fixture.)
Adjacent defect worth its own small issue (also surfaced while auditing #1879): the nodes-only variant of this repro (no
.edges()bound) now crashes on pandas/cuDF with a rawTypeError: 'NoneType' object is not subscriptableatgraphistry/compute/ast.py:265(g._edges[:0]with_edges=None), while polars gives a typed NotImplementedError. That's an untyped crash on a previously-answerable shape, but it is a different defect from the constant-1 silent-wrong filed here.
Summary (P0 correctness — silent wrong answers)
On the polars engine, a Cypher
RETURN count(*)(and any bare scalar-literal projection) returns a constant1instead of the true count. This is a silent wrong answer — the query runs without error but the number is wrong.Repro
Also affects filtered counts:
MATCH (a) WHERE a.age>=30 RETURN count(*)→ polars returns1, should be2. So graph-benchmark q3/q4/q5/q7 (filter+count) all return wrong numbers on polars today.Root cause (verified)
The shape lowers to
rows → with_(('__cypher_group__', 1)) → group_by([...],[('c','count')]) → select. On polars,with_routes toselect_polars(graphistry/compute/gfql/lazy/engine/polars/chain.py:321), andselect_polarsof a bare scalar literal collapses the frame to a single row instead of broadcasting to the existing row count.group_bythen sees 1 row →pl.len()= 1.select_polars(g, [('__cypher_group__', 1)])returns height 1 for a 4-row input.Fix locus (effort: S)
Literal broadcasting in
select_polars/lower_select_items/lower_expr—graphistry/compute/gfql/lazy/engine/polars/row_pipeline.py:409, 345, 253. A scalar literal projected without an aggregation must broadcast topl.lit(v)over the frame height, not collapse it.Why P0
This is a wrong-answer bug, strictly worse than an honest NIE. Any polars analytical-count result is currently untrustworthy. Must be fixed before any polars analytical numbers are published. Related: #1664 (openCypher semantics), #1665 (polars NIE/capability matrix).