Skip to content

GFQL/polars: Cypher RETURN count(*) returns constant 1 (silent wrong answer) #1707

Description

@lmeyerov

Summary (P0 correctness — silent wrong answers)

On the polars engine, a Cypher RETURN count(*) (and any bare scalar-literal projection) returns a constant 1 instead of the true count. This is a silent wrong answer — the query runs without error but the number is wrong.

Repro

import graphistry, pandas as pd, numpy as np
n=4
g = graphistry.nodes(pd.DataFrame({"id":range(n),"age":[10,20,30,40]}),"id")
print(g.gfql("MATCH (a) RETURN count(*) AS c", engine="pandas")._nodes)  # c=4  ✅
print(g.gfql("MATCH (a) RETURN count(*) AS c", engine="polars")._nodes)  # c=1  ❌

Also affects filtered counts: MATCH (a) WHERE a.age>=30 RETURN count(*) → polars returns 1, should be 2. 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 to select_polars (graphistry/compute/gfql/lazy/engine/polars/chain.py:321), and select_polars of a bare scalar literal collapses the frame to a single row instead of broadcasting to the existing row count. group_by then sees 1 row → pl.len() = 1.

  • Verified: 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 to pl.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).

Activity

  1. lmeyerov commented on Jul 7, 2026

    @lmeyerov
    ContributorAuthor

    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.

  2. lmeyerov commented on Aug 18, 2026

    @lmeyerov
    ContributorAuthor

    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 raw TypeError: 'NoneType' object is not subscriptable at graphistry/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.

  3. lmeyerov commented on Aug 18, 2026

    @lmeyerov
    ContributorAuthor

    Closing: verified fixed on master e6625ed28 by the 2026-08 stack — empirical repro + hand-computed oracle in the verification comment above. Residual sub-items, where any, are tracked in the successor issues named there (#1916, #1908, #1906, #1934-#1938).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions