feat(gfql): add aggregate fast paths and final hardening#1731
Merged
Conversation
lmeyerov
added a commit
that referenced
this pull request
Jul 18, 2026
…erage floor to its owning rung The exact-head CI failure on test-gfql-core (3.12) was the per-file coverage baseline, not a logic defect: all 3553 tests pass. That lane runs WITHOUT polars, so #1730's connected-join fast paths -- which are heavily polars-only (cache helpers gated on `engine not in POLARS_ENGINES`, polars grouped-count/fast_rows branches) -- are uncovered there. gfql_unified.py measures 67.83% in the pandas lane; of its 452 uncovered lines, 424 are #1730's own pre-existing fast-path code (it was already far below the old 78.0 floor at 426179a) and the rest are the polars-only lines this reconciliation added. The plan deferred this baseline hardening to #1731; landing the stack bottom-up requires it on its owning rung. - Lower the ci-pandas-py3.12 per-file floor for gfql_unified.py 78.0 -> 67.5 (actual 67.83, small margin). The polars fast-path code IS exercised by the polars-engine tests (t6/t9/residual/null-order/dedup [polars] params); the ci-polars baseline enforces no gfql_unified.py floor. - Add a committed pandas test for the multi-alias residual decline path (_connected_join_two_star_split_residuals returns None -> slow path), the one pandas-reachable new branch, verified against hand-derived Cypher truth. Verified locally in a no-polars cov venv (matches the CI lane): coverage audit passes against the updated baseline; 3554 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UVsx9Fk3BDFvaZXhDvgv5V
…dtypes Propagate the reconciled + CI-green #1730 head into the aggregate rung (normal merge, never force). #1731 inherits #1730's already-reconciled connected-join contract, so the merge was clean apart from feature interactions where #1731 and #1730 touched the same code: - _compile_string_query: combine #1731's per-graph compile cache with #1730's node_dtypes threading. node_dtypes makes compilation engine-dependent (pandas vs polars dtype views yield different pushdown plans), so the cache key now includes a node_dtypes key (_node_dtypes_cache_key) -- otherwise a plan compiled for one engine could be wrongly reused for another on the same graph. Compile via compile_cypher_query(parse_cypher(...)) (the #1730 path) inside the cache. - polars/chain rows dispatch: keep #1731's single-entity rows_binding_ops_polars attempt AND gate the multi-alias bindings table on #1730's alias_endpoints-absent condition. - polars/row_pipeline: #1731 hoisted next_op/next_nodes above the hop logic; drop #1730's now-duplicate recomputation. - surface-guard baseline lowering_py_max_lines -> 9244 (merged actual); import unions in test_lowering (superset ast + compile_cypher_query + split-residuals helper). - Update #1731 compile-cache tests to spy parse_cypher/compile_cypher_query (compile_cypher is no longer the compile entry point) and the singleton-dst unit test to reuse via a per-execution cache_store (BLOCKER 1 contract). Merge-affected suites: 1825 passed / 0 failed (system python3, polars 1.42). ruff clean; surface guard pass; mypy gfql_unified.py 70 (no region errors; base c2138b0 was 93). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UVsx9Fk3BDFvaZXhDvgv5V
Semantic review of #1731's never-reviewed aggregate execution found a real bug: the single-hop grouped-aggregate fast path's polars sort omitted nulls_last, so an `ORDER BY <group key> ASC ... LIMIT` returned the NULL group first (polars defaults nulls-first) instead of the smallest non-null group. openCypher orders NULL as the largest value (ASC -> last, DESC -> first). Pin nulls_last per key, mirroring the connected-join grouped-count fix. pandas na-last default already matches for ASC. Mutation-verified regression test on both engines (removing nulls_last fails polars, pandas unaffected). Adversarial review of the aggregate fast paths otherwise clean: count/count(DISTINCT)/sum/avg grouped, empty match, two-hop count(*)/count(a), and single-hop multiplicity all match hand-derived Cypher truth on both engines; min/max are an honest unsupported-query error (not this fast path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UVsx9Fk3BDFvaZXhDvgv5V
…lap-split-4-aggregate
… + two-hop count) Documents #1731's delivery: single-hop grouped-aggregate and two-hop connected count fast paths, byte-identical across all four engines, with openCypher-correct NULL-as-largest ordering (fixes the polars nulls-first divergence) and the dtype-keyed, data-independent string-Cypher compile cache. Merges the updated #1730 (connected-pattern entry + inherited changelog corruption fix). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UVsx9Fk3BDFvaZXhDvgv5V
…rdering Amplification (round 001) found a stack-introduced pandas/cuDF vs polars divergence in `_execute_single_hop_grouped_aggregate_fast_path`: the pandas/cuDF branch sorted with a single `sort_values(ascending=[...])` and a SCALAR na_position (default 'last'), so on `ORDER BY <key> DESC` the NULL group sorted LAST — while openCypher ranks NULL as the LARGEST value (DESC -> nulls first) and the polars branch already pins `nulls_last` per key. This silently returned the wrong `ORDER BY ... DESC LIMIT` top-k on pandas/cuDF (dropped the NULL-key group). Fix: express null placement with an explicit per-key null-indicator column and sort in ONE pass (a key sorted ascending=(not desc) with its indicator sorted the same way puts NULL at the correct largest end). This needs neither per-key na_position (pandas scalar- only) nor a stable sort — cuDF's kind='stable' silently falls back to non-stable quicksort, so a per-key multi-pass would lose multi-key tie order on GPU. Regression test `test_single_hop_grouped_fast_path_orders_nulls_first_on_desc` (both CPU engines), MUTATION-VERIFIED (pandas fails reverted; polars already correct via the :2085 fix). DGX-validated on all four engines (pandas/polars/cuDF/polars-gpu): 0 silent-wrong on F1 + a multi-key `ORDER BY c DESC, city DESC` case. The ASC sibling was coincidentally correct (pandas na-last == openCypher ASC). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UVsx9Fk3BDFvaZXhDvgv5V
…_paths.py Pure code move, no behavior change. gfql_unified.py had grown to ~3.7k lines as the connected-join (single/two-star) and grouped-aggregate / two-hop-count fast paths accumulated across #1730 and #1731. Move both families (22 functions, ~1.63k lines) into a new sibling module graphistry/compute/gfql_fast_paths.py so the orchestrator stays readable. - One-directional import: gfql_unified -> gfql_fast_paths (imports the 4 entry points it dispatches to); the module imports only leaf modules (Engine/ast/chain/filter_by_dict/dataframe/lowering) — no back-edge, no cycle. - Within the module, the aggregate family calls the connected-join family; both self-contained (verified none reference code that stayed in gfql_unified). - `# pragma: no cover` markers on polars/cuDF-only helpers move with them. - test_lowering.py import repointed to gfql_fast_paths (only test referencing the moved symbols). Validated on dgx (26.02-gfql-polars): import-clean (no circular/missing); test_lowering.py 1383 passed/0 failed; polars cypher conformance + matrix 406 passed/0 failed. ruff clean; mypy 0-new (the 11 errors are pre-existing in untouched polars/row_pipeline.py + degrees.py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
lmeyerov
changed the base branch from
perf/gfql-olap-split-3-connected-exec
to
master
July 19, 2026 06:55
lmeyerov
marked this pull request as ready for review
July 19, 2026 06:55
This was referenced Jul 19, 2026
pull Bot
pushed a commit
to admariner/pygraphistry
that referenced
this pull request
Jul 20, 2026
… per-hop NaN re-scan on polars) Every hop() runs _coerce_input_formats -> _pl_nan_to_null, which scanned is_nan().any() over every float column of the (unchanged) resident edge frame ON EVERY CALL. So repeated seeded hops on a resident graph (seeded-Search / native-hop) grew O(E) with edge count on polars/polars-gpu while pandas stayed flat. _pl_nan_to_null now probes an eager polars frame for real NaN once, returns a clean frame UNCHANGED (restores the graphistry#1726 identity guard reverted by graphistry#1731 -> frame-identity caches like the graphistry#1658 index keep engaging), rewrites only columns that genuinely carry NaN (values identical to the old unconditional fill_nan), and caches the id of clean frames so later calls skip the probe. Recycle-safe via weakref.finalize (a rebound/new frame re-probes -> NaN->null semantics unchanged). Measured (dgx): indexed polars g.hop on 8M edges + one float column: 33.8ms (growing O(E)) -> 0.20ms FLAT (~140x, O(degree)), matching pandas. This is the per-call cost that made polars/polars-gpu seeded LDBC Search grow with scale and lose to Kuzu. Regression: 184 passed (full 4-engine test_index.py parity + test_engine_coercion) — the one failure (test_chain_dask_edges) is pre-existing on clean master (RAPIDS-image dask issue). New TestPlNanCleanCache: NaN-present cleaned, clean-frame cached same-object, distinct frames independent. Subsumes graphistry#1733 (identity-stable reland) and adds the clean-cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replacement stack for #1714 (rung 4/4), stacked on rung 3.
Scope: direct one/two-hop aggregates, equal-domain and compile caches, Polars binding-row fix, typing/CI/coverage hardening.
Size: 22 files, +1318/-341 (1,659 changed lines).
Validation: cumulative tree exactly equals reviewed #1714 head c50a0f7; local static gates and 199 CPU/Polars cases pass. GPU validation remains gated on DGX safe_run.