alopex_dataframe/physical/
executor.rs1use arrow::record_batch::RecordBatch;
2
3use crate::physical::budget::{ResourceBudget, ResourceReservation, ResourceScope};
4use crate::physical::operators;
5use crate::physical::plan::{PhysicalPlan, ScanSource};
6use crate::physical::streaming::DataFrameStream;
7use crate::{DataFrame, Result};
8
9pub struct Executor;
11
12impl Executor {
13 pub fn execute(plan: PhysicalPlan) -> Result<Vec<RecordBatch>> {
15 execute_plan(&plan)
16 }
17}
18
19#[derive(Debug, Clone)]
25pub struct BudgetedMaterializedExecutor {
26 budget: ResourceBudget,
27}
28
29impl BudgetedMaterializedExecutor {
30 pub fn new(budget: ResourceBudget) -> Self {
32 Self { budget }
33 }
34
35 pub fn collect_batches<I>(&self, batches: I) -> Result<DataFrame>
41 where
42 I: IntoIterator<Item = Result<RecordBatch>>,
43 {
44 let mut retained = Vec::new();
45 let mut reservations: Vec<ResourceReservation> = Vec::new();
46
47 for batch in batches {
48 let batch = batch?;
49 let bytes = record_batch_memory_size(&batch);
50 let reservation = self
51 .budget
52 .reserve_batch(ResourceScope::MaterializedOutput, bytes)?;
53
54 retained.push(batch);
56 reservations.push(reservation);
57 }
58
59 let dataframe = DataFrame::from_batches(retained)?;
60 drop(reservations);
61 Ok(dataframe)
62 }
63
64 pub fn collect_stream(&self, stream: &mut DataFrameStream) -> Result<DataFrame> {
70 let mut retained = Vec::new();
71 let mut reservations: Vec<ResourceReservation> = Vec::new();
72
73 loop {
74 let next = stream.next_batch()?;
75 let Some(dataframe) = next else {
76 break;
77 };
78 for batch in dataframe.to_arrow() {
79 let bytes = record_batch_memory_size(&batch);
80 let reservation = match self
81 .budget
82 .reserve_batch(ResourceScope::MaterializedOutput, bytes)
83 {
84 Ok(reservation) => reservation,
85 Err(error) => {
86 let _ = stream.close();
87 return Err(error);
88 }
89 };
90 retained.push(batch);
91 reservations.push(reservation);
92 }
93 }
94
95 let dataframe = match DataFrame::from_batches(retained) {
96 Ok(dataframe) => dataframe,
97 Err(error) => {
98 let _ = stream.close();
99 return Err(error);
100 }
101 };
102 drop(reservations);
103 Ok(dataframe)
104 }
105
106 pub fn budget(&self) -> &ResourceBudget {
108 &self.budget
109 }
110}
111
112pub fn record_batch_memory_size(batch: &RecordBatch) -> u64 {
117 batch
118 .columns()
119 .iter()
120 .map(|column| column.get_array_memory_size() as u64)
121 .sum()
122}
123
124fn execute_plan(plan: &PhysicalPlan) -> Result<Vec<RecordBatch>> {
125 match plan {
126 PhysicalPlan::ScanExec { source } => execute_scan(source),
127 PhysicalPlan::ConcatExec { inputs, schema } => {
128 let mut output = Vec::new();
129 let mut expected_schema = schema.clone();
130 for (input_index, input) in inputs.iter().enumerate() {
131 let batches = execute_plan(input)?;
132 for (batch_index, batch) in batches.into_iter().enumerate() {
133 if let Some(expected_schema) = &expected_schema {
134 if batch.schema().as_ref() == expected_schema.as_ref() {
135 output.push(batch);
136 continue;
137 }
138 return Err(crate::DataFrameError::schema_mismatch(format!(
139 "concat_schema_mismatch: input {input_index} batch {batch_index} differs from validated schema"
140 )));
141 }
142 expected_schema = Some(batch.schema());
143 output.push(batch);
144 }
145 }
146 Ok(output)
147 }
148 PhysicalPlan::ProjectionExec { input, exprs, kind } => {
149 let batches = execute_plan(input)?;
150 operators::project_batches(batches, exprs, kind.clone())
151 }
152 PhysicalPlan::FilterExec { input, predicate } => {
153 let batches = execute_plan(input)?;
154 operators::filter_batches(batches, predicate)
155 }
156 PhysicalPlan::AggregateExec {
157 input,
158 group_by,
159 aggs,
160 } => {
161 let batches = execute_plan(input)?;
162 operators::aggregate_batches(batches, group_by, aggs)
163 }
164 PhysicalPlan::JoinExec {
165 left,
166 right,
167 keys,
168 how,
169 } => {
170 let left_batches = execute_plan(left)?;
171 let right_batches = execute_plan(right)?;
172 operators::join_batches(left_batches, right_batches, keys, how)
173 }
174 PhysicalPlan::SortExec { input, options } => {
175 let batches = execute_plan(input)?;
176 operators::sort_batches(batches, options)
177 }
178 PhysicalPlan::SliceExec {
179 input,
180 offset,
181 len,
182 from_end,
183 } => {
184 let batches = execute_plan(input)?;
185 operators::slice_batches(batches, *offset, *len, *from_end)
186 }
187 PhysicalPlan::UniqueExec { input, subset } => {
188 let batches = execute_plan(input)?;
189 operators::unique_batches(batches, subset.as_deref())
190 }
191 PhysicalPlan::FillNullExec { input, fill } => {
192 let batches = execute_plan(input)?;
193 operators::fill_null_batches(batches, fill)
194 }
195 PhysicalPlan::DropNullsExec { input, subset } => {
196 let batches = execute_plan(input)?;
197 operators::drop_nulls_batches(batches, subset.as_deref())
198 }
199 PhysicalPlan::NullCountExec { input } => {
200 let batches = execute_plan(input)?;
201 operators::null_count_batches(batches)
202 }
203 PhysicalPlan::ExplodeExec { input, column } => {
204 let batches = execute_plan(input)?;
205 operators::explode_batches(batches, column)
206 }
207 PhysicalPlan::ImplodeExec { input } => {
208 let batches = execute_plan(input)?;
209 operators::implode_batches(batches)
210 }
211 }
212}
213
214fn execute_scan(source: &ScanSource) -> Result<Vec<RecordBatch>> {
215 operators::scan_source(source)
216}
217
218#[cfg(test)]
219mod tests {
220 use std::num::NonZeroUsize;
221 use std::sync::Arc;
222
223 use arrow::array::{Array, ArrayRef, Int64Array};
224 use arrow::datatypes::{DataType, Field, Schema};
225 use arrow::record_batch::RecordBatch;
226
227 use super::{BudgetedMaterializedExecutor, Executor};
228 use crate::physical::budget::ResourceBudget;
229 use crate::physical::{PhysicalPlan, ScanSource};
230 use crate::{DataFrame, DataFrameError};
231
232 fn batch(values: Vec<i64>) -> RecordBatch {
233 let schema = Arc::new(Schema::new(vec![Field::new(
234 "value",
235 DataType::Int64,
236 true,
237 )]));
238 RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(values)) as ArrayRef]).unwrap()
239 }
240
241 #[test]
242 fn bounded_materialization_reserves_before_retaining_and_releases_on_error() {
243 let one = batch(vec![1, 2]);
244 let bytes = super::record_batch_memory_size(&one);
245 let budget = ResourceBudget::new(bytes, NonZeroUsize::new(2).unwrap());
246 let collector = BudgetedMaterializedExecutor::new(budget.clone());
247
248 let err = collector.collect_batches(vec![Ok(one), Ok(batch(vec![3, 4]))]);
249 assert!(matches!(
250 err,
251 Err(DataFrameError::ResourceLimitExceeded { .. })
252 ));
253 assert_eq!(budget.usage().reserved_bytes, 0);
254 assert_eq!(budget.usage().reserved_batches, 0);
255 }
256
257 #[test]
258 fn bounded_materialization_transfers_output_and_releases_budget() {
259 let batch = batch(vec![1, 2]);
260 let bytes = super::record_batch_memory_size(&batch);
261 let budget = ResourceBudget::new(bytes, NonZeroUsize::new(1).unwrap());
262 let output = BudgetedMaterializedExecutor::new(budget.clone())
263 .collect_batches(vec![Ok(batch)])
264 .unwrap();
265
266 assert_eq!(output.height(), 2);
267 assert_eq!(budget.usage().reserved_bytes, 0);
268 assert_eq!(budget.usage().reserved_batches, 0);
269 }
270
271 #[test]
272 fn concat_executor_emits_all_batches_of_each_input_in_declared_order() {
273 let first = DataFrame::from_batches(vec![batch(vec![1, 2])]).unwrap();
274 let second = DataFrame::from_batches(vec![batch(vec![3, 4])]).unwrap();
275 let plan = PhysicalPlan::ConcatExec {
276 inputs: vec![
277 PhysicalPlan::ScanExec {
278 source: ScanSource::DataFrame(first.clone()),
279 },
280 PhysicalPlan::ScanExec {
281 source: ScanSource::DataFrame(second),
282 },
283 ],
284 schema: Some(first.schema()),
285 };
286
287 let batches = Executor::execute(plan).unwrap();
288 let values = batches
289 .iter()
290 .flat_map(|batch| {
291 batch
292 .column(0)
293 .as_any()
294 .downcast_ref::<Int64Array>()
295 .unwrap()
296 .values()
297 })
298 .copied()
299 .collect::<Vec<_>>();
300 assert_eq!(values, vec![1, 2, 3, 4]);
301 }
302
303 #[test]
304 fn concat_executor_rejects_a_runtime_schema_contract_violation_before_output() {
305 let first = DataFrame::from_batches(vec![batch(vec![1])]).unwrap();
306 let other_schema = Arc::new(Schema::new(vec![Field::new(
307 "other",
308 DataType::Int64,
309 true,
310 )]));
311 let second = DataFrame::from_batches(vec![RecordBatch::try_new(
312 other_schema,
313 vec![Arc::new(Int64Array::from(vec![2])) as ArrayRef],
314 )
315 .unwrap()])
316 .unwrap();
317 let plan = PhysicalPlan::ConcatExec {
318 inputs: vec![
319 PhysicalPlan::ScanExec {
320 source: ScanSource::DataFrame(first.clone()),
321 },
322 PhysicalPlan::ScanExec {
323 source: ScanSource::DataFrame(second),
324 },
325 ],
326 schema: Some(first.schema()),
327 };
328
329 let err = Executor::execute(plan).unwrap_err();
330 assert!(matches!(err, DataFrameError::SchemaMismatch { message }
331 if message.contains("concat_schema_mismatch")));
332 }
333}