Skip to main content

datafusion_ffi/session/
mod.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 [`Session`].
19//!
20//! # Delegating physical planning
21//!
22//! Consider a session owned by library A that uses a query planner owned by
23//! library C. After A installs C's planner, [`ForeignSession::query_planner`]
24//! returns C's planner and [`ForeignSession::create_physical_plan`] dispatches
25//! to C's planner. C must not call `create_physical_plan`, or invoke the planner
26//! returned by `query_planner`, to delegate planning back to A. Repeating either
27//! self-call recurses until the stack is exhausted.
28//!
29//! To delegate safely, A must export its original planner before installing C's
30//! planner, and C must retain and invoke that planner directly. See the
31//! [`crate::query_planner`] module for details.
32
33use std::any::Any;
34use std::collections::HashMap;
35use std::ffi::c_void;
36use std::sync::{Arc, OnceLock};
37
38use arrow_schema::SchemaRef;
39use arrow_schema::ffi::FFI_ArrowSchema;
40use async_ffi::{FfiFuture, FutureExt};
41use async_trait::async_trait;
42use datafusion_common::config::{ConfigFileType, ConfigOptions, TableOptions};
43use datafusion_common::{DFSchema, DataFusionError};
44use datafusion_execution::TaskContext;
45use datafusion_execution::config::SessionConfig;
46use datafusion_execution::runtime_env::RuntimeEnv;
47use datafusion_expr::execution_props::ExecutionProps;
48use datafusion_expr::registry::{ExtensionTypeRegistryRef, MemoryExtensionTypeRegistry};
49use datafusion_expr::{
50    AggregateUDF, AggregateUDFImpl, Expr, HigherOrderUDF, LogicalPlan, ScalarUDF,
51    ScalarUDFImpl, WindowUDF, WindowUDFImpl,
52};
53use datafusion_physical_expr::PhysicalExpr;
54use datafusion_physical_plan::ExecutionPlan;
55use datafusion_proto::bytes::{
56    logical_plan_from_bytes_with_extension_codec,
57    logical_plan_to_bytes_with_extension_codec,
58};
59use datafusion_proto::logical_plan::LogicalExtensionCodec;
60use datafusion_proto::logical_plan::from_proto::parse_expr;
61use datafusion_proto::logical_plan::to_proto::serialize_expr;
62use datafusion_proto::physical_plan::DefaultPhysicalExtensionCodec;
63use datafusion_proto::protobuf::LogicalExprNode;
64use datafusion_session::{
65    CatalogProviderList, PhysicalOptimizerRule, QueryPlanner, Session,
66};
67use prost::Message;
68
69use stabby::str::Str as SStr;
70use stabby::string::String as SString;
71use stabby::vec::Vec as SVec;
72use tokio::runtime::Handle;
73
74use crate::arrow_wrappers::WrappedSchema;
75use crate::catalog_provider_list::FFI_CatalogProviderList;
76use crate::execution::FFI_TaskContext;
77use crate::execution_plan::FFI_ExecutionPlan;
78use crate::physical_expr::FFI_PhysicalExpr;
79use crate::physical_optimizer::FFI_PhysicalOptimizerRule;
80use crate::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
81use crate::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
82use crate::query_planner::FFI_QueryPlanner;
83use crate::session::config::FFI_SessionConfig;
84use crate::udaf::FFI_AggregateUDF;
85use crate::udf::FFI_ScalarUDF;
86use crate::udwf::FFI_WindowUDF;
87use crate::util::FFI_Result;
88use crate::{df_result, sresult, sresult_return};
89
90pub mod config;
91
92/// A stable struct for sharing [`Session`] across FFI boundaries.
93///
94/// Care must be taken when using this struct. Unlike most of the structs in
95/// this crate, the private data for [`FFI_SessionRef`] contains borrowed data.
96/// The lifetime of the borrow is lost when hidden within the ``*mut c_void``
97/// of the private data. For this reason, it is the user's responsibility to
98/// ensure the lifetime of the [`Session`] remains valid.
99///
100/// The reason for storing `&dyn Session` is because the primary motivation
101/// for implementing this struct is [`crate::table_provider::FFI_TableProvider`]
102/// which has methods that require `&dyn Session`. For usage within this crate
103/// we know the [`Session`] lifetimes are valid.
104#[repr(C)]
105#[derive(Debug)]
106pub(crate) struct FFI_SessionRef {
107    session_id: unsafe extern "C" fn(&Self) -> SStr,
108
109    config: unsafe extern "C" fn(&Self) -> FFI_SessionConfig,
110
111    catalog_list: unsafe extern "C" fn(&Self) -> FFI_CatalogProviderList,
112
113    query_planner: unsafe extern "C" fn(&Self) -> FFI_QueryPlanner,
114
115    optimize: unsafe extern "C" fn(
116        &Self,
117        logical_plan_serialized: SVec<u8>,
118    ) -> FFI_Result<SVec<u8>>,
119
120    create_physical_plan:
121        unsafe extern "C" fn(
122            &Self,
123            logical_plan_serialized: SVec<u8>,
124        ) -> FfiFuture<FFI_Result<FFI_ExecutionPlan>>,
125
126    create_physical_expr: unsafe extern "C" fn(
127        &Self,
128        expr_serialized: SVec<u8>,
129        schema: WrappedSchema,
130    ) -> FFI_Result<FFI_PhysicalExpr>,
131
132    scalar_functions: unsafe extern "C" fn(&Self) -> SVec<(SString, FFI_ScalarUDF)>,
133
134    aggregate_functions: unsafe extern "C" fn(&Self) -> SVec<(SString, FFI_AggregateUDF)>,
135
136    window_functions: unsafe extern "C" fn(&Self) -> SVec<(SString, FFI_WindowUDF)>,
137
138    table_options: unsafe extern "C" fn(&Self) -> SVec<(SString, SString)>,
139
140    default_table_options: unsafe extern "C" fn(&Self) -> SVec<(SString, SString)>,
141
142    task_ctx: unsafe extern "C" fn(&Self) -> FFI_TaskContext,
143
144    physical_optimizers: unsafe extern "C" fn(&Self) -> SVec<FFI_PhysicalOptimizerRule>,
145
146    logical_codec: FFI_LogicalExtensionCodec,
147
148    physical_codec: FFI_PhysicalExtensionCodec,
149
150    /// Used to create a clone on the provider of the registry. This should
151    /// only need to be called by the receiver of the plan.
152    clone: unsafe extern "C" fn(plan: &Self) -> Self,
153
154    /// Release the memory of the private data when it is no longer being used.
155    release: unsafe extern "C" fn(arg: &mut Self),
156
157    /// Return the major DataFusion version number of this registry.
158    pub version: unsafe extern "C" fn() -> u64,
159
160    /// Internal data. This is only to be accessed by the provider of the plan.
161    /// A [`ForeignSession`] should never attempt to access this data.
162    private_data: *mut c_void,
163
164    /// Utility to identify when FFI objects are accessed locally through
165    /// the foreign interface.
166    pub library_marker_id: extern "C" fn() -> usize,
167}
168
169unsafe impl Send for FFI_SessionRef {}
170unsafe impl Sync for FFI_SessionRef {}
171
172struct SessionPrivateData<'a> {
173    session: &'a dyn Session,
174    runtime: Option<Handle>,
175}
176
177impl FFI_SessionRef {
178    fn inner(&self) -> &dyn Session {
179        let private_data = self.private_data as *const SessionPrivateData;
180        unsafe { (*private_data).session }
181    }
182
183    unsafe fn runtime(&self) -> &Option<Handle> {
184        unsafe {
185            let private_data = self.private_data as *const SessionPrivateData;
186            &(*private_data).runtime
187        }
188    }
189}
190
191unsafe extern "C" fn session_id_fn_wrapper(session: &FFI_SessionRef) -> SStr<'_> {
192    let session = session.inner();
193    session.session_id().into()
194}
195
196unsafe extern "C" fn config_fn_wrapper(session: &FFI_SessionRef) -> FFI_SessionConfig {
197    let session = session.inner();
198    session.config().into()
199}
200
201unsafe extern "C" fn catalog_list_fn_wrapper(
202    session: &FFI_SessionRef,
203) -> FFI_CatalogProviderList {
204    FFI_CatalogProviderList::new_with_ffi_codec(
205        session.inner().catalog_list(),
206        unsafe { session.runtime() }.clone(),
207        session.logical_codec.clone(),
208    )
209}
210
211unsafe extern "C" fn query_planner_fn_wrapper(
212    session: &FFI_SessionRef,
213) -> FFI_QueryPlanner {
214    FFI_QueryPlanner::new_with_ffi_codecs(
215        session.inner().query_planner(),
216        session.logical_codec.clone(),
217        session.physical_codec.clone(),
218    )
219}
220
221unsafe extern "C" fn optimize_fn_wrapper(
222    session: &FFI_SessionRef,
223    logical_plan_serialized: SVec<u8>,
224) -> FFI_Result<SVec<u8>> {
225    let logical_codec: Arc<dyn LogicalExtensionCodec> = (&session.logical_codec).into();
226    let inner = session.inner();
227    let logical_plan = sresult_return!(logical_plan_from_bytes_with_extension_codec(
228        logical_plan_serialized.as_slice(),
229        inner.task_ctx().as_ref(),
230        logical_codec.as_ref(),
231    ));
232    let optimized_plan = sresult_return!(inner.optimize(&logical_plan));
233    let optimized_plan = sresult_return!(logical_plan_to_bytes_with_extension_codec(
234        &optimized_plan,
235        logical_codec.as_ref(),
236    ));
237
238    FFI_Result::Ok(SVec::from(optimized_plan.as_ref()))
239}
240
241unsafe extern "C" fn create_physical_plan_fn_wrapper(
242    session: &FFI_SessionRef,
243    logical_plan_serialized: SVec<u8>,
244) -> FfiFuture<FFI_Result<FFI_ExecutionPlan>> {
245    unsafe {
246        let runtime = session.runtime().clone();
247        let session = session.clone();
248        async move {
249            let logical_codec: Arc<dyn LogicalExtensionCodec> =
250                (&session.logical_codec).into();
251            let session = session.inner();
252            let task_ctx = session.task_ctx();
253
254            let logical_plan =
255                sresult_return!(logical_plan_from_bytes_with_extension_codec(
256                    logical_plan_serialized.as_slice(),
257                    task_ctx.as_ref(),
258                    logical_codec.as_ref(),
259                ));
260
261            let physical_plan = session.create_physical_plan(&logical_plan).await;
262
263            sresult!(physical_plan.map(|plan| FFI_ExecutionPlan::new(plan, runtime)))
264        }
265        .into_ffi()
266    }
267}
268
269unsafe extern "C" fn create_physical_expr_fn_wrapper(
270    session: &FFI_SessionRef,
271    expr_serialized: SVec<u8>,
272    schema: WrappedSchema,
273) -> FFI_Result<FFI_PhysicalExpr> {
274    let codec: Arc<dyn LogicalExtensionCodec> = (&session.logical_codec).into();
275    let session = session.inner();
276
277    let logical_expr = LogicalExprNode::decode(expr_serialized.as_slice()).unwrap();
278    let logical_expr =
279        parse_expr(&logical_expr, session.task_ctx().as_ref(), codec.as_ref()).unwrap();
280    let schema: SchemaRef = schema.into();
281    let schema: DFSchema = sresult_return!(schema.try_into());
282
283    let physical_expr =
284        sresult_return!(session.create_physical_expr(logical_expr, &schema));
285
286    FFI_Result::Ok(physical_expr.into())
287}
288
289unsafe extern "C" fn scalar_functions_fn_wrapper(
290    session: &FFI_SessionRef,
291) -> SVec<(SString, FFI_ScalarUDF)> {
292    let session = session.inner();
293    session
294        .scalar_functions()
295        .iter()
296        .map(|(name, udf)| (name.clone().into(), FFI_ScalarUDF::from(Arc::clone(udf))))
297        .collect()
298}
299
300unsafe extern "C" fn aggregate_functions_fn_wrapper(
301    session: &FFI_SessionRef,
302) -> SVec<(SString, FFI_AggregateUDF)> {
303    let session = session.inner();
304    session
305        .aggregate_functions()
306        .iter()
307        .map(|(name, udaf)| {
308            (
309                name.clone().into(),
310                FFI_AggregateUDF::from(Arc::clone(udaf)),
311            )
312        })
313        .collect()
314}
315
316unsafe extern "C" fn window_functions_fn_wrapper(
317    session: &FFI_SessionRef,
318) -> SVec<(SString, FFI_WindowUDF)> {
319    let session = session.inner();
320    session
321        .window_functions()
322        .iter()
323        .map(|(name, udwf)| (name.clone().into(), FFI_WindowUDF::from(Arc::clone(udwf))))
324        .collect()
325}
326
327fn table_options_to_rhash(mut options: TableOptions) -> SVec<(SString, SString)> {
328    // It is important that we mutate options here and set current format
329    // to None so that when we call `entries()` we get ALL format entries.
330    // We will pass current_format as a special case and strip it on the
331    // other side of the boundary.
332    let current_format = options.current_format.take();
333    let mut options: HashMap<SString, SString> = options
334        .entries()
335        .into_iter()
336        .filter_map(|entry| entry.value.map(|v| (entry.key.into(), v.into())))
337        .collect();
338    if let Some(current_format) = current_format {
339        options.insert(
340            "datafusion_ffi.table_current_format".into(),
341            match current_format {
342                ConfigFileType::JSON => "json",
343                #[cfg(feature = "parquet")]
344                ConfigFileType::PARQUET => "parquet",
345                ConfigFileType::CSV => "csv",
346            }
347            .into(),
348        );
349    }
350
351    options.into_iter().collect()
352}
353
354unsafe extern "C" fn table_options_fn_wrapper(
355    session: &FFI_SessionRef,
356) -> SVec<(SString, SString)> {
357    let session = session.inner();
358    let table_options = session.table_options();
359    table_options_to_rhash(table_options.clone())
360}
361
362unsafe extern "C" fn default_table_options_fn_wrapper(
363    session: &FFI_SessionRef,
364) -> SVec<(SString, SString)> {
365    let session = session.inner();
366    let table_options = session.default_table_options();
367
368    table_options_to_rhash(table_options)
369}
370
371unsafe extern "C" fn task_ctx_fn_wrapper(session: &FFI_SessionRef) -> FFI_TaskContext {
372    session.inner().task_ctx().into()
373}
374
375unsafe extern "C" fn physical_optimizers_fn_wrapper(
376    session: &FFI_SessionRef,
377) -> SVec<FFI_PhysicalOptimizerRule> {
378    let runtime = unsafe { session.runtime().clone() };
379    session
380        .inner()
381        .physical_optimizers()
382        .iter()
383        .map(|rule| FFI_PhysicalOptimizerRule::new(Arc::clone(rule), runtime.clone()))
384        .collect()
385}
386
387unsafe extern "C" fn release_fn_wrapper(provider: &mut FFI_SessionRef) {
388    unsafe {
389        let private_data =
390            Box::from_raw(provider.private_data as *mut SessionPrivateData);
391        drop(private_data);
392    }
393}
394
395unsafe extern "C" fn clone_fn_wrapper(provider: &FFI_SessionRef) -> FFI_SessionRef {
396    unsafe {
397        let old_private_data = provider.private_data as *const SessionPrivateData;
398
399        let private_data = Box::into_raw(Box::new(SessionPrivateData {
400            session: (*old_private_data).session,
401            runtime: (*old_private_data).runtime.clone(),
402        })) as *mut c_void;
403
404        FFI_SessionRef {
405            session_id: session_id_fn_wrapper,
406            config: config_fn_wrapper,
407            catalog_list: catalog_list_fn_wrapper,
408            query_planner: query_planner_fn_wrapper,
409            optimize: optimize_fn_wrapper,
410            create_physical_plan: create_physical_plan_fn_wrapper,
411            create_physical_expr: create_physical_expr_fn_wrapper,
412            scalar_functions: scalar_functions_fn_wrapper,
413            aggregate_functions: aggregate_functions_fn_wrapper,
414            window_functions: window_functions_fn_wrapper,
415            table_options: table_options_fn_wrapper,
416            default_table_options: default_table_options_fn_wrapper,
417            task_ctx: task_ctx_fn_wrapper,
418            physical_optimizers: physical_optimizers_fn_wrapper,
419            logical_codec: provider.logical_codec.clone(),
420            physical_codec: provider.physical_codec.clone(),
421
422            clone: clone_fn_wrapper,
423            release: release_fn_wrapper,
424            version: super::version,
425            private_data,
426            library_marker_id: crate::get_library_marker_id,
427        }
428    }
429}
430
431impl Drop for FFI_SessionRef {
432    fn drop(&mut self) {
433        unsafe { (self.release)(self) }
434    }
435}
436
437impl FFI_SessionRef {
438    /// Creates a new [`FFI_SessionRef`] with a default physical extension codec.
439    ///
440    /// The synthesized [`DefaultPhysicalExtensionCodec`] supports built-in physical
441    /// nodes only. A query planner obtained through this session reference therefore
442    /// cannot encode or decode custom physical extension nodes. Use
443    /// [`Self::new_with_ffi_codecs`] with matching logical and physical codecs when
444    /// custom physical nodes must cross the FFI boundary.
445    ///
446    /// The physical codec wrapper requires a
447    /// [`FFI_TaskContextProvider`](crate::execution::FFI_TaskContextProvider), but this
448    /// constructor has only a session reference and a logical codec. It therefore
449    /// reuses the logical codec's provider. The provider may be owned by another
450    /// library; this is safe, but it must remain live and return the task context
451    /// intended for codec callbacks. The default physical codec does not successfully
452    /// decode extension nodes, so callers that need such callbacks must instead use
453    /// [`Self::new_with_ffi_codecs`] with an explicitly configured physical codec and
454    /// task context provider.
455    pub fn new(
456        session: &dyn Session,
457        runtime: Option<Handle>,
458        logical_codec: FFI_LogicalExtensionCodec,
459    ) -> Self {
460        // `Session` provides a TaskContext but not the reference-counted
461        // TaskContextProvider needed by the FFI codec. Reuse the provider associated
462        // with the logical codec under the assumptions documented above.
463        let physical_codec = FFI_PhysicalExtensionCodec::new(
464            Arc::new(DefaultPhysicalExtensionCodec {}),
465            runtime.clone(),
466            logical_codec.task_ctx_provider.clone(),
467        );
468        Self::new_with_ffi_codecs(session, runtime, logical_codec, physical_codec)
469    }
470
471    /// Creates a new [`FFI_SessionRef`] using existing FFI codecs.
472    ///
473    /// The codecs must form a matching pair that can round-trip every logical and
474    /// physical extension node exposed through the session. Their task context
475    /// providers must remain live and return contexts appropriate for their decode
476    /// callbacks.
477    ///
478    /// If `session` is already foreign, this re-exports its original FFI handle
479    /// rather than adding another wrapper layer. The handle adopts the codecs
480    /// supplied here while retaining its original private data and runtime.
481    pub fn new_with_ffi_codecs(
482        session: &dyn Session,
483        runtime: Option<Handle>,
484        logical_codec: FFI_LogicalExtensionCodec,
485        physical_codec: FFI_PhysicalExtensionCodec,
486    ) -> Self {
487        if let Some(session) = session.as_any().downcast_ref::<ForeignSession>() {
488            let mut session = session.session.clone();
489            session.logical_codec = logical_codec;
490            session.physical_codec = physical_codec;
491            return session;
492        }
493
494        let private_data = Box::new(SessionPrivateData { session, runtime });
495
496        Self {
497            session_id: session_id_fn_wrapper,
498            config: config_fn_wrapper,
499            catalog_list: catalog_list_fn_wrapper,
500            query_planner: query_planner_fn_wrapper,
501            optimize: optimize_fn_wrapper,
502            create_physical_plan: create_physical_plan_fn_wrapper,
503            create_physical_expr: create_physical_expr_fn_wrapper,
504            scalar_functions: scalar_functions_fn_wrapper,
505            aggregate_functions: aggregate_functions_fn_wrapper,
506            window_functions: window_functions_fn_wrapper,
507            table_options: table_options_fn_wrapper,
508            default_table_options: default_table_options_fn_wrapper,
509            task_ctx: task_ctx_fn_wrapper,
510            physical_optimizers: physical_optimizers_fn_wrapper,
511            logical_codec,
512            physical_codec,
513
514            clone: clone_fn_wrapper,
515            release: release_fn_wrapper,
516            version: super::version,
517            private_data: Box::into_raw(private_data) as *mut c_void,
518            library_marker_id: crate::get_library_marker_id,
519        }
520    }
521}
522
523/// This wrapper struct exists on the receiver side of the FFI interface, so it has
524/// no guarantees about being able to access the data in `private_data`. Any functions
525/// defined on this struct must use only the stable function pointers in
526/// `FFI_SessionRef` to interact with the foreign session.
527///
528/// # Query planner delegation
529///
530/// If the session owner installed the current foreign query planner,
531/// [`Session::create_physical_plan`] dispatches back to that planner and
532/// [`Session::query_planner`] returns that planner. The planner must retain and
533/// invoke the session owner's previous planner instead of using either method to
534/// delegate back to the session. Otherwise, repeated delegation exhausts the
535/// stack. See [`crate::query_planner`] for details.
536#[derive(Debug)]
537pub struct ForeignSession {
538    session: FFI_SessionRef,
539    config: SessionConfig,
540    catalog_list: Arc<dyn CatalogProviderList>,
541    scalar_functions: HashMap<String, Arc<ScalarUDF>>,
542    higher_order_functions: HashMap<String, Arc<HigherOrderUDF>>,
543    aggregate_functions: HashMap<String, Arc<AggregateUDF>>,
544    window_functions: HashMap<String, Arc<WindowUDF>>,
545    extension_types: ExtensionTypeRegistryRef,
546    table_options: TableOptions,
547    runtime_env: Arc<RuntimeEnv>,
548    props: ExecutionProps,
549    query_planner: OnceLock<Arc<dyn QueryPlanner + Send + Sync>>,
550    physical_optimizers: OnceLock<Vec<Arc<dyn PhysicalOptimizerRule + Send + Sync>>>,
551}
552
553unsafe impl Send for ForeignSession {}
554unsafe impl Sync for ForeignSession {}
555
556impl FFI_SessionRef {
557    pub fn as_local(&self) -> Option<&dyn Session> {
558        if (self.library_marker_id)() == crate::get_library_marker_id() {
559            return Some(self.inner());
560        }
561        None
562    }
563}
564
565impl TryFrom<&FFI_SessionRef> for ForeignSession {
566    type Error = DataFusionError;
567    fn try_from(session: &FFI_SessionRef) -> Result<Self, Self::Error> {
568        unsafe {
569            let table_options =
570                table_options_from_rhashmap((session.table_options)(session));
571
572            let config = (session.config)(session);
573            let config = SessionConfig::try_from(&config)?;
574
575            let ffi_catalog_list = (session.catalog_list)(session);
576            let catalog_list = (&ffi_catalog_list).into();
577
578            let scalar_functions = (session.scalar_functions)(session)
579                .into_iter()
580                .map(|kv_pair| {
581                    let udf = <Arc<dyn ScalarUDFImpl>>::from(&kv_pair.1);
582
583                    (
584                        kv_pair.0.to_string(),
585                        Arc::new(ScalarUDF::new_from_shared_impl(udf)),
586                    )
587                })
588                .collect();
589            let aggregate_functions = (session.aggregate_functions)(session)
590                .into_iter()
591                .map(|kv_pair| {
592                    let udaf = <Arc<dyn AggregateUDFImpl>>::from(&kv_pair.1);
593
594                    (
595                        kv_pair.0.to_string(),
596                        Arc::new(AggregateUDF::new_from_shared_impl(udaf)),
597                    )
598                })
599                .collect();
600            let window_functions = (session.window_functions)(session)
601                .into_iter()
602                .map(|kv_pair| {
603                    let udwf = <Arc<dyn WindowUDFImpl>>::from(&kv_pair.1);
604
605                    (
606                        kv_pair.0.to_string(),
607                        Arc::new(WindowUDF::new_from_shared_impl(udwf)),
608                    )
609                })
610                .collect();
611            Ok(Self {
612                session: session.clone(),
613                config,
614                catalog_list,
615                table_options,
616                scalar_functions,
617                higher_order_functions: HashMap::new(),
618                aggregate_functions,
619                window_functions,
620                extension_types: Arc::new(MemoryExtensionTypeRegistry::default()),
621                runtime_env: Default::default(),
622                props: Default::default(),
623                query_planner: OnceLock::new(),
624                physical_optimizers: OnceLock::new(),
625            })
626        }
627    }
628}
629
630impl Clone for FFI_SessionRef {
631    fn clone(&self) -> Self {
632        unsafe { (self.clone)(self) }
633    }
634}
635
636fn table_options_from_rhashmap(options: SVec<(SString, SString)>) -> TableOptions {
637    let mut options: HashMap<String, String> = options
638        .into_iter()
639        .map(|kv_pair| (kv_pair.0.to_string(), kv_pair.1.to_string()))
640        .collect();
641    let current_format = options.remove("datafusion_ffi.table_current_format");
642
643    let mut table_options = TableOptions::default();
644    let formats = [
645        ConfigFileType::CSV,
646        ConfigFileType::JSON,
647        #[cfg(feature = "parquet")]
648        ConfigFileType::PARQUET,
649    ];
650    for format in formats {
651        // It is imperative that if new enum variants are added below that they be
652        // included in the formats list above and in the extension check below.
653        let format_name = match &format {
654            ConfigFileType::CSV => "csv",
655            #[cfg(feature = "parquet")]
656            ConfigFileType::PARQUET => "parquet",
657            ConfigFileType::JSON => "json",
658        };
659        let format_options: HashMap<String, String> = options
660            .iter()
661            .filter_map(|(k, v)| {
662                let (prefix, key) = k.split_once(".")?;
663                if prefix == format_name {
664                    Some((format!("format.{key}"), v.to_owned()))
665                } else {
666                    None
667                }
668            })
669            .collect();
670        if !format_options.is_empty() {
671            table_options.current_format = Some(format.clone());
672            table_options
673                .alter_with_string_hash_map(&format_options)
674                .unwrap_or_else(|err| log::warn!("Error parsing table options: {err}"));
675        }
676    }
677    let extension_options: HashMap<String, String> = options
678        .iter()
679        .filter_map(|(k, v)| {
680            let (prefix, _) = k.split_once(".")?;
681            if !["json", "parquet", "csv"].contains(&prefix) {
682                Some((k.to_owned(), v.to_owned()))
683            } else {
684                None
685            }
686        })
687        .collect();
688    if !extension_options.is_empty() {
689        table_options
690            .alter_with_string_hash_map(&extension_options)
691            .unwrap_or_else(|err| log::warn!("Error parsing table options: {err}"));
692    }
693
694    table_options.current_format =
695        current_format.and_then(|format| match format.as_str() {
696            "csv" => Some(ConfigFileType::CSV),
697            #[cfg(feature = "parquet")]
698            "parquet" => Some(ConfigFileType::PARQUET),
699            "json" => Some(ConfigFileType::JSON),
700            _ => None,
701        });
702    table_options
703}
704
705#[async_trait]
706impl Session for ForeignSession {
707    fn session_id(&self) -> &str {
708        unsafe { (self.session.session_id)(&self.session).as_str() }
709    }
710
711    fn config(&self) -> &SessionConfig {
712        &self.config
713    }
714
715    fn config_options(&self) -> &ConfigOptions {
716        self.config.options()
717    }
718
719    fn catalog_list(&self) -> Arc<dyn CatalogProviderList> {
720        Arc::clone(&self.catalog_list)
721    }
722
723    fn query_planner(&self) -> Arc<dyn QueryPlanner + Send + Sync> {
724        Arc::clone(self.query_planner.get_or_init(|| unsafe {
725            let planner = (self.session.query_planner)(&self.session);
726            (&planner).into()
727        }))
728    }
729
730    fn optimize(&self, plan: &LogicalPlan) -> datafusion_common::Result<LogicalPlan> {
731        unsafe {
732            let codec: Arc<dyn LogicalExtensionCodec> =
733                (&self.session.logical_codec).into();
734            let logical_plan =
735                logical_plan_to_bytes_with_extension_codec(plan, codec.as_ref())?;
736            let optimized_plan = df_result!((self.session.optimize)(
737                &self.session,
738                SVec::from(logical_plan.as_ref()),
739            ))?;
740            logical_plan_from_bytes_with_extension_codec(
741                optimized_plan.as_slice(),
742                self.task_ctx().as_ref(),
743                codec.as_ref(),
744            )
745        }
746    }
747
748    async fn create_physical_plan(
749        &self,
750        logical_plan: &LogicalPlan,
751    ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
752        unsafe {
753            let codec: Arc<dyn LogicalExtensionCodec> =
754                (&self.session.logical_codec).into();
755            let logical_plan =
756                logical_plan_to_bytes_with_extension_codec(logical_plan, codec.as_ref())?;
757            let physical_plan = df_result!(
758                (self.session.create_physical_plan)(
759                    &self.session,
760                    logical_plan.as_ref().into()
761                )
762                .await
763            )?;
764            let physical_plan = <Arc<dyn ExecutionPlan>>::try_from(&physical_plan)?;
765
766            Ok(physical_plan)
767        }
768    }
769
770    fn create_physical_expr(
771        &self,
772        expr: Expr,
773        df_schema: &DFSchema,
774    ) -> datafusion_common::Result<Arc<dyn PhysicalExpr>> {
775        unsafe {
776            let codec: Arc<dyn LogicalExtensionCodec> =
777                (&self.session.logical_codec).into();
778            let logical_expr = serialize_expr(&expr, codec.as_ref())?.encode_to_vec();
779            let schema = WrappedSchema(FFI_ArrowSchema::try_from(df_schema.as_arrow())?);
780
781            let physical_expr = df_result!((self.session.create_physical_expr)(
782                &self.session,
783                logical_expr.into_iter().collect(),
784                schema
785            ))?;
786
787            Ok((&physical_expr).into())
788        }
789    }
790
791    fn physical_optimizers(&self) -> &[Arc<dyn PhysicalOptimizerRule + Send + Sync>] {
792        self.physical_optimizers.get_or_init(|| unsafe {
793            (self.session.physical_optimizers)(&self.session)
794                .into_iter()
795                .map(|rule| (&rule).into())
796                .collect()
797        })
798    }
799
800    fn scalar_functions(&self) -> &HashMap<String, Arc<ScalarUDF>> {
801        &self.scalar_functions
802    }
803
804    fn higher_order_functions(&self) -> &HashMap<String, Arc<HigherOrderUDF>> {
805        &self.higher_order_functions
806    }
807
808    fn aggregate_functions(&self) -> &HashMap<String, Arc<AggregateUDF>> {
809        &self.aggregate_functions
810    }
811
812    fn window_functions(&self) -> &HashMap<String, Arc<WindowUDF>> {
813        &self.window_functions
814    }
815
816    fn extension_type_registry(&self) -> &ExtensionTypeRegistryRef {
817        &self.extension_types
818    }
819
820    fn runtime_env(&self) -> &Arc<RuntimeEnv> {
821        &self.runtime_env
822    }
823
824    fn execution_props(&self) -> &ExecutionProps {
825        &self.props
826    }
827
828    fn as_any(&self) -> &dyn Any {
829        self
830    }
831
832    fn table_options(&self) -> &TableOptions {
833        &self.table_options
834    }
835
836    fn default_table_options(&self) -> TableOptions {
837        unsafe {
838            table_options_from_rhashmap((self.session.default_table_options)(
839                &self.session,
840            ))
841        }
842    }
843
844    fn table_options_mut(&mut self) -> &mut TableOptions {
845        log::warn!(
846            "Mutating table options is not supported via FFI. Changes will not have an effect."
847        );
848        &mut self.table_options
849    }
850
851    fn task_ctx(&self) -> Arc<TaskContext> {
852        unsafe { (self.session.task_ctx)(&self.session).into() }
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use std::sync::Arc;
859    use std::sync::atomic::{AtomicUsize, Ordering};
860
861    use arrow::array::record_batch;
862    use arrow_schema::{DataType, Field, Schema};
863    use datafusion::catalog::{MemTable, MemoryCatalogProvider};
864    use datafusion::execution::SessionStateBuilder;
865    use datafusion_common::DataFusionError;
866    use datafusion_expr::col;
867    use datafusion_expr::registry::FunctionRegistry;
868    use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec;
869
870    use super::*;
871    use crate::proto::physical_extension_codec::tests::TestExtensionCodec;
872
873    static QUERY_PLANNER_CALLS: AtomicUsize = AtomicUsize::new(0);
874    static PHYSICAL_OPTIMIZER_CALLS: AtomicUsize = AtomicUsize::new(0);
875
876    unsafe extern "C" fn counting_query_planner(
877        session: &FFI_SessionRef,
878    ) -> FFI_QueryPlanner {
879        QUERY_PLANNER_CALLS.fetch_add(1, Ordering::Relaxed);
880        unsafe { query_planner_fn_wrapper(session) }
881    }
882
883    unsafe extern "C" fn counting_physical_optimizers(
884        session: &FFI_SessionRef,
885    ) -> SVec<FFI_PhysicalOptimizerRule> {
886        PHYSICAL_OPTIMIZER_CALLS.fetch_add(1, Ordering::Relaxed);
887        unsafe { physical_optimizers_fn_wrapper(session) }
888    }
889
890    #[test]
891    fn test_foreign_session_lazily_loads_planning_state() -> Result<(), DataFusionError> {
892        QUERY_PLANNER_CALLS.store(0, Ordering::Relaxed);
893        PHYSICAL_OPTIMIZER_CALLS.store(0, Ordering::Relaxed);
894
895        let (ctx, task_ctx_provider) = crate::util::tests::test_session_and_ctx();
896        let logical_codec = FFI_LogicalExtensionCodec::new(
897            Arc::new(DefaultLogicalExtensionCodec {}),
898            None,
899            task_ctx_provider,
900        );
901        let state = ctx.state();
902        let mut local_session = FFI_SessionRef::new(&state, None, logical_codec);
903        local_session.query_planner = counting_query_planner;
904        local_session.physical_optimizers = counting_physical_optimizers;
905
906        let mut foreign_session = ForeignSession::try_from(&local_session)?;
907        assert_eq!(QUERY_PLANNER_CALLS.load(Ordering::Relaxed), 0);
908        assert_eq!(PHYSICAL_OPTIMIZER_CALLS.load(Ordering::Relaxed), 0);
909
910        // `FFI_SessionRef::clone` restores the standard function pointers, so
911        // instrument the clone retained by `ForeignSession` as well.
912        foreign_session.session.query_planner = counting_query_planner;
913        foreign_session.session.physical_optimizers = counting_physical_optimizers;
914
915        foreign_session.query_planner();
916        foreign_session.query_planner();
917        assert_eq!(QUERY_PLANNER_CALLS.load(Ordering::Relaxed), 1);
918
919        foreign_session.physical_optimizers();
920        foreign_session.physical_optimizers();
921        assert_eq!(PHYSICAL_OPTIMIZER_CALLS.load(Ordering::Relaxed), 1);
922
923        Ok(())
924    }
925
926    #[tokio::test]
927    async fn test_ffi_session() -> Result<(), DataFusionError> {
928        let (ctx, task_ctx_provider) = crate::util::tests::test_session_and_ctx();
929        let mut table_options = TableOptions::default();
930        table_options.csv.has_header = Some(true);
931        table_options.json.schema_infer_max_rec = Some(10);
932        #[cfg(feature = "parquet")]
933        {
934            table_options.parquet.global.coerce_int96 = Some("123456789".into());
935        }
936        table_options.current_format = Some(ConfigFileType::JSON);
937
938        let state = SessionStateBuilder::new_from_existing(ctx.state())
939            .with_table_options(table_options)
940            .build();
941
942        let logical_codec = FFI_LogicalExtensionCodec::new(
943            Arc::new(DefaultLogicalExtensionCodec {}),
944            None,
945            task_ctx_provider,
946        );
947
948        let local_session = FFI_SessionRef::new(&state, None, logical_codec);
949        let foreign_session = ForeignSession::try_from(&local_session)?;
950
951        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
952        let df_schema = schema.try_into()?;
953        let physical_expr = foreign_session.create_physical_expr(col("a"), &df_schema)?;
954        assert_eq!(
955            format!("{physical_expr:?}"),
956            "Column { name: \"a\", index: 0 }"
957        );
958
959        assert_eq!(foreign_session.session_id(), state.session_id());
960
961        let foreign_catalog_list = foreign_session.catalog_list();
962        assert_eq!(
963            foreign_catalog_list.catalog_names(),
964            state.catalog_list().catalog_names()
965        );
966        foreign_catalog_list.register_catalog(
967            "foreign_registered".to_owned(),
968            Arc::new(MemoryCatalogProvider::new()),
969        );
970        assert!(state.catalog_list().catalog("foreign_registered").is_some());
971
972        let logical_plan = LogicalPlan::default();
973        assert_eq!(foreign_session.optimize(&logical_plan)?, logical_plan);
974        assert_eq!(
975            foreign_session.physical_optimizers().len(),
976            state.physical_optimizers().len()
977        );
978        assert!(foreign_session.statistics_registry().is_none());
979        let planned = foreign_session
980            .query_planner()
981            .create_physical_plan(&logical_plan, &foreign_session)
982            .await?;
983        assert_eq!(planned.name(), "EmptyExec");
984
985        let physical_plan = foreign_session.create_physical_plan(&logical_plan).await?;
986        assert_eq!(
987            format!("{physical_plan:?}"),
988            "EmptyExec { schema: Schema { fields: [], metadata: {} }, partitions: 1, cache: PlanProperties { eq_properties: EquivalenceProperties { eq_group: EquivalenceGroup { map: {}, classes: [] }, oeq_class: OrderingEquivalenceClass { orderings: [] }, oeq_cache: OrderingEquivalenceCache { normal_cls: OrderingEquivalenceClass { orderings: [] }, leading_map: {} }, constraints: Constraints { inner: [] }, schema: Schema { fields: [], metadata: {} } }, partitioning: UnknownPartitioning(1), emission_type: Incremental, boundedness: Bounded, evaluation_type: Lazy, scheduling_type: Cooperative, output_ordering: None } }"
989        );
990
991        assert_eq!(
992            format!("{:?}", foreign_session.default_table_options()),
993            format!("{:?}", state.default_table_options())
994        );
995
996        assert_eq!(
997            format!("{:?}", foreign_session.table_options()),
998            format!("{:?}", state.table_options())
999        );
1000
1001        let local_udfs = state.udfs();
1002        for udf in foreign_session.scalar_functions().keys() {
1003            assert!(local_udfs.contains(udf));
1004        }
1005        let local_udafs = state.udafs();
1006        for udaf in foreign_session.aggregate_functions().keys() {
1007            assert!(local_udafs.contains(udaf));
1008        }
1009        let local_udwfs = state.udwfs();
1010        for udwf in foreign_session.window_functions().keys() {
1011            assert!(local_udwfs.contains(udwf));
1012        }
1013
1014        Ok(())
1015    }
1016
1017    /// `create_physical_plan` must serialize with the session's logical codec on
1018    /// both sides of the boundary. A plan that scans a custom table provider is
1019    /// unserializable without it.
1020    #[tokio::test]
1021    async fn test_create_physical_plan_uses_logical_codec() -> Result<(), DataFusionError>
1022    {
1023        let (ctx, task_ctx_provider) = crate::util::tests::test_session_and_ctx();
1024
1025        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1026        let batch = record_batch!(("a", Int32, [1, 2, 3]))?;
1027        let table = MemTable::try_new(schema, vec![vec![batch]])?;
1028        ctx.register_table("test_table", Arc::new(table))?;
1029
1030        let logical_codec = FFI_LogicalExtensionCodec::new(
1031            Arc::new(TestExtensionCodec),
1032            None,
1033            task_ctx_provider,
1034        );
1035
1036        let state = ctx.state();
1037        let local_session = FFI_SessionRef::new(&state, None, logical_codec);
1038        let foreign_session = ForeignSession::try_from(&local_session)?;
1039
1040        let logical_plan = ctx.table("test_table").await?.into_optimized_plan()?;
1041        let physical_plan = foreign_session.create_physical_plan(&logical_plan).await?;
1042
1043        assert_eq!(physical_plan.name(), "DataSourceExec");
1044        assert_eq!(physical_plan.schema().field(0).name(), "a");
1045
1046        Ok(())
1047    }
1048}