1use std::collections::HashMap;
24
25use crate::error::{ColumnErrorKind, Error, Result};
26use crate::result::{Row, RowValue};
27
28#[derive(Debug)]
37enum Indices<'a> {
38 Borrowed(&'a HashMap<&'a str, usize>),
39 Owned(&'a HashMap<String, usize>),
40}
41
42impl Indices<'_> {
43 fn get(&self, name: &str) -> Option<usize> {
45 match self {
46 Indices::Borrowed(m) => m.get(name).copied(),
47 Indices::Owned(m) => m.get(name).copied(),
48 }
49 }
50}
51
52#[derive(Debug)]
78pub struct RowAccessor<'a> {
79 row: &'a Row,
80 indices: Indices<'a>,
81}
82
83impl<'a> RowAccessor<'a> {
84 pub(crate) fn new(row: &'a Row, indices: &'a HashMap<&'a str, usize>) -> Self {
88 Self {
89 row,
90 indices: Indices::Borrowed(indices),
91 }
92 }
93
94 pub(crate) fn new_owned(row: &'a Row, indices: &'a HashMap<String, usize>) -> Self {
98 Self {
99 row,
100 indices: Indices::Owned(indices),
101 }
102 }
103
104 pub(crate) fn build_indices(schema: &'a crate::ResultSchema) -> HashMap<&'a str, usize> {
112 let mut map = HashMap::with_capacity(schema.column_count());
113 for i in 0..schema.column_count() {
114 map.insert(schema.column(i).name(), i);
115 }
116 map
117 }
118
119 pub(crate) fn build_owned_indices(schema: &crate::ResultSchema) -> HashMap<String, usize> {
127 let mut map = HashMap::with_capacity(schema.column_count());
128 for i in 0..schema.column_count() {
129 map.insert(schema.column(i).name().to_string(), i);
130 }
131 map
132 }
133
134 pub fn get<T: RowValue>(&self, name: &str) -> Result<T> {
145 let idx = self
146 .indices
147 .get(name)
148 .ok_or_else(|| Error::column(name, ColumnErrorKind::Missing))?;
149 match self.row.get::<T>(idx) {
150 Some(v) => Ok(v),
151 None => {
152 if self.row.is_null(idx) {
156 Err(Error::column(name, ColumnErrorKind::Null))
157 } else {
158 let actual = self
159 .row
160 .sql_type(idx)
161 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
162 Err(Error::column(
163 name,
164 ColumnErrorKind::TypeMismatch {
165 expected: std::any::type_name::<T>().to_string(),
166 actual,
167 },
168 ))
169 }
170 }
171 }
172 }
173
174 pub fn get_opt<T: RowValue>(&self, name: &str) -> Result<Option<T>> {
184 let idx = self
185 .indices
186 .get(name)
187 .ok_or_else(|| Error::column(name, ColumnErrorKind::Missing))?;
188 if self.row.is_null(idx) {
189 return Ok(None);
190 }
191 if let Some(v) = self.row.get::<T>(idx) {
192 Ok(Some(v))
193 } else {
194 let actual = self
195 .row
196 .sql_type(idx)
197 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
198 Err(Error::column(
199 name,
200 ColumnErrorKind::TypeMismatch {
201 expected: std::any::type_name::<T>().to_string(),
202 actual,
203 },
204 ))
205 }
206 }
207
208 pub fn position<T: RowValue>(&self, idx: usize) -> Result<T> {
220 if idx >= self.row.column_count() {
221 return Err(Error::column_index_out_of_bounds(
222 idx,
223 self.row.column_count(),
224 ));
225 }
226 if let Some(v) = self.row.get::<T>(idx) {
230 Ok(v)
231 } else if self.row.is_null(idx) {
232 Err(Error::column(format!("col[{idx}]"), ColumnErrorKind::Null))
233 } else {
234 let actual = self
235 .row
236 .sql_type(idx)
237 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
238 Err(Error::column(
239 format!("col[{idx}]"),
240 ColumnErrorKind::TypeMismatch {
241 expected: std::any::type_name::<T>().to_string(),
242 actual,
243 },
244 ))
245 }
246 }
247
248 pub fn position_opt<T: RowValue>(&self, idx: usize) -> Result<Option<T>> {
261 if idx >= self.row.column_count() {
262 return Err(Error::column_index_out_of_bounds(
263 idx,
264 self.row.column_count(),
265 ));
266 }
267 if self.row.is_null(idx) {
268 return Ok(None);
269 }
270 if let Some(v) = self.row.get::<T>(idx) {
271 Ok(Some(v))
272 } else {
273 let actual = self
274 .row
275 .sql_type(idx)
276 .map_or_else(|| "<unknown>".to_string(), |t| format!("{t:?}"));
277 Err(Error::column(
278 format!("col[{idx}]"),
279 ColumnErrorKind::TypeMismatch {
280 expected: std::any::type_name::<T>().to_string(),
281 actual,
282 },
283 ))
284 }
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::result::{ResultColumn, ResultSchema};
292 use arrow::array::{Int32Array, StringArray};
293 use arrow::datatypes::{DataType as ArrowType, Field, Schema};
294 use arrow::record_batch::RecordBatch;
295 use hyperdb_api_core::types::SqlType;
296 use std::sync::Arc;
297
298 fn user_row(id: Option<i32>, name: Option<&str>) -> (Row, Arc<ResultSchema>) {
301 let id_array = Int32Array::from(vec![id]);
302 let name_array = StringArray::from(vec![name]);
303 let arrow_schema = Arc::new(Schema::new(vec![
304 Field::new("id", ArrowType::Int32, true),
305 Field::new("name", ArrowType::Utf8, true),
306 ]));
307 let batch = Arc::new(
308 RecordBatch::try_new(arrow_schema, vec![Arc::new(id_array), Arc::new(name_array)])
309 .expect("batch"),
310 );
311 let schema = Arc::new(ResultSchema::from_columns(vec![
312 ResultColumn::new("id", SqlType::int(), 0),
313 ResultColumn::new("name", SqlType::text(), 1),
314 ]));
315 let row = Row::from_arrow(batch, 0, Some(Arc::clone(&schema)));
316 (row, schema)
317 }
318
319 #[test]
320 fn missing_column_errors_with_kind_missing() {
321 let (row, schema) = user_row(Some(1), Some("alice"));
322 let indices = RowAccessor::build_indices(&schema);
323 let accessor = RowAccessor::new(&row, &indices);
324
325 let err = accessor.get::<i32>("does_not_exist").unwrap_err();
326 match err {
327 Error::Column { name, kind } => {
328 assert_eq!(name, "does_not_exist");
329 assert!(matches!(kind, ColumnErrorKind::Missing));
330 }
331 other => panic!("expected Error::Column {{ kind: Missing }}, got {other:?}"),
332 }
333 }
334
335 #[test]
336 fn null_in_required_column_errors_with_kind_null() {
337 let (row, schema) = user_row(Some(1), None);
338 let indices = RowAccessor::build_indices(&schema);
339 let accessor = RowAccessor::new(&row, &indices);
340
341 let err = accessor.get::<String>("name").unwrap_err();
342 match err {
343 Error::Column { name, kind } => {
344 assert_eq!(name, "name");
345 assert!(matches!(kind, ColumnErrorKind::Null));
346 }
347 other => panic!("expected Error::Column {{ kind: Null }}, got {other:?}"),
348 }
349 }
350
351 #[test]
352 fn null_in_optional_column_returns_none() {
353 let (row, schema) = user_row(Some(1), None);
354 let indices = RowAccessor::build_indices(&schema);
355 let accessor = RowAccessor::new(&row, &indices);
356
357 let v: Option<String> = accessor.get_opt("name").expect("get_opt for NULL");
358 assert_eq!(v, None);
359 }
360
361 #[test]
362 fn happy_path_get_returns_value() {
363 let (row, schema) = user_row(Some(42), Some("alice"));
364 let indices = RowAccessor::build_indices(&schema);
365 let accessor = RowAccessor::new(&row, &indices);
366
367 let id: i32 = accessor.get("id").expect("get id");
368 let name: String = accessor.get("name").expect("get name");
369 assert_eq!(id, 42);
370 assert_eq!(name, "alice");
371 }
372
373 #[test]
374 fn happy_path_get_opt_returns_some() {
375 let (row, schema) = user_row(Some(42), Some("alice"));
376 let indices = RowAccessor::build_indices(&schema);
377 let accessor = RowAccessor::new(&row, &indices);
378
379 let id: Option<i32> = accessor.get_opt("id").expect("get_opt id");
380 let name: Option<String> = accessor.get_opt("name").expect("get_opt name");
381 assert_eq!(id, Some(42));
382 assert_eq!(name, Some("alice".to_string()));
383 }
384
385 #[test]
386 fn position_out_of_range_errors_with_index_oob() {
387 let (row, schema) = user_row(Some(1), Some("alice"));
388 let indices = RowAccessor::build_indices(&schema);
389 let accessor = RowAccessor::new(&row, &indices);
390
391 let err = accessor.position::<i32>(5).unwrap_err();
393 match err {
394 Error::ColumnIndexOutOfBounds { idx, column_count } => {
395 assert_eq!(idx, 5);
396 assert_eq!(column_count, 2);
397 }
398 other => panic!("expected Error::ColumnIndexOutOfBounds, got {other:?}"),
399 }
400 }
401
402 #[test]
403 fn position_in_range_returns_value() {
404 let (row, schema) = user_row(Some(42), Some("alice"));
405 let indices = RowAccessor::build_indices(&schema);
406 let accessor = RowAccessor::new(&row, &indices);
407
408 let id: i32 = accessor.position(0).expect("position 0");
409 assert_eq!(id, 42);
410 }
411
412 #[test]
413 fn position_opt_null_returns_none() {
414 let (row, schema) = user_row(Some(1), None);
415 let indices = RowAccessor::build_indices(&schema);
416 let accessor = RowAccessor::new(&row, &indices);
417
418 let v: Option<String> = accessor.position_opt(1).expect("position_opt for NULL");
419 assert_eq!(v, None);
420 }
421
422 #[test]
423 fn position_opt_value_returns_some() {
424 let (row, schema) = user_row(Some(42), Some("alice"));
425 let indices = RowAccessor::build_indices(&schema);
426 let accessor = RowAccessor::new(&row, &indices);
427
428 let id: Option<i32> = accessor.position_opt(0).expect("position_opt id");
429 let name: Option<String> = accessor.position_opt(1).expect("position_opt name");
430 assert_eq!(id, Some(42));
431 assert_eq!(name, Some("alice".to_string()));
432 }
433
434 #[test]
435 fn position_opt_out_of_range_errors_with_index_oob() {
436 let (row, schema) = user_row(Some(1), Some("alice"));
437 let indices = RowAccessor::build_indices(&schema);
438 let accessor = RowAccessor::new(&row, &indices);
439
440 let err = accessor.position_opt::<i32>(5).unwrap_err();
441 match err {
442 Error::ColumnIndexOutOfBounds { idx, column_count } => {
443 assert_eq!(idx, 5);
444 assert_eq!(column_count, 2);
445 }
446 other => panic!("expected Error::ColumnIndexOutOfBounds, got {other:?}"),
447 }
448 }
449
450 #[test]
451 fn position_null_errors_with_kind_null() {
452 let (row, schema) = user_row(Some(1), None);
456 let indices = RowAccessor::build_indices(&schema);
457 let accessor = RowAccessor::new(&row, &indices);
458
459 let err = accessor.position::<String>(1).unwrap_err();
461 match err {
462 Error::Column { name, kind } => {
463 assert_eq!(name, "col[1]");
464 assert!(matches!(kind, ColumnErrorKind::Null));
465 }
466 other => panic!("expected Error::Column {{ kind: Null }}, got {other:?}"),
467 }
468 }
469
470 #[test]
473 fn owned_happy_path_get_returns_value() {
474 let (row, schema) = user_row(Some(42), Some("alice"));
475 let indices = RowAccessor::build_owned_indices(&schema);
476 let accessor = RowAccessor::new_owned(&row, &indices);
477
478 let id: i32 = accessor.get("id").expect("get id");
479 let name: String = accessor.get("name").expect("get name");
480 assert_eq!(id, 42);
481 assert_eq!(name, "alice");
482 }
483
484 #[test]
485 fn owned_missing_column_errors_with_kind_missing() {
486 let (row, schema) = user_row(Some(1), Some("alice"));
487 let indices = RowAccessor::build_owned_indices(&schema);
488 let accessor = RowAccessor::new_owned(&row, &indices);
489
490 let err = accessor.get::<i32>("does_not_exist").unwrap_err();
491 match err {
492 Error::Column { name, kind } => {
493 assert_eq!(name, "does_not_exist");
494 assert!(matches!(kind, ColumnErrorKind::Missing));
495 }
496 other => panic!("expected Error::Column {{ kind: Missing }}, got {other:?}"),
497 }
498 }
499
500 #[test]
501 fn owned_null_in_required_column_errors_with_kind_null() {
502 let (row, schema) = user_row(Some(1), None);
503 let indices = RowAccessor::build_owned_indices(&schema);
504 let accessor = RowAccessor::new_owned(&row, &indices);
505
506 let err = accessor.get::<String>("name").unwrap_err();
507 match err {
508 Error::Column { name, kind } => {
509 assert_eq!(name, "name");
510 assert!(matches!(kind, ColumnErrorKind::Null));
511 }
512 other => panic!("expected Error::Column {{ kind: Null }}, got {other:?}"),
513 }
514 }
515
516 #[test]
517 fn owned_null_in_optional_column_returns_none() {
518 let (row, schema) = user_row(Some(1), None);
519 let indices = RowAccessor::build_owned_indices(&schema);
520 let accessor = RowAccessor::new_owned(&row, &indices);
521
522 let v: Option<String> = accessor.get_opt("name").expect("get_opt for NULL");
523 assert_eq!(v, None);
524 }
525}