Skip to content

GFQL Cypher: undirected unbounded [*] row projection off the trail lane (impossible rows; count-twin break) #1906

Description

@lmeyerov

Found by the round-006 paths re-probe (brute-force trail enumerator as oracle, 140 Cypher texts × 2 engines × 2 repeats @ d7cf23d; scripts in session scratchpad probe-r6-paths/, repro at repro_f1.py).

Shape: undirected + unbounded (to_fixed_point) var-length, plain row projection, pandas. polars declines typed (safe).

4-cycle a→b→c→d→a:

query oracle actual
MATCH (x {id:'a'})-[*]-(y) RETURN y.id 8 rows 4
... RETURN count(*) 8 8 ✓
... RETURN y.id, count(*) 4 groups × 2
MATCH (x {id:'a'})-[*1..4]-(y) RETURN y.id 8

So the plain row lane is the only wrong lane — the count and grouped twins are trail-exact, and the bounded twin is exact. Breaks the "one MATCH = one cardinality" invariant #1903 established.

Worse than multiplicity — impossible rows. With a second element on a triangle: MATCH (x {id:'a'})-[*]-(y)-[]->(z) RETURN z.id[a,b,c]; oracle [a,c] (reaching b requires reusing an edge). The count(*) twin is 2 (correct).

Reproduces on every cyclic fixture, seeded and unseeded (TRI 3 vs 6, C4 4 vs 8, C4CH 4 vs 25, C5CH 5 vs 30, SLPAR 2 vs 43, DIASL 5 vs 17; unseeded TRI 3 vs 18, C4 4 vs 32).

Provenance (A/B vs master 25215ce): the row lane returns the same wrong 4 on master — the row values are NOT a #1903 regression. What changed is the neighbourhood: on master the adjacent count(*) twin raised a typed error on C4/C4CH (SLPAR returned the old walk value 11); #1903 moved the count lane onto trail semantics and left the row lane on the source-table path, so the shape now presents a correct number beside a silently wrong row set.

Origin, deliberate and unpinned: graphistry/compute/gfql/cypher/lowering.py:2583-2591 — the to_fixed_point and direction == "undirected" early return False that keeps this shape off binding rows "rather than trade one wrongness for a decline (#1903 residual)". There is no test pin on it. Fix: route it through the trail-filtered binding lane, or decline typed like polars does — a silently wrong row set with impossible values is the worst of the three options.

Related, separately scoped (do not conflate): the same probe re-measured the pre-existing #1787 pandas min_hops>=2 hop-window starvation (root cause row/pipeline.py:3955-3985: the trail expander consumes edge_op.execute output already pruned by hop.py's dedup-by-node eccentricity gate, so when that empties the frame the trail machinery never sees the edges). Master and fix head are byte-identical there, and it is documented + pinned — but two sub-cases sit outside any pin or gate and are silent on pandas: seeded undirected *4..4 (C4 0 vs 2, C4CH 0 vs 6, SLPAR 0 vs 12, while *3..3 is exact) and unseeded undirected *3..3 (TRI 0 vs 6, C4 0 vs 8, C4CH 0 vs 24). Worth pins at minimum.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions