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    #[test]
142    fn test_get_by_column_name() {
143        let mut con = get_test_connection();
144        con.query("CREATE TABLE t (id INTEGER, name TEXT)").unwrap();
145        con.query("INSERT INTO t VALUES (42, 'hello')").unwrap();
146
147        let mut stmt = con.prepare("SELECT id, name FROM t").unwrap();
148        let mut result = stmt.execute().unwrap();
149
150        let row = result.next().expect("expected a row").unwrap();
151        assert_eq!(row.get("id"), Some(&DuckValue::Int(42)));
152        assert_eq!(row.get("name"), Some(&DuckValue::Text("hello".to_string())));
153        assert_eq!(row.get("nonexistent"), None);
154    }
155}