Found while adversarially reviewing #1781. Pre-existing on master (84835988), engine-independent — wrong on pandas too, so it is not that PR's regression, but it is a silent wrong answer in ordinary two-query user code.
Repro
import graphistry, pandas as pd
N = pd.DataFrame({"id": list(range(7)), "kind": ["a","b"]*3 + ["a"]})
E = pd.DataFrame([(0,3),(1,2),(4,6),(3,5),(5,6),(0,6),(3,4)], columns=["s","d"])
g = graphistry.nodes(N, "id").edges(E, "s", "d")
g.gfql("MATCH (a {kind:'a'}) WITH a MATCH (a)-[*]->(b) RETURN count(*) AS c") # 5 (correct)
g.gfql("MATCH (a)-[*]->(b) RETURN count(*) AS c") # 5 <-- WRONG
The second query is unrelated to the first and should return 12. It returns the first query's answer instead.
Cause
WITH re-entry sets _gfql_start_nodes on the caller's Plottable and never clears it, so the next query on the same g is answered against the leaked seed. The native polars path does the same thing at graphistry/compute/gfql/lazy/engine/polars/chain.py:378-380.
Why it matters
- Violates the repo's pure-functional policy —
g is supposed to be immutable across queries, and users reasonably reuse one Plottable for many queries.
- Fails open: no error, just a wrong count. The two queries need not be related in any way.
- Engine-independent, so an engine A/B will not surface it.
Suggested fix
Carry the re-entry seed on the per-execution context rather than assigning it to the caller's object, or restore the previous value in a finally. Worth a regression test that runs two independent queries against the same Plottable and asserts the second is unaffected by the first.
🤖 Generated with Claude Code
https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
Found while adversarially reviewing #1781. Pre-existing on master (
84835988), engine-independent — wrong on pandas too, so it is not that PR's regression, but it is a silent wrong answer in ordinary two-query user code.Repro
The second query is unrelated to the first and should return 12. It returns the first query's answer instead.
Cause
WITHre-entry sets_gfql_start_nodeson the caller'sPlottableand never clears it, so the next query on the samegis answered against the leaked seed. The native polars path does the same thing atgraphistry/compute/gfql/lazy/engine/polars/chain.py:378-380.Why it matters
gis supposed to be immutable across queries, and users reasonably reuse onePlottablefor many queries.Suggested fix
Carry the re-entry seed on the per-execution context rather than assigning it to the caller's object, or restore the previous value in a
finally. Worth a regression test that runs two independent queries against the samePlottableand asserts the second is unaffected by the first.🤖 Generated with Claude Code
https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB