1use std::collections::{HashMap, HashSet};
19use std::path::{Path, PathBuf};
20use std::ptr::NonNull;
21use std::str::FromStr;
22use std::sync::Arc;
23
24use arrow::array::RecordBatchReader;
25use arrow::ffi_stream::ArrowArrayStreamReader;
26use arrow::pyarrow::FromPyArrow;
27use datafusion::arrow::datatypes::{DataType, Schema, SchemaRef};
28use datafusion::arrow::pyarrow::PyArrowType;
29use datafusion::arrow::record_batch::RecordBatch;
30use datafusion::catalog::{CatalogProvider, CatalogProviderList, TableProviderFactory};
31use datafusion::common::{DFSchema, ScalarValue, TableReference, exec_err};
32use datafusion::datasource::file_format::file_compression_type::FileCompressionType;
33use datafusion::datasource::file_format::parquet::ParquetFormat;
34use datafusion::datasource::listing::{
35 ListingOptions, ListingTable, ListingTableConfig, ListingTableUrl,
36};
37use datafusion::datasource::{MemTable, TableProvider};
38use datafusion::execution::context::{
39 DataFilePaths, SQLOptions, SessionConfig, SessionContext, TaskContext,
40};
41use datafusion::execution::disk_manager::DiskManagerMode;
42use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool, UnboundedMemoryPool};
43use datafusion::execution::options::{ArrowReadOptions, ReadOptions};
44use datafusion::execution::runtime_env::RuntimeEnvBuilder;
45use datafusion::execution::session_state::SessionStateBuilder;
46use datafusion::execution::{FunctionRegistry, TaskContextProvider};
47use datafusion::prelude::{
48 AvroReadOptions, CsvReadOptions, DataFrame, JsonReadOptions, ParquetReadOptions,
49};
50use datafusion_ffi::catalog_provider::FFI_CatalogProvider;
51use datafusion_ffi::catalog_provider_list::FFI_CatalogProviderList;
52use datafusion_ffi::config::extension_options::FFI_ExtensionOptions;
53use datafusion_ffi::execution::FFI_TaskContextProvider;
54use datafusion_ffi::proto::logical_extension_codec::FFI_LogicalExtensionCodec;
55use datafusion_ffi::proto::physical_extension_codec::FFI_PhysicalExtensionCodec;
56use datafusion_ffi::table_provider_factory::FFI_TableProviderFactory;
57use datafusion_proto::logical_plan::LogicalExtensionCodec;
58use datafusion_proto::physical_plan::PhysicalExtensionCodec;
59use datafusion_python_util::{
60 create_logical_extension_capsule, create_physical_extension_capsule,
61 ffi_logical_codec_from_pycapsule, get_global_ctx, get_tokio_runtime,
62 physical_codec_from_pycapsule, physical_optimizer_rule_from_pycapsule, spawn_future,
63 wait_for_future,
64};
65use object_store::ObjectStore;
66use pyo3::IntoPyObjectExt;
67use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError};
68use pyo3::prelude::*;
69use pyo3::types::{PyCapsule, PyDict, PyList, PyTuple};
70use url::Url;
71use uuid::Uuid;
72
73use crate::catalog::{
74 PyCatalog, PyCatalogList, RustWrappedPyCatalogProvider, RustWrappedPyCatalogProviderList,
75};
76use crate::codec::{PythonLogicalCodec, PythonPhysicalCodec};
77use crate::common::data_type::PyScalarValue;
78use crate::common::df_schema::PyDFSchema;
79use crate::dataframe::PyDataFrame;
80use crate::dataset::Dataset;
81use crate::errors::{
82 PyDataFusionError, PyDataFusionResult, from_datafusion_error, py_datafusion_err,
83};
84use crate::expr::PyExpr;
85use crate::expr::sort_expr::PySortExpr;
86use crate::options::PyCsvReadOptions;
87use crate::physical_plan::PyExecutionPlan;
88use crate::record_batch::PyRecordBatchStream;
89use crate::sql::logical::PyLogicalPlan;
90use crate::sql::util::replace_placeholders_with_strings;
91use crate::store::StorageContexts;
92use crate::table::{PyTable, RustWrappedPyTableProviderFactory};
93use crate::udaf::PyAggregateUDF;
94use crate::udf::PyScalarUDF;
95use crate::udtf::PyTableFunction;
96use crate::udwf::PyWindowUDF;
97
98#[pyclass(
100 from_py_object,
101 frozen,
102 name = "SessionConfig",
103 module = "datafusion",
104 subclass
105)]
106#[derive(Clone, Default)]
107pub struct PySessionConfig {
108 pub config: SessionConfig,
109}
110
111impl From<SessionConfig> for PySessionConfig {
112 fn from(config: SessionConfig) -> Self {
113 Self { config }
114 }
115}
116
117#[pymethods]
118impl PySessionConfig {
119 #[pyo3(signature = (config_options=None))]
120 #[new]
121 fn new(config_options: Option<HashMap<String, String>>) -> Self {
122 let mut config = SessionConfig::new();
123 if let Some(hash_map) = config_options {
124 for (k, v) in &hash_map {
125 config = config.set(k, &ScalarValue::Utf8(Some(v.clone())));
126 }
127 }
128
129 Self { config }
130 }
131
132 fn with_create_default_catalog_and_schema(&self, enabled: bool) -> Self {
133 Self::from(
134 self.config
135 .clone()
136 .with_create_default_catalog_and_schema(enabled),
137 )
138 }
139
140 fn with_default_catalog_and_schema(&self, catalog: &str, schema: &str) -> Self {
141 Self::from(
142 self.config
143 .clone()
144 .with_default_catalog_and_schema(catalog, schema),
145 )
146 }
147
148 fn with_information_schema(&self, enabled: bool) -> Self {
149 Self::from(self.config.clone().with_information_schema(enabled))
150 }
151
152 fn with_batch_size(&self, batch_size: usize) -> Self {
153 Self::from(self.config.clone().with_batch_size(batch_size))
154 }
155
156 fn with_target_partitions(&self, target_partitions: usize) -> Self {
157 Self::from(
158 self.config
159 .clone()
160 .with_target_partitions(target_partitions),
161 )
162 }
163
164 fn with_repartition_aggregations(&self, enabled: bool) -> Self {
165 Self::from(self.config.clone().with_repartition_aggregations(enabled))
166 }
167
168 fn with_repartition_joins(&self, enabled: bool) -> Self {
169 Self::from(self.config.clone().with_repartition_joins(enabled))
170 }
171
172 fn with_repartition_windows(&self, enabled: bool) -> Self {
173 Self::from(self.config.clone().with_repartition_windows(enabled))
174 }
175
176 fn with_repartition_sorts(&self, enabled: bool) -> Self {
177 Self::from(self.config.clone().with_repartition_sorts(enabled))
178 }
179
180 fn with_repartition_file_scans(&self, enabled: bool) -> Self {
181 Self::from(self.config.clone().with_repartition_file_scans(enabled))
182 }
183
184 fn with_repartition_file_min_size(&self, size: usize) -> Self {
185 Self::from(self.config.clone().with_repartition_file_min_size(size))
186 }
187
188 fn with_parquet_pruning(&self, enabled: bool) -> Self {
189 Self::from(self.config.clone().with_parquet_pruning(enabled))
190 }
191
192 fn set(&self, key: &str, value: &str) -> Self {
193 Self::from(self.config.clone().set_str(key, value))
194 }
195
196 pub fn with_extension(&self, extension: Bound<PyAny>) -> PyResult<Self> {
197 if !extension.hasattr("__datafusion_extension_options__")? {
198 return Err(pyo3::exceptions::PyAttributeError::new_err(
199 "Expected extension object to define __datafusion_extension_options__()",
200 ));
201 }
202 let capsule = extension.call_method0("__datafusion_extension_options__")?;
203 let capsule = capsule.cast::<PyCapsule>()?;
204
205 let extension: NonNull<FFI_ExtensionOptions> = capsule
206 .pointer_checked(Some(c"datafusion_extension_options"))?
207 .cast();
208 let mut extension = unsafe { extension.as_ref() }.clone();
209
210 let mut config = self.config.clone();
211 let options = config.options_mut();
212 if let Some(prior_extension) = options.extensions.get::<FFI_ExtensionOptions>() {
213 extension
214 .merge(prior_extension)
215 .map_err(py_datafusion_err)?;
216 }
217
218 options.extensions.insert(extension);
219
220 Ok(Self::from(config))
221 }
222}
223
224#[pyclass(
226 from_py_object,
227 frozen,
228 name = "RuntimeEnvBuilder",
229 module = "datafusion",
230 subclass
231)]
232#[derive(Clone)]
233pub struct PyRuntimeEnvBuilder {
234 pub builder: RuntimeEnvBuilder,
235}
236
237#[pymethods]
238impl PyRuntimeEnvBuilder {
239 #[new]
240 fn new() -> Self {
241 Self {
242 builder: RuntimeEnvBuilder::default(),
243 }
244 }
245
246 fn with_disk_manager_disabled(&self) -> Self {
247 let mut runtime_builder = self.builder.clone();
248
249 let mut disk_mgr_builder = runtime_builder
250 .disk_manager_builder
251 .clone()
252 .unwrap_or_default();
253 disk_mgr_builder.set_mode(DiskManagerMode::Disabled);
254
255 runtime_builder = runtime_builder.with_disk_manager_builder(disk_mgr_builder);
256 Self {
257 builder: runtime_builder,
258 }
259 }
260
261 fn with_disk_manager_os(&self) -> Self {
262 let mut runtime_builder = self.builder.clone();
263
264 let mut disk_mgr_builder = runtime_builder
265 .disk_manager_builder
266 .clone()
267 .unwrap_or_default();
268 disk_mgr_builder.set_mode(DiskManagerMode::OsTmpDirectory);
269
270 runtime_builder = runtime_builder.with_disk_manager_builder(disk_mgr_builder);
271 Self {
272 builder: runtime_builder,
273 }
274 }
275
276 fn with_disk_manager_specified(&self, paths: Vec<String>) -> Self {
277 let paths = paths.iter().map(|s| s.into()).collect();
278 let mut runtime_builder = self.builder.clone();
279
280 let mut disk_mgr_builder = runtime_builder
281 .disk_manager_builder
282 .clone()
283 .unwrap_or_default();
284 disk_mgr_builder.set_mode(DiskManagerMode::Directories(paths));
285
286 runtime_builder = runtime_builder.with_disk_manager_builder(disk_mgr_builder);
287 Self {
288 builder: runtime_builder,
289 }
290 }
291
292 fn with_unbounded_memory_pool(&self) -> Self {
293 let builder = self.builder.clone();
294 let builder = builder.with_memory_pool(Arc::new(UnboundedMemoryPool::default()));
295 Self { builder }
296 }
297
298 fn with_fair_spill_pool(&self, size: usize) -> Self {
299 let builder = self.builder.clone();
300 let builder = builder.with_memory_pool(Arc::new(FairSpillPool::new(size)));
301 Self { builder }
302 }
303
304 fn with_greedy_memory_pool(&self, size: usize) -> Self {
305 let builder = self.builder.clone();
306 let builder = builder.with_memory_pool(Arc::new(GreedyMemoryPool::new(size)));
307 Self { builder }
308 }
309
310 fn with_temp_file_path(&self, path: &str) -> Self {
311 let builder = self.builder.clone();
312 let builder = builder.with_temp_file_path(path);
313 Self { builder }
314 }
315}
316
317#[pyclass(
319 from_py_object,
320 frozen,
321 name = "SQLOptions",
322 module = "datafusion",
323 subclass
324)]
325#[derive(Clone)]
326pub struct PySQLOptions {
327 pub options: SQLOptions,
328}
329
330impl From<SQLOptions> for PySQLOptions {
331 fn from(options: SQLOptions) -> Self {
332 Self { options }
333 }
334}
335
336#[pymethods]
337impl PySQLOptions {
338 #[new]
339 fn new() -> Self {
340 let options = SQLOptions::new();
341 Self { options }
342 }
343
344 fn with_allow_ddl(&self, allow: bool) -> Self {
346 Self::from(self.options.with_allow_ddl(allow))
347 }
348
349 pub fn with_allow_dml(&self, allow: bool) -> Self {
351 Self::from(self.options.with_allow_dml(allow))
352 }
353
354 pub fn with_allow_statements(&self, allow: bool) -> Self {
356 Self::from(self.options.with_allow_statements(allow))
357 }
358}
359
360#[pyclass(
364 from_py_object,
365 frozen,
366 name = "SessionContext",
367 module = "datafusion",
368 subclass
369)]
370#[derive(Clone)]
371pub struct PySessionContext {
372 pub ctx: Arc<SessionContext>,
373 logical_codec: Arc<PythonLogicalCodec>,
374 physical_codec: Arc<PythonPhysicalCodec>,
375}
376
377#[pymethods]
378impl PySessionContext {
379 #[pyo3(signature = (config=None, runtime=None))]
380 #[new]
381 pub fn new(
382 config: Option<PySessionConfig>,
383 runtime: Option<PyRuntimeEnvBuilder>,
384 ) -> PyDataFusionResult<Self> {
385 let config = if let Some(c) = config {
386 c.config
387 } else {
388 SessionConfig::default().with_information_schema(true)
389 };
390 let runtime_env_builder = if let Some(c) = runtime {
391 c.builder
392 } else {
393 RuntimeEnvBuilder::default()
394 };
395 let runtime = Arc::new(runtime_env_builder.build()?);
396 let session_state = SessionStateBuilder::new()
397 .with_config(config)
398 .with_runtime_env(runtime)
399 .with_default_features()
400 .with_analyzer_rule(Arc::new(crate::analyzer::ResolveLambdaVariables::new()))
401 .build();
402 let ctx = Arc::new(SessionContext::new_with_state(session_state));
403 Ok(PySessionContext {
404 ctx,
405 logical_codec: Arc::new(PythonLogicalCodec::default()),
406 physical_codec: Arc::new(PythonPhysicalCodec::default()),
407 })
408 }
409
410 pub fn enable_url_table(&self) -> PyResult<Self> {
411 Ok(PySessionContext {
412 ctx: Arc::new(self.ctx.as_ref().clone().enable_url_table()),
413 logical_codec: Arc::clone(&self.logical_codec),
414 physical_codec: Arc::clone(&self.physical_codec),
415 })
416 }
417
418 #[staticmethod]
419 #[pyo3(signature = ())]
420 pub fn global_ctx() -> PyResult<Self> {
421 let ctx = get_global_ctx().clone();
422 Ok(Self {
423 ctx,
424 logical_codec: Arc::new(PythonLogicalCodec::default()),
425 physical_codec: Arc::new(PythonPhysicalCodec::default()),
426 })
427 }
428
429 #[pyo3(signature = (scheme, store, host=None))]
431 pub fn register_object_store(
432 &self,
433 scheme: &str,
434 store: StorageContexts,
435 host: Option<&str>,
436 ) -> PyResult<()> {
437 let (store, upstream_host): (Arc<dyn ObjectStore>, String) = match store {
439 StorageContexts::AmazonS3(s3) => (s3.inner, s3.bucket_name),
440 StorageContexts::GoogleCloudStorage(gcs) => (gcs.inner, gcs.bucket_name),
441 StorageContexts::MicrosoftAzure(azure) => (azure.inner, azure.container_name),
442 StorageContexts::LocalFileSystem(local) => (local.inner, "".to_string()),
443 StorageContexts::HTTP(http) => (http.store, http.url),
444 };
445
446 let derived_host = if let Some(host) = host {
448 host
449 } else {
450 &upstream_host
451 };
452 let url_string = format!("{scheme}{derived_host}");
453 let url = Url::parse(&url_string).map_err(|e| PyValueError::new_err(e.to_string()))?;
454 self.ctx.runtime_env().register_object_store(&url, store);
455 Ok(())
456 }
457
458 #[pyo3(signature = (scheme, host=None))]
460 pub fn deregister_object_store(
461 &self,
462 scheme: &str,
463 host: Option<&str>,
464 ) -> PyDataFusionResult<()> {
465 let host = host.unwrap_or("");
466 let url_string = format!("{scheme}{host}");
467 let url = Url::parse(&url_string).map_err(|e| PyDataFusionError::Common(e.to_string()))?;
468 self.ctx.runtime_env().deregister_object_store(&url)?;
469 Ok(())
470 }
471
472 #[allow(clippy::too_many_arguments)]
473 #[pyo3(signature = (name, path, table_partition_cols=vec![],
474 file_extension=".parquet",
475 schema=None,
476 file_sort_order=None))]
477 pub fn register_listing_table(
478 &self,
479 name: &str,
480 path: PathBuf,
481 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
482 file_extension: &str,
483 schema: Option<PyArrowType<Schema>>,
484 file_sort_order: Option<Vec<Vec<PySortExpr>>>,
485 py: Python,
486 ) -> PyDataFusionResult<()> {
487 let options = ListingOptions::new(Arc::new(ParquetFormat::new()))
488 .with_file_extension(file_extension)
489 .with_table_partition_cols(convert_partition_cols(table_partition_cols))
490 .with_file_sort_order(convert_file_sort_order(file_sort_order));
491 let table_path = ListingTableUrl::parse(path_to_str(&path)?)?;
492 let resolved_schema: SchemaRef = match schema {
493 Some(s) => Arc::new(s.0),
494 None => {
495 let state = self.ctx.state();
496 let schema = options.infer_schema(&state, &table_path);
497 wait_for_future(py, schema)??
498 }
499 };
500 let config = ListingTableConfig::new(table_path)
501 .with_listing_options(options)
502 .with_schema(resolved_schema);
503 let table = ListingTable::try_new(config)?;
504 self.ctx.register_table(name, Arc::new(table))?;
505 Ok(())
506 }
507
508 pub fn register_udtf(&self, func: PyTableFunction) {
509 let name = func.name.clone();
510 let func = Arc::new(func);
511 self.ctx.register_udtf(&name, func);
512 }
513
514 pub fn deregister_udtf(&self, name: &str) {
515 self.ctx.deregister_udtf(name);
516 }
517
518 #[pyo3(signature = (query, options=None, param_values=HashMap::default(), param_strings=HashMap::default()))]
519 pub fn sql_with_options(
520 &self,
521 py: Python,
522 mut query: String,
523 options: Option<PySQLOptions>,
524 param_values: HashMap<String, PyScalarValue>,
525 param_strings: HashMap<String, String>,
526 ) -> PyDataFusionResult<PyDataFrame> {
527 let options = if let Some(options) = options {
528 options.options
529 } else {
530 SQLOptions::new()
531 };
532
533 let param_values = param_values
534 .into_iter()
535 .map(|(name, value)| (name, ScalarValue::from(value)))
536 .collect::<HashMap<_, _>>();
537
538 let state = self.ctx.state();
539 let dialect = state.config().options().sql_parser.dialect.as_ref();
540
541 if !param_strings.is_empty() {
542 query = replace_placeholders_with_strings(&query, dialect, param_strings)?;
543 }
544
545 let mut df = wait_for_future(py, async {
546 self.ctx.sql_with_options(&query, options).await
547 })?
548 .map_err(from_datafusion_error)?;
549
550 if !param_values.is_empty() {
551 df = df.with_param_values(param_values)?;
552 }
553
554 Ok(PyDataFrame::new(df))
555 }
556
557 #[pyo3(signature = (partitions, name=None, schema=None))]
558 pub fn create_dataframe(
559 &self,
560 partitions: PyArrowType<Vec<Vec<RecordBatch>>>,
561 name: Option<&str>,
562 schema: Option<PyArrowType<Schema>>,
563 py: Python,
564 ) -> PyDataFusionResult<PyDataFrame> {
565 let schema = if let Some(schema) = schema {
566 SchemaRef::from(schema.0)
567 } else {
568 partitions.0[0][0].schema()
569 };
570
571 let table = MemTable::try_new(schema, partitions.0)?;
572
573 let table_name = match name {
576 Some(val) => val.to_owned(),
577 None => {
578 "c".to_owned()
579 + Uuid::new_v4()
580 .simple()
581 .encode_lower(&mut Uuid::encode_buffer())
582 }
583 };
584
585 self.ctx.register_table(&*table_name, Arc::new(table))?;
586
587 let table = wait_for_future(py, self._table(&table_name))??;
588
589 let df = PyDataFrame::new(table);
590 Ok(df)
591 }
592
593 pub fn create_dataframe_from_logical_plan(&self, plan: PyLogicalPlan) -> PyDataFrame {
595 PyDataFrame::new(DataFrame::new(self.ctx.state(), plan.plan.as_ref().clone()))
596 }
597
598 #[pyo3(signature = (data, name=None))]
600 pub fn from_pylist(
601 &self,
602 data: Bound<'_, PyList>,
603 name: Option<&str>,
604 ) -> PyResult<PyDataFrame> {
605 let py = data.py();
607
608 let table_class = py.import("pyarrow")?.getattr("Table")?;
610 let args = PyTuple::new(py, &[data])?;
611 let table = table_class.call_method1("from_pylist", args)?;
612
613 let df = self.from_arrow(table, name, py)?;
615 Ok(df)
616 }
617
618 #[pyo3(signature = (data, name=None))]
620 pub fn from_pydict(
621 &self,
622 data: Bound<'_, PyDict>,
623 name: Option<&str>,
624 ) -> PyResult<PyDataFrame> {
625 let py = data.py();
627
628 let table_class = py.import("pyarrow")?.getattr("Table")?;
630 let args = PyTuple::new(py, &[data])?;
631 let table = table_class.call_method1("from_pydict", args)?;
632
633 let df = self.from_arrow(table, name, py)?;
635 Ok(df)
636 }
637
638 #[pyo3(signature = (data, name=None))]
640 pub fn from_arrow(
641 &self,
642 data: Bound<'_, PyAny>,
643 name: Option<&str>,
644 py: Python,
645 ) -> PyDataFusionResult<PyDataFrame> {
646 let (schema, batches) =
647 if let Ok(stream_reader) = ArrowArrayStreamReader::from_pyarrow_bound(&data) {
648 let schema = stream_reader.schema().as_ref().to_owned();
651 let batches = stream_reader
652 .collect::<Result<Vec<RecordBatch>, arrow::error::ArrowError>>()?;
653
654 (schema, batches)
655 } else if let Ok(array) = RecordBatch::from_pyarrow_bound(&data) {
656 (array.schema().as_ref().to_owned(), vec![array])
660 } else {
661 return Err(PyDataFusionError::Common(
662 "Expected either a Arrow Array or Arrow Stream in from_arrow().".to_string(),
663 ));
664 };
665
666 let list_of_batches = PyArrowType::from(vec![batches]);
669 self.create_dataframe(list_of_batches, name, Some(schema.into()), py)
670 }
671
672 #[allow(clippy::wrong_self_convention)]
674 #[pyo3(signature = (data, name=None))]
675 pub fn from_pandas(&self, data: Bound<'_, PyAny>, name: Option<&str>) -> PyResult<PyDataFrame> {
676 let py = data.py();
678
679 let table_class = py.import("pyarrow")?.getattr("Table")?;
681 let args = PyTuple::new(py, &[data])?;
682 let table = table_class.call_method1("from_pandas", args)?;
683
684 let df = self.from_arrow(table, name, py)?;
686 Ok(df)
687 }
688
689 #[pyo3(signature = (data, name=None))]
691 pub fn from_polars(&self, data: Bound<'_, PyAny>, name: Option<&str>) -> PyResult<PyDataFrame> {
692 let table = data.call_method0("to_arrow")?;
694
695 let df = self.from_arrow(table, name, data.py())?;
697 Ok(df)
698 }
699
700 pub fn register_table(&self, name: &str, table: Bound<'_, PyAny>) -> PyDataFusionResult<()> {
701 let session = self.clone().into_bound_py_any(table.py())?;
702 let table = PyTable::new(table, Some(session))?;
703
704 self.ctx.register_table(name, table.table)?;
705 Ok(())
706 }
707
708 pub fn deregister_table(&self, name: &str) -> PyDataFusionResult<()> {
709 self.ctx.deregister_table(name)?;
710 Ok(())
711 }
712
713 pub fn register_table_factory(
714 &self,
715 format: &str,
716 mut factory: Bound<'_, PyAny>,
717 ) -> PyDataFusionResult<()> {
718 if factory.hasattr("__datafusion_table_provider_factory__")? {
719 let py = factory.py();
720 let ffi = self.ffi_logical_codec();
721 let codec_capsule = create_logical_extension_capsule(py, ffi.as_ref())?;
722 factory = factory
723 .getattr("__datafusion_table_provider_factory__")?
724 .call1((codec_capsule,))?;
725 }
726
727 let factory: Arc<dyn TableProviderFactory> =
728 if let Ok(capsule) = factory.cast::<PyCapsule>().map_err(py_datafusion_err) {
729 let data: NonNull<FFI_TableProviderFactory> = capsule
730 .pointer_checked(Some(c"datafusion_table_provider_factory"))?
731 .cast();
732 let factory = unsafe { data.as_ref() };
733 factory.into()
734 } else {
735 Arc::new(RustWrappedPyTableProviderFactory::new(
736 factory.into(),
737 self.ffi_logical_codec(),
738 ))
739 };
740
741 let st = self.ctx.state_ref();
742 let mut lock = st.write();
743 lock.table_factories_mut()
744 .insert(format.to_owned(), factory);
745
746 Ok(())
747 }
748
749 pub fn register_catalog_provider_list(
750 &self,
751 mut provider: Bound<PyAny>,
752 ) -> PyDataFusionResult<()> {
753 if provider.hasattr("__datafusion_catalog_provider_list__")? {
754 let py = provider.py();
755 let ffi = self.ffi_logical_codec();
756 let codec_capsule = create_logical_extension_capsule(py, ffi.as_ref())?;
757 provider = provider
758 .getattr("__datafusion_catalog_provider_list__")?
759 .call1((codec_capsule,))?;
760 }
761
762 let provider = if let Ok(capsule) = provider.cast::<PyCapsule>() {
763 let data: NonNull<FFI_CatalogProviderList> = capsule
764 .pointer_checked(Some(c"datafusion_catalog_provider_list"))?
765 .cast();
766 let provider = unsafe { data.as_ref() };
767 let provider: Arc<dyn CatalogProviderList> = provider.into();
768 provider
769 } else {
770 match provider.extract::<PyCatalogList>() {
771 Ok(py_catalog_list) => py_catalog_list.catalog_list,
772 Err(_) => Arc::new(RustWrappedPyCatalogProviderList::new(
773 provider.into(),
774 self.ffi_logical_codec(),
775 )) as Arc<dyn CatalogProviderList>,
776 }
777 };
778
779 self.ctx.register_catalog_list(provider);
780
781 Ok(())
782 }
783
784 pub fn register_catalog_provider(
785 &self,
786 name: &str,
787 mut provider: Bound<'_, PyAny>,
788 ) -> PyDataFusionResult<()> {
789 if provider.hasattr("__datafusion_catalog_provider__")? {
790 let py = provider.py();
791 let ffi = self.ffi_logical_codec();
792 let codec_capsule = create_logical_extension_capsule(py, ffi.as_ref())?;
793 provider = provider
794 .getattr("__datafusion_catalog_provider__")?
795 .call1((codec_capsule,))?;
796 }
797
798 let provider = if let Ok(capsule) = provider.cast::<PyCapsule>() {
799 let data: NonNull<FFI_CatalogProvider> = capsule
800 .pointer_checked(Some(c"datafusion_catalog_provider"))?
801 .cast();
802 let provider = unsafe { data.as_ref() };
803 let provider: Arc<dyn CatalogProvider> = provider.into();
804 provider
805 } else {
806 match provider.extract::<PyCatalog>() {
807 Ok(py_catalog) => py_catalog.catalog,
808 Err(_) => Arc::new(RustWrappedPyCatalogProvider::new(
809 provider.into(),
810 self.ffi_logical_codec(),
811 )) as Arc<dyn CatalogProvider>,
812 }
813 };
814
815 let _ = self.ctx.register_catalog(name, provider);
816
817 Ok(())
818 }
819
820 pub fn register_table_provider(
822 &self,
823 name: &str,
824 provider: Bound<'_, PyAny>,
825 ) -> PyDataFusionResult<()> {
826 self.register_table(name, provider)
828 }
829
830 pub fn register_record_batches(
831 &self,
832 name: &str,
833 partitions: PyArrowType<Vec<Vec<RecordBatch>>>,
834 ) -> PyDataFusionResult<()> {
835 let schema = partitions.0[0][0].schema();
836 let table = MemTable::try_new(schema, partitions.0)?;
837 self.ctx.register_table(name, Arc::new(table))?;
838 Ok(())
839 }
840
841 pub fn read_batches(
842 &self,
843 batches: PyArrowType<Vec<RecordBatch>>,
844 ) -> PyDataFusionResult<PyDataFrame> {
845 Ok(PyDataFrame::new(self.ctx.read_batches(batches.0)?))
846 }
847
848 #[allow(clippy::too_many_arguments)]
849 #[pyo3(signature = (name, path, table_partition_cols=vec![],
850 parquet_pruning=true,
851 file_extension=".parquet",
852 skip_metadata=true,
853 schema=None,
854 file_sort_order=None))]
855 pub fn register_parquet(
856 &self,
857 name: &str,
858 path: PathBuf,
859 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
860 parquet_pruning: bool,
861 file_extension: &str,
862 skip_metadata: bool,
863 schema: Option<PyArrowType<Schema>>,
864 file_sort_order: Option<Vec<Vec<PySortExpr>>>,
865 py: Python,
866 ) -> PyDataFusionResult<()> {
867 let options = build_parquet_options(
868 table_partition_cols,
869 parquet_pruning,
870 file_extension,
871 skip_metadata,
872 &schema,
873 file_sort_order,
874 );
875 wait_for_future(
876 py,
877 self.ctx
878 .register_parquet(name, path_to_str(&path)?, options),
879 )??;
880 Ok(())
881 }
882
883 #[pyo3(signature = (name,
884 path,
885 options=None))]
886 pub fn register_csv(
887 &self,
888 name: &str,
889 path: &Bound<'_, PyAny>,
890 options: Option<&PyCsvReadOptions>,
891 py: Python,
892 ) -> PyDataFusionResult<()> {
893 let options = convert_csv_options(options)?;
894
895 if path.is_instance_of::<PyList>() {
896 let paths = path
897 .extract::<Vec<PathBuf>>()?
898 .iter()
899 .map(|p| path_to_str(p).map(str::to_owned))
900 .collect::<PyDataFusionResult<Vec<_>>>()?;
901 wait_for_future(
902 py,
903 self.register_csv_from_multiple_paths(name, paths, options),
904 )??;
905 } else {
906 let path = path.extract::<PathBuf>()?;
907 wait_for_future(
908 py,
909 self.ctx.register_csv(name, path_to_str(&path)?, options),
910 )??;
911 }
912
913 Ok(())
914 }
915
916 #[allow(clippy::too_many_arguments)]
917 #[pyo3(signature = (name,
918 path,
919 schema=None,
920 schema_infer_max_records=1000,
921 file_extension=".json",
922 table_partition_cols=vec![],
923 file_compression_type=None))]
924 pub fn register_json(
925 &self,
926 name: &str,
927 path: PathBuf,
928 schema: Option<PyArrowType<Schema>>,
929 schema_infer_max_records: usize,
930 file_extension: &str,
931 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
932 file_compression_type: Option<String>,
933 py: Python,
934 ) -> PyDataFusionResult<()> {
935 let options = build_json_options(
936 table_partition_cols,
937 file_compression_type,
938 schema_infer_max_records,
939 file_extension,
940 &schema,
941 )?;
942 wait_for_future(
943 py,
944 self.ctx.register_json(name, path_to_str(&path)?, options),
945 )??;
946 Ok(())
947 }
948
949 #[allow(clippy::too_many_arguments)]
950 #[pyo3(signature = (name,
951 path,
952 schema=None,
953 file_extension=".avro",
954 table_partition_cols=vec![]))]
955 pub fn register_avro(
956 &self,
957 name: &str,
958 path: PathBuf,
959 schema: Option<PyArrowType<Schema>>,
960 file_extension: &str,
961 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
962 py: Python,
963 ) -> PyDataFusionResult<()> {
964 let options = build_avro_options(table_partition_cols, file_extension, &schema);
965 wait_for_future(
966 py,
967 self.ctx.register_avro(name, path_to_str(&path)?, options),
968 )??;
969 Ok(())
970 }
971
972 #[pyo3(signature = (name, path, schema=None, file_extension=".arrow", table_partition_cols=vec![]))]
973 pub fn register_arrow(
974 &self,
975 name: &str,
976 path: PathBuf,
977 schema: Option<PyArrowType<Schema>>,
978 file_extension: &str,
979 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
980 py: Python,
981 ) -> PyDataFusionResult<()> {
982 let options = build_arrow_options(table_partition_cols, file_extension, &schema);
983 wait_for_future(
984 py,
985 self.ctx.register_arrow(name, path_to_str(&path)?, options),
986 )??;
987 Ok(())
988 }
989
990 pub fn register_batch(
991 &self,
992 name: &str,
993 batch: PyArrowType<RecordBatch>,
994 ) -> PyDataFusionResult<()> {
995 self.ctx.register_batch(name, batch.0)?;
996 Ok(())
997 }
998
999 pub fn register_dataset(
1001 &self,
1002 name: &str,
1003 dataset: &Bound<'_, PyAny>,
1004 py: Python,
1005 ) -> PyDataFusionResult<()> {
1006 let table: Arc<dyn TableProvider> = Arc::new(Dataset::new(dataset, py)?);
1007
1008 self.ctx.register_table(name, table)?;
1009
1010 Ok(())
1011 }
1012
1013 pub fn register_udf(&self, udf: PyScalarUDF) -> PyResult<()> {
1014 self.ctx.register_udf(udf.function);
1015 Ok(())
1016 }
1017
1018 pub fn deregister_udf(&self, name: &str) {
1019 self.ctx.deregister_udf(name);
1020 }
1021
1022 pub fn register_udaf(&self, udaf: PyAggregateUDF) -> PyResult<()> {
1023 self.ctx.register_udaf(udaf.function);
1024 Ok(())
1025 }
1026
1027 pub fn enable_spark_functions(&self) -> PyResult<()> {
1030 for udf in datafusion_spark::all_default_scalar_functions() {
1031 self.ctx.register_udf((*udf).clone());
1032 }
1033 for udaf in datafusion_spark::all_default_aggregate_functions() {
1034 self.ctx.register_udaf((*udaf).clone());
1035 }
1036 for udwf in datafusion_spark::all_default_window_functions() {
1037 self.ctx.register_udwf((*udwf).clone());
1038 }
1039 Ok(())
1040 }
1041
1042 pub fn deregister_udaf(&self, name: &str) {
1043 self.ctx.deregister_udaf(name);
1044 }
1045
1046 pub fn register_udwf(&self, udwf: PyWindowUDF) -> PyResult<()> {
1047 self.ctx.register_udwf(udwf.function);
1048 Ok(())
1049 }
1050
1051 pub fn deregister_udwf(&self, name: &str) {
1052 self.ctx.deregister_udwf(name);
1053 }
1054
1055 pub fn udf(&self, name: &str) -> PyResult<PyScalarUDF> {
1056 if !self.ctx.udfs().contains(name) {
1057 return Err(PyKeyError::new_err(format!("no UDF named '{name}'")));
1058 }
1059 let function = (*self.ctx.udf(name).map_err(py_datafusion_err)?).clone();
1060 Ok(PyScalarUDF { function })
1061 }
1062
1063 pub fn udaf(&self, name: &str) -> PyResult<PyAggregateUDF> {
1064 if !self.ctx.udafs().contains(name) {
1065 return Err(PyKeyError::new_err(format!("no UDAF named '{name}'")));
1066 }
1067 let function = (*self.ctx.udaf(name).map_err(py_datafusion_err)?).clone();
1068 Ok(PyAggregateUDF { function })
1069 }
1070
1071 pub fn udwf(&self, name: &str) -> PyResult<PyWindowUDF> {
1072 if !self.ctx.udwfs().contains(name) {
1073 return Err(PyKeyError::new_err(format!("no UDWF named '{name}'")));
1074 }
1075 let function = (*self.ctx.udwf(name).map_err(py_datafusion_err)?).clone();
1076 Ok(PyWindowUDF { function })
1077 }
1078
1079 pub fn udfs(&self) -> Vec<String> {
1080 let mut names: Vec<String> = self.ctx.udfs().into_iter().collect();
1081 names.sort();
1082 names
1083 }
1084
1085 pub fn udafs(&self) -> Vec<String> {
1086 let mut names: Vec<String> = self.ctx.udafs().into_iter().collect();
1087 names.sort();
1088 names
1089 }
1090
1091 pub fn udwfs(&self) -> Vec<String> {
1092 let mut names: Vec<String> = self.ctx.udwfs().into_iter().collect();
1093 names.sort();
1094 names
1095 }
1096
1097 #[pyo3(signature = (name="datafusion"))]
1098 pub fn catalog(&self, py: Python, name: &str) -> PyResult<Py<PyAny>> {
1099 let catalog = self.ctx.catalog(name).ok_or(PyKeyError::new_err(format!(
1100 "Catalog with name {name} doesn't exist."
1101 )))?;
1102
1103 match catalog.downcast_ref::<RustWrappedPyCatalogProvider>() {
1104 Some(wrapped_schema) => Ok(wrapped_schema.catalog_provider.clone_ref(py)),
1105 None => {
1106 Ok(PyCatalog::new_from_parts(catalog, self.ffi_logical_codec()).into_py_any(py)?)
1107 }
1108 }
1109 }
1110
1111 pub fn catalog_names(&self) -> HashSet<String> {
1112 self.ctx.catalog_names().into_iter().collect()
1113 }
1114
1115 pub fn table(&self, name: &str, py: Python) -> PyResult<PyDataFrame> {
1116 let res = wait_for_future(py, self.ctx.table(name))
1117 .map_err(|e| PyKeyError::new_err(e.to_string()))?;
1118 match res {
1119 Ok(df) => Ok(PyDataFrame::new(df)),
1120 Err(e) => {
1121 if let datafusion::error::DataFusionError::Plan(msg) = &e
1122 && msg.contains("No table named")
1123 {
1124 return Err(PyKeyError::new_err(msg.to_string()));
1125 }
1126 Err(py_datafusion_err(e))
1127 }
1128 }
1129 }
1130
1131 pub fn table_exist(&self, name: &str) -> PyDataFusionResult<bool> {
1132 Ok(self.ctx.table_exist(name)?)
1133 }
1134
1135 pub fn empty_table(&self) -> PyDataFusionResult<PyDataFrame> {
1136 Ok(PyDataFrame::new(self.ctx.read_empty()?))
1137 }
1138
1139 pub fn session_id(&self) -> String {
1140 self.ctx.session_id()
1141 }
1142
1143 pub fn copied_config(&self) -> PySessionConfig {
1146 self.ctx.copied_config().into()
1147 }
1148
1149 #[staticmethod]
1153 pub fn parse_capacity_limit(config_name: &str, limit: &str) -> PyDataFusionResult<usize> {
1154 Ok(SessionContext::parse_capacity_limit(config_name, limit)?)
1155 }
1156
1157 pub fn session_start_time(&self) -> String {
1158 self.ctx.session_start_time().to_rfc3339()
1159 }
1160
1161 pub fn enable_ident_normalization(&self) -> bool {
1162 self.ctx.enable_ident_normalization()
1163 }
1164
1165 pub fn parse_sql_expr(&self, sql: &str, schema: PyDFSchema) -> PyDataFusionResult<PyExpr> {
1166 let df_schema: DFSchema = schema.into();
1167 Ok(self.ctx.parse_sql_expr(sql, &df_schema)?.into())
1168 }
1169
1170 pub fn execute_logical_plan(
1171 &self,
1172 plan: PyLogicalPlan,
1173 py: Python,
1174 ) -> PyDataFusionResult<PyDataFrame> {
1175 let df = wait_for_future(
1176 py,
1177 self.ctx.execute_logical_plan(plan.plan.as_ref().clone()),
1178 )??;
1179 Ok(PyDataFrame::new(df))
1180 }
1181
1182 pub fn refresh_catalogs(&self, py: Python) -> PyDataFusionResult<()> {
1183 wait_for_future(py, self.ctx.refresh_catalogs())??;
1184 Ok(())
1185 }
1186
1187 pub fn remove_optimizer_rule(&self, name: &str) -> bool {
1188 self.ctx.remove_optimizer_rule(name)
1189 }
1190
1191 pub fn add_physical_optimizer_rule(&self, rule: Bound<'_, PyAny>) -> PyDataFusionResult<()> {
1192 let rule = physical_optimizer_rule_from_pycapsule(&rule)?;
1193 let state_ref = self.ctx.state_ref();
1194 let mut guard = state_ref.write();
1195 let new_state = SessionStateBuilder::new_from_existing(guard.clone())
1196 .with_physical_optimizer_rule(rule)
1197 .build();
1198 *guard = new_state;
1199 Ok(())
1200 }
1201
1202 pub fn table_provider(&self, name: &str, py: Python) -> PyResult<PyTable> {
1203 let provider = wait_for_future(py, self.ctx.table_provider(name))
1204 .map_err(|e| PyRuntimeError::new_err(e.to_string()))?
1206 .map_err(|e| PyKeyError::new_err(e.to_string()))?;
1208 Ok(PyTable { table: provider })
1209 }
1210
1211 #[allow(clippy::too_many_arguments)]
1212 #[pyo3(signature = (path, schema=None, schema_infer_max_records=1000, file_extension=".json", table_partition_cols=vec![], file_compression_type=None))]
1213 pub fn read_json(
1214 &self,
1215 path: PathBuf,
1216 schema: Option<PyArrowType<Schema>>,
1217 schema_infer_max_records: usize,
1218 file_extension: &str,
1219 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1220 file_compression_type: Option<String>,
1221 py: Python,
1222 ) -> PyDataFusionResult<PyDataFrame> {
1223 let options = build_json_options(
1224 table_partition_cols,
1225 file_compression_type,
1226 schema_infer_max_records,
1227 file_extension,
1228 &schema,
1229 )?;
1230 let df = wait_for_future(py, self.ctx.read_json(path_to_str(&path)?, options))??;
1231 Ok(PyDataFrame::new(df))
1232 }
1233
1234 #[pyo3(signature = (
1235 path,
1236 options=None))]
1237 pub fn read_csv(
1238 &self,
1239 path: &Bound<'_, PyAny>,
1240 options: Option<&PyCsvReadOptions>,
1241 py: Python,
1242 ) -> PyDataFusionResult<PyDataFrame> {
1243 let options = convert_csv_options(options)?;
1244
1245 let paths: Vec<String> = if path.is_instance_of::<PyList>() {
1246 path.extract::<Vec<PathBuf>>()?
1247 .iter()
1248 .map(|p| path_to_str(p).map(str::to_owned))
1249 .collect::<PyDataFusionResult<_>>()?
1250 } else {
1251 vec![path_to_str(&path.extract::<PathBuf>()?)?.to_owned()]
1252 };
1253 let df = wait_for_future(py, self.ctx.read_csv(paths, options))??;
1254 Ok(PyDataFrame::new(df))
1255 }
1256
1257 #[allow(clippy::too_many_arguments)]
1258 #[pyo3(signature = (
1259 path,
1260 table_partition_cols=vec![],
1261 parquet_pruning=true,
1262 file_extension=".parquet",
1263 skip_metadata=true,
1264 schema=None,
1265 file_sort_order=None))]
1266 pub fn read_parquet(
1267 &self,
1268 path: PathBuf,
1269 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1270 parquet_pruning: bool,
1271 file_extension: &str,
1272 skip_metadata: bool,
1273 schema: Option<PyArrowType<Schema>>,
1274 file_sort_order: Option<Vec<Vec<PySortExpr>>>,
1275 py: Python,
1276 ) -> PyDataFusionResult<PyDataFrame> {
1277 let options = build_parquet_options(
1278 table_partition_cols,
1279 parquet_pruning,
1280 file_extension,
1281 skip_metadata,
1282 &schema,
1283 file_sort_order,
1284 );
1285 let df = PyDataFrame::new(wait_for_future(
1286 py,
1287 self.ctx.read_parquet(path_to_str(&path)?, options),
1288 )??);
1289 Ok(df)
1290 }
1291
1292 #[allow(clippy::too_many_arguments)]
1293 #[pyo3(signature = (path, schema=None, table_partition_cols=vec![], file_extension=".avro"))]
1294 pub fn read_avro(
1295 &self,
1296 path: PathBuf,
1297 schema: Option<PyArrowType<Schema>>,
1298 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1299 file_extension: &str,
1300 py: Python,
1301 ) -> PyDataFusionResult<PyDataFrame> {
1302 let options = build_avro_options(table_partition_cols, file_extension, &schema);
1303 let df = wait_for_future(py, self.ctx.read_avro(path_to_str(&path)?, options))??;
1304 Ok(PyDataFrame::new(df))
1305 }
1306
1307 #[pyo3(signature = (path, schema=None, file_extension=".arrow", table_partition_cols=vec![]))]
1308 pub fn read_arrow(
1309 &self,
1310 path: PathBuf,
1311 schema: Option<PyArrowType<Schema>>,
1312 file_extension: &str,
1313 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1314 py: Python,
1315 ) -> PyDataFusionResult<PyDataFrame> {
1316 let options = build_arrow_options(table_partition_cols, file_extension, &schema);
1317 let df = wait_for_future(py, self.ctx.read_arrow(path_to_str(&path)?, options))??;
1318 Ok(PyDataFrame::new(df))
1319 }
1320
1321 pub fn read_table(&self, table: Bound<'_, PyAny>) -> PyDataFusionResult<PyDataFrame> {
1322 let session = self.clone().into_bound_py_any(table.py())?;
1323 let table = PyTable::new(table, Some(session))?;
1324 let df = self.ctx.read_table(table.table())?;
1325 Ok(PyDataFrame::new(df))
1326 }
1327
1328 fn __repr__(&self) -> PyResult<String> {
1329 let config = self.ctx.copied_config();
1330 let mut config_entries = config
1331 .options()
1332 .entries()
1333 .iter()
1334 .filter(|e| e.value.is_some())
1335 .map(|e| format!("{} = {}", e.key, e.value.as_ref().unwrap()))
1336 .collect::<Vec<_>>();
1337 config_entries.sort();
1338 Ok(format!(
1339 "SessionContext: id={}; configs=[\n\t{}]",
1340 self.session_id(),
1341 config_entries.join("\n\t")
1342 ))
1343 }
1344
1345 pub fn execute(
1347 &self,
1348 plan: PyExecutionPlan,
1349 part: usize,
1350 py: Python,
1351 ) -> PyDataFusionResult<PyRecordBatchStream> {
1352 let ctx: TaskContext = TaskContext::from(&self.ctx.state());
1353 let plan = plan.plan.clone();
1354 let stream = spawn_future(py, async move { plan.execute(part, Arc::new(ctx)) })?;
1355 Ok(PyRecordBatchStream::new(stream))
1356 }
1357
1358 pub fn __datafusion_task_context_provider__<'py>(
1359 &self,
1360 py: Python<'py>,
1361 ) -> PyResult<Bound<'py, PyCapsule>> {
1362 let name = cr"datafusion_task_context_provider".into();
1363
1364 let ctx_provider = Arc::clone(&self.ctx) as Arc<dyn TaskContextProvider>;
1365 let ffi_ctx_provider = FFI_TaskContextProvider::from(&ctx_provider);
1366
1367 PyCapsule::new(py, ffi_ctx_provider, Some(name))
1368 }
1369
1370 pub fn __datafusion_logical_extension_codec__<'py>(
1371 &self,
1372 py: Python<'py>,
1373 ) -> PyResult<Bound<'py, PyCapsule>> {
1374 let ffi = self.ffi_logical_codec();
1375 create_logical_extension_capsule(py, ffi.as_ref())
1376 }
1377
1378 pub fn with_logical_extension_codec<'py>(
1379 &self,
1380 codec: Bound<'py, PyAny>,
1381 ) -> PyDataFusionResult<Self> {
1382 let inner_ffi = ffi_logical_codec_from_pycapsule(codec)?;
1383 let inner: Arc<dyn LogicalExtensionCodec> = (&inner_ffi).into();
1384 let logical_codec = Arc::new(PythonLogicalCodec::new(inner));
1385
1386 Ok(Self {
1387 ctx: Arc::clone(&self.ctx),
1388 logical_codec,
1389 physical_codec: Arc::clone(&self.physical_codec),
1390 })
1391 }
1392
1393 pub fn __datafusion_physical_extension_codec__<'py>(
1394 &self,
1395 py: Python<'py>,
1396 ) -> PyResult<Bound<'py, PyCapsule>> {
1397 let ffi = self.ffi_physical_codec();
1398 create_physical_extension_capsule(py, ffi.as_ref())
1399 }
1400
1401 pub fn with_physical_extension_codec<'py>(
1402 &self,
1403 codec: Bound<'py, PyAny>,
1404 ) -> PyDataFusionResult<Self> {
1405 let inner = physical_codec_from_pycapsule(&codec)?;
1406 let physical_codec = Arc::new(PythonPhysicalCodec::new(inner));
1407
1408 Ok(Self {
1409 ctx: Arc::clone(&self.ctx),
1410 logical_codec: Arc::clone(&self.logical_codec),
1411 physical_codec,
1412 })
1413 }
1414
1415 pub fn with_python_udf_inlining(&self, enabled: bool) -> Self {
1416 let logical_codec = Arc::new(
1417 PythonLogicalCodec::new(Arc::clone(self.logical_codec.inner()))
1418 .with_python_udf_inlining(enabled),
1419 );
1420 let physical_codec = Arc::new(
1421 PythonPhysicalCodec::new(Arc::clone(self.physical_codec.inner()))
1422 .with_python_udf_inlining(enabled),
1423 );
1424 Self {
1425 ctx: Arc::clone(&self.ctx),
1426 logical_codec,
1427 physical_codec,
1428 }
1429 }
1430}
1431
1432impl PySessionContext {
1433 async fn _table(&self, name: &str) -> datafusion::common::Result<DataFrame> {
1434 self.ctx.table(name).await
1435 }
1436
1437 async fn register_csv_from_multiple_paths(
1438 &self,
1439 name: &str,
1440 table_paths: Vec<String>,
1441 options: CsvReadOptions<'_>,
1442 ) -> datafusion::common::Result<()> {
1443 let table_paths = table_paths.to_urls()?;
1444 let session_config = self.ctx.copied_config();
1445 let listing_options =
1446 options.to_listing_options(&session_config, self.ctx.copied_table_options());
1447
1448 let option_extension = listing_options.file_extension.clone();
1449
1450 if table_paths.is_empty() {
1451 return exec_err!("No table paths were provided");
1452 }
1453
1454 for path in &table_paths {
1456 let file_path = path.as_str();
1457 if !file_path.ends_with(option_extension.as_str()) && !path.is_collection() {
1458 return exec_err!(
1459 "File path '{file_path}' does not match the expected extension '{option_extension}'"
1460 );
1461 }
1462 }
1463
1464 let resolved_schema = options
1465 .get_resolved_schema(&session_config, self.ctx.state(), table_paths[0].clone())
1466 .await?;
1467
1468 let config = ListingTableConfig::new_with_multi_paths(table_paths)
1469 .with_listing_options(listing_options)
1470 .with_schema(resolved_schema);
1471 let table = ListingTable::try_new(config)?;
1472 self.ctx
1473 .register_table(TableReference::Bare { table: name.into() }, Arc::new(table))?;
1474 Ok(())
1475 }
1476
1477 pub(crate) fn logical_codec(&self) -> &Arc<PythonLogicalCodec> {
1481 &self.logical_codec
1482 }
1483
1484 pub(crate) fn physical_codec(&self) -> &Arc<PythonPhysicalCodec> {
1487 &self.physical_codec
1488 }
1489
1490 pub(crate) fn ffi_logical_codec(&self) -> Arc<FFI_LogicalExtensionCodec> {
1494 let inner: Arc<dyn LogicalExtensionCodec> =
1495 Arc::clone(&self.logical_codec) as Arc<dyn LogicalExtensionCodec>;
1496 let runtime = get_tokio_runtime().handle().clone();
1497 let ctx_provider = Arc::clone(&self.ctx) as Arc<dyn TaskContextProvider>;
1498 Arc::new(FFI_LogicalExtensionCodec::new(
1499 inner,
1500 Some(runtime),
1501 &ctx_provider,
1502 ))
1503 }
1504
1505 pub(crate) fn ffi_physical_codec(&self) -> Arc<FFI_PhysicalExtensionCodec> {
1507 let inner: Arc<dyn PhysicalExtensionCodec + Send> =
1508 Arc::clone(&self.physical_codec) as Arc<dyn PhysicalExtensionCodec + Send>;
1509 let runtime = get_tokio_runtime().handle().clone();
1510 let ctx_provider = Arc::clone(&self.ctx) as Arc<dyn TaskContextProvider>;
1511 Arc::new(FFI_PhysicalExtensionCodec::new(
1512 inner,
1513 Some(runtime),
1514 &ctx_provider,
1515 ))
1516 }
1517}
1518
1519pub fn parse_file_compression_type(
1520 file_compression_type: Option<String>,
1521) -> Result<FileCompressionType, PyErr> {
1522 FileCompressionType::from_str(&file_compression_type.unwrap_or_default()).map_err(|_| {
1523 PyValueError::new_err("file_compression_type must be one of: gzip, bz2, xz, zstd")
1524 })
1525}
1526
1527fn path_to_str(path: &Path) -> PyDataFusionResult<&str> {
1528 path.to_str()
1529 .ok_or_else(|| PyValueError::new_err("Unable to convert path to a string").into())
1530}
1531
1532fn convert_csv_options(
1533 options: Option<&PyCsvReadOptions>,
1534) -> PyDataFusionResult<CsvReadOptions<'_>> {
1535 Ok(options
1536 .map(|opts| opts.try_into())
1537 .transpose()?
1538 .unwrap_or_default())
1539}
1540
1541fn convert_partition_cols(
1542 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1543) -> Vec<(String, DataType)> {
1544 table_partition_cols
1545 .into_iter()
1546 .map(|(name, ty)| (name, ty.0))
1547 .collect()
1548}
1549
1550fn convert_file_sort_order(
1551 file_sort_order: Option<Vec<Vec<PySortExpr>>>,
1552) -> Vec<Vec<datafusion::logical_expr::SortExpr>> {
1553 file_sort_order
1554 .unwrap_or_default()
1555 .into_iter()
1556 .map(|e| e.into_iter().map(|f| f.into()).collect())
1557 .collect()
1558}
1559
1560fn build_parquet_options<'a>(
1561 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1562 parquet_pruning: bool,
1563 file_extension: &'a str,
1564 skip_metadata: bool,
1565 schema: &'a Option<PyArrowType<Schema>>,
1566 file_sort_order: Option<Vec<Vec<PySortExpr>>>,
1567) -> ParquetReadOptions<'a> {
1568 let mut options = ParquetReadOptions::default()
1569 .table_partition_cols(convert_partition_cols(table_partition_cols))
1570 .parquet_pruning(parquet_pruning)
1571 .skip_metadata(skip_metadata);
1572 options.file_extension = file_extension;
1573 options.schema = schema.as_ref().map(|x| &x.0);
1574 options.file_sort_order = convert_file_sort_order(file_sort_order);
1575 options
1576}
1577
1578fn build_json_options<'a>(
1579 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1580 file_compression_type: Option<String>,
1581 schema_infer_max_records: usize,
1582 file_extension: &'a str,
1583 schema: &'a Option<PyArrowType<Schema>>,
1584) -> Result<JsonReadOptions<'a>, PyErr> {
1585 let mut options = JsonReadOptions::default()
1586 .table_partition_cols(convert_partition_cols(table_partition_cols))
1587 .file_compression_type(parse_file_compression_type(file_compression_type)?);
1588 options.schema_infer_max_records = schema_infer_max_records;
1589 options.file_extension = file_extension;
1590 options.schema = schema.as_ref().map(|x| &x.0);
1591 Ok(options)
1592}
1593
1594fn build_arrow_options<'a>(
1595 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1596 file_extension: &'a str,
1597 schema: &'a Option<PyArrowType<Schema>>,
1598) -> ArrowReadOptions<'a> {
1599 let mut options = ArrowReadOptions::default()
1600 .table_partition_cols(convert_partition_cols(table_partition_cols));
1601 options.file_extension = file_extension;
1602 options.schema = schema.as_ref().map(|x| &x.0);
1603 options
1604}
1605
1606fn build_avro_options<'a>(
1607 table_partition_cols: Vec<(String, PyArrowType<DataType>)>,
1608 file_extension: &'a str,
1609 schema: &'a Option<PyArrowType<Schema>>,
1610) -> AvroReadOptions<'a> {
1611 let mut options = AvroReadOptions::default()
1612 .table_partition_cols(convert_partition_cols(table_partition_cols));
1613 options.file_extension = file_extension;
1614 options.schema = schema.as_ref().map(|x| &x.0);
1615 options
1616}
1617
1618impl From<PySessionContext> for SessionContext {
1619 fn from(ctx: PySessionContext) -> SessionContext {
1620 ctx.ctx.as_ref().clone()
1621 }
1622}
1623
1624impl From<SessionContext> for PySessionContext {
1625 fn from(ctx: SessionContext) -> PySessionContext {
1626 PySessionContext {
1627 ctx: Arc::new(ctx),
1628 logical_codec: Arc::new(PythonLogicalCodec::default()),
1629 physical_codec: Arc::new(PythonPhysicalCodec::default()),
1630 }
1631 }
1632}