Skip to content

fix(gfql): explain engine-mismatched index scans - #1838

Merged
lmeyerov merged 2 commits into
masterfrom
fix/gfql-index-engine-mismatch-diagnostic
Jul 31, 2026
Merged

lmeyerov merged 2 commits into
masterfrom
fix/gfql-index-engine-mismatch-diagnostic

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

What

Add an gfql_explain diagnostic when a valid resident adjacency index belongs to the other requested engine and GFQL intentionally declines it for a scan.

Why

The mismatch was silent in both pandas-to-polars and polars-to-pandas directions while show_indexes() remained valid, hiding a severe default-path cliff.

Scope

Diagnostic only: no execution, coercion, rebuild, or index-policy change.

Validation

Targeted diagnostic tests, the GFQL index module suite, lint/hygiene, and mypy 2.3.0 passed on DGX.

@lmeyerov
lmeyerov marked this pull request as ready for review July 30, 2026 23:28
@lmeyerov
lmeyerov merged commit 84e05d5 into master Jul 31, 2026
57 checks passed
@lmeyerov
lmeyerov deleted the fix/gfql-index-engine-mismatch-diagnostic branch July 31, 2026 06:49
pull Bot pushed a commit to admariner/pygraphistry that referenced this pull request Aug 2, 2026
…t 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 graphistry#1767's disposition comment ("stop
show_indexes() reporting valid=True for an index the resolved query engine
cannot use"). graphistry#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 graphistry#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 (graphistry#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 <[email protected]>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
lmeyerov added a commit that referenced this pull request Aug 12, 2026
, stacked on #1743)

resolve_engine(AUTO) maps polars input frames to PANDAS (legacy input-format
policy), so gfql_index_all()/create_index() with the default engine
coerce-and-REPLACED a user's polars frames with pandas copies: every later
polars-engine query then paid a full-frame pandas->polars re-conversion (O(E)
per call) and the resident pandas index could never fingerprint-match.

New resolve_index_engine(): when the caller said AUTO and BOTH resident frames
are eager polars, the index layer resolves POLARS and indexes the frames in
place (the layer is already engine-polymorphic: numpy sidecar arrays + polars
row-gather). Wired into create_index, _is_resident_index_valid, show_indexes,
and the DDL wire's NODE_PROP reuse check. Explicit engines are unchanged.

Why this is safe NOW and was not in 2026-07: the retracted #1767 shipped this
without query-side AUTO routing, so an AUTO-built polars index met an
AUTO(pandas) query -> every hop declined to the scan floor, a measured
125-534x DEFAULT-path regression. This branch is STACKED ON #1743
(fix/gfql-auto-engine-polars-native), whose gfql() guard routes AUTO on
all-polars-frame graphs to the native polars engine -- so the cliff inverts
into the win: AUTO builds the polars index, AUTO queries route polars, index
serves. Pinned by test_inversion_auto_index_auto_gfql_serves_polars_index
(index_trace() must show path=index engine=polars with NO engine argument
anywhere, plus value parity vs both explicit-engine spellings).

show_indexes() under AUTO now reports the routed truth through the #1841
usable/reason columns: the previously not-usable "polars index + AUTO query"
combination flips to usable=True (test renamed to
test_polars_index_auto_query_usable_the_1841_flip); explicit mismatched-engine
previews keep the #1838 decline wording.

Deliberately narrower than #1743's query gate, self-consistent either way
because create_index coerces the frames it indexes: LazyFrame graphs keep the
legacy pandas build (an index cannot gather from a lazy plan), and edges-only
graphs keep it too (materialize_nodes() does not yet produce polars nodes from
polars edges -- pre-existing gap, also reachable via explicit engine='polars').
Pandas/cuDF graphs, mixed frames, and nodes-only graphs are unchanged, each
pinned.

Verification (local CPU lane, cudf/gpu params env-excluded on this box):
index suite 229 passed; polars cypher conformance + cache coverage lock
177 passed / 3 skipped; lint.sh clean; scoped mypy (api.py, wire.py) clean;
type-hygiene guard OK (no growth).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
lmeyerov added a commit that referenced this pull request Aug 12, 2026
, stacked on #1743)

resolve_engine(AUTO) maps polars input frames to PANDAS (legacy input-format
policy), so gfql_index_all()/create_index() with the default engine
coerce-and-REPLACED a user's polars frames with pandas copies: every later
polars-engine query then paid a full-frame pandas->polars re-conversion (O(E)
per call) and the resident pandas index could never fingerprint-match.

