Skip to main content

kyu_executor/
result.rs

1//! QueryResult — final output of query execution.
2//!
3//! Uses flat `Vec<TypedValue>` storage (row-major) instead of `Vec<Vec<TypedValue>>`
4//! to eliminate per-row heap allocations. For 100K rows this avoids 100K small
5//! Vec allocations, giving ~2-3x speedup on the result-collection path.
6
7use kyu_types::{LogicalType, TypedValue};
8use smol_str::SmolStr;
9use std::fmt;
10
11use crate::value_vector::{FlatVector, ValueVector};
12
13/// The result of executing a query: column metadata + rows of typed values.
14///
15/// Values are stored in a single flat `Vec<TypedValue>` in row-major order
16/// (row0_col0, row0_col1, ..., row1_col0, row1_col1, ...). Access via
17/// `row(i)` returns a `&[TypedValue]` slice for row `i`.
18#[derive(Clone, Debug)]
19pub struct QueryResult {
20    pub column_names: Vec<SmolStr>,
21    pub column_types: Vec<LogicalType>,
22    data: Vec<TypedValue>,
23    num_rows_count: usize,
24}
25
26impl QueryResult {
27    pub fn new(column_names: Vec<SmolStr>, column_types: Vec<LogicalType>) -> Self {
28        Self {
29            column_names,
30            column_types,
31            data: Vec::new(),
32            num_rows_count: 0,
33        }
34    }
35
36    pub fn num_columns(&self) -> usize {
37        self.column_names.len()
38    }
39
40    pub fn num_rows(&self) -> usize {
41        self.num_rows_count
42    }
43
44    /// Access row `idx` as a typed-value slice.
45    pub fn row(&self, idx: usize) -> &[TypedValue] {
46        let nc = self.column_names.len();
47        let start = idx * nc;
48        &self.data[start..start + nc]
49    }
50
51    /// Iterate over all rows as slices.
52    pub fn iter_rows(&self) -> impl Iterator<Item = &[TypedValue]> {
53        let nc = self.column_names.len();
54        (0..self.num_rows_count).map(move |i| {
55            let start = i * nc;
56            &self.data[start..start + nc]
57        })
58    }
59
60    pub fn push_row(&mut self, row: Vec<TypedValue>) {
61        debug_assert_eq!(row.len(), self.column_names.len());
62        self.data.extend(row);
63        self.num_rows_count += 1;
64    }
65
66    /// Batch-append all rows from a DataChunk.
67    ///
68    /// Uses typed fast-paths for FlatVector columns (direct slice access) to
69    /// avoid per-element byte parsing and type dispatch. The flat storage
70    /// eliminates per-row Vec allocations entirely.
71    pub fn push_chunk(&mut self, chunk: &crate::data_chunk::DataChunk) {
72        let n = chunk.num_rows();
73        if n == 0 {
74            return;
75        }
76        let num_cols = self.column_names.len();
77
78        // Single allocation for all values in this chunk.
79        self.data.reserve(n * num_cols);
80
81        if chunk.selection().is_identity() {
82            push_chunk_identity(&mut self.data, chunk, n, num_cols);
83        } else {
84            for row_idx in 0..n {
85                for col_idx in 0..num_cols {
86                    self.data.push(chunk.get_value(row_idx, col_idx));
87                }
88            }
89        }
90        self.num_rows_count += n;
91    }
92}
93
94/// Fast path for identity selection: read each column via typed accessors,
95/// write row-major into the flat output buffer.
96fn push_chunk_identity(
97    data: &mut Vec<TypedValue>,
98    chunk: &crate::data_chunk::DataChunk,
99    n: usize,
100    num_cols: usize,
101) {
102    // Pre-fill with Null, then overwrite column-by-column.
103    let base = data.len();
104    data.resize(base + n * num_cols, TypedValue::Null);
105    let dest = &mut data[base..];
106
107    for col_idx in 0..num_cols {
108        let col = chunk.column(col_idx);
109        match col {
110            ValueVector::Flat(flat) => {
111                push_flat_strided(dest, col_idx, num_cols, flat, n);
112            }
113            ValueVector::String(sv) => {
114                let sdata = sv.data();
115                for i in 0..n {
116                    dest[i * num_cols + col_idx] = match &sdata[i] {
117                        Some(s) => TypedValue::String(s.clone()),
118                        None => TypedValue::Null,
119                    };
120                }
121            }
122            ValueVector::Bool(bv) => {
123                for i in 0..n {
124                    dest[i * num_cols + col_idx] = bv.get_value(i);
125                }
126            }
127            ValueVector::Owned(v) => {
128                for i in 0..n {
129                    dest[i * num_cols + col_idx] = v[i].clone();
130                }
131            }
132        }
133    }
134}
135
136/// Write a FlatVector column into strided flat storage using typed slice accessors.
137fn push_flat_strided(
138    dest: &mut [TypedValue],
139    col_idx: usize,
140    stride: usize,
141    flat: &FlatVector,
142    n: usize,
143) {
144    let nm = flat.null_mask();
145    match flat.logical_type() {
146        LogicalType::Int64 | LogicalType::Serial => {
147            let slice = flat.data_as_i64_slice();
148            for i in 0..n {
149                if !nm.is_null(i as u64) {
150                    dest[i * stride + col_idx] = TypedValue::Int64(slice[i]);
151                }
152            }
153        }
154        LogicalType::Int32 => {
155            let slice = flat.data_as_i32_slice();
156            for i in 0..n {
157                if !nm.is_null(i as u64) {
158                    dest[i * stride + col_idx] = TypedValue::Int32(slice[i]);
159                }
160            }
161        }
162        LogicalType::Double => {
163            let slice = flat.data_as_f64_slice();
164            for i in 0..n {
165                if !nm.is_null(i as u64) {
166                    dest[i * stride + col_idx] = TypedValue::Double(slice[i]);
167                }
168            }
169        }
170        LogicalType::Float => {
171            let slice = flat.data_as_f32_slice();
172            for i in 0..n {
173                if !nm.is_null(i as u64) {
174                    dest[i * stride + col_idx] = TypedValue::Float(slice[i]);
175                }
176            }
177        }
178        _ => {
179            for i in 0..n {
180                dest[i * stride + col_idx] = flat.get_value(i);
181            }
182        }
183    }
184}
185
186impl fmt::Display for QueryResult {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        // Header.
189        let headers: Vec<&str> = self.column_names.iter().map(|n| n.as_str()).collect();
190        writeln!(f, "| {} |", headers.join(" | "))?;
191        writeln!(
192            f,
193            "|{}|",
194            headers
195                .iter()
196                .map(|h| "-".repeat(h.len() + 2))
197                .collect::<Vec<_>>()
198                .join("|")
199        )?;
200        // Rows.
201        for row in self.iter_rows() {
202            let cells: Vec<String> = row
203                .iter()
204                .zip(&self.column_names)
205                .map(|(val, name)| format!("{:>width$}", format_value(val), width = name.len()))
206                .collect();
207            writeln!(f, "| {} |", cells.join(" | "))?;
208        }
209        writeln!(f, "({} row{})", self.num_rows_count, if self.num_rows_count == 1 { "" } else { "s" })
210    }
211}
212
213fn format_value(val: &TypedValue) -> String {
214    match val {
215        TypedValue::Null => "NULL".to_string(),
216        TypedValue::Bool(b) => b.to_string(),
217        TypedValue::Int8(v) => v.to_string(),
218        TypedValue::Int16(v) => v.to_string(),
219        TypedValue::Int32(v) => v.to_string(),
220        TypedValue::Int64(v) => v.to_string(),
221        TypedValue::Float(v) => format!("{v:.1}"),
222        TypedValue::Double(v) => format!("{v:.1}"),
223        TypedValue::String(s) => s.to_string(),
224        _ => format!("{val:?}"),
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn empty_result() {
234        let result = QueryResult::new(
235            vec![SmolStr::new("x")],
236            vec![LogicalType::Int64],
237        );
238        assert_eq!(result.num_rows(), 0);
239        assert_eq!(result.num_columns(), 1);
240    }
241
242    #[test]
243    fn push_rows() {
244        let mut result = QueryResult::new(
245            vec![SmolStr::new("a"), SmolStr::new("b")],
246            vec![LogicalType::Int64, LogicalType::String],
247        );
248        result.push_row(vec![
249            TypedValue::Int64(1),
250            TypedValue::String(SmolStr::new("hello")),
251        ]);
252        result.push_row(vec![
253            TypedValue::Int64(2),
254            TypedValue::String(SmolStr::new("world")),
255        ]);
256        assert_eq!(result.num_rows(), 2);
257    }
258
259    #[test]
260    fn row_access() {
261        let mut result = QueryResult::new(
262            vec![SmolStr::new("a"), SmolStr::new("b")],
263            vec![LogicalType::Int64, LogicalType::String],
264        );
265        result.push_row(vec![
266            TypedValue::Int64(1),
267            TypedValue::String(SmolStr::new("hello")),
268        ]);
269        assert_eq!(result.row(0)[0], TypedValue::Int64(1));
270        assert_eq!(result.row(0)[1], TypedValue::String(SmolStr::new("hello")));
271    }
272
273    #[test]
274    fn iter_rows_works() {
275        let mut result = QueryResult::new(
276            vec![SmolStr::new("x")],
277            vec![LogicalType::Int64],
278        );
279        result.push_row(vec![TypedValue::Int64(1)]);
280        result.push_row(vec![TypedValue::Int64(2)]);
281        let rows: Vec<_> = result.iter_rows().collect();
282        assert_eq!(rows.len(), 2);
283        assert_eq!(rows[0][0], TypedValue::Int64(1));
284        assert_eq!(rows[1][0], TypedValue::Int64(2));
285    }
286
287    #[test]
288    fn display_format() {
289        let mut result = QueryResult::new(
290            vec![SmolStr::new("x")],
291            vec![LogicalType::Int64],
292        );
293        result.push_row(vec![TypedValue::Int64(42)]);
294        let output = format!("{result}");
295        assert!(output.contains("42"));
296        assert!(output.contains("1 row"));
297    }
298
299    #[test]
300    fn display_plural() {
301        let mut result = QueryResult::new(
302            vec![SmolStr::new("x")],
303            vec![LogicalType::Int64],
304        );
305        result.push_row(vec![TypedValue::Int64(1)]);
306        result.push_row(vec![TypedValue::Int64(2)]);
307        let output = format!("{result}");
308        assert!(output.contains("2 rows"));
309    }
310}