feat(gfql): native lazy Polars engine — collect-once traversals + cypher row pipeline + followups (multi-hop, to_fixed_point, undirected, more predicates, NIE→native)#1660
Merged
Conversation
…tructured returns Squashed reconciliation of the native lazy Polars GFQL engine (was #1648's 28 commits; full history preserved at tag bak/1648) restacked onto the colleague's Engine: native polars hop/chain (semi/anti joins), native cypher row pipeline (select/where/order_by/group_by/unwind/projection), lazy single-hop collect-once with CPU/GPU execution targets (gfql/lazy/). NO pandas bridge — native or honest NotImplementedError (plan.md NO-CHEATING). Reconciliation with #1650 structured returns: apply_result_projection now threads `structured` to the polars path (apply_result_projection_polars). Whole-entity RETURN a flattens to {alias}.{field} columns natively (mirrors the pandas _flat_entity_field_names selection exactly), which — unlike the legacy entity-text expr — works for ANY dtype (float/temporal/nested just become columns), so polars structured == pandas structured across the board. structured=False still renders the native Cypher display string for int/string/bool single-entity nodes. _include_numeric_id_as_property is now polars-aware so id flattens identically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alized hops Build the whole forward/backward combine (combine_nodes/edges + endpoint + alias names) as ONE deferred pl.LazyFrame plan over the already-materialized hop frames and collect once, instead of ~a dozen eager ops that each internally lazy().op().collect(). Stable order columns (NORD/EORD) restore the eager g._nodes/g._edges order since lazy joins don't preserve it -> trailing LIMIT/SKIP unaffected, byte-identical (full polars conformance + row-pipeline parity, 2858 gfql tests). NO recompute (inputs materialized; unlike the disproven whole-chain fusion). ~5% faster polars 1-hop chain @1M/@10m; GPU-target neutral. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single MATCH (n) with no edge hop — the dominant tabular/crossfilter shape (MATCH (n) WHERE/RETURN ..., histograms, filters, table search) — now returns the filtered node table directly and skips the whole forward/backward/combine + collect_all (~2.5 ms fixed cost that dominated small/interactive queries). Byte-identical (full polars conformance + row-pipeline parity, 389 polars tests). Moves the polars>pandas crossover BELOW 100K for real product workloads: categorical histogram 0.68->1.70x @100k / 1.38->7.62x @1m; node filter 2.44->13.85x @1m; timeline 2.55->8.12x @1m. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A single MATCH (a)-[e]->(b) with both nodes unconstrained (no filter/name/query) and a plain edge (no match/name/query) — the basic graph query and the viz edge-crossfilter MATCH — returns ALL edges + their endpoint nodes directly (direction-independent; isolated nodes excluded), skipping forward/backward/ combine. For unconstrained nodes the backward pass prunes nothing, so this is byte-identical (full polars conformance + row-pipeline parity + adversarial graphs: dup/self-loop/cycle/isolated). ~9x faster polars [n,e,n]: 95.6->10.3 ms @1m, 855->99 ms @10m. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the unconstrained 1-hop fast path to filtered nodes: MATCH (a {f})-[e]->(b)
(src/dst/both filters; the dominant "filter then expand" viz crossfilter pattern)
returns the edges whose endpoints pass the node filters + those endpoint nodes,
skipping forward/backward/combine. For one hop the backward pass prunes nothing
beyond the endpoint filters, so byte-identical (verified vs pandas: src/dst/both
filters, reverse, dup/self-loop/cycle/isolated; full polars conformance +
row-pipeline parity, 2858 gfql tests). Unconstrained: all edges any direction;
filtered: forward/reverse (filtered-undirected falls through to the full path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aster) The GPU target collects with pl.GPUEngine(executor="in-memory") instead of the default streaming engine="gpu" (DefaultSingletonEngine). GFQL results fit in device memory, the in-memory engine's regime: faster on the hop primitives (semijoin 1.33x, antijoin 2.58x, unique 1.49x @10m) and far more STABLE -- the streaming executor spiked bimodally to ~1s on the same semijoin (median ~360ms), in-memory holds ~30ms. Fixes the GPU instability seen in the pr11 measurements. Parity preserved (polars-gpu == polars, 39 tests). gfql chains aren't GPU-compute-bound (orchestration + eager fast paths dominate) so this is a stability/correctness fix for GPU-collect paths, not a chain speedup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the #1656 author's handoff: the elif-structured single-column text fallback in _apply_result_projection_pandas looks redundant but fixes two regressions (top-level OPTIONAL-MATCH miss; OPTIONAL-WITH-reentry no-match). Mark DO NOT REMOVE so a later 'tidy' doesn't reintroduce them. Our polars structured-returns reconciliation touched this file; verified the fallback is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pandas drop_duplicates) RETURN ... UNION RETURN ... (distinct) crashed under engine='polars'/'polars-gpu' with AttributeError: 'DataFrame' object has no attribute 'drop_duplicates' — the union de-dup in gfql_unified._execute_compiled_query called pandas-only drop_duplicates on a polars frame. Added engine-aware Engine.df_unique (polars unique(maintain_order=True); pandas/cuDF drop_duplicates(keep='first')), matching the row/frame_ops.distinct convention, and routed the UNION DISTINCT through it. Surfaced by the cross-repo TCK conformance run (tck-gfql TEST_POLARS=1, union1). Regression-tested in test_engine_polars_cypher_conformance.py (4 UNION cases). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t to Boolean)
null AND null / null OR null / NOT null crashed under engine='polars' with
InvalidOperationError ('bitand'/'not' not supported for dtype null): a bare null
literal lowers to a Null-dtype polars expr where &/|/~ are undefined. Cast AND/OR/
NOT operands to pl.Boolean in the expr lowering so Cypher Kleene 3-valued logic
evaluates (true AND null=null, false OR null=null, NOT null=null); casting a real
Boolean column is a no-op, and polars Boolean &/|/~ already match Cypher Kleene.
Surfaced by the TCK run (expr-boolean1/2/4). Regression-tested in
test_engine_polars_cypher_conformance.py (bare-RETURN null boolean cases).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot ArrowInvalid A pandas object column holding mixed Python types (e.g. int 0 + str 'xx' — legal for dynamically-typed Cypher properties) is unrepresentable in polars/Arrow: pl.from_pandas raised a cryptic 'pyarrow.lib.ArrowInvalid: Could not convert xx with type str: tried to convert to int64' from deep inside construction. Wrap the pandas->polars conversion in Engine.df_to_engine (_pl_from_pandas) to raise a clear NotImplementedError naming the offending column(s) and pointing at engine='pandas' (NO-CHEATING: no silent string-coercion, which would change comparison semantics). Surfaced by the TCK run (expr-comparison2, match-where5, with-where5). The harness tolerates honest NIE as a coverage decline; before this they crashed as failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…projection
A property column holding Cypher temporal-constructor text (date({year:1910,...}),
how Cypher/TCK store temporal values) leaked the raw constructor string under
engine='polars' instead of the ISO form ('1910-05-06') the pandas projection
produces via _normalize_temporal_constructor_series. That normalizer is not yet
native, so both projection paths (engine_polars.projection final result projection
+ row_pipeline.select_polars WITH/RETURN) now detect temporal-constructor String
columns (reusing TEMPORAL_CALL_EXPR_RE, native .str.contains scan over String cols
only) and raise NotImplementedError rather than emit a wrong rendering.
Surfaced by the TCK run (with-orderby1-33+, the largest wrong-answer cluster, ~33).
Whole-entity RETURN a over a temporal property is unaffected (flattens + renders via
render_entity_text). Regression-tested in test_engine_polars_cypher_conformance.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… +/-)
a.time + duration({minutes: 6}) silently became STRING CONCATENATION under
engine='polars': cypher duration({...}) translates to an ISO duration string
literal ('PT6M'), and the expr lowering applied + to two strings, so an ORDER BY
sorted lexicographically on the concatenated text (wrong order). The lowering now
raises NotImplementedError when +/- has an ISO-duration string-literal operand
(^-?P(?=[0-9T]), which doesn't misfire on ordinary strings like 'Prefix'); the
pandas engine handles temporal arithmetic.
Surfaced by the TCK run (with-orderby2 cluster, silent wrong-order). Regression-
tested in test_engine_polars_cypher_conformance.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n native lowering filter_by_dict on engine='polars' evaluated any non-natively-lowerable predicate by converting the column to pandas (.to_pandas()), running the pandas callable, and carrying the mask back — a silent polars->pandas bridge presenting pandas semantics as polars. Removed it: unsupported predicates now raise NotImplementedError (use engine='pandas'). To keep common queries native, widened predicate_to_expr: - AllOf (conjunction, e.g. n.val > 20 AND n.val < 90 -> AllOf[GT,LT]) lowered recursively - IsNull/IsNA -> is_null(), NotNull/NotNA -> is_not_null() - case-insensitive STARTS WITH / ENDS WITH via anchored (?i) regex on re.escape'd literal Surfaced from the source-mined optimization review (pygraphistry4 opportunity #6 — a flagged NO-CHEATING violation in the shipping polars lane). The old fallback test (which asserted the bridge worked) now asserts the honest NIE. TCK: no wrong-answer regression; ~39 scenarios that silently passed via the bridge now honestly decline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
….iloc crash) A bounded MATCH ... WITH <scalar> ... MATCH query crashed under engine='polars' with AttributeError: 'DataFrame' object has no attribute 'iloc' — the engine- agnostic re-entry broadcast (cypher/reentry/execution.py) used pandas .iloc / .assign / .drop(columns=) on a polars frame. Added engine-aware helpers (polars row(i, named=True) + with_columns(pl.lit(...)) / drop / head(0)) for the scalar-row extraction + constant-column broadcast. Re-entry now completes; a downstream RETURN the polars engine can't yet render raises honest NotImplementedError, not a crash. Surfaced by the TCK run (with2-1, with4-2, expr-typeconversion2/3/4-*). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ot validation error)
A multi-clause OPTIONAL MATCH needing null-row fill (some seed rows unmatched)
raised GFQLValidationError ('unsupported-cypher-query ... null-row alignment could
not recover matched seed identities') under engine='polars' — the null-fill
alignment (matched-id meta, .iloc row slicing, per-segment concat) is pandas-centric
and the polars OPTIONAL MATCH doesn't populate the _cypher_entity_projection_meta
['ids'] it needs. Guarded the polars path to raise NotImplementedError instead (the
honest 'not native yet' signal the TCK harness tolerates) — pandas runs these fine.
Surfaced by the TCK run (match7-7, expr-graph4-4).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
OPTIONAL MATCH (n) RETURN n with no match rendered the absent whole entity as '()' under engine='polars' instead of null — the native entity-text expr didn't nullify absent rows (whose alias marker column is null). Now wraps the rendered text with pl.when(col(alias).is_null()).then(None) (mirrors pandas _nullify_missing_alias_rows); a real property-less node still renders '()'. Surfaced by the TCK run (match7-1). Regression-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (not == cast crash) A label match MATCH (n:Label) targeting the reserved 'labels' List column (a label with no one-hot label__X column: typed-schema unknown labels, OPTIONAL MATCH to a non-existent label) crashed under engine='polars' with InvalidOperationError: cannot cast List type to String — filter_by_dict_polars lowered it to a scalar == that tried to cast the List to String. Now uses pl.col(c).list.contains(val) for List-dtype columns: correct Cypher label-membership (Label in n.labels), empty for a non-existent label (matching pandas). Surfaced by the TCK run (match7-28, firstparty-typed-schema1-3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NaN: comparisons over a NaN computed inside polars (0.0/0.0 > 1) used polars' semantics (NaN = largest value, NaN>1 True), but IEEE/Python/pandas/Neo4j-Cypher compare any NaN false (!= true). The expr lowering now masks float comparisons to the IEEE answer (& ~is_nan for < > <= >= =, | is_nan for <> !=), gated by conservative float-operand inference (via a free schema contextvar) so int/string/ bool comparisons are untouched and is_nan() never hits a non-float expr. Input NaN is already nan_to_null'd by pl.from_pandas, so this only affects in-query float math. Numeric-vs-string: comparing a number to a string (n.val > 'a', 0.0/0.0 > 'a') crashed with ComputeError: cannot compare string with numeric type. Detect the mismatch in both the expression path (lower_expr) and the folded filter-predicate path (filter_by_dict_polars) and raise honest NotImplementedError, not a crash. Surfaced by the TCK run (expr-comparison2-5-*, the 4-scenario NaN cluster). Regression-tested in test_engine_polars_cypher_conformance.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphic wrong-answer)
Comparing cypher temporal values (time({...}) > time({...}), date < date) gave a
WRONG answer under engine='polars': the cypher->gfql lowering renders the
constructors to ISO strings ('10:00+01:00'), and the polars engine compared them
LEXICOGRAPHICALLY — wrong across timezones/precision (pandas parses them temporally).
The lowering now detects an ISO date/datetime/time string-literal operand in a
comparison (specific regex; requires seconds-or-tz on bare times so ordinary '10:00'
strings don't match) and raises honest NotImplementedError. Native temporal-typed
comparison is the tracked proper fix.
With this, the native polars engine has ZERO wrong-answers across the full Cypher
TCK (3834 passed / 0 failed / 388 honest declines) — every scenario matches pandas
or honestly declines. Surfaced by the TCK run (expr-temporal7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oral guards Multi-wave adversarial review of the session conformance fixes found 3 BLOCKERs (silent wrong-answers/panics, all NO-CHEATING violations) + IMPORTANTs: - BLOCKER: NaN guard missed int/int->Float division and function results (abs/ coalesce) -> polars NaN-as-largest leaked as wrong answers. Now drive the NaN + cross-type guards from the lowered exprs OUTPUT dtype (_expr_output_dtype, schema- only) instead of AST type inference — robustly catches division/functions. Replaces the three _infer_is_* helpers (DRY). - BLOCKER: list.contains was applied to ANY List column, so a user List property (n.tags = scalar) returned membership (wrong) vs pandas equality. Gated to the reserved labels column; other List columns decline honestly. - BLOCKER: numeric-vs-string nested in AllOf (x>20 AND x<z) or Between bypassed the cross-type guard and PANICKED (uncatchable Rust). _is_cross_type_predicate now recurses AllOf/Between. - IMPORTANT: Categorical/Enum columns now treated as string-like in both cross-type guards (categorical-vs-numeric was a raw ComputeError). - IMPORTANT: all-null columns (typed String by from_pandas) crashed on arithmetic (n.val + 1); cross-type guard now covers arithmetic ops, not just comparison. - IMPORTANT: ISO-temporal comparison guard narrowed to ORDERING of two temporal literals (was declining valid string-column-vs-date-literal compares; = and <> are lexicographically correct so not declined). - Anchored the temporal-constructor scan regex (no false-positive on update(...)). - Added the missing CHANGELOG entry (OPTIONAL MATCH null-fill decline) + streaming comment clarity. Full TCK still 3834 passed / 0 wrong-answers / 387 honest declines. 457 polars tests pass. 6 new adversarial regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… OTel span placement Adversarial review of the lower stack layers (#1648 polars engine, #1652 generic fast paths) found 2 bugs: - BLOCKER (#1648): a chain crashed under engine=polars with SchemaError when an edge endpoint dtype differed from the node-id dtype across int<->float (e.g. a null in a source/dest column -> float64 vs int64 ids) where pandas joins fine. The hop aligns join keys; the chain fast paths + combine did not. Added _align_edge_endpoints (cast endpoints to node-id dtype for the traversal, restore output dtype to match pandas; no-op when dtypes match) wired into the single-hop fast path + multi-hop. - (#1652): the gfql.chain OTel @otel_traced decorator had landed on the internal _try_chain_fast_path probe (inserted between the decorator and def chain) instead of the public chain() — chain() lost its span, span recorded wrong fn/attrs. Moved it. Both verified + regression-tested. 457 polars tests + 334 generic chain tests pass (4 fails are the pre-existing local libnvrtc CUDA-env issue, not these changes). Row-order divergences the review also found (fast path returns table order vs the full machinery BFS-discovery order, for reverse/undirected without ORDER BY — Cypher- undefined, sets/values identical, no repo test depends on it) are claim-precision, not bugs; CHANGELOG wording to be tightened in the per-layer #1648/#1652 review pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ype + DRY/docs
Multi-dimension adversarial review (correctness/robustness/quality/docs) of the
native polars engine found three reachable pandas-oracle divergences, now fixed
(NO-CHEATING — match pandas or decline honestly):
- BLOCKER: duplicate alias [n('a'), e(), n('a')] returned a malformed colliding-
join schema (a/a_right) instead of raising; now raises GFQLValidationError E201
like pandas (node/edge aliases scoped separately, mirroring combine_steps).
- BLOCKER: integer-literal division 5/2 lowered to polars true division (2.5) but
Cypher folds to int division (2) — silent wrong order when embedded non-
monotonically (ORDER BY n.val % (10/4)); now declines (NIE). Column / int (Float
on both) unaffected.
- IMPORTANT: internal start_nodes seed with a divergent id dtype (empty crossfilter
-> float64 vs int64 node ids) crashed the combine join (SchemaError); now aligns
the seed key (_align_seed_dtype), mirroring the hop + edge-endpoint alignment.
Quality/docs:
- Removed stale 'pandas bridge' docstrings/comments (row_pipeline, projection) —
the bridge was removed in the de-cheat commit; the code raises NIE.
- DRY: consolidated the cross-type/NaN dtype classifiers (numeric/int/float/
stringlike), duplicated 4x, into engine_polars/dtypes.py (the guard contract).
- Aligned the lazy hop allowed_source guard textually with the eager hop (no-op:
to_fixed_point is NIE'd upstream) to stop future eager/lazy drift.
- Removed two dead imports in chain.py (hop_polars, Engine).
- Documented a narrow filter_by_dict genuine-NaN residual (unreachable on the
from_pandas ingestion path that null's NaN).
+3 regression tests. 457 polars+chain tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion (NaN vs None) CI polars 1.40.1 renders a null entity column as NaN (newer polars: None) on polars->pandas conversion. Assert is-null (pd.isna) instead of == [None]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rs/ (was engine_polars/)
Pure move, no logic change. lazy/__init__ already specifies per-backend lowering under
lazy/engine/<backend>/ (future lazy/engine/duckdb, .../dask); the eager polars lowering
was in a flat sibling engine_polars/. Collapse it into the one polars-backend home so
future lazy backends don't proliferate flat engine_* siblings:
- engine_polars/{chain,dtypes,predicates,projection,row_pipeline}.py -> lazy/engine/polars/
- engine_polars/hop.py (eager) -> lazy/engine/polars/hop_eager.py (distinct name; the existing
lazy collect-once hop.py is left in place — no collision).
- engine_polars/__init__ re-exports folded into lazy/engine/polars/__init__; engine_polars/ removed.
- imports rewritten engine_polars -> lazy.engine.polars (eager hop -> .hop_eager).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redo of the per-op GPU engine (#1654, a perf regression) as a TARGET of the lazy engine: engine='polars-gpu' runs the same single-deferred-plan + collect-once on the cudf_polars GPU backend. Tiny wiring on top of the lazy engine — the lazy/ framework already does target-aware collect. - Engine.POLARS_GPU = 'polars-gpu' + POLARS_ENGINES; explicit opt-in (AUTO never picks it); frames stay pl.DataFrame (treated like POLARS in frame ops). - compute/{hop,chain}.py dispatch: engine in (POLARS, POLARS_GPU) -> wrap the lazy call in target_mode(GPU if POLARS_GPU else CPU). ComputeMixin + gfql_unified same-path WHERE accept POLARS_GPU. engine='polars' (CPU) byte-for-byte unchanged. - raise_on_fail=False (GPU-incapable nodes stay on CPU in polars; no pandas bridge). dgx: parity engine='polars-gpu' == engine='polars' (test_engine_polars_gpu.py 36 passed); full gfql suite 2921 passed, 0 failed. Single-hop GPU 2.84x @1m (vs the per-op regression); chain-level GPU win currently dilutes (fwd+bwd 2 collects + eager combine) -> next opt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(P-B) GFQL_POLARS_CPU_STREAMING=1 runs the polars-CPU lazy collects (hop/chain) on the streaming executor. Benchmarked (dgx, interleaved A/B, parity-identical): ~1.11x at 10M nodes/80M edges (20.0->18.0s), ~1.04x at 1M, but ~0.86x (slower) at 100K — the streaming overhead loses on small/interactive sizes. So default OFF (behavior unchanged); opt-in for large batch traversals. From the blogpost perf-opt handoff item B (polars-CPU heavy-join scaling). The full streaming win in isolation is larger (80M 2-hop semijoin 1669->1040ms, 1.6x); the real chain dilutes it via the forward/backward/combine overhead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ragma GPU collect #1655 changed-line-coverage gate (newly enforced once upstream jobs pass) flagged 8 GPU/polars dispatch lines. Fix honestly: a CPU test exercises the lazy collect()/ collect_all() CPU path + the POLARS branches of df_concat/df_cons/s_cons/df_to_engine (reachable but not hit by the coverage suites); only the 2 genuinely GPU-target collect lines (lf.collect(engine=gpu) / pl.collect_all(..., engine=gpu)) are pragma:no-cover (need a device CI lacks). Changed-line coverage of #1655 back to ~100%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…allback (NO-CHEATING) The lazy GPU collect used pl.GPUEngine(raise_on_fail=False): any plan node the cudf_polars backend cannot execute silently ran on CPU and was still reported as a polars-gpu result -- so engine='polars-gpu' was indistinguishable from engine='polars' whenever the plan was not fully GPU-capable. A bulk bench showing near-identical polars/polars-gpu timings is exactly this tell. Flip to raise_on_fail=True and translate the cudf_polars failure into a clear NotImplementedError pointing at engine='polars'. polars-gpu is now GPU-or-error: any timing it produces is real on-device work, never CPU mislabeled as GPU. Verified on dgx-spark (LiveJournal 35M): the seeded hop / 2-hop chain plan runs fully on GPU without raising (nvidia-smi 92% util), so existing GPU timings are unchanged -- only the honesty guarantee is added. +1 regression test covering the translated error path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
f7dcde6 to
8577f4f
Compare
…to' honesty
Add test_gpu_executor_modes_parity[in-memory|streaming]: runs a traversal under each
cudf-polars GPU executor on a real GPU and asserts parity with CPU polars (pandas-gated).
Locks in the 'streaming' executor, previously covered only by a mock-wiring assertion +
manual dgx runs. Skipped in CI (no GPU); runs on the dgx GPU lane. dgx-verified: 2 pass.
Also tighten the gpu_executor() docstring: 'in-memory'/'streaming' are the ONLY selectable
values; a size-aware 'auto' is a possible future addition, NOT selectable today
(set_gpu_executor('auto') raises) — so a reader doesn't mistake it for a current option.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r ladders (cleanup P1) pl.Expr implements the Python operator protocol, so op(lhs, rhs) builds the identical expression the if-ladders did. predicates._cmp_expr gains _CMP_OPS (both the DateValue and scalar ladders collapse); row_pipeline._apply_binop gains _BINOP_FNS. No behavior change; parity comments retained. Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ort, drop dead can_* probes (cleanup P2) select_polars and with_columns_polars were byte-identical except for the projection method — shared _project_polars(extend=) keeps ONE copy of the temporal-constructor NO-CHEATING decline. _lower_function's 7 branch-local 'import polars as pl' hoist to one function-level import (polars stays an optional dep). can_select_native/can_order_by_native had zero callers repo-wide (grep-verified) — removed. Module docstring corrected: CaseWhen and the function whitelist ARE lowered (was stale). No behavior change. Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…at copies (cleanup P3) The src-stacked-on-dst node-id-universe concat (with the int/float join-key dtype cast polars won't do implicitly) appeared verbatim 8x across hop/hop_eager/chain. One dtypes.py helper builds the identical expression; each call site keeps its own .unique(...) variant (plain vs subset= is load-bearing for lazy maintain_order). Typed with a constrained PolarsT TypeVar (eager-in/eager-out, lazy-in/lazy-out). Built plans unchanged. Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nts/col_dtype (cleanup P4) executor.py's three per-function import+call blocks collapse to a membership test over the explicit (grep-able) name tuple + getattr on the degrees module. degrees.py's three group_by-cast-count expressions share _endpoint_counts (the int/float join-key cast comment lives once); node-dtype lookups use the new dtypes.col_dtype. chain._try_native_row_op degree branches intentionally kept unrolled (collapsing costs legibility for ~1 line). No behavior change. Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…leanup P5) rows() and count_table() carried two divergent copies of the alias-mask NULL->False logic (where/isna vs plain fillna — same intent); one helper now serves both (rows' more defensive variant; identical result, 4-engine count_table parity covers it). ComputeMixin's two inline polars module-string checks swap to Engine.is_polars_df (the declared single source of truth). Skipped as not-worth-it: resolve_engine swap (polars-Series edge case), and/or reduce()-folds (3 idiomatic lines each, reduce is not clearer), chain degree-branch collapse (legibility loss for ~1 line). Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-> AXES table + 7 parametrized checks (cleanup T1) Same 28 test cases (7 checks x 4 axes, per-axis parametrize ids), same failure messages (registry/exercised/waiver names injected), all four waiver dicts byte-identical — the ledger CONTENT is untouched, only its test encoding is table-driven. A new coverage axis is now one AXES entry. Mutation-verified: deleting the AllOf waiver fails [predicates] naming AllOf; a bogus waiver key fails the stale-waiver check on both axes it lands in. Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hared polars_test_utils (cleanup T2+T3a) polars_test_utils.py now holds the ONE definition of the comparison machinery (to_pandas_any / typed_frame_sig / run_status / available_nonpandas_engines / assert_parity_or_nie / assert_surfaces_agree) with loud-failure contracts documented. The matrix's hand-unrolled clone families become tables that keep per-case ids + one-line 'why' rationales: _NATIVE_OK_CYPHER (7) + _HONEST_NIE_CYPHER (5) + _native_ok_query_cases (4) + _polars_nie_query_cases (10); the degree trio unifies over one fn parametrize (native probe kept for all 3). assert_surfaces_agree STRENGTHENS the old inline chain-vs-dag check (a non-NIE 'err' on either surface now fails; before it fell through). Added test_pandas_oracle_sanity canary (a global oracle break would otherwise silently skip the matrix). Collected cases: 231 -> 232 (+1 canary, zero subjects lost); ledger anchors (_predicate_queries etc.) untouched. dgx full lane + GPU: 1703 passed. Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… one chain-parity body (cleanup T3b+T4) hop's _node_set/_edge_set/_node_attrs_hop were verbatim/behavior-identical copies of chain's _nset/_eset/_node_attrs — all five graph-shape helpers (node_id_set/edge_pair_set/edge_pair_multiset/node_attr_map/named_flag_set) now live once in polars_test_utils (no try/except, no non-empty defaults — loud-failure contract documented), aliased at import so call sites read unchanged. The five near-identical chain parity bodies (CHAINS, both fuzzers, adversarial multihop, to_fixed_point, undirected-multiedge) collapse into _assert_chain_parity with per-table opt-in flags (multiplicity/attrs = the min_hops-bug dimensions; native probe; edge-count; aliases; nie_skip reason). Same checks per table as before — encoding only. dgx full lane + GPU: 1703 passed (same count). Part of the #1660 cleanup pass (plans/gfql-engine-followups/cleanup-1660). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…s (no crash) GPU-parity pass (viz-filter #1673 item 2) on dgx found `MATCH (n) WHERE n.name =~ '(?i)…'` CRASHES on engine='cudf' with libcudf "invalid regex pattern: nothing to repeat". Root cause: libcudf's regex engine rejects inline flag groups ((?i)/(?m)/ (?s)) at ANY position (verified: '(?i)abc', '^(?i)abc$', '(?i)^abc$' all raise; only flag-free '^abc$' works) — not merely a position issue. Fix: the Match/Fullmatch cuDF branches now translate a leading (?i) to the existing lowercase case-folding workaround (parity with pandas' (?i)), and honestly decline any other inline flag with NotImplementedError instead of crashing. Shared helper _cudf_regex_prep. pandas/polars paths untouched. Validated on dgx (RAPIDS 26.02): cudf =~ '(?i)a.c' == pandas [2,3] (was RuntimeError); 446 regex/match/fullmatch/contains/numeric tests pass across pandas/polars/cudf; +1 cudf-gated regression test (test_regex_cudf_inline_flag_parity). ruff+mypy clean. Also confirms viz-filter #1673 item 2: cuDF numeric (floor/ceil/round) + toLower/ toUpper parity OK; polars-gpu is N/A on this branch (#1675 is off #1660, below #1655 which introduces polars-gpu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…docstring, NB placement (cleanup review) Adversarial-review outcome on the cleanup diff (aa8c3e5..33908cd): behavior + coverage preserved, zero blockers. Nits fixed: the '%' dict comment no longer claims a parity verification no conformance case backs (negative- operand % case tracked as follow-up); endpoint_ids docstring no longer overstates plain-vs-subset unique as load-bearing on its one-column output (kept per-site for byte-identical diffs); the _polars_nie_query_cases NB comment moved from after the return into the docstring. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…ceil/ceiling/round/toLower/toUpper) The coverage ledger (cascaded from #1660) correctly flagged the new GFQL_SCALAR_FUNCTIONS entries as neither exercised nor waived. Real 4-engine parity-or-NIE cases beat waivers: each new function gets a cypher expression case through the standard matrix driver (chain + DAG surfaces). dgx: matrix + ledger 267 passed (cudf + polars-gpu lanes active). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e change (cleanup P8+T6) Owner-directed rewrite of the ~1,745 comment/docstring lines across the 9 engine modules + 9 polars test files: the NO-CHEATING/parity-or-NIE contract now states once per module docstring with short per-site 'decline (NIE): <reason>' forms; multi-line parity narratives compressed ~35-50% keeping every distinct fact (all fuzz-seed citations, pandas source line refs, perf numbers, dtype gates, divergence counterexamples; public-API docstrings keep every accepted value/default/env var/precedence rule). Ledger waiver dicts and case- table reason strings untouched (data, not prose). Mechanically gated: every file AST-identical to HEAD with docstrings stripped (prose_check), independently re-verified; ruff+mypy clean; dgx full lane + GPU parity 1703 passed (same count). -384 lines (prod -205, tests -179). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e hop implementation (cleanup A1) The single-bounded-hop lazy collect-once plan (formerly hop.py's 176-line twin, kept 'textually identical' by discipline) is now an early branch inside hop_polars, sharing the setup/validation/epilogue with the eager loop; hop.py collapses to the thin hop_lazy_or_eager entry (stable hook site). Eager loop math untouched. The branch's seed/match/target id-frames stay INSIDE the lazy plan — A/B benching (3 interleaved rounds, 1M+10M edges, dgx) proved that boundary is a perf contract: eager gate materialization cost +5-14% on chain workloads. Final round: within-noise-or-better on all 10 workloads (hop1 -7..9%, chain 2-edge -4%); full parity lane + GPU 1703 passed. Experiment history (2 gate-caught bugs incl. a return_as_wave_front wrong-answer) in plans/gfql-engine-followups/cleanup-1660/plan.md Step 9.A1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…s (no crash) GPU-parity pass (viz-filter #1673 item 2) on dgx found `MATCH (n) WHERE n.name =~ '(?i)…'` CRASHES on engine='cudf' with libcudf "invalid regex pattern: nothing to repeat". Root cause: libcudf's regex engine rejects inline flag groups ((?i)/(?m)/ (?s)) at ANY position (verified: '(?i)abc', '^(?i)abc$', '(?i)^abc$' all raise; only flag-free '^abc$' works) — not merely a position issue. Fix: the Match/Fullmatch cuDF branches now translate a leading (?i) to the existing lowercase case-folding workaround (parity with pandas' (?i)), and honestly decline any other inline flag with NotImplementedError instead of crashing. Shared helper _cudf_regex_prep. pandas/polars paths untouched. Validated on dgx (RAPIDS 26.02): cudf =~ '(?i)a.c' == pandas [2,3] (was RuntimeError); 446 regex/match/fullmatch/contains/numeric tests pass across pandas/polars/cudf; +1 cudf-gated regression test (test_regex_cudf_inline_flag_parity). ruff+mypy clean. Also confirms viz-filter #1673 item 2: cuDF numeric (floor/ceil/round) + toLower/ toUpper parity OK; polars-gpu is N/A on this branch (#1675 is off #1660, below #1655 which introduces polars-gpu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…ceil/ceiling/round/toLower/toUpper) The coverage ledger (cascaded from #1660) correctly flagged the new GFQL_SCALAR_FUNCTIONS entries as neither exercised nor waived. Real 4-engine parity-or-NIE cases beat waivers: each new function gets a cypher expression case through the standard matrix driver (chain + DAG surfaces). dgx: matrix + ledger 267 passed (cudf + polars-gpu lanes active). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_hop_setup_columns/_build_hop_pairs/_min_hops_labeled_node_output and the _idframe closures get full signatures via the TYPE_CHECKING-polars pattern (PolarsT for the eager/lazy-generic pairs builder). Typing the defs made mypy check the whole body, which surfaced a function-wide name collision: the single-shot lazy branch bound hop_edges as LazyFrame — renamed to hop_edges_lf (consistent with the branch's _lf convention). ruff+mypy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…hop-entry hook) The polars lane's per-file coverage audit gates hop.py; after #1660's hop unification the file is a thin entry dominated by this PR's index hook, which the lane didn't exercise (57% < 87% floor). Adding the engine-parametrized index tests to POLARS_TEST_FILES exercises the hook for real: dgx-measured 92.86% (>87 floor), 1724 lane tests pass. Real coverage, not floor surgery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…s (no crash) GPU-parity pass (viz-filter #1673 item 2) on dgx found `MATCH (n) WHERE n.name =~ '(?i)…'` CRASHES on engine='cudf' with libcudf "invalid regex pattern: nothing to repeat". Root cause: libcudf's regex engine rejects inline flag groups ((?i)/(?m)/ (?s)) at ANY position (verified: '(?i)abc', '^(?i)abc$', '(?i)^abc$' all raise; only flag-free '^abc$' works) — not merely a position issue. Fix: the Match/Fullmatch cuDF branches now translate a leading (?i) to the existing lowercase case-folding workaround (parity with pandas' (?i)), and honestly decline any other inline flag with NotImplementedError instead of crashing. Shared helper _cudf_regex_prep. pandas/polars paths untouched. Validated on dgx (RAPIDS 26.02): cudf =~ '(?i)a.c' == pandas [2,3] (was RuntimeError); 446 regex/match/fullmatch/contains/numeric tests pass across pandas/polars/cudf; +1 cudf-gated regression test (test_regex_cudf_inline_flag_parity). ruff+mypy clean. Also confirms viz-filter #1673 item 2: cuDF numeric (floor/ceil/round) + toLower/ toUpper parity OK; polars-gpu is N/A on this branch (#1675 is off #1660, below #1655 which introduces polars-gpu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…ceil/ceiling/round/toLower/toUpper) The coverage ledger (cascaded from #1660) correctly flagged the new GFQL_SCALAR_FUNCTIONS entries as neither exercised nor waived. Real 4-engine parity-or-NIE cases beat waivers: each new function gets a cypher expression case through the standard matrix driver (chain + DAG surfaces). dgx: matrix + ledger 267 passed (cudf + polars-gpu lanes active). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…s (no crash) GPU-parity pass (viz-filter #1673 item 2) on dgx found `MATCH (n) WHERE n.name =~ '(?i)…'` CRASHES on engine='cudf' with libcudf "invalid regex pattern: nothing to repeat". Root cause: libcudf's regex engine rejects inline flag groups ((?i)/(?m)/ (?s)) at ANY position (verified: '(?i)abc', '^(?i)abc$', '(?i)^abc$' all raise; only flag-free '^abc$' works) — not merely a position issue. Fix: the Match/Fullmatch cuDF branches now translate a leading (?i) to the existing lowercase case-folding workaround (parity with pandas' (?i)), and honestly decline any other inline flag with NotImplementedError instead of crashing. Shared helper _cudf_regex_prep. pandas/polars paths untouched. Validated on dgx (RAPIDS 26.02): cudf =~ '(?i)a.c' == pandas [2,3] (was RuntimeError); 446 regex/match/fullmatch/contains/numeric tests pass across pandas/polars/cudf; +1 cudf-gated regression test (test_regex_cudf_inline_flag_parity). ruff+mypy clean. Also confirms viz-filter #1673 item 2: cuDF numeric (floor/ceil/round) + toLower/ toUpper parity OK; polars-gpu is N/A on this branch (#1675 is off #1660, below #1655 which introduces polars-gpu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 4, 2026
…ceil/ceiling/round/toLower/toUpper) The coverage ledger (cascaded from #1660) correctly flagged the new GFQL_SCALAR_FUNCTIONS entries as neither exercised nor waived. Real 4-engine parity-or-NIE cases beat waivers: each new function gets a cypher expression case through the standard matrix driver (chain + DAG surfaces). dgx: matrix + ledger 267 passed (cudf + polars-gpu lanes active). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
…s (no crash) GPU-parity pass (viz-filter #1673 item 2) on dgx found `MATCH (n) WHERE n.name =~ '(?i)…'` CRASHES on engine='cudf' with libcudf "invalid regex pattern: nothing to repeat". Root cause: libcudf's regex engine rejects inline flag groups ((?i)/(?m)/ (?s)) at ANY position (verified: '(?i)abc', '^(?i)abc$', '(?i)^abc$' all raise; only flag-free '^abc$' works) — not merely a position issue. Fix: the Match/Fullmatch cuDF branches now translate a leading (?i) to the existing lowercase case-folding workaround (parity with pandas' (?i)), and honestly decline any other inline flag with NotImplementedError instead of crashing. Shared helper _cudf_regex_prep. pandas/polars paths untouched. Validated on dgx (RAPIDS 26.02): cudf =~ '(?i)a.c' == pandas [2,3] (was RuntimeError); 446 regex/match/fullmatch/contains/numeric tests pass across pandas/polars/cudf; +1 cudf-gated regression test (test_regex_cudf_inline_flag_parity). ruff+mypy clean. Also confirms viz-filter #1673 item 2: cuDF numeric (floor/ceil/round) + toLower/ toUpper parity OK; polars-gpu is N/A on this branch (#1675 is off #1660, below #1655 which introduces polars-gpu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 5, 2026
…ceil/ceiling/round/toLower/toUpper) The coverage ledger (cascaded from #1660) correctly flagged the new GFQL_SCALAR_FUNCTIONS entries as neither exercised nor waived. Real 4-engine parity-or-NIE cases beat waivers: each new function gets a cypher expression case through the standard matrix driver (chain + DAG surfaces). dgx: matrix + ledger 267 passed (cudf + polars-gpu lanes active). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 6, 2026
…hop-entry hook) The polars lane's per-file coverage audit gates hop.py; after #1660's hop unification the file is a thin entry dominated by this PR's index hook, which the lane didn't exercise (57% < 87% floor). Adding the engine-parametrized index tests to POLARS_TEST_FILES exercises the hook for real: dgx-measured 92.86% (>87 floor), 1724 lane tests pass. Real coverage, not floor surgery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lmeyerov
added a commit
that referenced
this pull request
Jul 6, 2026
…hop-entry hook) The polars lane's per-file coverage audit gates hop.py; after #1660's hop unification the file is a thin entry dominated by this PR's index hook, which the lane didn't exercise (57% < 87% floor). Adding the engine-parametrized index tests to POLARS_TEST_FILES exercises the hook for real: dgx-measured 92.86% (>87 floor), 1724 lane tests pass. Real coverage, not floor surgery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pull Bot
pushed a commit
to admariner/pygraphistry
that referenced
this pull request
Jul 9, 2026
…hop-entry hook) The polars lane's per-file coverage audit gates hop.py; after graphistry#1660's hop unification the file is a thin entry dominated by this PR's index hook, which the lane didn't exercise (57% < 87% floor). Adding the engine-parametrized index tests to POLARS_TEST_FILES exercises the hook for real: dgx-measured 92.86% (>87 floor), 1724 lane tests pass. Real coverage, not floor surgery. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Native CPU Polars execution engine for GFQL (
Engine.POLARS, opt-in viaengine='polars') — combines the former two-PR split (hop/chain traversals + cypher row pipeline) into one cohesive CPU-engine PR. The production pandas/cuDF paths are untouched;engine='auto'with Polars input still coerces to pandas as before.GPU target —
engine='polars-gpu'(folded #1655)The same lazy plan, executed on the RAPIDS cudf_polars backend via ONE
collect_allper hop (collect-once is what makes GPU pay off: per-op eager GPU collect was a measured regression). Explicit opt-in only (AUTO never selects it). GPU-or-error (raise_on_fail=True): a GPU-incapable plan node raises (pointing atengine='polars') — never silent-CPU mislabeled as GPU. Includes opt-in CPU streaming collect (GFQL_POLARS_CPU_STREAMING=1) for large batch traversals. Validated byengine='polars-gpu' == engine='polars'differential parity across the conformance corpus + traversals (2.84× single-hop GPU win @1m vs CPU).Followups — native completions (folded #1667)
The former stacked PR #1667 was folded in (GitHub shows it merged) — it completes the native surface rather than adding a separable feature:
min_hops>1forward/reverse traversals (no pandas bridge) — the subtle min_hops NODE-output rule proven against the pandas oracle (500/500 chain fuzz on both polars and polars-gpu).get_degrees/get_indegrees/get_outdegrees(parity vsComputeMixin), incl. via the CypherCALLsurface.toFloat,collect/collect(DISTINCT),WHERE … IN,size/substring,to_fixed_point, more predicates — each parity-validated or honest-NIE.count(*)short-circuit: a loneRETURN count(*)reads table height (or sums a boolean mask) in one reduction instead of materializing + group_by — engine-polymorphic across all four engines.Traversals —
hop()/chain()Native vectorized BFS via semi/anti joins (no per-row Python). Forward/reverse/undirected single-hop, directed multi-hop chains, node/edge filter dicts and predicates (lowered to
pl.Expr),edge_match/source_node_match/destination_node_match,target_wave_front, alias names. Deferred (honestNotImplementedError): variable-length/multi-hop edges, undirected edges in multi-edge chains, hop labels, nodequery=.Cypher row pipeline —
MATCH … RETURNNO CHEATING: every query runs natively on Polars or raises an honest
NotImplementedErrorpointing atengine='pandas'— never a silent pandas bridge. Native: frame ops (rows/limit/skip/distinct/drop_cols), select/with_/return_ projection (cypher-expr-AST →pl.Expr: property/arithmetic/comparison/boolean/literal +coalesce/abs),where_rows(OR/NOT WHERE, Kleene 3-valued), order_by, group_by (count/sum/avg/min/max), unwind (literal cross-join), property/expr result projection, int/string/bool entity-text (pl.concat_str). Honestly deferred → NIE: cross-entity same-path WHERE, multi-entity binding_ops, float/temporal/nested entity-text, exotic exprs.Conformance hardening
Driven by the cross-repo Cypher TCK differential (pandas-vs-polars). Every fix either matches pandas natively or declines honestly (NO-CHEATING — no silent bridge, no wrong answer):
0.0/0.0 > 1etc.: Polars treats NaN as the largest value; now masked to the IEEE/pandas answer (& ~is_nanfor ordering/=,| is_nanfor<>), driven by the lowered expression's output dtype (robustly covers int/int→float division + function results).n.val > 'a') — would Rust-panic; now NIE (recurses intoAllOf/Between, covers Categorical/Enum + arithmetic on all-null→String columns).time({...}) > time({...}),a.time + duration({...})) — were lexicographic/string-concat wrong answers; now NIE (narrowed to ordering of two temporal literals;=/<>stay native).ArrowInvalid.UNION DISTINCT— engine-awareEngine.df_unique(Polarsunique(maintain_order=True)) instead of the pandas-onlydrop_duplicatescrash.OPTIONAL MATCH— absent whole-entity rendersnull(not'()'); null-row-fill alignment shape declines honestly (NIE) instead of a misleading validation error.null AND null,NOT null) — AND/OR/NOT cast topl.Booleanso Kleene logic evaluates instead of raising.labelsList column —list.contains(membership) instead of a List→String cast crash; user List properties decline honestly.filter_by_dictpredicate now raises NIE (was silently.to_pandas()-bridging); native lowering widened (AllOf,IsNull/NotNull, case-insensitiveSTARTS/ENDS WITH).WITH-scalarMATCHre-entry — engine-aware (pl.row/with_columns) instead of a pandas.iloccrash.POLARS_ENGINES(POLARS + POLARS_GPU) is introduced here so the engine-aware helpers are self-contained at this layer.Validation
Differential parity vs the pandas engine (hop + chain suites + seeded fuzzer + a TCK-style cypher conformance lane with NULL/3-valued-logic + a
DEFERREDlist asserting deferred queries raise rather than bridge). Fullgraphistry/tests/compute/gfql/suite green (incl. the 1610-test cypher dir).Cross-repo Cypher TCK, polars arm: the differential pandas-vs-polars lane is clean — 0 wrong-answers across the full TCK (every scenario either matches pandas or honestly declines; 387 honest declines). This is the headline correctness guarantee: the Polars engine never silently disagrees with pandas.
Perf (interleaved, 1M nodes, each engine on its native-frame graph, all native)
Polars wins 5.6–38× across the surface:
RETURN n~38×,ORDER BY~17×, traversals 6–7.5×, projections/aggregations/DISTINCT5.6–6.9×. Plus eager fast paths (node-only / single-hop / unconstrained 1-hop) that move the polars>pandas crossover below ~100K for real viz/crossfilter shapes.Recent refinements (review-driven)
Post-fold polish — ergonomics, correctness, and typing:
cuDF→polars conversion is dtype-lossless: converts via Arrow (cuDF's native interchange) instead of cuDF→pandas→polars, which double-converted and was lossy (nullable
Int64→float64+NaN). Verified on dgx; also removes a device→host→device round trip forpolars-gpu.validate/warnconvention on engine conversion:Engine.df_to_engine(..., validate=, warn=)—strict(polars default) raises on an un-representable mixed-type column,autofixcoerces+warns (matching cuDF + theplot()/upload()boundary). BothEngine.pyfrom_pandas converters unified onto the protocol.Polars execution config is Python-settable + live:
set_cpu_streaming/set_gpu_executor(+ the publicGPU_EXECUTORSoptions) — previously env-only (GFQL_POLARS_*) and frozen at import; documented in the engine-selection guide (docs PR).Typing + dead-code sweep: removed the
getattr(g, "_nodes", None)no-op anti-pattern on declaredPlottableattrs (result_postprocess / reentry / row pipeline /gfql_unified/ call procedures); typed the polars-engine dtype/expr helpers (pl.DataType/pl.Expr/ExprNodeinstead ofAny); removed verified-redundantcast()s. (The systemicPlottable._nodes: Anyroot — ~517 casts repo-wide — is filed separately as Type Plottable._nodes/_edges (Any → Optional[DataFrameT]) + make DataFrameT a real polymorphic type #1678, out of scope here.)Housekeeping: degree helpers moved out of
chain.pyintodegrees.py;bin/test-polars.shde-duplicated (one test-file list, coverage via env toggle).Off-engine
call()analytics under Polars —call_mode='auto'(default) /'strict': acall()running a whole-graph analytic with no Polars kernel (umap,hypergraph,compute_cugraph/compute_igraph,*_layout,collapse, …) previously hard-raisedNotImplementedErrorunder a Polars engine — forcing users off Polars for the whole pipeline. It now runs as a mode-gated, warned modality switch:autobridges off-engine (polars→pandas,polars-gpu→cuDF on-device), runs the analytic, and coerces the result back to Polars losslessly via Arrow (warn once per analytic);strictkeeps the parity-or-NotImplementedErrordecline (benchmark integrity / memory ceiling).polars-gpuis GPU-or-error (declines rather than silently dropping to host). Traversal / filter / row ops stay parity-or-NIE — the split is mechanical (is_row_pipeline_call), and the chain and DAG surfaces bridge consistently. Python-settable (set_call_mode) + envGFQL_POLARS_CALL_MODE. dgx-verified:compute_cugraphPageRank byte-parity with the pandas oracle under bothpolarsandpolars-gpu; full polars CPU lane green. Documented in the engine guide (docs PR).More typing (review batch): typed
lazy/engine/polars/predicates.py(pl.Expr/pl.DataType+ a documented scalar union; audited everyis/getattr); aRowPipelineCtxProtocolreplacesctx: Anyinrow/frame_ops.py(checked interface, zero runtime); explicit__all__on thelazypublic surface.(Supersedes the former PR2 #1649, folded in here.)
🤖 Generated with Claude Code
Review notes
engine_polars/→lazy/engine/polars/) means the first commits touch pre-rename paths — review by diff, not commit-by-commit.fe8218c5): stale rename-era docstring paths fixed, internal plan/NO-CHEATINGcitations scrubbed from user-facing error strings ("no pandas fallback; parity-or-error by design"),Engine.pyerror strings truthful about polars support, inline engine tuples →POLARS_ENGINES.Plottable._nodestyping (Type Plottable._nodes/_edges (Any → Optional[DataFrameT]) + make DataFrameT a real polymorphic type #1678); unify the 3 mixed-type coercion impls under one protocol-aware helper.4db768a9..33908cde) — zero-behavior refactor, verified: consolidated the previously-tracked duplication (sharedendpoint_ids()/col_dtype()indtypes.py, one_project_polarsbody for select/with_columns,_endpoint_countsin degrees,_alias_true_maskin frame_ops, comparison-op whitelists replacing 6-way operator ladders, degree-dispatch collapse) and re-encoded the test suite without shrinking coverage (sharedpolars_test_utils.py; conformance-ledger axes and the matrix's native-ok/honest-NIE families are table-driven with per-case ids; one chain-parity body with per-table strictness flags; +1 pandas-oracle canary; the chain-vs-DAG surface check is now STRICTER — a non-NIE error on either surface fails). Evidence: dgx full lane + GPU parity 1703 passed at every batch (collected conformance cases 231→232, none lost); polarity/waiver mutation checks fail loudly; ruff+mypy clean. Diff 8,515→8,220 insertions. A follow-up owner-directed prose pass (commitf862657e) tightened the ~1,745 comment/docstring lines (NO-CHEATING contract once per module, per-site one-liners, every fact/citation/perf number kept) — mechanically gated as code-identical (AST-equal with docstrings stripped) + the same dgx lane (1703 passed). Final diff: 7,747 insertions (prod ~3,850 / tests 3,361 / benchmarks+infra 534). A further experiment-gated refactor (commitb956d115) unified the lazy single-hop intohop_polars— ONE hop implementation instead of the twinhop.py/hop_eager.pypair — proven by a 3-round interleaved A/B bench at 1M+10M edges (within-noise-or-better on all workloads; the lazy plan boundary is a measured perf contract) + full parity lane (1703) + the feat(gfql): physical adjacency indexes for O(degree) seeded traversal #1658 index-hook stack re-verified (1724+68). Follow-up typing pass (7acdd411) fully annotates the hop helpers (surfacing and fixing a latent lazy-vs-eager variable name collision). CHANGELOG: unchanged by design — the cleanup/unification is zero-behavior, and the existing feature entries (incl. the collect-once single-hop + eager multi-hop design they describe) remain accurate.