blob: 640b38853ae45bfab3d5706ca1f2d7a6e4f72a9a [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use std::path::PathBuf;
use std::{collections::HashSet, sync::Arc};
use uuid::Uuid;
use pyo3::exceptions::{PyKeyError, PyValueError};
use pyo3::prelude::*;
use datafusion::arrow::datatypes::{DataType, Schema};
use datafusion::arrow::pyarrow::PyArrowType;
use datafusion::arrow::record_batch::RecordBatch;
use datafusion::datasource::datasource::TableProvider;
use datafusion::datasource::MemTable;
use datafusion::execution::context::{SessionConfig, SessionContext};
use datafusion::prelude::{CsvReadOptions, DataFrame, ParquetReadOptions};
use crate::catalog::{PyCatalog, PyTable};
use crate::dataframe::PyDataFrame;
use crate::dataset::Dataset;
use crate::datatype::PyDataType;
use crate::errors::DataFusionError;
use crate::udf::PyScalarUDF;
use crate::utils::wait_for_future;
/// `PySessionContext` is able to plan and execute DataFusion plans.
/// It has a powerful optimizer, a physical planner for local execution, and a
/// multi-threaded execution engine to perform the execution.
#[pyclass(name = "SessionContext", module = "ballista", subclass, unsendable)]
pub(crate) struct PySessionContext {
ctx: SessionContext,
}
#[pymethods]
impl PySessionContext {
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
default_catalog = "datafusion",
default_schema = "public",
create_default_catalog_and_schema = true,
information_schema = false,
repartition_joins = true,
repartition_aggregations = true,
repartition_windows = true,
parquet_pruning = true,
target_partitions = None
))]
#[new]
fn new(
default_catalog: &str,
default_schema: &str,
create_default_catalog_and_schema: bool,
information_schema: bool,
repartition_joins: bool,
repartition_aggregations: bool,
repartition_windows: bool,
parquet_pruning: bool,
target_partitions: Option<usize>,
// TODO: config_options
) -> Self {
let cfg = SessionConfig::new()
.with_create_default_catalog_and_schema(create_default_catalog_and_schema)
.with_default_catalog_and_schema(default_catalog, default_schema)
.with_information_schema(information_schema)
.with_repartition_joins(repartition_joins)
.with_repartition_aggregations(repartition_aggregations)
.with_repartition_windows(repartition_windows)
.with_parquet_pruning(parquet_pruning);
let cfg_full = match target_partitions {
None => cfg,
Some(x) => cfg.with_target_partitions(x),
};
PySessionContext {
ctx: SessionContext::with_config(cfg_full),
}
}
/// Returns a PyDataFrame whose plan corresponds to the SQL statement.
fn sql(&mut self, query: &str, py: Python) -> PyResult<PyDataFrame> {
let result = self.ctx.sql(query);
let df = wait_for_future(py, result).map_err(DataFusionError::from)?;
Ok(PyDataFrame::new(df))
}
fn create_dataframe(
&mut self,
partitions: PyArrowType<Vec<Vec<RecordBatch>>>,
py: Python,
) -> PyResult<PyDataFrame> {
let table = MemTable::try_new(partitions.0[0][0].schema(), partitions.0)
.map_err(DataFusionError::from)?;
// generate a random (unique) name for this table
// table name cannot start with numeric digit
let name = "c".to_owned()
+ Uuid::new_v4()
.to_simple()
.encode_lower(&mut Uuid::encode_buffer());
self.ctx
.register_table(&*name, Arc::new(table))
.map_err(DataFusionError::from)?;
let table = wait_for_future(py, self._table(&name)).map_err(DataFusionError::from)?;
Ok(PyDataFrame::new(table))
}
fn register_table(&mut self, name: &str, table: &PyTable) -> PyResult<()> {
self.ctx
.register_table(name, table.table())
.map_err(DataFusionError::from)?;
Ok(())
}
fn deregister_table(&mut self, name: &str) -> PyResult<()> {
self.ctx
.deregister_table(name)
.map_err(DataFusionError::from)?;
Ok(())
}
fn register_record_batches(
&mut self,
name: &str,
partitions: PyArrowType<Vec<Vec<RecordBatch>>>,
) -> PyResult<()> {
let schema = partitions.0[0][0].schema();
let table = MemTable::try_new(schema, partitions.0)?;
self.ctx
.register_table(name, Arc::new(table))
.map_err(DataFusionError::from)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
name,
path,
table_partition_cols = vec![],
parquet_pruning = true,
file_extension = ".parquet"
))]
fn register_parquet(
&mut self,
name: &str,
path: &str,
table_partition_cols: Vec<(String, PyDataType)>,
parquet_pruning: bool,
file_extension: &str,
py: Python,
) -> PyResult<()> {
let mut options = ParquetReadOptions::default()
.table_partition_cols(convert_table_partition_cols(table_partition_cols))
.parquet_pruning(parquet_pruning);
options.file_extension = file_extension;
let result = self.ctx.register_parquet(name, path, options);
wait_for_future(py, result).map_err(DataFusionError::from)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (
name,
path,
schema = None,
has_header = true,
delimiter = ",",
schema_infer_max_records = 1000,
file_extension = ".csv"
))]
fn register_csv(
&mut self,
name: &str,
path: PathBuf,
schema: Option<PyArrowType<Schema>>,
has_header: bool,
delimiter: &str,
schema_infer_max_records: usize,
file_extension: &str,
py: Python,
) -> PyResult<()> {
let path = path
.to_str()
.ok_or_else(|| PyValueError::new_err("Unable to convert path to a string"))?;
let delimiter = delimiter.as_bytes();
if delimiter.len() != 1 {
return Err(PyValueError::new_err(
"Delimiter must be a single character",
));
}
let mut options = CsvReadOptions::new()
.has_header(has_header)
.delimiter(delimiter[0])
.schema_infer_max_records(schema_infer_max_records)
.file_extension(file_extension);
options.schema = schema.as_ref().map(|x| &x.0);
let result = self.ctx.register_csv(name, path, options);
wait_for_future(py, result).map_err(DataFusionError::from)?;
Ok(())
}
// Registers a PyArrow.Dataset
fn register_dataset(&self, name: &str, dataset: &PyAny, py: Python) -> PyResult<()> {
let table: Arc<dyn TableProvider> = Arc::new(Dataset::new(dataset, py)?);
self.ctx
.register_table(name, table)
.map_err(DataFusionError::from)?;
Ok(())
}
fn register_udf(&mut self, udf: PyScalarUDF) -> PyResult<()> {
self.ctx.register_udf(udf.function);
Ok(())
}
#[pyo3(signature = (name = "datafusion"))]
fn catalog(&self, name: &str) -> PyResult<PyCatalog> {
match self.ctx.catalog(name) {
Some(catalog) => Ok(PyCatalog::new(catalog)),
None => Err(PyKeyError::new_err(format!(
"Catalog with name {} doesn't exist.",
&name
))),
}
}
fn tables(&self) -> HashSet<String> {
#[allow(deprecated)]
self.ctx.tables().unwrap()
}
fn table(&self, name: &str, py: Python) -> PyResult<PyDataFrame> {
let table = wait_for_future(py, self._table(name)).map_err(DataFusionError::from)?;
Ok(PyDataFrame::new(table))
}
fn empty_table(&self) -> PyResult<PyDataFrame> {
Ok(PyDataFrame::new(self.ctx.read_empty()?))
}
}
impl PySessionContext {
async fn _table(&self, name: &str) -> datafusion_common::Result<DataFrame> {
self.ctx.table(name).await
}
}
fn convert_table_partition_cols(
table_partition_cols: Vec<(String, PyDataType)>,
) -> Vec<(String, DataType)> {
table_partition_cols
.iter()
.map(|(name, t)| (name.clone(), t.data_type.clone()))
.collect()
}