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        // MemoryStream advertises `self.schema`, therefore emitted RecordBatches
110        // must conform to it when batches were provided with stricter nested types
111        // (e.g. MemTable accepts stricter batches via Schema::contains).
112        let batch = if batch.schema().as_ref() != self.schema.as_ref()
113            && self.schema.contains(batch.schema().as_ref())
114        {
115            datafusion_common::nested_struct::adapt_batch_to_schema(batch, &self.schema)?
116        } else {
117            batch
118        };
119
120        let Some(&fetch) = self.fetch.as_ref() else {
121            return Poll::Ready(Some(Ok(batch)));
122        };
123        if fetch == 0 {
124            return Poll::Ready(None);
125        }
126
127        let batch = if batch.num_rows() > fetch {
128            batch.slice(0, fetch)
129        } else {
130            batch
131        };
132        self.fetch = Some(fetch - batch.num_rows());
133        Poll::Ready(Some(Ok(batch)))
134    }
135
136    fn size_hint(&self) -> (usize, Option<usize>) {
137        (self.data.len(), Some(self.data.len()))
138    }
139}
140
141impl RecordBatchStream for MemoryStream {
142    /// Get the schema
143    fn schema(&self) -> SchemaRef {
144        Arc::clone(&self.schema)
145    }
146}
147
148pub trait LazyBatchGenerator: Send + Sync + fmt::Debug + fmt::Display {
149    /// Returns the generator as [`Any`] so that it can be
150    /// downcast to a specific implementation.
151    fn as_any(&self) -> &dyn Any;
152
153    fn boundedness(&self) -> Boundedness {
154        Boundedness::Bounded
155    }
156
157    /// Generate the next batch, return `None` when no more batches are available
158    fn generate_next_batch(&mut self) -> Result<Option<RecordBatch>>;
159
160    /// Returns a new instance with the state reset.
161    fn reset_state(&self) -> Arc<RwLock<dyn LazyBatchGenerator>>;
162}
163
164/// Execution plan for lazy in-memory batches of data
165///
166/// This plan generates output batches lazily, it doesn't have to buffer all batches
167/// in memory up front (compared to `MemorySourceConfig`), thus consuming constant memory.
168pub struct LazyMemoryExec {
169    /// Schema representing the data
170    schema: SchemaRef,
171    /// Optional projection for which columns to load
172    projection: Option<Vec<usize>>,
173    /// Functions to generate batches for each partition
174    batch_generators: Vec<Arc<RwLock<dyn LazyBatchGenerator>>>,
175    /// Plan properties cache storing equivalence properties, partitioning, and execution mode
176    cache: Arc<PlanProperties>,
177    /// Execution metrics
178    metrics: ExecutionPlanMetricsSet,
179}
180
181impl LazyMemoryExec {
182    /// Create a new lazy memory execution plan
183    pub fn try_new(
184        schema: SchemaRef,
185        generators: Vec<Arc<RwLock<dyn LazyBatchGenerator>>>,
186    ) -> Result<Self> {
187        let boundedness = generators
188            .iter()
189            .map(|g| g.read().boundedness())
190            .reduce(|acc, b| match acc {
191                Boundedness::Bounded => b,
192                Boundedness::Unbounded {
193                    requires_infinite_memory,
194                } => {
195                    let acc_infinite_memory = requires_infinite_memory;
196                    match b {
197                        Boundedness::Bounded => acc,
198                        Boundedness::Unbounded {
199                            requires_infinite_memory,
200                        } => Boundedness::Unbounded {
201                            requires_infinite_memory: requires_infinite_memory
202                                || acc_infinite_memory,
203                        },
204                    }
205                }
206            })
207            .unwrap_or(Boundedness::Bounded);
208
209        let cache = PlanProperties::new(
210            EquivalenceProperties::new(Arc::clone(&schema)),
211            Partitioning::RoundRobinBatch(generators.len()),
212            EmissionType::Incremental,
213            boundedness,
214        )
215        .with_scheduling_type(SchedulingType::Cooperative)
216        .into();
217
218        Ok(Self {
219            schema,
220            projection: None,
221            batch_generators: generators,
222            cache,
223            metrics: ExecutionPlanMetricsSet::new(),
224        })
225    }
226
227    pub fn with_projection(mut self, projection: Option<Vec<usize>>) -> Self {
228        match projection.as_ref() {
229            Some(columns) => {
230                let projected = Arc::new(self.schema.project(columns).unwrap());
231                Arc::make_mut(&mut self.cache).set_eq_properties(
232                    EquivalenceProperties::new(Arc::clone(&projected)),
233                );
234                self.schema = projected;
235                self.projection = projection;
236                self
237            }
238            _ => self,
239        }
240    }
241
242    pub fn try_set_partitioning(&mut self, partitioning: Partitioning) -> Result<()> {
243        let partition_count = partitioning.partition_count();
244        let generator_count = self.batch_generators.len();
245        assert_eq_or_internal_err!(
246            partition_count,
247            generator_count,
248            "Partition count must match generator count: {} != {}",
249            partition_count,
250            generator_count
251        );
252        Arc::make_mut(&mut self.cache).partitioning = partitioning;
253        Ok(())
254    }
255
256    pub fn add_ordering(&mut self, ordering: impl IntoIterator<Item = PhysicalSortExpr>) {
257        Arc::make_mut(&mut self.cache)
258            .eq_properties
259            .add_orderings(std::iter::once(ordering));
260    }
261
262    /// Get the batch generators
263    pub fn generators(&self) -> &Vec<Arc<RwLock<dyn LazyBatchGenerator>>> {
264        &self.batch_generators
265    }
266}
267
268impl fmt::Debug for LazyMemoryExec {
269    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
270        f.debug_struct("LazyMemoryExec")
271            .field("schema", &self.schema)
272            .field("batch_generators", &self.batch_generators)
273            .finish()
274    }
275}
276
277impl DisplayAs for LazyMemoryExec {
278    fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
279        match t {
280            DisplayFormatType::Default | DisplayFormatType::Verbose => {
281                write!(
282                    f,
283                    "LazyMemoryExec: partitions={}, batch_generators=[{}]",
284                    self.batch_generators.len(),
285                    self.batch_generators
286                        .iter()
287                        .map(|g| g.read().to_string())
288                        .collect::<Vec<_>>()
289                        .join(", ")
290                )
291            }
292            DisplayFormatType::TreeRender => {
293                //TODO: remove batch_size, add one line per generator
294                writeln!(
295                    f,
296                    "batch_generators={}",
297                    self.batch_generators
298                        .iter()
299                        .map(|g| g.read().to_string())
300                        .collect::<Vec<String>>()
301                        .join(", ")
302                )?;
303                Ok(())
304            }
305        }
306    }
307}
308
309impl ExecutionPlan for LazyMemoryExec {
310    fn name(&self) -> &'static str {
311        "LazyMemoryExec"
312    }
313
314    fn schema(&self) -> SchemaRef {
315        Arc::clone(&self.schema)
316    }
317
318    fn properties(&self) -> &Arc<PlanProperties> {
319        &self.cache
320    }
321
322    fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
323        vec![]
324    }
325
326    fn apply_expressions(
327        &self,
328        _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
329    ) -> Result<TreeNodeRecursion> {
330        Ok(TreeNodeRecursion::Continue)
331    }
332
333    fn replace_children(
334        self: Arc<Self>,
335        children: Vec<Arc<dyn ExecutionPlan>>,
336        _: ReplaceChildrenOptions,
337    ) -> Result<Arc<dyn ExecutionPlan>> {
338        assert_or_internal_err!(
339            children.is_empty(),
340            "Children cannot be replaced in LazyMemoryExec"
341        );
342        Ok(self)
343    }
344
345    fn with_new_children(
346        self: Arc<Self>,
347        children: Vec<Arc<dyn ExecutionPlan>>,
348    ) -> Result<Arc<dyn ExecutionPlan>> {
349        self.replace_children(
350            children,
351            ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
352        )
353    }
354
355    fn execute(
356        &self,
357        partition: usize,
358        _context: Arc<TaskContext>,
359    ) -> Result<SendableRecordBatchStream> {
360        assert_or_internal_err!(
361            partition < self.batch_generators.len(),
362            "Invalid partition {} for LazyMemoryExec with {} partitions",
363            partition,
364            self.batch_generators.len()
365        );
366
367        let baseline_metrics = BaselineMetrics::new(&self.metrics, partition);
368
369        // Create a fresh generator via reset_state() so that each execute()
370        // call produces an independent stream starting from the beginning.
371        let generator = self.batch_generators[partition].read().reset_state();
372
373        let stream = LazyMemoryStream {
374            schema: Arc::clone(&self.schema),
375            projection: self.projection.clone(),
376            generator,
377            baseline_metrics,
378        };
379        Ok(Box::pin(cooperative(stream)))
380    }
381
382    fn metrics(&self) -> Option<MetricsSet> {
383        Some(self.metrics.clone_inner())
384    }
385
386    fn reset_state(self: Arc<Self>) -> Result<Arc<dyn ExecutionPlan>> {
387        let generators = self
388            .generators()
389            .iter()
390            .map(|g| g.read().reset_state())
391            .collect::<Vec<_>>();
392        Ok(Arc::new(LazyMemoryExec {
393            schema: Arc::clone(&self.schema),
394            batch_generators: generators,
395            cache: Arc::clone(&self.cache),
396            metrics: ExecutionPlanMetricsSet::new(),
397            projection: self.projection.clone(),
398        }))
399    }
400}
401
402/// Stream that generates record batches on demand
403pub struct LazyMemoryStream {
404    schema: SchemaRef,
405    /// Optional projection for which columns to load
406    projection: Option<Vec<usize>>,
407    /// Generator to produce batches
408    ///
409    /// Note: Idiomatically, DataFusion uses plan-time parallelism - each stream
410    /// should have a unique `LazyBatchGenerator`. Use RepartitionExec or
411    /// construct multiple `LazyMemoryStream`s during planning to enable
412    /// parallel execution.
413    /// Sharing generators between streams should be used with caution.
414    generator: Arc<RwLock<dyn LazyBatchGenerator>>,
415    /// Execution metrics
416    baseline_metrics: BaselineMetrics,
417}
418
419impl Stream for LazyMemoryStream {
420    type Item = Result<RecordBatch>;
421
422    fn poll_next(
423        self: std::pin::Pin<&mut Self>,
424        _: &mut Context<'_>,
425    ) -> Poll<Option<Self::Item>> {
426        let _timer_guard = self.baseline_metrics.elapsed_compute().timer();
427        let batch = self.generator.write().generate_next_batch();
428
429        let poll = match batch {
430            Ok(Some(batch)) => {
431                // return just the columns requested
432                let batch = match self.projection.as_ref() {
433                    Some(columns) => batch.project(columns)?,
434                    None => batch,
435                };
436                Poll::Ready(Some(Ok(batch)))
437            }
438            Ok(None) => Poll::Ready(None),
439            Err(e) => Poll::Ready(Some(Err(e))),
440        };
441
442        self.baseline_metrics.record_poll(poll)
443    }
444}
445
446impl RecordBatchStream for LazyMemoryStream {
447    fn schema(&self) -> SchemaRef {
448        Arc::clone(&self.schema)
449    }
450}
451
452#[cfg(test)]
453mod lazy_memory_tests {
454    use super::*;
455    use crate::common::collect;
456    use arrow::array::Int64Array;
457    use arrow::datatypes::{DataType, Field, Schema};
458    use futures::StreamExt;
459
460    #[derive(Debug, Clone)]
461    struct TestGenerator {
462        counter: i64,
463        max_batches: i64,
464        batch_size: usize,
465        schema: SchemaRef,
466    }
467
468    impl fmt::Display for TestGenerator {
469        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
470            write!(
471                f,
472                "TestGenerator: counter={}, max_batches={}, batch_size={}",
473                self.counter, self.max_batches, self.batch_size
474            )
475        }
476    }
477
478    impl LazyBatchGenerator for TestGenerator {
479        fn as_any(&self) -> &dyn Any {
480            self
481        }
482
483        fn generate_next_batch(&mut self) -> Result<Option<RecordBatch>> {
484            if self.counter >= self.max_batches {
485                return Ok(None);
486            }
487
488            let array = Int64Array::from_iter_values(
489                (self.counter * self.batch_size as i64)
490                    ..(self.counter * self.batch_size as i64 + self.batch_size as i64),
491            );
492            self.counter += 1;
493            Ok(Some(RecordBatch::try_new(
494                Arc::clone(&self.schema),
495                vec![Arc::new(array)],
496            )?))
497        }
498
499        fn reset_state(&self) -> Arc<RwLock<dyn LazyBatchGenerator>> {
500            Arc::new(RwLock::new(TestGenerator {
501                counter: 0,
502                max_batches: self.max_batches,
503                batch_size: self.batch_size,
504                schema: Arc::clone(&self.schema),
505            }))
506        }
507    }
508
509    #[tokio::test]
510    async fn test_lazy_memory_exec() -> Result<()> {
511        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
512        let generator = TestGenerator {
513            counter: 0,
514            max_batches: 3,
515            batch_size: 2,
516            schema: Arc::clone(&schema),
517        };
518
519        let exec =
520            LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
521
522        // Test schema
523        assert_eq!(exec.schema().fields().len(), 1);
524        assert_eq!(exec.schema().field(0).name(), "a");
525
526        // Test execution
527        let stream = exec.execute(0, Arc::new(TaskContext::default()))?;
528        let batches: Vec<_> = stream.collect::<Vec<_>>().await;
529
530        assert_eq!(batches.len(), 3);
531
532        // Verify batch contents
533        let batch0 = batches[0].as_ref().unwrap();
534        let array0 = batch0
535            .column(0)
536            .as_any()
537            .downcast_ref::<Int64Array>()
538            .unwrap();
539        assert_eq!(array0.values(), &[0, 1]);
540
541        let batch1 = batches[1].as_ref().unwrap();
542        let array1 = batch1
543            .column(0)
544            .as_any()
545            .downcast_ref::<Int64Array>()
546            .unwrap();
547        assert_eq!(array1.values(), &[2, 3]);
548
549        let batch2 = batches[2].as_ref().unwrap();
550        let array2 = batch2
551            .column(0)
552            .as_any()
553            .downcast_ref::<Int64Array>()
554            .unwrap();
555        assert_eq!(array2.values(), &[4, 5]);
556
557        Ok(())
558    }
559
560    /// Verify that calling execute(0) twice on the same LazyMemoryExec
561    /// produces independent streams with the same data.
562    #[tokio::test]
563    async fn test_lazy_memory_exec_multiple_executions_are_independent() -> Result<()> {
564        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
565        let generator = TestGenerator {
566            counter: 0,
567            max_batches: 3,
568            batch_size: 2,
569            schema: Arc::clone(&schema),
570        };
571
572        let exec =
573            LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
574        let task_ctx = Arc::new(TaskContext::default());
575
576        // First execution — consume all batches
577        let batches_1 = collect(exec.execute(0, Arc::clone(&task_ctx))?).await?;
578        let total_rows_1: usize = batches_1.iter().map(|b| b.num_rows()).sum();
579        assert_eq!(total_rows_1, 6);
580
581        // Second execution — should produce the same data, not continue
582        // from where the first execution left off
583        let batches_2 = collect(exec.execute(0, Arc::clone(&task_ctx))?).await?;
584        let total_rows_2: usize = batches_2.iter().map(|b| b.num_rows()).sum();
585        assert_eq!(total_rows_2, 6);
586
587        // Verify contents are identical
588        for (b1, b2) in batches_1.iter().zip(batches_2.iter()) {
589            assert_eq!(b1, b2);
590        }
591
592        Ok(())
593    }
594
595    #[tokio::test]
596    async fn test_lazy_memory_exec_invalid_partition() -> Result<()> {
597        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
598        let generator = TestGenerator {
599            counter: 0,
600            max_batches: 1,
601            batch_size: 1,
602            schema: Arc::clone(&schema),
603        };
604
605        let exec =
606            LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
607
608        // Test invalid partition
609        let result = exec.execute(1, Arc::new(TaskContext::default()));
610
611        // partition is 0-indexed, so there only should be partition 0
612        assert!(matches!(
613            result,
614            Err(e) if e.to_string().contains("Invalid partition 1 for LazyMemoryExec with 1 partitions")
615        ));
616
617        Ok(())
618    }
619
620    #[tokio::test]
621    async fn test_generate_series_metrics_integration() -> Result<()> {
622        // Test LazyMemoryExec metrics with different configurations
623        let test_cases = vec![
624            (10, 2, 10),    // 10 rows, batch size 2, expected 10 rows
625            (100, 10, 100), // 100 rows, batch size 10, expected 100 rows
626            (5, 1, 5),      // 5 rows, batch size 1, expected 5 rows
627        ];
628
629        for (total_rows, batch_size, expected_rows) in test_cases {
630            let schema =
631                Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
632            let generator = TestGenerator {
633                counter: 0,
634                max_batches: (total_rows + batch_size - 1) / batch_size, // ceiling division
635                batch_size: batch_size as usize,
636                schema: Arc::clone(&schema),
637            };
638
639            let exec =
640                LazyMemoryExec::try_new(schema, vec![Arc::new(RwLock::new(generator))])?;
641            let task_ctx = Arc::new(TaskContext::default());
642
643            let stream = exec.execute(0, task_ctx)?;
644            let batches = collect(stream).await?;
645
646            // Verify metrics exist with actual expected numbers
647            let metrics = exec.metrics().unwrap();
648
649            // Count actual rows returned
650            let actual_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
651            assert_eq!(actual_rows, expected_rows);
652
653            // Verify metrics match actual output
654            assert_eq!(metrics.output_rows().unwrap(), expected_rows);
655            assert!(metrics.elapsed_compute().unwrap() > 0);
656        }
657
658        Ok(())
659    }
660
661    #[tokio::test]
662    async fn test_lazy_memory_exec_reset_state() -> Result<()> {
663        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)]));
664        let generator = TestGenerator {
665            counter: 0,
666            max_batches: 3,
667            batch_size: 2,
668            schema: Arc::clone(&schema),
669        };
670
671        let exec = Arc::new(LazyMemoryExec::try_new(
672            schema,
673            vec![Arc::new(RwLock::new(generator))],
674        )?);
675        let stream = exec.execute(0, Arc::new(TaskContext::default()))?;
676        let batches = collect(stream).await?;
677
678        let exec_reset = exec.reset_state()?;
679        let stream = exec_reset.execute(0, Arc::new(TaskContext::default()))?;
680        let batches_reset = collect(stream).await?;
681
682        // if the reset_state is not correct, the batches_reset will be empty
683        assert_eq!(batches, batches_reset);
684
685        Ok(())
686    }
687
688    #[tokio::test]
689    async fn test_memory_stream_emitted_batch_matches_declared_schema() -> Result<()> {
690        use arrow::array::{ArrayRef, BooleanArray, StructArray};
691        use arrow::datatypes::{DataType, Field, Fields, Schema};
692        use futures::StreamExt;
693
694        // Declared schema expects nullable struct field colA
695        let declared_fields =
696            Fields::from(vec![Field::new("colA", DataType::Boolean, true)]);
697        let declared_schema = Arc::new(Schema::new(vec![Field::new(
698            "b",
699            DataType::Struct(declared_fields),
700            false,
701        )]));
702
703        // Runtime batch has stricter non-nullable struct field colA
704        let source_fields =
705            Fields::from(vec![Field::new("colA", DataType::Boolean, false)]);
706        let source_schema = Arc::new(Schema::new(vec![Field::new(
707            "b",
708            DataType::Struct(source_fields.clone()),
709            false,
710        )]));
711
712        let struct_array: ArrayRef = Arc::new(StructArray::new(
713            source_fields,
714            vec![Arc::new(BooleanArray::from(vec![true, false]))],
715            None,
716        ));
717        let stricter_batch = RecordBatch::try_new(source_schema, vec![struct_array])?;
718
719        let mut stream = MemoryStream::try_new(
720            vec![stricter_batch],
721            Arc::clone(&declared_schema),
722            None,
723        )?;
724
725        assert_eq!(stream.schema(), declared_schema);
726
727        let emitted_batch = stream.next().await.unwrap()?;
728        assert_eq!(emitted_batch.schema(), declared_schema);
729
730        let struct_col = emitted_batch
731            .column(0)
732            .as_any()
733            .downcast_ref::<StructArray>()
734            .unwrap();
735        assert!(struct_col.fields()[0].is_nullable());
736        let bool_child = struct_col
737            .column(0)
738            .as_any()
739            .downcast_ref::<BooleanArray>()
740            .unwrap();
741        assert!(bool_child.value(0));
742        assert!(!bool_child.value(1));
743
744        Ok(())
745    }
746
747    #[tokio::test]
748    async fn test_memory_stream_emitted_batch_matches_declared_schema_with_projection()
749    -> Result<()> {
750        use arrow::array::{ArrayRef, BooleanArray, Int32Array, StructArray};
751        use arrow::datatypes::{DataType, Field, Fields, Schema};
752        use futures::StreamExt;
753
754        // Declared full schema: col a (Int32), col b (Struct<colA: nullable Boolean>)
755        let declared_fields =
756            Fields::from(vec![Field::new("colA", DataType::Boolean, true)]);
757        let full_declared_schema = Arc::new(Schema::new(vec![
758            Field::new("a", DataType::Int32, false),
759            Field::new("b", DataType::Struct(declared_fields), false),
760        ]));
761
762        // Projected schema for column "b" (projection = [1])
763        let projected_schema = Arc::new(full_declared_schema.project(&[1])?);
764
765        // Runtime batch has stricter struct
766        let source_fields =
767            Fields::from(vec![Field::new("colA", DataType::Boolean, false)]);
768        let source_schema = Arc::new(Schema::new(vec![
769            Field::new("a", DataType::Int32, false),
770            Field::new("b", DataType::Struct(source_fields.clone()), false),
771        ]));
772
773        let struct_array: ArrayRef = Arc::new(StructArray::new(
774            source_fields,
775            vec![Arc::new(BooleanArray::from(vec![true, false]))],
776            None,
777        ));
778        let stricter_batch = RecordBatch::try_new(
779            source_schema,
780            vec![Arc::new(Int32Array::from(vec![10, 20])), struct_array],
781        )?;
782
783        let mut stream = MemoryStream::try_new(
784            vec![stricter_batch],
785            Arc::clone(&projected_schema),
786            Some(vec![1]),
787        )?;
788
789        assert_eq!(stream.schema(), projected_schema);
790
791        let emitted_batch = stream.next().await.unwrap()?;
792        assert_eq!(emitted_batch.schema(), projected_schema);
793        assert_eq!(emitted_batch.num_columns(), 1);
794
795        let struct_col = emitted_batch
796            .column(0)
797            .as_any()
798            .downcast_ref::<StructArray>()
799            .unwrap();
800        assert!(struct_col.fields()[0].is_nullable());
801        let bool_child = struct_col
802            .column(0)
803            .as_any()
804            .downcast_ref::<BooleanArray>()
805            .unwrap();
806        assert!(bool_child.value(0));
807        assert!(!bool_child.value(1));
808
809        Ok(())
810    }
811
812    /// Regression for the Union reconstruction path at the `MemoryStream`
813    /// producer boundary: a declared nullable Union child vs a stricter
814    /// non-nullable runtime child.
815    #[tokio::test]
816    async fn test_memory_stream_emitted_batch_matches_declared_schema_union() -> Result<()>
817    {
818        use arrow::array::{Array, ArrayRef, Float64Array, Int32Array, UnionArray};
819        use arrow::buffer::ScalarBuffer;
820        use arrow::datatypes::{DataType, Field, Schema, UnionFields, UnionMode};
821        use futures::StreamExt;
822
823        let declared_union_fields = UnionFields::try_new(
824            vec![0_i8, 1],
825            vec![
826                Field::new("i", DataType::Int32, true),
827                Field::new("f", DataType::Float64, true),
828            ],
829        )?;
830        let declared_schema = Arc::new(Schema::new(vec![Field::new(
831            "u",
832            DataType::Union(declared_union_fields, UnionMode::Dense),
833            false,
834        )]));
835
836        let source_union_fields = UnionFields::try_new(
837            vec![0_i8, 1],
838            vec![
839                Field::new("i", DataType::Int32, false),
840                Field::new("f", DataType::Float64, false),
841            ],
842        )?;
843        let source_schema = Arc::new(Schema::new(vec![Field::new(
844            "u",
845            DataType::Union(source_union_fields.clone(), UnionMode::Dense),
846            false,
847        )]));
848
849        let type_ids = ScalarBuffer::from(vec![0_i8, 1, 0]);
850        let offsets = ScalarBuffer::from(vec![0_i32, 0, 1]);
851        let union_array: ArrayRef = Arc::new(UnionArray::try_new(
852            source_union_fields,
853            type_ids,
854            Some(offsets),
855            vec![
856                Arc::new(Int32Array::from(vec![10, 20])),
857                Arc::new(Float64Array::from(vec![1.5])),
858            ],
859        )?);
860        let stricter_batch = RecordBatch::try_new(source_schema, vec![union_array])?;
861
862        assert!(declared_schema.contains(stricter_batch.schema().as_ref()));
863
864        let mut stream = MemoryStream::try_new(
865            vec![stricter_batch],
866            Arc::clone(&declared_schema),
867            None,
868        )?;
869
870        assert_eq!(stream.schema(), declared_schema);
871
872        let emitted_batch = stream.next().await.unwrap()?;
873        assert_eq!(emitted_batch.schema(), stream.schema());
874        assert_eq!(emitted_batch.schema(), declared_schema);
875
876        let union_col = emitted_batch
877            .column(0)
878            .as_any()
879            .downcast_ref::<UnionArray>()
880            .unwrap();
881        assert_eq!(union_col.len(), 3);
882        assert_eq!(union_col.type_id(0), 0);
883        assert_eq!(union_col.type_id(1), 1);
884        assert_eq!(union_col.type_id(2), 0);
885        let i_child = union_col
886            .child(0)
887            .as_any()
888            .downcast_ref::<Int32Array>()
889            .unwrap();
890        assert_eq!(i_child.values(), &[10, 20]);
891
892        Ok(())
893    }
894
895    /// Regression for a contained `Map<.., Struct>` whose runtime nested field
896    /// is non-nullable while the declared nested field is nullable.
897    #[tokio::test]
898    async fn test_memory_stream_emitted_batch_matches_declared_schema_map_of_struct()
899    -> Result<()> {
900        use arrow::array::{
901            Array, ArrayRef, Int32Array, MapArray, StringArray, StructArray,
902        };
903        use arrow::buffer::OffsetBuffer;
904        use arrow::datatypes::{DataType, Field, Fields, Schema};
905        use futures::StreamExt;
906
907        fn map_field(value_child_nullable: bool) -> Field {
908            let value_struct = DataType::Struct(Fields::from(vec![Field::new(
909                "v",
910                DataType::Int32,
911                value_child_nullable,
912            )]));
913            let entries = Field::new(
914                "entries",
915                DataType::Struct(Fields::from(vec![
916                    Field::new("keys", DataType::Utf8, false),
917                    Field::new("values", value_struct, true),
918                ])),
919                false,
920            );
921            Field::new("m", DataType::Map(Arc::new(entries), false), true)
922        }
923
924        let declared_schema = Arc::new(Schema::new(vec![map_field(true)]));
925        let source_schema = Arc::new(Schema::new(vec![map_field(false)]));
926
927        let value_fields = Fields::from(vec![Field::new("v", DataType::Int32, false)]);
928        let values_struct = StructArray::new(
929            value_fields,
930            vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
931            None,
932        );
933        let entries = StructArray::new(
934            Fields::from(vec![
935                Field::new("keys", DataType::Utf8, false),
936                Field::new("values", values_struct.data_type().clone(), true),
937            ]),
938            vec![
939                Arc::new(StringArray::from(vec!["a", "b", "c"])) as ArrayRef,
940                Arc::new(values_struct) as ArrayRef,
941            ],
942            None,
943        );
944        let DataType::Map(source_entries_field, _) = source_schema.field(0).data_type()
945        else {
946            unreachable!("map field")
947        };
948        let map_array: ArrayRef = Arc::new(MapArray::try_new(
949            Arc::clone(source_entries_field),
950            OffsetBuffer::new(vec![0, 2, 3].into()),
951            entries,
952            None,
953            false,
954        )?);
955        let stricter_batch = RecordBatch::try_new(source_schema, vec![map_array])?;
956
957        // The stricter batch is accepted by `MemTable::try_new`-style checks.
958        assert!(declared_schema.contains(stricter_batch.schema().as_ref()));
959
960        let mut stream = MemoryStream::try_new(
961            vec![stricter_batch],
962            Arc::clone(&declared_schema),
963            None,
964        )?;
965
966        assert_eq!(stream.schema(), declared_schema);
967
968        let emitted_batch = stream.next().await.unwrap()?;
969        assert_eq!(emitted_batch.schema(), stream.schema());
970        assert_eq!(emitted_batch.schema(), declared_schema);
971
972        let map_col = emitted_batch
973            .column(0)
974            .as_any()
975            .downcast_ref::<MapArray>()
976            .unwrap();
977        assert_eq!(map_col.len(), 2);
978        let values = map_col
979            .values()
980            .as_any()
981            .downcast_ref::<StructArray>()
982            .unwrap();
983        assert!(values.fields()[0].is_nullable());
984        let ints = values
985            .column(0)
986            .as_any()
987            .downcast_ref::<Int32Array>()
988            .unwrap();
989        assert_eq!(ints.values(), &[1, 2, 3]);
990
991        Ok(())
992    }
993}