Skip to content

test(gfql): pin #2019 single-alias WHERE on a multi-alias MATCH - #2122

Merged
lmeyerov merged 5 commits into
masterfrom
test/gfql-2019-single-alias-where-pin
Oct 4, 2026
Merged

lmeyerov merged 5 commits into
masterfrom
test/gfql-2019-single-alias-where-pin

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

#2019 reports MATCH (a)-[e]->(t) WHERE a.type IN [...] RETURN t.id AS id raising "Cypher row lowering currently supports one MATCH source alias at a time". The exact repro passes at the issue's own stated commit 3fb216d and at master (b18d808), on default/pandas/polars/auto engines, plain and after gfql_index_all(); a git bisect run over 0.59.0..master found no commit where it raises. 17 nearby single-alias-WHERE variants also pass; the two that raise do so with their own messages (ORDER BY on a non-returned alias → "ORDER BY expressions must reference the active alias"; node+edge projection RETURN t.id, e.w → the documented #1273 multi-source residual).

This PR pins the shape so it stays working: the repro plus edge/destination/comparison/NOT/DISTINCT/WITH/2-hop variants, plain and indexed, pandas and polars (polars-guarded, registered in bin/test-polars.sh), plus the two-alias WHERE and the ORDER BY residual's own message.

Refs #2019 (not closed): the issue's exact repro passes, but its class is still open for edge-alias projections: MATCH (a)-[e]->(t) WHERE a.type IN [...] RETURN e.e_type (and RETURN t.id, e.e_type) raise unsupported-cypher-query with field: where, on every engine, plain and indexed. Those are pinned here as declines, by error code and field, so the day they are served the pin says so.

Review (2026-10-04): the pin now runs on pandas, cuDF and polars with cuDF frames (_ids goes through to_pandas), the DISTINCT case has a real duplicate to collapse, and the residual pins are structured (code == E108, context['field']) instead of message substrings. cuDF params need a GPU; receipt to follow from dgx.

Test plan

  • graphistry/tests/compute/gfql/cypher/test_single_alias_where_2019.py: 20 passed locally (pandas + polars)
  • bin/lint.sh
  • CI green at the head

🤖 Generated with Claude Code

https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp

The issue's exact repro passes at its stated commit 3fb216d and at master; pin the shape
(plain and indexed, pandas and polars) plus the residuals that keep their own messages.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
…als by field

Review found the PR would auto-close #2019 while projecting the edge alias from a node-alias
WHERE still raises, and that the pin ran on pandas and polars only. The pin now runs on pandas,
cuDF and polars with cuDF frames, the DISTINCT case has a real duplicate to collapse, and the
residuals (ORDER BY on a non-returned alias; the two edge-alias projections) are pinned on the
error code and its field rather than on message text.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Read-only review (parallel session) — test pin for #2019, head 207a272

Nothing here touches the branch; findings only, fixes deferred.

Scope: test-only (+77: one test file + bin/test-polars.sh registration). Diff range origin/master...207a272.
Evidence: the new file copied onto a MASTER worktree passes as-is: 20 passed in 3.28s (pandas + polars,
plain + gfql_index_all()), so the PR's claim that #2019's repro does not reproduce on master is confirmed
here, and the pin is purely protective.

Findings

  • SUGGESTION graphistry/tests/compute/gfql/cypher/test_single_alias_where_2019.py:16-20 — polars
    availability is probed with try/except Exception + # pragma: no cover. SKILL.md treats broad
    excepts as a smell and pragmas on non-bugs as off-limits; the repo's existing pattern is
    pytest.importorskip("polars") inside a _require(engine) helper (see
    graphistry/tests/compute/gfql/cypher/test_in_list_literal_lane.py). Same behaviour, no pragma.
  • SUGGESTION — expected rows for WHERE a.score > 1 RETURN t.id are ["c", "c", "tx2"], i.e. the
    test correctly treats the result as a BAG (one row per matched edge). Worth one comment line so a future
    "fix" to DISTINCT does not look like a regression.
  • SUGGESTION — the negative test (ORDER BY a.id on a non-returned alias) matches the error by message
    substring (match="ORDER BY expressions must reference"). SKILL.md asks control-flow checks to use
    structured signals; for a pytest assertion this is tolerable, but if GFQLValidationError carries a
    code, assert that instead.
  • No cuDF leg. The shape is engine-agnostic lowering, and the file is registered in the polars lane, so
    this is acceptable for a pin; note it in the PR body.
  • CI: green at 207a272 (one rollup entry still reported pending at check time — re-check before merge).

Recommendation: mergeable as a pin; the three suggestions are optional polish.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ropeBMLJUuy6ViYwy15ud

… verdict

CI showed the shape answering on python 3.9 and 3.14 where it declines on this box, so pinning
the decline made the suite environment-dependent. The pin now accepts either outcome and checks
the one that matters: a decline is E108 on `where`, and an answer matches the rows computed from
the frames. The ORDER BY decline is stable and stays its own pin.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Your three suggestions are in, and one of them turned into a real finding. At 4d89b3c.

