Repository navigation
fix(gfql): make gfql_validate agree with execution on unqueryable graph shapes (#1889) - #1951
Conversation
…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
Gate receiptsFull Failure SET diff (sorted nodeids, Anti-vacuity receipts Red at master (pins copied into a detached Mutation battery — each fix site reverted individually, then restored (pin file +
Other gates: ruff clean; mypy CI note: |
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
Fixes #1889
The drift
gfql_validatesold 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):MATCH (a) RETURN adiagnostics: []ValueError: Missing edges; polarsAssertionError(empty message)GFQLSchemaError [E305 graph-not-bound][n({'v': 20})]diagnostics: []MATCH (a) RETURN a[n({'v': 20})]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:GFQLSchemaErrorwith the newE305 = "graph-not-bound"and a bind remedy. Emitted bygfql_validate(chain + Cypher) and by execution on every engine: pandas/cuDF reach it throughvalidate_chain_schema, polars through a pre-dispatch guard inchain()(the polars mirror files are untouched — another lane owns them).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.Execution was not weakened anywhere: every shape that answered at master still answers with identical values (pinned), and the bare
ValueError/AssertionErrorbecame typed diagnostics rather than the validator becoming permissive.schema=Falsecallers skip the shape check — remote preflight (chain_remote) validates against a graph whose frames live server-side, so local emptiness says nothing there.E305chose a new code rather than overloadinginvalid-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):[{'a.id': 0, 'a.v': 10}, ...]), a fully bound graph and an edges-only graph are NOT flagged, andschema=Falsestill returns ok. py3.13 NaN handling normalizes at value level (None if isinstance(v, float) and isnan(v)), neverwhere(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_onlyasserted the drift itself (ok:true on a frameless graph, from #1321). It moves toschema=False, which is the contract it was really pinning, and the schema-on case now asserts E305.Gates
Success: no issues found in 331 source files(no new errors)graphistry/compute/gfql/lazy/engine/polars/row_pipeline.py(7 findings vs baseline 6) — byte-identical at0c3f3a1fa, and that file belongs to another lanegraphistry/tests/computefailure SET compared against master (see PR comment)