Repository navigation
gfql: the general chain path upcasts integer node/property columns to float where every fast lane keeps int (route-dependent dtypes) #2058
Description
Activity
Split into two seams.
Chain seam (fixed in #2062). The float came from
_chain_impl's endpoint-closure backfill: every endpoint id was concatenated onto the node frame as an id-only row and deduplicated afterwards, so the phantom rows widenedint64→float64/bool→objectbefore being dropped. Traced with a wrapper on everysafe_merge: all merges keptint64; only the closure concat changed the dtype. #2062 appends only endpoints missing from the node frame (dedup kept), and the existing endpoint-closure matrix now passes withGFQL_ROUTES_OFF=native-fast.Rows-pivot seam (open, needs a contract decision).
test_indexed_bindings.py::test_destination_property_projection_dtype_paritywith the hop index path off compares a lane answer (int64) against the generic answer that goes through the pandas rows pivot (float64/object). That upcast is not an accident today:gfql_fast_paths._pivot_parity_castsmakes the Cypher fast lanes reproduce the pivot's dtypes on pandas on purpose (parity with the canonical path), while cuDF keeps source dtypes. So the question is which dtype contract RETURN projections should have on pandas: the pivot's float64/object (current canonical, lanes match it) or the source dtypes (what the chain surface and the index kernel keep). Once decided, the fix is either in the rows pivot (keep source dtypes when no null is introduced, same rule as #2062) with_pivot_parity_castsretired, or akeep_source_dtypes=Trueparity pin on the kernel side. Leaving this half open on the issue.- added 20 commits that reference this issue
on Sep 6, 2026
Summary
Found by route-off test amplification (replaying the existing GFQL suites with each hot path forced to decline). Two independent existing pins fail only when the general path serves the shape:
test_endpoint_closure_matrix.py::test_chain_surface_keeps_node_attribute_dtypes[pandas](andtest_clean_graph_is_untouched_by_the_gate) getfloat64for an int node attribute on[n(), e_forward(), n()].test_indexed_bindings.pyparity pins (test_indexed_execution_is_pure,test_node_property_index_seeds_without_scanning,test_destination_property_projection_dtype_parity,test_node_property_index_duplicate_values_match_scan, …) show the scan path returningfloat64where the indexed path returnsint64for the same columns (b,r,b.public).chain._try_chain_fast_pathdocuments it: "the full path upcasts int→float via merge — the merge is the artifact, int is the Cypher-conformant type". So the dtype a user gets depends on which route admitted the shape; any variant a lane declines silently comes back as floats.Expected
One dtype contract on every route: the general path preserves integer dtypes the way the lanes do (or the lanes are made to match, which is the wrong direction per the docstring).
Pins
The existing tests above ARE the pins: run them with
GFQL_ROUTES_OFF=native-fast/index-hop(conftest switch from #2054) and they must pass.