Skip to main content

linkrs_query/executor/streaming/chunk/columnar_batch/
collector.rs

1use std::cmp::Ordering;
2
3use crate::executor::streaming::chunk::core::DataChunk;
4use linkrs_core::value::NullType;
5use linkrs_core::Value;
6
7use super::BatchColumn;
8
9/// Column-major accumulation of a full relation (one [`BatchColumn`] per
10/// output column), used by blocking operators below the spill boundary.
11#[derive(Debug, Clone, Default)]
12pub struct ColumnarBatch {
13    columns: Vec<BatchColumn>,
14    num_rows: usize,
15}
16
17impl ColumnarBatch {
18    /// Create an empty batch with `num_columns` columns.
19    pub fn new(num_columns: usize) -> Self {
20        Self {
21            columns: vec![BatchColumn::Empty; num_columns],
22            num_rows: 0,
23        }
24    }
25
26    pub fn num_columns(&self) -> usize {
27        self.columns.len()
28    }
29
30    pub fn num_rows(&self) -> usize {
31        self.num_rows
32    }
33
34    pub fn is_empty(&self) -> bool {
35        self.num_rows == 0
36    }
37
38    pub fn column(&self, idx: usize) -> &BatchColumn {
39        &self.columns[idx]
40    }
41
42    /// Append all visible rows of `chunk`.
43    ///
44    /// Each column takes its raw kind from the chunk's typed layout; a kind
45    /// mismatch (or a fallback chunk column) degrades that column to
46    /// [`BatchColumn::Fallback`] with the accumulated rows preserved.
47    pub fn append_chunk(&mut self, chunk: &DataChunk) -> usize {
48        let indices = chunk.visible_indices();
49        if indices.is_empty() {
50            return self.num_rows;
51        }
52        let num_cols = chunk.num_columns();
53        if self.columns.is_empty() {
54            self.columns = vec![BatchColumn::Empty; num_cols];
55        }
56        if self.columns.len() < num_cols {
57            self.columns.resize(num_cols, BatchColumn::Empty);
58        }
59        for j in 0..num_cols {
60            let column = &mut self.columns[j];
61            if let Some(typed) = chunk.typed_column(j) {
62                column.append_typed(typed, &indices);
63            } else {
64                // Row-based chunk: per-value append.
65                let is_typed = column.is_typed();
66                if is_typed {
67                    for &i in &indices {
68                        let value = chunk
69                            .rows
70                            .get(i)
71                            .and_then(|r| r.get(j))
72                            .cloned()
73                            .unwrap_or_else(|| Value::Null(NullType::Null));
74                        column.append_row_value(&value);
75                    }
76                } else {
77                    for &i in &indices {
78                        let value = chunk
79                            .rows
80                            .get(i)
81                            .and_then(|r| r.get(j))
82                            .cloned()
83                            .unwrap_or_else(|| Value::Null(NullType::Null));
84                        if matches!(column, BatchColumn::Empty) {
85                            *column = BatchColumn::Fallback(vec![value]);
86                        } else {
87                            column.append_row_value(&value);
88                        }
89                    }
90                }
91            }
92        }
93        self.num_rows += indices.len();
94        self.num_rows
95    }
96
97    /// Append a single visible row of `chunk` at `idx`.
98    ///
99    /// Keeps the raw typed fast path per column (same degradation rules as
100    /// [`Self::append_chunk`]); used by operators that account memory per
101    /// appended row before buffering.
102    pub fn append_chunk_row(&mut self, chunk: &DataChunk, idx: usize) {
103        let num_cols = chunk.num_columns();
104        if self.columns.is_empty() {
105            self.columns = vec![BatchColumn::Empty; num_cols];
106        }
107        if self.columns.len() < num_cols {
108            self.columns.resize(num_cols, BatchColumn::Empty);
109        }
110        for (j, column) in self.columns.iter_mut().enumerate().take(num_cols) {
111            if let Some(typed) = chunk.typed_column(j) {
112                column.append_typed(typed, std::slice::from_ref(&idx));
113            } else {
114                let value = chunk
115                    .rows
116                    .get(idx)
117                    .and_then(|r| r.get(j))
118                    .cloned()
119                    .unwrap_or_else(|| Value::Null(NullType::Null));
120                if matches!(column, BatchColumn::Empty) {
121                    *column = BatchColumn::Fallback(vec![value]);
122                } else {
123                    column.append_row_value(&value);
124                }
125            }
126        }
127        self.num_rows += 1;
128    }
129
130    /// Append a single row of values (fallback path).
131    pub fn append_row(&mut self, row: &[Value]) {
132        if self.columns.is_empty() {
133            self.columns = vec![BatchColumn::Empty; row.len()];
134        }
135        if self.columns.len() < row.len() {
136            self.columns.resize(row.len(), BatchColumn::Empty);
137        }
138        for (j, column) in self.columns.iter_mut().enumerate() {
139            let value = row
140                .get(j)
141                .cloned()
142                .unwrap_or_else(|| Value::Null(NullType::Null));
143            column.append_row_value(&value);
144        }
145        self.num_rows += 1;
146    }
147
148    /// Materialize rows (row-major) from the current columnar state.
149    pub fn to_rows(&self) -> Vec<Vec<Value>> {
150        let mut rows = Vec::with_capacity(self.num_rows);
151        for i in 0..self.num_rows {
152            let mut row = Vec::with_capacity(self.columns.len());
153            for column in &self.columns {
154                row.push(column.value_at(i));
155            }
156            rows.push(row);
157        }
158        rows
159    }
160
161    /// Compare rows `a` and `b` on column `col` (raw fast path when typed).
162    pub fn compare_rows_at(&self, col: usize, a: usize, b: usize) -> Ordering {
163        self.columns[col].compare_at(a, b)
164    }
165
166    /// Compare the value `v` against row `idx` on column `col`.
167    pub fn compare_value_at(&self, col: usize, v: &Value, idx: usize) -> Ordering {
168        self.columns[col].compare_value_at(v, idx)
169    }
170
171    /// Reorder rows by `perm` (`result[i] = old[perm[i]]`).
172    pub fn permute(&mut self, perm: &[usize]) {
173        for column in &mut self.columns {
174            column.permute(perm);
175        }
176    }
177
178    /// Keep only the first `len` rows (columns must already be in the
179    /// desired order).
180    pub fn truncate(&mut self, len: usize) {
181        for column in &mut self.columns {
182            column.truncate(len);
183        }
184        self.num_rows = len.min(self.num_rows);
185    }
186
187    /// Drop all accumulated rows.
188    pub fn clear(&mut self) {
189        for column in &mut self.columns {
190            *column = BatchColumn::Empty;
191        }
192        self.num_rows = 0;
193    }
194
195    /// Estimated heap bytes (for memory accounting).
196    pub fn estimated_size(&self) -> usize {
197        self.columns.iter().map(|c| c.estimated_size()).sum()
198    }
199}