Skip to main content

linkrs_query/executor/streaming/chunk/
selection.rs

1//! Selection vectors, index-based take, and slice operations
2//!
3//! # Selection propagation contract
4//!
5//! Operators fall into two categories:
6//! - **Transparent**: `Filter`, `Project` (via `evaluate_expression_visible`),
7//!   `Limit`/`Offset`, and stateless unary operators. They consume the
8//!   selection vector without moving rows, so chunks stay compact.
9//!   [`DataChunk::selection`] hands the visible indices to such consumers.
10//! - **Opaque**: blocking operators (aggregate/sort) and join builds (hash
11//!   join). They own the rows and must call
12//!   [`DataChunk::materialize_selection_by`] — the selection degenerates into
13//!   a compact row batch at the boundary.
14//!
15//! # Multiplicity propagation contract
16//!
17//! `multiplicity` is a symbolic per-chunk row factor carried alongside the
18//! selection vector:
19//! - Row-preserving rebuilds (`Project`/`Assign`/`Remove`/`AppendVertices`/
20//!   `Sample`/`Unwind`) must carry it with `with_multiplicity(source)`,
21//!   because every output row still occurs `multiplicity` times.
22//! - Row-collapsing operators (`Dedup`/`Distinct`/aggregates/sets) start a new
23//!   grouping, so `multiplicity` resets to 1 (the constructor default).
24//! - Opaque order-preserving operators (`Sort`/`TopN`/`Window`/join probe)
25//!   merge chunks whose uniform factors cannot be represented after the
26//!   merge; they must enter through [`DataChunk::normalize_for_opaque`],
27//!   which expands `multiplicity` and materializes the selection together.
28//!   Calling [`DataChunk::materialize_selection_by`] alone is not enough:
29//!   it leaves `multiplicity > 1` behind and downstream row counts shrink.
30
31use super::pool::RowBufferPool;
32use super::typed::{gather_typed_column, repeat_typed_column};
33use crate::executor::streaming::chunk::core::DataChunk;
34
35impl DataChunk {
36    // ── Multiplicity (symbolic row duplication) ──
37
38    /// Symbolic multiplicity: how many times each visible row occurs.
39    pub fn multiplicity(&self) -> u64 {
40        self.multiplicity
41    }
42
43    pub fn set_multiplicity(&mut self, multiplicity: u64) {
44        debug_assert!(multiplicity >= 1, "multiplicity must be >= 1");
45        self.multiplicity = multiplicity.max(1);
46    }
47
48    pub fn with_multiplicity(mut self, multiplicity: u64) -> Self {
49        self.set_multiplicity(multiplicity);
50        self
51    }
52
53    /// Expanded (flat) visible row count: `visible_count * multiplicity`.
54    /// Saturates instead of overflowing so metrics paths stay total.
55    pub fn logical_len(&self) -> u64 {
56        (self.visible_count() as u64).saturating_mul(self.multiplicity)
57    }
58
59    /// Borrow visible rows in upstream order (respects `selection`).
60    pub fn visible_rows(&self) -> impl Iterator<Item = &Vec<linkrs_core::Value>> {
61        match &self.selection {
62            Some(indices) => VisibleRows::Selected {
63                rows: &self.rows,
64                indices,
65                pos: 0,
66            },
67            None => VisibleRows::All {
68                rows: &self.rows,
69                pos: 0,
70            },
71        }
72    }
73
74    /// Move visible rows out, repeating each row `multiplicity` times.
75    ///
76    /// This is the single terminal move: rows are moved out with `mem::take`
77    /// with no per-Value clone on the move itself (`multiplicity == 1` returns the moved rows
78    /// directly; larger factors repeat them). After the call the chunk is
79    /// empty with `selection=None` and derived column caches cleared.
80    ///
81    /// Opaque order-preserving operators assume `multiplicity == 1`; a future
82    /// multiplicity producer must expand at those boundaries with this call
83    /// first (see the module-level multiplicity contract).
84    pub fn expand_visible_rows(&mut self) -> Vec<Vec<linkrs_core::Value>> {
85        let multiplicity = self.multiplicity;
86        // Capture the visible indices before the selection is taken so the
87        // typed layout can be re-expanded in lockstep with the rows.
88        let visible_indices: Vec<usize> = match &self.selection {
89            Some(indices) => indices.clone(),
90            None => (0..self.rows.len()).collect(),
91        };
92        let out = match self.selection.take() {
93            Some(indices) => {
94                let mut selected = Vec::with_capacity(indices.len());
95                for &i in &indices {
96                    selected.push(std::mem::take(&mut self.rows[i]));
97                }
98                self.rows.clear();
99                selected
100            }
101            None => std::mem::take(&mut self.rows),
102        };
103        // Keep the typed layout across the expansion (same ownership rule as
104        // `materialize_selection_inner`): gather the visible rows, then repeat
105        // each per `multiplicity`, so downstream operators still see a typed
106        // columnar chunk.
107        self.typed_columns = self.typed_columns.take().map(|cols| {
108            cols.iter()
109                .map(|col| {
110                    let gathered = gather_typed_column(col, &visible_indices);
111                    if multiplicity <= 1 {
112                        gathered
113                    } else {
114                        repeat_typed_column(&gathered, multiplicity as usize)
115                    }
116                })
117                .collect()
118        });
119        self.multiplicity = 1;
120        debug_assert!(
121            self.selection.is_none() && self.multiplicity == 1,
122            "expand_visible_rows must leave a flat chunk: \
123             selection consumed, multiplicity reset to 1"
124        );
125        if multiplicity <= 1 || out.is_empty() {
126            return out;
127        }
128        let total = (out.len() as u64).saturating_mul(multiplicity) as usize;
129        if let Some(stats) = &self.columnar_stats {
130            stats.record_multiplicity_expanded(total as u64);
131        }
132        expand_rows_reusing_buffers(out, multiplicity, total)
133    }
134
135    // ── Selection vectors ──
136
137    pub fn with_selection(mut self, indices: Vec<usize>) -> Self {
138        debug_assert!(indices.is_sorted() && indices.windows(2).all(|w| w[0] < w[1]));
139        debug_assert!(indices.last().is_none_or(|&i| i < self.rows.len()));
140        self.selection = Some(indices);
141        self
142    }
143
144    pub fn selection(&self) -> Option<&[usize]> {
145        self.selection.as_deref()
146    }
147
148    pub fn visible_count(&self) -> usize {
149        self.selection
150            .as_ref()
151            .map(Vec::len)
152            .unwrap_or(self.rows.len())
153    }
154
155    pub fn visible_indices(&self) -> Vec<usize> {
156        match &self.selection {
157            Some(indices) => indices.clone(),
158            None => (0..self.rows.len()).collect(),
159        }
160    }
161
162    pub fn is_visible(&self, idx: usize) -> bool {
163        match &self.selection {
164            None => idx < self.rows.len(),
165            Some(indices) => indices.binary_search(&idx).is_ok(),
166        }
167    }
168
169    pub fn take_selection(&mut self) -> Option<Vec<usize>> {
170        self.selection.take()
171    }
172
173    /// Expand a symbolic `multiplicity > 1` into physical rows in place.
174    ///
175    /// Opaque operators (sort/window/join/aggregate) cannot carry the factor
176    /// symbolically, so they must call this (directly or via
177    /// [`DataChunk::normalize_for_opaque`]) before consuming rows.
178    /// Returns `true` when an expansion happened. Visible selection is
179    /// preserved: only visible rows are replicated, hidden rows are dropped.
180    /// Derived column caches are cleared, matching `expand_visible_rows`.
181    pub fn expand_multiplicity_in_place(&mut self) -> bool {
182        if self.multiplicity <= 1 {
183            return false;
184        }
185        let multiplicity = self.multiplicity;
186        // Capture the visible indices before the selection is taken so the
187        // typed layout can be re-expanded in lockstep with the rows.
188        let visible_indices: Vec<usize> = match &self.selection {
189            Some(indices) => indices.clone(),
190            None => (0..self.rows.len()).collect(),
191        };
192        // Take visible rows once (same ownership discipline as
193        // `expand_visible_rows`); hidden rows are dropped.
194        let taken: Vec<Vec<linkrs_core::Value>> = match self.selection.take() {
195            Some(indices) => {
196                let mut selected = Vec::with_capacity(indices.len());
197                for &i in &indices {
198                    selected.push(std::mem::take(&mut self.rows[i]));
199                }
200                self.rows.clear();
201                selected
202            }
203            None => std::mem::take(&mut self.rows),
204        };
205        // Keep the typed layout across the expansion (consistent with
206        // `materialize_selection_inner`): gather the visible rows, then repeat
207        // each per `multiplicity`.
208        self.typed_columns = self.typed_columns.take().map(|cols| {
209            cols.iter()
210                .map(|col| {
211                    let gathered = gather_typed_column(col, &visible_indices);
212                    repeat_typed_column(&gathered, multiplicity as usize)
213                })
214                .collect()
215        });
216        self.multiplicity = 1;
217        debug_assert!(
218            self.selection.is_none() && self.multiplicity == 1,
219            "expand_multiplicity_in_place must leave a flat chunk: \
220             selection consumed, multiplicity reset to 1"
221        );
222        if taken.is_empty() {
223            return true;
224        }
225        let total = (taken.len() as u64).saturating_mul(multiplicity) as usize;
226        self.rows = expand_rows_reusing_buffers(taken, multiplicity, total);
227        if let Some(stats) = &self.columnar_stats {
228            stats.record_multiplicity_expanded(total as u64);
229        }
230        true
231    }
232
233    /// Unified opaque-operator boundary: expand `multiplicity`, then
234    /// materialize any attached selection. Returns `true` when either step
235    /// changed the chunk. `op` must be one of `SELECTION_BOUNDARY_OPS`.
236    ///
237    /// This is the single entry point opaque operators should use instead of
238    /// calling [`DataChunk::materialize_selection_by`] directly.
239    pub fn normalize_for_opaque(&mut self, op: &'static str) -> bool {
240        let expanded = self.expand_multiplicity_in_place();
241        let materialized = self.materialize_selection_by(op);
242        expanded || materialized
243    }
244
245    /// Contract: materialize any attached selection (opaque-operator
246    /// boundary). Returns `true` when a selection was actually materialized.
247    ///
248    /// Prefer [`DataChunk::normalize_for_opaque`]: this method alone does not
249    /// expand `multiplicity`, so opaque operators calling it directly must
250    /// guarantee `multiplicity == 1` (debug-asserted by callers via the
251    /// normalize path).
252    ///
253    /// This is the single boundary-materialization entry point; `op` must be
254    /// one of `SELECTION_BOUNDARY_OPS` so per-operator counters stay exact
255    /// (ad-hoc names fall back to the unattributed counter).
256    pub fn materialize_selection_by(&mut self, op: &'static str) -> bool {
257        let did = self.materialize_selection_inner();
258        if did {
259            if let Some(stats) = &self.columnar_stats {
260                stats.record_selection_materialized_by(op);
261            }
262        }
263        did
264    }
265
266    fn materialize_selection_inner(&mut self) -> bool {
267        let Some(indices) = self.selection.take() else {
268            return false;
269        };
270        let mut selected = Vec::with_capacity(indices.len());
271        for &i in &indices {
272            selected.push(std::mem::take(&mut self.rows[i]));
273        }
274        self.rows = selected;
275        self.typed_columns = self.typed_columns.as_ref().map(|cols| {
276            cols.iter()
277                .map(|col| gather_typed_column(col, &indices))
278                .collect()
279        });
280        true
281    }
282
283    // ── Index-based take & slice ──
284
285    pub fn take_indices(&mut self, indices: &[usize]) -> Self {
286        let layout = std::sync::Arc::clone(&self.layout);
287        let schema = std::sync::Arc::clone(&self.schema);
288        let mut selected = Vec::with_capacity(indices.len());
289        for &i in indices {
290            selected.push(std::mem::take(&mut self.rows[i]));
291        }
292        let typed_columns = self.typed_columns.as_ref().map(|cols| {
293            cols.iter()
294                .map(|col| gather_typed_column(col, indices))
295                .collect()
296        });
297        Self {
298            rows: selected,
299            typed_columns,
300            columnar_build_deferred: self.columnar_build_deferred,
301            selection: None,
302            multiplicity: self.multiplicity,
303            schema,
304            layout,
305            memory_reservation: self.memory_reservation.take(),
306            columnar_stats: self.columnar_stats.clone(),
307        }
308    }
309
310    pub fn slice(&mut self, start: usize, end: usize) -> Self {
311        assert!(end <= self.rows.len(), "slice end out of bounds");
312        let layout = std::sync::Arc::clone(&self.layout);
313        let schema = std::sync::Arc::clone(&self.schema);
314        let mut selected = Vec::with_capacity(end - start);
315        for i in start..end {
316            selected.push(std::mem::take(&mut self.rows[i]));
317        }
318        let indices: Vec<usize> = (start..end).collect();
319        let typed_columns = self.typed_columns.as_ref().map(|cols| {
320            cols.iter()
321                .map(|col| gather_typed_column(col, &indices))
322                .collect()
323        });
324        Self {
325            rows: selected,
326            typed_columns,
327            columnar_build_deferred: self.columnar_build_deferred,
328            selection: None,
329            multiplicity: self.multiplicity,
330            schema,
331            layout,
332            memory_reservation: self.memory_reservation.take(),
333            columnar_stats: self.columnar_stats.clone(),
334        }
335    }
336}
337
338/// Physically repeat `source` rows `multiplicity` times, reusing pooled
339/// row buffers for the copies.
340///
341/// The first repetition moves the source buffers (no per-Value clone); each
342/// further repetition clones into a recycled buffer from
343/// [`RowBufferPool`] (falling back to a fresh allocation when the pool is
344/// dry). Repetition order matches the old `extend`-loop: all visible rows,
345/// then the same sequence again. Callers pass the already-computed expanded
346/// `total` for capacity planning; growth past `1 << 22` rows reserves in
347/// bounded steps, mirroring the previous behavior.
348fn expand_rows_reusing_buffers(
349    source: Vec<Vec<linkrs_core::Value>>,
350    multiplicity: u64,
351    total: usize,
352) -> Vec<Vec<linkrs_core::Value>> {
353    debug_assert!(multiplicity > 1, "pool path is only for real expansion");
354    debug_assert!(!source.is_empty(), "empty source needs no expansion");
355    let round = source.len();
356    let mut expanded = Vec::with_capacity(total.min(1 << 22));
357    expanded.extend(source);
358    // One pool lock per expansion: grab every buffer the clone rounds need.
359    let mut recycled = RowBufferPool::acquire_rows(total.saturating_sub(round));
360    for _ in 1..multiplicity {
361        for i in 0..round {
362            let mut buf = recycled.pop().unwrap_or_default();
363            buf.extend(expanded[i].iter().cloned());
364            expanded.push(buf);
365        }
366        if expanded.len() >= (1 << 22) {
367            expanded.reserve(total.saturating_sub(expanded.len()).min(1 << 22));
368        }
369    }
370    expanded
371}
372
373enum VisibleRows<'a> {
374    All {
375        rows: &'a [Vec<linkrs_core::Value>],
376        pos: usize,
377    },
378    Selected {
379        rows: &'a [Vec<linkrs_core::Value>],
380        indices: &'a [usize],
381        pos: usize,
382    },
383}
384
385impl<'a> Iterator for VisibleRows<'a> {
386    type Item = &'a Vec<linkrs_core::Value>;
387
388    fn next(&mut self) -> Option<Self::Item> {
389        match self {
390            VisibleRows::All { rows, pos } => {
391                let row = rows.get(*pos)?;
392                *pos += 1;
393                Some(row)
394            }
395            VisibleRows::Selected { rows, indices, pos } => {
396                let &i = indices.get(*pos)?;
397                *pos += 1;
398                rows.get(i)
399            }
400        }
401    }
402
403    fn size_hint(&self) -> (usize, Option<usize>) {
404        let remaining = match self {
405            VisibleRows::All { rows, pos } => rows.len().saturating_sub(*pos),
406            VisibleRows::Selected { indices, pos, .. } => indices.len().saturating_sub(*pos),
407        };
408        (remaining, Some(remaining))
409    }
410}
411
412impl ExactSizeIterator for VisibleRows<'_> {}
413
414#[cfg(test)]
415mod tests {
416    use super::super::core::DataChunk;
417    use crate::executor::streaming::slot::SlotLayout;
418    use linkrs_core::Value;
419    use std::sync::Arc;
420
421    fn layout1() -> Arc<SlotLayout> {
422        Arc::new(SlotLayout::from_names(&["a".to_string()]))
423    }
424
425    #[test]
426    fn opaque_boundary_expands_multiplicity() {
427        let rows = vec![vec![Value::Int(1)], vec![Value::Int(2)]];
428        let mut chunk = DataChunk::new_with_layout(rows, layout1()).with_selection(vec![0, 1]);
429        chunk.set_multiplicity(3);
430        assert_eq!(chunk.logical_len(), 6);
431        assert!(chunk.normalize_for_opaque("Sort"));
432        assert_eq!(chunk.multiplicity(), 1);
433        assert!(chunk.selection().is_none());
434        assert_eq!(chunk.rows.len(), 6);
435        assert_eq!(
436            chunk.rows,
437            vec![
438                vec![Value::Int(1)],
439                vec![Value::Int(2)],
440                vec![Value::Int(1)],
441                vec![Value::Int(2)],
442                vec![Value::Int(1)],
443                vec![Value::Int(2)],
444            ]
445        );
446    }
447
448    #[test]
449    fn opaque_boundary_noop_when_flat() {
450        let rows = vec![vec![Value::Int(1)]];
451        let mut chunk = DataChunk::new_with_layout(rows, layout1());
452        assert!(!chunk.normalize_for_opaque("Sort"));
453        assert_eq!(chunk.rows.len(), 1);
454    }
455
456    #[test]
457    fn expand_multiplicity_drops_hidden_rows() {
458        let rows = vec![
459            vec![Value::Int(1)],
460            vec![Value::Int(2)],
461            vec![Value::Int(3)],
462        ];
463        let mut chunk = DataChunk::new_with_layout(rows, layout1()).with_selection(vec![0, 2]);
464        chunk.set_multiplicity(2);
465        assert!(chunk.expand_multiplicity_in_place());
466        assert_eq!(chunk.rows.len(), 4);
467        assert_eq!(
468            chunk.rows,
469            vec![
470                vec![Value::Int(1)],
471                vec![Value::Int(3)],
472                vec![Value::Int(1)],
473                vec![Value::Int(3)],
474            ]
475        );
476    }
477
478    #[test]
479    fn multiplicity_expansion_runs_on_pooled_buffers() {
480        use super::super::pool::{RowBufferPool, MAX_POOLED_ROWS};
481        // Pre-fill the pool past its bound: only MAX buffers are retained.
482        // The pool is process-global, so this test only asserts the upper
483        // bound (concurrent tests may also touch the pool) plus the
484        // deterministic expanded contents.
485        RowBufferPool::clear_for_test();
486        let mut spare: Vec<Vec<Value>> = (0..(MAX_POOLED_ROWS + 8))
487            .map(|i| vec![Value::Int(i as i32)])
488            .collect();
489        RowBufferPool::release_rows(&mut spare);
490        let rows = vec![vec![Value::Int(7)], vec![Value::Int(8)]];
491        let mut chunk = DataChunk::new_with_layout(rows, layout1());
492        chunk.set_multiplicity(3);
493        assert!(chunk.expand_multiplicity_in_place());
494        assert_eq!(
495            chunk.rows,
496            vec![
497                vec![Value::Int(7)],
498                vec![Value::Int(8)],
499                vec![Value::Int(7)],
500                vec![Value::Int(8)],
501                vec![Value::Int(7)],
502                vec![Value::Int(8)],
503            ]
504        );
505        assert!(RowBufferPool::pool_len() <= MAX_POOLED_ROWS);
506        RowBufferPool::clear_for_test();
507    }
508}