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 is_null(&self, column: &str) -> Result<bool, RowError> {
200 self.get_str_opt(column).map(|opt| opt.is_none())
201 }
202}
203
204fn unsupported_get(column: &str, getter: &str) -> RowError {
209 RowError::TypeConversion {
210 column: column.to_string(),
211 message: format!("{getter} not supported by this row type"),
212 }
213}
214
215pub fn into_row_error<T, E: std::fmt::Display>(
220 column: &str,
221 res: Result<T, E>,
222) -> Result<T, RowError> {
223 res.map_err(|e| RowError::TypeConversion {
224 column: column.to_string(),
225 message: e.to_string(),
226 })
227}
228
229pub trait FromRowRef<'a>: Sized {
234 fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
236}
237
238pub trait FromRow: Sized {
243 fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
245}
246
247impl<T: FromRow> FromRowRef<'_> for T {
249 fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
250 T::from_row(row)
251 }
252}
253
254pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
256 rows: std::slice::Iter<'a, R>,
257 _marker: std::marker::PhantomData<T>,
258}
259
260impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
261 pub fn new(rows: &'a [R]) -> Self {
263 Self {
264 rows: rows.iter(),
265 _marker: std::marker::PhantomData,
266 }
267 }
268}
269
270impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
271 type Item = Result<T, RowError>;
272
273 fn next(&mut self) -> Option<Self::Item> {
274 self.rows.next().map(|row| T::from_row_ref(row))
275 }
276
277 fn size_hint(&self) -> (usize, Option<usize>) {
278 self.rows.size_hint()
279 }
280}
281
282impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
283
284#[derive(Debug, Clone)]
289pub enum RowData<'a> {
290 Borrowed(&'a str),
292 Owned(String),
294}
295
296impl<'a> RowData<'a> {
297 pub fn as_str(&self) -> &str {
299 match self {
300 Self::Borrowed(s) => s,
301 Self::Owned(s) => s,
302 }
303 }
304
305 pub fn into_owned(self) -> String {
307 match self {
308 Self::Borrowed(s) => s.to_string(),
309 Self::Owned(s) => s,
310 }
311 }
312
313 pub const fn borrowed(s: &'a str) -> Self {
315 Self::Borrowed(s)
316 }
317
318 pub fn owned(s: impl Into<String>) -> Self {
320 Self::Owned(s.into())
321 }
322}
323
324impl<'a> From<&'a str> for RowData<'a> {
325 fn from(s: &'a str) -> Self {
326 Self::Borrowed(s)
327 }
328}
329
330impl From<String> for RowData<'static> {
331 fn from(s: String) -> Self {
332 Self::Owned(s)
333 }
334}
335
336impl<'a> AsRef<str> for RowData<'a> {
337 fn as_ref(&self) -> &str {
338 self.as_str()
339 }
340}
341
342#[macro_export]
364macro_rules! impl_from_row {
365 ($type:ident { $($field:ident : i32),* $(,)? }) => {
366 impl $crate::row::FromRow for $type {
367 fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
368 Ok(Self {
369 $(
370 $field: row.get_i32(stringify!($field))?,
371 )*
372 })
373 }
374 }
375 };
376 ($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
377 impl $crate::row::FromRow for $type {
378 fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
379 Ok(Self {
380 $(
381 $field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
382 )*
383 })
384 }
385 }
386 };
387}
388
389#[doc(hidden)]
391pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
392 T::from_column(row, column)
393}
394
395pub trait FromColumn: Sized {
397 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
399}
400
401impl FromColumn for i32 {
402 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
403 row.get_i32(column)
404 }
405}
406
407impl FromColumn for i64 {
408 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
409 row.get_i64(column)
410 }
411}
412
413impl FromColumn for f64 {
414 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
415 row.get_f64(column)
416 }
417}
418
419impl FromColumn for bool {
420 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
421 row.get_bool(column)
422 }
423}
424
425impl FromColumn for String {
426 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
427 row.get_string(column)
428 }
429}
430
431impl FromColumn for Vec<u8> {
432 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
433 row.get_bytes(column).map(|b| b.to_vec())
434 }
435}
436
437impl FromColumn for chrono::DateTime<chrono::Utc> {
438 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
439 row.get_datetime_utc(column)
440 }
441}
442impl FromColumn for chrono::NaiveDateTime {
443 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
444 row.get_naive_datetime(column)
445 }
446}
447impl FromColumn for chrono::NaiveDate {
448 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
449 row.get_naive_date(column)
450 }
451}
452impl FromColumn for chrono::NaiveTime {
453 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
454 row.get_naive_time(column)
455 }
456}
457impl FromColumn for uuid::Uuid {
458 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
459 row.get_uuid(column)
460 }
461}
462impl FromColumn for serde_json::Value {
463 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
464 row.get_json(column)
465 }
466}
467impl FromColumn for rust_decimal::Decimal {
468 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
469 row.get_decimal(column)
470 }
471}
472
473impl FromColumn for Vec<f32> {
479 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
480 row.get_vector(column)
481 }
482}
483
484impl<T: FromColumn> FromColumn for Option<T> {
497 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
498 if row.is_null(column)? {
499 Ok(None)
500 } else {
501 T::from_column(row, column).map(Some)
502 }
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 struct MockRow {
512 data: std::collections::HashMap<String, String>,
513 }
514
515 impl RowRef for MockRow {
516 fn get_i32(&self, column: &str) -> Result<i32, RowError> {
517 self.data
518 .get(column)
519 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
520 .parse()
521 .map_err(|e| RowError::TypeConversion {
522 column: column.to_string(),
523 message: format!("{}", e),
524 })
525 }
526
527 fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
528 match self.data.get(column) {
529 Some(v) if v == "NULL" => Ok(None),
530 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
531 column: column.to_string(),
532 message: format!("{}", e),
533 }),
534 None => Ok(None),
535 }
536 }
537
538 fn get_i64(&self, column: &str) -> Result<i64, RowError> {
539 self.data
540 .get(column)
541 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
542 .parse()
543 .map_err(|e| RowError::TypeConversion {
544 column: column.to_string(),
545 message: format!("{}", e),
546 })
547 }
548
549 fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
550 match self.data.get(column) {
551 Some(v) if v == "NULL" => Ok(None),
552 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
553 column: column.to_string(),
554 message: format!("{}", e),
555 }),
556 None => Ok(None),
557 }
558 }
559
560 fn get_f64(&self, column: &str) -> Result<f64, RowError> {
561 self.data
562 .get(column)
563 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
564 .parse()
565 .map_err(|e| RowError::TypeConversion {
566 column: column.to_string(),
567 message: format!("{}", e),
568 })
569 }
570
571 fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
572 match self.data.get(column) {
573 Some(v) if v == "NULL" => Ok(None),
574 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
575 column: column.to_string(),
576 message: format!("{}", e),
577 }),
578 None => Ok(None),
579 }
580 }
581
582 fn get_bool(&self, column: &str) -> Result<bool, RowError> {
583 let v = self
584 .data
585 .get(column)
586 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
587 match v.as_str() {
588 "true" | "t" | "1" => Ok(true),
589 "false" | "f" | "0" => Ok(false),
590 _ => Err(RowError::TypeConversion {
591 column: column.to_string(),
592 message: "invalid boolean".to_string(),
593 }),
594 }
595 }
596
597 fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
598 match self.data.get(column) {
599 Some(v) if v == "NULL" => Ok(None),
600 Some(v) => match v.as_str() {
601 "true" | "t" | "1" => Ok(Some(true)),
602 "false" | "f" | "0" => Ok(Some(false)),
603 _ => Err(RowError::TypeConversion {
604 column: column.to_string(),
605 message: "invalid boolean".to_string(),
606 }),
607 },
608 None => Ok(None),
609 }
610 }
611
612 fn get_str(&self, column: &str) -> Result<&str, RowError> {
613 self.data
614 .get(column)
615 .map(|s| s.as_str())
616 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
617 }
618
619 fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
620 match self.data.get(column) {
621 Some(v) if v == "NULL" => Ok(None),
622 Some(v) => Ok(Some(v.as_str())),
623 None => Ok(None),
624 }
625 }
626
627 fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
628 self.data
629 .get(column)
630 .map(|s| s.as_bytes())
631 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
632 }
633
634 fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
635 match self.data.get(column) {
636 Some(v) if v == "NULL" => Ok(None),
637 Some(v) => Ok(Some(v.as_bytes())),
638 None => Ok(None),
639 }
640 }
641 }
642
643 #[test]
644 fn test_row_ref_get_i32() {
645 let mut data = std::collections::HashMap::new();
646 data.insert("id".to_string(), "42".to_string());
647 let row = MockRow { data };
648
649 assert_eq!(row.get_i32("id").unwrap(), 42);
650 }
651
652 #[test]
653 fn test_row_ref_get_str_zero_copy() {
654 let mut data = std::collections::HashMap::new();
655 data.insert("email".to_string(), "test@example.com".to_string());
656 let row = MockRow { data };
657
658 let email = row.get_str("email").unwrap();
659 assert_eq!(email, "test@example.com");
660 }
663
664 #[test]
665 fn test_row_data() {
666 let borrowed: RowData = RowData::borrowed("hello");
667 assert_eq!(borrowed.as_str(), "hello");
668
669 let owned: RowData = RowData::owned("world".to_string());
670 assert_eq!(owned.as_str(), "world");
671 }
672
673 #[test]
674 fn default_datetime_method_errors() {
675 let mut data = std::collections::HashMap::new();
676 data.insert("created_at".into(), "2026-04-27T00:00:00Z".into());
677 let row = MockRow { data };
678 assert!(matches!(
679 row.get_datetime_utc("created_at"),
680 Err(RowError::TypeConversion { .. })
681 ));
682 }
683}