1use std::sync::Arc;
8
9use arrow::array::{Array, StringArray};
10use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
11use arrow::record_batch::RecordBatch;
12
13use crate::expr::{Expr as E, Operator, Scalar};
14use crate::lazy::ProjectionKind;
15use crate::physical::budget::{
16 ResourceBudget, ResourceReservation, ResourceScope, StreamFailure, StreamFailureClass,
17 StreamOptions, StreamTerminal, StreamTerminalState,
18};
19use crate::physical::plan::{
20 ScanSource, SourceLimit, StreamingOperator, StreamingPhysicalPlan, StreamingSource,
21};
22use crate::physical::{operators, record_batch_memory_size};
23use crate::{DataFrame, DataFrameError, Expr, Result};
24
25#[derive(Debug, Clone)]
27pub struct BatchOpenContext {
28 pub options: StreamOptions,
30 pub budget: ResourceBudget,
32}
33
34impl BatchOpenContext {
35 pub fn new(options: StreamOptions) -> Self {
37 Self {
38 options,
39 budget: ResourceBudget::from_options(options),
40 }
41 }
42}
43
44#[derive(Debug)]
46pub struct StreamBatch {
47 batch: RecordBatch,
48 reservation: ResourceReservation,
49}
50
51impl StreamBatch {
52 pub fn new(batch: RecordBatch, reservation: ResourceReservation) -> Self {
54 Self { batch, reservation }
55 }
56
57 fn into_parts(self) -> (RecordBatch, ResourceReservation) {
58 (self.batch, self.reservation)
59 }
60}
61
62pub trait BatchSource: Send {
64 fn schema(&self) -> SchemaRef;
66
67 fn next_batch(&mut self) -> Result<Option<StreamBatch>>;
69
70 fn close(&mut self) -> Result<()> {
72 Ok(())
73 }
74}
75
76pub trait BatchSourceFactory {
78 fn source_name(&self) -> &'static str;
80
81 fn schema(&self) -> Result<SchemaRef>;
83
84 fn source_limits(&self) -> Vec<SourceLimit> {
86 Vec::new()
87 }
88
89 fn open(&self, context: BatchOpenContext) -> Result<Box<dyn BatchSource>>;
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct ColumnarSegmentBatchPlan {
96 pub decoded_bytes: u64,
98 pub arrow_allocation_upper_bound: u64,
100}
101
102impl ColumnarSegmentBatchPlan {
103 pub const fn reservation_upper_bound(self) -> u64 {
105 self.decoded_bytes
106 .saturating_add(self.arrow_allocation_upper_bound)
107 }
108}
109
110pub trait ColumnarSegmentCursor: Send {
116 fn schema(&self) -> SchemaRef;
118
119 fn metadata_footprint_upper_bound(&self) -> u64;
121
122 fn next_batch_plan(&self) -> Result<Option<ColumnarSegmentBatchPlan>>;
124
125 fn next_batch(&mut self) -> Result<Option<RecordBatch>>;
127
128 fn close(&mut self) -> Result<()> {
130 Ok(())
131 }
132}
133
134pub trait ColumnarSegmentProvider: Send + Sync {
136 fn source_name(&self) -> &'static str {
138 "columnar_segment"
139 }
140
141 fn source_limits(&self) -> Vec<SourceLimit>;
143
144 fn open(&self, metadata_limit_bytes: u64) -> Result<Box<dyn ColumnarSegmentCursor>>;
146}
147
148pub(crate) fn open_streaming_plan(
152 plan: StreamingPhysicalPlan,
153 context: BatchOpenContext,
154) -> Result<Box<dyn BatchSource>> {
155 Ok(Box::new(StreamingPlanBatchSource::open(plan, context)?))
156}
157
158struct StreamingPlanBatchSource {
160 runner: StreamingSourceRunner,
161 operators: Vec<RuntimeStreamingOperator>,
162 schema: SchemaRef,
163 budget: ResourceBudget,
164 closed: bool,
165}
166
167impl StreamingPlanBatchSource {
168 fn open(plan: StreamingPhysicalPlan, context: BatchOpenContext) -> Result<Self> {
169 let schema = preflight_streaming_schema(&plan, &context)?;
170 let runner = StreamingSourceRunner::open(plan.source, context.clone(), schema.clone())?;
171 Ok(Self {
172 runner,
173 operators: plan
174 .operators
175 .into_iter()
176 .map(RuntimeStreamingOperator::from)
177 .collect(),
178 schema,
179 budget: context.budget,
180 closed: false,
181 })
182 }
183
184 fn next_transformed_batch(&mut self) -> Result<Option<StreamBatch>> {
185 loop {
186 let Some(batch) = self.runner.next_batch()? else {
187 return Ok(None);
188 };
189 let mut batch = Some(batch);
190 for operator in &mut self.operators {
191 let Some(input) = batch.take() else {
192 break;
193 };
194 batch = operator.apply(input, &self.budget)?;
195 }
196 if let Some(batch) = batch {
197 return Ok(Some(batch));
198 }
199 }
200 }
201
202 fn close_once(&mut self) -> Result<()> {
203 if self.closed {
204 return Ok(());
205 }
206 self.closed = true;
207 self.runner.close()
208 }
209}
210
211impl BatchSource for StreamingPlanBatchSource {
212 fn schema(&self) -> SchemaRef {
213 self.schema.clone()
214 }
215
216 fn next_batch(&mut self) -> Result<Option<StreamBatch>> {
217 self.next_transformed_batch()
218 }
219
220 fn close(&mut self) -> Result<()> {
221 self.close_once()
222 }
223}
224
225enum StreamingSourceRunner {
228 Leaf(Box<dyn BatchSource>),
229 Concat {
230 inputs: Vec<StreamingPhysicalPlan>,
231 schema: SchemaRef,
232 context: BatchOpenContext,
233 next_input: usize,
234 active: Option<Box<StreamingPlanBatchSource>>,
235 },
236}
237
238impl StreamingSourceRunner {
239 fn open(
240 source: StreamingSource,
241 context: BatchOpenContext,
242 expected_schema: SchemaRef,
243 ) -> Result<Self> {
244 match source {
245 StreamingSource::Scan(source) => Ok(Self::Leaf(open_scan_source(source, context)?)),
246 StreamingSource::Concat { inputs, schema } => Ok(Self::Concat {
247 inputs,
248 schema: match schema {
249 Some(schema) if schema.as_ref() != expected_schema.as_ref() => {
250 return Err(DataFrameError::schema_mismatch(
251 "concat_schema_mismatch: declared schema differs from bounded preflight schema",
252 ));
253 }
254 Some(schema) => schema,
255 None => expected_schema,
256 },
257 context,
258 next_input: 0,
259 active: None,
260 }),
261 }
262 }
263
264 fn next_batch(&mut self) -> Result<Option<StreamBatch>> {
265 match self {
266 Self::Leaf(source) => source.next_batch(),
267 Self::Concat {
268 inputs,
269 schema,
270 context,
271 next_input,
272 active,
273 } => loop {
274 if let Some(child) = active.as_mut() {
275 match child.next_batch()? {
276 Some(batch) => {
277 if batch.batch.schema().as_ref() != schema.as_ref() {
278 return Err(DataFrameError::schema_mismatch(
279 "concat_schema_mismatch: child emitted a schema different from the validated concat schema",
280 ));
281 }
282 return Ok(Some(batch));
283 }
284 None => {
285 child.close()?;
286 active.take();
287 *next_input += 1;
288 }
289 }
290 continue;
291 }
292 let Some(plan) = inputs.get(*next_input).cloned() else {
293 return Ok(None);
294 };
295 *active = Some(Box::new(StreamingPlanBatchSource::open(
296 plan,
297 context.clone(),
298 )?));
299 },
300 }
301 }
302
303 fn close(&mut self) -> Result<()> {
304 match self {
305 Self::Leaf(source) => source.close(),
306 Self::Concat { active, .. } => {
307 if let Some(mut child) = active.take() {
308 child.close()?;
309 }
310 Ok(())
311 }
312 }
313 }
314}
315
316fn open_scan_source(source: ScanSource, context: BatchOpenContext) -> Result<Box<dyn BatchSource>> {
317 match source {
318 ScanSource::Csv {
319 path,
320 predicate,
321 projection,
322 } => {
323 if predicate.is_some() {
324 return Err(DataFrameError::streaming_unsupported(
325 "csv_scan",
326 "predicate_was_not_lowered_to_batch_operator",
327 ));
328 }
329 let factory = crate::io::CsvBatchSourceFactory::from_scan(path, None, projection);
330 factory.open(context)
331 }
332 ScanSource::Parquet {
333 path,
334 predicate,
335 projection,
336 } => {
337 if predicate.is_some() {
338 return Err(DataFrameError::streaming_unsupported(
339 "parquet_scan",
340 "predicate_was_not_lowered_to_batch_operator",
341 ));
342 }
343 let factory = crate::io::ParquetBatchSourceFactory::from_scan(path, None, projection);
344 factory.open(context)
345 }
346 ScanSource::DataFrame(_) => Err(DataFrameError::streaming_unsupported(
347 "dataframe_scan",
348 "legacy_materialized_source",
349 )),
350 }
351}
352
353fn preflight_streaming_schema(
354 plan: &StreamingPhysicalPlan,
355 context: &BatchOpenContext,
356) -> Result<SchemaRef> {
357 let source_schema = match &plan.source {
358 StreamingSource::Scan(source) => {
359 let mut source = open_scan_source(source.clone(), context.clone())?;
360 let schema = source.schema();
361 source.close()?;
362 schema
363 }
364 StreamingSource::Concat { inputs, schema } => {
365 let mut expected = schema.clone();
366 for (index, input) in inputs.iter().enumerate() {
367 let input_schema = preflight_streaming_schema(input, context)?;
368 if let Some(expected_schema) = &expected {
369 if input_schema.as_ref() != expected_schema.as_ref() {
370 return Err(DataFrameError::schema_mismatch(format!(
371 "concat_schema_mismatch: input {index} differs from the validated concat schema"
372 )));
373 }
374 } else {
375 expected = Some(input_schema);
376 }
377 }
378 expected.ok_or_else(|| {
379 DataFrameError::invalid_operation("concat requires at least two inputs")
380 })?
381 }
382 };
383 infer_streaming_output_schema(&source_schema, &plan.operators)
384}
385
386fn infer_streaming_output_schema(
387 source_schema: &SchemaRef,
388 operators: &[StreamingOperator],
389) -> Result<SchemaRef> {
390 let mut schema = source_schema.clone();
391 for operator in operators {
392 schema = match operator {
393 StreamingOperator::Projection { exprs, kind } => {
394 infer_projection_schema(&schema, exprs, kind)?
395 }
396 StreamingOperator::Filter { predicate } => {
397 let dtype = infer_expression_dtype(predicate, &schema)?;
398 if dtype != DataType::Boolean {
399 return Err(DataFrameError::type_mismatch(
400 None::<String>,
401 DataType::Boolean.to_string(),
402 dtype.to_string(),
403 ));
404 }
405 schema
406 }
407 StreamingOperator::ForwardSlice { .. } => schema,
408 };
409 }
410 Ok(schema)
411}
412
413fn infer_projection_schema(
414 schema: &SchemaRef,
415 expressions: &[Expr],
416 kind: &ProjectionKind,
417) -> Result<SchemaRef> {
418 match kind {
419 ProjectionKind::Select => {
420 let mut fields = Vec::new();
421 for expression in expressions {
422 match expression {
423 E::Wildcard => fields.extend(schema.fields().iter().cloned()),
424 E::Column(name) => fields.push(schema_field(schema, name)?),
425 E::Alias { expr, name } => fields.push(Arc::new(Field::new(
426 name,
427 infer_expression_dtype(expr, schema)?,
428 true,
429 ))),
430 expression => fields.push(Arc::new(Field::new(
431 format!("{expression:?}"),
432 infer_expression_dtype(expression, schema)?,
433 true,
434 ))),
435 }
436 }
437 Ok(Arc::new(Schema::new(fields)))
438 }
439 ProjectionKind::WithColumns => {
440 let mut fields = schema
441 .fields()
442 .iter()
443 .map(|field| field.as_ref().clone())
444 .collect::<Vec<_>>();
445 for expression in expressions {
446 let (name, expression) = match expression {
447 E::Alias { expr, name } => (name, expr.as_ref()),
448 E::Column(name) => (name, expression),
449 _ => {
450 return Err(DataFrameError::invalid_operation(
451 "with_columns requires alias for non-column expressions",
452 ))
453 }
454 };
455 let field = Field::new(name, infer_expression_dtype(expression, schema)?, true);
456 if let Some(index) = fields.iter().position(|existing| existing.name() == name) {
457 fields[index] = field;
458 } else {
459 fields.push(field);
460 }
461 }
462 Ok(Arc::new(Schema::new(fields)))
463 }
464 }
465}
466
467fn infer_expression_dtype(expression: &Expr, schema: &SchemaRef) -> Result<DataType> {
468 match expression {
469 E::Column(name) => Ok(schema_field(schema, name)?.data_type().clone()),
470 E::Literal(Scalar::Null) => Ok(DataType::Null),
471 E::Literal(Scalar::Boolean(_)) => Ok(DataType::Boolean),
472 E::Literal(Scalar::Int64(_)) => Ok(DataType::Int64),
473 E::Literal(Scalar::Float64(_)) => Ok(DataType::Float64),
474 E::Literal(Scalar::Utf8(_)) => Ok(DataType::Utf8),
475 E::Alias { expr, .. } => infer_expression_dtype(expr, schema),
476 E::UnaryOp { expr, .. } => {
477 let dtype = infer_expression_dtype(expr, schema)?;
478 if dtype != DataType::Boolean {
479 return Err(DataFrameError::type_mismatch(
480 None::<String>,
481 DataType::Boolean.to_string(),
482 dtype.to_string(),
483 ));
484 }
485 Ok(DataType::Boolean)
486 }
487 E::BinaryOp { left, op, right } => {
488 let left = infer_expression_dtype(left, schema)?;
489 let right = infer_expression_dtype(right, schema)?;
490 match op {
491 Operator::And | Operator::Or => {
492 if left != DataType::Boolean || right != DataType::Boolean {
493 return Err(DataFrameError::type_mismatch(
494 None::<String>,
495 DataType::Boolean.to_string(),
496 format!("{left} and {right}"),
497 ));
498 }
499 Ok(DataType::Boolean)
500 }
501 Operator::Eq
502 | Operator::Neq
503 | Operator::Gt
504 | Operator::Lt
505 | Operator::Ge
506 | Operator::Le => {
507 if left != right {
508 return Err(DataFrameError::type_mismatch(
509 None::<String>,
510 left.to_string(),
511 right.to_string(),
512 ));
513 }
514 Ok(DataType::Boolean)
515 }
516 Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => {
517 if left != right || !is_numeric_dtype(&left) {
518 return Err(DataFrameError::type_mismatch(
519 None::<String>,
520 "matching numeric operands".to_string(),
521 format!("{left} and {right}"),
522 ));
523 }
524 Ok(left)
525 }
526 }
527 }
528 E::ConcatStr { inputs, .. } => {
529 if inputs.len() < 2 {
530 return Err(DataFrameError::invalid_operation(
531 "concat_str requires at least two input expressions",
532 ));
533 }
534 for input in inputs {
535 let dtype = infer_expression_dtype(input, schema)?;
536 if dtype != DataType::Utf8 {
537 return Err(DataFrameError::type_mismatch(
538 None::<String>,
539 DataType::Utf8.to_string(),
540 dtype.to_string(),
541 ));
542 }
543 }
544 Ok(DataType::Utf8)
545 }
546 E::Wildcard => Err(DataFrameError::invalid_operation(
547 "wildcard cannot be evaluated as a standalone expression",
548 )),
549 E::Agg { .. } | E::Function { .. } => Err(DataFrameError::streaming_unsupported(
550 "expression",
551 "expression_allocation_bound_not_installed",
552 )),
553 }
554}
555
556fn schema_field(schema: &SchemaRef, name: &str) -> Result<Arc<Field>> {
557 schema
558 .fields()
559 .iter()
560 .find(|field| field.name() == name)
561 .cloned()
562 .ok_or_else(|| DataFrameError::column_not_found(name.to_string()))
563}
564
565fn is_numeric_dtype(dtype: &DataType) -> bool {
566 matches!(
567 dtype,
568 DataType::Int8
569 | DataType::Int16
570 | DataType::Int32
571 | DataType::Int64
572 | DataType::UInt8
573 | DataType::UInt16
574 | DataType::UInt32
575 | DataType::UInt64
576 | DataType::Float32
577 | DataType::Float64
578 )
579}
580
581enum RuntimeStreamingOperator {
583 Projection {
584 exprs: Vec<Expr>,
585 kind: ProjectionKind,
586 },
587 Filter {
588 predicate: Expr,
589 },
590 ForwardSlice {
591 remaining_offset: usize,
592 remaining_len: usize,
593 },
594}
595
596impl From<StreamingOperator> for RuntimeStreamingOperator {
597 fn from(operator: StreamingOperator) -> Self {
598 match operator {
599 StreamingOperator::Projection { exprs, kind } => Self::Projection { exprs, kind },
600 StreamingOperator::Filter { predicate } => Self::Filter { predicate },
601 StreamingOperator::ForwardSlice { offset, len } => Self::ForwardSlice {
602 remaining_offset: offset,
603 remaining_len: len,
604 },
605 }
606 }
607}
608
609impl RuntimeStreamingOperator {
610 fn apply(
611 &mut self,
612 input: StreamBatch,
613 budget: &ResourceBudget,
614 ) -> Result<Option<StreamBatch>> {
615 match self {
616 Self::Projection { exprs, kind } => {
617 let bound = projection_upper_bound(&input.batch, exprs)?;
618 let output = replace_with_operator_output(input, budget, bound, |batch| {
619 let mut output = operators::project_batches(vec![batch], exprs, kind.clone())?;
620 output.pop().ok_or_else(|| {
621 DataFrameError::invalid_operation("projection produced no batch")
622 })
623 })?;
624 Ok((output.batch.num_rows() > 0).then_some(output))
625 }
626 Self::Filter { predicate } => {
627 let bound = filter_upper_bound(&input.batch, predicate)?;
628 let output = replace_with_operator_output(input, budget, bound, |batch| {
629 let mut output = operators::filter_batches(vec![batch], predicate)?;
630 output.pop().ok_or_else(|| {
631 DataFrameError::invalid_operation("filter produced no batch")
632 })
633 })?;
634 Ok((output.batch.num_rows() > 0).then_some(output))
635 }
636 Self::ForwardSlice {
637 remaining_offset,
638 remaining_len,
639 } => {
640 if *remaining_len == 0 {
641 return Ok(None);
642 }
643 let (batch, mut reservation) = input.into_parts();
644 let rows = batch.num_rows();
645 let skip = (*remaining_offset).min(rows);
646 *remaining_offset -= skip;
647 let available = rows.saturating_sub(skip);
648 let take = (*remaining_len).min(available);
649 *remaining_len -= take;
650 if take == 0 {
651 reservation.release();
652 return Ok(None);
653 }
654 let output = batch.slice(skip, take);
655 reservation.shrink_to(record_batch_memory_size(&output));
656 Ok(Some(StreamBatch::new(output, reservation)))
657 }
658 }
659 }
660}
661
662fn replace_with_operator_output<F>(
666 input: StreamBatch,
667 budget: &ResourceBudget,
668 upper_bound: u64,
669 transform: F,
670) -> Result<StreamBatch>
671where
672 F: FnOnce(RecordBatch) -> Result<RecordBatch>,
673{
674 let (batch, mut source_reservation) = input.into_parts();
675 let mut output_reservation = budget.reserve(ResourceScope::Operator, upper_bound)?;
676 let output = transform(batch)?;
677
678 source_reservation.release();
680 let actual = record_batch_memory_size(&output);
681 if actual > output_reservation.bytes() {
682 return Err(DataFrameError::resource_limit_exceeded(
683 budget.memory_limit_bytes(),
684 budget
685 .usage()
686 .reserved_bytes
687 .saturating_add(actual.saturating_sub(output_reservation.bytes())),
688 budget.max_in_flight_batches(),
689 budget.usage().reserved_batches,
690 ResourceScope::Operator,
691 ));
692 }
693 output_reservation.shrink_to(actual);
694 output_reservation.promote_to_batch(ResourceScope::Output)?;
695 Ok(StreamBatch::new(output, output_reservation))
696}
697
698fn projection_upper_bound(batch: &RecordBatch, expressions: &[Expr]) -> Result<u64> {
699 expressions
700 .iter()
701 .try_fold(record_batch_memory_size(batch), |bound, expression| {
702 Ok(bound.saturating_add(expression_upper_bound(expression, batch)?))
703 })
704}
705
706fn filter_upper_bound(batch: &RecordBatch, predicate: &Expr) -> Result<u64> {
707 let batch_bytes = record_batch_memory_size(batch);
713 Ok(batch_bytes
714 .saturating_mul(2)
715 .saturating_add(expression_upper_bound(predicate, batch)?))
716}
717
718fn expression_upper_bound(expression: &Expr, batch: &RecordBatch) -> Result<u64> {
722 let rows = u64::try_from(batch.num_rows()).unwrap_or(u64::MAX);
723 match expression {
724 E::Column(_) | E::Wildcard => Ok(0),
725 E::Alias { expr, .. } => expression_upper_bound(expr, batch),
726 E::Literal(scalar) => Ok(literal_upper_bound(scalar, rows)),
727 E::UnaryOp { expr, .. } => Ok(expression_upper_bound(expr, batch)?
728 .saturating_add(fixed_width_upper_bound(&DataType::Boolean, rows))),
729 E::BinaryOp { left, op, right } => {
730 let dtype = infer_expression_dtype(expression, &batch.schema())?;
731 let output = match op {
732 Operator::And
733 | Operator::Or
734 | Operator::Eq
735 | Operator::Neq
736 | Operator::Gt
737 | Operator::Lt
738 | Operator::Ge
739 | Operator::Le => fixed_width_upper_bound(&DataType::Boolean, rows),
740 Operator::Add | Operator::Sub | Operator::Mul | Operator::Div => {
741 fixed_width_upper_bound(&dtype, rows)
742 }
743 };
744 Ok(expression_upper_bound(left, batch)?
745 .saturating_add(expression_upper_bound(right, batch)?)
746 .saturating_add(output))
747 }
748 E::ConcatStr {
749 inputs,
750 separator,
751 null_behavior,
752 } => {
753 let children = inputs.iter().try_fold(0_u64, |bound, input| {
754 Ok(bound.saturating_add(expression_upper_bound(input, batch)?))
755 })?;
756 let values = concat_string_value_upper_bound(inputs, separator, null_behavior, batch)?;
757 Ok(children.saturating_add(utf8_array_upper_bound(values, rows)))
758 }
759 E::Agg { .. } | E::Function { .. } => Err(DataFrameError::streaming_unsupported(
760 "expression",
761 "expression_allocation_bound_not_installed",
762 )),
763 }
764}
765
766fn literal_upper_bound(value: &Scalar, rows: u64) -> u64 {
767 match value {
768 Scalar::Null => 128,
769 Scalar::Boolean(_) => fixed_width_upper_bound(&DataType::Boolean, rows),
770 Scalar::Int64(_) => fixed_width_upper_bound(&DataType::Int64, rows),
771 Scalar::Float64(_) => fixed_width_upper_bound(&DataType::Float64, rows),
772 Scalar::Utf8(value) => utf8_array_upper_bound(
773 rows.saturating_mul(u64::try_from(value.len()).unwrap_or(u64::MAX)),
774 rows,
775 ),
776 }
777}
778
779const ARROW_ARRAY_ALLOCATION_OVERHEAD: u64 = 2 * 1024;
781
782fn fixed_width_upper_bound(dtype: &DataType, rows: u64) -> u64 {
783 let value_bytes = match dtype {
784 DataType::Boolean => rows.saturating_add(7) / 8,
785 DataType::Int8 | DataType::UInt8 => rows,
786 DataType::Int16 | DataType::UInt16 => rows.saturating_mul(2),
787 DataType::Int32 | DataType::UInt32 | DataType::Float32 => rows.saturating_mul(4),
788 DataType::Int64 | DataType::UInt64 | DataType::Float64 => rows.saturating_mul(8),
789 DataType::Null => 0,
790 _ => rows.saturating_mul(16),
791 };
792 value_bytes
793 .saturating_add(rows.saturating_add(7) / 8)
794 .saturating_add(ARROW_ARRAY_ALLOCATION_OVERHEAD)
798}
799
800fn utf8_array_upper_bound(value_bytes: u64, rows: u64) -> u64 {
801 value_bytes
802 .saturating_add(rows.saturating_add(1).saturating_mul(4))
803 .saturating_add(rows.saturating_add(7) / 8)
804 .saturating_add(ARROW_ARRAY_ALLOCATION_OVERHEAD)
805}
806
807fn concat_string_value_upper_bound(
808 inputs: &[Expr],
809 separator: &str,
810 null_behavior: &crate::expr::ConcatStrNullBehavior,
811 batch: &RecordBatch,
812) -> Result<u64> {
813 let rows = u64::try_from(batch.num_rows()).unwrap_or(u64::MAX);
814 let input_values = inputs.iter().try_fold(0_u64, |bound, input| {
815 Ok(bound.saturating_add(string_value_upper_bound(input, batch)?))
816 })?;
817 let separators = rows
818 .saturating_mul(u64::try_from(inputs.len().saturating_sub(1)).unwrap_or(u64::MAX))
819 .saturating_mul(u64::try_from(separator.len()).unwrap_or(u64::MAX));
820 let replacements = match null_behavior {
821 crate::expr::ConcatStrNullBehavior::Replace(value) => rows
822 .saturating_mul(u64::try_from(inputs.len()).unwrap_or(u64::MAX))
823 .saturating_mul(u64::try_from(value.len()).unwrap_or(u64::MAX)),
824 crate::expr::ConcatStrNullBehavior::Propagate
825 | crate::expr::ConcatStrNullBehavior::Ignore => 0,
826 };
827 Ok(input_values
828 .saturating_add(separators)
829 .saturating_add(replacements))
830}
831
832fn string_value_upper_bound(expression: &Expr, batch: &RecordBatch) -> Result<u64> {
833 match expression {
834 E::Column(name) => {
835 let field = schema_field(&batch.schema(), name)?;
836 if field.data_type() != &DataType::Utf8 {
837 return Err(DataFrameError::type_mismatch(
838 None::<String>,
839 DataType::Utf8.to_string(),
840 field.data_type().to_string(),
841 ));
842 }
843 let index = batch
844 .schema()
845 .fields()
846 .iter()
847 .position(|field| field.name() == name)
848 .expect("schema_field found the column");
849 let array = batch
850 .column(index)
851 .as_any()
852 .downcast_ref::<StringArray>()
853 .ok_or_else(|| {
854 DataFrameError::type_mismatch(
855 None::<String>,
856 DataType::Utf8.to_string(),
857 batch.column(index).data_type().to_string(),
858 )
859 })?;
860 Ok((0..array.len()).fold(0_u64, |bytes, index| {
861 if array.is_null(index) {
862 bytes
863 } else {
864 bytes
865 .saturating_add(u64::try_from(array.value(index).len()).unwrap_or(u64::MAX))
866 }
867 }))
868 }
869 E::Literal(Scalar::Utf8(value)) => Ok(u64::try_from(batch.num_rows())
870 .unwrap_or(u64::MAX)
871 .saturating_mul(u64::try_from(value.len()).unwrap_or(u64::MAX))),
872 E::Literal(Scalar::Null) => Ok(0),
873 E::Alias { expr, .. } => string_value_upper_bound(expr, batch),
874 E::ConcatStr {
875 inputs,
876 separator,
877 null_behavior,
878 } => concat_string_value_upper_bound(inputs, separator, null_behavior, batch),
879 _ => {
880 let dtype = infer_expression_dtype(expression, &batch.schema())?;
881 Err(DataFrameError::type_mismatch(
882 None::<String>,
883 DataType::Utf8.to_string(),
884 dtype.to_string(),
885 ))
886 }
887 }
888}
889
890pub struct DataFrameStream {
892 source: Option<Box<dyn BatchSource>>,
893 schema: SchemaRef,
894 terminal: StreamTerminalState,
895 budget: ResourceBudget,
896}
897
898impl DataFrameStream {
899 pub fn from_factory(factory: &dyn BatchSourceFactory, options: StreamOptions) -> Result<Self> {
901 let context = BatchOpenContext::new(options);
902 let source = factory.open(context.clone())?;
903 Ok(Self::from_opened_source(source, context.budget))
904 }
905
906 pub(crate) fn from_opened_source(source: Box<dyn BatchSource>, budget: ResourceBudget) -> Self {
908 let schema = source.schema();
909 Self {
910 source: Some(source),
911 schema,
912 terminal: StreamTerminalState::new(),
913 budget,
914 }
915 }
916
917 pub fn schema(&self) -> SchemaRef {
919 self.schema.clone()
920 }
921
922 pub fn status(&self) -> StreamTerminal {
924 self.terminal.status()
925 }
926
927 pub fn budget(&self) -> &ResourceBudget {
929 &self.budget
930 }
931
932 pub fn next_batch(&mut self) -> Result<Option<DataFrame>> {
934 match self.terminal.status() {
935 StreamTerminal::Open => {}
936 StreamTerminal::Exhausted => return Ok(None),
937 terminal => return Err(terminal.as_error().expect("terminal has stable error")),
938 }
939
940 let next = self
941 .source
942 .as_mut()
943 .expect("open stream always owns a source")
944 .next_batch();
945
946 match next {
947 Ok(Some(batch)) => match self.transfer_to_consumer(batch) {
948 Ok(batch) => Ok(Some(batch)),
949 Err(error) => Err(self.fail(error)),
950 },
951 Ok(None) => {
952 if let Err(error) = self.close_source() {
953 return Err(self.fail(error));
954 }
955 self.terminal.finish(StreamTerminal::Exhausted);
956 Ok(None)
957 }
958 Err(error) => Err(self.fail(error)),
959 }
960 }
961
962 pub fn close(&mut self) -> Result<()> {
964 match self.terminal.status() {
965 StreamTerminal::Open => {
966 if let Err(error) = self.close_source() {
967 return Err(self.fail(error));
968 }
969 self.terminal.finish(StreamTerminal::Closed);
970 Ok(())
971 }
972 StreamTerminal::Exhausted | StreamTerminal::Closed => Ok(()),
973 terminal => Err(terminal.as_error().expect("terminal has stable error")),
974 }
975 }
976
977 pub fn cancel(&mut self) -> Result<()> {
979 match self.terminal.status() {
980 StreamTerminal::Open => {
981 if let Err(error) = self.close_source() {
982 return Err(self.fail(error));
983 }
984 self.terminal.finish(StreamTerminal::Cancelled);
985 Ok(())
986 }
987 StreamTerminal::Cancelled => Ok(()),
988 StreamTerminal::Exhausted => Ok(()),
989 terminal => Err(terminal.as_error().expect("terminal has stable error")),
990 }
991 }
992
993 fn transfer_to_consumer(&self, batch: StreamBatch) -> Result<DataFrame> {
994 let (batch, reservation) = batch.into_parts();
995 let dataframe = DataFrame::from_batches(vec![batch]);
997 drop(reservation);
998 dataframe
999 }
1000
1001 fn close_source(&mut self) -> Result<()> {
1002 if let Some(mut source) = self.source.take() {
1003 source.close()?;
1004 }
1005 Ok(())
1006 }
1007
1008 fn fail(&mut self, error: DataFrameError) -> DataFrameError {
1009 let failure = failure_for(&error);
1010 let _ = self.close_source();
1011 let terminal = self.terminal.finish(StreamTerminal::Failed(failure));
1012 terminal
1013 .as_error()
1014 .expect("failed terminal has a stable error")
1015 }
1016}
1017
1018impl Drop for DataFrameStream {
1019 fn drop(&mut self) {
1020 if matches!(self.terminal.status(), StreamTerminal::Open) {
1021 let _ = self.close_source();
1022 self.terminal.finish(StreamTerminal::Closed);
1023 }
1024 }
1025}
1026
1027fn failure_for(error: &DataFrameError) -> StreamFailure {
1028 match error {
1029 DataFrameError::ResourceLimitExceeded { .. } => {
1030 StreamFailure::new("resource_limit_exceeded", StreamFailureClass::ResourceLimit)
1031 }
1032 DataFrameError::Io { .. } => StreamFailure::new("source_error", StreamFailureClass::Source),
1033 DataFrameError::Arrow { .. } | DataFrameError::Parquet { .. } => {
1034 StreamFailure::new("decode_error", StreamFailureClass::Decode)
1035 }
1036 DataFrameError::SchemaMismatch { .. } => {
1037 StreamFailure::new("schema_error", StreamFailureClass::Schema)
1038 }
1039 DataFrameError::StreamingUnsupported { .. }
1040 | DataFrameError::StreamingRequiresMaterialization { .. } => {
1041 StreamFailure::new("streaming_unsupported", StreamFailureClass::Unsupported)
1042 }
1043 DataFrameError::StreamClosed => {
1044 StreamFailure::new("stream_closed", StreamFailureClass::Closed)
1045 }
1046 DataFrameError::StreamCancelled => {
1047 StreamFailure::new("stream_cancelled", StreamFailureClass::Cancelled)
1048 }
1049 DataFrameError::StreamFailed {
1050 code,
1051 classification,
1052 } => StreamFailure::new(code, *classification),
1053 _ => StreamFailure::new("stream_internal", StreamFailureClass::Internal),
1054 }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use std::cell::Cell;
1060 use std::num::NonZeroUsize;
1061 use std::sync::atomic::{AtomicUsize, Ordering};
1062 use std::sync::Arc;
1063
1064 use arrow::array::{ArrayRef, Int64Array};
1065 use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
1066 use arrow::record_batch::RecordBatch;
1067
1068 use super::{BatchOpenContext, BatchSource, BatchSourceFactory, DataFrameStream, StreamBatch};
1069 use crate::physical::budget::{ResourceScope, StreamOptions, StreamTerminal};
1070 use crate::{DataFrameError, Result};
1071
1072 struct TestFactory {
1073 opened: Cell<usize>,
1074 closed: Arc<AtomicUsize>,
1075 batches: usize,
1076 }
1077
1078 impl BatchSourceFactory for TestFactory {
1079 fn source_name(&self) -> &'static str {
1080 "test"
1081 }
1082
1083 fn schema(&self) -> Result<SchemaRef> {
1084 Ok(schema())
1085 }
1086
1087 fn open(&self, context: BatchOpenContext) -> Result<Box<dyn BatchSource>> {
1088 self.opened.set(self.opened.get() + 1);
1089 Ok(Box::new(TestSource {
1090 context,
1091 closed: self.closed.clone(),
1092 remaining: self.batches,
1093 }))
1094 }
1095 }
1096
1097 struct TestSource {
1098 context: BatchOpenContext,
1099 closed: Arc<AtomicUsize>,
1100 remaining: usize,
1101 }
1102
1103 impl BatchSource for TestSource {
1104 fn schema(&self) -> SchemaRef {
1105 schema()
1106 }
1107
1108 fn next_batch(&mut self) -> Result<Option<StreamBatch>> {
1109 if self.remaining == 0 {
1110 return Ok(None);
1111 }
1112 self.remaining -= 1;
1113 let batch = batch();
1114 let reservation = self.context.budget.reserve_batch(
1115 ResourceScope::Decode,
1116 super::super::executor::record_batch_memory_size(&batch),
1117 )?;
1118 Ok(Some(StreamBatch::new(batch, reservation)))
1119 }
1120
1121 fn close(&mut self) -> Result<()> {
1122 self.closed.fetch_add(1, Ordering::SeqCst);
1123 Ok(())
1124 }
1125 }
1126
1127 fn schema() -> SchemaRef {
1128 Arc::new(Schema::new(vec![Field::new(
1129 "value",
1130 DataType::Int64,
1131 true,
1132 )]))
1133 }
1134
1135 fn batch() -> RecordBatch {
1136 RecordBatch::try_new(
1137 schema(),
1138 vec![Arc::new(Int64Array::from(vec![1_i64])) as ArrayRef],
1139 )
1140 .unwrap()
1141 }
1142
1143 fn options() -> StreamOptions {
1144 StreamOptions::new(
1145 1024,
1146 NonZeroUsize::new(1).unwrap(),
1147 NonZeroUsize::new(1).unwrap(),
1148 )
1149 }
1150
1151 #[test]
1152 fn stream_exhaustion_is_repeatable_and_releases_source() {
1153 let closed = Arc::new(AtomicUsize::new(0));
1154 let factory = TestFactory {
1155 opened: Cell::new(0),
1156 closed: closed.clone(),
1157 batches: 1,
1158 };
1159 let mut stream = DataFrameStream::from_factory(&factory, options()).unwrap();
1160
1161 assert_eq!(factory.opened.get(), 1);
1162 assert_eq!(stream.next_batch().unwrap().unwrap().height(), 1);
1163 assert!(stream.next_batch().unwrap().is_none());
1164 assert!(stream.next_batch().unwrap().is_none());
1165 assert_eq!(stream.status(), StreamTerminal::Exhausted);
1166 assert_eq!(stream.schema().fields()[0].name(), "value");
1167 assert_eq!(closed.load(Ordering::SeqCst), 1);
1168 assert_eq!(stream.budget().usage().reserved_bytes, 0);
1169 }
1170
1171 #[test]
1172 fn cancel_is_repeatable_and_prevents_extra_batches() {
1173 let closed = Arc::new(AtomicUsize::new(0));
1174 let factory = TestFactory {
1175 opened: Cell::new(0),
1176 closed: closed.clone(),
1177 batches: 2,
1178 };
1179 let mut stream = DataFrameStream::from_factory(&factory, options()).unwrap();
1180
1181 stream.cancel().unwrap();
1182 stream.cancel().unwrap();
1183 assert!(matches!(
1184 stream.next_batch(),
1185 Err(DataFrameError::StreamCancelled)
1186 ));
1187 assert_eq!(closed.load(Ordering::SeqCst), 1);
1188 }
1189}