Skip to main content

datafusion_ffi/
table_provider.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
18use std::ffi::c_void;
19use std::sync::Arc;
20
21use arrow::datatypes::SchemaRef;
22use async_ffi::{FfiFuture, FutureExt};
23use async_trait::async_trait;
24use datafusion_catalog::{Session, TableProvider};
25use datafusion_common::Statistics;
26use datafusion_common::error::{DataFusionError, Result};
27use datafusion_execution::TaskContext;
28use datafusion_expr::dml::InsertOp;
29use datafusion_expr::{Expr, TableProviderFilterPushDown, TableType};
30use datafusion_physical_plan::ExecutionPlan;
31use datafusion_proto::logical_plan::from_proto::parse_exprs;
32use datafusion_proto::logical_plan::to_proto::serialize_exprs;
33use datafusion_proto::logical_plan::{
34    DefaultLogicalExtensionCodec, LogicalExtensionCodec,
35};
36use datafusion_proto::protobuf::LogicalExprList;
37use prost::Message;
38
39use stabby::vec::Vec as SVec;
40use tokio::runtime::Handle;
41
42use super::execution_plan::FFI_ExecutionPlan;
43use super::insert_op::FFI_InsertOp;
44use crate::arrow_wrappers::WrappedSchema;
45use crate::execution::FFI_TaskContextProvider;
46use crate::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
47use crate::session::{FFI_SessionRef, ForeignSession};
48use crate::statistics::{deserialize_statistics, serialize_statistics};
49use crate::table_source::{FFI_TableProviderFilterPushDown, FFI_TableType};
50use crate::util::{FFI_Option, FFI_Result};
51use crate::{df_result, sresult_return};
52
53/// A stable struct for sharing [`TableProvider`] across FFI boundaries.
54///
55/// # Struct Layout
56///
57/// The following description applies to all structs provided in this crate.
58///
59/// Each of the exposed structs in this crate is provided with a variant prefixed
60/// with `Foreign`. This variant is designed to be used by the consumer of the
61/// foreign code. The `Foreign` structs should _never_ access the `private_data`
62/// fields. Instead they should only access the data returned through the function
63/// calls defined on the `FFI_` structs. The second purpose of the `Foreign`
64/// structs is to contain additional data that may be needed by the traits that
65/// are implemented on them. Some of these traits require borrowing data which
66/// can be far more convenient to be locally stored.
67///
68/// For example, we have a struct `FFI_TableProvider` to give access to the
69/// `TableProvider` functions like `table_type()` and `scan()`. If we write a
70/// library that wishes to expose it's `TableProvider`, then we can access the
71/// private data that contains the Arc reference to the `TableProvider` via
72/// `FFI_TableProvider`. This data is local to the library.
73///
74/// If we have a program that accesses a `TableProvider` via FFI, then it
75/// will use `ForeignTableProvider`. When using `ForeignTableProvider` we **must**
76/// not attempt to access the `private_data` field in `FFI_TableProvider`. If a
77/// user is testing locally, you may be able to successfully access this field, but
78/// it will only work if you are building against the exact same version of
79/// `DataFusion` for both libraries **and** the same compiler. It will not work
80/// in general.
81///
82/// It is worth noting that which library is the `local` and which is `foreign`
83/// depends on which interface we are considering. For example, suppose we have a
84/// Python library called `my_provider` that exposes a `TableProvider` called
85/// `MyProvider` via `FFI_TableProvider`. Within the library `my_provider` we can
86/// access the `private_data` via `FFI_TableProvider`. We connect this to
87/// `datafusion-python`, where we access it as a `ForeignTableProvider`. Now when
88/// we call `scan()` on this interface, we have to pass it a `FFI_SessionConfig`.
89/// The `SessionConfig` is local to `datafusion-python` and **not** `my_provider`.
90/// It is important to be careful when expanding these functions to be certain which
91/// side of the interface each object refers to.
92#[repr(C)]
93#[derive(Debug)]
94pub struct FFI_TableProvider {
95    /// Return the table schema
96    schema: unsafe extern "C" fn(provider: &Self) -> WrappedSchema,
97
98    /// Perform a scan on the table. See [`TableProvider`] for detailed usage information.
99    ///
100    /// # Arguments
101    ///
102    /// * `provider` - the table provider
103    /// * `session` - session
104    /// * `projections` - if specified, only a subset of the columns are returned
105    /// * `filters_serialized` - filters to apply to the scan, which are a
106    ///   [`LogicalExprList`] protobuf message serialized into bytes to pass
107    ///   across the FFI boundary.
108    /// * `limit` - if specified, limit the number of rows returned
109    scan: unsafe extern "C" fn(
110        provider: &Self,
111        session: FFI_SessionRef,
112        projections: FFI_Option<SVec<usize>>,
113        filters_serialized: SVec<u8>,
114        limit: FFI_Option<usize>,
115    ) -> FfiFuture<FFI_Result<FFI_ExecutionPlan>>,
116
117    /// Return the type of table. See [`TableType`] for options.
118    table_type: unsafe extern "C" fn(provider: &Self) -> FFI_TableType,
119
120    /// Based upon the input filters, identify which are supported. The filters
121    /// are a [`LogicalExprList`] protobuf message serialized into bytes to pass
122    /// across the FFI boundary.
123    supports_filters_pushdown: Option<
124        unsafe extern "C" fn(
125            provider: &FFI_TableProvider,
126            filters_serialized: SVec<u8>,
127        )
128            -> FFI_Result<SVec<FFI_TableProviderFilterPushDown>>,
129    >,
130
131    insert_into: unsafe extern "C" fn(
132        provider: &Self,
133        session: FFI_SessionRef,
134        input: &FFI_ExecutionPlan,
135        insert_op: FFI_InsertOp,
136    ) -> FfiFuture<FFI_Result<FFI_ExecutionPlan>>,
137
138    /// Snapshot the provider's table-level statistics. [`FFI_Option::None`]
139    /// corresponds to [`TableProvider::statistics`] returning `None`;
140    /// `Some(bytes)` is a prost-encoded `datafusion_proto_common::Statistics`.
141    pub statistics: unsafe extern "C" fn(provider: &Self) -> FFI_Option<SVec<u8>>,
142
143    pub logical_codec: FFI_LogicalExtensionCodec,
144
145    /// Used to create a clone on the provider of the execution plan. This should
146    /// only need to be called by the receiver of the plan.
147    clone: unsafe extern "C" fn(plan: &Self) -> Self,
148
149    /// Release the memory of the private data when it is no longer being used.
150    release: unsafe extern "C" fn(arg: &mut Self),
151
152    /// Return the major DataFusion version number of this provider.
153    pub version: unsafe extern "C" fn() -> u64,
154
155    /// Internal data. This is only to be accessed by the provider of the plan.
156    /// A [`ForeignTableProvider`] should never attempt to access this data.
157    private_data: *mut c_void,
158
159    /// Utility to identify when FFI objects are accessed locally through
160    /// the foreign interface. See [`crate::get_library_marker_id`] and
161    /// the crate's `README.md` for more information.
162    pub library_marker_id: extern "C" fn() -> usize,
163}
164
165unsafe impl Send for FFI_TableProvider {}
166unsafe impl Sync for FFI_TableProvider {}
167
168struct ProviderPrivateData {
169    provider: Arc<dyn TableProvider>,
170    runtime: Option<Handle>,
171}
172
173impl FFI_TableProvider {
174    fn inner(&self) -> &Arc<dyn TableProvider> {
175        let private_data = self.private_data as *const ProviderPrivateData;
176        unsafe { &(*private_data).provider }
177    }
178
179    fn runtime(&self) -> &Option<Handle> {
180        let private_data = self.private_data as *const ProviderPrivateData;
181        unsafe { &(*private_data).runtime }
182    }
183}
184
185unsafe extern "C" fn schema_fn_wrapper(provider: &FFI_TableProvider) -> WrappedSchema {
186    provider.inner().schema().into()
187}
188
189unsafe extern "C" fn statistics_fn_wrapper(
190    provider: &FFI_TableProvider,
191) -> FFI_Option<SVec<u8>> {
192    let serialized: Option<SVec<u8>> = provider
193        .inner()
194        .statistics()
195        .map(|s| SVec::from(&*serialize_statistics(&s)));
196    serialized.into()
197}
198
199unsafe extern "C" fn table_type_fn_wrapper(
200    provider: &FFI_TableProvider,
201) -> FFI_TableType {
202    provider.inner().table_type().into()
203}
204
205fn supports_filters_pushdown_internal(
206    provider: &Arc<dyn TableProvider>,
207    filters_serialized: &[u8],
208    task_ctx: &Arc<TaskContext>,
209    codec: &dyn LogicalExtensionCodec,
210) -> Result<SVec<FFI_TableProviderFilterPushDown>> {
211    let filters = match filters_serialized.is_empty() {
212        true => vec![],
213        false => {
214            let proto_filters = LogicalExprList::decode(filters_serialized)
215                .map_err(|e| DataFusionError::Plan(e.to_string()))?;
216
217            parse_exprs(proto_filters.expr.iter(), task_ctx.as_ref(), codec)?
218        }
219    };
220    let filters_borrowed: Vec<&Expr> = filters.iter().collect();
221
222    let results: SVec<_> = provider
223        .supports_filters_pushdown(&filters_borrowed)?
224        .iter()
225        .map(|v| v.into())
226        .collect();
227
228    Ok(results)
229}
230
231unsafe extern "C" fn supports_filters_pushdown_fn_wrapper(
232    provider: &FFI_TableProvider,
233    filters_serialized: SVec<u8>,
234) -> FFI_Result<SVec<FFI_TableProviderFilterPushDown>> {
235    let logical_codec: Arc<dyn LogicalExtensionCodec> = (&provider.logical_codec).into();
236    let task_ctx = sresult_return!(<Arc<TaskContext>>::try_from(
237        &provider.logical_codec.task_ctx_provider
238    ));
239    supports_filters_pushdown_internal(
240        provider.inner(),
241        &filters_serialized,
242        &task_ctx,
243        logical_codec.as_ref(),
244    )
245    .into()
246}
247
248unsafe extern "C" fn scan_fn_wrapper(
249    provider: &FFI_TableProvider,
250    session: FFI_SessionRef,
251    projections: FFI_Option<SVec<usize>>,
252    filters_serialized: SVec<u8>,
253    limit: FFI_Option<usize>,
254) -> FfiFuture<FFI_Result<FFI_ExecutionPlan>> {
255    let task_ctx: Result<Arc<TaskContext>, DataFusionError> =
256        (&provider.logical_codec.task_ctx_provider).try_into();
257    let runtime = provider.runtime().clone();
258    let logical_codec: Arc<dyn LogicalExtensionCodec> = (&provider.logical_codec).into();
259    let internal_provider = Arc::clone(provider.inner());
260
261    async move {
262        let mut foreign_session = None;
263        let session = sresult_return!(
264            session
265                .as_local()
266                .map(Ok::<&dyn Session, DataFusionError>)
267                .unwrap_or_else(|| {
268                    foreign_session = Some(ForeignSession::try_from(&session)?);
269                    Ok(foreign_session.as_ref().unwrap())
270                })
271        );
272
273        let task_ctx = sresult_return!(task_ctx);
274        let filters = match filters_serialized.is_empty() {
275            true => vec![],
276            false => {
277                let proto_filters =
278                    sresult_return!(LogicalExprList::decode(filters_serialized.as_ref()));
279
280                sresult_return!(parse_exprs(
281                    proto_filters.expr.iter(),
282                    task_ctx.as_ref(),
283                    logical_codec.as_ref(),
284                ))
285            }
286        };
287
288        let projections: Option<Vec<usize>> =
289            projections.into_option().map(|p| p.into_iter().collect());
290
291        let plan = sresult_return!(
292            internal_provider
293                .scan(session, projections.as_ref(), &filters, limit.into())
294                .await
295        );
296
297        FFI_Result::Ok(FFI_ExecutionPlan::new(plan, runtime.clone()))
298    }
299    .into_ffi()
300}
301
302unsafe extern "C" fn insert_into_fn_wrapper(
303    provider: &FFI_TableProvider,
304    session: FFI_SessionRef,
305    input: &FFI_ExecutionPlan,
306    insert_op: FFI_InsertOp,
307) -> FfiFuture<FFI_Result<FFI_ExecutionPlan>> {
308    let runtime = provider.runtime().clone();
309    let internal_provider = Arc::clone(provider.inner());
310    let input = input.clone();
311
312    async move {
313        let mut foreign_session = None;
314        let session = sresult_return!(
315            session
316                .as_local()
317                .map(Ok::<&dyn Session, DataFusionError>)
318                .unwrap_or_else(|| {
319                    foreign_session = Some(ForeignSession::try_from(&session)?);
320                    Ok(foreign_session.as_ref().unwrap())
321                })
322        );
323
324        let input = sresult_return!(<Arc<dyn ExecutionPlan>>::try_from(&input));
325
326        let insert_op = InsertOp::from(insert_op);
327
328        let plan = sresult_return!(
329            internal_provider
330                .insert_into(session, input, insert_op)
331                .await
332        );
333
334        FFI_Result::Ok(FFI_ExecutionPlan::new(plan, runtime.clone()))
335    }
336    .into_ffi()
337}
338
339unsafe extern "C" fn release_fn_wrapper(provider: &mut FFI_TableProvider) {
340    unsafe {
341        debug_assert!(!provider.private_data.is_null());
342        let private_data =
343            Box::from_raw(provider.private_data as *mut ProviderPrivateData);
344        drop(private_data);
345        provider.private_data = std::ptr::null_mut();
346    }
347}
348
349unsafe extern "C" fn clone_fn_wrapper(provider: &FFI_TableProvider) -> FFI_TableProvider {
350    let runtime = provider.runtime().clone();
351    let old_provider = Arc::clone(provider.inner());
352
353    let private_data = Box::into_raw(Box::new(ProviderPrivateData {
354        provider: old_provider,
355        runtime,
356    })) as *mut c_void;
357
358    FFI_TableProvider {
359        schema: schema_fn_wrapper,
360        scan: scan_fn_wrapper,
361        table_type: table_type_fn_wrapper,
362        supports_filters_pushdown: provider.supports_filters_pushdown,
363        insert_into: provider.insert_into,
364        statistics: statistics_fn_wrapper,
365        logical_codec: provider.logical_codec.clone(),
366        clone: clone_fn_wrapper,
367        release: release_fn_wrapper,
368        version: super::version,
369        private_data,
370        library_marker_id: crate::get_library_marker_id,
371    }
372}
373
374impl Drop for FFI_TableProvider {
375    fn drop(&mut self) {
376        unsafe { (self.release)(self) }
377    }
378}
379
380impl FFI_TableProvider {
381    /// Creates a new [`FFI_TableProvider`].
382    pub fn new(
383        provider: Arc<dyn TableProvider>,
384        can_support_pushdown_filters: bool,
385        runtime: Option<Handle>,
386        task_ctx_provider: impl Into<FFI_TaskContextProvider>,
387        logical_codec: Option<Arc<dyn LogicalExtensionCodec>>,
388    ) -> Self {
389        let task_ctx_provider = task_ctx_provider.into();
390        let logical_codec =
391            logical_codec.unwrap_or_else(|| Arc::new(DefaultLogicalExtensionCodec {}));
392        let logical_codec = FFI_LogicalExtensionCodec::new(
393            logical_codec,
394            runtime.clone(),
395            task_ctx_provider.clone(),
396        );
397        Self::new_with_ffi_codec(
398            provider,
399            can_support_pushdown_filters,
400            runtime,
401            logical_codec,
402        )
403    }
404
405    /// Creates an [`FFI_TableProvider`] using a prebuilt FFI logical codec.
406    ///
407    /// If `provider` is already foreign, this re-exports its original FFI
408    /// handle rather than adding another wrapper layer. The handle still adopts
409    /// the `logical_codec` supplied here, so it is never silently discarded and
410    /// an imported provider can be rebound to a different session.
411    ///
412    /// `runtime` is only honored when a new wrapper is created. An
413    /// already-foreign handle keeps the runtime of the library that owns it,
414    /// because that value lives in private data this side cannot reach.
415    pub fn new_with_ffi_codec(
416        provider: Arc<dyn TableProvider>,
417        can_support_pushdown_filters: bool,
418        runtime: Option<Handle>,
419        logical_codec: FFI_LogicalExtensionCodec,
420    ) -> Self {
421        if let Some(provider) = provider.downcast_ref::<ForeignTableProvider>() {
422            let mut provider = provider.0.clone();
423            provider.logical_codec = logical_codec;
424            return provider;
425        }
426        let private_data = Box::new(ProviderPrivateData { provider, runtime });
427
428        Self {
429            schema: schema_fn_wrapper,
430            scan: scan_fn_wrapper,
431            table_type: table_type_fn_wrapper,
432            supports_filters_pushdown: match can_support_pushdown_filters {
433                true => Some(supports_filters_pushdown_fn_wrapper),
434                false => None,
435            },
436            insert_into: insert_into_fn_wrapper,
437            statistics: statistics_fn_wrapper,
438            logical_codec,
439            clone: clone_fn_wrapper,
440            release: release_fn_wrapper,
441            version: super::version,
442            private_data: Box::into_raw(private_data) as *mut c_void,
443            library_marker_id: crate::get_library_marker_id,
444        }
445    }
446}
447
448/// This wrapper struct exists on the receiver side of the FFI interface, so it has
449/// no guarantees about being able to access the data in `private_data`. Any functions
450/// defined on this struct must only use the stable functions provided in
451/// FFI_TableProvider to interact with the foreign table provider.
452#[derive(Debug)]
453pub struct ForeignTableProvider(pub FFI_TableProvider);
454
455unsafe impl Send for ForeignTableProvider {}
456unsafe impl Sync for ForeignTableProvider {}
457
458impl From<&FFI_TableProvider> for Arc<dyn TableProvider> {
459    fn from(provider: &FFI_TableProvider) -> Self {
460        if (provider.library_marker_id)() == crate::get_library_marker_id() {
461            Arc::clone(provider.inner()) as Arc<dyn TableProvider>
462        } else {
463            Arc::new(ForeignTableProvider(provider.clone()))
464        }
465    }
466}
467
468impl Clone for FFI_TableProvider {
469    fn clone(&self) -> Self {
470        unsafe { (self.clone)(self) }
471    }
472}
473
474#[async_trait]
475impl TableProvider for ForeignTableProvider {
476    fn schema(&self) -> SchemaRef {
477        let wrapped_schema = unsafe { (self.0.schema)(&self.0) };
478        wrapped_schema.into()
479    }
480
481    fn table_type(&self) -> TableType {
482        unsafe { (self.0.table_type)(&self.0).into() }
483    }
484
485    fn statistics(&self) -> Option<Statistics> {
486        let ffi_opt = unsafe { (self.0.statistics)(&self.0) };
487        let bytes: Option<SVec<u8>> = ffi_opt.into();
488        let bytes = bytes?;
489        match deserialize_statistics(bytes.as_slice()) {
490            Ok(stats) => Some(stats),
491            Err(e) => {
492                log::warn!("Failed to deserialize FFI statistics: {e}");
493                // Fires in debug builds to surface encoding bugs early; callers see None.
494                debug_assert!(false, "Failed to deserialize FFI statistics: {e}");
495                None
496            }
497        }
498    }
499
500    async fn scan(
501        &self,
502        session: &dyn Session,
503        projection: Option<&Vec<usize>>,
504        filters: &[Expr],
505        limit: Option<usize>,
506    ) -> Result<Arc<dyn ExecutionPlan>> {
507        let session = FFI_SessionRef::new(session, None, self.0.logical_codec.clone());
508
509        let projections: FFI_Option<SVec<usize>> = projection
510            .map(|p| p.iter().map(|v| v.to_owned()).collect())
511            .into();
512
513        let codec: Arc<dyn LogicalExtensionCodec> = (&self.0.logical_codec).into();
514        let filter_list = LogicalExprList {
515            expr: serialize_exprs(filters, codec.as_ref())?,
516        };
517        let filters_serialized = filter_list.encode_to_vec().into_iter().collect();
518
519        let plan = unsafe {
520            let maybe_plan = (self.0.scan)(
521                &self.0,
522                session,
523                projections,
524                filters_serialized,
525                limit.into(),
526            )
527            .await;
528
529            <Arc<dyn ExecutionPlan>>::try_from(&df_result!(maybe_plan)?)?
530        };
531
532        Ok(plan)
533    }
534
535    /// Tests whether the table provider can make use of a filter expression
536    /// to optimize data retrieval.
537    fn supports_filters_pushdown(
538        &self,
539        filters: &[&Expr],
540    ) -> Result<Vec<TableProviderFilterPushDown>> {
541        unsafe {
542            let pushdown_fn = match self.0.supports_filters_pushdown {
543                Some(func) => func,
544                None => {
545                    return Ok(vec![
546                        TableProviderFilterPushDown::Unsupported;
547                        filters.len()
548                    ]);
549                }
550            };
551
552            let codec: Arc<dyn LogicalExtensionCodec> = (&self.0.logical_codec).into();
553
554            let expr_list = LogicalExprList {
555                expr: serialize_exprs(
556                    filters.iter().map(|f| f.to_owned()),
557                    codec.as_ref(),
558                )?,
559            };
560            let serialized_filters = expr_list.encode_to_vec();
561
562            let pushdowns = df_result!(pushdown_fn(
563                &self.0,
564                serialized_filters.into_iter().collect()
565            ))?;
566
567            Ok(pushdowns.iter().map(|v| v.into()).collect())
568        }
569    }
570
571    async fn insert_into(
572        &self,
573        session: &dyn Session,
574        input: Arc<dyn ExecutionPlan>,
575        insert_op: InsertOp,
576    ) -> Result<Arc<dyn ExecutionPlan>> {
577        let session = FFI_SessionRef::new(session, None, self.0.logical_codec.clone());
578
579        let rc = Handle::try_current().ok();
580        let input = FFI_ExecutionPlan::new(input, rc);
581        let insert_op: FFI_InsertOp = insert_op.into();
582
583        let plan = unsafe {
584            let maybe_plan =
585                (self.0.insert_into)(&self.0, session, &input, insert_op).await;
586
587            <Arc<dyn ExecutionPlan>>::try_from(&df_result!(maybe_plan)?)?
588        };
589
590        Ok(plan)
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use arrow::datatypes::Schema;
597    use datafusion::prelude::{SessionContext, col, lit};
598    use datafusion_execution::TaskContextProvider;
599
600    use super::*;
601
602    fn create_test_table_provider() -> Result<Arc<dyn TableProvider>> {
603        use arrow::datatypes::Field;
604        use datafusion::arrow::array::Float32Array;
605        use datafusion::arrow::datatypes::DataType;
606        use datafusion::arrow::record_batch::RecordBatch;
607        use datafusion::datasource::MemTable;
608
609        let schema =
610            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)]));
611
612        // define data in two partitions
613        let batch1 = RecordBatch::try_new(
614            Arc::clone(&schema),
615            vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))],
616        )?;
617        let batch2 = RecordBatch::try_new(
618            Arc::clone(&schema),
619            vec![Arc::new(Float32Array::from(vec![64.0]))],
620        )?;
621
622        Ok(Arc::new(MemTable::try_new(
623            schema,
624            vec![vec![batch1], vec![batch2]],
625        )?))
626    }
627
628    #[tokio::test]
629    async fn test_round_trip_ffi_table_provider_scan() -> Result<()> {
630        let provider = create_test_table_provider()?;
631        let ctx = Arc::new(SessionContext::new());
632        let task_ctx_provider = Arc::clone(&ctx) as Arc<dyn TaskContextProvider>;
633        let task_ctx_provider = FFI_TaskContextProvider::from(&task_ctx_provider);
634
635        let mut ffi_provider =
636            FFI_TableProvider::new(provider, true, None, task_ctx_provider, None);
637        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
638
639        let foreign_table_provider: Arc<dyn TableProvider> = (&ffi_provider).into();
640
641        ctx.register_table("t", foreign_table_provider)?;
642
643        let df = ctx.table("t").await?;
644
645        df.select(vec![col("a")])?
646            .filter(col("a").gt(lit(3.0)))?
647            .show()
648            .await?;
649
650        Ok(())
651    }
652
653    #[tokio::test]
654    async fn test_round_trip_ffi_table_provider_insert_into() -> Result<()> {
655        let provider = create_test_table_provider()?;
656        let ctx = Arc::new(SessionContext::new());
657        let task_ctx_provider = Arc::clone(&ctx) as Arc<dyn TaskContextProvider>;
658        let task_ctx_provider = FFI_TaskContextProvider::from(&task_ctx_provider);
659
660        let mut ffi_provider =
661            FFI_TableProvider::new(provider, true, None, task_ctx_provider, None);
662        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
663
664        let foreign_table_provider: Arc<dyn TableProvider> = (&ffi_provider).into();
665
666        ctx.register_table("t", foreign_table_provider)?;
667
668        let result = ctx
669            .sql("INSERT INTO t VALUES (128.0);")
670            .await?
671            .collect()
672            .await?;
673
674        assert!(result.len() == 1 && result[0].num_rows() == 1);
675
676        ctx.table("t")
677            .await?
678            .select(vec![col("a")])?
679            .filter(col("a").gt(lit(3.0)))?
680            .show()
681            .await?;
682
683        Ok(())
684    }
685
686    #[tokio::test]
687    async fn test_aggregation() -> Result<()> {
688        use arrow::datatypes::Field;
689        use datafusion::arrow::array::Float32Array;
690        use datafusion::arrow::datatypes::DataType;
691        use datafusion::arrow::record_batch::RecordBatch;
692        use datafusion::common::assert_batches_eq;
693        use datafusion::datasource::MemTable;
694
695        let schema =
696            Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)]));
697
698        // define data in two partitions
699        let batch1 = RecordBatch::try_new(
700            Arc::clone(&schema),
701            vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))],
702        )?;
703
704        let ctx = Arc::new(SessionContext::new());
705        let task_ctx_provider = Arc::clone(&ctx) as Arc<dyn TaskContextProvider>;
706        let task_ctx_provider = FFI_TaskContextProvider::from(&task_ctx_provider);
707
708        let provider = Arc::new(MemTable::try_new(schema, vec![vec![batch1]])?);
709
710        let mut ffi_provider =
711            FFI_TableProvider::new(provider, true, None, task_ctx_provider, None);
712        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
713
714        let foreign_table_provider: Arc<dyn TableProvider> = (&ffi_provider).into();
715
716        ctx.register_table("t", foreign_table_provider)?;
717
718        let result = ctx
719            .sql("SELECT COUNT(*) as cnt FROM t")
720            .await?
721            .collect()
722            .await?;
723        #[rustfmt::skip]
724        let expected = [
725            "+-----+",
726            "| cnt |",
727            "+-----+",
728            "| 3   |",
729            "+-----+"
730        ];
731        assert_batches_eq!(expected, &result);
732        Ok(())
733    }
734
735    #[test]
736    fn test_ffi_table_provider_local_bypass() -> Result<()> {
737        let table_provider = create_test_table_provider()?;
738
739        let ctx = Arc::new(SessionContext::new()) as Arc<dyn TaskContextProvider>;
740        let task_ctx_provider = FFI_TaskContextProvider::from(&ctx);
741        let mut ffi_table =
742            FFI_TableProvider::new(table_provider, false, None, task_ctx_provider, None);
743
744        // Verify local libraries can be downcast to their original
745        let foreign_table: Arc<dyn TableProvider> = (&ffi_table).into();
746        assert!(
747            foreign_table
748                .downcast_ref::<datafusion::datasource::MemTable>()
749                .is_some()
750        );
751
752        // Verify different library markers generate foreign providers
753        ffi_table.library_marker_id = crate::mock_foreign_marker_id;
754        let foreign_table: Arc<dyn TableProvider> = (&ffi_table).into();
755        assert!(
756            foreign_table
757                .downcast_ref::<ForeignTableProvider>()
758                .is_some()
759        );
760
761        Ok(())
762    }
763
764    #[tokio::test]
765    async fn test_scan_with_none_projection_returns_all_columns() -> Result<()> {
766        use arrow::datatypes::Field;
767        use datafusion::arrow::array::Float32Array;
768        use datafusion::arrow::datatypes::DataType;
769        use datafusion::arrow::record_batch::RecordBatch;
770        use datafusion::datasource::MemTable;
771        use datafusion::physical_plan::collect;
772
773        let schema = Arc::new(Schema::new(vec![
774            Field::new("a", DataType::Float32, false),
775            Field::new("b", DataType::Float32, false),
776            Field::new("c", DataType::Float32, false),
777        ]));
778
779        let batch = RecordBatch::try_new(
780            Arc::clone(&schema),
781            vec![
782                Arc::new(Float32Array::from(vec![1.0, 2.0])),
783                Arc::new(Float32Array::from(vec![3.0, 4.0])),
784                Arc::new(Float32Array::from(vec![5.0, 6.0])),
785            ],
786        )?;
787
788        let provider =
789            Arc::new(MemTable::try_new(Arc::clone(&schema), vec![vec![batch]])?);
790
791        let ctx = Arc::new(SessionContext::new());
792        let task_ctx_provider = Arc::clone(&ctx) as Arc<dyn TaskContextProvider>;
793        let task_ctx_provider = FFI_TaskContextProvider::from(&task_ctx_provider);
794
795        // Wrap in FFI and force the foreign path (not local bypass)
796        let mut ffi_provider =
797            FFI_TableProvider::new(provider, true, None, task_ctx_provider, None);
798        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
799
800        let foreign_table_provider: Arc<dyn TableProvider> = (&ffi_provider).into();
801
802        // Call scan with projection=None, meaning "return all columns"
803        let plan = foreign_table_provider
804            .scan(&ctx.state(), None, &[], None)
805            .await?;
806        assert_eq!(
807            plan.schema().fields().len(),
808            3,
809            "scan(projection=None) should return all columns; got {}",
810            plan.schema().fields().len()
811        );
812
813        // Also verify we can execute and get correct data
814        let batches = collect(plan, ctx.task_ctx()).await?;
815        assert_eq!(batches.len(), 1);
816        assert_eq!(batches[0].num_columns(), 3);
817        assert_eq!(batches[0].num_rows(), 2);
818
819        Ok(())
820    }
821
822    #[test]
823    fn test_ffi_table_provider_statistics_round_trip() -> Result<()> {
824        use arrow::datatypes::{DataType, Field};
825        use datafusion::arrow::array::Int32Array;
826        use datafusion::arrow::record_batch::RecordBatch;
827        use datafusion::datasource::MemTable;
828        use datafusion_common::stats::Precision;
829        use datafusion_common::{ColumnStatistics, ScalarValue};
830
831        // A thin wrapper that lets us inject statistics onto any TableProvider.
832        #[derive(Debug)]
833        struct TableWithStats {
834            inner: Arc<dyn TableProvider>,
835            stats: Option<Statistics>,
836        }
837
838        #[async_trait]
839        impl TableProvider for TableWithStats {
840            fn schema(&self) -> SchemaRef {
841                self.inner.schema()
842            }
843            fn table_type(&self) -> TableType {
844                self.inner.table_type()
845            }
846            fn statistics(&self) -> Option<Statistics> {
847                self.stats.clone()
848            }
849            async fn scan(
850                &self,
851                session: &dyn Session,
852                projection: Option<&Vec<usize>>,
853                filters: &[Expr],
854                limit: Option<usize>,
855            ) -> Result<Arc<dyn ExecutionPlan>> {
856                self.inner.scan(session, projection, filters, limit).await
857            }
858        }
859
860        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
861
862        let batch = RecordBatch::try_new(
863            Arc::clone(&schema),
864            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
865        )?;
866
867        let ctx = Arc::new(SessionContext::new());
868        let task_ctx_provider = Arc::clone(&ctx) as Arc<dyn TaskContextProvider>;
869        let task_ctx_provider = FFI_TaskContextProvider::from(&task_ctx_provider);
870
871        // Provider without statistics should cross the boundary as None.
872        let no_stats_inner = Arc::new(MemTable::try_new(
873            Arc::clone(&schema),
874            vec![vec![batch.clone()]],
875        )?);
876        let no_stats_provider = Arc::new(TableWithStats {
877            inner: no_stats_inner,
878            stats: None,
879        });
880        let mut ffi_provider = FFI_TableProvider::new(
881            no_stats_provider,
882            true,
883            None,
884            task_ctx_provider.clone(),
885            None,
886        );
887        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
888        let foreign: Arc<dyn TableProvider> = (&ffi_provider).into();
889        assert!(foreign.statistics().is_none());
890
891        // Provider with statistics should round-trip faithfully.
892        let original_stats = Statistics {
893            num_rows: Precision::Exact(3),
894            total_byte_size: Precision::Inexact(12),
895            column_statistics: vec![ColumnStatistics {
896                null_count: Precision::Exact(0),
897                max_value: Precision::Exact(ScalarValue::Int32(Some(3))),
898                min_value: Precision::Exact(ScalarValue::Int32(Some(1))),
899                sum_value: Precision::Exact(ScalarValue::Int64(Some(6))),
900                distinct_count: Precision::Exact(3),
901                byte_size: Precision::Exact(12),
902            }],
903        };
904        let stats_inner =
905            Arc::new(MemTable::try_new(Arc::clone(&schema), vec![vec![batch]])?);
906        let stats_provider = Arc::new(TableWithStats {
907            inner: stats_inner,
908            stats: Some(original_stats.clone()),
909        });
910        let mut ffi_provider =
911            FFI_TableProvider::new(stats_provider, true, None, task_ctx_provider, None);
912        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
913        let foreign: Arc<dyn TableProvider> = (&ffi_provider).into();
914        assert_eq!(foreign.statistics().as_ref(), Some(&original_stats));
915
916        Ok(())
917    }
918
919    /// Re-wrapping an imported provider with a rebuilt logical codec must adopt
920    /// that codec. See <https://github.com/apache/datafusion/issues/24722>.
921    #[test]
922    fn test_rebind_foreign_table_provider_adopts_logical_codec() -> Result<()> {
923        let (_ctx_a, provider_a) = crate::util::tests::test_session_and_ctx();
924        let (ctx_b, provider_b) = crate::util::tests::test_session_and_ctx();
925
926        let mut ffi_provider = FFI_TableProvider::new(
927            create_test_table_provider()?,
928            true,
929            None,
930            provider_a,
931            None,
932        );
933        ffi_provider.library_marker_id = crate::mock_foreign_marker_id;
934
935        let imported: Arc<dyn TableProvider> = (&ffi_provider).into();
936        assert!(imported.downcast_ref::<ForeignTableProvider>().is_some());
937
938        // Rebuild the codec against session B and re-wrap.
939        let codec_b = FFI_LogicalExtensionCodec::new(
940            Arc::new(DefaultLogicalExtensionCodec {}),
941            None,
942            provider_b,
943        );
944        let rebound =
945            FFI_TableProvider::new_with_ffi_codec(imported, true, None, codec_b);
946
947        let task_ctx: Arc<TaskContext> = (&rebound.logical_codec.task_ctx_provider)
948            .try_into()
949            .expect("rebound provider's codec resolves");
950        assert_eq!(task_ctx.session_id(), ctx_b.task_ctx().session_id());
951
952        Ok(())
953    }
954}