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, logical_plan_from_bytes_with_extension_codec,
57    logical_plan_to_bytes, 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 session = session.inner();
250            let task_ctx = session.task_ctx();
251
252            let logical_plan = sresult_return!(logical_plan_from_bytes(
253                logical_plan_serialized.as_slice(),
254                task_ctx.as_ref(),
255            ));
256
257            let physical_plan = session.create_physical_plan(&logical_plan).await;
258
259            sresult!(physical_plan.map(|plan| FFI_ExecutionPlan::new(plan, runtime)))
260        }
261        .into_ffi()
262    }
263}
264
265unsafe extern "C" fn create_physical_expr_fn_wrapper(
266    session: &FFI_SessionRef,
267    expr_serialized: SVec<u8>,
268    schema: WrappedSchema,
269) -> FFI_Result<FFI_PhysicalExpr> {
270    let codec: Arc<dyn LogicalExtensionCodec> = (&session.logical_codec).into();
271    let session = session.inner();
272
273    let logical_expr = LogicalExprNode::decode(expr_serialized.as_slice()).unwrap();
274    let logical_expr =
275        parse_expr(&logical_expr, session.task_ctx().as_ref(), codec.as_ref()).unwrap();
276    let schema: SchemaRef = schema.into();
277    let schema: DFSchema = sresult_return!(schema.try_into());
278
279    let physical_expr =
280        sresult_return!(session.create_physical_expr(logical_expr, &schema));
281
282    FFI_Result::Ok(physical_expr.into())
283}
284
285unsafe extern "C" fn scalar_functions_fn_wrapper(
286    session: &FFI_SessionRef,
287) -> SVec<(SString, FFI_ScalarUDF)> {
288    let session = session.inner();
289    session
290        .scalar_functions()
291        .iter()
292        .map(|(name, udf)| (name.clone().into(), FFI_ScalarUDF::from(Arc::clone(udf))))
293        .collect()
294}
295
296unsafe extern "C" fn aggregate_functions_fn_wrapper(
297    session: &FFI_SessionRef,
298) -> SVec<(SString, FFI_AggregateUDF)> {
299    let session = session.inner();
300    session
301        .aggregate_functions()
302        .iter()
303        .map(|(name, udaf)| {
304            (
305                name.clone().into(),
306                FFI_AggregateUDF::from(Arc::clone(udaf)),
307            )
308        })
309        .collect()
310}
311
312unsafe extern "C" fn window_functions_fn_wrapper(
313    session: &FFI_SessionRef,
314) -> SVec<(SString, FFI_WindowUDF)> {
315    let session = session.inner();
316    session
317        .window_functions()
318        .iter()
319        .map(|(name, udwf)| (name.clone().into(), FFI_WindowUDF::from(Arc::clone(udwf))))
320        .collect()
321}
322
323fn table_options_to_rhash(mut options: TableOptions) -> SVec<(SString, SString)> {
324    // It is important that we mutate options here and set current format
325    // to None so that when we call `entries()` we get ALL format entries.
326    // We will pass current_format as a special case and strip it on the
327    // other side of the boundary.
328    let current_format = options.current_format.take();
329    let mut options: HashMap<SString, SString> = options
330        .entries()
331        .into_iter()
332        .filter_map(|entry| entry.value.map(|v| (entry.key.into(), v.into())))
333        .collect();
334    if let Some(current_format) = current_format {
335        options.insert(
336            "datafusion_ffi.table_current_format".into(),
337            match current_format {
338                ConfigFileType::JSON => "json",
339                #[cfg(feature = "parquet")]
340                ConfigFileType::PARQUET => "parquet",
341                ConfigFileType::CSV => "csv",
342            }
343            .into(),
344        );
345    }
346
347    options.into_iter().collect()
348}
349
350unsafe extern "C" fn table_options_fn_wrapper(
351    session: &FFI_SessionRef,
352) -> SVec<(SString, SString)> {
353    let session = session.inner();
354    let table_options = session.table_options();
355    table_options_to_rhash(table_options.clone())
356}
357
358unsafe extern "C" fn default_table_options_fn_wrapper(
359    session: &FFI_SessionRef,
360) -> SVec<(SString, SString)> {
361    let session = session.inner();
362    let table_options = session.default_table_options();
363
364    table_options_to_rhash(table_options)
365}
366
367unsafe extern "C" fn task_ctx_fn_wrapper(session: &FFI_SessionRef) -> FFI_TaskContext {
368    session.inner().task_ctx().into()
369}
370
371unsafe extern "C" fn physical_optimizers_fn_wrapper(
372    session: &FFI_SessionRef,
373) -> SVec<FFI_PhysicalOptimizerRule> {
374    let runtime = unsafe { session.runtime().clone() };
375    session
376        .inner()
377        .physical_optimizers()
378        .iter()
379        .map(|rule| FFI_PhysicalOptimizerRule::new(Arc::clone(rule), runtime.clone()))
380        .collect()
381}
382
383unsafe extern "C" fn release_fn_wrapper(provider: &mut FFI_SessionRef) {
384    unsafe {
385        let private_data =
386            Box::from_raw(provider.private_data as *mut SessionPrivateData);
387        drop(private_data);
388    }
389}
390
391unsafe extern "C" fn clone_fn_wrapper(provider: &FFI_SessionRef) -> FFI_SessionRef {
392    unsafe {
393        let old_private_data = provider.private_data as *const SessionPrivateData;
394
395        let private_data = Box::into_raw(Box::new(SessionPrivateData {
396            session: (*old_private_data).session,
397            runtime: (*old_private_data).runtime.clone(),
398        })) as *mut c_void;
399
400        FFI_SessionRef {
401            session_id: session_id_fn_wrapper,
402            config: config_fn_wrapper,
403            catalog_list: catalog_list_fn_wrapper,
404            query_planner: query_planner_fn_wrapper,
405            optimize: optimize_fn_wrapper,
406            create_physical_plan: create_physical_plan_fn_wrapper,
407            create_physical_expr: create_physical_expr_fn_wrapper,
408            scalar_functions: scalar_functions_fn_wrapper,
409            aggregate_functions: aggregate_functions_fn_wrapper,
410            window_functions: window_functions_fn_wrapper,
411            table_options: table_options_fn_wrapper,
412            default_table_options: default_table_options_fn_wrapper,
413            task_ctx: task_ctx_fn_wrapper,
414            physical_optimizers: physical_optimizers_fn_wrapper,
415            logical_codec: provider.logical_codec.clone(),
416            physical_codec: provider.physical_codec.clone(),
417
418            clone: clone_fn_wrapper,
419            release: release_fn_wrapper,
420            version: super::version,
421            private_data,
422            library_marker_id: crate::get_library_marker_id,
423        }
424    }
425}
426
427impl Drop for FFI_SessionRef {
428    fn drop(&mut self) {
429        unsafe { (self.release)(self) }
430    }
431}
432
433impl FFI_SessionRef {
434    /// Creates a new [`FFI_SessionRef`] with a default physical extension codec.
435    ///
436    /// The synthesized [`DefaultPhysicalExtensionCodec`] supports built-in physical
437    /// nodes only. A query planner obtained through this session reference therefore
438    /// cannot encode or decode custom physical extension nodes. Use
439    /// [`Self::new_with_ffi_codecs`] with matching logical and physical codecs when
440    /// custom physical nodes must cross the FFI boundary.
441    ///
442    /// The physical codec wrapper requires a
443    /// [`FFI_TaskContextProvider`](crate::execution::FFI_TaskContextProvider), but this
444    /// constructor has only a session reference and a logical codec. It therefore
445    /// reuses the logical codec's provider. The provider may be owned by another
446    /// library; this is safe, but it must remain live and return the task context
447    /// intended for codec callbacks. The default physical codec does not successfully
448    /// decode extension nodes, so callers that need such callbacks must instead use
449    /// [`Self::new_with_ffi_codecs`] with an explicitly configured physical codec and
450    /// task context provider.
451    pub fn new(
452        session: &dyn Session,
453        runtime: Option<Handle>,
454        logical_codec: FFI_LogicalExtensionCodec,
455    ) -> Self {
456        // `Session` provides a TaskContext but not the reference-counted
457        // TaskContextProvider needed by the FFI codec. Reuse the provider associated
458        // with the logical codec under the assumptions documented above.
459        let physical_codec = FFI_PhysicalExtensionCodec::new(
460            Arc::new(DefaultPhysicalExtensionCodec {}),
461            runtime.clone(),
462            logical_codec.task_ctx_provider.clone(),
463        );
464        Self::new_with_ffi_codecs(session, runtime, logical_codec, physical_codec)
465    }
466
467    /// Creates a new [`FFI_SessionRef`] using existing FFI codecs.
468    ///
469    /// The codecs must form a matching pair that can round-trip every logical and
470    /// physical extension node exposed through the session. Their task context
471    /// providers must remain live and return contexts appropriate for their decode
472    /// callbacks.
473    ///
474    /// If `session` is already foreign, this re-exports its original FFI handle
475    /// rather than adding another wrapper layer. The handle adopts the codecs
476    /// supplied here while retaining its original private data and runtime.
477    pub fn new_with_ffi_codecs(
478        session: &dyn Session,
479        runtime: Option<Handle>,
480        logical_codec: FFI_LogicalExtensionCodec,
481        physical_codec: FFI_PhysicalExtensionCodec,
482    ) -> Self {
483        if let Some(session) = session.as_any().downcast_ref::<ForeignSession>() {
484            let mut session = session.session.clone();
485            session.logical_codec = logical_codec;
486            session.physical_codec = physical_codec;
487            return session;
488        }
489
490        let private_data = Box::new(SessionPrivateData { session, runtime });
491
492        Self {
493            session_id: session_id_fn_wrapper,
494            config: config_fn_wrapper,
495            catalog_list: catalog_list_fn_wrapper,
496            query_planner: query_planner_fn_wrapper,
497            optimize: optimize_fn_wrapper,
498            create_physical_plan: create_physical_plan_fn_wrapper,
499            create_physical_expr: create_physical_expr_fn_wrapper,
500            scalar_functions: scalar_functions_fn_wrapper,
501            aggregate_functions: aggregate_functions_fn_wrapper,
502            window_functions: window_functions_fn_wrapper,
503            table_options: table_options_fn_wrapper,
504            default_table_options: default_table_options_fn_wrapper,
505            task_ctx: task_ctx_fn_wrapper,
506            physical_optimizers: physical_optimizers_fn_wrapper,
507            logical_codec,
508            physical_codec,
509
510            clone: clone_fn_wrapper,
511            release: release_fn_wrapper,
512            version: super::version,
513            private_data: Box::into_raw(private_data) as *mut c_void,
514            library_marker_id: crate::get_library_marker_id,
515        }
516    }
517}
518
519/// This wrapper struct exists on the receiver side of the FFI interface, so it has
520/// no guarantees about being able to access the data in `private_data`. Any functions
521/// defined on this struct must use only the stable function pointers in
522/// `FFI_SessionRef` to interact with the foreign session.
523///
524/// # Query planner delegation
525///
526/// If the session owner installed the current foreign query planner,
527/// [`Session::create_physical_plan`] dispatches back to that planner and
528/// [`Session::query_planner`] returns that planner. The planner must retain and
529/// invoke the session owner's previous planner instead of using either method to
530/// delegate back to the session. Otherwise, repeated delegation exhausts the
531/// stack. See [`crate::query_planner`] for details.
532#[derive(Debug)]
533pub struct ForeignSession {
534    session: FFI_SessionRef,
535    config: SessionConfig,
536    catalog_list: Arc<dyn CatalogProviderList>,
537    scalar_functions: HashMap<String, Arc<ScalarUDF>>,
538    higher_order_functions: HashMap<String, Arc<HigherOrderUDF>>,
539    aggregate_functions: HashMap<String, Arc<AggregateUDF>>,
540    window_functions: HashMap<String, Arc<WindowUDF>>,
541    extension_types: ExtensionTypeRegistryRef,
542    table_options: TableOptions,
543    runtime_env: Arc<RuntimeEnv>,
544    props: ExecutionProps,
545    query_planner: OnceLock<Arc<dyn QueryPlanner + Send + Sync>>,
546    physical_optimizers: OnceLock<Vec<Arc<dyn PhysicalOptimizerRule + Send + Sync>>>,
547}
548
549unsafe impl Send for ForeignSession {}
550unsafe impl Sync for ForeignSession {}
551
552impl FFI_SessionRef {
553    pub fn as_local(&self) -> Option<&dyn Session> {
554        if (self.library_marker_id)() == crate::get_library_marker_id() {
555            return Some(self.inner());
556        }
557        None
558    }
559}
560
561impl TryFrom<&FFI_SessionRef> for ForeignSession {
562    type Error = DataFusionError;
563    fn try_from(session: &FFI_SessionRef) -> Result<Self, Self::Error> {
564        unsafe {
565            let table_options =
566                table_options_from_rhashmap((session.table_options)(session));
567
568            let config = (session.config)(session);
569            let config = SessionConfig::try_from(&config)?;
570
571            let ffi_catalog_list = (session.catalog_list)(session);
572            let catalog_list = (&ffi_catalog_list).into();
573
574            let scalar_functions = (session.scalar_functions)(session)
575                .into_iter()
576                .map(|kv_pair| {
577                    let udf = <Arc<dyn ScalarUDFImpl>>::from(&kv_pair.1);
578
579                    (
580                        kv_pair.0.to_string(),
581                        Arc::new(ScalarUDF::new_from_shared_impl(udf)),
582                    )
583                })
584                .collect();
585            let aggregate_functions = (session.aggregate_functions)(session)
586                .into_iter()
587                .map(|kv_pair| {
588                    let udaf = <Arc<dyn AggregateUDFImpl>>::from(&kv_pair.1);
589
590                    (
591                        kv_pair.0.to_string(),
592                        Arc::new(AggregateUDF::new_from_shared_impl(udaf)),
593                    )
594                })
595                .collect();
596            let window_functions = (session.window_functions)(session)
597                .into_iter()
598                .map(|kv_pair| {
599                    let udwf = <Arc<dyn WindowUDFImpl>>::from(&kv_pair.1);
600
601                    (
602                        kv_pair.0.to_string(),
603                        Arc::new(WindowUDF::new_from_shared_impl(udwf)),
604                    )
605                })
606                .collect();
607            Ok(Self {
608                session: session.clone(),
609                config,
610                catalog_list,
611                table_options,
612                scalar_functions,
613                higher_order_functions: HashMap::new(),
614                aggregate_functions,
615                window_functions,
616                extension_types: Arc::new(MemoryExtensionTypeRegistry::default()),
617                runtime_env: Default::default(),
618                props: Default::default(),
619                query_planner: OnceLock::new(),
620                physical_optimizers: OnceLock::new(),
621            })
622        }
623    }
624}
625
626impl Clone for FFI_SessionRef {
627    fn clone(&self) -> Self {
628        unsafe { (self.clone)(self) }
629    }
630}
631
632fn table_options_from_rhashmap(options: SVec<(SString, SString)>) -> TableOptions {
633    let mut options: HashMap<String, String> = options
634        .into_iter()
635        .map(|kv_pair| (kv_pair.0.to_string(), kv_pair.1.to_string()))
636        .collect();
637    let current_format = options.remove("datafusion_ffi.table_current_format");
638
639    let mut table_options = TableOptions::default();
640    let formats = [
641        ConfigFileType::CSV,
642        ConfigFileType::JSON,
643        #[cfg(feature = "parquet")]
644        ConfigFileType::PARQUET,
645    ];
646    for format in formats {
647        // It is imperative that if new enum variants are added below that they be
648        // included in the formats list above and in the extension check below.
649        let format_name = match &format {
650            ConfigFileType::CSV => "csv",
651            #[cfg(feature = "parquet")]
652            ConfigFileType::PARQUET => "parquet",
653            ConfigFileType::JSON => "json",
654        };
655        let format_options: HashMap<String, String> = options
656            .iter()
657            .filter_map(|(k, v)| {
658                let (prefix, key) = k.split_once(".")?;
659                if prefix == format_name {
660                    Some((format!("format.{key}"), v.to_owned()))
661                } else {
662                    None
663                }
664            })
665            .collect();
666        if !format_options.is_empty() {
667            table_options.current_format = Some(format.clone());
668            table_options
669                .alter_with_string_hash_map(&format_options)
670                .unwrap_or_else(|err| log::warn!("Error parsing table options: {err}"));
671        }
672    }
673    let extension_options: HashMap<String, String> = options
674        .iter()
675        .filter_map(|(k, v)| {
676            let (prefix, _) = k.split_once(".")?;
677            if !["json", "parquet", "csv"].contains(&prefix) {
678                Some((k.to_owned(), v.to_owned()))
679            } else {
680                None
681            }
682        })
683        .collect();
684    if !extension_options.is_empty() {
685        table_options
686            .alter_with_string_hash_map(&extension_options)
687            .unwrap_or_else(|err| log::warn!("Error parsing table options: {err}"));
688    }
689
690    table_options.current_format =
691        current_format.and_then(|format| match format.as_str() {
692            "csv" => Some(ConfigFileType::CSV),
693            #[cfg(feature = "parquet")]
694            "parquet" => Some(ConfigFileType::PARQUET),
695            "json" => Some(ConfigFileType::JSON),
696            _ => None,
697        });
698    table_options
699}
700
701#[async_trait]
702impl Session for ForeignSession {
703    fn session_id(&self) -> &str {
704        unsafe { (self.session.session_id)(&self.session).as_str() }
705    }
706
707    fn config(&self) -> &SessionConfig {
708        &self.config
709    }
710
711    fn config_options(&self) -> &ConfigOptions {
712        self.config.options()
713    }
714
715    fn catalog_list(&self) -> Arc<dyn CatalogProviderList> {
716        Arc::clone(&self.catalog_list)
717    }
718
719    fn query_planner(&self) -> Arc<dyn QueryPlanner + Send + Sync> {
720        Arc::clone(self.query_planner.get_or_init(|| unsafe {
721            let planner = (self.session.query_planner)(&self.session);
722            (&planner).into()
723        }))
724    }
725
726    fn optimize(&self, plan: &LogicalPlan) -> datafusion_common::Result<LogicalPlan> {
727        unsafe {
728            let codec: Arc<dyn LogicalExtensionCodec> =
729                (&self.session.logical_codec).into();
730            let logical_plan =
731                logical_plan_to_bytes_with_extension_codec(plan, codec.as_ref())?;
732            let optimized_plan = df_result!((self.session.optimize)(
733                &self.session,
734                SVec::from(logical_plan.as_ref()),
735            ))?;
736            logical_plan_from_bytes_with_extension_codec(
737                optimized_plan.as_slice(),
738                self.task_ctx().as_ref(),
739                codec.as_ref(),
740            )
741        }
742    }
743
744    async fn create_physical_plan(
745        &self,
746        logical_plan: &LogicalPlan,
747    ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
748        unsafe {
749            let logical_plan = logical_plan_to_bytes(logical_plan)?;
750            let physical_plan = df_result!(
751                (self.session.create_physical_plan)(
752                    &self.session,
753                    logical_plan.as_ref().into()
754                )
755                .await
756            )?;
757            let physical_plan = <Arc<dyn ExecutionPlan>>::try_from(&physical_plan)?;
758
759            Ok(physical_plan)
760        }
761    }
762
763    fn create_physical_expr(
764        &self,
765        expr: Expr,
766        df_schema: &DFSchema,
767    ) -> datafusion_common::Result<Arc<dyn PhysicalExpr>> {
768        unsafe {
769            let codec: Arc<dyn LogicalExtensionCodec> =
770                (&self.session.logical_codec).into();
771            let logical_expr = serialize_expr(&expr, codec.as_ref())?.encode_to_vec();
772            let schema = WrappedSchema(FFI_ArrowSchema::try_from(df_schema.as_arrow())?);
773
774            let physical_expr = df_result!((self.session.create_physical_expr)(
775                &self.session,
776                logical_expr.into_iter().collect(),
777                schema
778            ))?;
779
780            Ok((&physical_expr).into())
781        }
782    }
783
784    fn physical_optimizers(&self) -> &[Arc<dyn PhysicalOptimizerRule + Send + Sync>] {
785        self.physical_optimizers.get_or_init(|| unsafe {
786            (self.session.physical_optimizers)(&self.session)
787                .into_iter()
788                .map(|rule| (&rule).into())
789                .collect()
790        })
791    }
792
793    fn scalar_functions(&self) -> &HashMap<String, Arc<ScalarUDF>> {
794        &self.scalar_functions
795    }
796
797    fn higher_order_functions(&self) -> &HashMap<String, Arc<HigherOrderUDF>> {
798        &self.higher_order_functions
799    }
800
801    fn aggregate_functions(&self) -> &HashMap<String, Arc<AggregateUDF>> {
802        &self.aggregate_functions
803    }
804
805    fn window_functions(&self) -> &HashMap<String, Arc<WindowUDF>> {
806        &self.window_functions
807    }
808
809    fn extension_type_registry(&self) -> &ExtensionTypeRegistryRef {
810        &self.extension_types
811    }
812
813    fn runtime_env(&self) -> &Arc<RuntimeEnv> {
814        &self.runtime_env
815    }
816
817    fn execution_props(&self) -> &ExecutionProps {
818        &self.props
819    }
820
821    fn as_any(&self) -> &dyn Any {
822        self
823    }
824
825    fn table_options(&self) -> &TableOptions {
826        &self.table_options
827    }
828
829    fn default_table_options(&self) -> TableOptions {
830        unsafe {
831            table_options_from_rhashmap((self.session.default_table_options)(
832                &self.session,
833            ))
834        }
835    }
836
837    fn table_options_mut(&mut self) -> &mut TableOptions {
838        log::warn!(
839            "Mutating table options is not supported via FFI. Changes will not have an effect."
840        );
841        &mut self.table_options
842    }
843
844    fn task_ctx(&self) -> Arc<TaskContext> {
845        unsafe { (self.session.task_ctx)(&self.session).into() }
846    }
847}
848
849#[cfg(test)]
850mod tests {
851    use std::sync::Arc;
852    use std::sync::atomic::{AtomicUsize, Ordering};
853
854    use arrow_schema::{DataType, Field, Schema};
855    use datafusion::catalog::MemoryCatalogProvider;
856    use datafusion::execution::SessionStateBuilder;
857    use datafusion_common::DataFusionError;
858    use datafusion_expr::col;
859    use datafusion_expr::registry::FunctionRegistry;
860    use datafusion_proto::logical_plan::DefaultLogicalExtensionCodec;
861
862    use super::*;
863
864    static QUERY_PLANNER_CALLS: AtomicUsize = AtomicUsize::new(0);
865    static PHYSICAL_OPTIMIZER_CALLS: AtomicUsize = AtomicUsize::new(0);
866
867    unsafe extern "C" fn counting_query_planner(
868        session: &FFI_SessionRef,
869    ) -> FFI_QueryPlanner {
870        QUERY_PLANNER_CALLS.fetch_add(1, Ordering::Relaxed);
871        unsafe { query_planner_fn_wrapper(session) }
872    }
873
874    unsafe extern "C" fn counting_physical_optimizers(
875        session: &FFI_SessionRef,
876    ) -> SVec<FFI_PhysicalOptimizerRule> {
877        PHYSICAL_OPTIMIZER_CALLS.fetch_add(1, Ordering::Relaxed);
878        unsafe { physical_optimizers_fn_wrapper(session) }
879    }
880
881    #[test]
882    fn test_foreign_session_lazily_loads_planning_state() -> Result<(), DataFusionError> {
883        QUERY_PLANNER_CALLS.store(0, Ordering::Relaxed);
884        PHYSICAL_OPTIMIZER_CALLS.store(0, Ordering::Relaxed);
885
886        let (ctx, task_ctx_provider) = crate::util::tests::test_session_and_ctx();
887        let logical_codec = FFI_LogicalExtensionCodec::new(
888            Arc::new(DefaultLogicalExtensionCodec {}),
889            None,
890            task_ctx_provider,
891        );
892        let state = ctx.state();
893        let mut local_session = FFI_SessionRef::new(&state, None, logical_codec);
894        local_session.query_planner = counting_query_planner;
895        local_session.physical_optimizers = counting_physical_optimizers;
896
897        let mut foreign_session = ForeignSession::try_from(&local_session)?;
898        assert_eq!(QUERY_PLANNER_CALLS.load(Ordering::Relaxed), 0);
899        assert_eq!(PHYSICAL_OPTIMIZER_CALLS.load(Ordering::Relaxed), 0);
900
901        // `FFI_SessionRef::clone` restores the standard function pointers, so
902        // instrument the clone retained by `ForeignSession` as well.
903        foreign_session.session.query_planner = counting_query_planner;
904        foreign_session.session.physical_optimizers = counting_physical_optimizers;
905
906        foreign_session.query_planner();
907        foreign_session.query_planner();
908        assert_eq!(QUERY_PLANNER_CALLS.load(Ordering::Relaxed), 1);
909
910        foreign_session.physical_optimizers();
911        foreign_session.physical_optimizers();
912        assert_eq!(PHYSICAL_OPTIMIZER_CALLS.load(Ordering::Relaxed), 1);
913
914        Ok(())
915    }
916
917    #[tokio::test]
918    async fn test_ffi_session() -> Result<(), DataFusionError> {
919        let (ctx, task_ctx_provider) = crate::util::tests::test_session_and_ctx();
920        let mut table_options = TableOptions::default();
921        table_options.csv.has_header = Some(true);
922        table_options.json.schema_infer_max_rec = Some(10);
923        #[cfg(feature = "parquet")]
924        {
925            table_options.parquet.global.coerce_int96 = Some("123456789".into());
926        }
927        table_options.current_format = Some(ConfigFileType::JSON);
928
929        let state = SessionStateBuilder::new_from_existing(ctx.state())
930            .with_table_options(table_options)
931            .build();
932
933        let logical_codec = FFI_LogicalExtensionCodec::new(
934            Arc::new(DefaultLogicalExtensionCodec {}),
935            None,
936            task_ctx_provider,
937        );
938
939        let local_session = FFI_SessionRef::new(&state, None, logical_codec);
940        let foreign_session = ForeignSession::try_from(&local_session)?;
941
942        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
943        let df_schema = schema.try_into()?;
944        let physical_expr = foreign_session.create_physical_expr(col("a"), &df_schema)?;
945        assert_eq!(
946            format!("{physical_expr:?}"),
947            "Column { name: \"a\", index: 0 }"
948        );
949
950        assert_eq!(foreign_session.session_id(), state.session_id());
951
952        let foreign_catalog_list = foreign_session.catalog_list();
953        assert_eq!(
954            foreign_catalog_list.catalog_names(),
955            state.catalog_list().catalog_names()
956        );
957        foreign_catalog_list.register_catalog(
958            "foreign_registered".to_owned(),
959            Arc::new(MemoryCatalogProvider::new()),
960        );
961        assert!(state.catalog_list().catalog("foreign_registered").is_some());
962
963        let logical_plan = LogicalPlan::default();
964        assert_eq!(foreign_session.optimize(&logical_plan)?, logical_plan);
965        assert_eq!(
966            foreign_session.physical_optimizers().len(),
967            state.physical_optimizers().len()
968        );
969        assert!(foreign_session.statistics_registry().is_none());
970        let planned = foreign_session
971            .query_planner()
972            .create_physical_plan(&logical_plan, &foreign_session)
973            .await?;
974        assert_eq!(planned.name(), "EmptyExec");
975
976        let physical_plan = foreign_session.create_physical_plan(&logical_plan).await?;
977        assert_eq!(
978            format!("{physical_plan:?}"),
979            "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 } }"
980        );
981
982        assert_eq!(
983            format!("{:?}", foreign_session.default_table_options()),
984            format!("{:?}", state.default_table_options())
985        );
986
987        assert_eq!(
988            format!("{:?}", foreign_session.table_options()),
989            format!("{:?}", state.table_options())
990        );
991
992        let local_udfs = state.udfs();
993        for udf in foreign_session.scalar_functions().keys() {
994            assert!(local_udfs.contains(udf));
995        }
996        let local_udafs = state.udafs();
997        for udaf in foreign_session.aggregate_functions().keys() {
998            assert!(local_udafs.contains(udaf));
999        }
1000        let local_udwfs = state.udwfs();
1001        for udwf in foreign_session.window_functions().keys() {
1002            assert!(local_udwfs.contains(udwf));
1003        }
1004
1005        Ok(())
1006    }
1007}