use std::future::Future;
use std::ptr::NonNull;
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use datafusion::datasource::TableProvider;
use datafusion::execution::TaskContext;
use datafusion::execution::context::SessionContext;
use datafusion::logical_expr::Volatility;
use datafusion::physical_optimizer::PhysicalOptimizerRule;
use datafusion_ffi::execution::FFI_TaskContextProvider;
use datafusion_ffi::physical_optimizer::FFI_PhysicalOptimizerRule;
use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
use datafusion_ffi::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
use datafusion_ffi::table_provider::FFI_TableProvider;
use datafusion_proto::physical_plan::PhysicalExtensionCodec;
use pyo3::exceptions::{PyImportError, PyTypeError, PyValueError};
use pyo3::prelude::*;
use pyo3::types::{PyCapsule, PyType};
use tokio::runtime::Runtime;
use tokio::task::JoinHandle;
use tokio::time::sleep;
pub mod errors;
pub use crate::errors::to_datafusion_err;
use crate::errors::{PyDataFusionError, PyDataFusionResult};
#[inline]
pub fn get_tokio_runtime() -> &'static Runtime {
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
RUNTIME.get_or_init(|| Runtime::new().unwrap())
}
#[inline]
pub fn is_ipython_env(py: Python) -> &'static bool {
static IS_IPYTHON_ENV: OnceLock<bool> = OnceLock::new();
IS_IPYTHON_ENV.get_or_init(|| {
py.import("IPython")
.and_then(|ipython| ipython.call_method0("get_ipython"))
.map(|ipython| !ipython.is_none())
.unwrap_or(false)
})
}
#[inline]
pub fn get_global_ctx() -> &'static Arc<SessionContext> {
static CTX: OnceLock<Arc<SessionContext>> = OnceLock::new();
CTX.get_or_init(|| Arc::new(SessionContext::new()))
}
pub fn wait_for_future<F>(py: Python, fut: F) -> PyResult<F::Output>
where
F: Future + Send,
F::Output: Send,
{
let runtime: &Runtime = get_tokio_runtime();
const INTERVAL_CHECK_SIGNALS: Duration = Duration::from_millis(1_000);
py.run(cr"pass", None, None)?;
py.check_signals()?;
py.detach(|| {
runtime.block_on(async {
tokio::pin!(fut);
loop {
tokio::select! {
res = &mut fut => break Ok(res),
_ = sleep(INTERVAL_CHECK_SIGNALS) => {
Python::attach(|py| {
py.run(cr"pass", None, None)?;
py.check_signals()
})?;
}
}
}
})
})
}
pub fn spawn_future<F, T>(py: Python, fut: F) -> PyDataFusionResult<T>
where
F: Future<Output = datafusion::common::Result<T>> + Send + 'static,
T: Send + 'static,
{
let rt = get_tokio_runtime();
let handle: JoinHandle<datafusion::common::Result<T>> = rt.spawn(fut);
let inner_result = wait_for_future(py, async {
match handle.await {
Ok(inner) => inner,
Err(join_err) => Err(to_datafusion_err(join_err)),
}
})?;
Ok(inner_result?)
}
pub fn parse_volatility(value: &str) -> PyDataFusionResult<Volatility> {
Ok(match value {
"immutable" => Volatility::Immutable,
"stable" => Volatility::Stable,
"volatile" => Volatility::Volatile,
value => {
return Err(PyDataFusionError::Common(format!(
"Unsupported volatility type: `{value}`, supported \
values are: immutable, stable and volatile."
)));
}
})
}
pub fn validate_pycapsule(capsule: &Bound<PyCapsule>, name: &str) -> PyResult<()> {
let capsule_name = capsule.name()?;
if capsule_name.is_none() {
return Err(PyValueError::new_err(format!(
"Expected {name} PyCapsule to have name set."
)));
}
let capsule_name = unsafe { capsule_name.unwrap().as_cstr().to_str()? };
if capsule_name != name {
return Err(PyValueError::new_err(format!(
"Expected name '{name}' in PyCapsule, instead got '{capsule_name}'"
)));
}
Ok(())
}
pub fn table_provider_from_pycapsule<'py>(
mut obj: Bound<'py, PyAny>,
session: Bound<'py, PyAny>,
) -> PyResult<Option<Arc<dyn TableProvider>>> {
if obj.hasattr("__datafusion_table_provider__")? {
obj = obj
.getattr("__datafusion_table_provider__")?
.call1((session,)).map_err(|err| {
let py = obj.py();
if err.get_type(py).is(PyType::new::<PyTypeError>(py)) {
PyImportError::new_err("Incompatible libraries. DataFusion 52.0.0 introduced an incompatible signature change for table providers. Either downgrade DataFusion or upgrade your function library.")
} else {
err
}
})?;
}
if let Ok(capsule) = obj.cast::<PyCapsule>() {
let data: NonNull<FFI_TableProvider> = capsule
.pointer_checked(Some(c"datafusion_table_provider"))?
.cast();
let provider = unsafe { data.as_ref() };
let provider: Arc<dyn TableProvider> = provider.into();
Ok(Some(provider))
} else {
Ok(None)
}
}
pub fn create_logical_extension_capsule<'py>(
py: Python<'py>,
codec: &FFI_LogicalExtensionCodec,
) -> PyResult<Bound<'py, PyCapsule>> {
let name = cr"datafusion_logical_extension_codec".into();
let codec = codec.clone();
PyCapsule::new(py, codec, Some(name))
}
pub fn ffi_logical_codec_from_pycapsule(obj: Bound<PyAny>) -> PyResult<FFI_LogicalExtensionCodec> {
let attr_name = "__datafusion_logical_extension_codec__";
let capsule = if obj.hasattr(attr_name)? {
obj.getattr(attr_name)?.call0()?
} else {
obj
};
let capsule = capsule.cast::<PyCapsule>()?;
let data: NonNull<FFI_LogicalExtensionCodec> = capsule
.pointer_checked(Some(c"datafusion_logical_extension_codec"))?
.cast();
let codec = unsafe { data.as_ref() };
Ok(codec.clone())
}
pub fn create_physical_extension_capsule<'py>(
py: Python<'py>,
codec: &FFI_PhysicalExtensionCodec,
) -> PyResult<Bound<'py, PyCapsule>> {
let name = cr"datafusion_physical_extension_codec".into();
let codec = codec.clone();
PyCapsule::new(py, codec, Some(name))
}
#[macro_export]
macro_rules! from_pycapsule {
($fn_name:ident, $capsule_name:literal, $ffi_type:ty, $output_type:ty) => {
pub fn $fn_name(
obj: &$crate::pyo3::Bound<$crate::pyo3::PyAny>,
) -> $crate::pyo3::PyResult<std::sync::Arc<$output_type>> {
use $crate::pyo3::prelude::*;
use $crate::pyo3::types::PyCapsule;
let mut obj = obj.clone();
if obj.hasattr(concat!("__", $capsule_name, "__"))? {
obj = obj.getattr(concat!("__", $capsule_name, "__"))?.call0()?;
}
let capsule = obj.cast::<PyCapsule>().map_err(|_| {
$crate::errors::py_datafusion_err(concat!(
"Invalid ",
$capsule_name,
". Does not contain PyCapsule object."
))
})?;
$crate::validate_pycapsule(&capsule, $capsule_name)?;
let expected_name = std::ffi::CString::new($capsule_name)
.expect("capsule name must not contain interior NUL bytes");
let data: std::ptr::NonNull<$ffi_type> = capsule
.pointer_checked(Some(expected_name.as_c_str()))?
.cast();
let output_obj = unsafe { data.as_ref() };
let output_obj: std::sync::Arc<$output_type> = output_obj.into();
Ok(output_obj)
}
};
}
#[macro_export]
macro_rules! try_from_pycapsule {
($fn_name:ident, $capsule_name:literal, $ffi_type:ty, $output_type:ty) => {
pub fn $fn_name(
obj: &$crate::pyo3::Bound<$crate::pyo3::PyAny>,
) -> $crate::pyo3::PyResult<std::sync::Arc<$output_type>> {
use $crate::pyo3::prelude::*;
use $crate::pyo3::types::PyCapsule;
let mut obj = obj.clone();
if obj.hasattr(concat!("__", $capsule_name, "__"))? {
obj = obj.getattr(concat!("__", $capsule_name, "__"))?.call0()?;
}
let capsule = obj.cast::<PyCapsule>().map_err(|_| {
$crate::errors::py_datafusion_err(concat!(
"Invalid ",
$capsule_name,
". Does not contain PyCapsule object."
))
})?;
$crate::validate_pycapsule(&capsule, $capsule_name)?;
let expected_name = std::ffi::CString::new($capsule_name)
.expect("capsule name must not contain interior NUL bytes");
let data: std::ptr::NonNull<$ffi_type> = capsule
.pointer_checked(Some(expected_name.as_c_str()))?
.cast();
let output_obj = unsafe { data.as_ref() };
let output_obj: std::sync::Arc<$output_type> = output_obj
.try_into()
.map_err($crate::errors::py_datafusion_err)?;
Ok(output_obj)
}
};
}
#[doc(hidden)]
pub use pyo3;
from_pycapsule!(
physical_codec_from_pycapsule,
"datafusion_physical_extension_codec",
FFI_PhysicalExtensionCodec,
dyn PhysicalExtensionCodec
);
from_pycapsule!(
physical_optimizer_rule_from_pycapsule,
"datafusion_physical_optimizer_rule",
FFI_PhysicalOptimizerRule,
dyn PhysicalOptimizerRule + Send + Sync
);
try_from_pycapsule!(
task_context_from_pycapsule,
"datafusion_task_context_provider",
FFI_TaskContextProvider,
TaskContext
);