Skip to content

[branch-55] fix: preserve provided arguments during FFI object construction (#24723) - #24752

Merged
timsaucer merged 1 commit into
apache:branch-55from
timsaucer:fix/ffi-constructor-argument-drop-55
Aug 28, 2026
Merged

[branch-55] fix: preserve provided arguments during FFI object construction (#24723)#24752
timsaucer merged 1 commit into
apache:branch-55from
timsaucer:fix/ffi-constructor-argument-drop-55

Conversation

@timsaucer

Copy link
Copy Markdown
Member

This is a back port of #24723 onto branch-55 to support datafusion-python upgrade to 55.1.0. The details can be found in the linked PR. This is needed for apache/datafusion-python#1677

…he#24723)

## Which issue does this PR close?

- Closes apache#24722.

## Rationale for this change

Three `datafusion-ffi` constructors unwrap an already-foreign input and
return its original handle, dropping the arguments passed alongside
without an error or a warning:

- `FFI_LogicalExtensionCodec::new` — discards `task_ctx_provider`
- `FFI_PhysicalExtensionCodec::new` — discards `task_ctx_provider`
- `FFI_TableProvider::new_with_ffi_codec` — discards `logical_codec`

The consequence is that a consumer which imports a foreign codec can
never rebind it. Re-wrapping with a different provider compiles, runs,
and has no effect, so the handle keeps resolving against whatever
session it was first built with. In `datafusion-python` that shows up as
decode callbacks resolving names against a pre-fork session: a UDF
registered after the fork is invisible to them, and the config they see
is a stale snapshot.

There is a second failure mode with the same root cause. The provider is
held as a `Weak`, so a consumer that cannot rebind must keep the
original session alive artificially or the capsule starts failing with
`TaskContextProvider went out of scope over FFI boundary`.

The two sibling constructors that hit the same case already do the
opposite — `FFI_QueryPlanner::new_with_ffi_codecs` and
`FFI_SessionRef::new_with_ffi_codecs` both adopt the supplied codecs on
the unwrap path, and the former documents that guarantee explicitly.
This PR makes the other three consistent with them.

## What changes are included in this PR?

On the already-foreign path, each of the three constructors now clones
the original handle and overwrites the relevant `#[repr(C)]` field
before returning it, matching `FFI_QueryPlanner::new_with_ffi_codecs`:

```rust
if let Some(codec) = (Arc::clone(&codec) as Arc<dyn Any>)
    .downcast_ref::<ForeignLogicalExtensionCodec>()
{
    let mut codec = codec.0.clone();
    codec.task_ctx_provider = task_ctx_provider.into();
    return codec;
}
```

The `runtime` argument is a deliberate exception. Unlike the codecs and
the task context provider, `runtime` lives in `private_data`, which
belongs to the library that owns the handle — this side cannot write it
without an ABI change. `FFI_SessionRef::new_with_ffi_codecs` already
takes the same position ("retaining its original private data and
runtime"). Rather than leave that silent, all three constructors now
document it, alongside the new adopt-on-unwrap guarantee.

No public signatures change, and no behavior changes on the non-foreign
path.

## Are these changes tested?

Yes — five new unit tests, one per behavior, in each affected module's
own test module. All five fail on `main` and pass here.

- `ffi_logical_extension_codec_rebind_adopts_task_ctx_provider`
- `ffi_logical_extension_codec_rebind_releases_original_session` —
covers the dangling-`Weak` failure mode: session A is dropped after the
rebind, and the handle stays usable
- `ffi_physical_extension_codec_rebind_adopts_task_ctx_provider`
- `test_rebind_foreign_table_provider_adopts_logical_codec`
- `test_rebind_foreign_query_planner_adopts_codecs` — a control over the
already-correct sibling, so the two paths stay in agreement

Worth flagging for reviewers, since it is easy to write a test here that
silently proves nothing: `impl From<&FFI_LogicalExtensionCodec> for
Arc<dyn LogicalExtensionCodec>` compares `library_marker_id` first and
returns the original local `Arc` on a match, so within one library the
foreign branch is never reached. Each test overrides `library_marker_id`
with `crate::mock_foreign_marker_id` and asserts the import really did
produce a `Foreign*` wrapper before exercising the rebind.

`cargo test -p datafusion-ffi --all-features` passes (152 tests).

## Are there any user-facing changes?

Yes, a behavior change, though it replaces a silent no-op with the
documented intent.

Callers that pass a `task_ctx_provider` or `logical_codec` to these
constructors alongside an already-foreign input previously had that
argument ignored; it now takes effect. Anything relying on the old
handle being returned untouched would see the change — but since the old
path gave no way to observe or opt into that, it is hard to depend on
deliberately.

Downstream, this lets `datafusion-python` drop the workaround in
apache/datafusion-python#1677, which retains the
pre-fork `SessionContext` purely to keep the `Weak` valid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the ffi Changes to the ffi crate label Aug 28, 2026
@timsaucer
timsaucer requested a review from milenkovicm August 28, 2026 12:36

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

Thanks @timsaucer

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.09910% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.17%. Comparing base (20268a7) to head (ea75a91).

Files with missing lines Patch % Lines
datafusion/ffi/src/table_provider.rs 96.15% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           branch-55   #24752    +/-   ##
===========================================
  Coverage      81.17%   81.17%            
===========================================
  Files           1110     1110            
  Lines         386935   387043   +108     
  Branches      386935   387043   +108     
===========================================
+ Hits          314077   314173    +96     
- Misses         54366    54376    +10     
- Partials       18492    18494     +2     

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

@timsaucer
timsaucer merged commit 1a944f4 into apache:branch-55 Aug 28, 2026
34 checks passed
@timsaucer
timsaucer deleted the fix/ffi-constructor-argument-drop-55 branch August 28, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ffi Changes to the ffi crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants