Skip to main content

datafusion_physical_plan/
memory.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
18//! Execution plan for reading in-memory batches of data
19
20use std::any::Any;
21use std::fmt;
22use std::sync::Arc;
23use std::task::{Context, Poll};
24
25use crate::coop::cooperative;
26use crate::execution_plan::{Boundedness, EmissionType, SchedulingType};
27use crate::metrics::{BaselineMetrics, ExecutionPlanMetricsSet, MetricsSet};
28use crate::{
29    ChildrenPropertiesMode, DisplayAs, DisplayFormatType, ExecutionPlan, Partitioning,
30    PlanProperties, RecordBatchStream, ReplaceChildrenOptions, SendableRecordBatchStream,
31};
32
33use arrow::array::RecordBatch;
34use arrow::datatypes::SchemaRef;
35use datafusion_common::tree_node::TreeNodeRecursion;
36use datafusion_common::{Result, assert_eq_or_internal_err, assert_or_internal_err};
37use datafusion_execution::TaskContext;
38use datafusion_execution::memory_pool::MemoryReservation;
39use datafusion_physical_expr::{EquivalenceProperties, PhysicalExpr};
40
41use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr;
42use futures::Stream;
43use parking_lot::RwLock;
44
45/// Iterator over batches
46pub struct MemoryStream {
47    /// Vector of record batches
48    data: Vec<RecordBatch>,
49    /// Optional memory reservation bound to the data, freed on drop
50    reservation: Option<MemoryReservation>,
51    /// Schema representing the data
52    schema: SchemaRef,
53    /// Optional projection for which columns to load
54    projection: Option<Vec<usize>>,
55    /// Index into the data
56    index: usize,
57    /// The remaining number of rows to return. If None, all rows are returned
58    fetch: Option<usize>,
59}
60
61impl MemoryStream {
62    /// Create an iterator for a vector of record batches
63    pub fn try_new(
64        data: Vec<RecordBatch>,
65        schema: SchemaRef,
66        projection: Option<Vec<usize>>,
67    ) -> Result<Self> {
68        Ok(Self {
69            data,
70            reservation: None,
71            schema,
72            projection,
73            index: 0,
74            fetch: None,
75        })
76    }
77
78    /// Set the memory reservation for the data
79    pub fn with_reservation(mut self, reservation: MemoryReservation) -> Self {
80        self.reservation = Some(reservation);
81        self
82    }
83
84    /// Set the number of rows to produce
85    pub fn with_fetch(mut self, fetch: Option<usize>) -> Self {
86        self.fetch = fetch;
87        self
88    }
89}
90
91impl Stream for MemoryStream {
92    type Item = Result<RecordBatch>;
93
94    fn poll_next(
95        mut self: std::pin::Pin<&mut Self>,
96        _: &mut Context<'_>,
97    ) -> Poll<Option<Self::Item>> {
98        if self.index >= self.data.len() {
99            return Poll::Ready(None);
100        }
101        self.index += 1;
102        let batch = &self.data[self.index - 1];
103        // return just the columns requested
104        let batch = match self.projection.as_ref() {
105            Some(columns) => batch.project(columns)?,
106            None => batch.clone(),
107        };
108
109        let Some(&fetch) = self.fetch.as_ref() else {
110            return Poll::Ready(Some(Ok(batch)));
111        };
112        if fetch == 0 {
113            return Poll::Ready(None);
114        }
115
116        let batch = if batch.num_rows() > fetch {
117            batch.slice(0, fetch)
118        } else {
119            batch
120        };
121        self.fetch = Some(fetch - batch.num_rows());
122        Poll::Ready(Some(Ok(batch)))
123    }
124
125    fn size_hint(&self) -> (usize, Option<usize>) {
126        (self.data.len(), Some(self.data.len()))
127    }
128}
129
130impl RecordBatchStream for MemoryStream {
131    /// Get the schema
132    fn schema(&self) -> SchemaRef {
133        Arc::clone(&self.schema)
134    }
135}
136
137pub trait LazyBatchGenerator: Send + Sync + fmt::Debug + fmt::Display {
138    /// Returns the generator as [`Any`] so that it can be
139    /// downcast to a specific implementation.
140    fn as_any(&self) -> &dyn Any;
141
142    fn boundedness(&self) -> Boundedness {
143        Boundedness::Bounded
144    }
145
146    /// Generate the next batch, return `None` when no more batches are available
147    fn generate_next_batch(&mut self) -> Result<Option<RecordBatch>>;
148
149    /// Returns a new instance with the state reset.
150    fn reset_state(&self) -> Arc<RwLock<dyn LazyBatchGenerator>>;
151}
152
153/// Execution plan for lazy in-memory batches of data
154///
155/// This plan generates output batches lazily, it doesn't have to buffer all batches
156/// in memory up front (compared to `MemorySourceConfig`), thus consuming constant memory.
157pub struct LazyMemoryExec {
158    /// Schema representing the data
159    schema: SchemaRef,
160    /// Optional projection for which columns to load
161    projection: Option<Vec<usize>>,
162    /// Functions to generate batches for each partition
163    batch_generators: Vec<Arc<RwLock<dyn LazyBatchGenerator>>>,
164    /// Plan properties cache storing equivalence properties, partitioning, and execution mode
165    cache: Arc<PlanProperties>,
166    /// Execution metrics
167    metrics: ExecutionPlanMetricsSet,
168}
169
170impl LazyMemoryExec {
171    /// Create a new lazy memory execution plan
172    pub fn try_new(
173        schema: SchemaRef,
174        generators: Vec<Arc<RwLock<dyn LazyBatchGenerator>>>,
175    ) -> Result<Self> {
176        let boundedness = generators
177            .iter()
178            .map(|g| g.read().boundedness())
179            .reduce(|acc, b| match acc {
180                Boundedness::Bounded => b,
181                Boundedness::Unbounded {
182                    requires_infinite_memory,
183                } => {
184                    let acc_infinite_memory = requires_infinite_memory;
185                    match b {
186                        Boundedness::Bounded => acc,
187                        Boundedness::Unbounded {
188                            requires_infinite_memory,
189                        } => Boundedness::Unbounded {
190                            requires_infinite_memory: requires_infinite_memory
191                                || acc_infinite_memory,
192                        },
193                    }
194                }
195            })
196            .unwrap_or(Boundedness::Bounded);
197
198        let cache = PlanProperties::new(
199            EquivalenceProperties::new(Arc::clone(&schema)),
200            Partitioning::RoundRobinBatch(generators.len()),
201            EmissionType::Incremental,
202            boundedness,
203        )
204        .with_scheduling_type(SchedulingType::Cooperative)
205        .into();
206
207        Ok(Self {
208            schema,
209            projection: None,
210            batch_generators: generators,
211            cache,
212            metrics: ExecutionPlanMetricsSet::new(),
213        })
214    }
215
216    pub fn with_projection(mut self, projection: Option<Vec<usize>>) -> Self {
217        match projection.as_ref() {
218            Some(columns) => {
219                let projected = Arc::new(self.schema.project(columns).unwrap());
220                Arc::make_mut(&mut self.cache).set_eq_properties(
221                    EquivalenceProperties::new(Arc::clone(&projected)),
222                );
223                self.schema = projected;
224                self.projection = projection;
225                self
226            }
227            _ => self,
228        }
229    }
230
231    pub fn try_set_partitioning(&mut self, partitioning: Partitioning) -> Result<()> {
232        let partition_count = partitioning.partition_count();
233        let generator_count = self.batch_generators.len();
234        assert_eq_or_internal_err!(
235            partition_count,
236            generator_count,
237            "Partition count must match generator count: {} != {}",
238            partition_count,
239            generator_count
240        );
241        Arc::make_mut(&mut self.cache).partitioning = partitioning;
242        Ok(())
243    }
244
245    pub fn add_ordering(&mut self, ordering: impl IntoIterator<Item = PhysicalSortExpr>) {
246        Arc::make_mut(&mut self.cache)
247            .eq_properties
248            .add_orderings(std::iter::once(ordering));
249    }
250
251    /// Get the batch generators
252    pub fn generators(&self) -> &Vec<Arc<RwLock<dyn LazyBatchGenerator>>> {
253        &self.batch_generators
254    }
255}
256
257impl fmt::Debug for LazyMemoryExec {
258    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
259        f.debug_struct("LazyMemoryExec")
260            .field("schema", &self.schema)
261            .field("batch_generators", &self.batch_generators)
262            .finish()
263    }
264}
265
266impl DisplayAs for LazyMemoryExec {
267    fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
268        match t {
269            DisplayFormatType::Default | DisplayFormatType::Verbose => {
270                write!(
271                    f,
272                    "LazyMemoryExec: partitions={}, batch_generators=[{}]",
273                    self.batch_generators.len(),
274                    self.batch_generators
275                        .iter()
276                        .map(|g| g.read().to_string())
277                        .collect::<Vec<_>>()
278                        .join(", ")
279                )
280            }
281            DisplayFormatType::TreeRender => {
282                //TODO: remove batch_size, add one line per generator
283                writeln!(
284                    f,
285                    "batch_generators={}",
286                    self.batch_generators
287                        .iter()
288                        .map(|g| g.read().to_string())
289                        .collect::<Vec<String>>()
290                        .join(", ")
291                )?;
292                Ok(())
293            }
294        }
295    }
296}
297
298impl ExecutionPlan for LazyMemoryExec {
299    fn name(&self) -> &'static str {
300        "LazyMemoryExec"
301    }
302
303    fn schema(&self) -> SchemaRef {
304        Arc::clone(&self.schema)
305    }
306
307    fn properties(&self) -> &Arc<PlanProperties> {
308        &self.cache
309    }
310
311    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
312        vec![]
313    }
314
315    fn apply_expressions(
316        &self,
317        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
318    ) -> Result<TreeNodeRecursion> {
319        Ok(TreeNodeRecursion::Continue)
320    }
321
322    fn replace_children(
323        self: Arc<Self>,
324        children: Vec<Arc<dyn ExecutionPlan>>,
325        _: ReplaceChildrenOptions,
326    ) -> Result<Arc<dyn ExecutionPlan>> {
327        assert_or_internal_err!(
328            children.is_empty(),
329            "Children cannot be replaced in LazyMemoryExec"
330        );
331        Ok(self)
332    }
333
334    fn with_new_children(
335        self: Arc<Self>,
336        children: Vec<Arc<dyn ExecutionPlan>>,
337    ) -> Result<Arc<dyn ExecutionPlan>> {
338        self.replace_children(
339            children,
340            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
341        )
342    }
343
344    fn execute(
345        &self,
346        partition: usize,
347        _context: Arc<TaskContext>,
348    ) -> Result<SendableRecordBatchStream> {
349        assert_or_internal_err!(
350            partition < self.batch_generators.len(),
351            "Invalid partition {} for LazyMemoryExec with {} partitions",
352            partition,
353            self.batch_generators.len()
354        );
355
356        let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
357
358        // Create a fresh generator via reset_state() so that each execute()
359        // call produces an independent stream starting from the beginning.
360        let generator = self.batch_generators[partition].read().reset_state();
361
362        let stream = LazyMemoryStream {
363            schema: Arc::clone(&self.schema),
364            projection: self.projection.clone(),
365            generator,
366            baseline_metrics,
367        };
368        Ok(Box::pin(cooperative(stream)))
369    }
370
371    fn metrics(&self) -> Option<MetricsSet> {
372        Some(self.metrics.clone_inner())
373    }
374
375    fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
376        let generators = self
377            .generators()
378            .iter()
379            .map(|g| g.read().reset_state())
380            .collect::<Vec<_>>();
381        Ok(Arc::new(LazyMemoryExec {
382            schema: Arc::clone(&self.schema),
383            batch_generators: generators,
384            cache: Arc::clone(&self.cache),
385            metrics: ExecutionPlanMetricsSet::new(),
386            projection: self.projection.clone(),
387        }))
388    }
389}
390
391/// Stream that generates record batches on demand
392pub struct LazyMemoryStream {
393    schema: SchemaRef,
394    /// Optional projection for which columns to load
395    projection: Option<Vec<usize>>,
396    /// Generator to produce batches
397    ///
398    /// Note: Idiomatically, DataFusion uses plan-time parallelism - each stream
399    /// should have a unique `LazyBatchGenerator`. Use RepartitionExec or
400    /// construct multiple `LazyMemoryStream`s during planning to enable
401    /// parallel execution.
402    /// Sharing generators between streams should be used with caution.
403    generator: Arc<RwLock<dyn LazyBatchGenerator>>,
404    /// Execution metrics
405    baseline_metrics: BaselineMetrics,
406}
407
408impl Stream for LazyMemoryStream {
409    type Item = Result<RecordBatch>;
410
411    fn poll_next(
412        self: std::pin::Pin<&mut Self>,
413        _: &mut Context<'_>,
414    ) -> Poll<Option<Self::Item>> {
415        let _timer_guard = self.baseline_metrics.elapsed_compute().timer();
416        let batch = self.generator.write().generate_next_batch();
417
418        let poll = match batch {
419            Ok(Some(batch)) => {
420                // return just the columns requested
421                let batch = match self.projection.as_ref() {
422                    Some(columns) => batch.project(columns)?,
423                    None => batch,
424                };
425                Poll::Ready(Some(Ok(batch)))
426            }
427            Ok(None) => Poll::Ready(None),
428            Err(e) => Poll::Ready(Some(Err(e))),
429        };
430
431        self.baseline_metrics.record_poll(poll)
432    }
433}
434
435impl RecordBatchStream for LazyMemoryStream {
436    fn schema(&self) -> SchemaRef {
437        Arc::clone(&self.schema)
438    }
439}
440
441#[cfg(test)]
442mod lazy_memory_tests {
443    use super::*;
444    use crate::common::collect;
445    use arrow::array::Int64Array;
446    use arrow::datatypes::{DataType, Field, Schema};
447    use futures::StreamExt;
448
449    #[derive(Debug, Clone)]
450    struct TestGenerator {
451        counter: i64,
452        max_batches: i64,
453        batch_size: usize,
454        schema: SchemaRef,
455    }
456
457    impl fmt::Display for TestGenerator {
458        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
459            write!(
460                f,
461                "TestGenerator: counter={}, max_batches={}, batch_size={}",
462                self.counter, self.max_batches, self.batch_size
463            )
464        }
465    }
466
467    impl LazyBatchGenerator for TestGenerator {
468        fn as_any(&self) -> &dyn Any {
469            self
470        }
471
472        fn generate_next_batch(&mut self) -> Result<Option<RecordBatch>> {
473            if self.counter >= self.max_batches {
474                return Ok(None);
475            }
476
477            let array = Int64Array::from_iter_values(
478                (self.counter * self.batch_size as i64)
479                    ..(self.counter * self.batch_size as i64 + self.batch_size as i64),
480            );
481            self.counter += 1;
482            Ok(Some(RecordBatch::try_new(
483                Arc::clone(&self.schema),
484                vec![Arc::new(array)],
485            )?))
486        }
487
488        fn reset_state(&self) -> Arc<RwLock<dyn LazyBatchGenerator>> {
489            Arc::new(RwLock::new(TestGenerator {
490                counter: 0,
491                max_batches: self.max_batches,
492                batch_size: self.batch_size,
493                schema: Arc::clone(&self.schema),
494            }))
495        }
496    }
497
498    #[tokio::test]
499    async fn test_lazy_memory_exec() -> Result<()> {
500        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
501        let generator = TestGenerator {
502            counter: 0,
503            max_batches: 3,
504            batch_size: 2,
505            schema: Arc::clone(&schema),
506        };
507
508        let exec =
509            LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
510
511        // Test schema
512        assert_eq!(exec.schema().fields().len(), 1);
513        assert_eq!(exec.schema().field(0).name(), "a");
514
515        // Test execution
516        let stream = exec.execute(0, Arc::new(TaskContext::default()))?;
517        let batches: Vec<_> = stream.collect::<Vec<_>>().await;
518
519        assert_eq!(batches.len(), 3);
520
521        // Verify batch contents
522        let batch0 = batches[0].as_ref().unwrap();
523        let array0 = batch0
524            .column(0)
525            .as_any()
526            .downcast_ref::<Int64Array>()
527            .unwrap();
528        assert_eq!(array0.values(), &[0, 1]);
529
530        let batch1 = batches[1].as_ref().unwrap();
531        let array1 = batch1
532            .column(0)
533            .as_any()
534            .downcast_ref::<Int64Array>()
535            .unwrap();
536        assert_eq!(array1.values(), &[2, 3]);
537
538        let batch2 = batches[2].as_ref().unwrap();
539        let array2 = batch2
540            .column(0)
541            .as_any()
542            .downcast_ref::<Int64Array>()
543            .unwrap();
544        assert_eq!(array2.values(), &[4, 5]);
545
546        Ok(())
547    }
548
549    /// Verify that calling execute(0) twice on the same LazyMemoryExec
550    /// produces independent streams with the same data.
551    #[tokio::test]
552    async fn test_lazy_memory_exec_multiple_executions_are_independent() -> Result<()> {
553        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
554        let generator = TestGenerator {
555            counter: 0,
556            max_batches: 3,
557            batch_size: 2,
558            schema: Arc::clone(&schema),
559        };
560
561        let exec =
562            LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
563        let task_ctx = Arc::new(TaskContext::default());
564
565        // First execution — consume all batches
566        let batches_1 = collect(exec.execute(0, Arc::clone(&task_ctx))?).await?;
567        let total_rows_1: usize = batches_1.iter().map(|b| b.num_rows()).sum();
568        assert_eq!(total_rows_1, 6);
569
570        // Second execution — should produce the same data, not continue
571        // from where the first execution left off
572        let batches_2 = collect(exec.execute(0, Arc::clone(&task_ctx))?).await?;
573        let total_rows_2: usize = batches_2.iter().map(|b| b.num_rows()).sum();
574        assert_eq!(total_rows_2, 6);
575
576        // Verify contents are identical
577        for (b1, b2) in batches_1.iter().zip(batches_2.iter()) {
578            assert_eq!(b1, b2);
579        }
580
581        Ok(())
582    }
583
584    #[tokio::test]
585    async fn test_lazy_memory_exec_invalid_partition() -> Result<()> {
586        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
587        let generator = TestGenerator {
588            counter: 0,
589            max_batches: 1,
590            batch_size: 1,
591            schema: Arc::clone(&schema),
592        };
593
594        let exec =
595            LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
596
597        // Test invalid partition
598        let result = exec.execute(1, Arc::new(TaskContext::default()));
599
600        // partition is 0-indexed, so there only should be partition 0
601        assert!(matches!(
602            result,
603            Err(e) if e.to_string().contains("Invalid partition 1 for LazyMemoryExec with 1 partitions")
604        ));
605
606        Ok(())
607    }
608
609    #[tokio::test]
610    async fn test_generate_series_metrics_integration() -> Result<()> {
611        // Test LazyMemoryExec metrics with different configurations
612        let test_cases = vec![
613            (10, 2, 10),    // 10 rows, batch size 2, expected 10 rows
614            (100, 10, 100), // 100 rows, batch size 10, expected 100 rows
615            (5, 1, 5),      // 5 rows, batch size 1, expected 5 rows
616        ];
617
618        for (total_rows, batch_size, expected_rows) in test_cases {
619            let schema =
620                Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
621            let generator = TestGenerator {
622                counter: 0,
623                max_batches: (total_rows + batch_size - 1) / batch_size, // ceiling division
624                batch_size: batch_size as usize,
625                schema: Arc::clone(&schema),
626            };
627
628            let exec =
629                LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
630            let task_ctx = Arc::new(TaskContext::default());
631
632            let stream = exec.execute(0, task_ctx)?;
633            let batches = collect(stream).await?;
634
635            // Verify metrics exist with actual expected numbers
636            let metrics = exec.metrics().unwrap();
637
638            // Count actual rows returned
639            let actual_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
640            assert_eq!(actual_rows, expected_rows);
641
642            // Verify metrics match actual output
643            assert_eq!(metrics.output_rows().unwrap(), expected_rows);
644            assert!(metrics.elapsed_compute().unwrap() > 0);
645        }
646
647        Ok(())
648    }
649
650    #[tokio::test]
651    async fn test_lazy_memory_exec_reset_state() -> Result<()> {
652        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
653        let generator = TestGenerator {
654            counter: 0,
655            max_batches: 3,
656            batch_size: 2,
657            schema: Arc::clone(&schema),
658        };
659
660        let exec = Arc::new(LazyMemoryExec::try_new(
661            schema,
662            vec![Arc::new(RwLock::new(generator))],
663        )?);
664        let stream = exec.execute(0, Arc::new(TaskContext::default()))?;
665        let batches = collect(stream).await?;
666
667        let exec_reset = exec.reset_state()?;
668        let stream = exec_reset.execute(0, Arc::new(TaskContext::default()))?;
669        let batches_reset = collect(stream).await?;
670
671        // if the reset_state is not correct, the batches_reset will be empty
672        assert_eq!(batches, batches_reset);
673
674        Ok(())
675    }
676}