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!(
210            f,
211            "({} row{})",
212            self.num_rows_count,
213            if self.num_rows_count == 1 { "" } else { "s" }
214        )
215    }
216}
217
218fn format_value(val: &TypedValue) -> String {
219    match val {
220        TypedValue::Null => "NULL".to_string(),
221        TypedValue::Bool(b) => b.to_string(),
222        TypedValue::Int8(v) => v.to_string(),
223        TypedValue::Int16(v) => v.to_string(),
224        TypedValue::Int32(v) => v.to_string(),
225        TypedValue::Int64(v) => v.to_string(),
226        TypedValue::Float(v) => format!("{v:.1}"),
227        TypedValue::Double(v) => format!("{v:.1}"),
228        TypedValue::String(s) => s.to_string(),
229        _ => format!("{val:?}"),
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn empty_result() {
239        let result = QueryResult::new(vec![SmolStr::new("x")], vec![LogicalType::Int64]);
240        assert_eq!(result.num_rows(), 0);
241        assert_eq!(result.num_columns(), 1);
242    }
243
244    #[test]
245    fn push_rows() {
246        let mut result = QueryResult::new(
247            vec![SmolStr::new("a"), SmolStr::new("b")],
248            vec![LogicalType::Int64, LogicalType::String],
249        );
250        result.push_row(vec![
251            TypedValue::Int64(1),
252            TypedValue::String(SmolStr::new("hello")),
253        ]);
254        result.push_row(vec![
255            TypedValue::Int64(2),
256            TypedValue::String(SmolStr::new("world")),
257        ]);
258        assert_eq!(result.num_rows(), 2);
259    }
260
261    #[test]
262    fn row_access() {
263        let mut result = QueryResult::new(
264            vec![SmolStr::new("a"), SmolStr::new("b")],
265            vec![LogicalType::Int64, LogicalType::String],
266        );
267        result.push_row(vec![
268            TypedValue::Int64(1),
269            TypedValue::String(SmolStr::new("hello")),
270        ]);
271        assert_eq!(result.row(0)[0], TypedValue::Int64(1));
272        assert_eq!(result.row(0)[1], TypedValue::String(SmolStr::new("hello")));
273    }
274
275    #[test]
276    fn iter_rows_works() {
277        let mut result = QueryResult::new(vec![SmolStr::new("x")], vec![LogicalType::Int64]);
278        result.push_row(vec![TypedValue::Int64(1)]);
279        result.push_row(vec![TypedValue::Int64(2)]);
280        let rows: Vec<_> = result.iter_rows().collect();
281        assert_eq!(rows.len(), 2);
282        assert_eq!(rows[0][0], TypedValue::Int64(1));
283        assert_eq!(rows[1][0], TypedValue::Int64(2));
284    }
285
286    #[test]
287    fn display_format() {
288        let mut result = QueryResult::new(vec![SmolStr::new("x")], vec![LogicalType::Int64]);
289        result.push_row(vec![TypedValue::Int64(42)]);
290        let output = format!("{result}");
291        assert!(output.contains("42"));
292        assert!(output.contains("1 row"));
293    }
294
295    #[test]
296    fn display_plural() {
297        let mut result = QueryResult::new(vec![SmolStr::new("x")], vec![LogicalType::Int64]);
298        result.push_row(vec![TypedValue::Int64(1)]);
299        result.push_row(vec![TypedValue::Int64(2)]);
300        let output = format!("{result}");
301        assert!(output.contains("2 rows"));
302    }
303}