Skip to content

fix(gfql): apply GRAPH residual predicates as masks#1706

Merged
lmeyerov merged 10 commits into
masterfrom
fix/issue-1662-graph-residual-guard
Jul 8, 2026
Merged

fix(gfql): apply GRAPH residual predicates as masks#1706
lmeyerov merged 10 commits into
masterfrom
fix/issue-1662-graph-residual-guard

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

What

Fixes GRAPH { MATCH ... WHERE ... } residual predicate handling for the safe graph-mask cases instead of silently dropping the predicate or merely raising.

This PR compiles single-alias residual predicates into explicit graph residual-mask metadata, then applies those masks to the target node or edge frame before the graph-state Chain runs. That covers residual forms such as OR/NULL expressions and searchAny(...) for a single node or edge alias.

Pattern-predicate and multi-alias residuals still fail loudly because they need first-class projection semantics rather than a local entity mask. Polars also declines these GRAPH residual masks for now instead of silently bridging or mis-evaluating.

Why

Issue #1662 exposed a silent wrong-results bug: GRAPH constructors could accept row residual predicates from Cypher lowering, but Chain(lowered.query, where=lowered.where) had nowhere to apply them.

The root fix is to preserve the residual and apply it at graph state construction time when it is semantically a node or edge mask.

Handoff / backport notes

Use the net PR diff as the source of truth; the first commit (b1903892, fail-fast) was intentionally superseded by the later mask implementation. If cherry-picking into a branch that has moved ahead, prefer either cherry-picking the full stack or reapplying the net diff for these files:

  • graphistry/compute/gfql/cypher/lowering.py: adds CompiledGraphResidualFilter, records it on compiled query/binding structs, and compiles only safe single-alias node/edge residuals.
  • graphistry/compute/gfql_unified.py: applies residual masks before _chain_dispatch; pandas/cuDF path is implemented, Polars raises a clear validation error.
  • graphistry/tests/compute/gfql/cypher/test_lowering.py: semantic contract for node residuals, alias/property-column collision regressions, cuDF smoke, Polars honest decline, searchAny, edge residuals, named GRAPH ... USE, and pattern-predicate rejection.
  • docs/source/gfql/cypher.rst: user-facing supported/unsupported behavior.
  • bin/ci_cypher_surface_guard_baseline.json: intentional internal compiled-struct surface growth.

Backport gotchas:

  • Do not port only the fail-fast guard; that was the rejected interim state.
  • Keep graph_residual_filters on both standalone CompiledCypherGraphQuery and named CompiledGraphBinding; otherwise GRAPH g = GRAPH { ... } USE g ... regresses.
  • Keep endpoint reconciliation for node masks: after filtering nodes, filter edges to endpoints in the surviving node IDs before running the Chain.
  • Keep the residual eval-frame alias marker unconditional; otherwise a real node/edge property column named like the alias (a, r, etc.) breaks alias-property residuals such as a.score / r.weight.
  • Keep unsupported pattern/multi-alias residuals loud. They are not equivalent to a simple entity mask.
  • If the target branch already has a native Polars row-expression graph-mask path, replace the honest Polars decline with parity tests; otherwise keep the decline test.

Surface change

The compiled Cypher structs gain one internal metadata field for GRAPH residual masks. The Cypher surface guard baseline was regenerated intentionally for:

  • CompiledCypherQuery: 7 -> 8 fields
  • CompiledGraphBinding: 6 -> 7 fields
  • CompiledCypherGraphQuery: 6 -> 7 fields

User impact

  • Native GRAPH predicates continue to work.
  • Single node/edge residual predicates inside GRAPH { } now filter the returned subgraph on pandas/cuDF.
  • searchAny(...) works in the supported single-alias GRAPH mask form.
  • Unsupported pattern/multi-alias residuals still fail clearly instead of returning wrong results.
  • Polars GRAPH residual masks fail clearly pending native support.

Validation

  • python bin/ci_cypher_surface_guard.py -> passed
  • python -m pytest -q graphistry/tests/compute/gfql/cypher/test_lowering.py -k 'graph_constructor or graph_binding_applies_residual_mask_before_use' -> 17 passed
  • python -m pytest -q graphistry/tests/compute/gfql/test_engine_polars_conformance_matrix.py -k 'prune_isolated_graph_shape' -> 1 passed
  • python -m pytest -q graphistry/tests/compute/gfql/test_viz_pipeline_conformance.py -k 'graph_state_prune_pipeline' -> 1 passed
  • python -m ruff check graphistry/compute/gfql/cypher/lowering.py graphistry/compute/gfql_unified.py graphistry/tests/compute/gfql/cypher/test_lowering.py -> passed

Refs #1662

@lmeyerov lmeyerov changed the title fix(gfql): fail fast on GRAPH residual predicates fix(gfql): apply GRAPH residual predicates as masks Jul 7, 2026
lmeyerov added 4 commits July 6, 2026 22:33
Node residual GRAPH masks were applied as global node masks before the graph-state chain, which changes alias-specific MATCH semantics for multi-alias patterns. Keep single-node node residual masks and edge residual masks, but reject multi-alias node residual masks loudly.
@lmeyerov lmeyerov marked this pull request as ready for review July 8, 2026 09:58
Comment thread docs/source/gfql/cypher.rst
Comment thread graphistry/compute/gfql/cypher/lowering.py
@lmeyerov lmeyerov force-pushed the fix/issue-1662-graph-residual-guard branch from 5433dbf to 40a3770 Compare July 8, 2026 15:51
@lmeyerov lmeyerov merged commit 4113fa2 into master Jul 8, 2026
58 of 59 checks passed
@lmeyerov lmeyerov deleted the fix/issue-1662-graph-residual-guard branch July 8, 2026 15:59
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