Skip to content

feat(ffi): add an FFI extension codec bundle - #24108

Draft
timsaucer wants to merge 18 commits into
apache:mainfrom
timsaucer:feat/ffi-codec-bundle
Draft

feat(ffi): add an FFI extension codec bundle#24108
timsaucer wants to merge 18 commits into
apache:mainfrom
timsaucer:feat/ffi-codec-bundle

Conversation

@timsaucer

@timsaucer timsaucer commented Aug 5, 2026

Copy link
Copy Markdown
Member

Note

This PR is stacked on top of #24028 (feat: Implement FFI_QueryPlanner) and
contains that PR's commits, because a cross-repository PR cannot use a fork
branch as its base. Only the final commit, feat(ffi): add an FFI extension codec bundle, is new here. Please review #24028 first; this PR will shrink to
a single commit once that one merges.

Which issue does this PR close?

Rationale for this change

Serializing plans across an FFI boundary needs three values that must agree with
one another: a task context provider, a logical extension codec, and a physical
extension codec. The wrappers took them separately, and the ones that carried
only a logical codec synthesized a DefaultPhysicalExtensionCodec whenever they
had to export a session.

The user-visible consequence: a foreign library that provides a query planner
returning a custom physical extension node cannot get that node back across the
boundary. A consumer that reaches Session::query_planner through an exported
session gets a planner whose physical codec is the default one, and the query
fails with PhysicalExtensionCodec is not provided. The synthesized codec also
borrowed the logical codec's task context provider, which is not guaranteed to
represent the exported session.

What changes are included in this PR?

Adds FFI_ExtensionCodecBundle, which carries the task context provider and both
codecs as one unit with private fields, so the constructors are the only way to
pair them. The bundle owns no private data of its own; each member already carries
its own lifecycle pointers and library marker. The dependency direction is bundle
→ codecs → task context provider — a bundle stored inside a codec would make
cloning recurse forever.

The bundle is propagated through every wrapper that exports a session or builds a
nested provider: FFI_TableProvider, FFI_TableProviderFactory,
FFI_TableFunction, FFI_CatalogProvider, FFI_CatalogProviderList,
FFI_SchemaProvider, FFI_SessionRef, and FFI_QueryPlanner. Each keeps one
constructor taking the bundle; the new_with_ffi_codec(s) variants and the
Option<Arc<dyn LogicalExtensionCodec>> argument whose None meant "use the
default" are gone, so choosing the defaults is now explicit
(FFI_ExtensionCodecBundle::new_default).

The two paths inside FFI_LogicalExtensionCodec that rebuild a table provider
receive only the codec, so they pair it with an explicit default physical codec
and document what that costs a consumer.

Session::create_physical_plan serialized its plan with no extension codec on
either side of the boundary; it now uses the bundle's logical codec, matching
optimize and create_physical_expr.

Are these changes tested?

Yes.

  • A three-library integration test where library A owns the session and a custom
    physical codec, installs library C's planner, and queries library B's provider,
    which reaches that planner through the session A handed it and returns a custom
    physical extension node. Restoring the old default-codec behaviour makes it fail
    with PhysicalExtensionCodec is not provided.
  • Unit coverage that nested catalog list → catalog → schema → table construction
    preserves the bundle, that cloning does not nest foreign codec wrappers, and
    that an expired task context provider reports a clear error rather than
    panicking.

Are there any user-facing changes?

Yes — this is a breaking change to the datafusion-ffi public API and to the
FFI_ struct layouts. FFI providers and consumers must both be rebuilt against
DataFusion 55. The ABI is already evolving in DF55.

Documented in docs/source/library-user-guide/upgrading/55.0.0.md with
before/after migration examples, and in datafusion/ffi/README.md. The api change label applies.

