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
176fn unsupported_get(column: &str, getter: &str) -> RowError {
181 RowError::TypeConversion {
182 column: column.to_string(),
183 message: format!("{getter} not supported by this row type"),
184 }
185}
186
187pub fn into_row_error<T, E: std::fmt::Display>(
192 column: &str,
193 res: Result<T, E>,
194) -> Result<T, RowError> {
195 res.map_err(|e| RowError::TypeConversion {
196 column: column.to_string(),
197 message: e.to_string(),
198 })
199}
200
201pub trait FromRowRef<'a>: Sized {
206 fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
208}
209
210pub trait FromRow: Sized {
215 fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
217}
218
219impl<T: FromRow> FromRowRef<'_> for T {
221 fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
222 T::from_row(row)
223 }
224}
225
226pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
228 rows: std::slice::Iter<'a, R>,
229 _marker: std::marker::PhantomData<T>,
230}
231
232impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
233 pub fn new(rows: &'a [R]) -> Self {
235 Self {
236 rows: rows.iter(),
237 _marker: std::marker::PhantomData,
238 }
239 }
240}
241
242impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
243 type Item = Result<T, RowError>;
244
245 fn next(&mut self) -> Option<Self::Item> {
246 self.rows.next().map(|row| T::from_row_ref(row))
247 }
248
249 fn size_hint(&self) -> (usize, Option<usize>) {
250 self.rows.size_hint()
251 }
252}
253
254impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
255
256#[derive(Debug, Clone)]
261pub enum RowData<'a> {
262 Borrowed(&'a str),
264 Owned(String),
266}
267
268impl<'a> RowData<'a> {
269 pub fn as_str(&self) -> &str {
271 match self {
272 Self::Borrowed(s) => s,
273 Self::Owned(s) => s,
274 }
275 }
276
277 pub fn into_owned(self) -> String {
279 match self {
280 Self::Borrowed(s) => s.to_string(),
281 Self::Owned(s) => s,
282 }
283 }
284
285 pub const fn borrowed(s: &'a str) -> Self {
287 Self::Borrowed(s)
288 }
289
290 pub fn owned(s: impl Into<String>) -> Self {
292 Self::Owned(s.into())
293 }
294}
295
296impl<'a> From<&'a str> for RowData<'a> {
297 fn from(s: &'a str) -> Self {
298 Self::Borrowed(s)
299 }
300}
301
302impl From<String> for RowData<'static> {
303 fn from(s: String) -> Self {
304 Self::Owned(s)
305 }
306}
307
308impl<'a> AsRef<str> for RowData<'a> {
309 fn as_ref(&self) -> &str {
310 self.as_str()
311 }
312}
313
314#[macro_export]
336macro_rules! impl_from_row {
337 ($type:ident { $($field:ident : i32),* $(,)? }) => {
338 impl $crate::row::FromRow for $type {
339 fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
340 Ok(Self {
341 $(
342 $field: row.get_i32(stringify!($field))?,
343 )*
344 })
345 }
346 }
347 };
348 ($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
349 impl $crate::row::FromRow for $type {
350 fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
351 Ok(Self {
352 $(
353 $field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
354 )*
355 })
356 }
357 }
358 };
359}
360
361#[doc(hidden)]
363pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
364 T::from_column(row, column)
365}
366
367pub trait FromColumn: Sized {
369 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
371}
372
373impl FromColumn for i32 {
374 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
375 row.get_i32(column)
376 }
377}
378
379impl FromColumn for i64 {
380 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
381 row.get_i64(column)
382 }
383}
384
385impl FromColumn for f64 {
386 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
387 row.get_f64(column)
388 }
389}
390
391impl FromColumn for bool {
392 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
393 row.get_bool(column)
394 }
395}
396
397impl FromColumn for String {
398 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
399 row.get_string(column)
400 }
401}
402
403impl FromColumn for Option<i32> {
404 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
405 row.get_i32_opt(column)
406 }
407}
408
409impl FromColumn for Option<i64> {
410 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
411 row.get_i64_opt(column)
412 }
413}
414
415impl FromColumn for Option<f64> {
416 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
417 row.get_f64_opt(column)
418 }
419}
420
421impl FromColumn for Option<bool> {
422 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
423 row.get_bool_opt(column)
424 }
425}
426
427impl FromColumn for Option<String> {
428 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
429 row.get_string_opt(column)
430 }
431}
432
433impl FromColumn for Vec<u8> {
434 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
435 row.get_bytes(column).map(|b| b.to_vec())
436 }
437}
438
439impl FromColumn for Option<Vec<u8>> {
440 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
441 row.get_bytes_opt(column).map(|opt| opt.map(|b| b.to_vec()))
442 }
443}
444
445impl FromColumn for chrono::DateTime<chrono::Utc> {
446 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
447 row.get_datetime_utc(column)
448 }
449}
450impl FromColumn for Option<chrono::DateTime<chrono::Utc>> {
451 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
452 row.get_datetime_utc_opt(column)
453 }
454}
455impl FromColumn for chrono::NaiveDateTime {
456 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
457 row.get_naive_datetime(column)
458 }
459}
460impl FromColumn for Option<chrono::NaiveDateTime> {
461 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
462 row.get_naive_datetime_opt(column)
463 }
464}
465impl FromColumn for chrono::NaiveDate {
466 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
467 row.get_naive_date(column)
468 }
469}
470impl FromColumn for Option<chrono::NaiveDate> {
471 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
472 row.get_naive_date_opt(column)
473 }
474}
475impl FromColumn for chrono::NaiveTime {
476 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
477 row.get_naive_time(column)
478 }
479}
480impl FromColumn for Option<chrono::NaiveTime> {
481 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
482 row.get_naive_time_opt(column)
483 }
484}
485impl FromColumn for uuid::Uuid {
486 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
487 row.get_uuid(column)
488 }
489}
490impl FromColumn for Option<uuid::Uuid> {
491 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
492 row.get_uuid_opt(column)
493 }
494}
495impl FromColumn for serde_json::Value {
496 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
497 row.get_json(column)
498 }
499}
500impl FromColumn for Option<serde_json::Value> {
501 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
502 row.get_json_opt(column)
503 }
504}
505impl FromColumn for rust_decimal::Decimal {
506 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
507 row.get_decimal(column)
508 }
509}
510impl FromColumn for Option<rust_decimal::Decimal> {
511 fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
512 row.get_decimal_opt(column)
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519
520 struct MockRow {
522 data: std::collections::HashMap<String, String>,
523 }
524
525 impl RowRef for MockRow {
526 fn get_i32(&self, column: &str) -> Result<i32, RowError> {
527 self.data
528 .get(column)
529 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
530 .parse()
531 .map_err(|e| RowError::TypeConversion {
532 column: column.to_string(),
533 message: format!("{}", e),
534 })
535 }
536
537 fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
538 match self.data.get(column) {
539 Some(v) if v == "NULL" => Ok(None),
540 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
541 column: column.to_string(),
542 message: format!("{}", e),
543 }),
544 None => Ok(None),
545 }
546 }
547
548 fn get_i64(&self, column: &str) -> Result<i64, RowError> {
549 self.data
550 .get(column)
551 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
552 .parse()
553 .map_err(|e| RowError::TypeConversion {
554 column: column.to_string(),
555 message: format!("{}", e),
556 })
557 }
558
559 fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
560 match self.data.get(column) {
561 Some(v) if v == "NULL" => Ok(None),
562 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
563 column: column.to_string(),
564 message: format!("{}", e),
565 }),
566 None => Ok(None),
567 }
568 }
569
570 fn get_f64(&self, column: &str) -> Result<f64, RowError> {
571 self.data
572 .get(column)
573 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
574 .parse()
575 .map_err(|e| RowError::TypeConversion {
576 column: column.to_string(),
577 message: format!("{}", e),
578 })
579 }
580
581 fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
582 match self.data.get(column) {
583 Some(v) if v == "NULL" => Ok(None),
584 Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
585 column: column.to_string(),
586 message: format!("{}", e),
587 }),
588 None => Ok(None),
589 }
590 }
591
592 fn get_bool(&self, column: &str) -> Result<bool, RowError> {
593 let v = self
594 .data
595 .get(column)
596 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
597 match v.as_str() {
598 "true" | "t" | "1" => Ok(true),
599 "false" | "f" | "0" => Ok(false),
600 _ => Err(RowError::TypeConversion {
601 column: column.to_string(),
602 message: "invalid boolean".to_string(),
603 }),
604 }
605 }
606
607 fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
608 match self.data.get(column) {
609 Some(v) if v == "NULL" => Ok(None),
610 Some(v) => match v.as_str() {
611 "true" | "t" | "1" => Ok(Some(true)),
612 "false" | "f" | "0" => Ok(Some(false)),
613 _ => Err(RowError::TypeConversion {
614 column: column.to_string(),
615 message: "invalid boolean".to_string(),
616 }),
617 },
618 None => Ok(None),
619 }
620 }
621
622 fn get_str(&self, column: &str) -> Result<&str, RowError> {
623 self.data
624 .get(column)
625 .map(|s| s.as_str())
626 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
627 }
628
629 fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
630 match self.data.get(column) {
631 Some(v) if v == "NULL" => Ok(None),
632 Some(v) => Ok(Some(v.as_str())),
633 None => Ok(None),
634 }
635 }
636
637 fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
638 self.data
639 .get(column)
640 .map(|s| s.as_bytes())
641 .ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
642 }
643
644 fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
645 match self.data.get(column) {
646 Some(v) if v == "NULL" => Ok(None),
647 Some(v) => Ok(Some(v.as_bytes())),
648 None => Ok(None),
649 }
650 }
651 }
652
653 #[test]
654 fn test_row_ref_get_i32() {
655 let mut data = std::collections::HashMap::new();
656 data.insert("id".to_string(), "42".to_string());
657 let row = MockRow { data };
658
659 assert_eq!(row.get_i32("id").unwrap(), 42);
660 }
661
662 #[test]
663 fn test_row_ref_get_str_zero_copy() {
664 let mut data = std::collections::HashMap::new();
665 data.insert("email".to_string(), "test@example.com".to_string());
666 let row = MockRow { data };
667
668 let email = row.get_str("email").unwrap();
669 assert_eq!(email, "test@example.com");
670 }
673
674 #[test]
675 fn test_row_data() {
676 let borrowed: RowData = RowData::borrowed("hello");
677 assert_eq!(borrowed.as_str(), "hello");
678
679 let owned: RowData = RowData::owned("world".to_string());
680 assert_eq!(owned.as_str(), "world");
681 }
682
683 #[test]
684 fn default_datetime_method_errors() {
685 let mut data = std::collections::HashMap::new();
686 data.insert("created_at".into(), "2026-04-27T00:00:00Z".into());
687 let row = MockRow { data };
688 assert!(matches!(
689 row.get_datetime_utc("created_at"),
690 Err(RowError::TypeConversion { .. })
691 ));
692 }
693}