blob: 890511997a7061396f0b6d6b9e825b84e68e9b7b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::{any::Any, ffi::c_void, sync::Arc};
use abi_stable::{
std_types::{ROption, RResult, RString, RVec},
StableAbi,
};
use arrow::datatypes::SchemaRef;
use async_ffi::{FfiFuture, FutureExt};
use async_trait::async_trait;
use datafusion::{
catalog::{Session, TableProvider},
datasource::TableType,
error::DataFusionError,
execution::{session_state::SessionStateBuilder, TaskContext},
logical_expr::{logical_plan::dml::InsertOp, TableProviderFilterPushDown},
physical_plan::ExecutionPlan,
prelude::{Expr, SessionContext},
};
use datafusion_proto::{
logical_plan::{
from_proto::parse_exprs, to_proto::serialize_exprs, DefaultLogicalExtensionCodec,
},
protobuf::LogicalExprList,
};
use prost::Message;
use tokio::runtime::Handle;
use crate::{
arrow_wrappers::WrappedSchema,
df_result, rresult_return,
session_config::ForeignSessionConfig,
table_source::{FFI_TableProviderFilterPushDown, FFI_TableType},
};
use super::{
execution_plan::{FFI_ExecutionPlan, ForeignExecutionPlan},
insert_op::FFI_InsertOp,
session_config::FFI_SessionConfig,
};
use datafusion::error::Result;
/// A stable struct for sharing [`TableProvider`] across FFI boundaries.
///
/// # Struct Layout
///
/// The following description applies to all structs provided in this crate.
///
/// Each of the exposed structs in this crate is provided with a variant prefixed
/// with `Foreign`. This variant is designed to be used by the consumer of the
/// foreign code. The `Foreign` structs should _never_ access the `private_data`
/// fields. Instead they should only access the data returned through the function
/// calls defined on the `FFI_` structs. The second purpose of the `Foreign`
/// structs is to contain additional data that may be needed by the traits that
/// are implemented on them. Some of these traits require borrowing data which
/// can be far more convenient to be locally stored.
///
/// For example, we have a struct `FFI_TableProvider` to give access to the
/// `TableProvider` functions like `table_type()` and `scan()`. If we write a
/// library that wishes to expose it's `TableProvider`, then we can access the
/// private data that contains the Arc reference to the `TableProvider` via
/// `FFI_TableProvider`. This data is local to the library.
///
/// If we have a program that accesses a `TableProvider` via FFI, then it
/// will use `ForeignTableProvider`. When using `ForeignTableProvider` we **must**
/// not attempt to access the `private_data` field in `FFI_TableProvider`. If a
/// user is testing locally, you may be able to successfully access this field, but
/// it will only work if you are building against the exact same version of
/// `DataFusion` for both libraries **and** the same compiler. It will not work
/// in general.
///
/// It is worth noting that which library is the `local` and which is `foreign`
/// depends on which interface we are considering. For example, suppose we have a
/// Python library called `my_provider` that exposes a `TableProvider` called
/// `MyProvider` via `FFI_TableProvider`. Within the library `my_provider` we can
/// access the `private_data` via `FFI_TableProvider`. We connect this to
/// `datafusion-python`, where we access it as a `ForeignTableProvider`. Now when
/// we call `scan()` on this interface, we have to pass it a `FFI_SessionConfig`.
/// The `SessionConfig` is local to `datafusion-python` and **not** `my_provider`.
/// It is important to be careful when expanding these functions to be certain which
/// side of the interface each object refers to.
#[repr(C)]
#[derive(Debug, StableAbi)]
#[allow(non_camel_case_types)]
pub struct FFI_TableProvider {
/// Return the table schema
pub schema: unsafe extern "C" fn(provider: &Self) -> WrappedSchema,
/// Perform a scan on the table. See [`TableProvider`] for detailed usage information.
///
/// # Arguments
///
/// * `provider` - the table provider
/// * `session_config` - session configuration
/// * `projections` - if specified, only a subset of the columns are returned
/// * `filters_serialized` - filters to apply to the scan, which are a
/// [`LogicalExprList`] protobuf message serialized into bytes to pass
/// across the FFI boundary.
/// * `limit` - if specified, limit the number of rows returned
pub scan: unsafe extern "C" fn(
provider: &Self,
session_config: &FFI_SessionConfig,
projections: RVec<usize>,
filters_serialized: RVec<u8>,
limit: ROption<usize>,
) -> FfiFuture<RResult<FFI_ExecutionPlan, RString>>,
/// Return the type of table. See [`TableType`] for options.
pub table_type: unsafe extern "C" fn(provider: &Self) -> FFI_TableType,
/// Based upon the input filters, identify which are supported. The filters
/// are a [`LogicalExprList`] protobuf message serialized into bytes to pass
/// across the FFI boundary.
pub supports_filters_pushdown: Option<
unsafe extern "C" fn(
provider: &FFI_TableProvider,
filters_serialized: RVec<u8>,
)
-> RResult<RVec<FFI_TableProviderFilterPushDown>, RString>,
>,
pub insert_into:
unsafe extern "C" fn(
provider: &Self,
session_config: &FFI_SessionConfig,
input: &FFI_ExecutionPlan,
insert_op: FFI_InsertOp,
) -> FfiFuture<RResult<FFI_ExecutionPlan, RString>>,
/// Used to create a clone on the provider of the execution plan. This should
/// only need to be called by the receiver of the plan.
pub clone: unsafe extern "C" fn(plan: &Self) -> Self,
/// Release the memory of the private data when it is no longer being used.
pub release: unsafe extern "C" fn(arg: &mut Self),
/// Return the major DataFusion version number of this provider.
pub version: unsafe extern "C" fn() -> u64,
/// Internal data. This is only to be accessed by the provider of the plan.
/// A [`ForeignExecutionPlan`] should never attempt to access this data.
pub private_data: *mut c_void,
}
unsafe impl Send for FFI_TableProvider {}
unsafe impl Sync for FFI_TableProvider {}
struct ProviderPrivateData {
provider: Arc<dyn TableProvider + Send>,
runtime: Option<Handle>,
}
unsafe extern "C" fn schema_fn_wrapper(provider: &FFI_TableProvider) -> WrappedSchema {
let private_data = provider.private_data as *const ProviderPrivateData;
let provider = &(*private_data).provider;
provider.schema().into()
}
unsafe extern "C" fn table_type_fn_wrapper(
provider: &FFI_TableProvider,
) -> FFI_TableType {
let private_data = provider.private_data as *const ProviderPrivateData;
let provider = &(*private_data).provider;
provider.table_type().into()
}
fn supports_filters_pushdown_internal(
provider: &Arc<dyn TableProvider + Send>,
filters_serialized: &[u8],
) -> Result<RVec<FFI_TableProviderFilterPushDown>> {
let default_ctx = SessionContext::new();
let codec = DefaultLogicalExtensionCodec {};
let filters = match filters_serialized.is_empty() {
true => vec![],
false => {
let proto_filters = LogicalExprList::decode(filters_serialized)
.map_err(|e| DataFusionError::Plan(e.to_string()))?;
parse_exprs(proto_filters.expr.iter(), &default_ctx, &codec)?
}
};
let filters_borrowed: Vec<&Expr> = filters.iter().collect();
let results: RVec<_> = provider
.supports_filters_pushdown(&filters_borrowed)?
.iter()
.map(|v| v.into())
.collect();
Ok(results)
}
unsafe extern "C" fn supports_filters_pushdown_fn_wrapper(
provider: &FFI_TableProvider,
filters_serialized: RVec<u8>,
) -> RResult<RVec<FFI_TableProviderFilterPushDown>, RString> {
let private_data = provider.private_data as *const ProviderPrivateData;
let provider = &(*private_data).provider;
supports_filters_pushdown_internal(provider, &filters_serialized)
.map_err(|e| e.to_string().into())
.into()
}
unsafe extern "C" fn scan_fn_wrapper(
provider: &FFI_TableProvider,
session_config: &FFI_SessionConfig,
projections: RVec<usize>,
filters_serialized: RVec<u8>,
limit: ROption<usize>,
) -> FfiFuture<RResult<FFI_ExecutionPlan, RString>> {
let private_data = provider.private_data as *mut ProviderPrivateData;
let internal_provider = &(*private_data).provider;
let session_config = session_config.clone();
let runtime = &(*private_data).runtime;
async move {
let config = rresult_return!(ForeignSessionConfig::try_from(&session_config));
let session = SessionStateBuilder::new()
.with_default_features()
.with_config(config.0)
.build();
let ctx = SessionContext::new_with_state(session);
let filters = match filters_serialized.is_empty() {
true => vec![],
false => {
let default_ctx = SessionContext::new();
let codec = DefaultLogicalExtensionCodec {};
let proto_filters =
rresult_return!(LogicalExprList::decode(filters_serialized.as_ref()));
rresult_return!(parse_exprs(
proto_filters.expr.iter(),
&default_ctx,
&codec
))
}
};
let projections: Vec<_> = projections.into_iter().collect();
let plan = rresult_return!(
internal_provider
.scan(&ctx.state(), Some(&projections), &filters, limit.into())
.await
);
RResult::ROk(FFI_ExecutionPlan::new(
plan,
ctx.task_ctx(),
runtime.clone(),
))
}
.into_ffi()
}
unsafe extern "C" fn insert_into_fn_wrapper(
provider: &FFI_TableProvider,
session_config: &FFI_SessionConfig,
input: &FFI_ExecutionPlan,
insert_op: FFI_InsertOp,
) -> FfiFuture<RResult<FFI_ExecutionPlan, RString>> {
let private_data = provider.private_data as *mut ProviderPrivateData;
let internal_provider = &(*private_data).provider;
let session_config = session_config.clone();
let input = input.clone();
let runtime = &(*private_data).runtime;
async move {
let config = rresult_return!(ForeignSessionConfig::try_from(&session_config));
let session = SessionStateBuilder::new()
.with_default_features()
.with_config(config.0)
.build();
let ctx = SessionContext::new_with_state(session);
let input = rresult_return!(ForeignExecutionPlan::try_from(&input).map(Arc::new));
let insert_op = InsertOp::from(insert_op);
let plan = rresult_return!(
internal_provider
.insert_into(&ctx.state(), input, insert_op)
.await
);
RResult::ROk(FFI_ExecutionPlan::new(
plan,
ctx.task_ctx(),
runtime.clone(),
))
}
.into_ffi()
}
unsafe extern "C" fn release_fn_wrapper(provider: &mut FFI_TableProvider) {
let private_data = Box::from_raw(provider.private_data as *mut ProviderPrivateData);
drop(private_data);
}
unsafe extern "C" fn clone_fn_wrapper(provider: &FFI_TableProvider) -> FFI_TableProvider {
let old_private_data = provider.private_data as *const ProviderPrivateData;
let runtime = (*old_private_data).runtime.clone();
let private_data = Box::into_raw(Box::new(ProviderPrivateData {
provider: Arc::clone(&(*old_private_data).provider),
runtime,
})) as *mut c_void;
FFI_TableProvider {
schema: schema_fn_wrapper,
scan: scan_fn_wrapper,
table_type: table_type_fn_wrapper,
supports_filters_pushdown: provider.supports_filters_pushdown,
insert_into: provider.insert_into,
clone: clone_fn_wrapper,
release: release_fn_wrapper,
version: super::version,
private_data,
}
}
impl Drop for FFI_TableProvider {
fn drop(&mut self) {
unsafe { (self.release)(self) }
}
}
impl FFI_TableProvider {
/// Creates a new [`FFI_TableProvider`].
pub fn new(
provider: Arc<dyn TableProvider + Send>,
can_support_pushdown_filters: bool,
runtime: Option<Handle>,
) -> Self {
let private_data = Box::new(ProviderPrivateData { provider, runtime });
Self {
schema: schema_fn_wrapper,
scan: scan_fn_wrapper,
table_type: table_type_fn_wrapper,
supports_filters_pushdown: match can_support_pushdown_filters {
true => Some(supports_filters_pushdown_fn_wrapper),
false => None,
},
insert_into: insert_into_fn_wrapper,
clone: clone_fn_wrapper,
release: release_fn_wrapper,
version: super::version,
private_data: Box::into_raw(private_data) as *mut c_void,
}
}
}
/// This wrapper struct exists on the receiver side of the FFI interface, so it has
/// no guarantees about being able to access the data in `private_data`. Any functions
/// defined on this struct must only use the stable functions provided in
/// FFI_TableProvider to interact with the foreign table provider.
#[derive(Debug)]
pub struct ForeignTableProvider(pub FFI_TableProvider);
unsafe impl Send for ForeignTableProvider {}
unsafe impl Sync for ForeignTableProvider {}
impl From<&FFI_TableProvider> for ForeignTableProvider {
fn from(provider: &FFI_TableProvider) -> Self {
Self(provider.clone())
}
}
impl Clone for FFI_TableProvider {
fn clone(&self) -> Self {
unsafe { (self.clone)(self) }
}
}
#[async_trait]
impl TableProvider for ForeignTableProvider {
fn as_any(&self) -> &dyn Any {
self
}
fn schema(&self) -> SchemaRef {
let wrapped_schema = unsafe { (self.0.schema)(&self.0) };
wrapped_schema.into()
}
fn table_type(&self) -> TableType {
unsafe { (self.0.table_type)(&self.0).into() }
}
async fn scan(
&self,
session: &dyn Session,
projection: Option<&Vec<usize>>,
filters: &[Expr],
limit: Option<usize>,
) -> Result<Arc<dyn ExecutionPlan>> {
let session_config: FFI_SessionConfig = session.config().into();
let projections: Option<RVec<usize>> =
projection.map(|p| p.iter().map(|v| v.to_owned()).collect());
let codec = DefaultLogicalExtensionCodec {};
let filter_list = LogicalExprList {
expr: serialize_exprs(filters, &codec)?,
};
let filters_serialized = filter_list.encode_to_vec().into();
let plan = unsafe {
let maybe_plan = (self.0.scan)(
&self.0,
&session_config,
projections.unwrap_or_default(),
filters_serialized,
limit.into(),
)
.await;
ForeignExecutionPlan::try_from(&df_result!(maybe_plan)?)?
};
Ok(Arc::new(plan))
}
/// Tests whether the table provider can make use of a filter expression
/// to optimize data retrieval.
fn supports_filters_pushdown(
&self,
filters: &[&Expr],
) -> Result<Vec<TableProviderFilterPushDown>> {
unsafe {
let pushdown_fn = match self.0.supports_filters_pushdown {
Some(func) => func,
None => {
return Ok(vec![
TableProviderFilterPushDown::Unsupported;
filters.len()
])
}
};
let codec = DefaultLogicalExtensionCodec {};
let expr_list = LogicalExprList {
expr: serialize_exprs(filters.iter().map(|f| f.to_owned()), &codec)?,
};
let serialized_filters = expr_list.encode_to_vec();
let pushdowns = df_result!(pushdown_fn(&self.0, serialized_filters.into()))?;
Ok(pushdowns.iter().map(|v| v.into()).collect())
}
}
async fn insert_into(
&self,
session: &dyn Session,
input: Arc<dyn ExecutionPlan>,
insert_op: InsertOp,
) -> Result<Arc<dyn ExecutionPlan>> {
let session_config: FFI_SessionConfig = session.config().into();
let rc = Handle::try_current().ok();
let input =
FFI_ExecutionPlan::new(input, Arc::new(TaskContext::from(session)), rc);
let insert_op: FFI_InsertOp = insert_op.into();
let plan = unsafe {
let maybe_plan =
(self.0.insert_into)(&self.0, &session_config, &input, insert_op).await;
ForeignExecutionPlan::try_from(&df_result!(maybe_plan)?)?
};
Ok(Arc::new(plan))
}
}
#[cfg(test)]
mod tests {
use arrow::datatypes::Schema;
use datafusion::prelude::{col, lit};
use super::*;
#[tokio::test]
async fn test_round_trip_ffi_table_provider_scan() -> Result<()> {
use arrow::datatypes::Field;
use datafusion::arrow::{
array::Float32Array, datatypes::DataType, record_batch::RecordBatch,
};
use datafusion::datasource::MemTable;
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)]));
// define data in two partitions
let batch1 = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))],
)?;
let batch2 = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Float32Array::from(vec![64.0]))],
)?;
let ctx = SessionContext::new();
let provider =
Arc::new(MemTable::try_new(schema, vec![vec![batch1], vec![batch2]])?);
let ffi_provider = FFI_TableProvider::new(provider, true, None);
let foreign_table_provider: ForeignTableProvider = (&ffi_provider).into();
ctx.register_table("t", Arc::new(foreign_table_provider))?;
let df = ctx.table("t").await?;
df.select(vec![col("a")])?
.filter(col("a").gt(lit(3.0)))?
.show()
.await?;
Ok(())
}
#[tokio::test]
async fn test_round_trip_ffi_table_provider_insert_into() -> Result<()> {
use arrow::datatypes::Field;
use datafusion::arrow::{
array::Float32Array, datatypes::DataType, record_batch::RecordBatch,
};
use datafusion::datasource::MemTable;
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)]));
// define data in two partitions
let batch1 = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))],
)?;
let batch2 = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Float32Array::from(vec![64.0]))],
)?;
let ctx = SessionContext::new();
let provider =
Arc::new(MemTable::try_new(schema, vec![vec![batch1], vec![batch2]])?);
let ffi_provider = FFI_TableProvider::new(provider, true, None);
let foreign_table_provider: ForeignTableProvider = (&ffi_provider).into();
ctx.register_table("t", Arc::new(foreign_table_provider))?;
let result = ctx
.sql("INSERT INTO t VALUES (128.0);")
.await?
.collect()
.await?;
assert!(result.len() == 1 && result[0].num_rows() == 1);
ctx.table("t")
.await?
.select(vec![col("a")])?
.filter(col("a").gt(lit(3.0)))?
.show()
.await?;
Ok(())
}
#[tokio::test]
async fn test_aggregation() -> Result<()> {
use arrow::datatypes::Field;
use datafusion::arrow::{
array::Float32Array, datatypes::DataType, record_batch::RecordBatch,
};
use datafusion::common::assert_batches_eq;
use datafusion::datasource::MemTable;
let schema =
Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)]));
// define data in two partitions
let batch1 = RecordBatch::try_new(
Arc::clone(&schema),
vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))],
)?;
let ctx = SessionContext::new();
let provider = Arc::new(MemTable::try_new(schema, vec![vec![batch1]])?);
let ffi_provider = FFI_TableProvider::new(provider, true, None);
let foreign_table_provider: ForeignTableProvider = (&ffi_provider).into();
ctx.register_table("t", Arc::new(foreign_table_provider))?;
let result = ctx
.sql("SELECT COUNT(*) as cnt FROM t")
.await?
.collect()
.await?;
#[rustfmt::skip]
let expected = [
"+-----+",
"| cnt |",
"+-----+",
"| 3 |",
"+-----+"
];
assert_batches_eq!(expected, &result);
Ok(())
}
}