Skip to main content

datafusion_python/
context.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::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/// Configuration options for a SessionContext
99#[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/// Runtime options for a SessionContext
225#[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/// `PySQLOptions` allows you to specify options to the sql execution.
318#[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    /// Should DDL data modification commands  (e.g. `CREATE TABLE`) be run? Defaults to `true`.
345    fn with_allow_ddl(&self, allow: bool) -> Self {
346        Self::from(self.options.with_allow_ddl(allow))
347    }
348
349    /// Should DML data modification commands (e.g. `INSERT and COPY`) be run? Defaults to `true`
350    pub fn with_allow_dml(&self, allow: bool) -> Self {
351        Self::from(self.options.with_allow_dml(allow))
352    }
353
354    /// Should Statements such as (e.g. `SET VARIABLE and `BEGIN TRANSACTION` ...`) be run?. Defaults to `true`
355    pub fn with_allow_statements(&self, allow: bool) -> Self {
356        Self::from(self.options.with_allow_statements(allow))
357    }
358}
359
360/// `PySessionContext` is able to plan and execute DataFusion plans.
361/// It has a powerful optimizer, a physical planner for local execution, and a
362/// multi-threaded execution engine to perform the execution.
363#[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    /// Register an object store with the given name
430    #[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        // for most stores the "host" is the bucket name and can be inferred from the store
438        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 users override the host to match the api signature from upstream
447        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    /// Deregister an object store with the given url
459    #[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        // generate a random (unique) name for this table if none is provided
574        // table name cannot start with numeric digit
575        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    /// Create a DataFrame from an existing logical plan
594    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    /// Construct datafusion dataframe from Python list
599    #[pyo3(signature = (data, name=None))]
600    pub fn from_pylist(
601        &self,
602        data: Bound<'_, PyList>,
603        name: Option<&str>,
604    ) -> PyResult<PyDataFrame> {
605        // Acquire GIL Token
606        let py = data.py();
607
608        // Instantiate pyarrow Table object & convert to Arrow Table
609        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        // Convert Arrow Table to datafusion DataFrame
614        let df = self.from_arrow(table, name, py)?;
615        Ok(df)
616    }
617
618    /// Construct datafusion dataframe from Python dictionary
619    #[pyo3(signature = (data, name=None))]
620    pub fn from_pydict(
621        &self,
622        data: Bound<'_, PyDict>,
623        name: Option<&str>,
624    ) -> PyResult<PyDataFrame> {
625        // Acquire GIL Token
626        let py = data.py();
627
628        // Instantiate pyarrow Table object & convert to Arrow Table
629        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        // Convert Arrow Table to datafusion DataFrame
634        let df = self.from_arrow(table, name, py)?;
635        Ok(df)
636    }
637
638    /// Construct datafusion dataframe from Arrow Table
639    #[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                // Works for any object that implements __arrow_c_stream__ in pycapsule.
649
650                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                // While this says RecordBatch, it will work for any object that implements
657                // __arrow_c_array__ and returns a StructArray.
658
659                (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        // Because create_dataframe() expects a vector of vectors of record batches
667        // here we need to wrap the vector of record batches in an additional vector
668        let list_of_batches = PyArrowType::from(vec![batches]);
669        self.create_dataframe(list_of_batches, name, Some(schema.into()), py)
670    }
671
672    /// Construct datafusion dataframe from pandas
673    #[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        // Obtain GIL token
677        let py = data.py();
678
679        // Instantiate pyarrow Table object & convert to Arrow Table
680        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        // Convert Arrow Table to datafusion DataFrame
685        let df = self.from_arrow(table, name, py)?;
686        Ok(df)
687    }
688
689    /// Construct datafusion dataframe from polars
690    #[pyo3(signature = (data, name=None))]
691    pub fn from_polars(&self, data: Bound<'_, PyAny>, name: Option<&str>) -> PyResult<PyDataFrame> {
692        // Convert Polars dataframe to Arrow Table
693        let table = data.call_method0("to_arrow")?;
694
695        // Convert Arrow Table to datafusion DataFrame
696        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    /// Construct datafusion dataframe from Arrow Table
821    pub fn register_table_provider(
822        &self,
823        name: &str,
824        provider: Bound<'_, PyAny>,
825    ) -> PyDataFusionResult<()> {
826        // Deprecated: use `register_table` instead
827        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    // Registers a PyArrow.Dataset
1000    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    /// Register all `datafusion-spark` UDFs/UDAFs/UDWFs, overriding any built-in
1028    /// DataFusion functions of the same name with their Spark-semantics version.
1029    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    /// Return a copy of the active `SessionConfig`. Mutating the returned
1144    /// config does not affect this context.
1145    pub fn copied_config(&self) -> PySessionConfig {
1146        self.ctx.copied_config().into()
1147    }
1148
1149    /// Parse a string like `"100M"`, `"1.5G"`, or `"512K"` into a byte count.
1150    /// `"0"` is accepted and returns 0. Use this when constructing a
1151    /// `RuntimeEnvBuilder` from a human-friendly size string.
1152    #[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            // Outer error: runtime/async failure
1205            .map_err(|e| PyRuntimeError::new_err(e.to_string()))?
1206            // Inner error: table not found
1207            .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    /// Execute a partition of an execution plan and return a stream of record batches
1346    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        // check if the file extension matches the expected extension
1455        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    /// Session-scoped logical codec. Sibling modules read this when they
1478    /// need to serialize/deserialize logical-layer types (LogicalPlan,
1479    /// Expr) against the user-installed (or default) codec stack.
1480    pub(crate) fn logical_codec(&self) -> &Arc<PythonLogicalCodec> {
1481        &self.logical_codec
1482    }
1483
1484    /// Session-scoped physical codec. Sibling modules read this for
1485    /// ExecutionPlan / PhysicalExpr serialization.
1486    pub(crate) fn physical_codec(&self) -> &Arc<PythonPhysicalCodec> {
1487        &self.physical_codec
1488    }
1489
1490    /// Build an FFI-wrapped clone of the session's logical codec on demand.
1491    /// Used at every site that exports the codec across an FFI boundary
1492    /// (capsule getters, Rust wrappers for Python-defined providers, etc.).
1493    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    /// Build an FFI-wrapped clone of the session's physical codec on demand.
1506    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}