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#[derive(Debug, Clone)]
20pub struct DuckRow(Vec<DuckValue>, Arc<[Box<str>]>);
21
22impl DuckRow {
23 pub fn new(
31 result: Vec<DuckValue>,
32 col_names: Arc<[Box<str>]>,
33 ) -> DuckRow {
34 DuckRow(result, col_names)
35 }
36
37 #[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 #[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 pub fn column_count(&self) -> u64 {
77 self.1.len() as u64
78 }
79
80 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; 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 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 unsafe { ptr::write(values_ptr.add(col_idx as usize), val) };
120 }
121 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}