1use std::collections::HashMap;
24
25use crate::error::{ColumnErrorKind, Error, Result};
26use crate::result::{Row, RowValue};
27
28#[derive(Debug)]
54pub struct RowAccessor<'a> {
55 row: &'a Row,
56 indices: &'a HashMap<&'a str, usize>,
57}
58
59impl<'a> RowAccessor<'a> {
60 pub(crate) fn new(row: &'a Row, indices: &'a HashMap<&'a str, usize>) -> Self {
64 Self { row, indices }
65 }
66
67 pub(crate) fn build_indices(schema: &'a crate::ResultSchema) -> HashMap<&'a str, usize> {
75 let mut map = HashMap::with_capacity(schema.column_count());
76 for i in 0..schema.column_count() {
77 map.insert(schema.column(i).name(), i);
78 }
79 map
80 }
81
82 pub fn get<T: RowValue>(&self, name: &str) -> Result<T> {
93 let idx = self
94 .indices
95 .get(name)
96 .copied()
97 .ok_or_else(|| Error::column(name, ColumnErrorKind::Missing))?;
98 match self.row.get::<T>(idx) {
99 Some(v) => Ok(v),
100 None => {
101 if self.row.is_null(idx) {
105 Err(Error::column(name, ColumnErrorKind::Null))
106 } else {
107 let actual = self
108 .row
109 .sql_type(idx)
110 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
111 Err(Error::column(
112 name,
113 ColumnErrorKind::TypeMismatch {
114 expected: std::any::type_name::<T>().to_string(),
115 actual,
116 },
117 ))
118 }
119 }
120 }
121 }
122
123 pub fn get_opt<T: RowValue>(&self, name: &str) -> Result<Option<T>> {
133 let idx = self
134 .indices
135 .get(name)
136 .copied()
137 .ok_or_else(|| Error::column(name, ColumnErrorKind::Missing))?;
138 if self.row.is_null(idx) {
139 return Ok(None);
140 }
141 if let Some(v) = self.row.get::<T>(idx) {
142 Ok(Some(v))
143 } else {
144 let actual = self
145 .row
146 .sql_type(idx)
147 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
148 Err(Error::column(
149 name,
150 ColumnErrorKind::TypeMismatch {
151 expected: std::any::type_name::<T>().to_string(),
152 actual,
153 },
154 ))
155 }
156 }
157
158 pub fn position<T: RowValue>(&self, idx: usize) -> Result<T> {
170 if idx >= self.row.column_count() {
171 return Err(Error::column_index_out_of_bounds(
172 idx,
173 self.row.column_count(),
174 ));
175 }
176 if let Some(v) = self.row.get::<T>(idx) {
180 Ok(v)
181 } else if self.row.is_null(idx) {
182 Err(Error::column(format!("col[{idx}]"), ColumnErrorKind::Null))
183 } else {
184 let actual = self
185 .row
186 .sql_type(idx)
187 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
188 Err(Error::column(
189 format!("col[{idx}]"),
190 ColumnErrorKind::TypeMismatch {
191 expected: std::any::type_name::<T>().to_string(),
192 actual,
193 },
194 ))
195 }
196 }
197
198 pub fn position_opt<T: RowValue>(&self, idx: usize) -> Result<Option<T>> {
211 if idx >= self.row.column_count() {
212 return Err(Error::column_index_out_of_bounds(
213 idx,
214 self.row.column_count(),
215 ));
216 }
217 if self.row.is_null(idx) {
218 return Ok(None);
219 }
220 if let Some(v) = self.row.get::<T>(idx) {
221 Ok(Some(v))
222 } else {
223 let actual = self
224 .row
225 .sql_type(idx)
226 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
227 Err(Error::column(
228 format!("col[{idx}]"),
229 ColumnErrorKind::TypeMismatch {
230 expected: std::any::type_name::<T>().to_string(),
231 actual,
232 },
233 ))
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::result::{ResultColumn, ResultSchema};
242 use arrow::array::{Int32Array, StringArray};
243 use arrow::datatypes::{DataType as ArrowType, Field, Schema};
244 use arrow::record_batch::RecordBatch;
245 use hyperdb_api_core::types::SqlType;
246 use std::sync::Arc;
247
248 fn user_row(id: Option<i32>, name: Option<&str>) -> (Row, Arc<ResultSchema>) {
251 let id_array = Int32Array::from(vec![id]);
252 let name_array = StringArray::from(vec![name]);
253 let arrow_schema = Arc::new(Schema::new(vec![
254 Field::new("id", ArrowType::Int32, true),
255 Field::new("name", ArrowType::Utf8, true),
256 ]));
257 let batch = Arc::new(
258 RecordBatch::try_new(arrow_schema, vec![Arc::new(id_array), Arc::new(name_array)])
259 .expect("batch"),
260 );
261 let schema = Arc::new(ResultSchema::from_columns(vec![
262 ResultColumn::new("id", SqlType::int(), 0),
263 ResultColumn::new("name", SqlType::text(), 1),
264 ]));
265 let row = Row::from_arrow(batch, 0, Some(Arc::clone(&schema)));
266 (row, schema)
267 }
268
269 #[test]
270 fn missing_column_errors_with_kind_missing() {
271 let (row, schema) = user_row(Some(1), Some("alice"));
272 let indices = RowAccessor::build_indices(&schema);
273 let accessor = RowAccessor::new(&row, &indices);
274
275 let err = accessor.get::<i32>("does_not_exist").unwrap_err();
276 match err {
277 Error::Column { name, kind } => {
278 assert_eq!(name, "does_not_exist");
279 assert!(matches!(kind, ColumnErrorKind::Missing));
280 }
281 other => panic!("expected Error::Column {{ kind: Missing }}, got {other:?}"),
282 }
283 }
284
285 #[test]
286 fn null_in_required_column_errors_with_kind_null() {
287 let (row, schema) = user_row(Some(1), None);
288 let indices = RowAccessor::build_indices(&schema);
289 let accessor = RowAccessor::new(&row, &indices);
290
291 let err = accessor.get::<String>("name").unwrap_err();
292 match err {
293 Error::Column { name, kind } => {
294 assert_eq!(name, "name");
295 assert!(matches!(kind, ColumnErrorKind::Null));
296 }
297 other => panic!("expected Error::Column {{ kind: Null }}, got {other:?}"),
298 }
299 }
300
301 #[test]
302 fn null_in_optional_column_returns_none() {
303 let (row, schema) = user_row(Some(1), None);
304 let indices = RowAccessor::build_indices(&schema);
305 let accessor = RowAccessor::new(&row, &indices);
306
307 let v: Option<String> = accessor.get_opt("name").expect("get_opt for NULL");
308 assert_eq!(v, None);
309 }
310
311 #[test]
312 fn happy_path_get_returns_value() {
313 let (row, schema) = user_row(Some(42), Some("alice"));
314 let indices = RowAccessor::build_indices(&schema);
315 let accessor = RowAccessor::new(&row, &indices);
316
317 let id: i32 = accessor.get("id").expect("get id");
318 let name: String = accessor.get("name").expect("get name");
319 assert_eq!(id, 42);
320 assert_eq!(name, "alice");
321 }
322
323 #[test]
324 fn happy_path_get_opt_returns_some() {
325 let (row, schema) = user_row(Some(42), Some("alice"));
326 let indices = RowAccessor::build_indices(&schema);
327 let accessor = RowAccessor::new(&row, &indices);
328
329 let id: Option<i32> = accessor.get_opt("id").expect("get_opt id");
330 let name: Option<String> = accessor.get_opt("name").expect("get_opt name");
331 assert_eq!(id, Some(42));
332 assert_eq!(name, Some("alice".to_string()));
333 }
334
335 #[test]
336 fn position_out_of_range_errors_with_index_oob() {
337 let (row, schema) = user_row(Some(1), Some("alice"));
338 let indices = RowAccessor::build_indices(&schema);
339 let accessor = RowAccessor::new(&row, &indices);
340
341 let err = accessor.position::<i32>(5).unwrap_err();
343 match err {
344 Error::ColumnIndexOutOfBounds { idx, column_count } => {
345 assert_eq!(idx, 5);
346 assert_eq!(column_count, 2);
347 }
348 other => panic!("expected Error::ColumnIndexOutOfBounds, got {other:?}"),
349 }
350 }
351
352 #[test]
353 fn position_in_range_returns_value() {
354 let (row, schema) = user_row(Some(42), Some("alice"));
355 let indices = RowAccessor::build_indices(&schema);
356 let accessor = RowAccessor::new(&row, &indices);
357
358 let id: i32 = accessor.position(0).expect("position 0");
359 assert_eq!(id, 42);
360 }
361
362 #[test]
363 fn position_opt_null_returns_none() {
364 let (row, schema) = user_row(Some(1), None);
365 let indices = RowAccessor::build_indices(&schema);
366 let accessor = RowAccessor::new(&row, &indices);
367
368 let v: Option<String> = accessor.position_opt(1).expect("position_opt for NULL");
369 assert_eq!(v, None);
370 }
371
372 #[test]
373 fn position_opt_value_returns_some() {
374 let (row, schema) = user_row(Some(42), Some("alice"));
375 let indices = RowAccessor::build_indices(&schema);
376 let accessor = RowAccessor::new(&row, &indices);
377
378 let id: Option<i32> = accessor.position_opt(0).expect("position_opt id");
379 let name: Option<String> = accessor.position_opt(1).expect("position_opt name");
380 assert_eq!(id, Some(42));
381 assert_eq!(name, Some("alice".to_string()));
382 }
383
384 #[test]
385 fn position_opt_out_of_range_errors_with_index_oob() {
386 let (row, schema) = user_row(Some(1), Some("alice"));
387 let indices = RowAccessor::build_indices(&schema);
388 let accessor = RowAccessor::new(&row, &indices);
389
390 let err = accessor.position_opt::<i32>(5).unwrap_err();
391 match err {
392 Error::ColumnIndexOutOfBounds { idx, column_count } => {
393 assert_eq!(idx, 5);
394 assert_eq!(column_count, 2);
395 }
396 other => panic!("expected Error::ColumnIndexOutOfBounds, got {other:?}"),
397 }
398 }
399
400 #[test]
401 fn position_null_errors_with_kind_null() {
402 let (row, schema) = user_row(Some(1), None);
406 let indices = RowAccessor::build_indices(&schema);
407 let accessor = RowAccessor::new(&row, &indices);
408
409 let err = accessor.position::<String>(1).unwrap_err();
411 match err {
412 Error::Column { name, kind } => {
413 assert_eq!(name, "col[1]");
414 assert!(matches!(kind, ColumnErrorKind::Null));
415 }
416 other => panic!("expected Error::Column {{ kind: Null }}, got {other:?}"),
417 }
418 }
419}