datafusion_python_util/
lib.rs1use std::future::Future;
19use std::ptr::NonNull;
20use std::sync::{Arc, OnceLock};
21use std::time::Duration;
22
23use datafusion::datasource::TableProvider;
24use datafusion::execution::TaskContext;
25use datafusion::execution::context::SessionContext;
26use datafusion::logical_expr::Volatility;
27use datafusion::physical_optimizer::PhysicalOptimizerRule;
28use datafusion_ffi::execution::FFI_TaskContextProvider;
29use datafusion_ffi::physical_optimizer::FFI_PhysicalOptimizerRule;
30use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
31use datafusion_ffi::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
32use datafusion_ffi::table_provider::FFI_TableProvider;
33use datafusion_proto::physical_plan::PhysicalExtensionCodec;
34use pyo3::exceptions::{PyImportError, PyTypeError, PyValueError};
35use pyo3::prelude::*;
36use pyo3::types::{PyCapsule, PyType};
37use tokio::runtime::Runtime;
38use tokio::task::JoinHandle;
39use tokio::time::sleep;
40
41pub mod errors;
42pub use crate::errors::to_datafusion_err;
43use crate::errors::{PyDataFusionError, PyDataFusionResult};
44
45#[inline]
47pub fn get_tokio_runtime() -> &'static Runtime {
48 static RUNTIME: OnceLock<Runtime> = OnceLock::new();
54 RUNTIME.get_or_init(|| Runtime::new().unwrap())
55}
56
57#[inline]
58pub fn is_ipython_env(py: Python) -> &'static bool {
59 static IS_IPYTHON_ENV: OnceLock<bool> = OnceLock::new();
60 IS_IPYTHON_ENV.get_or_init(|| {
61 py.import("IPython")
62 .and_then(|ipython| ipython.call_method0("get_ipython"))
63 .map(|ipython| !ipython.is_none())
64 .unwrap_or(false)
65 })
66}
67
68#[inline]
70pub fn get_global_ctx() -> &'static Arc<SessionContext> {
71 static CTX: OnceLock<Arc<SessionContext>> = OnceLock::new();
72 CTX.get_or_init(|| Arc::new(SessionContext::new()))
73}
74
75pub fn wait_for_future<F>(py: Python, fut: F) -> PyResult<F::Output>
80where
81 F: Future + Send,
82 F::Output: Send,
83{
84 let runtime: &Runtime = get_tokio_runtime();
85 const INTERVAL_CHECK_SIGNALS: Duration = Duration::from_millis(1_000);
86
87 py.run(cr"pass", None, None)?;
90 py.check_signals()?;
91
92 py.detach(|| {
93 runtime.block_on(async {
94 tokio::pin!(fut);
95 loop {
96 tokio::select! {
97 res = &mut fut => break Ok(res),
98 _ = sleep(INTERVAL_CHECK_SIGNALS) => {
99 Python::attach(|py| {
100 py.run(cr"pass", None, None)?;
107 py.check_signals()
108 })?;
109 }
110 }
111 }
112 })
113 })
114}
115
116pub fn spawn_future<F, T>(py: Python, fut: F) -> PyDataFusionResult<T>
119where
120 F: Future<Output = datafusion::common::Result<T>> + Send + 'static,
121 T: Send + 'static,
122{
123 let rt = get_tokio_runtime();
124 let handle: JoinHandle<datafusion::common::Result<T>> = rt.spawn(fut);
125 let inner_result = wait_for_future(py, async {
130 match handle.await {
134 Ok(inner) => inner,
135 Err(join_err) => Err(to_datafusion_err(join_err)),
136 }
137 })?; Ok(inner_result?)
143}
144
145pub fn parse_volatility(value: &str) -> PyDataFusionResult<Volatility> {
146 Ok(match value {
147 "immutable" => Volatility::Immutable,
148 "stable" => Volatility::Stable,
149 "volatile" => Volatility::Volatile,
150 value => {
151 return Err(PyDataFusionError::Common(format!(
152 "Unsupported volatility type: `{value}`, supported \
153 values are: immutable, stable and volatile."
154 )));
155 }
156 })
157}
158
159pub fn validate_pycapsule(capsule: &Bound<PyCapsule>, name: &str) -> PyResult<()> {
160 let capsule_name = capsule.name()?;
161 if capsule_name.is_none() {
162 return Err(PyValueError::new_err(format!(
163 "Expected {name} PyCapsule to have name set."
164 )));
165 }
166
167 let capsule_name = unsafe { capsule_name.unwrap().as_cstr().to_str()? };
168 if capsule_name != name {
169 return Err(PyValueError::new_err(format!(
170 "Expected name '{name}' in PyCapsule, instead got '{capsule_name}'"
171 )));
172 }
173
174 Ok(())
175}
176
177pub fn table_provider_from_pycapsule<'py>(
178 mut obj: Bound<'py, PyAny>,
179 session: Bound<'py, PyAny>,
180) -> PyResult<Option<Arc<dyn TableProvider>>> {
181 if obj.hasattr("__datafusion_table_provider__")? {
182 obj = obj
183 .getattr("__datafusion_table_provider__")?
184 .call1((session,)).map_err(|err| {
185 let py = obj.py();
186 if err.get_type(py).is(PyType::new::<PyTypeError>(py)) {
187 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.")
188 } else {
189 err
190 }
191 })?;
192 }
193
194 if let Ok(capsule) = obj.cast::<PyCapsule>() {
195 let data: NonNull<FFI_TableProvider> = capsule
196 .pointer_checked(Some(c"datafusion_table_provider"))?
197 .cast();
198 let provider = unsafe { data.as_ref() };
199 let provider: Arc<dyn TableProvider> = provider.into();
200
201 Ok(Some(provider))
202 } else {
203 Ok(None)
204 }
205}
206
207pub fn create_logical_extension_capsule<'py>(
208 py: Python<'py>,
209 codec: &FFI_LogicalExtensionCodec,
210) -> PyResult<Bound<'py, PyCapsule>> {
211 let name = cr"datafusion_logical_extension_codec".into();
212 let codec = codec.clone();
213
214 PyCapsule::new(py, codec, Some(name))
215}
216
217pub fn ffi_logical_codec_from_pycapsule(obj: Bound<PyAny>) -> PyResult<FFI_LogicalExtensionCodec> {
218 let attr_name = "__datafusion_logical_extension_codec__";
219 let capsule = if obj.hasattr(attr_name)? {
220 obj.getattr(attr_name)?.call0()?
221 } else {
222 obj
223 };
224
225 let capsule = capsule.cast::<PyCapsule>()?;
226 let data: NonNull<FFI_LogicalExtensionCodec> = capsule
227 .pointer_checked(Some(c"datafusion_logical_extension_codec"))?
228 .cast();
229 let codec = unsafe { data.as_ref() };
230
231 Ok(codec.clone())
232}
233
234pub fn create_physical_extension_capsule<'py>(
235 py: Python<'py>,
236 codec: &FFI_PhysicalExtensionCodec,
237) -> PyResult<Bound<'py, PyCapsule>> {
238 let name = cr"datafusion_physical_extension_codec".into();
239 let codec = codec.clone();
240
241 PyCapsule::new(py, codec, Some(name))
242}
243
244#[macro_export]
252macro_rules! from_pycapsule {
253 ($fn_name:ident, $capsule_name:literal, $ffi_type:ty, $output_type:ty) => {
254 pub fn $fn_name(
255 obj: &$crate::pyo3::Bound<$crate::pyo3::PyAny>,
256 ) -> $crate::pyo3::PyResult<std::sync::Arc<$output_type>> {
257 use $crate::pyo3::prelude::*;
258 use $crate::pyo3::types::PyCapsule;
259
260 let mut obj = obj.clone();
261 if obj.hasattr(concat!("__", $capsule_name, "__"))? {
262 obj = obj.getattr(concat!("__", $capsule_name, "__"))?.call0()?;
263 }
264 let capsule = obj.cast::<PyCapsule>().map_err(|_| {
265 $crate::errors::py_datafusion_err(concat!(
266 "Invalid ",
267 $capsule_name,
268 ". Does not contain PyCapsule object."
269 ))
270 })?;
271 $crate::validate_pycapsule(&capsule, $capsule_name)?;
272
273 let expected_name = std::ffi::CString::new($capsule_name)
274 .expect("capsule name must not contain interior NUL bytes");
275 let data: std::ptr::NonNull<$ffi_type> = capsule
276 .pointer_checked(Some(expected_name.as_c_str()))?
277 .cast();
278 let output_obj = unsafe { data.as_ref() };
279 let output_obj: std::sync::Arc<$output_type> = output_obj.into();
280
281 Ok(output_obj)
282 }
283 };
284}
285
286#[macro_export]
289macro_rules! try_from_pycapsule {
290 ($fn_name:ident, $capsule_name:literal, $ffi_type:ty, $output_type:ty) => {
291 pub fn $fn_name(
292 obj: &$crate::pyo3::Bound<$crate::pyo3::PyAny>,
293 ) -> $crate::pyo3::PyResult<std::sync::Arc<$output_type>> {
294 use $crate::pyo3::prelude::*;
295 use $crate::pyo3::types::PyCapsule;
296
297 let mut obj = obj.clone();
298 if obj.hasattr(concat!("__", $capsule_name, "__"))? {
299 obj = obj.getattr(concat!("__", $capsule_name, "__"))?.call0()?;
300 }
301 let capsule = obj.cast::<PyCapsule>().map_err(|_| {
302 $crate::errors::py_datafusion_err(concat!(
303 "Invalid ",
304 $capsule_name,
305 ". Does not contain PyCapsule object."
306 ))
307 })?;
308 $crate::validate_pycapsule(&capsule, $capsule_name)?;
309
310 let expected_name = std::ffi::CString::new($capsule_name)
311 .expect("capsule name must not contain interior NUL bytes");
312 let data: std::ptr::NonNull<$ffi_type> = capsule
313 .pointer_checked(Some(expected_name.as_c_str()))?
314 .cast();
315 let output_obj = unsafe { data.as_ref() };
316 let output_obj: std::sync::Arc<$output_type> = output_obj
317 .try_into()
318 .map_err($crate::errors::py_datafusion_err)?;
319
320 Ok(output_obj)
321 }
322 };
323}
324
325#[doc(hidden)]
328pub use pyo3;
329
330from_pycapsule!(
331 physical_codec_from_pycapsule,
332 "datafusion_physical_extension_codec",
333 FFI_PhysicalExtensionCodec,
334 dyn PhysicalExtensionCodec
335);
336
337from_pycapsule!(
338 physical_optimizer_rule_from_pycapsule,
339 "datafusion_physical_optimizer_rule",
340 FFI_PhysicalOptimizerRule,
341 dyn PhysicalOptimizerRule + Send + Sync
342);
343
344try_from_pycapsule!(
345 task_context_from_pycapsule,
346 "datafusion_task_context_provider",
347 FFI_TaskContextProvider,
348 TaskContext
349);