Skip to content

feat: Implement FFI_QueryPlanner - #24028

Open
timsaucer wants to merge 18 commits into
apache:mainfrom
timsaucer:feat/ffi-query-planner-main
Open

feat: Implement FFI_QueryPlanner#24028
timsaucer wants to merge 18 commits into
apache:mainfrom
timsaucer:feat/ffi-query-planner-main

Conversation

@timsaucer

@timsaucer timsaucer commented Jul 31, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

Rationale for this change

This is the last in a series of PRs that would enable FFI Session to support a QueryPlanner. The prior work was in

With those changes in place we now have the dependencies correct that we can expose a FFI_QueryPlanner on a FFI_Session. With this we can enable foreign libraries such as datafusion-distributed and ballista to provide a query planner in Python and connect it directly to a datafusion-python's SessionContext.

What changes are included in this PR?

Addition only. Adds these functions to FFI_Session and their supporting structures:

  • query_planner()
  • optimize()
  • physical_optimizers()

Are these changes tested?

Unit and integration tests are provided.

Are there any user-facing changes?

This is addition, but it does break the FFI ABI, which is already evolving in DF55.

Add FFI_QueryPlanner and ForeignQueryPlanner with logical and physical plan codec support. Forward query planners, logical optimization, and physical optimizer rules through foreign sessions, with unit and cross-library coverage.\n\nAI Disclosure: This code was written in part by an AI agent.
@timsaucer timsaucer added api change Changes the API exposed to users of the crate ffi Changes to the ffi crate labels Jul 31, 2026

#[cfg(not(feature = "parquet_encryption"))]
#[expect(dead_code)]
#[expect(clippy::unused_async)]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by fix to when you run clippy and don't have the parquet_encryption feature enabled.

}

#[cfg(not(feature = "parquet_encryption"))]
#[expect(clippy::unused_async)]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by fix to when you run clippy and don't have the parquet_encryption feature enabled.

}

#[cfg(not(feature = "parquet_encryption"))]
#[expect(clippy::unused_async)]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by fix to when you run clippy and don't have the parquet_encryption feature enabled.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Thank you for opening this pull request!

Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch).

Details
     Cloning apache/main
    Building datafusion-datasource-parquet v54.1.0 (current)
       Built [  52.387s] (current)
     Parsing datafusion-datasource-parquet v54.1.0 (current)
      Parsed [   0.032s] (current)
    Building datafusion-datasource-parquet v54.1.0 (baseline)
       Built [  46.589s] (baseline)
     Parsing datafusion-datasource-parquet v54.1.0 (baseline)
      Parsed [   0.032s] (baseline)
    Checking datafusion-datasource-parquet v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.159s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 100.711s] datafusion-datasource-parquet
    Building datafusion-ffi v54.1.0 (current)
       Built [  61.918s] (current)
     Parsing datafusion-ffi v54.1.0 (current)
      Parsed [   0.064s] (current)
    Building datafusion-ffi v54.1.0 (baseline)
       Built [  62.245s] (baseline)
     Parsing datafusion-ffi v54.1.0 (baseline)
      Parsed [   0.062s] (baseline)
    Checking datafusion-ffi v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.237s] 223 checks: 219 pass, 3 fail, 1 warn, 30 skip

--- failure constructible_struct_adds_field: externally-constructible struct adds field ---

Description:
A pub struct constructible with a struct literal has a new pub field. Existing struct literals must be updated to include the new field.
        ref: https://doc.rust-lang.org/reference/expressions/struct-expr.html
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/constructible_struct_adds_field.ron

Failed in:
  field ForeignLibraryModule.create_query_planner in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:126

--- failure struct_pub_field_missing: pub struct's pub field removed or renamed ---

Description:
A publicly-visible struct has at least one public field that is no longer available under its prior name. It may have been renamed or removed entirely.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/struct_pub_field_missing.ron

Failed in:
  field task_ctx_provider of struct FFI_LogicalExtensionCodec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/proto/logical_extension_codec.rs:102

--- failure struct_pub_field_now_doc_hidden: pub struct field is now #[doc(hidden)] ---

