Skip to content

fix(gfql): pre-existing engine-parity/correctness bugs (ORDER BY DESC nulls, predicate from_json, str value-safety)#1732

Merged
lmeyerov merged 7 commits into
masterfrom
fix/gfql-engine-parity
Jul 19, 2026
Merged

fix(gfql): pre-existing engine-parity/correctness bugs (ORDER BY DESC nulls, predicate from_json, str value-safety)#1732
lmeyerov merged 7 commits into
masterfrom
fix/gfql-engine-parity

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Fixes three PRE-EXISTING master bugs surfaced during the GFQL OLAP-stack review + test-amplification (not introduced by #1729#1731 / #1705). Held as draft — no merge, no force-push. Validated across pandas/polars/cuDF on dgx-spark.

Fixes

  1. ORDER BY ... DESC NULL placement (HIGH, silent-wrong) — openCypher orders NULL as largest (DESC → nulls first), but the general row pipeline hardcoded nulls-last for every key, so DESC ... LIMIT k silently dropped the NULL group. Fixed pandas/cuDF (per-key null-indicator single-pass; cuDF non-stable-sort-safe) + polars (nulls_last list). Mutation-verified vs pristine master ([C,B][None,C]) on all 3 engines.
  2. Predicate from_json downgraded comparison→numericEQ/NE/GT/GE/LT/LE/Between deserialized as numeric-only, so a string/temporal = or temporal < > round-trip raised GFQLTypeError "val must be numeric". Now registers the predicates.comparison (numeric+temporal+string) versions.
  3. String predicates value-safetyContains/Startswith/Endswith/Match/Fullmatch raised AttributeError on numeric/temporal/bool columns; now openCypher-null (excluded), never stringifying (parity-safe).

Validation (dgx-spark, 26.02-gfql-polars)

  • New/updated regression tests: test_order_by_null_placement.py (12), test_from_json.py round-trips, test_str.py value-safety.
  • Regressions: lowering ORDER BY 139/0, polars conformance 136/0, predicate suite 139/0 (GPU), str suite 106/0.
  • All new tests mutation-verified.

🤖 Generated with Claude Code

lmeyerov and others added 7 commits July 18, 2026 20:04
F2 from the OLAP-stack review. The general row pipeline hardcoded nulls-last for
every ORDER BY key on all engines, so `ORDER BY <col> DESC` over a column with
NULLs was mis-ordered and `DESC ... LIMIT k` silently returned the wrong top-k
(dropping the NULL group, which openCypher ranks first since NULL is largest).

- pandas/cuDF (row/pipeline.py): per-key null-indicator single-pass sort
  (ind = col.isna(), sorted in the key's own direction) — stability-free so
  cuDF's non-stable quicksort is safe; scalar na_position cannot be per-key.
- polars (order_by_polars): per-key nulls_last list aligned with exprs.
- test_order_by_null_placement.py: hand-derived openCypher oracle, ASC+DESC x
  grouped+scalar, across pandas/polars/cuDF. Mutation-verified vs pristine master
  (wrong [C,B] -> correct [None,C]) on all three engines.

Pre-existing on master (not introduced by the OLAP split); the OLAP fast paths
already ordered nulls correctly, so this aligns the general/fallback path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
Bug #1 from the OLAP-stack review. The predicate from_json registry mapped
EQ/NE/GT/GE/LT/LE/Between/IsNA/NotNA to predicates.numeric (numeric-only), but
Cypher lowering and the public predicate API build predicates.comparison (which
also accept strings + temporals). Same __name__ meant a JSON round-trip
downgraded a live comparison predicate to numeric-only, raising
GFQLTypeError "val must be numeric" for a string/temporal equality or a temporal
comparison (breaks remote GFQL, serialization, caching).

Fix: from_json imports the comparison versions. Numeric-valued predicates are
unaffected (comparison is a superset). Round-trip regression tests added to
test_from_json.py (string/temporal/numeric eq + temporal gt), mutation-verified
(revert import -> 10 tests fail). Full predicate suite 139 passed/0 failed on GPU.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
Bug #2 from the OLAP-stack review. Contains/Startswith/Endswith/Match/Fullmatch
called `s.str.<op>` directly, raising an opaque AttributeError ("Can only use
.str accessor with string values!") on a numeric/temporal/bool column (pandas
and cuDF).

Fix: guard each predicate — a non-string-capable series yields an all-null
result (openCypher: string op over a non-string is null -> excluded), mirroring
the per-cell behavior on an object column holding non-strings. Never stringifies
(would diverge pandas<->cuDF, e.g. wrongly match `5 CONTAINS '5'`). String,
mixed-object, and all-null columns unchanged. Helpers _series_supports_str_ops /
_nonstring_null_result. Regression tests in test_str.py across dtypes; cuDF
parity confirmed (int -> all-null, not stringified). str suite 106 passed.

Scope: the 5 pattern predicates. The str-property _CallablePredicate family
(IsNumeric/IsAlpha/...) is deferred — its semantics on non-string dtypes are
genuinely ambiguous, tracked for a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
…n non-string column

Bugs #3 (polars connected-join clean-NIE) + #4 (pushdown polars-dtype) from the
OLAP-stack review. A string predicate (CONTAINS/STARTS WITH/ENDS WITH/regex) on a
Categorical/Enum/numeric column raised a clean GFQLSchemaError on pandas/cuDF but
leaked polars' opaque InvalidOperationError ("expected String type, got: cat").
filter_by_dict_polars now raises the same GFQLSchemaError (E302) so all three
engines agree; categorical is treated as non-string exactly as filter_by_dict.

Investigation (documented): the connected-join predicate-pushdown dtype gate is
parity-correct on polars for string/numeric/eq/bool columns (pushdown_probe:
PARITY=True on all) — no silent-wrong push found; the only divergence was this
opaque-vs-clean error, now fixed. Regression test
test_polars_string_predicate_nonstring.py; polars conformance 406 passed/0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
Bug #6 (R4) from the OLAP-stack review. pandas coerces CASE branches to a common
type, but cuDF's `.where` raises TypeError "cudf does not support mixed types",
surfaced as a hard GFQLTypeError. Bit `CASE WHEN path IS NULL THEN -1 ELSE
length(path) END` over an UNREACHABLE shortestPath (int -1 vs object/null hops
branch): pandas returned -1, cuDF errored.

Fix: the row-AST CASE evaluator (_gfql_eval_expr_ast) now catches the cuDF
mixed-type TypeError, unifies the branches via _gfql_coerce_case_branch_dtypes
(common numeric dtype, then string fallback), and retries -> cuDF returns the
same value pandas does. Reachable paths keep clean int parity (direct .where
succeeds, no coercion).

Validated on #1705 (shortestPath): reachable 0->2 = 2 both engines; unreachable
0->4 = -1 (pandas) / -1.0 (cuDF, value-equal, no raise). General regression test
test_case_mixed_dtype.py (all-null object column, same mechanism, no shortestPath)
mutation-verified: pristine master cuDF RAISES, fixed cuDF returns -1. CASE test
suite 58 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The new order-by-null and polars-string-predicate regression tests hardcoded
engine="polars", which ImportErrors on the CPU CI lanes (test-gfql-core /
test-core-python) that ship without polars. Guard polars like cudf
(_HAS_POLARS), so those lanes run pandas(+cudf) and the test-polars lane runs
polars. No behavior change where polars is installed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
The changed-line-coverage gate (combines core + gfql + polars lanes; NO cuDF
lane) flagged the cuDF-only branches and two polars lines.

- cuDF-only branches marked `# pragma: no cover` with rationale (structurally
  uncoverable by this gate — no cuDF lane; all validated on dgx + mutation-
  verified): str.py _nonstring_null_result cuDF branch, pipeline.py CASE
  mixed-dtype except + _gfql_coerce_case_branch_dtypes helper.
- order_by_polars: restructured the scalar-vs-list `descending` branch into a
  single coverable path (normalize to a per-key list); multi-key ORDER BY
  validated parity pandas/polars on dgx.
- Added the new polars regression tests (test_polars_string_predicate_nonstring,
  test_order_by_null_placement) to bin/test-polars.sh so the polars lane
  actually exercises (and covers) the GFQLSchemaError raise + null-order paths.

No production behavior change beyond the order_by_polars refactor (parity-
validated). 24 order-by/string-predicate tests pass incl. polars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y6dQEcjdazEnzuvuwf73ZL
@lmeyerov
lmeyerov marked this pull request as ready for review July 19, 2026 06:59
@lmeyerov
lmeyerov merged commit 95250b1 into master Jul 19, 2026
77 checks passed
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