Skip to main content

better_duck_core/raw/
row.rs

1use crate::{
2    error::{Error, Result},
3    ffi::{self, DUCKDB_TYPE},
4    raw::data_chunk::DataChunk,
5    types::value::DuckValue,
6};
7use std::ptr;
8use std::sync::Arc;
9
10/// A single row of data returned by a DuckDB query, consisting of typed values
11/// and their associated column names.
12///
13/// Values are accessible by column index via [`get_idx`](DuckRow::get_idx)
14/// or by column name via [`get`](DuckRow::get).
15///
16/// Column names are shared via `Arc` across every row of the same result set:
17/// cloning a `DuckRow` (as the iterator does internally when rewind support is
18/// enabled) is one `Arc` bump, not a fresh per-row allocation of every column name.
19#[derive(Debug, Clone)]
20pub struct DuckRow(Vec<DuckValue>, Arc<[Box<str>]>);
21
22impl DuckRow {
23    /// Creates a new [`DuckRow`] from a vector of values and a shared, owned slice of
24    /// column names.
25    ///
26    /// # Arguments
27    ///
28    /// * `result` - The column values for this row.
29    /// * `col_names` - Owned column names, one per value, shared across the result set.
30    pub fn new(
31        result: Vec<DuckValue>,
32        col_names: Arc<[Box<str>]>,
33    ) -> DuckRow {
34        DuckRow(result, col_names)
35    }
36
37    /// Returns a reference to the value for the given column name, or `None` if
38    /// no column with that name exists in this row.
39    ///
40    /// The comparison is case-sensitive and matches by string value (not by pointer).
41    ///
42    /// # Arguments
43    ///
44    /// * `name` - The column name to look up.
45    #[allow(unused)]
46    pub fn get(
47        &self,
48        name: &str,
49    ) -> Option<&DuckValue> {
50        self.1
51            .iter()
52            .zip(self.0.iter())
53            .find(|(col_name, _)| col_name.as_ref() == name)
54            .map(|(_, value)| value)
55    }
56
57    /// Returns a reference to the value at the given column index, or `None` if the
58    /// index is out of range.
59    ///
60    /// # Arguments
61    ///
62    /// * `idx` - Zero-based column index.
63    #[allow(unused)]
64    pub fn get_idx(
65        &self,
66        idx: usize,
67    ) -> Option<&DuckValue> {
68        if idx < self.0.len() {
69            Some(&self.0[idx])
70        } else {
71            None
72        }
73    }
74
75    /// Returns the number of columns in this row.
76    pub fn column_count(&self) -> u64 {
77        self.1.len() as u64
78    }
79
80    /// Constructs a [`DuckRow`] from the current position of a `DataChunk`.
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if the chunk has no columns or if a column vector is null.
85    pub fn from_chunk(
86        chunk: &mut DataChunk,
87        col_names: Arc<[Box<str>]>,
88        col_types: &[DUCKDB_TYPE],
89    ) -> Result<Self> {
90        let row_idx = chunk.current_row() - 1; // Adjust for 0-based index
91        let column_count = col_names.len() as u64;
92        if column_count == 0 {
93            return Err(Error::DuckDBFailure(
94                ffi::Error::new(ffi::DuckDBError),
95                Some("No columns in result".to_owned()),
96            ));
97        }
98        let mut values: Vec<DuckValue> = Vec::with_capacity(column_count as usize);
99        let values_ptr: *mut DuckValue = values.as_mut_ptr();
100
101        for col_idx in 0..column_count {
102            // SAFETY: `**chunk` is a valid duckdb_data_chunk; `col_idx` is within
103            // [0, column_count). `duckdb_data_chunk_get_vector` returns null on failure,
104            // which we check immediately below.
105            let col_vec = unsafe { ffi::duckdb_data_chunk_get_vector(**chunk, col_idx) };
106
107            if col_vec.is_null() {
108                return Err(Error::DuckDBFailure(
109                    ffi::Error::new(ffi::DuckDBError),
110                    Some("Column returned invalid null ptr".to_owned()),
111                ));
112            }
113            let val = DuckValue::from_duckdb_vec(col_vec, col_types[col_idx as usize], row_idx)
114                .map_err(Error::ConversionError)?;
115
116            // SAFETY: `values_ptr` points to the allocation backing `values` with capacity
117            // `column_count`. `col_idx` is within that capacity, so `add(col_idx)` is in
118            // bounds. We set the length after all writes succeed.
119            unsafe { ptr::write(values_ptr.add(col_idx as usize), val) };
120        }
121        // SAFETY: We have written exactly `column_count` initialized elements starting at
122        // `values_ptr`. Setting the length to `column_count` is therefore sound.
123        unsafe { values.set_len(column_count as usize) };
124
125        Ok(DuckRow(values, col_names))
126    }
127}
128
129#[cfg(test)]
130#[allow(clippy::undocumented_unsafe_blocks)]
131mod tests {
132    use super::*;
133    use crate::{config::Config, helpers::path::path_to_cstring, raw::connection::RawConnection};
134
135    fn get_test_connection() -> RawConnection {
136        let c_path = path_to_cstring(":memory:".as_ref()).unwrap();
137        let config = Config::default().with("duckdb_api", "rust").unwrap();
138        RawConnection::open_with_flags(&c_path, config).unwrap()
139    }
140
141    fn column_names(names: &[&str]) -> Arc<[Box<str>]> {
142        names.iter().map(|name| Box::<str>::from(*name)).collect::<Vec<_>>().into()
143    }
144
145    fn empty_data_chunk_at_current_row() -> DataChunk {
146        let mut types = Vec::<ffi::duckdb_logical_type>::new();
147        let raw = unsafe { ffi::duckdb_create_data_chunk(types.as_mut_ptr(), 0) };
148        let mut chunk = DataChunk::new(raw).expect("DuckDB should create a zero-column chunk");
149        chunk.1 = 1;
150        chunk
151    }
152
153    #[test]
154    fn test_get_by_column_name() {
155        let mut con = get_test_connection();
156        con.query("CREATE TABLE t (id INTEGER, name TEXT)").unwrap();
157        con.query("INSERT INTO t VALUES (42, 'hello')").unwrap();
158
159        let mut stmt = con.prepare("SELECT id, name FROM t").unwrap();
160        let mut result = stmt.execute().unwrap();
161
162        let row = result.next().expect("expected a row").unwrap();
163        assert_eq!(row.get("id"), Some(&DuckValue::Int(42)));
164        assert_eq!(row.get("name"), Some(&DuckValue::Text("hello".to_string())));
165        assert_eq!(row.get("nonexistent"), None);
166    }
167
168    #[test]
169    fn new_preserves_values_names_and_order() {
170        let names = column_names(&["id", "active", "note"]);
171        let row = DuckRow::new(
172            vec![DuckValue::Int(7), DuckValue::Boolean(true), DuckValue::Null],
173            Arc::clone(&names),
174        );
175
176        assert_eq!(row.column_count(), 3);
177        assert_eq!(row.0, [DuckValue::Int(7), DuckValue::Boolean(true), DuckValue::Null]);
178        assert!(Arc::ptr_eq(&row.1, &names));
179        assert_eq!(row.get_idx(0), Some(&DuckValue::Int(7)));
180        assert_eq!(row.get_idx(1), Some(&DuckValue::Boolean(true)));
181        assert_eq!(row.get_idx(2), Some(&DuckValue::Null));
182    }
183
184    #[test]
185    fn empty_row_reports_no_columns_or_values() {
186        let row = DuckRow::new(Vec::new(), column_names(&[]));
187
188        assert_eq!(row.column_count(), 0);
189        assert!(row.0.is_empty());
190        assert_eq!(row.get_idx(0), None);
191        assert_eq!(row.get(""), None);
192        assert_eq!(row.get("anything"), None);
193    }
194
195    #[test]
196    fn get_idx_returns_values_at_boundaries_and_none_out_of_bounds() {
197        let row = DuckRow::new(
198            vec![DuckValue::Int(-1), DuckValue::Text("last".into())],
199            column_names(&["first", "last"]),
200        );
201
202        assert_eq!(row.get_idx(0), Some(&DuckValue::Int(-1)));
203        assert_eq!(row.get_idx(1), Some(&DuckValue::Text("last".into())));
204        assert_eq!(row.get_idx(2), None);
205        assert_eq!(row.get_idx(usize::MAX), None);
206    }
207
208    #[test]
209    fn get_is_case_sensitive_and_matches_exact_names() {
210        let row = DuckRow::new(
211            vec![DuckValue::Int(1), DuckValue::Int(2), DuckValue::Int(3)],
212            column_names(&["Name", "name", "name "]),
213        );
214
215        assert_eq!(row.get("Name"), Some(&DuckValue::Int(1)));
216        assert_eq!(row.get("name"), Some(&DuckValue::Int(2)));
217        assert_eq!(row.get("name "), Some(&DuckValue::Int(3)));
218        assert_eq!(row.get("NAME"), None);
219        assert_eq!(row.get(" name"), None);
220    }
221
222    #[test]
223    fn get_returns_first_value_for_duplicate_names() {
224        let row = DuckRow::new(
225            vec![DuckValue::Text("first".into()), DuckValue::Text("second".into())],
226            column_names(&["duplicate", "duplicate"]),
227        );
228
229        assert_eq!(row.get("duplicate"), Some(&DuckValue::Text("first".into())));
230        assert_eq!(row.get_idx(1), Some(&DuckValue::Text("second".into())));
231    }
232
233    #[test]
234    fn lookup_only_pairs_names_with_existing_values() {
235        let row = DuckRow::new(vec![DuckValue::Int(10)], column_names(&["first", "orphan"]));
236
237        assert_eq!(row.column_count(), 2);
238        assert_eq!(row.get("first"), Some(&DuckValue::Int(10)));
239        assert_eq!(row.get("orphan"), None);
240    }
241
242    #[test]
243    fn clone_has_independent_values_and_shared_column_names() {
244        let names = column_names(&["value"]);
245        let original = DuckRow::new(vec![DuckValue::Text("before".into())], Arc::clone(&names));
246        let mut cloned = original.clone();
247
248        assert!(Arc::ptr_eq(&original.1, &cloned.1));
249        assert!(Arc::ptr_eq(&cloned.1, &names));
250        cloned.0[0] = DuckValue::Text("after".into());
251        assert_eq!(original.get("value"), Some(&DuckValue::Text("before".into())));
252        assert_eq!(cloned.get("value"), Some(&DuckValue::Text("after".into())));
253    }
254
255    #[test]
256    fn debug_includes_values_and_column_names() {
257        let row = DuckRow::new(vec![DuckValue::Int(9)], column_names(&["score"]));
258        let debug = format!("{row:?}");
259
260        assert!(debug.contains("Int(9)"), "unexpected debug output: {debug}");
261        assert!(debug.contains("score"), "unexpected debug output: {debug}");
262    }
263
264    #[test]
265    fn from_chunk_rejects_empty_column_names_before_accessing_chunk() {
266        let mut chunk = empty_data_chunk_at_current_row();
267
268        let error = DuckRow::from_chunk(&mut chunk, column_names(&[]), &[]).unwrap_err();
269        assert_eq!(
270            error,
271            Error::DuckDBFailure(
272                ffi::Error::new(ffi::DuckDBError),
273                Some("No columns in result".to_owned()),
274            )
275        );
276    }
277
278    #[test]
279    fn from_chunk_rejects_a_name_without_a_corresponding_vector() {
280        let mut chunk = empty_data_chunk_at_current_row();
281
282        let error = DuckRow::from_chunk(
283            &mut chunk,
284            column_names(&["missing"]),
285            &[ffi::DUCKDB_TYPE_DUCKDB_TYPE_INTEGER],
286        )
287        .unwrap_err();
288        assert_eq!(
289            error,
290            Error::DuckDBFailure(
291                ffi::Error::new(ffi::DuckDBError),
292                Some("Column returned invalid null ptr".to_owned()),
293            )
294        );
295    }
296}