Skip to content

Four gaps for a query planner in datafusion-python's process, through its FFI — draft for upstream #58

Open

Description

@alxmrs

🪭 Claude (einfold)

Spike S29 (report) runs einfold inside xarray-sql, an XQL Systems project for querying xarray Datasets with SQL. It installs einfold as a query planner through datafusion-python's FFI, with SessionContext.with_extensions. datafusion-python is Apache DataFusion's Python bindings, and its FFI is DataFusion's stable C interface between separately built libraries. Versions: datafusion-python 55.0.0, built with DataFusion and datafusion-ffi 55.2.0.

It works, but only through four workarounds in the planner's library. Each is a gap a planner living in the same process as the session will hit. Drafted here for the maintainer to decide what goes upstream, and where.

Reproduction. The spike's run.py, gradient.py and overhead.py. Remove the matching workaround from src/lib.rs to see each failure. These need the Python host, so einfold's Rust tests can't pin them.

1. A table can't reach the planner without a codec

datafusion-python hands the planner its logical plan as protobuf. A scan of an in-memory table, as register_record_batches and xarray-sql's from_dataset create, fails before the planner runs:

ValueError: Error serializing custom table at …/datafusion-proto-55.2.0/src/logical_plan/mod.rs:1534
caused by
This feature is not implemented: LogicalExtensionCodec is not provided

The spike's codec encodes any table as an index into a registry in this process, and decodes it back to the same table. Ask: a built-in by-reference codec, or a hook that hands the planner the plan as a handle, for planners in the host's process.

2. Built-in functions arrive as FFI wrappers

Decoded through the host's registry, sum is an FFI wrapper of the host's AggregateUDF, not datafusion::functions_aggregate::sum::Sum. Rules that match built-ins by type, as DataFusion's docs advise, stop matching. The spike relinks a foreign aggregate named sum to its own copy. That's unsound if a session registered another sum. Ask: decode DataFusion's built-in functions as the receiving library's own.

3. The physical plan returns as protobuf, tables included

The planner's physical plan returns to the host as protobuf. Its standard nodes come back as the host's own, and DataFusion's protobuf encodes an in-memory scan with its data. So a plan copies every table it reads on the way back: 800,000 rows, about 9 ms per query here. Nodes protobuf doesn't know need a physical codec. The spike returns each plan inside one node, with no visible children, passed by reference. Ask: an option for the result to come back as an FFI_ExecutionPlan handle.

4. A library's plans run without its Tokio runtime, and a spawn aborts Python

The planner's library holds its own copy of Tokio, and the host polls its plans from the host's threads. Any operator in the planner's library that spawns, as collect_partitioned, RepartitionExec and SpawnedTask do, then panics:

there is no reactor running, must be called from the context of a Tokio 1.x runtime
panic in a function that cannot unwind … aborting.

That happened even with a runtime handle passed to FFI_LogicalExtensionCodec::new and FFI_PhysicalExtensionCodec::new. The spike enters a runtime of its own whenever the host polls its plan, and einfold now runs without one (#57). Ask:

  • datafusion-ffi enters the runtime a library supplied whenever it executes or polls that library's plans;
  • at the least, it catches the panic instead of aborting.

Also measured

Installing the planner costs every query 1.4–3.4 ms, folds or not: SELECT 1 takes 1.65 ms rather than 0.24. Most of that is the logical plan's protobuf round trip and the session's calls across the FFI. That matters for xarray-sql's demos, which plan thousands of statements.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M96xSez6PZwTmfaGh2eJgS

Activity

  1. added
    upstreamA bug in a dependency, to be reviewed before reporting upstream
    on Oct 26, 2026
  2. alxmrs commented on Nov 1, 2026

    @alxmrs
    MemberAuthor

    🪭 Claude (einfold)

    New measurements for this draft, from spike S32 (docs/spikes/s32-bridge-cost), on TPC-H at SF1 through datafusion (Python) 55.0.0:

    • The round trip costs 1.3 ms per query, about 29 ms over TPC-H's 22, outside einfold's planner. That covers datafusion-python encoding the optimized logical plan as protobuf, datafusion-ffi building a ForeignSession, decoding the plan, and the physical plan's reference back. TPC-H's shortest queries take 20–30 ms, so this alone is 4–6% of them.
    • Converting a session's task context through the FFI takes 234 µs, because it converts every function the session has. The wrapper converts once for ForeignSession's function maps and once for task_ctx() to decode the plan. A lazy conversion, or a cached one per session, would remove most of this.
    • A fifth gap: the RuntimeEnv doesn't cross. ForeignSession and every converted TaskContext get RuntimeEnv::default(). So the session's object stores, such as one registered for S3, its memory pool and its caches are all missing on the planner's side. That makes queries over registered object stores fail with einfold installed (datafusion-ffi 55: a session's object stores don't cross the FFI, so with einfold a query over S3 fails — draft for upstream #149), and memory limits not apply. Keep a session's runtime environment from query to query in the Python bridge #152 keeps a RuntimeEnv per session on einfold's side, which brings back the caches but not the session's stores or limits.

    For upstream, I'd suggest datafusion-ffi passes the session's RuntimeEnv across, or at least its object store registry and memory pool, and converts function maps lazily.

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

    upstreamA bug in a dependency, to be reviewed before reporting upstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions