Skip to content

GFQL: a DANGLING DESTINATION is matched by pandas/cuDF and dropped by polars — silent cross-engine divergence #1808

Description

@lmeyerov

Minimal repro

import pandas as pd, polars as pl, graphistry
nodes = pd.DataFrame({"id": [0, 1, 2]})
q = "MATCH (a)-[]->(b) RETURN count(*) AS c"

# edge (1 -> 9): destination 9 is NOT in the node table
edges = pd.DataFrame({"s": [0, 1], "d": [1, 9]})
case edges pandas cuDF polars
dangling source (9 -> 2) 2 1 1 1
dangling destination (1 -> 9) 2 2 2 1
both 3 2 2 1

A dangling source is dropped by all three engines. A dangling destination is dropped only by polars — pandas and cuDF match it and bind b to an endpoint that is not a node.

Which is right

MATCH (a)-[]->(b) binds b to a node. If the destination id has no row in the node table there is nothing to bind, so the pattern must not match. polars is correct; pandas and cuDF are wrong — and pandas is this repo's oracle, so the cross-engine parity suites are calibrated against the wrong answer for this shape.

It is also internally inconsistent: the same engine drops a dangling source and keeps a dangling destination, so it is an asymmetry in the endpoint gate rather than a deliberate "edges are authoritative" policy.

Scope

No error, no warning — just a different count. Found while auditing merged PRs #1782/#1783/#1784/#1790/#1792/#1793 with a pandas-vs-polars differential harness; it reproduces identically on 84be35fb (before that stack) and on 233b64c8, so it is pre-existing and not caused by any of them. It reproduces at every graph size tried and on every seed.

Why it has not been caught

The polars parity suites compare against the pandas oracle, so a shape where pandas is the wrong one shows up as "polars diverges" and is easy to read as a polars gap. Nothing in the corpus builds a graph with a dangling destination on purpose.

Suggested fix direction

Make the pandas/cuDF endpoint gate symmetric — require BOTH endpoints to resolve to node rows — then re-baseline any parity fixture that silently encoded the old count. Worth pinning with an explicit dangling-endpoint fixture on all four engines rather than relying on random fuzz to regenerate it.

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