Skip to main content

linkrs_query/executor/streaming/operators/
source_operator.rs

1use std::ops::Range;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5
6use crate::executor::streaming::chunk::DataChunk;
7use crate::executor::streaming::plan::types::PhysicalOperatorId;
8use crate::executor::streaming::runtime::ExecutionRuntime;
9use crate::executor::streaming::slot::SlotLayout;
10use crate::executor::streaming::state::{GlobalState, GlobalStateKey, StateArenaSet};
11use crate::storage::EdgeCursor;
12use crate::storage::IndexCursor;
13use crate::storage::IndexRow;
14use crate::storage::ScanPredicate;
15use crate::storage::ShardSlice;
16use crate::storage::VertexCursor;
17use linkrs_core::error::QueryError;
18use linkrs_core::types::storage_ids::VertexId;
19use linkrs_core::Value;
20
21use super::spec::{BoundIndexPredicate, IndexProjection};
22
23mod buffered;
24mod index_scan;
25mod neighbors;
26mod point_lookup;
27mod storage_scan;
28mod util;
29
30pub use neighbors::NeighborScanState;
31pub use storage_scan::{column_block_enabled, set_column_block_enabled};
32
33/// Immutable per-operator execution config injected at `open()`.
34///
35/// Mirrors the immutable fields of the executor's [`OperatorBase`] so the
36/// operator hot path (in particular `next()`) never needs the base passed
37/// down as a parameter.
38#[derive(Debug, Clone, Copy)]
39pub struct OperatorConfig {
40    pub chunk_size: usize,
41    pub partition_id: Option<usize>,
42    pub physical_operator_id: PhysicalOperatorId,
43}
44
45impl Default for OperatorConfig {
46    fn default() -> Self {
47        Self {
48            chunk_size: 2048,
49            partition_id: None,
50            physical_operator_id: PhysicalOperatorId(0),
51        }
52    }
53}
54
55/// Source operator kind with arena-based state for counters.
56///
57/// Heavy mutable resources (cursors) are kept inline for practical lifetime
58/// management; simple counters and state machines live in the `SourceState`
59/// arena on the execution runtime.
60#[derive(Debug)]
61pub enum SourceOperatorKind {
62    /// Buffered vertex scan — rows come from the spec.
63    ScanVertices {
64        buffer: Vec<Vec<Value>>,
65        current_index: usize,
66        col_names: Vec<String>,
67    },
68    /// Recursive-CTE working-table scan — rows are snapshotted from the
69    /// enclosing fixpoint operator's published table at `open()`/`reset()`
70    /// (once per step iteration) and served from the in-kind buffer.
71    CteScan {
72        cte_name: String,
73        col_names: Vec<String>,
74        buffer: Vec<Vec<Value>>,
75        current_index: usize,
76    },
77    /// Standalone DML values. Expressions are evaluated once in `open()`
78    /// (i.e. once per execution) so volatile expressions such as `now()`
79    /// are resolved at execution time.
80    StandaloneValues {
81        values: Vec<Vec<linkrs_core::types::expr::ContextualExpression>>,
82        buffer: Vec<Vec<Value>>,
83        current_index: usize,
84        col_names: Vec<String>,
85    },
86    /// Storage-backed vertex scan — rows come from a storage cursor.
87    StorageScanVertices {
88        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
89        space_name: String,
90        limit: Option<usize>,
91        partition_range: Option<std::ops::Range<i64>>,
92        /// Shard slice dividing enumeration across partitions (see the spec
93        /// field): forwarded into the storage scan options at open.
94        shard_slice: Option<crate::storage::ShardSlice>,
95        col_names: Vec<String>,
96        projected_properties: Vec<Arc<str>>,
97        /// Identity mode (from the scan annotation): emit `Value::VertexId`
98        /// in the entity column and skip the per-row property map.
99        identity_only: bool,
100        /// Rowless mode: skip the row view, emit typed columns only.
101        /// Honored only when the columnar path is on.
102        skip_rows: bool,
103        /// Scan predicates pushed into the storage layer.
104        predicate: Vec<ScanPredicate>,
105        /// Tag-restricted scan: only rows of this tag are scanned at the
106        /// storage layer.
107        tag: Option<String>,
108        /// Optional semi-mask pushed from join or expand: decode exactly
109        /// these global internal ids instead of enumerating the table.
110        semi_mask: Option<Vec<u32>>,
111        cursor: Option<Box<dyn VertexCursor>>,
112    },
113    /// Buffered edge scan — rows come from the spec.
114    ScanEdges {
115        buffer: Vec<Vec<Value>>,
116        current_index: usize,
117        col_names: Vec<String>,
118    },
119    /// Storage-backed edge scan — rows come from a storage cursor.
120    StorageScanEdges {
121        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
122        space_name: String,
123        limit: Option<usize>,
124        edge_type: Option<String>,
125        shard_slice: Option<ShardSlice>,
126        col_names: Vec<String>,
127        projected_properties: Vec<Arc<str>>,
128        /// Identity mode (from the scan annotation): emit
129        /// `Value::EdgeHeader` in the entity column and skip the per-row
130        /// property map.
131        identity_only: bool,
132        /// Scan predicates pushed into the storage layer.
133        predicate: Vec<ScanPredicate>,
134        cursor: Option<Box<dyn EdgeCursor>>,
135    },
136    /// Fetch vertices by ID.
137    GetVertices {
138        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
139        space_name: String,
140        tag: String,
141        vertex_ids: Option<Vec<Value>>,
142        cached_ids: Vec<VertexId>,
143        projected_properties: Vec<Arc<str>>,
144    },
145    /// Fetch edges by src/dst/type/rank.
146    GetEdges {
147        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
148        space_name: String,
149        edge_type: Option<String>,
150        src: Option<String>,
151        dst: Option<String>,
152        rank: i64,
153        projected_properties: Vec<Arc<str>>,
154        cursor: Option<Box<dyn EdgeCursor>>,
155    },
156    /// Traverse neighbors of each input vertex.
157    GetNeighbors {
158        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
159        space_name: String,
160        tag: String,
161        direction: String,
162        projected_properties: Vec<Arc<str>>,
163        state: NeighborScanState,
164    },
165    /// Index scan with typed predicate and projection.
166    IndexScan {
167        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
168        space_name: String,
169        index_name: String,
170        index_id: u64,
171        tag: String,
172        predicate: BoundIndexPredicate,
173        projection: IndexProjection,
174        output_layout: Arc<SlotLayout>,
175        partition_range: Option<Range<i64>>,
176        cursor: Option<Box<dyn IndexCursor<Row = IndexRow>>>,
177        edge_type_names: std::collections::HashMap<u32, String>,
178    },
179    Argument,
180    /// Property retrieval as a zero-input source (prefer the unary GetV).
181    GetProp {
182        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
183        space_name: String,
184        entity_slot: usize,
185        prop_names: Vec<String>,
186        is_vertex: bool,
187        output_layout: Arc<SlotLayout>,
188    },
189    Start,
190}
191
192/// Source operator.
193///
194/// Wraps [`SourceOperatorKind`] with the runtime context injected at
195/// `open()`. Lifecycle state is owned exclusively by the executor; operators
196/// never write it.
197#[derive(Debug)]
198pub struct SourceOperator {
199    pub kind: SourceOperatorKind,
200    pub runtime: Option<Arc<ExecutionRuntime>>,
201    pub output_layout: Arc<SlotLayout>,
202    pub config: OperatorConfig,
203    /// Correlation frame bound to this operator instance by a parent
204    /// executor (`StreamingExecutor::inject_correlation_frame`). Consumed
205    /// (one-shot) by `Argument`. Per-instance, so parallel partitions and
206    /// nested subqueries never share or overwrite frames.
207    pub frame: Option<(Arc<SlotLayout>, Vec<Value>)>,
208}
209
210/// Property names reach the planner as owned `String`s, while every storage
211/// read takes shared `Arc<str>` names. Converting once here keeps the
212/// per-chunk read paths free of per-row name allocations.
213fn shared_names(names: &[String]) -> Vec<Arc<str>> {
214    names.iter().map(|name| Arc::from(name.as_str())).collect()
215}
216
217impl SourceOperator {
218    /// Create a SourceOperator with immutable config from an immutable spec
219    /// and the per-query storage client.  Mutable runtime state is created
220    /// separately in [`SourceOperator::open`] and stored in the operator
221    /// state arena on the execution runtime.
222    pub fn from_spec(
223        spec: &super::spec::SourceSpec,
224        storage: Option<Arc<RwLock<dyn crate::storage::QueryStorage>>>,
225        output_layout: Arc<SlotLayout>,
226    ) -> Self {
227        let kind = match spec {
228            super::spec::SourceSpec::ScanVertices { rows, col_names } => {
229                SourceOperatorKind::ScanVertices {
230                    buffer: rows.clone(),
231                    current_index: 0,
232                    col_names: col_names.clone(),
233                }
234            }
235            super::spec::SourceSpec::StandaloneValues { values, col_names } => {
236                SourceOperatorKind::StandaloneValues {
237                    values: values.clone(),
238                    buffer: Vec::new(),
239                    current_index: 0,
240                    col_names: col_names.clone(),
241                }
242            }
243            super::spec::SourceSpec::CteScan {
244                cte_name,
245                col_names,
246            } => SourceOperatorKind::CteScan {
247                cte_name: cte_name.clone(),
248                col_names: col_names.clone(),
249                buffer: Vec::new(),
250                current_index: 0,
251            },
252            super::spec::SourceSpec::StorageScanVertices {
253                space_name,
254                limit,
255                col_names,
256                projected_properties,
257                identity_only,
258                skip_rows,
259                predicate,
260                tag,
261                partition_range,
262                shard_slice,
263            } => SourceOperatorKind::StorageScanVertices {
264                storage: storage.clone(),
265                space_name: space_name.clone(),
266                limit: *limit,
267                partition_range: partition_range.clone(),
268                shard_slice: *shard_slice,
269                col_names: col_names.clone(),
270                projected_properties: shared_names(projected_properties),
271                identity_only: *identity_only,
272                skip_rows: *skip_rows,
273                predicate: predicate.clone(),
274                tag: tag.clone(),
275                semi_mask: None,
276                cursor: None,
277            },
278            super::spec::SourceSpec::ScanEdges { rows, col_names } => {
279                SourceOperatorKind::ScanEdges {
280                    buffer: rows.clone(),
281                    current_index: 0,
282                    col_names: col_names.clone(),
283                }
284            }
285            super::spec::SourceSpec::StorageScanEdges {
286                space_name,
287                limit,
288                edge_type,
289                col_names,
290                projected_properties,
291                identity_only,
292                predicate,
293                shard_slice,
294            } => SourceOperatorKind::StorageScanEdges {
295                storage: storage.clone(),
296                space_name: space_name.clone(),
297                limit: *limit,
298                edge_type: edge_type.clone(),
299                shard_slice: *shard_slice,
300                col_names: col_names.clone(),
301                projected_properties: shared_names(projected_properties),
302                identity_only: *identity_only,
303                predicate: predicate.clone(),
304                cursor: None,
305            },
306            super::spec::SourceSpec::GetVertices {
307                space_name,
308                tag,
309                vertex_ids,
310                projected_properties,
311                col_names: _,
312            } => SourceOperatorKind::GetVertices {
313                storage: storage.clone(),
314                space_name: space_name.clone(),
315                tag: tag.clone(),
316                vertex_ids: vertex_ids.clone(),
317                cached_ids: Vec::new(),
318                projected_properties: shared_names(projected_properties),
319            },
320            super::spec::SourceSpec::GetEdges {
321                space_name,
322                edge_type,
323                src,
324                dst,
325                rank,
326                projected_properties,
327            } => SourceOperatorKind::GetEdges {
328                storage: storage.clone(),
329                space_name: space_name.clone(),
330                edge_type: edge_type.clone(),
331                src: src.clone(),
332                dst: dst.clone(),
333                rank: *rank,
334                projected_properties: shared_names(projected_properties),
335                cursor: None,
336            },
337            super::spec::SourceSpec::GetNeighbors {
338                space_name,
339                tag,
340                direction,
341                projected_properties,
342            } => SourceOperatorKind::GetNeighbors {
343                storage: storage.clone(),
344                space_name: space_name.clone(),
345                tag: tag.clone(),
346                direction: direction.clone(),
347                projected_properties: shared_names(projected_properties),
348                state: NeighborScanState::Init,
349            },
350            super::spec::SourceSpec::IndexScan {
351                space_name,
352                index_name,
353                index_id,
354                tag,
355                predicate,
356                projection,
357                output_layout,
358                ..
359            } => SourceOperatorKind::IndexScan {
360                storage: storage.clone(),
361                space_name: space_name.clone(),
362                index_name: index_name.clone(),
363                index_id: *index_id,
364                tag: tag.clone(),
365                predicate: (**predicate).clone(),
366                projection: projection.clone(),
367                output_layout: output_layout.clone(),
368                partition_range: None,
369                cursor: None,
370                edge_type_names: std::collections::HashMap::new(),
371            },
372            super::spec::SourceSpec::Argument { .. } => SourceOperatorKind::Argument,
373            super::spec::SourceSpec::GetProp {
374                space_name,
375                entity_slot,
376                prop_names,
377                is_vertex,
378                output_layout,
379            } => SourceOperatorKind::GetProp {
380                storage: storage.clone(),
381                space_name: space_name.clone(),
382                entity_slot: *entity_slot,
383                prop_names: prop_names.clone(),
384                is_vertex: *is_vertex,
385                output_layout: output_layout.clone(),
386            },
387            super::spec::SourceSpec::Start => SourceOperatorKind::Start,
388        };
389        Self {
390            kind,
391            runtime: None,
392            output_layout,
393            config: OperatorConfig {
394                chunk_size: 2048,
395                partition_id: None,
396                physical_operator_id: PhysicalOperatorId(0),
397            },
398            frame: None,
399        }
400    }
401
402    pub fn new(kind: SourceOperatorKind, output_layout: Arc<SlotLayout>) -> Self {
403        Self {
404            kind,
405            runtime: None,
406            output_layout,
407            config: OperatorConfig {
408                chunk_size: 2048,
409                partition_id: None,
410                physical_operator_id: PhysicalOperatorId(0),
411            },
412            frame: None,
413        }
414    }
415
416    /// Install a join or expand semi-mask on a vertex scan. The mask decodes
417    /// exactly the given global internal ids instead of enumerating the
418    /// table. Must be called before open; opening without a tag fails.
419    pub fn set_semi_mask(&mut self, ids: Vec<u32>) {
420        if let SourceOperatorKind::StorageScanVertices { semi_mask, .. } = &mut self.kind {
421            *semi_mask = Some(ids);
422        }
423    }
424
425    /// Inject the runtime and execution config (called once by the executor
426    /// before this operator produces any data).
427    pub fn inject_context(
428        &mut self,
429        runtime: Option<&Arc<ExecutionRuntime>>,
430        config: OperatorConfig,
431    ) {
432        if let Some(rt) = runtime {
433            self.runtime = Some(rt.clone());
434        }
435        self.config = config;
436    }
437
438    fn state_key(&self) -> GlobalStateKey {
439        GlobalStateKey(self.config.physical_operator_id, self.config.partition_id)
440    }
441
442    fn state_arena(&self) -> parking_lot::MutexGuard<'_, StateArenaSet> {
443        self.runtime
444            .as_ref()
445            .expect("runtime required")
446            .state_arena_for(self.config.partition_id)
447            .lock()
448    }
449
450    fn insert_state(&mut self, state: GlobalState) {
451        let Some(rt) = self.runtime.as_ref() else {
452            return;
453        };
454        let key = self.state_key();
455        rt.state_arena_for(self.config.partition_id)
456            .lock()
457            .global
458            .insert(key, state);
459    }
460
461    fn take_state(&mut self) -> Option<GlobalState> {
462        let rt = self.runtime.as_ref()?;
463        let key = self.state_key();
464        rt.state_arena_for(self.config.partition_id)
465            .lock()
466            .global
467            .remove(&key)
468    }
469
470    pub fn open(&mut self) -> Result<(), QueryError> {
471        use crate::executor::streaming::operators::state::SourceState;
472        use crate::executor::streaming::state::GlobalState;
473        let mut state: Option<GlobalState> = None;
474        match &mut self.kind {
475            SourceOperatorKind::ScanVertices { .. }
476            | SourceOperatorKind::StandaloneValues { .. }
477            | SourceOperatorKind::ScanEdges { .. } => buffered::open(self)?,
478            SourceOperatorKind::StorageScanVertices { .. }
479            | SourceOperatorKind::StorageScanEdges { .. } => storage_scan::open(self)?,
480            SourceOperatorKind::GetVertices { .. } | SourceOperatorKind::GetEdges { .. } => {
481                point_lookup::open(self)?
482            }
483            SourceOperatorKind::GetNeighbors { .. } => neighbors::open(self)?,
484            SourceOperatorKind::IndexScan { .. } => index_scan::open(self)?,
485            SourceOperatorKind::CteScan { .. } => Self::open_cte_scan(self)?,
486            SourceOperatorKind::Argument => {
487                state = Some(GlobalState::Source(SourceState::Argument));
488            }
489            SourceOperatorKind::GetProp {
490                entity_slot,
491                prop_names,
492                ..
493            } => {
494                state = Some(GlobalState::Source(SourceState::GetProp {
495                    entity_slot: *entity_slot,
496                    prop_names: prop_names.clone(),
497                }));
498            }
499            SourceOperatorKind::Start => {
500                state = Some(GlobalState::Source(SourceState::Start { emitted: false }));
501            }
502        }
503        if let Some(state) = state {
504            self.insert_state(state);
505        }
506        Ok(())
507    }
508
509    #[allow(clippy::should_implement_trait)]
510    pub fn next(&mut self) -> Result<Option<DataChunk>, QueryError> {
511        use crate::executor::streaming::operators::state::SourceState;
512        use crate::executor::streaming::state::GlobalState;
513        match &mut self.kind {
514            SourceOperatorKind::ScanVertices { .. }
515            | SourceOperatorKind::StandaloneValues { .. }
516            | SourceOperatorKind::ScanEdges { .. } => buffered::next(self),
517            SourceOperatorKind::StorageScanVertices { .. }
518            | SourceOperatorKind::StorageScanEdges { .. } => storage_scan::next(self),
519            SourceOperatorKind::GetVertices { .. } | SourceOperatorKind::GetEdges { .. } => {
520                point_lookup::next(self)
521            }
522            SourceOperatorKind::GetNeighbors { .. } => neighbors::next(self),
523            SourceOperatorKind::IndexScan { .. } => index_scan::next(self),
524            SourceOperatorKind::CteScan { .. } => Self::next_cte_scan(self),
525            SourceOperatorKind::GetProp { .. } => Err(QueryError::execution(
526                "GetProp is not available as a source operator; \
527                  use the unary GetProp operator"
528                    .to_string(),
529            )),
530            SourceOperatorKind::Start => {
531                let mut arena = self.state_arena();
532                let s = arena.global.get_mut(&self.state_key());
533                let emitted = match s {
534                    Some(GlobalState::Source(SourceState::Start { ref mut emitted })) => emitted,
535                    _ => return Ok(None),
536                };
537                if *emitted {
538                    return Ok(None);
539                }
540                *emitted = true;
541                let chunk =
542                    DataChunk::new_with_layout(vec![Vec::new()], Arc::clone(&self.output_layout));
543                Ok(Some(chunk))
544            }
545            SourceOperatorKind::Argument => {
546                let frame = self.frame.take();
547                match frame {
548                    Some((layout, row)) => {
549                        // Project the injected frame down to this Argument's
550                        // own output layout: the injected row may carry more
551                        // columns than the outer column names the sub-plan
552                        // was compiled against (e.g. optimizer-added flat
553                        // property slots).
554                        let projected: Vec<Value> = self
555                            .output_layout
556                            .names()
557                            .iter()
558                            .map(|name| {
559                                layout
560                                    .slot_id(name)
561                                    .and_then(|slot| row.get(slot).cloned())
562                                    .unwrap_or_else(|| {
563                                        Value::Null(linkrs_core::value::NullType::Null)
564                                    })
565                            })
566                            .collect();
567                        let chunk = DataChunk::new_with_layout(
568                            vec![projected],
569                            Arc::clone(&self.output_layout),
570                        );
571                        Ok(Some(chunk))
572                    }
573                    None => Ok(None),
574                }
575            }
576        }
577    }
578
579    pub fn stop(&mut self) -> Result<(), QueryError> {
580        Ok(())
581    }
582
583    /// Rewind this source so it re-produces the same logical stream.
584    ///
585    /// Buffered sources reset their row index; storage-backed sources
586    /// re-open their cursor (the query-level snapshot guarantees identical
587    /// re-reads); `Argument`/`GetProp` keep their injected frame/state and
588    /// `Start` un-emits its single row.
589    pub fn reset(&mut self) -> Result<bool, QueryError> {
590        use crate::executor::streaming::operators::state::SourceState;
591        use crate::executor::streaming::state::GlobalState;
592        match &mut self.kind {
593            SourceOperatorKind::ScanVertices { current_index, .. }
594            | SourceOperatorKind::ScanEdges { current_index, .. }
595            | SourceOperatorKind::StandaloneValues { current_index, .. } => {
596                *current_index = 0;
597            }
598            SourceOperatorKind::StorageScanVertices { .. }
599            | SourceOperatorKind::StorageScanEdges { .. } => {
600                storage_scan::open(self)?;
601            }
602            SourceOperatorKind::GetVertices { .. } | SourceOperatorKind::GetEdges { .. } => {
603                point_lookup::open(self)?
604            }
605            SourceOperatorKind::GetNeighbors { .. } => neighbors::open(self)?,
606            SourceOperatorKind::IndexScan { .. } => index_scan::open(self)?,
607            SourceOperatorKind::CteScan { .. } => {
608                // Re-snapshot the working table: the fixpoint operator
609                // publishes a fresh delta before every step iteration.
610                Self::open_cte_scan(self)?;
611            }
612            SourceOperatorKind::Argument => {
613                // The correlation frame lives on the operator and is
614                // re-injected per run by the parent; nothing to rewind.
615            }
616            SourceOperatorKind::GetProp { .. } => {
617                // State is immutable; nothing to rewind.
618            }
619            SourceOperatorKind::Start => {
620                if let Some(rt) = &self.runtime {
621                    let key = self.state_key();
622                    let mut arena = rt.state_arena_for(self.config.partition_id).lock();
623                    if let Some(GlobalState::Source(SourceState::Start { emitted })) =
624                        arena.global.get_mut(&key)
625                    {
626                        *emitted = false;
627                    }
628                }
629            }
630        }
631        Ok(false)
632    }
633
634    pub fn close(&mut self) -> Result<(), QueryError> {
635        self.take_state();
636        Ok(())
637    }
638
639    /// Snapshot the enclosing fixpoint's working table into the in-kind
640    /// buffer. Called at `open()` and at every `reset()` (once per step
641    /// iteration); a missing table reads as an empty stream.
642    fn open_cte_scan(op: &mut SourceOperator) -> Result<(), QueryError> {
643        let (cte_name, buffer, current_index) = match &mut op.kind {
644            SourceOperatorKind::CteScan {
645                cte_name,
646                buffer,
647                current_index,
648                ..
649            } => (cte_name.clone(), buffer, current_index),
650            _ => {
651                return Err(QueryError::execution(
652                    "open_cte_scan called for a non-CTE source".to_string(),
653                ));
654            }
655        };
656        let rows = op
657            .runtime
658            .as_ref()
659            .and_then(|rt| rt.cte_table(&cte_name))
660            .unwrap_or_default();
661        *buffer = rows;
662        *current_index = 0;
663        Ok(())
664    }
665
666    /// Serve the next chunk from the snapshotted working table.
667    fn next_cte_scan(op: &mut SourceOperator) -> Result<Option<DataChunk>, QueryError> {
668        let layout = Arc::clone(&op.output_layout);
669        let chunk_size = op.config.chunk_size;
670        let (buffer, current_index) = match &mut op.kind {
671            SourceOperatorKind::CteScan {
672                buffer,
673                current_index,
674                ..
675            } => (buffer, current_index),
676            _ => {
677                return Err(QueryError::execution(
678                    "next_cte_scan called for a non-CTE source".to_string(),
679                ));
680            }
681        };
682        if *current_index >= buffer.len() {
683            return Ok(None);
684        }
685        let end = (*current_index + chunk_size).min(buffer.len());
686        let rows = buffer[*current_index..end].to_vec();
687        *current_index = end;
688        Ok(Some(DataChunk::new_with_layout(rows, layout)))
689    }
690}
691
692#[cfg(test)]
693#[cfg(test)]
694mod tests;