1use std::borrow::Cow;
39use std::fmt;
40
41#[derive(Debug, Clone)]
43pub enum RowError {
44 ColumnNotFound(String),
46 TypeConversion { column: String, message: String },
48 UnexpectedNull(String),
50}
51
52impl fmt::Display for RowError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 Self::ColumnNotFound(col) => write!(f, "column '{}' not found", col),
56 Self::TypeConversion { column, message } => {
57 write!(f, "type conversion error for '{}': {}", column, message)
58 }
59 Self::UnexpectedNull(col) => write!(f, "unexpected null in column '{}'", col),
60 }
61 }
62}
63
64impl std::error::Error for RowError {}
65
66pub trait RowRef {
71 fn get_i32(&self, column: &str) -> Result<i32, RowError>;
73
74 fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError>;
76
77 fn get_i64(&self, column: &str) -> Result<i64, RowError>;
79
80 fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError>;
82
83 fn get_f64(&self, column: &str) -> Result<f64, RowError>;
85
86 fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError>;
88
89 fn get_bool(&self, column: &str) -> Result<bool, RowError>;
91
92 fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError>;
94
95 fn get_str(&self, column: &str) -> Result<&str, RowError>;
100
101 fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError>;
103
104 fn get_string(&self, column: &str) -> Result<String, RowError> {
106 self.get_str(column).map(|s| s.to_string())
107 }
108
109 fn get_string_opt(&self, column: &str) -> Result<Option<String>, RowError> {
111 self.get_str_opt(column)
112 .map(|opt| opt.map(|s| s.to_string()))
113 }
114
115 fn get_bytes(&self, column: &str) -> Result<&[u8], RowError>;
117
118 fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError>;
120
121 fn get_cow_str(&self, column: &str) -> Result<Cow<'_, str>, RowError> {
123 self.get_str(column).map(Cow::Borrowed)
124 }
125
126 fn get_datetime_utc(&self, column: &str) -> Result<chrono::DateTime<chrono::Utc>, RowError> {
127 Err(unsupported_get(column, "datetime_utc"))
128 }
129 fn get_datetime_utc_opt(
130 &self,
131 column: &str,
132 ) -> Result<Option<chrono::DateTime<chrono::Utc>>, RowError> {
133 Err(unsupported_get(column, "datetime_utc_opt"))
134 }
135 fn get_naive_datetime(&self, column: &str) -> Result<chrono::NaiveDateTime, RowError> {
136 Err(unsupported_get(column, "naive_datetime"))
137 }
138 fn get_naive_datetime_opt(
139 &self,
140 column: &str,
141 ) -> Result<Option<chrono::NaiveDateTime>, RowError> {
142 Err(unsupported_get(column, "naive_datetime_opt"))
143 }
144 fn get_naive_date(&self, column: &str) -> Result<chrono::NaiveDate, RowError> {
145 Err(unsupported_get(column, "naive_date"))
146 }
147 fn get_naive_date_opt(&self, column: &str) -> Result<Option<chrono::NaiveDate>, RowError> {
148 Err(unsupported_get(column, "naive_date_opt"))
149 }
150 fn get_naive_time(&self, column: &str) -> Result<chrono::NaiveTime, RowError> {
151 Err(unsupported_get(column, "naive_time"))
152 }
153 fn get_naive_time_opt(&self, column: &str) -> Result<Option<chrono::NaiveTime>, RowError> {
154 Err(unsupported_get(column, "naive_time_opt"))
155 }
156 fn get_uuid(&self, column: &str) -> Result<uuid::Uuid, RowError> {
157 Err(unsupported_get(column, "uuid"))
158 }
159 fn get_uuid_opt(&self, column: &str) -> Result<Option<uuid::Uuid>, RowError> {
160 Err(unsupported_get(column, "uuid_opt"))
161 }
162 fn get_json(&self, column: &str) -> Result<serde_json::Value, RowError> {
163 Err(unsupported_get(column, "json"))
164 }
165 fn get_json_opt(&self, column: &str) -> Result<Option<serde_json::Value>, RowError> {
166 Err(unsupported_get(column, "json_opt"))
167 }
168 fn get_decimal(&self, column: &str) -> Result<rust_decimal::Decimal, RowError> {
169 Err(unsupported_get(column, "decimal"))
170 }
171 fn get_decimal_opt(&self, column: &str) -> Result<Option<rust_decimal::Decimal>, RowError> {
172 Err(unsupported_get(column, "decimal_opt"))
173 }
174
175 fn get_vector(&self, column: &str) -> Result<Vec<f32>, RowError> {
183 Err(unsupported_get(column, "vector"))
184 }
185
186 fn get_string_array(&self, column: &str) -> Result<Vec<String>, RowError> {
193 Err(unsupported_get(column, "string_array"))
194 }
195
196 fn is_null(&self, column: &str) -> Result<bool, RowError> {
210 self.get_str_opt(column).map(|opt| opt.is_none())
211 }
212}
213
214fn unsupported_get(column: &str, getter: &str) -> RowError {
219 RowError::TypeConversion {
220 column: column.to_string(),
221 message: format!("{getter} not supported by this row type"),
222 }
223}
224
225pub fn into_row_error<T, E: std::fmt::Display>(
230 column: &str,
231 res: Result<T, E>,
232) -> Result<T, RowError> {
233 res.map_err(|e| RowError::TypeConversion {
234 column: column.to_string(),
235 message: e.to_string(),
236 })
237}
238
239pub trait FromRowRef<'a>: Sized {
244 fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
246}
247
248pub trait FromRow: Sized {
253 fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
255}
256
257impl<T: FromRow> FromRowRef<'_> for T {
259 fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
260 T::from_row(row)
261 }
262}
263
264pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
266 rows: std::slice::Iter<'a, R>,
267 _marker: std::marker::PhantomData<T>,
268}
269
270impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
271 pub fn new(rows: &'a [R]) -> Self {
273 Self {
274 rows: rows.iter(),
275 _marker: std::marker::PhantomData,
276 }
277 }
278}
279
280impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
281 type Item = Result<T, RowError>;
282
283 fn next(&mut self) -> Option<Self::Item> {
284 self.rows.next().map(|row| T::from_row_ref(row))
285 }
286
287 fn size_hint(&self) -> (usize, Option<usize>) {
288 self.rows.size_hint()
289 }
290}
291
292impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
293
294#[derive(Debug, Clone)]
299pub enum RowData<'a> {
300 Borrowed(&'a str),
302 Owned(String),
304}
305
306impl<'a> RowData<'a> {
307 pub fn as_str(&self) -> &str {
309 match self {
310 Self::Borrowed(s) => s,
311 Self::Owned(s) => s,
312 }
313 }
314
315 pub fn into_owned(self) -> String {
317 match self {
318 Self::Borrowed(s) => s.to_string(),
319 Self::Owned(s) => s,
320 }
321 }
322
323 pub const fn borrowed(s: &'a str) -> Self {
325 Self::Borrowed(s)
326 }
327
328 pub fn owned(s: impl Into<String>) -> Self {
330 Self::Owned(s.into())
331 }
332}
333
334impl<'a> From<&'a str> for RowData<'a> {
335 fn from(s: &'a str) -> Self {
336 Self::Borrowed(s)
337 }
338}
339
340impl From<String> for RowData<'static> {
341 fn from(s: String) -> Self {
342 Self::Owned(s)
343 }
344}
345
346impl<'a> AsRef<str> for RowData<'a> {
347 fn as_ref(&self) -> &str {
348 self.as_str()
349 }
350}
351
352#[macro_export]
374macro_rules! impl_from_row {
375 ($type:ident { $($field:ident : i32),* $(,)? }) => {
376 impl $crate::row::FromRow for $type {
377 fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
378 Ok(Self {
379 $(
380 $field: row.get_i32(stringify!($field))?,
381 )*
382 })
383 }
384 }
385 };
386 ($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
387 impl $crate::row::FromRow for $type {
388 fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
389 Ok(Self {
390 $(
391 $field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
392 )*
393 })
394 }
395 }
396 };
397}
398
399#[doc(hidden)]
401pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
402 T::from_column(row, column)
403}
404
405pub trait FromColumn: Sized {
407 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
409}
410
411impl FromColumn for i32 {
412 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
413 row.get_i32(column)
414 }
415}
416
417impl FromColumn for i64 {
418 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
419 row.get_i64(column)
420 }
421}
422
423impl FromColumn for f64 {
424 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
425 row.get_f64(column)
426 }
427}
428
429impl FromColumn for bool {
430 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
431 row.get_bool(column)
432 }
433}
434
435impl FromColumn for String {
436 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
437 row.get_string(column)
438 }
439}
440
441impl FromColumn for Vec<u8> {
442 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
443 row.get_bytes(column).map(|b| b.to_vec())
444 }
445}
446
447impl FromColumn for chrono::DateTime<chrono::Utc> {
448 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
449 row.get_datetime_utc(column)
450 }
451}
452impl FromColumn for chrono::NaiveDateTime {
453 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
454 row.get_naive_datetime(column)
455 }
456}
457impl FromColumn for chrono::NaiveDate {
458 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
459 row.get_naive_date(column)
460 }
461}
462impl FromColumn for chrono::NaiveTime {
463 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
464 row.get_naive_time(column)
465 }
466}
467impl FromColumn for uuid::Uuid {
468 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
469 row.get_uuid(column)
470 }
471}
472impl FromColumn for serde_json::Value {
473 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
474 row.get_json(column)
475 }
476}
477impl FromColumn for rust_decimal::Decimal {
478 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
479 row.get_decimal(column)
480 }
481}
482
483impl FromColumn for Vec<f32> {
489 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
490 row.get_vector(column)
491 }
492}
493
494impl FromColumn for Vec<String> {
498 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
499 row.get_string_array(column)
500 }
501}
502
503impl<T: FromColumn> FromColumn for Option<T> {
516 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
517 if row.is_null(column)? {
518 Ok(None)
519 } else {
520 T::from_column(row, column).map(Some)
521 }
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528
529 struct MockRow {
531 data: std::collections::HashMap<String, String>,
532 }
533
534 impl RowRef for MockRow {
535 fn get_i32(&self, column: &str) -> Result<i32, RowError> {
536 self.data
537 .get(column)
538 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
539 .parse()
540 .map_err(|e| RowError::TypeConversion {
541 column: column.to_string(),
542 message: format!("{}", e),
543 })
544 }
545
546 fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
547 match self.data.get(column) {
548 Some(v) if v == "NULL" => Ok(None),
549 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
550 column: column.to_string(),
551 message: format!("{}", e),
552 }),
553 None => Ok(None),
554 }
555 }
556
557 fn get_i64(&self, column: &str) -> Result<i64, RowError> {
558 self.data
559 .get(column)
560 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
561 .parse()
562 .map_err(|e| RowError::TypeConversion {
563 column: column.to_string(),
564 message: format!("{}", e),
565 })
566 }
567
568 fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
569 match self.data.get(column) {
570 Some(v) if v == "NULL" => Ok(None),
571 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
572 column: column.to_string(),
573 message: format!("{}", e),
574 }),
575 None => Ok(None),
576 }
577 }
578
579 fn get_f64(&self, column: &str) -> Result<f64, RowError> {
580 self.data
581 .get(column)
582 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
583 .parse()
584 .map_err(|e| RowError::TypeConversion {
585 column: column.to_string(),
586 message: format!("{}", e),
587 })
588 }
589
590 fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
591 match self.data.get(column) {
592 Some(v) if v == "NULL" => Ok(None),
593 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
594 column: column.to_string(),
595 message: format!("{}", e),
596 }),
597 None => Ok(None),
598 }
599 }
600
601 fn get_bool(&self, column: &str) -> Result<bool, RowError> {
602 let v = self
603 .data
604 .get(column)
605 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
606 match v.as_str() {
607 "true" | "t" | "1" => Ok(true),
608 "false" | "f" | "0" => Ok(false),
609 _ => Err(RowError::TypeConversion {
610 column: column.to_string(),
611 message: "invalid boolean".to_string(),
612 }),
613 }
614 }
615
616 fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
617 match self.data.get(column) {
618 Some(v) if v == "NULL" => Ok(None),
619 Some(v) => match v.as_str() {
620 "true" | "t" | "1" => Ok(Some(true)),
621 "false" | "f" | "0" => Ok(Some(false)),
622 _ => Err(RowError::TypeConversion {
623 column: column.to_string(),
624 message: "invalid boolean".to_string(),
625 }),
626 },
627 None => Ok(None),
628 }
629 }
630
631 fn get_str(&self, column: &str) -> Result<&str, RowError> {
632 self.data
633 .get(column)
634 .map(|s| s.as_str())
635 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
636 }
637
638 fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
639 match self.data.get(column) {
640 Some(v) if v == "NULL" => Ok(None),
641 Some(v) => Ok(Some(v.as_str())),
642 None => Ok(None),
643 }
644 }
645
646 fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
647 self.data
648 .get(column)
649 .map(|s| s.as_bytes())
650 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
651 }
652
653 fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
654 match self.data.get(column) {
655 Some(v) if v == "NULL" => Ok(None),
656 Some(v) => Ok(Some(v.as_bytes())),
657 None => Ok(None),
658 }
659 }
660 }
661
662 #[test]
663 fn test_row_ref_get_i32() {
664 let mut data = std::collections::HashMap::new();
665 data.insert("id".to_string(), "42".to_string());
666 let row = MockRow { data };
667
668 assert_eq!(row.get_i32("id").unwrap(), 42);
669 }
670
671 #[test]
672 fn test_row_ref_get_str_zero_copy() {
673 let mut data = std::collections::HashMap::new();
674 data.insert("email".to_string(), "test@example.com".to_string());
675 let row = MockRow { data };
676
677 let email = row.get_str("email").unwrap();
678 assert_eq!(email, "test@example.com");
679 }
682
683 #[test]
684 fn test_row_data() {
685 let borrowed: RowData = RowData::borrowed("hello");
686 assert_eq!(borrowed.as_str(), "hello");
687
688 let owned: RowData = RowData::owned("world".to_string());
689 assert_eq!(owned.as_str(), "world");
690 }
691
692 struct ArrayRow(MockRow);
695
696 impl RowRef for ArrayRow {
697 fn get_i32(&self, c: &str) -> Result<i32, RowError> {
698 self.0.get_i32(c)
699 }
700 fn get_i32_opt(&self, c: &str) -> Result<Option<i32>, RowError> {
701 self.0.get_i32_opt(c)
702 }
703 fn get_i64(&self, c: &str) -> Result<i64, RowError> {
704 self.0.get_i64(c)
705 }
706 fn get_i64_opt(&self, c: &str) -> Result<Option<i64>, RowError> {
707 self.0.get_i64_opt(c)
708 }
709 fn get_f64(&self, c: &str) -> Result<f64, RowError> {
710 self.0.get_f64(c)
711 }
712 fn get_f64_opt(&self, c: &str) -> Result<Option<f64>, RowError> {
713 self.0.get_f64_opt(c)
714 }
715 fn get_bool(&self, c: &str) -> Result<bool, RowError> {
716 self.0.get_bool(c)
717 }
718 fn get_bool_opt(&self, c: &str) -> Result<Option<bool>, RowError> {
719 self.0.get_bool_opt(c)
720 }
721 fn get_str(&self, c: &str) -> Result<&str, RowError> {
722 self.0.get_str(c)
723 }
724 fn get_str_opt(&self, c: &str) -> Result<Option<&str>, RowError> {
725 self.0.get_str_opt(c)
726 }
727 fn get_bytes(&self, c: &str) -> Result<&[u8], RowError> {
728 self.0.get_bytes(c)
729 }
730 fn get_bytes_opt(&self, c: &str) -> Result<Option<&[u8]>, RowError> {
731 self.0.get_bytes_opt(c)
732 }
733 fn get_string_array(&self, c: &str) -> Result<Vec<String>, RowError> {
734 let raw = self.get_str(c)?;
735 Ok(if raw.is_empty() {
736 vec![]
737 } else {
738 raw.split(',').map(str::to_string).collect()
739 })
740 }
741 }
742
743 fn array_row(pairs: &[(&str, &str)]) -> ArrayRow {
744 let data = pairs
745 .iter()
746 .map(|(k, v)| (k.to_string(), v.to_string()))
747 .collect();
748 ArrayRow(MockRow { data })
749 }
750
751 #[test]
752 fn vec_string_decodes_through_get_string_array() {
753 let row = array_row(&[("scopes", "read,write"), ("none", "")]);
754 assert_eq!(
755 Vec::<String>::from_column(&row, "scopes").unwrap(),
756 vec!["read".to_string(), "write".to_string()]
757 );
758 assert!(Vec::<String>::from_column(&row, "none").unwrap().is_empty());
759 }
760
761 #[test]
762 fn option_vec_string_maps_null_to_none() {
763 let row = array_row(&[("scopes", "NULL"), ("tags", "a")]);
764 assert_eq!(
765 Option::<Vec<String>>::from_column(&row, "scopes").unwrap(),
766 None
767 );
768 assert_eq!(
769 Option::<Vec<String>>::from_column(&row, "tags").unwrap(),
770 Some(vec!["a".to_string()])
771 );
772 }
773
774 #[test]
775 fn default_string_array_method_errors() {
776 let mut data = std::collections::HashMap::new();
777 data.insert("scopes".into(), "read".into());
778 let row = MockRow { data };
779 assert!(matches!(
780 Vec::<String>::from_column(&row, "scopes"),
781 Err(RowError::TypeConversion { message, .. }) if message.contains("string_array")
782 ));
783 }
784
785 #[test]
786 fn default_datetime_method_errors() {
787 let mut data = std::collections::HashMap::new();
788 data.insert("created_at".into(), "2026-04-27T00:00:00Z".into());
789 let row = MockRow { data };
790 assert!(matches!(
791 row.get_datetime_utc("created_at"),
792 Err(RowError::TypeConversion { .. })
793 ));
794 }
795}