New resolve_index_engine(): when the caller said AUTO and BOTH resident frames
are eager polars, the index layer resolves POLARS and indexes the frames in
place (the layer is already engine-polymorphic: numpy sidecar arrays + polars
row-gather). Wired into create_index, _is_resident_index_valid, show_indexes,
and the DDL wire's NODE_PROP reuse check. Explicit engines are unchanged.

Why this is safe NOW and was not in 2026-07: the retracted #1767 shipped this
without query-side AUTO routing, so an AUTO-built polars index met an
AUTO(pandas) query -> every hop declined to the scan floor, a measured
125-534x DEFAULT-path regression. This branch is STACKED ON #1743
(fix/gfql-auto-engine-polars-native), whose gfql() guard routes AUTO on
all-polars-frame graphs to the native polars engine -- so the cliff inverts
into the win: AUTO builds the polars index, AUTO queries route polars, index
serves. Pinned by test_inversion_auto_index_auto_gfql_serves_polars_index
(index_trace() must show path=index engine=polars with NO engine argument
anywhere, plus value parity vs both explicit-engine spellings).

show_indexes() under AUTO now reports the routed truth through the #1841
usable/reason columns: the previously not-usable "polars index + AUTO query"
combination flips to usable=True (test renamed to
test_polars_index_auto_query_usable_the_1841_flip); explicit mismatched-engine
previews keep the #1838 decline wording.

Deliberately narrower than #1743's query gate, self-consistent either way
because create_index coerces the frames it indexes: LazyFrame graphs keep the
legacy pandas build (an index cannot gather from a lazy plan), and edges-only
graphs keep it too (materialize_nodes() does not yet produce polars nodes from
polars edges -- pre-existing gap, also reachable via explicit engine='polars').
Pandas/cuDF graphs, mixed frames, and nodes-only graphs are unchanged, each
pinned.

Verification (local CPU lane, cudf/gpu params env-excluded on this box):
index suite 229 passed; polars cypher conformance + cache coverage lock
177 passed / 3 skipped; lint.sh clean; scoped mypy (api.py, wire.py) clean;
type-hygiene guard OK (no growth).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_011AB4RZpph3uSFUpzKnZJcr
lmeyerov added a commit that referenced this pull request Aug 13, 2026
, stacked on #1743)

resolve_engine(AUTO) maps polars input frames to PANDAS (legacy input-format
policy), so gfql_index_all()/create_index() with the default engine
coerce-and-REPLACED a user's polars frames with pandas copies: every later
polars-engine query then paid a full-frame pandas->polars re-conversion (O(E)
per call) and the resident pandas index could never fingerprint-match.

New resolve_index_engine(): when the caller said AUTO and BOTH resident frames
are eager polars, the index layer resolves POLARS and indexes the frames in
place (the layer is already engine-polymorphic: numpy sidecar arrays + polars
row-gather). Wired into create_index, _is_resident_index_valid, show_indexes,
and the DDL wire's NODE_PROP reuse check. Explicit engines are unchanged.

Why this is safe NOW and was not in 2026-07: the retracted #1767 shipped this
without query-side AUTO routing, so an AUTO-built polars index met an
AUTO(pandas) query -> every hop declined to the scan floor, a measured
125-534x DEFAULT-path regression. This branch is STACKED ON #1743
(fix/gfql-auto-engine-polars-native), whose gfql() guard routes AUTO on
all-polars-frame graphs to the native polars engine -- so the cliff inverts
into the win: AUTO builds the polars index, AUTO queries route polars, index
serves. Pinned by test_inversion_auto_index_auto_gfql_serves_polars_index
(index_trace() must show path=index engine=polars with NO engine argument
anywhere, plus value parity vs both explicit-engine spellings).

show_indexes() under AUTO now reports the routed truth through the #1841
usable/reason columns: the previously not-usable "polars index + AUTO query"
combination flips to usable=True (test renamed to
test_polars_index_auto_query_usable_the_1841_flip); explicit mismatched-engine
previews keep the #1838 decline wording.

Deliberately narrower than #1743's query gate, self-consistent either way
because create_index coerces the frames it indexes: LazyFrame graphs keep the
legacy pandas build (an index cannot gather from a lazy plan), and edges-only
graphs keep it too (materialize_nodes() does not yet produce polars nodes from
polars edges -- pre-existing gap, also reachable via explicit engine='polars').
Pandas/cuDF graphs, mixed frames, and nodes-only graphs are unchanged, each
pinned.

Verification (local CPU lane, cudf/gpu params env-excluded on this box):
index suite 229 passed; polars cypher conformance + cache coverage lock
177 passed / 3 skipped; lint.sh clean; scoped mypy (api.py, wire.py) clean;
type-hygiene guard OK (no growth).

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