Skip to main content

better_duck_core/
result_set.rs

1use crate::raw::row::DuckRow;
2
3/// A fully materialized, owned query result.
4///
5/// Unlike [`DuckResult`](crate::raw::result::DuckResult), a `ResultSet` holds no FFI
6/// handles: it is `Send`, `Sync`, and `Clone`, and can be moved across threads or
7/// returned from a blocking task. Build one with
8/// [`DuckResult::materialize`](crate::raw::result::DuckResult::materialize).
9#[derive(Debug, Clone)]
10pub struct ResultSet {
11    rows: Vec<DuckRow>,
12    changes: u64,
13    column_names: Box<[Box<str>]>,
14    /// Lossless per-column type descriptors, carried over from the `DuckResult`
15    /// this was materialized from (same order as `column_names`).
16    column_schema: std::sync::Arc<[crate::types::TypeInfo]>,
17    /// The kind of statement that produced this result, carried from `DuckResult`.
18    statement_type: crate::raw::statement::StatementType,
19    /// What kind of output this result is, carried from `DuckResult`.
20    result_type: crate::raw::result::ResultType,
21}
22
23impl ResultSet {
24    /// Constructs a `ResultSet` from already-materialized parts.
25    pub(crate) fn new(
26        rows: Vec<DuckRow>,
27        changes: u64,
28        column_names: Box<[Box<str>]>,
29        column_schema: std::sync::Arc<[crate::types::TypeInfo]>,
30        statement_type: crate::raw::statement::StatementType,
31        result_type: crate::raw::result::ResultType,
32    ) -> ResultSet {
33        ResultSet { rows, changes, column_names, column_schema, statement_type, result_type }
34    }
35
36    /// Returns the rows as a slice.
37    pub fn rows(&self) -> &[DuckRow] {
38        &self.rows
39    }
40
41    /// Consumes this result set, returning the owned rows.
42    pub fn into_rows(self) -> Vec<DuckRow> {
43        self.rows
44    }
45
46    /// Returns the number of rows changed by the query that produced this result.
47    ///
48    /// `0` for `SELECT` statements.
49    pub fn changes(&self) -> u64 {
50        self.changes
51    }
52
53    /// Returns the column names in result order.
54    pub fn column_names(&self) -> &[Box<str>] {
55        &self.column_names
56    }
57
58    /// Returns the lossless [`TypeInfo`](crate::types::TypeInfo) of every column,
59    /// in result order — DECIMAL precision, nested list/struct/map/union shape,
60    /// enum labels, and so on, preserved from the query result.
61    pub fn column_schema(&self) -> &[crate::types::TypeInfo] {
62        &self.column_schema
63    }
64
65    /// The kind of statement that produced this result (SELECT, INSERT, …).
66    pub fn statement_type(&self) -> crate::raw::statement::StatementType {
67        self.statement_type
68    }
69
70    /// What kind of output this result is (rows, changed-row count, nothing).
71    pub fn result_type(&self) -> crate::raw::result::ResultType {
72        self.result_type
73    }
74
75    /// Returns the number of rows in this result set.
76    pub fn len(&self) -> usize {
77        self.rows.len()
78    }
79
80    /// Returns `true` if this result set has no rows.
81    pub fn is_empty(&self) -> bool {
82        self.rows.is_empty()
83    }
84
85    /// Returns the first row, if any.
86    pub fn first(&self) -> Option<&DuckRow> {
87        self.rows.first()
88    }
89}
90
91impl IntoIterator for ResultSet {
92    type Item = DuckRow;
93    type IntoIter = std::vec::IntoIter<DuckRow>;
94
95    fn into_iter(self) -> Self::IntoIter {
96        self.rows.into_iter()
97    }
98}
99
100impl<'a> IntoIterator for &'a ResultSet {
101    type Item = &'a DuckRow;
102    type IntoIter = std::slice::Iter<'a, DuckRow>;
103
104    fn into_iter(self) -> Self::IntoIter {
105        self.rows.iter()
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use std::sync::Arc;
112
113    use super::*;
114    use crate::types::value::DuckValue;
115
116    fn column_names() -> Box<[Box<str>]> {
117        vec![Box::from("id"), Box::from("name")].into_boxed_slice()
118    }
119
120    fn result_set() -> ResultSet {
121        let column_names = column_names();
122        let row_column_names = Arc::from(column_names.clone());
123        let rows = vec![
124            DuckRow::new(
125                vec![DuckValue::Int(20), DuckValue::Text("second".into())],
126                Arc::clone(&row_column_names),
127            ),
128            DuckRow::new(
129                vec![DuckValue::Int(10), DuckValue::Text("first".into())],
130                row_column_names,
131            ),
132        ];
133
134        let schema = Arc::from([
135            crate::types::TypeInfo::Scalar(crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER),
136            crate::types::TypeInfo::Scalar(crate::ffi::DUCKDB_TYPE_DUCKDB_TYPE_VARCHAR),
137        ]);
138        ResultSet::new(
139            rows,
140            2,
141            column_names,
142            schema,
143            crate::raw::statement::StatementType::Select,
144            crate::raw::result::ResultType::QueryResult,
145        )
146    }
147
148    fn ids<'a>(rows: impl IntoIterator<Item = &'a DuckRow>) -> Vec<i32> {
149        rows.into_iter()
150            .map(|row| match row.get_idx(0) {
151                Some(DuckValue::Int(id)) => *id,
152                value => panic!("expected integer id, got {value:?}"),
153            })
154            .collect()
155    }
156
157    #[test]
158    fn empty_result_exposes_metadata_and_no_rows() {
159        let result = ResultSet::new(
160            Vec::new(),
161            3,
162            column_names(),
163            Arc::from([]),
164            crate::raw::statement::StatementType::Select,
165            crate::raw::result::ResultType::QueryResult,
166        );
167
168        assert!(result.is_empty());
169        assert_eq!(result.len(), 0);
170        assert!(result.rows().is_empty());
171        assert!(result.first().is_none());
172        assert_eq!(result.changes(), 3);
173        assert_eq!(result.column_names(), &[Box::from("id"), Box::from("name")]);
174        assert!(result.into_rows().is_empty());
175    }
176
177    #[test]
178    fn non_empty_accessors_clone_and_debug_preserve_data() {
179        let result = result_set();
180        let cloned = result.clone();
181
182        assert!(!result.is_empty());
183        assert_eq!(result.len(), 2);
184        assert_eq!(result.changes(), 2);
185        assert_eq!(result.column_names(), &[Box::from("id"), Box::from("name")]);
186        assert_eq!(
187            result.first().and_then(|row| row.get("name")),
188            Some(&DuckValue::Text("second".into()))
189        );
190        assert_eq!(ids(result.rows()), vec![20, 10]);
191        assert_eq!(ids(cloned.rows()), vec![20, 10]);
192
193        let debug = format!("{result:?}");
194        assert!(debug.contains("ResultSet"));
195        assert!(debug.contains("changes: 2"));
196        assert!(debug.contains("second"));
197    }
198
199    #[test]
200    fn borrowed_and_consuming_iteration_retain_order() {
201        let result = result_set();
202
203        assert_eq!(ids(&result), vec![20, 10]);
204        assert_eq!(result.len(), 2, "borrowing must not consume the result");
205
206        let names: Vec<_> = result.into_iter().map(|row| row.get("name").cloned()).collect();
207        assert_eq!(
208            names,
209            vec![Some(DuckValue::Text("second".into())), Some(DuckValue::Text("first".into())),]
210        );
211    }
212}