use arrow::record_batch::RecordBatch;
use crate::physical::budget::{ResourceBudget, ResourceReservation, ResourceScope};
use crate::physical::operators;
use crate::physical::plan::{PhysicalPlan, ScanSource};
use crate::physical::streaming::DataFrameStream;
use crate::{DataFrame, Result};
pub struct Executor;
impl Executor {
pub fn execute(plan: PhysicalPlan) -> Result<Vec<RecordBatch>> {
execute_plan(&plan)
}
}
#[derive(Debug, Clone)]
pub struct BudgetedMaterializedExecutor {
budget: ResourceBudget,
}
impl BudgetedMaterializedExecutor {
pub fn new(budget: ResourceBudget) -> Self {
Self { budget }
}
pub fn collect_batches<I>(&self, batches: I) -> Result<DataFrame>
where
I: IntoIterator<Item = Result<RecordBatch>>,
{
let mut retained = Vec::new();
let mut reservations: Vec<ResourceReservation> = Vec::new();
for batch in batches {
let batch = batch?;
let bytes = record_batch_memory_size(&batch);
let reservation = self
.budget
.reserve_batch(ResourceScope::MaterializedOutput, bytes)?;
retained.push(batch);
reservations.push(reservation);
}
let dataframe = DataFrame::from_batches(retained)?;
drop(reservations);
Ok(dataframe)
}
pub fn collect_stream(&self, stream: &mut DataFrameStream) -> Result<DataFrame> {
let mut retained = Vec::new();
let mut reservations: Vec<ResourceReservation> = Vec::new();
loop {
let next = stream.next_batch()?;
let Some(dataframe) = next else {
break;
};
for batch in dataframe.to_arrow() {
let bytes = record_batch_memory_size(&batch);
let reservation = match self
.budget
.reserve_batch(ResourceScope::MaterializedOutput, bytes)
{
Ok(reservation) => reservation,
Err(error) => {
let _ = stream.close();
return Err(error);
}
};
retained.push(batch);
reservations.push(reservation);
}
}
let dataframe = match DataFrame::from_batches(retained) {
Ok(dataframe) => dataframe,
Err(error) => {
let _ = stream.close();
return Err(error);
}
};
drop(reservations);
Ok(dataframe)
}
pub fn budget(&self) -> &ResourceBudget {
&self.budget
}
}
pub fn record_batch_memory_size(batch: &RecordBatch) -> u64 {
batch
.columns()
.iter()
.map(|column| column.get_array_memory_size() as u64)
.sum()
}
fn execute_plan(plan: &PhysicalPlan) -> Result<Vec<RecordBatch>> {
match plan {
PhysicalPlan::ScanExec { source } => execute_scan(source),
PhysicalPlan::ConcatExec { inputs, schema } => {
let mut output = Vec::new();
let mut expected_schema = schema.clone();
for (input_index, input) in inputs.iter().enumerate() {
let batches = execute_plan(input)?;
for (batch_index, batch) in batches.into_iter().enumerate() {
if let Some(expected_schema) = &expected_schema {
if batch.schema().as_ref() == expected_schema.as_ref() {
output.push(batch);
continue;
}
return Err(crate::DataFrameError::schema_mismatch(format!(
"concat_schema_mismatch: input {input_index} batch {batch_index} differs from validated schema"
)));
}
expected_schema = Some(batch.schema());
output.push(batch);
}
}
Ok(output)
}
PhysicalPlan::ProjectionExec { input, exprs, kind } => {
let batches = execute_plan(input)?;
operators::project_batches(batches, exprs, kind.clone())
}
PhysicalPlan::FilterExec { input, predicate } => {
let batches = execute_plan(input)?;
operators::filter_batches(batches, predicate)
}
PhysicalPlan::AggregateExec {
input,
group_by,
aggs,
} => {
let batches = execute_plan(input)?;
operators::aggregate_batches(batches, group_by, aggs)
}
PhysicalPlan::JoinExec {
left,
right,
keys,
how,
} => {
let left_batches = execute_plan(left)?;
let right_batches = execute_plan(right)?;
operators::join_batches(left_batches, right_batches, keys, how)
}
PhysicalPlan::SortExec { input, options } => {
let batches = execute_plan(input)?;
operators::sort_batches(batches, options)
}
PhysicalPlan::SliceExec {
input,
offset,
len,
from_end,
} => {
let batches = execute_plan(input)?;
operators::slice_batches(batches, *offset, *len, *from_end)
}
PhysicalPlan::UniqueExec { input, subset } => {
let batches = execute_plan(input)?;
operators::unique_batches(batches, subset.as_deref())
}
PhysicalPlan::FillNullExec { input, fill } => {
let batches = execute_plan(input)?;
operators::fill_null_batches(batches, fill)
}
PhysicalPlan::DropNullsExec { input, subset } => {
let batches = execute_plan(input)?;
operators::drop_nulls_batches(batches, subset.as_deref())
}
PhysicalPlan::NullCountExec { input } => {
let batches = execute_plan(input)?;
operators::null_count_batches(batches)
}
PhysicalPlan::ExplodeExec { input, column } => {
let batches = execute_plan(input)?;
operators::explode_batches(batches, column)
}
PhysicalPlan::ImplodeExec { input } => {
let batches = execute_plan(input)?;
operators::implode_batches(batches)
}
}
}
fn execute_scan(source: &ScanSource) -> Result<Vec<RecordBatch>> {
operators::scan_source(source)
}
#[cfg(test)]
mod tests {
use std::num::NonZeroUsize;
use std::sync::Arc;
use arrow::array::{Array, ArrayRef, Int64Array};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use super::{BudgetedMaterializedExecutor, Executor};
use crate::physical::budget::ResourceBudget;
use crate::physical::{PhysicalPlan, ScanSource};
use crate::{DataFrame, DataFrameError};
fn batch(values: Vec<i64>) -> RecordBatch {
let schema = Arc::new(Schema::new(vec![Field::new(
"value",
DataType::Int64,
true,
)]));
RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(values)) as ArrayRef]).unwrap()
}
#[test]
fn bounded_materialization_reserves_before_retaining_and_releases_on_error() {
let one = batch(vec![1, 2]);
let bytes = super::record_batch_memory_size(&one);
let budget = ResourceBudget::new(bytes, NonZeroUsize::new(2).unwrap());
let collector = BudgetedMaterializedExecutor::new(budget.clone());
let err = collector.collect_batches(vec![Ok(one), Ok(batch(vec![3, 4]))]);
assert!(matches!(
err,
Err(DataFrameError::ResourceLimitExceeded { .. })
));
assert_eq!(budget.usage().reserved_bytes, 0);
assert_eq!(budget.usage().reserved_batches, 0);
}
#[test]
fn bounded_materialization_transfers_output_and_releases_budget() {
let batch = batch(vec![1, 2]);
let bytes = super::record_batch_memory_size(&batch);
let budget = ResourceBudget::new(bytes, NonZeroUsize::new(1).unwrap());
let output = BudgetedMaterializedExecutor::new(budget.clone())
.collect_batches(vec![Ok(batch)])
.unwrap();
assert_eq!(output.height(), 2);
assert_eq!(budget.usage().reserved_bytes, 0);
assert_eq!(budget.usage().reserved_batches, 0);
}
#[test]
fn concat_executor_emits_all_batches_of_each_input_in_declared_order() {
let first = DataFrame::from_batches(vec![batch(vec![1, 2])]).unwrap();
let second = DataFrame::from_batches(vec![batch(vec![3, 4])]).unwrap();
let plan = PhysicalPlan::ConcatExec {
inputs: vec![
PhysicalPlan::ScanExec {
source: ScanSource::DataFrame(first.clone()),
},
PhysicalPlan::ScanExec {
source: ScanSource::DataFrame(second),
},
],
schema: Some(first.schema()),
};
let batches = Executor::execute(plan).unwrap();
let values = batches
.iter()
.flat_map(|batch| {
batch
.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.values()
})
.copied()
.collect::<Vec<_>>();
assert_eq!(values, vec![1, 2, 3, 4]);
}
#[test]
fn concat_executor_rejects_a_runtime_schema_contract_violation_before_output() {
let first = DataFrame::from_batches(vec![batch(vec![1])]).unwrap();
let other_schema = Arc::new(Schema::new(vec![Field::new(
"other",
DataType::Int64,
true,
)]));
let second = DataFrame::from_batches(vec![RecordBatch::try_new(
other_schema,
vec![Arc::new(Int64Array::from(vec![2])) as ArrayRef],
)
.unwrap()])
.unwrap();
let plan = PhysicalPlan::ConcatExec {
inputs: vec![
PhysicalPlan::ScanExec {
source: ScanSource::DataFrame(first.clone()),
},
PhysicalPlan::ScanExec {
source: ScanSource::DataFrame(second),
},
],
schema: Some(first.schema()),
};
let err = Executor::execute(plan).unwrap_err();
assert!(matches!(err, DataFrameError::SchemaMismatch { message }
if message.contains("concat_schema_mismatch")));
}
}