The pin was environment-dependent. The first push pinned the edge-alias projection as a decline, because that is what it does here on Python 3.12. CI disagreed: test-core-python (3.9) and test-gfql-core (3.14) both reported DID NOT RAISE for RETURN e.e_type and RETURN t.id, e.e_type. So acceptance of that shape varies by environment, which means the decline is not a contract. The pin now fixes the contract instead: a decline must be E108 on field: where, and an answer must equal the rows computed from the frames. ORDER BY on a non-returned alias is stable and keeps its own decline pin.

Root cause so far: not the hash seed (six seeds locally all decline) and not the parser version (lark 1.3.1 both sides). The decline comes from _append_match_row_where being reached with allowed_match_aliases=None and active_match_alias='e', which happens only when plan.all_source_aliases is None, so the projection plan itself differs between environments. A full-suite single-process run is in flight to rule test-order state in or out, and I will file the divergence with whatever it says.

Closes versus Refs. Changed to Refs, with the open half named in the body: the issue's exact repro passes, the edge-alias projection does not, and the pin covers both outcomes.

Engine sweep and the cuDF-unsafe helper. The pin now runs pandas, polars and cuDF with cuDF frames, and _ids goes through to_pandas so your cudf.Series.to_list TypeError cannot happen.

DISTINCT exercised nothing. Right — it now uses a.score > 1, which produces a duplicated c, so DISTINCT has something to collapse.

Negative pin by message substring. Replaced by structured assertions on code == ErrorCode.E108 and context['field'], which also distinguishes the order_by residual from the where one.

bin/test-polars.sh grouping. Moved to the engine-parametrized group, which is what this file is.

The "indexed" loop serving nothing is a fair observation; it is cheap and keeps the shape honest across index policies, so I have left it with that expectation rather than claiming engagement.

@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Root-caused, and it is simpler and better news than "environment-dependent". I was wrong to call it that; the cause was my own branch being stale.

What it was. This branch was 33 commits behind master. Master landed graphistry/compute/gfql/cypher/where_membership.py, which seeds the MATCH pattern from WHERE alias.prop IN [literals]. With that in, the predicate is consumed as a pattern filter, so lowered.row_where is empty, _append_match_row_where returns before validating anything, and the multi-source decline never fires. CI tests the PR merge commit, so CI had the feature and my worktree did not. The projection plan was never the variable.

What I ruled out along the way, for the record: PYTHONHASHSEED (six seeds, all decline), the parser (lark 1.3.1 both sides), the dependency set (a local Python 3.9.23 venv with CI's exact pandas 2.3.3 / numpy 2.0.2 still declines), and test order (the full single-process graphistry/tests/compute run with a probe asserting the decline passed, so no cross-test state).

Proof after merging master, same graph, same two queries:

MATCH (a)-[e]->(t) WHERE a.type IN ['person','company'] RETURN e.e_type AS et
  -> ['knows', 'works_at']
MATCH (a)-[e]->(t) WHERE a.type IN ['person','company'] RETURN t.id AS id, e.e_type AS et
  -> ['knows', 'works_at']

So the edge-alias projection is not an open residual at all on master. #2019's class is closed for it, and the pin I wrote for the unsettled case now takes its answering branch and checks the rows against the frames. I have kept that form rather than hardening it back to "must answer", because the decline is still the right outcome for a WHERE that cannot be seeded into the pattern, and the pin covers both.

At 9bd3ed6 (master merged): 36 passed, lint and mypy green.

The general lesson is worth more than the finding: a gate run on a branch head that is behind master is measuring the wrong tree. I have checked the rest of the queue for the same trap — #2127 was 20 commits behind and is now merged up at 806deb9, the others were current.

… indexed there

The GPU receipt failed 12 cells on `cupy does not support object`, raised by gfql_index_all() over a
string-keyed cuDF graph. Master raises the same way, and an int-keyed cuDF graph indexes fine, so the
pins run plain on cuDF and the limitation gets its own pin: the plain query answers, the index build
raises. It is a raw TypeError rather than a typed decline, which is worth fixing separately.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_012Me1E7ZdDuGqJGu3mMEzhp
@lmeyerov
lmeyerov merged commit 69119a7 into master Oct 4, 2026
90 checks passed
@lmeyerov

lmeyerov commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

GPU receipt at the current head, and it changed the test rather than the claim.

dgx-spark, GB10, graphistry/test-rapids-official:26.02-gfql-polars, head 0ae0b9b:

graphistry/tests/compute/gfql/cypher/test_single_alias_where_2019.py
  -> 37 passed, 0 failed   (EXIT=0)

The pass before this one failed twelve cells on TypeError: cupy does not support object, every one of them from gfql_index_all() over the string-keyed cuDF graph the pin builds. Probing master tip on the same GPU:

plain query on a string-keyed cuDF graph      -> OK, 2 rows
gfql_index_all() on that graph                -> TypeError: cupy does not support object
gfql_index_all() on an int-keyed cuDF graph   -> OK, 2 rows

So it is a pre-existing product limitation, not this PR, and it deserves a typed decline rather than a raw cupy error. The pin now runs plain on cuDF and the limitation carries its own expectation, so the day it is fixed the pin says so.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant