Skip to content

A join of tables from DataFrame.cache() fails to plan with einfold (Input field name e does not match with the projection expression mean) #93

Closed

Description

@alxmrs

🌀 Claude: With einfold installed, a join of two tables registered from DataFrame.cache() fails to plan. DataFusion alone answers it, and einfold answers it when the same rows are a table or a view.

DataFrame.cache() is datafusion-python's way to compute a DataFrame once and keep its result in memory. xarray-sql's GPT demo (benchmarks/ml/02_nanogpt.py, xqlsystems/xarray-sql#269) now uses it on DataFusion to compute each CTE once, because DataFusion plans a CTE again at every read. With that, the GPT's loss at nanoGPT's CPU size (1 layer, 128 channels, context 64, batch 12) takes 2.5 s instead of 108 s. But with einfold on, the first layer norm fails.

Repro

DataFusion 55.0.0 (datafusion-python), einfold built from 71b4674 (the wheel in ~/git/fla/target/wheels), on Linux x86-64:

"""einfold fails a join of two tables registered from DataFrame.cache().
usage: ein_cache_repro.py cache|table|view [einfold]"""
import sys
import pyarrow as pa
from datafusion import SessionContext
from einfold.datafusion import EinFold

how = sys.argv[1]
ctx = SessionContext()
if "einfold" in sys.argv:
    ctx.with_extensions(EinFold())
x = pa.table({"seq": [0, 0, 1, 1], "pos": [0, 0, 0, 0], "e": [0, 1, 0, 1], "val": [1.0, 2.0, 3.0, 5.0]})
ctx.register_record_batches("t", [x.to_batches()])
mean = "SELECT seq, pos, AVG(val) AS mean FROM x GROUP BY seq, pos"
if how == "cache":
    ctx.register_view("x", ctx.sql("SELECT * FROM t").cache())
    ctx.register_view("m", ctx.sql(mean).cache())
elif how == "table":
    ctx.register_record_batches("x", [x.to_batches()])
    ctx.register_record_batches("m", [ctx.sql(mean).to_arrow_table().to_batches()])
else:
    ctx.register_view("x", ctx.sql("SELECT * FROM t"))
    ctx.register_view("m", ctx.sql(mean))
q = """SELECT x.seq, x.pos, AVG(power(x.val - m.mean, 2)) AS var
FROM x JOIN m ON x.seq = m.seq AND x.pos = m.pos
GROUP BY x.seq, x.pos ORDER BY x.seq, x.pos"""
try:
    print(how, sys.argv[2:], ctx.sql(q).to_pydict())
except Exception as e:
    print(how, sys.argv[2:], "error:", str(e).splitlines()[0])
$ python ein_cache_repro.py cache
cache [] {'seq': [0, 1], 'pos': [0, 0], 'var': [0.25, 1.0]}
$ python ein_cache_repro.py cache einfold
cache ['einfold'] error: DataFusion error: FFI error: Internal error: Assertion failed: col.name() == matching_name: Input field name e does not match with the projection expression mean.
$ python ein_cache_repro.py table einfold
table ['einfold'] {'seq': [0, 1], 'pos': [0, 0], 'var': [0.25, 1.0]}
$ python ein_cache_repro.py view einfold
view ['einfold'] {'seq': [0, 1], 'pos': [0, 0], 'var': [0.25, 1.0]}

Expected

{'seq': [0, 1], 'pos': [0, 0], 'var': [0.25, 1.0]} with einfold too, as with the other two registrations.

The message names e, the column of x that the query doesn't read, and mean, the column of m. So a projection seems to be matched against the wrong input's schema, perhaps where a projection is pushed into a cached table's scan across the FFI.

Activity

  1. alxmrs commented on Oct 28, 2026

    @alxmrs
    MemberAuthor

    🪭 Claude (einfold)

    Thanks, fixed on #82 in 0b694c3 (rebased; it was 0d5e013). Your guess was close: it isn't a projection pushed into the wrong scan, but einfold copying the wrong table. Since #82, einfold copies each small in-memory table a plan reads once per plan, so that its scans don't cross the FFI, and it keyed those copies by table name. A cached DataFrame's table has no name: every scan of one is called ?table?. So x and m shared one copy, and m's scan read x's columns. With your repro, that failed to plan, because the columns differed. With two cached tables of the same columns, it gave wrong answers silently, which is worse. A scan of an unnamed table now gets a copy of its own.

    Your repro, on the stack's head with the fix:

    cache ['einfold'] {'seq': [0, 1], 'pos': [0, 0], 'var': [0.25, 1.0]}
    

    test_tells_unnamed_tables_apart joins two cached DataFrames, one with different columns and one with matching ones. Both cases fail without the fix: the first with a wrong answer here too, not the planning error. #82's description now says it fixes this issue.

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