Skip to content

GFQL polars: LazyFrame node input crashes undirected single-hop Cypher (TypeError, not parity/NIE) #1740

Description

@lmeyerov

Found by the #1737 seam re-review's input-type probes; pre-existing (the crash site is untouched by #1737's diff and predates it).

Repro

import polars as pl, graphistry
nodes = pl.DataFrame({"id": [1, 2], "label__Person": [True, True]}).lazy()  # LazyFrame nodes
edges = pl.DataFrame({"src": [1], "dst": [2], "type": ["KNOWS"]})
g = graphistry.nodes(nodes, "id").edges(edges, "src", "dst")
g.gfql("MATCH (a:Person)-[:KNOWS]-(b) RETURN b.id AS x", engine="polars")
# TypeError: expected `other` to be a 'LazyFrame', not 'DataFrame'

Matrix (undirected single-hop shape): lazy nodes + eager edges → crash; eager nodes + lazy edges → OK; lazy both → crash. Directed hops with LazyFrame inputs work fine.

Cause

_chain_traversal_polars's undirected single-hop node override (graphistry/compute/gfql/lazy/engine/polars/chain.py:~720):

_both = endpoint_ids(rev._edges, src, dst, node_col).unique(subset=[node_col])
rev = rev.nodes(g._nodes.join(_both, on=node_col, how="semi"), node_col)

g._nodes is the user's LazyFrame while _both is eager (or vice versa) — polars refuses mixed-laziness joins. Uncaught TypeError violates the parity-or-honest-NIE contract (should be a correct result or an NIE, never a crash).

Fix sketch

Normalize laziness at the join (_both.lazy() when g._nodes is a LazyFrame, or .lazy() both sides and keep the plan deferred). Add a lazy/eager input matrix test for the undirected shape.

Activity

  1. lmeyerov commented on Aug 18, 2026

    @lmeyerov
    ContributorAuthor

    Verified fixed on master e6625ed28 (post correctness-stack merge) — recommending close after review.

    Repro (issue's undirected single-hop with LazyFrame inputs), all four laziness combos:

    import polars as pl, graphistry
    nodes_e = pl.DataFrame({"id": [1,2], "label__Person": [True, True]})
    edges_e = pl.DataFrame({"src": [1], "dst": [2], "type": ["KNOWS"]})
    Q = "MATCH (a:Person)-[:KNOWS]-(b) RETURN b.id AS x ORDER BY x"
    for tag, nn, ee in [("lazy nodes + eager edges", nodes_e.lazy(), edges_e),
                        ("eager nodes + lazy edges", nodes_e, edges_e.lazy()),
                        ("lazy both", nodes_e.lazy(), edges_e.lazy()),
                        ("eager both", nodes_e, edges_e)]:
        g = graphistry.nodes(nn, "id").edges(ee, "src", "dst")
        f = g.gfql(Q, engine="polars")._nodes
        print(tag, sorted((f.collect() if isinstance(f, pl.LazyFrame) else f)["x"].to_list()))

    Output on e6625ed28 (polars 1.42.0) — hand-computed expectation: undirected KNOWS from Person → x=[1,2]:

    lazy nodes + eager edges: OK -> [1, 2]
    eager nodes + lazy edges: OK -> [1, 2]
    lazy both: OK -> [1, 2]
    eager both: OK -> [1, 2]
    

    No TypeError on any mixed-laziness combination; all four return the correct answer. (A polars PerformanceWarning about LazyFrame schema resolution fires from graphistry/compute/filter_by_dict.py:75 — cosmetic, not a correctness issue.)

  2. lmeyerov commented on Aug 18, 2026

    @lmeyerov
    ContributorAuthor

    Closing: verified fixed on master e6625ed28 by the 2026-08 stack — empirical repro + hand-computed oracle in the verification comment above. Residual sub-items, where any, are tracked in the successor issues named there (#1916, #1908, #1906, #1934-#1938).

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