Description:
A pub field of a pub struct is now marked #[doc(hidden)] and is no longer part of the public API.
        ref: https://doc.rust-lang.org/rustdoc/write-documentation/the-doc-attribute.html#hidden
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/struct_pub_field_now_doc_hidden.ron

Failed in:
  field FFI_LogicalExtensionCodec.task_ctx_provider in file /home/runner/work/datafusion/datafusion/datafusion/ffi/src/proto/logical_extension_codec.rs:53

--- warning repr_c_plain_struct_fields_reordered: struct fields reordered in repr(C) struct ---

Description:
A public repr(C) struct had its fields reordered. This can change the struct's memory layout, possibly breaking FFI use cases that depend on field position and order.
        ref: https://doc.rust-lang.org/reference/type-layout.html#reprc-structs
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/repr_c_plain_struct_fields_reordered.ron

Failed in:
  ForeignLibraryModule.version moved from position 19 to 20, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:132
  ForeignLibraryModule.create_first_value_udaf moved from position 20 to 21, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:135

     Summary semver requires new major version: 3 major and 0 minor checks failed
     Warning produced 1 major and 0 minor level warnings
    Finished [ 125.732s] datafusion-ffi

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Jul 31, 2026
Keep the standard FFI_SessionRef constructor focused on the required logical codec and derive a default physical codec. Add an explicit constructor for callers that already own matching logical and physical codecs.\n\nAI Disclosure: This code was written in part by an AI agent.
@codecov-commenter

codecov-commenter commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.54545% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.93%. Comparing base (3e3a92d) to head (9f970e3).

Files with missing lines Patch % Lines
datafusion/ffi/src/query_planner.rs 94.17% 0 Missing and 11 partials ⚠️
datafusion/ffi/src/session/mod.rs 94.73% 0 Missing and 7 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24028      +/-   ##
==========================================
+ Coverage   80.91%   80.93%   +0.01%     
==========================================
  Files        1103     1104       +1     
  Lines      377219   377529     +310     
  Branches   377219   377529     +310     
==========================================
+ Hits       305244   305555     +311     
+ Misses      53775    53757      -18     
- Partials    18200    18217      +17     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Document the query planner serialization boundary and exercise an A/B/C ownership model with independently loaded cdylib images. Reuse the existing FFI table provider and verify foreign plans are reconstructed as local nodes through A's codecs.

AI Disclosure: This code was written in part by an AI agent.
@github-actions github-actions Bot added the datasource Changes to the datasource crate label Jul 31, 2026
@timsaucer timsaucer changed the title Feat/ffi query planner main feat: Implement FFI_QueryPlanner Jul 31, 2026
Use one test query planner for both the basic round trip and the three-library table scan scenario. Remove the redundant library C constructor from the integration-test module.

AI Disclosure: This code was written in part by an AI agent.
Render the private FFI_SessionRef type as code so public query planner documentation passes rustdoc's private intra-doc link checks.

AI Disclosure: This code was written in part by an AI agent.
@timsaucer
timsaucer marked this pull request as ready for review July 31, 2026 14:53

@paleolimbot paleolimbot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try to circle back to take a closer look at the details, but I did take two passes and didn't spot anything out of place here. You've documented the motivation nicely here and conceptually I think this is a great approach that allows quite a lot of flexibility for multiple datafusion-based libraries to interact. The FFI pattern you've established from previous PRs works well here.

I believe this will allow our (SedonaDB) spatial join to be used in datafusion-python plans (injected via a combination of logical optimizer rules and the query planner). Exciting!

@milenkovicm milenkovicm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @timsaucer first of all appologies for late review and thank you for sticking with this. Can't wait for this release to finally get py ballista support.

I have fiew minor questions, more for my understanding than blockers


/// Library A's logical codec stores library B's provider while the logical
/// plan crosses into library C. A real application would encode enough
/// metadata to reconstruct or locate the provider instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for my reference, which metadata is needed? will need some help to integrate this with py ballista

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "metadata" was poor phrasing. Basically you need enough data to faithfully reproduce the plan on both sides of the (de)serialization. I'm updating the docstring to make it more clear.