timsaucer and others added 18 commits July 31, 2026 08:31
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.
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.
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.
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.
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>
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.:
Serializing plans across an FFI boundary needs three values that must agree
with one another: a task context provider, a logical extension codec, and a
physical extension codec. The wrappers passed them separately, and the ones
that carried only a logical codec synthesized a
DefaultPhysicalExtensionCodec when they had to export a session. A query
planner reached through such a session could not round-trip custom physical
extension nodes; it failed later with "PhysicalExtensionCodec is not
provided". The synthesized codec also borrowed the logical codec's task
context provider, which is not guaranteed to represent the exported session.

Add FFI_ExtensionCodecBundle, which carries all three as one unit with
private fields so the constructors are the only way to pair them. The bundle
owns no private data of its own; each member already carries its own
lifecycle pointers and marker. The dependency direction is bundle to codecs
to task context provider — a bundle inside a codec would make cloning
recurse forever.

Propagate it through every wrapper that exports a session or builds a nested
provider: FFI_TableProvider, FFI_TableProviderFactory, FFI_TableFunction,
FFI_CatalogProvider, FFI_CatalogProviderList, FFI_SchemaProvider,
FFI_SessionRef, and FFI_QueryPlanner. Each keeps one constructor taking the
bundle; the new_with_ffi_codec(s) variants and the Option<Arc<dyn
LogicalExtensionCodec>> argument whose None meant "default" are gone, so
choosing the defaults is now explicit.

The two paths inside FFI_LogicalExtensionCodec that rebuild a table provider
receive only the codec, so they pair it with an explicit default physical
codec and document what that costs a consumer.

Session::create_physical_plan serialized its plan with no extension codec on
either side of the boundary; it now uses the bundle's logical codec, matching
optimize and create_physical_expr.

Tests: a three-library integration test where library A owns the session and
a custom physical codec, installs library C's planner, and queries library
B's provider, which reaches that planner through the session A handed it and
returns a custom physical extension node. Restoring the old default-codec
behaviour makes it fail with "PhysicalExtensionCodec is not provided". Plus
unit coverage that nested catalog/schema/table construction preserves the
bundle and that an expired task context provider reports a clear error.

BREAKING CHANGE: these FFI struct layouts changed. Rebuild FFI providers and
consumers against this release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@timsaucer timsaucer added the api change Changes the API exposed to users of the crate label Aug 5, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation datasource Changes to the datasource crate ffi Changes to the ffi crate labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 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 [  54.345s] (current)
     Parsing datafusion-datasource-parquet v54.1.0 (current)
      Parsed [   0.033s] (current)
    Building datafusion-datasource-parquet v54.1.0 (baseline)
       Built [  46.133s] (baseline)
     Parsing datafusion-datasource-parquet v54.1.0 (baseline)
      Parsed [   0.034s] (baseline)
    Checking datafusion-datasource-parquet v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.229s] 223 checks: 223 pass, 30 skip
     Summary no semver update required
    Finished [ 102.385s] datafusion-datasource-parquet
    Building datafusion-ffi v54.1.0 (current)
       Built [  61.580s] (current)
     Parsing datafusion-ffi v54.1.0 (current)
      Parsed [   0.067s] (current)
    Building datafusion-ffi v54.1.0 (baseline)
       Built [  60.746s] (baseline)
     Parsing datafusion-ffi v54.1.0 (baseline)
      Parsed [   0.066s] (baseline)
    Checking datafusion-ffi v54.1.0 -> v54.1.0 (no change; assume patch)
     Checked [   0.340s] 223 checks: 217 pass, 5 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:125
  field ForeignLibraryModule.create_extension_node_query_planner in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:133
  field ForeignLibraryModule.create_session_planning_table in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:138
  field FFI_CatalogProviderList.codecs in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/catalog_provider_list.rs:50
  field FFI_SchemaProvider.codecs in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/schema_provider.rs:66
  field FFI_CatalogProvider.codecs in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/catalog_provider.rs:59

--- failure inherent_method_missing: pub method removed or renamed ---

Description:
A publicly-visible method or associated fn 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/inherent_method_missing.ron

