You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(gfql): show_indexes reports engine usability, not just fingerprint validity
show_indexes() claimed valid=True for an index the resolved query engine
cannot use. Indexes are engine-specific, so a fingerprint-fresh index built
for another engine (e.g. polars-built, inspected on a graph whose AUTO
queries resolve to pandas) silently declines to a scan at query time while
the inspection surface said everything was fine.
This is the follow-up proposed in #1767's disposition comment ("stop
show_indexes() reporting valid=True for an index the resolved query engine
cannot use"). #1838 made the decline loud in gfql_explain via
_engine_mismatch_reason; this completes the pair on show_indexes by reusing
the same mechanism and wording rather than inventing a parallel one.
- valid keeps its fingerprint-only meaning (backward compatible)
- new columns: query_engine (what engine= resolves to for THIS graph, the
same resolution a query makes; default 'auto'), usable (fresh AND
engine-matched), reason (shared #1838 mismatch wording + stale reason)
- show_indexes(engine=...) previews an explicit engine choice; SHOW GFQL
INDEXES respects the gfql(engine=...) of the call; gfql_explain passes
its engine through
- reporting-only: no query routing changes (#1743 AUTO-routing stays held)
Tests: 9 new (polars index vs AUTO/pandas query not-usable with exact
reason, matching-engine usable, explicit-engine preview both directions,
stale-but-engine-matched not usable, stale+mismatched combined reason,
per-kind reasons across all four index kinds, DDL surface, cudf lane with
import skip-guard). Index suite: 219 passed, 1 skipped locally (GPU lanes
deselected, no GPU on this box). Lint + scoped mypy clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,6 +42,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
42
42
-**GFQL execution context is declared rather than attached at runtime**: the private `_gfql_*` per-execution fields (index policy and registry, row-pipeline base graph, carried seed nodes, edge aliases, shortest-path backend, and the indexed-bindings handoff) are now declared on `Plottable` with defaults on `PlotterBase`, instead of being set with `setattr` and read back with `getattr(..., default)`. No public API or behaviour change; internal call sites are typed, and hand-rolled `Plottable` stand-ins must now construct the full context.
43
43
44
44
### Fixed
45
+
- **`show_indexes()` reported `valid=True` for an index the resolved query engine cannot use**: GFQL physical indexes are engine-specific, so a fingerprint-fresh index built for another engine (e.g. a polars-built index inspected on a graph whose AUTO queries resolve to pandas) silently declines to a scan at query time — yet `show_indexes()` told the user everything was fine. This is the follow-up proposed in #1767's disposition ("stop `show_indexes()` reporting `valid=True` for an index the resolved query engine cannot use"); #1838 made the decline loud in `gfql_explain`, and this completes the pair on the inspection surface. `valid` keeps its fingerprint-only meaning (backward compatible); three columns are added: `query_engine` (what the `engine` argument — default `'auto'` — resolves to for THIS graph, the same resolution a query makes), `usable` (fresh AND engine-matched), and `reason` (why not, reusing #1838's mismatch wording, e.g. `resident edge_out_adj index engine=polars, requested engine=pandas -> scan`, plus a stale-fingerprint reason). `show_indexes(engine=...)` previews an explicit engine choice, and `SHOW GFQL INDEXES` respects the `gfql(engine=...)` of the call. Reporting-only: no query routing changes (#1743 AUTO-routing remains on hold).
45
46
-**The compiled-query cache key omitted the engine, which a shared cache would have turned into a wrong answer.** The key relied on `node_dtypes` to capture engine-dependent compilation, but polars and polars-gpu report *identical* node dtypes and compile differently. While the cache was partitioned per `Plottable` this was masked rather than avoided; sharing plans process-wide exposed it immediately as 16 failures in `test_const_fold_engine_parity.py` on the polars-gpu arm. The key now carries the **resolved** engine -- resolved, because `auto` resolves per graph, so keying on the requested engine would let two graphs that resolve differently share a plan.
46
47
47
48
- **`master` is green again: the type-hygiene ratchet tripped on merge, and the fix is fewer `cast()`s rather than a raised cap**: `bin/ci_type_hygiene_baseline.json` is a per-file SNAPSHOT, so it goes stale whenever a PR *other than the one that owns the baseline* touches a baselined file. #1830 captured its baseline on a branch whose merge-base is `b6181d355` — before #1800, #1799 and #1816 landed — and each of those three added `cast()` calls to a file #1830 had already pinned. All four were green on their own bases; the combination was not, and nothing in any of the four merges signalled it: `explicit-cast` came out 7 over baseline across `gfql_fast_paths.py` (+5), `gfql_unified.py` (+1) and the polars `row_pipeline.py` (+1), failing `python-lint-types` on 3.11/3.13/3.14. The baseline is **unchanged** here; the findings were removed instead. Three of the seven were never `typing.cast` at all — the guard matches any call whose name is `cast`, including the attribute form, so `pl.Expr.cast`, a polars RUNTIME dtype conversion, counts as a typing finding; those three carry the documented `# hygiene-ok: explicit-cast` escape hatch with the reason. The other four were real, and are gone by DECLARATION rather than by call-site assertion, which is the idiom the guard is asking for: `_two_hop_cached_equal_domain_degree_counts` declares `counts: Tuple[DataFrameT, DataFrameT]` once and both arms assign to it, so four casts collapse to one localized `# type: ignore[assignment]` on the polars arm (`DataFrameT` is pinned to pandas at checking time); `_apply_connected_optional_match` declares `seed_ids: SeriesT` / `node_ids: SeriesT` instead of casting, because selecting one column off a frame is a Series on every engine. (The neighbouring `cast(DataFrameT, df_to_engine(...))` pair is deliberately left alone: `df_to_engine` carries no return annotation, so removing those would mean annotating a helper with 115 call sites, and the polars arm of that branch is not reached by any CI lane — rewriting it would have dragged a pre-existing coverage blind spot into the changed-line gate for no typing gain.) All three files now sit AT or BELOW baseline (18/18, 132/133, 41/42). Typing-only: `typing.cast` is the identity function at runtime, so every removal is provably value-preserving, and the one restructured line binds an unchanged `pl.Expr` list to a name so the per-line suppression fits the 127-column limit. **The hazard is latent, not spent** — any future PR adding a finding to a baselined file reds `master` the same way, and the guard cannot see it from inside either PR.
"""Return a pandas DataFrame describing resident GFQL indexes (name, kind, column, valid). Empty if none; ``valid=False`` marks a stale index after a frame rebind."""
0 commit comments