Skip to content

fix(gfql): Cypher numeric-operator conformance (#1900) - #1902

Merged
lmeyerov merged 17 commits into
masterfrom
fix/gfql-1900-numeric-conformance
Aug 16, 2026
Merged

fix(gfql): Cypher numeric-operator conformance (#1900)#1902
lmeyerov merged 17 commits into
masterfrom
fix/gfql-1900-numeric-conformance

Conversation

@lmeyerov

Copy link
Copy Markdown
Contributor

Stacked on #1901. Fixes the #1900 umbrella:

1-3. One numeric-tower root: conformance moved into the runtime evaluators (both engines) — truncated modulo (-7 % 3 = -1), int/int division truncates toward zero on columns (matching the literal lane), integer div/mod by zero raises typed E203 (was inf silently on columns, mislabeled E303 on literals); float/0.0 keeps IEEE inf (Neo4j parity). Polars lowers int /,% natively only with provably nonzero literal divisors (measured: polars // 0 silently nulls) — non-literal divisors NIE to pandas' typed error.
4. bool-vs-number ordering → null semantics across three lanes, with STRICT dtype detection — the value-based bool-like heuristic would have silently nulled IC4's sum(...) > 0 (caught mid-cycle).
5. Simple CASE WHEN null conformed: null never matches, falls to ELSE (openCypher '='); both evaluators + comments.
6. Typing polish: 1 + null nulls on pandas; toInteger('x1') → null (scalar-scoped — TCK forced the narrowing); cross-property STARTS WITH typed E108 (was raw ValueError); typed errors pass through the relabel catch.

44 new pins (red-at-base evidenced). Existing-test changes all oracle-quoted: 5 CASE-WHEN-null pins conformed, 9 sites rewritten to CASE WHEN x IS NULL (preserves intent + values), 2 bool-coercion pins flipped to null-semantics with the BUG-4-family empty-aggregate gap noted. Gates: full-tree byte-identical failure set, cypher 3181P, polars battery 887P, TCK 4145P/0F, ruff+guards green (surface 9686).

🤖 Generated with Claude Code

https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi

lmeyerov and others added 5 commits August 14, 2026 07:27
1. Single-endpoint projections over relationship patterns kept openCypher
BAG semantics: _forces_relationship_multiplicity_projection_bindings routes
bare node_alias.prop projections onto binding rows (one row per match) in
the terminal projection chain, the general row projection, and the WITH
stage scope -- the per-alias node-set source silently deduplicated
(RETURN a.id gave [1,2,3] for the [1,1,2,3] bag). Scope: alias-free or
bare-node-prop expressions only; whole-row refs, edge-alias refs,
function-wrapped refs (keys(r)), and var-length arms keep the conservative
path. A terminal pure bare-alias WITH carry over non-OPTIONAL matches
(flatten_pure_carry_terminal_with_nonoptional) folds away so the carry
shape gets the same rows. Exposed and fixed two polars binding-row gaps:
int-vs-float endpoint dtype mismatch now aligns join keys losslessly
(SchemaError used to decline), and edges-only graphs materialize nodes.

2. Single-hop grouped-aggregate fast path: rename the lookup's node-id join
key BEFORE writing projected outputs, so `a.id AS id` survives as an output
(was a raw pandas KeyError) and an output NAMED id sourced from another
prop cannot corrupt the key.

3. Ungrouped aggregates over an empty UNWIND stream emit their identity row
(count -> 0, sum -> 0, collect -> [], min/max/avg -> null) via
empty_result_row on the row-only sequence; non-aggregate finals stay
zero-row. MATCH-sourced BUG-4 (sum-over-empty null) untouched, pin stays.

4. pandas negative list subscripts index from the end (l[-1] -> last;
out-of-range null either direction); polars keeps its honest UNWIND NIE.

Fold-in: the reentry null-fill concat pre-aligns all-NA fill columns to the
result dtypes (silences the pandas all-NA concat FutureWarning class, no
behavior change).

Pins: test_row_multiplicity_semantics.py (50 green + 2 strict-xfail
residual: whole-row endpoint projection multiplicity). Surface baseline
regenerated: lowering.py 9503 -> 9650 (forcing helper + flatten hook +
identity-row synthesis).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…ath (#1899 follow-up)

The #1899 binding-row forcing claimed the seeded typed-hop shape
(single [seed-node, single-hop edge, node] pattern projecting only
destination props), changing the compiled plan the #1755 fast path
pattern-matches -- the benchmark-critical seeded lever stopped engaging
(15 engagement/parity pins), the served fallback shifted dtypes (2), and
polars shapes that the plain chain executor served with HAS_<Label>
narrowing fell into binding_rows_polars' duplicate-id decline (2 NIEs
where answers existed). Fast-path precedence restored: the forcing
predicate leaves that exact shape on the rows(table, source) plan (its
seeded reduction is value-correct there -- unique seed, per-edge rows).

Oracle adjudication (engagement negative control): for edges
(0->1),(1->2),(2->0),(0->3),(0->4), `RETURN a.id` is the bag [0,0,0,1,2]
-- 5 rows. The control's `== 3` asserted the deduplicated node set, i.e.
exactly the #1899 bug; pinned to the ordered bag instead.

Lane: test_row_multiplicity_semantics.py registered in POLARS_TEST_FILES
(lane-completeness pin). Surface baseline regenerated: lowering.py
9648 -> 9674 (fast-path-precedence exclusion).

Full-tree gate: graphistry/tests/compute/gfql failure set is IDENTICAL to
the branch-base baseline at d044e3e (93 local-env failures, all
cudf/no-GPU or pre-existing numeric parity; 8105 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…->NaN)

The FutureWarning silencer cast all-NA fill columns to result dtypes; under
pandas-3 string dtypes that turns null-extension None into NaN (caught by
py3.13/3.14 CI lanes on the #1461 pins). The warning is cosmetic; the
representation is not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
One numeric-tower root for items 1-3: column arithmetic lowered to raw
pandas/polars ops without the Cypher discipline the literal lane's
toInteger wrap applies.

1. Modulo is TRUNCATED (Java/openCypher, sign of the dividend: -7 % 3 =
-1), literals and columns, int and float, both engines -- pandas via
int-quotient math / np.fmod, polars via abs-floordiv-sign (int) and
floor/ceil-trunc (float).
2. Column int/int division truncates toward zero (n.rank / 2 over int64
-> ints; -7 / 2 = -3); mixed/float operands keep true division.
3. Integer zero divisor ('/' and '%') is a typed error
(GFQLTypeError E203 'by zero', incl. the literal 1/0 that was an
'invalid-node-reference' mislabel); float / 0.0 keeps IEEE infinity
(Neo4j parity). polars serves int '/' and '%' natively only with a
provably nonzero literal divisor -- its `// 0` yields null, so other
divisors decline to the pandas lane's typed error.
4. Ordering a boolean against a number is incomparable -> null (rows
drop), via STRICT dtype detection in the shared comparison predicate,
the row evaluator, and the polars predicate/expr lanes -- the value-based
bool-like heuristic would have nulled int 0/1 columns (IC4 sum(...) > 0).
Boolean-vs-boolean ordering and equality stay served.
5. Simple CASE conformed to '=' semantics: `WHEN null` NEVER matches
(null = null is null) -- null subjects fall to ELSE, both engines.
6. Typing polish: 1 + null (and -,*,/,% with null operands) -> null on
pandas like polars; toInteger('x1') -> null for scalar strings (lists/
maps still error, TCK-pinned); cross-property STARTS WITH declines typed
instead of a raw same-path ValueError; typed GFQL errors pass through the
row evaluator's relabel catch.

Existing-test updates (each encodes the pre-#1900 bug, oracle-quoted in
place): 5 simple-CASE WHEN-null pins flip to null-never-matches; the
996/1052/1472 OPTIONAL-arm null-flag queries and IS7 twins
(test_optional_match_polars_frames, test_binder untouched, dgx smoke)
rewrite `CASE x WHEN null` to the conformant `CASE WHEN x IS NULL`
keeping their null-propagation intent and expected values; the
connected-join bool-widening pins' `p.flag > 0`/`>= 1` params flip to
0 matches (empty frame pinned as the lane's known empty-aggregate gap,
BUG-4 family).

Pins: test_numeric_conformance_semantics.py (44 green, pandas+polars,
registered in POLARS_TEST_FILES). Surface baseline regenerated:
lowering.py 9674 -> 9686 (cross-property predicate guard).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
@lmeyerov

Copy link
Copy Markdown
Contributor Author

GPU sweep receipt (dgx, @d8668fdf7): 8934 passed / 53 skipped / 24 xfailed / 0 failed (4m44s) — +44 vs prior tip, matching the new pin count exactly; numeric-evaluator changes clean on the cudf-shared pandas path and polars-gpu. Log retained (sweep1900.log).

lmeyerov and others added 2 commits August 15, 2026 11:28
Cascading base update after #1897 took its review remediation.

Resolutions:

- graphistry/compute/gfql/cypher/reentry/flatten.py: the base refactored
  flatten_terminal_with_over_optional into named helpers and DELETED
  _pure_carry_aliases_ignoring_where (verified: absent from
  ghhttps/fix/gfql-1896-om-with-pipeline:flatten.py, its only caller replaced
  by _is_terminal_with_over_optional_match / _stage_reshapes_rows /
  _bare_carry_aliases). Took the base's deletion plus its new
  _stage_has_aggregates, which the cleanly-merged
  _query_with_terminal_stage_folded_into_return already calls. Kept this
  branch's genuinely-new flatten_pure_carry_terminal_with_nonoptional (#1899),
  which reads _pure_carry_aliases, not the deleted helper. `git grep
  _pure_carry_aliases_ignoring_where` is now empty.

- bin/test-polars.sh: kept BOTH lane entries -- test_row_multiplicity_semantics.py
  (this branch) and test_hop_boundary_matrix.py (base) are different files, so
  picking a side would silently drop a polars lane.

- bin/ci_cypher_surface_guard_baseline.json: took the base's 9493, then
  --write-baseline. lowering.py is 9635 lines after the merge, which is the
  union of both sides' additions and BELOW this branch's own previous cap of
  9674; the ratchet is tightened, not loosened.

Gates: no conflict markers, ruff clean, type-hygiene guard clean, cypher
surface guard pass, mypy shows only the 4 known polars-skew errors.
test_row_multiplicity_semantics + test_fast_path_engagement +
tests/compute/gfql/cypher + test_optional_match_semantics = 3671 passed,
8 failed -- all 8 are [cudf] parametrizations that fail identically on the
base branch alone (no GPU in this environment).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Cascading base update after #1901 took its base.

Resolutions:

- bin/test-polars.sh: kept BOTH lane entries --
  test_numeric_conformance_semantics.py (this branch) and
  test_hop_boundary_matrix.py (base) are different files. Dropping either
  would leave a module-level polars-gated file running in NO lane, which
  graphistry/tests/compute/gfql/test_polars_lane_completeness.py fails on;
  that test passes here.

- bin/ci_cypher_surface_guard_baseline.json: took the base's 9635, then
  --write-baseline. lowering.py is 9647 lines merged, below this branch's own
  previous cap of 9686.

Gates: no conflict markers, ruff clean, type-hygiene guard clean, cypher
surface guard pass, mypy shows only the 4 known polars-skew errors.
test_lowering + test_numeric_conformance_semantics +
test_optional_match_polars_frames + test_polars_lane_completeness = 1620
passed, 7 failed -- all 7 are [cudf] parametrizations that fail identically
on the base branch alone (no GPU here).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
lmeyerov added a commit that referenced this pull request Aug 15, 2026
Cascading base update after #1902 took its base.

Resolutions:

- bin/test-polars.sh: kept BOTH lane entries -- test_path_trail_semantics.py
  (this branch) and test_hop_boundary_matrix.py (base) are different files;
  the diff3 merge base for that hunk is empty, so neither is a deletion.
  test_polars_lane_completeness.py passes.

- bin/ci_cypher_surface_guard_baseline.json: took the base's 9647, then
  --write-baseline. lowering.py is 9720 lines merged, below this branch's own
  previous cap of 9759.

Gates: no conflict markers, ruff clean, type-hygiene guard clean, cypher
surface guard pass, mypy shows only the 4 known polars-skew errors. All nine
test files this PR touches plus test_polars_lane_completeness = 2136 passed,
9 failed -- every failure is a [cudf]/on_cudf case (no GPU in this
environment), none in the merged surfaces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
):
import polars as pl
if dtype == pl.Boolean:
return pl.lit(False)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

presumably x-engine testing on this

so only a provably nonzero literal divisor may run natively -- anything
else declines to the pandas lane's typed error."""
from graphistry.compute.gfql.expr_parser import Literal, UnaryOp as _UnaryOp
if isinstance(node, _UnaryOp) and node.op in ("+", "-"):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aren't there other unary ops like ~ and ! ?

after handling, does that also mean we have a structural/design error here, like inexhaustive case handling due to static pattern matching?

return "int"
if isinstance(value, numbers.Real):
return "float"
dtype = getattr(value, "dtype", None)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we replace the dynamic typing with static patterns?

try:
return True, int(float(inner))
except ValueError:
return True, None

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we raise a rich exn / NIE ?


@staticmethod
def _series_is_boolean(s: SeriesT) -> bool:
dtype = getattr(s, "dtype", None)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replace with static typing patterns..

lmeyerov and others added 7 commits August 15, 2026 14:39
#1901 went CONFLICTING when #1897's merge landed, so GitHub could build no
merge ref and created no workflow runs at all.

One conflict, in ci_cypher_surface_guard_baseline.json: `lowering_py_max_lines`
was 9635 ours / 9464 theirs / 9493 at the merge base. Neither side's number
describes the merged tree, so it is set to the MEASURED count of the merged
lowering.py, 9635. The cypher-surface guard passes at that value.

gfql_fast_paths.py auto-merged. ruff clean; cypher suite 3517 passed with 7
failures, all `[cudf]` lanes already present in the recorded baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…eger overflow (#1902 review)

Review round on #1902 (five inline comments).

Unary surface: the grammar admits exactly {+, -, not} -- `~`/`!` are
unparseable. `UnaryOpName` types `UnaryOp.op` so dispatch is checked instead
of falling through an if/elif over a bare `str`; polars now lowers unary `+`
(pandas/cudf already did) and closes with assert_never, and the pandas
evaluator raises a typed E203 naming an unadmitted op.

`- -2` rendered as the unparseable `(--2)`, so a legal expression failed with
a misleading invalid-node-reference; `_sign_separator` keeps the signs apart.

Truncated integer division used np.sign, which needs the cupy JIT on cudf and
made every int `/` and `%` raise there; the `.where` form is engine-portable.

toInteger('inf') leaked OverflowError past the ValueError catch and surfaced
as "AST evaluator unsupported"; unrepresentable strings are null like
unparseable ones.

Typing: `_gfql_cypher_numeric_kind` takes `object` and returns
`Optional[CypherNumericKind]`; the bool-vs-number guards became named
predicates, dropping the Any/cast/hygiene-ok markers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Resolutions: gfql_unified takes master's row_guard_needs_single_column_entity_text
spelling (same predicate, no prose); test_optional_match_semantics keeps BOTH
new sections (F #1896 from the base stack, E #1891 from master).

Master's comment-density guard is new to this branch. The prose this PR added
is now carried by names and pins instead -- `_orders_boolean_column_against_number`,
`_orders_boolean_against_number`, `_orders_boolean_series_against_number`,
`_sign_separator` -- and the remaining findings inherited from the base stack
are stripped of their issue-number rationale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…gines

Carries the rule the deleted comment stated; polars' decline is asserted as a
typed NotImplementedError naming the row op, not skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
…gaps

Adds direct pins for the ROW-lane bool-vs-number guard (previously reached only
through flipped lowering pins) and for a simple CASE over an UNMATCHED optional
alias, whose value this PR changed but left unpinned.

De-vacuums the simple-CASE null pin (a real null cell, not an unknown column, so
polars evaluates instead of declining) and makes _served_or_nie assert the typed
row-op decline rather than swallowing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
test_polars_lane_completeness caught it: polars is installed only in
test-polars, which runs an explicit file list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
The base moved to f37833e mid-review, flipping the PR to CONFLICTING (and
therefore to zero CI runs). Criss-cross resolutions:

- chain.py: master's `_bound_edge_endpoints` line WITH the base's trimmed
  comment (the base cut that block's perf claim for the density guard).
- test_optional_match_semantics: section E arrived from both sides; kept once.
- surface baseline: the true post-merge lowering.py length.
- hop.py: the merge left it at 8 comment-block findings against a cap of 6;
  two blocks cut rather than raising the cap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
@lmeyerov

Copy link
Copy Markdown
Contributor Author

Round on the five inline comments. Worked in a fresh worktree at 89abb3c11; polars 1.42 and cudf 25.10 both really execute here, so the GPU lane below is measured, not skipped.

Commits: edfcacf74 (fixes), 2a8744fbe (merge master + comment-density guard), 740484aee + ce820daa0 (pins), be4156e99 (polars CI lane), dbf86d745 (merge the moved base). Head is now dbf86d745.


2. row_pipeline.py:328 — "other unary ops like ~ and !? … inexhaustive case handling?"

Yes, inexhaustive by construction — and it was hiding a real bug. Taking the two halves separately.

Which unary ops actually exist. The authoritative set is GFQL_ALLOWED_UNARY_OPS, and the grammar has exactly three productions (uplus, uminus, not_op):

op status evidence
~ unreachable — not in the language parse_expr("~x") raises GFQLExprParseError at col 1
! unreachable same
not handled in every dispatcher
- handled
+ polars correctly declined (NIE); pandas + cudf served asymmetry, no wrong answer

So ~/! were never the risk — pinned as unreachable in test_tilde_and_bang_are_not_in_the_language, so a future grammar change that admits them fails loudly instead of reaching a fallthrough.

The real defect was a fourth case neither of us listed: nested unary over a signed literal. The parser constant-folds the sign into the literal, so - -2 is UnaryOp('-', Literal(-2)), and render_expr_node concatenated it into the unparseable (--2). Legal Cypher n.neg / - -2 failed on every engine with a misleading [invalid-node-reference] parser validation failed. Category: crashes with a mis-typed error (not silently wrong). Honest provenance: this pre-dates the PR — it reproduces identically at base 228d16ae4. Fixed by _sign_separator, a helper named for the rule.

The structural answer. Every dispatcher was an if/elif over UnaryOp.op: str with a silent tail — return None (polars), return False, None (pandas), and a f"({node.op}{operand})" catch-all in expression_text. Now:

  • UnaryOpName = Literal["+", "-", "not"] types UnaryOp.op, and GFQL_ALLOWED_UNARY_OPS is derived from it via get_args so the two cannot drift;
  • polars closes with assert_never(node.op) — mypy-checked exhaustiveness;
  • the pandas evaluator (the terminal fallback, where a decline has nowhere left to go) raises a typed E203 naming the op instead of vanishing;
  • polars now lowers unary +, removing the engine asymmetry.

Carriers: test_grammar_admits_exactly_plus_minus_not, test_unknown_unary_op_raises_typed_error_naming_the_op, test_rendered_unary_reparses, test_nested_unary_minus_divisor_serves_truncated_division, test_unary_plus_is_identity.


1. predicates.py:117 — "presumably x-engine testing on this"

There was no cudf lane, and adding one found a live defect in this PR. ENGINES in test_numeric_conformance_semantics.py was ["pandas", "polars"] only.

_gfql_truncated_int_div used np.sign, which on a cudf Series dispatches to cupy and needs the nvrtc JIT. Where that isn't loadable the RuntimeError fell outside the typed-error allowlist and got relabelled to [invalid-node-reference] AST evaluator unsupported. Measured on this box:

n.neg / 2 (oracle -3, -4, 3, 4) base 228d16ae4 this PR before now
pandas -3.5, -4.0, … (silently wrong) correct correct
polars -3.5, -4.0, … (silently wrong) correct correct
cudf -3.5, -4.0, … (silently wrong) hard error correct

So the PR fixed the silent wrongness on two engines and turned it into a hard failure on the third. Caveat, stated plainly: this depends on the cupy install — a box with a complete nvrtc would likely have served it. The fix removes the cupy dependency entirely (.where instead of np.sign), so the lane no longer depends on that at all.

The predicates.py:117 behaviour itself is now pinned on all three engines (test_bool_column_ordered_against_number_never_matches), with the equality twin (test_bool_column_equality_against_bool_still_served) guarding against the guard over-reaching. The audit also showed the row-lane bool guard was reached only through flipped lowering pins, so test_boolean_ordered_against_number_in_the_row_lane_is_null now covers it directly.


4. row/pipeline.py:1657 — "shouldn't we raise a rich exn / NIE?"

Two answers.

The return True, None itself is already correct — openCypher mandates null, not an exception, for a string with no integer value, and the TCK pins it. Verified by test_tointeger_of_a_string_without_an_integer_value_is_null['x1'-None-*].

But the try above it leaked. int(float('inf')) raises OverflowError, which except ValueError did not catch, so it escaped to the relabel and surfaced as [invalid-node-reference] … AST evaluator unsupported — exactly the un-rich error you were pointing at. toInteger('inf'), '-inf' and '1e400' all hit it. Now caught alongside ValueError: unrepresentable is still "not an integer", so it is null like the unparseable case.


3 & 5 — static typing (row/pipeline.py:495, comparison.py:233)

No new cast(), no new hygiene-ok; both markers removed, not moved.

  • _gfql_cypher_numeric_kind(value: Any) -> Optional[str](value: object) -> Optional[CypherNumericKind]. object is genuinely static (it forces the narrowing the body already does) where Any disabled checking, and the stringly-typed "int"/"float" return became a Literal. Its # hygiene-ok: explicit-any is gone.
  • comparison.py: getattr(s, "dtype", None)s.dtype, and the blanket except Exception is gone — verified is_bool_dtype never raises across int/float/bool/object/string/boolean-ext/categorical/datetime/timedelta/empty and the cudf equivalents, under warnings.simplefilter("error"). The new cast(SeriesT, …) # hygiene-ok: explicit-cast this PR added is deleted, and mypy is clean without it or any type: ignore.

Both guards' bool-vs-number conditions became named predicates (_orders_boolean_column_against_number, _orders_boolean_against_number, _orders_boolean_series_against_number), which is what let the prose go.


(a) Test amplification — NOT QUIET

  • 79 new cells in test_unary_op_surface.py; 2 existing pins strengthened.
  • test_polars_lane_completeness caught that the new file was in no CI lane (polars is installed only in test-polars, which runs an explicit file list) — its polars cells would have been dead in CI. Registered in POLARS_TEST_FILES.
  • Anti-vacuity: 46 of 79 fail at merge-base 228d16ae4. The 33 that pass are deliberate guardrails (grammar op-set, ~/! unreachability, equality-still-served, empty-frame) and I am not claiming they prove anything about this PR.
  • Amplified around row multiplicity as asked: duplicate rows, single-row, empty frame, null cells, plus the unary/predicate surface.
  • Mutation audit of the EXISTING pins: no decorative pins. All 7 targeted mutations (truncated div → floor, zero-divisor guard → no-op, truncated % → floored, both bool-ordering guards deleted, CASE-null reverted, _nonzero_int_literalTrue) went red. Two extra mutations confirmed the pandas-side guards are uniquely covered.
  • What the audit did find, and I fixed rather than reported:
    • 3 polars cells were vacuous_served_or_nie swallowed NotImplementedError silently. It now asserts the typed row-op decline, so a lane that stops evaluating can no longer pass as agreement.
    • test_simple_case_when_null_never_matches used n.missing (an unknown column), so polars declined instead of evaluating. Repointed at a real null cell; it now genuinely serves on polars. It was also redundant with 5 pre-existing test_lowering.py cells — I strengthened rather than deleted it, since it now covers the executor round-trip those don't.
    • 2 coverage gaps closed: the row-lane bool guard (above), and CASE <alias> WHEN null over an unmatched OPTIONAL alias — this PR changed that value (base 'none' → head 'found', and 'found' is the openCypher-correct answer) but rewrote the test sites instead of pinning it. Now pinned.
  • Verdict NOT QUIET — one real pre-existing bug, one cudf portability defect introduced by this PR, one leaked OverflowError, 2 coverage gaps, 3 vacuous cells. Not convergence.

(b) Verbosity / encoding audit

Master's bin/ci_comment_density_guard.py post-dates this PR, so I merged ghhttps/master in and ran it. It failed on 11 file/check pairs.

Zero keeps-with-justification. No guard-ok markers added, no baseline cap raised. Every block this PR added was either deleted or replaced by a named helper — including one I had written myself this round (a 4-line toInteger rationale, now a single line with the rule carried by the pin's name). The four Any+hygiene-ok annotations and the one cast are gone.

Worth flagging honestly: 7 of those failures pre-exist on the base branch228d16ae4 fails the guard on its own (lowering.py perf-claim + issue-rationale, flatten.py, gfql_fast_paths.py). Those are #1899/#1896-stack comments, not this PR's. I stripped the issue-number rationale from them so CI is green here, but they properly belong to #1901.

All three guards now pass; ruff clean; mypy 4 errors at head vs 4 at base — zero new (both pre-existing, in hop_eager.py and degrees.py).


Not merged, not force-pushed, base unchanged, threads left open.


Full-tree gate. graphistry/tests/compute at head: 103 failures, of which 102 fail identically at the merge-base (re-run nodeid-for-nodeid). The one head-only failure was the polars-lane guard above, now fixed. After merging the moved base, the test_hop + gfql sweep is 93 failures with zero outside that pre-existing set.

Two things worth knowing about this box, neither caused by the PR: ~60 of those pre-existing failures are cudf index/ tests failing on a broken cupy libnvrtc — the same missing JIT behind the np.sign defect above — and a second agent was running the same suite concurrently.

The base branch moved to f37833e32 mid-review, which flipped the PR to CONFLICTING and therefore to zero CI runs. Merged in with -c merge.conflictstyle=diff3; now MERGEABLE with runs created.

lmeyerov and others added 3 commits August 15, 2026 15:48
`python-lint-types (3.8)` was the only red lane on #1902:

  row/pipeline.py:12: error: Incompatible import of "Literal" (imported name
  has type "typing_extensions._SpecialForm", local name has type
  "<typing special form>")  [assignment]

The file imported `Literal` from `typing` at line 8 AND from `typing_extensions`
at line 12, with the `CypherNumericKind` alias wedged between them. mypy on 3.8
rejects the shadowing import; newer interpreters do not, which is why only that
one lane was red.

`typing.Literal` exists on every interpreter this repo supports (3.8-3.14), so
the `typing_extensions` line is dropped rather than the `typing` one, and the
alias moves below the import block where it belongs instead of interrupting it.

Sibling check: `plugins_types/umap_types.py` also imports both, but aliases the
second (`Literal as Literal_ext`), so it is not affected.

mypy now reports only the 4 pre-existing polars-version-skew errors; ruff clean;
graphistry/tests/compute/gfql/row 32 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MF7uRZLKZaD6Q9FGWSmyXi
Merging #1897 with --delete-branch CLOSED #1901 (its base was #1897's head).
Recovered by recreating the ref at #1897's final commit, reopening, retargeting
to master, then deleting the temp ref — all review threads intact.

One textual conflict: bin/test-polars.sh, where both sides registered a
different test module in the polars lane. Kept BOTH
(test_optional_match_with_pipeline_boundaries.py from master,
test_row_multiplicity_semantics.py from this branch).

The merge also brought master's tightened comment-guard baselines, which
caught 14 findings this branch ADDED relative to master, across
row/pipeline.py, cypher/lowering.py, reentry/flatten.py and gfql_fast_paths.py
(measured per-file by diffing guard output against master's file content, not
by trusting the aggregate counts). All fixed by deletion or one-lining:
issue citations stripped with contracts kept, two narration blocks folded into
the names below them, and one perf claim ("the benchmark-critical 2.5ms lever")
deleted outright. No baseline cap raised; the guard now reports 25 files BELOW
baseline.

Gates: ruff clean, all three guards rc=0, row-multiplicity + lowering +
reentry suites 1709 passed with 7 [cudf] failures, all in the recorded
pre-existing libnvrtc baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
#1902 went CONFLICTING when #1901's recovery merge landed. All four conflict
hunks are COMMENT-ONLY: #1902's remediation and #1901's guard cleanup fixed
the same four blocks independently. Took the base's versions throughout —
including the deletion at the flatten call site, where the function name
`flatten_pure_carry_terminal_with_nonoptional` carries the contract.

Gates: ruff clean, all three guards rc=0; lowering + polars conformance suites
1810 passed, 15 failures all verified present in the recorded pre-existing
baseline (comm -13 empty).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjbKuKheqDu78oapRT5AYm
@lmeyerov
lmeyerov changed the base branch from fix/gfql-1899-row-multiplicity to master August 16, 2026 12:32
@lmeyerov
lmeyerov merged commit bb1fa0d into master Aug 16, 2026
75 checks passed
@lmeyerov
lmeyerov deleted the fix/gfql-1900-numeric-conformance branch August 16, 2026 12:34
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