Skip to main content

datafusion_ffi/
query_planner.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! FFI support for [`QueryPlanner`].
19//!
20//! A typical deployment has three libraries. Library A (for example,
21//! `datafusion-python`) owns the [`Session`] and codec registry. Library B owns
22//! a custom table provider and its extension nodes. Library C (for example,
23//! Ballista or `datafusion-distributed`) owns the query planner. A serializes a
24//! logical plan and invokes C, while `FFI_SessionRef` lets C call session
25//! services in A. C deserializes the logical plan, creates a physical plan,
26//! serializes that result, and returns it for A to deserialize. The logical and
27//! physical extension codecs preserve nodes supplied by B.
28//!
29//! The physical result is serialized instead of returned as an
30//! [`crate::execution_plan::FFI_ExecutionPlan`]. An FFI execution-plan handle is
31//! a foreign trait-object proxy, so even a built-in plan created in C cannot be
32//! downcast to its concrete
33//! type in A. Serialization reconstructs known plan nodes with A's local Rust
34//! type identities, allowing A's optimizers and other consumers to downcast
35//! them. Extension codecs control how custom nodes are reconstructed.
36//!
37//! A node returned by B while C is planning is still foreign to C unless a
38//! codec boundary reconstructs it in C. The query-planner boundary guarantees
39//! that C-local serializable nodes, and extension nodes understood by the
40//! configured codecs, are reconstructed for A when the completed plan returns.
41//!
42//! # Delegating back to library A
43//!
44//! C commonly wants A's built-in planning as a starting point, then rewrites the
45//! result. A must export its planner *before* installing C's planner on the
46//! session, and C must retain that handle: after the swap,
47//! [`Session::query_planner`] reports C's own planner, and
48//! [`Session::create_physical_plan`] dispatches to it, so either one is a
49//! self-call. Delegating to the retained handle is safe, because DataFusion's
50//! built-in physical planner never re-dispatches through [`Session`].
51//!
52//! Retain the planner rather than the session. [`FFI_QueryPlanner`] owns a
53//! reference-counted planner, so it outlives A's original session, whereas
54//! `FFI_SessionRef` borrows its session with the lifetime erased.
55
56use std::ffi::c_void;
57use std::sync::Arc;
58
59use async_ffi::{FfiFuture, FutureExt};
60use async_trait::async_trait;
61use datafusion_common::error::{DataFusionError, Result};
62use datafusion_expr::LogicalPlan;
63use datafusion_physical_plan::ExecutionPlan;
64use datafusion_proto::bytes::{
65    logical_plan_from_bytes_with_extension_codec,
66    logical_plan_to_bytes_with_extension_codec,
67    physical_plan_from_bytes_with_extension_codec,
68    physical_plan_to_bytes_with_extension_codec,
69};
70use datafusion_proto::logical_plan::LogicalExtensionCodec;
71use datafusion_proto::physical_plan::PhysicalExtensionCodec;
72use datafusion_session::{QueryPlanner, Session};
73use stabby::vec::Vec as SVec;
74use tokio::runtime::Handle;
75
76use crate::execution::FFI_TaskContextProvider;
77use crate::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
78use crate::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
79use crate::session::{FFI_SessionRef, ForeignSession};
80use crate::util::FFI_Result;
81use crate::{df_result, sresult_return};
82
83/// An ABI-stable handle to a [`QueryPlanner`] owned by another library.
84///
85/// The Rust-facing adapters serialize the input [`LogicalPlan`] and resulting
86/// [`ExecutionPlan`]; callers do not invoke the byte-oriented function pointer
87/// directly.
88#[repr(C)]
89#[derive(Debug)]
90pub struct FFI_QueryPlanner {
91    create_physical_plan: unsafe extern "C" fn(
92        &Self,
93        logical_plan_serialized: SVec<u8>,
94        session: FFI_SessionRef,
95    ) -> FfiFuture<FFI_Result<SVec<u8>>>,
96
97    /// Codec used to encode and decode logical plans and extension nodes.
98    logical_codec: FFI_LogicalExtensionCodec,
99
100    /// Codec used to encode and decode physical plans and extension nodes.
101    physical_codec: FFI_PhysicalExtensionCodec,
102
103    /// Used to create a clone of the query planner.
104    clone: unsafe extern "C" fn(planner: &Self) -> Self,
105
106    /// Release the memory of the private data when it is no longer being used.
107    release: unsafe extern "C" fn(arg: &mut Self),
108
109    /// Return the major DataFusion version number of this planner.
110    pub version: unsafe extern "C" fn() -> u64,
111
112    /// Internal data. This is only to be accessed by the provider of the planner.
113    /// A [`ForeignQueryPlanner`] should never attempt to access this data.
114    private_data: *mut c_void,
115
116    /// Utility to identify when FFI objects are accessed locally through
117    /// the foreign interface. See [`crate::get_library_marker_id`].
118    pub library_marker_id: extern "C" fn() -> usize,
119}
120
121unsafe impl Send for FFI_QueryPlanner {}
122unsafe impl Sync for FFI_QueryPlanner {}
123
124struct QueryPlannerPrivateData {
125    planner: Arc<dyn QueryPlanner + Send + Sync>,
126}
127
128impl FFI_QueryPlanner {
129    fn inner(&self) -> &Arc<dyn QueryPlanner + Send + Sync> {
130        let private_data = self.private_data as *const QueryPlannerPrivateData;
131        unsafe { &(*private_data).planner }
132    }
133}
134
135unsafe extern "C" fn create_physical_plan_fn_wrapper(
136    planner: &FFI_QueryPlanner,
137    logical_plan_serialized: SVec<u8>,
138    session: FFI_SessionRef,
139) -> FfiFuture<FFI_Result<SVec<u8>>> {
140    let internal_planner = Arc::clone(planner.inner());
141    let logical_codec: Arc<dyn LogicalExtensionCodec> = (&planner.logical_codec).into();
142    let physical_codec: Arc<dyn PhysicalExtensionCodec> =
143        (&planner.physical_codec).into();
144
145    async move {
146        let mut foreign_session = None;
147        let session = sresult_return!(
148            session
149                .as_local()
150                .map(Ok::<&dyn Session, DataFusionError>)
151                .unwrap_or_else(|| {
152                    foreign_session = Some(ForeignSession::try_from(&session)?);
153                    Ok(foreign_session.as_ref().unwrap())
154                })
155        );
156
157        let logical_plan = sresult_return!(logical_plan_from_bytes_with_extension_codec(
158            logical_plan_serialized.as_slice(),
159            session.task_ctx().as_ref(),
160            logical_codec.as_ref(),
161        ));
162
163        let physical_plan = sresult_return!(
164            internal_planner
165                .create_physical_plan(&logical_plan, session)
166                .await
167        );
168        let physical_plan = sresult_return!(physical_plan_to_bytes_with_extension_codec(
169            physical_plan,
170            physical_codec.as_ref(),
171        ));
172
173        FFI_Result::Ok(SVec::from(physical_plan.as_ref()))
174    }
175    .into_ffi()
176}
177
178unsafe extern "C" fn release_fn_wrapper(planner: &mut FFI_QueryPlanner) {
179    unsafe {
180        debug_assert!(!planner.private_data.is_null());
181        let private_data =
182            Box::from_raw(planner.private_data as *mut QueryPlannerPrivateData);
183        drop(private_data);
184        planner.private_data = std::ptr::null_mut();
185    }
186}
187
188unsafe extern "C" fn clone_fn_wrapper(planner: &FFI_QueryPlanner) -> FFI_QueryPlanner {
189    let old_planner = Arc::clone(planner.inner());
190
191    let private_data = Box::into_raw(Box::new(QueryPlannerPrivateData {
192        planner: old_planner,
193    })) as *mut c_void;
194
195    FFI_QueryPlanner {
196        create_physical_plan: create_physical_plan_fn_wrapper,
197        logical_codec: planner.logical_codec.clone(),
198        physical_codec: planner.physical_codec.clone(),
199        clone: clone_fn_wrapper,
200        release: release_fn_wrapper,
201        version: super::version,
202        private_data,
203        library_marker_id: crate::get_library_marker_id,
204    }
205}
206
207impl Drop for FFI_QueryPlanner {
208    fn drop(&mut self) {
209        unsafe { (self.release)(self) }
210    }
211}
212
213impl Clone for FFI_QueryPlanner {
214    fn clone(&self) -> Self {
215        unsafe { (self.clone)(self) }
216    }
217}
218
219impl FFI_QueryPlanner {
220    /// Creates an [`FFI_QueryPlanner`] with native extension codecs.
221    ///
222    /// Both codecs are required so that the caller states which extension nodes
223    /// survive the boundary. Pass
224    /// [`DefaultLogicalExtensionCodec`](datafusion_proto::logical_plan::DefaultLogicalExtensionCodec)
225    /// and
226    /// [`DefaultPhysicalExtensionCodec`](datafusion_proto::physical_plan::DefaultPhysicalExtensionCodec)
227    /// when no custom nodes are involved. `runtime` and `task_ctx_provider`
228    /// support codec callbacks across the FFI boundary.
229    pub fn new(
230        planner: Arc<dyn QueryPlanner + Send + Sync>,
231        runtime: Option<Handle>,
232        task_ctx_provider: impl Into<FFI_TaskContextProvider>,
233        logical_codec: Arc<dyn LogicalExtensionCodec>,
234        physical_codec: Arc<dyn PhysicalExtensionCodec>,
235    ) -> Self {
236        let task_ctx_provider = task_ctx_provider.into();
237        let logical_codec = FFI_LogicalExtensionCodec::new(
238            logical_codec,
239            runtime.clone(),
240            task_ctx_provider.clone(),
241        );
242        let physical_codec =
243            FFI_PhysicalExtensionCodec::new(physical_codec, runtime, task_ctx_provider);
244        Self::new_with_ffi_codecs(planner, logical_codec, physical_codec)
245    }
246
247    /// Creates an [`FFI_QueryPlanner`] using prebuilt FFI extension codecs.
248    ///
249    /// If `planner` is already foreign, this re-exports its original FFI handle
250    /// rather than adding another wrapper layer. The handle still adopts the
251    /// codecs supplied here, so they are never silently discarded.
252    pub fn new_with_ffi_codecs(
253        planner: Arc<dyn QueryPlanner + Send + Sync>,
254        logical_codec: FFI_LogicalExtensionCodec,
255        physical_codec: FFI_PhysicalExtensionCodec,
256    ) -> Self {
257        let any_ref: &dyn std::any::Any = planner.as_ref();
258        if let Some(planner) = any_ref.downcast_ref::<ForeignQueryPlanner>() {
259            let mut planner = planner.0.clone();
260            planner.logical_codec = logical_codec;
261            planner.physical_codec = physical_codec;
262            return planner;
263        }
264
265        let private_data = Box::new(QueryPlannerPrivateData { planner });
266
267        Self {
268            create_physical_plan: create_physical_plan_fn_wrapper,
269            logical_codec,
270            physical_codec,
271            clone: clone_fn_wrapper,
272            release: release_fn_wrapper,
273            version: super::version,
274            private_data: Box::into_raw(private_data) as *mut c_void,
275            library_marker_id: crate::get_library_marker_id,
276        }
277    }
278
279    /// Creates a physical plan through this planner's FFI interface.
280    ///
281    /// This serializes `logical_plan`, exports `session` as an
282    /// `FFI_SessionRef`, invokes the planner's owning library, and
283    /// deserializes its physical-plan response. `session_runtime` is attached
284    /// to the exported session for callbacks that need its Tokio runtime.
285    ///
286    /// The [`QueryPlanner`] implementation for [`ForeignQueryPlanner`] cannot
287    /// obtain the session owner's runtime from the trait API, so it calls this
288    /// method with `None`. Embedders that own the runtime and need session
289    /// callbacks to enter it must call this method directly with `Some(handle)`.
290    pub async fn create_physical_plan_with_session_runtime(
291        &self,
292        logical_plan: &LogicalPlan,
293        session: &dyn Session,
294        session_runtime: Option<Handle>,
295    ) -> Result<Arc<dyn ExecutionPlan>> {
296        let codec: Arc<dyn LogicalExtensionCodec> = (&self.logical_codec).into();
297        let logical_plan =
298            logical_plan_to_bytes_with_extension_codec(logical_plan, codec.as_ref())?;
299        let logical_plan = SVec::from(logical_plan.as_ref());
300        let task_ctx = session.task_ctx();
301        let session = FFI_SessionRef::new_with_ffi_codecs(
302            session,
303            session_runtime,
304            self.logical_codec.clone(),
305            self.physical_codec.clone(),
306        );
307
308        let physical_plan = unsafe {
309            df_result!((self.create_physical_plan)(self, logical_plan, session).await)?
310        };
311        let physical_codec: Arc<dyn PhysicalExtensionCodec> =
312            (&self.physical_codec).into();
313
314        physical_plan_from_bytes_with_extension_codec(
315            physical_plan.as_slice(),
316            task_ctx.as_ref(),
317            physical_codec.as_ref(),
318        )
319    }
320}
321
322/// Consumer-side [`QueryPlanner`] adapter for an [`FFI_QueryPlanner`].
323///
324/// Calls serialize the logical plan, invoke the producing library, and
325/// deserialize its physical-plan response.
326#[derive(Debug)]
327pub struct ForeignQueryPlanner(pub FFI_QueryPlanner);
328
329unsafe impl Send for ForeignQueryPlanner {}
330unsafe impl Sync for ForeignQueryPlanner {}
331
332impl From<&FFI_QueryPlanner> for Arc<dyn QueryPlanner + Send + Sync> {
333    fn from(planner: &FFI_QueryPlanner) -> Self {
334        if (planner.library_marker_id)() == crate::get_library_marker_id() {
335            Arc::clone(planner.inner())
336        } else {
337            Arc::new(ForeignQueryPlanner(planner.clone()))
338        }
339    }
340}
341
342#[async_trait]
343impl QueryPlanner for ForeignQueryPlanner {
344    async fn create_physical_plan(
345        &self,
346        logical_plan: &LogicalPlan,
347        session: &dyn Session,
348    ) -> Result<Arc<dyn ExecutionPlan>> {
349        self.0
350            .create_physical_plan_with_session_runtime(logical_plan, session, None)
351            .await
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use std::sync::Arc;
358
359    use arrow::datatypes::{DataType, Field, Schema};
360    use datafusion::prelude::SessionContext;
361    use datafusion_common::Result;
362    use datafusion_execution::TaskContextProvider;
363    use datafusion_expr::LogicalPlanBuilder;
364    use datafusion_physical_plan::empty::EmptyExec;
365    use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec;
366    use datafusion_proto::physical_plan::DefaultPhysicalExtensionCodec;
367
368    use super::*;
369
370    #[derive(Debug)]
371    struct EmptyQueryPlanner;
372
373    #[async_trait]
374    impl QueryPlanner for EmptyQueryPlanner {
375        async fn create_physical_plan(
376            &self,
377            _logical_plan: &LogicalPlan,
378            _session: &dyn Session,
379        ) -> Result<Arc<dyn ExecutionPlan>> {
380            let schema =
381                Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
382            Ok(Arc::new(EmptyExec::new(schema)))
383        }
384    }
385
386    fn create_ffi_query_planner(ctx: Arc<SessionContext>) -> FFI_QueryPlanner {
387        let task_ctx_provider = Arc::clone(&ctx) as Arc<dyn TaskContextProvider>;
388        FFI_QueryPlanner::new(
389            Arc::new(EmptyQueryPlanner),
390            None,
391            &task_ctx_provider,
392            Arc::new(DefaultLogicalExtensionCodec {}),
393            Arc::new(DefaultPhysicalExtensionCodec {}),
394        )
395    }
396
397    #[test]
398    fn test_ffi_query_planner_local_bypass() {
399        let ctx = Arc::new(SessionContext::new());
400        let ffi_planner = create_ffi_query_planner(ctx);
401        let planner: Arc<dyn QueryPlanner + Send + Sync> = (&ffi_planner).into();
402        let any_ref: &dyn std::any::Any = planner.as_ref();
403        assert!(any_ref.downcast_ref::<EmptyQueryPlanner>().is_some());
404    }
405
406    #[tokio::test]
407    async fn test_round_trip_ffi_query_planner_create_physical_plan() -> Result<()> {
408        let ctx = Arc::new(SessionContext::new());
409        let mut ffi_planner = create_ffi_query_planner(Arc::clone(&ctx));
410        ffi_planner.library_marker_id = crate::mock_foreign_marker_id;
411
412        let planner: Arc<dyn QueryPlanner + Send + Sync> = (&ffi_planner).into();
413        let any_ref: &dyn std::any::Any = planner.as_ref();
414        assert!(any_ref.downcast_ref::<ForeignQueryPlanner>().is_some());
415
416        let logical_plan = LogicalPlanBuilder::empty(false).build()?;
417        let state = ctx.state();
418        let physical_plan = planner.create_physical_plan(&logical_plan, &state).await?;
419        assert_eq!(physical_plan.name(), "EmptyExec");
420        assert!(physical_plan.is::<EmptyExec>());
421
422        Ok(())
423    }
424
425    #[tokio::test]
426    async fn test_create_physical_plan_with_session_runtime() -> Result<()> {
427        let ctx = Arc::new(SessionContext::new());
428        let ffi_planner = create_ffi_query_planner(Arc::clone(&ctx));
429        let logical_plan = LogicalPlanBuilder::empty(false).build()?;
430        let state = ctx.state();
431
432        let physical_plan = ffi_planner
433            .create_physical_plan_with_session_runtime(
434                &logical_plan,
435                &state,
436                Some(Handle::current()),
437            )
438            .await?;
439
440        assert_eq!(physical_plan.name(), "EmptyExec");
441        assert!(physical_plan.is::<EmptyExec>());
442
443        Ok(())
444    }
445}