Failed in:
  FFI_CatalogProviderList::new_with_ffi_codec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/catalog_provider_list.rs:210
  FFI_TableFunction::new_with_ffi_codec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/udtf.rs:221
  FFI_SchemaProvider::new_with_ffi_codec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/schema_provider.rs:257
  FFI_TableProvider::new_with_ffi_codec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/table_provider.rs:405
  FFI_TableProviderFactory::new_with_ffi_codec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/table_provider_factory.rs:116
  FFI_CatalogProvider::new_with_ffi_codec, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/catalog_provider.rs:247

--- failure method_parameter_count_changed: pub method parameter count changed ---

Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
        ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
       impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.49.0/src/lints/method_parameter_count_changed.ron

Failed in:
  datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList::new takes 4 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/catalog_provider_list.rs:194, but now takes 3 parameters in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/catalog_provider_list.rs:187
  datafusion_ffi::udtf::FFI_TableFunction::new takes 4 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/udtf.rs:203, but now takes 3 parameters in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/udtf.rs:199
  datafusion_ffi::schema_provider::FFI_SchemaProvider::new takes 4 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/schema_provider.rs:240, but now takes 3 parameters in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/schema_provider.rs:235
  datafusion_ffi::table_provider::FFI_TableProvider::new takes 5 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/table_provider.rs:382, but now takes 4 parameters in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/table_provider.rs:386
  datafusion_ffi::table_provider_factory::FFI_TableProviderFactory::new takes 4 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/table_provider_factory.rs:99, but now takes 3 parameters in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/table_provider_factory.rs:101
  datafusion_ffi::catalog_provider::FFI_CatalogProvider::new takes 4 parameters in /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/catalog_provider.rs:230, but now takes 3 parameters in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/catalog_provider.rs:223

--- 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
  field logical_codec of struct FFI_CatalogProviderList, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/catalog_provider_list.rs:52
  field logical_codec of struct FFI_TableFunction, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/udtf.rs:66
  field logical_codec of struct FFI_SchemaProvider, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/schema_provider.rs:68
  field logical_codec of struct FFI_TableProvider, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/table_provider.rs:143
  field logical_codec of struct FFI_CatalogProvider, previously in file /home/runner/work/datafusion/datafusion/target/semver-checks/git-apache_main/d61bae2d1d21781ca2b1344d47398aba1cbb9276/datafusion/ffi/src/catalog_provider.rs:61

--- 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:54

--- 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 22, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:141
  ForeignLibraryModule.create_first_value_udaf moved from position 20 to 23, in /home/runner/work/datafusion/datafusion/datafusion/ffi/src/tests/mod.rs:144

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

@github-actions github-actions Bot added the auto detected api change Auto detected API change label Aug 5, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.00156% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.93%. Comparing base (3e3a92d) to head (815acd9).

Files with missing lines Patch % Lines
datafusion/ffi/src/proto/extension_codec_bundle.rs 92.72% 10 Missing and 2 partials ⚠️
datafusion/ffi/src/query_planner.rs 93.37% 0 Missing and 11 partials ⚠️
datafusion/ffi/src/session/mod.rs 94.44% 0 Missing and 8 partials ⚠️
datafusion/ffi/src/udtf.rs 72.72% 5 Missing and 1 partial ⚠️
datafusion/ffi/src/catalog_provider_list.rs 89.79% 1 Missing and 4 partials ⚠️
datafusion/ffi/src/schema_provider.rs 92.85% 0 Missing and 1 partial ⚠️
datafusion/ffi/src/table_provider.rs 97.72% 0 Missing and 1 partial ⚠️
datafusion/ffi/src/table_provider_factory.rs 92.85% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #24108      +/-   ##
==========================================
+ Coverage   80.91%   80.93%   +0.01%     
==========================================
  Files        1103     1105       +2     
  Lines      377219   377579     +360     
  Branches   377219   377579     +360     
==========================================
+ Hits       305244   305589     +345     
+ Misses      53775    53769       -6     
- Partials    18200    18221      +21     

☔ 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.

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.

Add an FFI extension codec bundle

2 participants