Skip to main content

linkrs_query/executor/streaming/operators/
flatten.rs

1use crate::executor::streaming::chunk::DataChunk;
2use crate::executor::streaming::chunk::{gather_typed_column, VECTORIZED_BATCH_SIZE};
3use crate::executor::streaming::executor::StreamingExecutor;
4use linkrs_core::error::QueryError;
5
6/// Default batch size for the batched flatten path.
7///
8/// Kept in sync with the columnar `VECTORIZED_BATCH_SIZE` so one flatten
9/// output never exceeds a single vectorized morsel.
10pub const DEFAULT_FLATTEN_BATCH_SIZE: usize = VECTORIZED_BATCH_SIZE;
11
12/// Shared flatten logic used by `UnaryOperatorKind::Flatten`.
13/// Production streaming uses `UnaryOperatorKind::Flatten` via `unary_operator.rs`.
14/// This module provides the single-row `flatten_next_inner` implementation
15/// on top of the batched `flatten_next_batch` core.
16///
17/// The streaming engine stores flat row batches only. There is no nested
18/// vector layout behind `group_pos`, so flatten replays visible rows in order
19/// instead of expanding one factorization group.
20pub(crate) fn prepare_flatten_buffer(chunk: DataChunk) -> (Vec<usize>, DataChunk) {
21    let sel = chunk.visible_indices();
22    let mut buffered = chunk;
23    let _ = buffered.take_selection();
24    (sel, buffered)
25}
26
27pub(crate) fn flatten_next_inner(
28    current_idx: &mut usize,
29    size_to_flatten: &mut usize,
30    saved_sel_vector: &mut Option<Vec<usize>>,
31    buffered_chunk: &mut Option<DataChunk>,
32    input: &mut StreamingExecutor,
33) -> Result<Option<DataChunk>, QueryError> {
34    flatten_next_batch(
35        current_idx,
36        size_to_flatten,
37        saved_sel_vector,
38        buffered_chunk,
39        input,
40        1,
41    )
42}
43
44/// Batched flatten: emit up to `batch_size` visible rows per output chunk.
45///
46/// The caller-visible state machine (`current_idx` / `size_to_flatten` /
47/// `saved_sel_vector` / `buffered_chunk`) is shared with the single-row
48/// path, so both paths observe identical row order and identical
49/// `typed_columns` content: each output column is gathered with the
50/// columnar `gather_typed_column` helper over the emitted selection
51/// slice instead of per-row clones.
52pub(crate) fn flatten_next_batch(
53    current_idx: &mut usize,
54    size_to_flatten: &mut usize,
55    saved_sel_vector: &mut Option<Vec<usize>>,
56    buffered_chunk: &mut Option<DataChunk>,
57    input: &mut StreamingExecutor,
58    batch_size: usize,
59) -> Result<Option<DataChunk>, QueryError> {
60    let batch_size = batch_size.max(1);
61    loop {
62        if buffered_chunk.is_some() {
63            if *current_idx < *size_to_flatten {
64                let take = (*size_to_flatten - *current_idx).min(batch_size);
65                // Borrow scope ends before the state mutation below, so the
66                // retained buffered chunk is never cloned back into place.
67                let out = {
68                    let chunk = buffered_chunk
69                        .as_ref()
70                        .expect("buffered chunk must be present");
71                    let sel_vec = saved_sel_vector
72                        .as_ref()
73                        .expect("saved sel vector must be present");
74                    let positions: Vec<usize> = sel_vec[*current_idx..*current_idx + take].to_vec();
75                    build_flatten_batch_chunk(chunk, &positions)
76                };
77                *current_idx += take;
78                if *current_idx >= *size_to_flatten {
79                    *buffered_chunk = None;
80                    *saved_sel_vector = None;
81                    *size_to_flatten = 0;
82                    *current_idx = 0;
83                }
84                return Ok(Some(out));
85            } else {
86                *saved_sel_vector = None;
87                *size_to_flatten = 0;
88                *current_idx = 0;
89            }
90        }
91        let child_chunk = match input.advance()? {
92            Some(c) => c,
93            None => return Ok(None),
94        };
95        if child_chunk.visible_count() == 0 {
96            continue;
97        }
98        let (sel, buffered) = prepare_flatten_buffer(child_chunk);
99        *size_to_flatten = sel.len();
100        *saved_sel_vector = Some(sel);
101        *current_idx = 0;
102        *buffered_chunk = Some(buffered);
103    }
104}
105
106/// Materialize one flatten output chunk for `positions` of `chunk`.
107///
108/// Row order follows `positions`; rows are gathered column-wise through the
109/// shared batch primitive while `typed_columns` keeps its dedicated gather
110/// path so the vectorized layout is preserved end to end.
111fn build_flatten_batch_chunk(chunk: &DataChunk, positions: &[usize]) -> DataChunk {
112    let layout = chunk.get_layout();
113    let schema = chunk.schema.clone();
114    let rows = chunk.to_batch().gather(positions).to_rows();
115    let typed_columns = chunk.typed_columns.as_ref().map(|cols| {
116        cols.iter()
117            .map(|col| gather_typed_column(col, positions))
118            .collect::<Vec<_>>()
119    });
120    let columnar_stats = chunk.columnar_stats.clone();
121    let mut out = DataChunk::new_with_layout(rows, layout);
122    out.schema = schema;
123    out.typed_columns = typed_columns;
124    // A deferred source layout stays deferred when no typed columns were
125    // gathered; the flattened rows remain a valid build input.
126    out.columnar_build_deferred = chunk.columnar_build_deferred && out.typed_columns.is_none();
127    out.columnar_stats = columnar_stats;
128    out
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::executor::streaming::chunk::DataChunk;
135    use crate::executor::streaming::executor::StreamingExecutor;
136    use crate::executor::streaming::operators::base::OperatorBase;
137    use crate::executor::streaming::operators::source_operator::{
138        SourceOperator, SourceOperatorKind,
139    };
140    use crate::executor::streaming::slot::SlotLayout;
141    use linkrs_core::Value;
142    use std::sync::Arc;
143
144    fn test_layout(names: &[&str]) -> Arc<SlotLayout> {
145        Arc::new(SlotLayout::from_names(
146            &names.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
147        ))
148    }
149
150    fn source_executor(rows: Vec<Vec<Value>>, col_names: Vec<&str>) -> StreamingExecutor {
151        let layout = test_layout(&col_names);
152        StreamingExecutor::Source(
153            OperatorBase::new(0),
154            SourceOperator::new(
155                SourceOperatorKind::ScanVertices {
156                    buffer: rows,
157                    current_index: 0,
158                    col_names: col_names.into_iter().map(|s| s.to_string()).collect(),
159                },
160                layout,
161            ),
162        )
163    }
164
165    #[test]
166    fn flatten_single_batch_selection_vector_path() {
167        let rows = vec![
168            vec![Value::Int(1), Value::string("a")],
169            vec![Value::Int(2), Value::string("b")],
170            vec![Value::Int(3), Value::string("c")],
171        ];
172        let mut src = source_executor(rows, vec!["id", "name"]);
173        src.open().expect("open");
174        let mut current_idx = 0usize;
175        let mut size_to_flatten = 0usize;
176        let mut saved = None;
177        let mut buffered = None;
178        let mut out_rows = Vec::new();
179        loop {
180            let chunk = flatten_next_inner(
181                &mut current_idx,
182                &mut size_to_flatten,
183                &mut saved,
184                &mut buffered,
185                &mut src,
186            )
187            .expect("next");
188            match chunk {
189                Some(c) => {
190                    assert_eq!(c.len(), 1);
191                    out_rows.push(c.rows[0].clone());
192                }
193                None => break,
194            }
195        }
196        assert_eq!(out_rows.len(), 3);
197        assert_eq!(out_rows[0][0], Value::Int(1));
198        assert_eq!(out_rows[1][0], Value::Int(2));
199        assert_eq!(out_rows[2][0], Value::Int(3));
200    }
201
202    #[test]
203    fn flatten_with_child_selection() {
204        let rows = vec![
205            vec![Value::Int(10)],
206            vec![Value::Int(20)],
207            vec![Value::Int(30)],
208        ];
209        let layout = test_layout(&["v"]);
210        let mut chunk = DataChunk::new_with_layout(rows.clone(), layout.clone());
211        chunk = chunk.with_selection(vec![0, 2]);
212        assert_eq!(chunk.visible_count(), 2);
213        let (sel, buffered) = prepare_flatten_buffer(chunk);
214        assert_eq!(sel, vec![0, 2]);
215        assert_eq!(buffered.rows.len(), 3);
216        assert_eq!(buffered.visible_count(), 3);
217    }
218
219    #[test]
220    fn flatten_empty_input_returns_none() {
221        let mut src = source_executor(vec![], vec!["id"]);
222        src.open().expect("open");
223        let mut current_idx = 0usize;
224        let mut size_to_flatten = 0usize;
225        let mut saved = None;
226        let mut buffered = None;
227        let out = flatten_next_inner(
228            &mut current_idx,
229            &mut size_to_flatten,
230            &mut saved,
231            &mut buffered,
232            &mut src,
233        )
234        .expect("next");
235        assert!(out.is_none());
236    }
237
238    #[test]
239    fn typed_columns_preserved() {
240        let layout = test_layout(&["a", "b"]);
241        let mut chunk = DataChunk::new_with_layout(
242            vec![
243                vec![Value::Int(1), Value::Int(10)],
244                vec![Value::Int(2), Value::Int(20)],
245            ],
246            layout.clone(),
247        );
248        chunk.build_typed_columns(true);
249        assert!(chunk.typed_columns.is_some());
250        let (sel, buffered) = prepare_flatten_buffer(chunk);
251        assert_eq!(sel.len(), 2);
252        let rows = vec![
253            vec![Value::Int(1), Value::Int(10)],
254            vec![Value::Int(2), Value::Int(20)],
255        ];
256        let mut src = source_executor(rows, vec!["a", "b"]);
257        let mut current_idx = 0usize;
258        let mut size_to_flatten = sel.len();
259        let mut saved = Some(sel);
260        let mut buf_opt = Some(buffered);
261        let out1 = flatten_next_inner(
262            &mut current_idx,
263            &mut size_to_flatten,
264            &mut saved,
265            &mut buf_opt,
266            &mut src,
267        )
268        .expect("next")
269        .expect("chunk");
270        assert_eq!(out1.len(), 1);
271        assert!(out1.typed_columns.is_some());
272        assert_eq!(out1.typed_columns.as_ref().unwrap()[0].len(), 1);
273        let out2 = flatten_next_inner(
274            &mut current_idx,
275            &mut size_to_flatten,
276            &mut saved,
277            &mut buf_opt,
278            &mut src,
279        )
280        .expect("next")
281        .expect("chunk");
282        assert_eq!(out2.len(), 1);
283        assert!(out2.typed_columns.is_some());
284    }
285
286    #[test]
287    fn empty_selection_batch() {
288        let layout = test_layout(&["v"]);
289        let mut chunk = DataChunk::new_with_layout(vec![], layout.clone());
290        chunk = chunk.with_selection(vec![]);
291        assert_eq!(chunk.visible_count(), 0);
292        let (sel, _) = prepare_flatten_buffer(chunk);
293        assert_eq!(sel.len(), 0);
294    }
295
296    fn drain_batch(src: &mut StreamingExecutor, batch_size: usize) -> Vec<DataChunk> {
297        let mut current_idx = 0usize;
298        let mut size_to_flatten = 0usize;
299        let mut saved = None;
300        let mut buffered = None;
301        let mut out = Vec::new();
302        loop {
303            let chunk = flatten_next_batch(
304                &mut current_idx,
305                &mut size_to_flatten,
306                &mut saved,
307                &mut buffered,
308                src,
309                batch_size,
310            )
311            .expect("next");
312            match chunk {
313                Some(c) => out.push(c),
314                None => break,
315            }
316        }
317        out
318    }
319
320    fn flat_rows(chunks: &[DataChunk]) -> Vec<Vec<Value>> {
321        chunks
322            .iter()
323            .flat_map(|c| c.rows.clone())
324            .collect::<Vec<_>>()
325    }
326
327    #[test]
328    fn flatten_batch_size_two_matches_single_row_path() {
329        let rows = vec![
330            vec![Value::Int(1), Value::string("a")],
331            vec![Value::Int(2), Value::string("b")],
332            vec![Value::Int(3), Value::string("c")],
333            vec![Value::Int(4), Value::string("d")],
334            vec![Value::Int(5), Value::string("e")],
335        ];
336        let mut single = source_executor(rows.clone(), vec!["id", "name"]);
337        single.open().expect("open");
338        let mut current_idx = 0usize;
339        let mut size_to_flatten = 0usize;
340        let mut saved = None;
341        let mut buffered = None;
342        let mut single_rows = Vec::new();
343        loop {
344            let chunk = flatten_next_inner(
345                &mut current_idx,
346                &mut size_to_flatten,
347                &mut saved,
348                &mut buffered,
349                &mut single,
350            )
351            .expect("next");
352            match chunk {
353                Some(c) => {
354                    assert_eq!(c.len(), 1);
355                    single_rows.push(c.rows[0].clone());
356                }
357                None => break,
358            }
359        }
360        let mut batched = source_executor(rows, vec!["id", "name"]);
361        batched.open().expect("open");
362        let chunks = drain_batch(&mut batched, 2);
363        assert_eq!(chunks.len(), 3);
364        assert_eq!(chunks[0].len(), 2);
365        assert_eq!(chunks[1].len(), 2);
366        assert_eq!(chunks[2].len(), 1);
367        assert_eq!(flat_rows(&chunks), single_rows);
368    }
369
370    #[test]
371    fn flatten_single_row_path_matches_full_morsel() {
372        // Monotonicity across batch sizes: single-row drains must replay the
373        // same rows in the same order as one full-morsel batch.
374        let rows = vec![
375            vec![Value::Int(1)],
376            vec![Value::Int(2)],
377            vec![Value::Int(3)],
378            vec![Value::Int(4)],
379        ];
380        let mut single = source_executor(rows.clone(), vec!["id"]);
381        single.open().expect("open");
382        let mut current_idx = 0usize;
383        let mut size_to_flatten = 0usize;
384        let mut saved = None;
385        let mut buffered = None;
386        let mut single_rows = Vec::new();
387        loop {
388            let chunk = flatten_next_inner(
389                &mut current_idx,
390                &mut size_to_flatten,
391                &mut saved,
392                &mut buffered,
393                &mut single,
394            )
395            .expect("next");
396            match chunk {
397                Some(c) => single_rows.extend(c.rows.clone()),
398                None => break,
399            }
400        }
401        let mut batched = source_executor(rows.clone(), vec!["id"]);
402        batched.open().expect("open");
403        let chunks = drain_batch(&mut batched, 2048);
404        assert_eq!(chunks.len(), 1);
405        assert_eq!(flat_rows(&chunks), single_rows);
406        assert_eq!(flat_rows(&chunks), rows);
407    }
408
409    #[test]
410    fn flatten_batch_full_morsel_single_call() {
411        let rows = vec![
412            vec![Value::Int(1)],
413            vec![Value::Int(2)],
414            vec![Value::Int(3)],
415        ];
416        let mut src = source_executor(rows.clone(), vec!["id"]);
417        src.open().expect("open");
418        let chunks = drain_batch(&mut src, 2048);
419        assert_eq!(chunks.len(), 1);
420        assert_eq!(chunks[0].len(), 3);
421        assert_eq!(
422            flat_rows(&chunks),
423            rows,
424            "batch_size=2048 must emit the whole morsel at once"
425        );
426    }
427
428    #[test]
429    fn flatten_batch_typed_columns_equivalence() {
430        let layout = test_layout(&["a", "b"]);
431        let rows = vec![
432            vec![Value::Int(1), Value::Int(10)],
433            vec![Value::Int(2), Value::Int(20)],
434            vec![Value::Int(3), Value::Int(30)],
435        ];
436        let mut chunk = DataChunk::new_with_layout(rows.clone(), layout.clone());
437        chunk.build_typed_columns(true);
438        assert!(chunk.typed_columns.is_some());
439        let (sel, buffered) = prepare_flatten_buffer(chunk);
440        assert_eq!(sel.len(), 3);
441        let mut src = source_executor(rows.clone(), vec!["a", "b"]);
442        let mut current_idx = 0usize;
443        let mut size_to_flatten = sel.len();
444        let mut saved = Some(sel);
445        let mut buf_opt = Some(buffered);
446        let out = flatten_next_batch(
447            &mut current_idx,
448            &mut size_to_flatten,
449            &mut saved,
450            &mut buf_opt,
451            &mut src,
452            2,
453        )
454        .expect("next")
455        .expect("chunk");
456        assert_eq!(out.len(), 2);
457        let typed = out.typed_columns.as_ref().expect("typed columns");
458        assert_eq!(typed[0].len(), 2);
459        assert_eq!(typed[1].len(), 2);
460        assert_eq!(typed[0].value_at(0), Some(Value::Int(1)));
461        assert_eq!(typed[0].value_at(1), Some(Value::Int(2)));
462        assert_eq!(typed[1].value_at(0), Some(Value::Int(10)));
463        assert_eq!(typed[1].value_at(1), Some(Value::Int(20)));
464        let rest = flatten_next_batch(
465            &mut current_idx,
466            &mut size_to_flatten,
467            &mut saved,
468            &mut buf_opt,
469            &mut src,
470            2,
471        )
472        .expect("next")
473        .expect("chunk");
474        assert_eq!(rest.len(), 1);
475        assert_eq!(
476            rest.typed_columns.as_ref().expect("typed")[0].value_at(0),
477            Some(Value::Int(3))
478        );
479        // State machine is drained after the exact row count.
480        assert!(saved.is_none());
481        assert!(buf_opt.is_none());
482    }
483
484    #[test]
485    fn flatten_batch_empty_input_returns_none() {
486        let mut src = source_executor(vec![], vec!["id"]);
487        src.open().expect("open");
488        let chunks = drain_batch(&mut src, 2);
489        assert!(chunks.is_empty());
490    }
491}