Skip to content

fix(gfql): make gfql_validate agree with execution on unqueryable graph shapes (#1889) - #1951

Merged
lmeyerov merged 3 commits into
masterfrom
fix/gfql-1889-validator-drift
Aug 19, 2026
Merged

lmeyerov merged 3 commits into
masterfrom
fix/gfql-1889-validator-drift

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Fixes #1889

The drift

gfql_validate sold itself as preflight ("validate without executing") and returned {ok: True, diagnostics: []} on a graph whose frames were never attached — graphistry.bind(source='s', destination='d', node='id') names columns only. Execution of the exact same query then died with a bare error, on every surface and engine.

Verified live at master 0c3f3a1fa (per-combo, the 4 filed cells):

shape query validator @master execution @master after
both-frames-None-after-bind MATCH (a) RETURN a ok:true, diagnostics: [] pandas/cuDF ValueError: Missing edges; polars AssertionError (empty message) both diagnose GFQLSchemaError [E305 graph-not-bound]
both-frames-None-after-bind [n({'v': 20})] ok:true, diagnostics: [] same bare pair both diagnose E305
nodes-only MATCH (a) RETURN a ok:true pandas answers 2 rows (#1942) unchanged — both admit, values pinned
nodes-only [n({'v': 20})] ok:true pandas answers 1 row (#1942) unchanged — both admit, values pinned

The nodes-only cells already agreed after #1942, so they only needed pins. The both-None cells were live drift on both query languages.

The fix

One predicate, validate_graph_shape(g, ops), consulted by the validator and by execution so the two can't disagree:

  • neither nodes nor edges bound → GFQLSchemaError with the new E305 = "graph-not-bound" and a bind remedy. Emitted by gfql_validate (chain + Cypher) and by execution on every engine: pandas/cuDF reach it through validate_chain_schema, polars through a pre-dispatch guard in chain() (the polars mirror files are untouched — another lane owns them).
  • edges unbound + an edge pattern → E304, the same typed decline execution has given since fix(gfql): crash family — polars filter helpers (#1882, #1913-f4) + nodes-only pandas Cypher (#1879) #1942. The validator no longer stays silent about a shape the runtime refuses.
  • nodes-only + node-only pattern → still admitted on both sides, still answers.

Execution was not weakened anywhere: every shape that answered at master still answers with identical values (pinned), and the bare ValueError/AssertionError became typed diagnostics rather than the validator becoming permissive.

schema=False callers skip the shape check — remote preflight (chain_remote) validates against a graph whose frames live server-side, so local emptiness says nothing there. E305 chose a new code rather than overloading invalid-node-reference / invalid-edge-reference, both of which would repeat #1889's own complaint about wrong-subject messages ("Missing edges" for a node-only query).

Pins

graphistry/tests/compute/gfql/test_validate_execute_agreement_1889.py (18 cases, registered in the polars lane):

  • the agreement matrix: 4 combos × {pandas, polars} — no cell may validate clean then raise a bare (untyped / empty-message) error, and a diagnosing validator must mean a declining execution;
  • typed-code pins: E305 on both surfaces with identical message text; E304 on both surfaces for edge-patterns-without-edges;
  • anti-vacuity: nodes-only answers with value-level record oracles ([{'a.id': 0, 'a.v': 10}, ...]), a fully bound graph and an edges-only graph are NOT flagged, and schema=False still returns ok. py3.13 NaN handling normalizes at value level (None if isinstance(v, float) and isnan(v)), never where(notna(), None).

Red at master 0c3f3a1fa: 10 of 18 fail (the 8 that pass are exactly the anti-vacuity / already-agreeing cells, which must stay green on both sides).

Mutation check — reverting each of the 5 fix sites individually turns the pins red: drop the E305 diagnostic → 8 failed; drop the E304 diagnostic → 2 failed; drop the exec check in validate_chain_schema → 7 failed; drop the polars pre-dispatch check → 5 failed; drop the Cypher-validator check → 4 failed.

test_gfql_validate_only.py::test_gfql_validate_chain_without_bound_tables_is_structural_only asserted the drift itself (ok:true on a frameless graph, from #1321). It moves to schema=False, which is the contract it was really pinning, and the schema-on case now asserts E305.

Gates

  • ruff: clean
  • mypy: Success: no issues found in 331 source files (no new errors)
  • type-hygiene guard: OK (no growth); cypher-surface guard: pass
  • comment-encoding guard: my four touched product files are at/below baseline; the run still exits non-zero on a pre-existing master finding in graphistry/compute/gfql/lazy/engine/polars/row_pipeline.py (7 findings vs baseline 6) — byte-identical at 0c3f3a1fa, and that file belongs to another lane
  • full graphistry/tests/compute failure SET compared against master (see PR comment)

…ph shapes (#1889)

`gfql_validate` returned `{ok: True, diagnostics: []}` on a graph whose frames were
never attached (`graphistry.bind(...)` names columns only), then execution died with a
bare `ValueError: Missing edges` (pandas/cuDF `materialize_nodes`) or an empty-message
`AssertionError` (polars `ensure_nodes_polars`). A preflight that approves what the
runtime always rejects is drift by construction.

Both sides now consult one predicate, `validate_graph_shape`:

* neither nodes nor edges bound -> GFQLSchemaError E305 `graph-not-bound` (new code),
  with a bind remedy -- raised by the validator AND by execution on every engine
  (pandas/cuDF via `validate_chain_schema`, polars via the pre-dispatch guard).
* edges unbound + an edge pattern -> E304, the same typed decline execution already
  gave (#1942); the validator no longer stays silent about it.
* nodes-only + node-only pattern stays admitted on both sides and keeps answering.

`schema=False` callers (remote preflight, which holds no local frames) skip the shape
check, so `chain_remote` is unaffected.

Pins: `test_validate_execute_agreement_1889.py` -- the 4 filed combos x {pandas, polars}
agreement matrix (10/18 red at master 0c3f3a1), typed-code pins on both surfaces,
value-level served-record oracles, and the not-flagged shapes (bound graph, edges-only,
schema=False). Each of the 5 fix sites was mutation-checked. `test_gfql_validate_only`'s
structural-only expectation moved to `schema=False`, which is what it was really pinning.

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

Copy link
Copy Markdown
Contributor Author

Gate receipts

Full graphistry/tests/compute, same box, same interpreter, head vs master 0c3f3a1fa:

master  105 failed, 12146 passed, 978 skipped, 46 xfailed  in 636.80s
head    105 failed, 12167 passed, 978 skipped, 46 xfailed  in 619.36s

Failure SET diff (sorted nodeids, - reason stripped): empty in both directions — 0 new failures, 0 fixed-by-accident. The +21 passes are exactly this PR's new pins (18) plus the reworked test_gfql_validate_only cases.

Anti-vacuity receipts

Red at master (pins copied into a detached 0c3f3a1fa worktree): 10 of the 18 new cases fail, plus test_gfql_validate_chain_without_bound_tables_diagnoses_the_unqueryable_graph. The 8 that pass at master are the cells that must stay green on both sides (nodes-only served with value oracles, bound graph, edges-only, schema=False).

Mutation battery — each fix site reverted individually, then restored (pin file + test_gfql_validate_only.py, 33 cases green intact):

mutation result
drop the E305 diagnostic 9 failed, 24 passed
drop the E304 diagnostic 2 failed, 31 passed
drop the shape check in validate_chain_schema (pandas/cuDF exec + chain validator) 8 failed, 25 passed
drop the polars pre-dispatch shape check in chain() 5 failed, 28 passed
drop the Cypher-validator shape check 4 failed, 29 passed
restored 33 passed

Other gates: ruff clean; mypy Success: no issues found in 331 source files; type-hygiene guard OK (no growth); cypher-surface guard pass.

CI note: python-lint-types (3.12) fails on this PR — and python-lint-types (3.10) fails identically on master 0c3f3a1fa itself — on a pre-existing comment-encoding finding in graphistry/compute/gfql/lazy/engine/polars/row_pipeline.py (7 findings vs baseline 6), a file this PR does not touch and which another lane owns. Because the test lanes needs: the lint job, test-gfql-core / test-polars / the rest show as skipped here — that is the master breakage propagating, not a signal about this change. The local full-suite comparison above stands in for them until master's lint lane is green again.

lmeyerov and others added 2 commits August 18, 2026 22:02
The change is user-visible (new ErrorCode.E305; shapes that returned
ok:true now report a typed GFQLSchemaError), so it needs an entry like
every other fix in this wave. Notes explicitly that nothing which
executed before is refused, and that schema=False still skips the check
so chain_remote preflight is unaffected.

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
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.

bug(gfql): gfql_validate returns ok:true on shapes where execution 100% crashes (validator/runtime drift)

1 participant