&Self,
logical_plan_serialized: SVec<u8>,
session: FFI_SessionRef,
) -> FfiFuture<FFI_Result<SVec<u8>>>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question, why FFI_Result<FFI_ExecutionPlan> not used instad of serialized plan?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this is the case that is illustrated in the integration test test_three_library_query_planner_restores_type_identity.

Suppose we want one query planner to call another. Each of these planners tend to rely on downcasting the execution plans. By making the FFI query planner calls return the serialized plans, then it gets decoded within each library locally. By doing this we should get local versions of the well known execution plans that the default codec can provide.

timsaucer and others added 2 commits August 3, 2026 09:16
Library C typically captures library A's query planner, then A installs C's
planner on its session. C plans by delegating back to the captured handle.
This is the deployment that requires serialized plans in both directions: C
must downcast the nodes A produced in order to rewrite them, and A must
downcast the nodes C produced to run its own passes over the result.

Add an integration test for that topology. Replacing either serialization
step with an FFI_ExecutionPlan handoff makes it fail, which the prior tests
could not detect on the inbound leg.

The test also asserts that after the swap the session reports C's own planner,
documenting why C must delegate to the captured handle rather than call
Session::query_planner or Session::create_physical_plan, both of which are
self-references at that point.

Widen the test physical codec to accept an A-local node during encode.
FFI_ExecutionPlan::new unwraps a ForeignExecutionPlan back to its origin
handle, so when C serializes a node A gave it, A is asked to encode the very
plan its own try_decode produced.

Take the delegate planner as FFI_Option<FFI_QueryPlanner> on the existing
create_query_planner module entry instead of adding a second entry point.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread datafusion/ffi/src/session/mod.rs Outdated
Comment thread datafusion/ffi/src/query_planner.rs Outdated
Comment thread datafusion/ffi/src/query_planner.rs Outdated
physical_codec.unwrap_or_else(|| Arc::new(DefaultPhysicalExtensionCodec {}));
let physical_codec =
FFI_PhysicalExtensionCodec::new(physical_codec, runtime, task_ctx_provider);
Self::new_with_ffi_codecs(planner, logical_codec, physical_codec)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If None is passed to logical_codec or physical_codec, those arguments would default to DefaultLogicalExtensionCodec and DefaultPhysicalExtensionCodec, replacing any previously registered codecs even if users provide None as arguments.

🤔 there seems to be a couple of footguns with the FFI_QueryPlanner::new and FFI_QueryPlanner::new_with_ffi_codecs public APIs, maybe there's a cleaner constructor method we can expose publicly that makes these things irrepresentable?

timsaucer and others added 4 commits August 4, 2026 08:26
Drop redundant `Send`/`Sync` bounds and remove two codec footguns in the
FFI query planner API, per review on apache#24028.

- `Session` already requires `Send + Sync`, so `&(dyn Session + Send + Sync)`
  was noise. Narrowing to `&dyn Session` also widens what callers can pass.
- `LogicalExtensionCodec` and `PhysicalExtensionCodec` already require `Send`.
  The `+ Send` on the codec constructor parameters bought nothing and blocked
  callers holding an existing `Arc<dyn PhysicalExtensionCodec>`, since Rust
  will not coerce that to `Arc<dyn PhysicalExtensionCodec + Send>`.
- `FFI_QueryPlanner::new_with_ffi_codecs` silently dropped the supplied codecs
  when re-exporting an already-foreign planner. It now adopts them while
  keeping the original planner identity.
- `FFI_QueryPlanner::new` no longer takes `Option` codecs. Passing `None` used
  to install the default codecs, clobbering extension-node handling; requiring
  an explicit codec makes that unrepresentable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AI Disclosure: This code was written in part by an AI agent.:
AI Disclosure: This code was written in part by an AI agent.:
AI Disclosure: This code was written in part by an AI agent.:

AI Disclosure: This code was written in part by an AI agent.:
AI Disclosure: This code was written in part by an AI agent.:
AI Disclosure: This code was written in part by an AI agent.:
AI Disclosure: This code was written in part by an AI agent.:
AI Disclosure: This code was written in part by an AI agent.:
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api change Changes the API exposed to users of the crate auto detected api change Auto detected API change datasource Changes to the datasource crate documentation Improvements or additions to documentation ffi Changes to the ffi crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants