Skip to content

gfql: rows(table=nodes, source=alias) multiplies rows for duplicate node ids and joins null ids to each other #2034

Description

@lmeyerov

Found while pinning the seeded node-lookup fast path (parity harness on the branch for the seeded fast paths, 2026-09-04).

  1. Duplicate node ids: with two node rows carrying id = 7, MATCH (p {id: 7}) RETURN p.age AS a on the full path returns 8 rows on pandas and cuDF (2 duplicate rows → 2^3 through the chain's forward/reverse/combine self-joins); polars returns 2. The expected answer is one row per matching node row (2).
  2. Null ids: with two rows whose id is null, MATCH (p {name: 'n3'}) RETURN p.id, p.age returns both null-id rows on the full path (NaN-keyed merge matches NaN to NaN) instead of the one row that matches the predicate. A seeded typed hop whose seed row has a null id raises GFQLTypeError on the full path (merge object vs float64) instead of returning no rows.

The seeded fast paths (_execute_seeded_node_lookup_fast_path, _execute_seeded_typed_hop_fast_path) return the intended answer for these inputs; graphistry/tests/compute/gfql/test_seeded_node_lookup_fastpath.py::test_node_lookup_returns_each_duplicate_id_row_once pins the fast answer on all three engines. The full path should agree; until it does, the differential parity tests exclude duplicate and null ids.

Where: graphistry/compute/chain.py _chain_impl combine step (self-join on the node id) and graphistry/compute/gfql/row/frame_ops.py rows(source=...); the merges should key on row position for node-frame lookups, and null ids must never link.

Activity

  1. lmeyerov commented on Sep 5, 2026

    @lmeyerov
    ContributorAuthor

    Disposition (0.60 landing phase): deferred behind the stacks. The seeded fast paths (#2035/#2037/#2038) return the intended one-row-per-node answer for duplicate and null ids and pin it; the full-path chain combine still self-joins on the id column. This is the same chain-combine seam as the cuDF 26.2 duplicate-id divergence in #2043, so both get fixed together right after the stacks land (positional node-frame lookups; null ids never link). Not release-blocking on its own: the shapes that reach the full path with duplicate ids are the ones the fast paths already decline, and the differential harness excludes duplicate/null ids until then.

  2. lmeyerov commented on Sep 5, 2026

    @lmeyerov
    ContributorAuthor

    Evidence update (round-003 differential sweep of the native op-list lanes, 2026-09-05): with a repeated node row, the native single-node lookup in #2037 answers one row per matching table row (as polars does on both routes) while the pandas/cuDF full path still self-joins to 8; #2037 now carries a two-sided pin (test_native_seed_resolution_2027.py::test_duplicate_node_rows_are_answered_once_each_on_the_native_lookup) so the full-path fix flips it. Disposition unchanged: fix the chain-combine seam together with #2043 right after the stacks land.

  3. lmeyerov commented on Sep 6, 2026

    @lmeyerov
    ContributorAuthor

    Status after the routes-off replay (#2054/#2061) and the dtype fixes (#2062): unchanged. With every hot path declined, MATCH (p:Person {id: 7}) RETURN p.score AS s on a node table carrying two id = 7 rows still returns 8 rows on pandas (the combine's node-id self-join multiplies the duplicates through forward/reverse/combine), while the seeded lanes return 2; the all-off ledger lists test_node_lookup_returns_each_duplicate_id_row_once as the one result divergence outside #2058/#2059.

    One contract question to settle before the fix, since two pins disagree by shape today: test_chain.py::test_fast_path_dedups_duplicate_node_ids_on_hop requires a 1-hop chain to COLLAPSE duplicate node-id rows (the closure step's drop_duplicates(subset=[node]), which #2062 keeps), whereas this issue's pin requires a node lookup to return EACH duplicate row once. Proposal: node-frame lookups (rows(table=nodes, source=alias) and the seeded lanes) key on row position and keep one output row per source row; hop/chain results keep the documented collapse; null ids never link on either. The fix then lives in combine_steps' node self-join (positional key) and frame_ops.rows(source=...), with the two pins stated side by side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions