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.
Minimal repro
(9 -> 2)(1 -> 9)A dangling source is dropped by all three engines. A dangling destination is dropped only by polars — pandas and cuDF match it and bind
bto an endpoint that is not a node.Which is right
MATCH (a)-[]->(b)bindsbto 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 on233b64c8, 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.