use std::borrow::Cow;
use std::fmt;
#[derive(Debug, Clone)]
pub enum RowError {
ColumnNotFound(String),
TypeConversion { column: String, message: String },
UnexpectedNull(String),
}
impl fmt::Display for RowError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ColumnNotFound(col) => write!(f, "column '{}' not found", col),
Self::TypeConversion { column, message } => {
write!(f, "type conversion error for '{}': {}", column, message)
}
Self::UnexpectedNull(col) => write!(f, "unexpected null in column '{}'", col),
}
}
}
impl std::error::Error for RowError {}
pub trait RowRef {
fn get_i32(&self, column: &str) -> Result<i32, RowError>;
fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError>;
fn get_i64(&self, column: &str) -> Result<i64, RowError>;
fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError>;
fn get_f64(&self, column: &str) -> Result<f64, RowError>;
fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError>;
fn get_bool(&self, column: &str) -> Result<bool, RowError>;
fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError>;
fn get_str(&self, column: &str) -> Result<&str, RowError>;
fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError>;
fn get_string(&self, column: &str) -> Result<String, RowError> {
self.get_str(column).map(|s| s.to_string())
}
fn get_string_opt(&self, column: &str) -> Result<Option<String>, RowError> {
self.get_str_opt(column)
.map(|opt| opt.map(|s| s.to_string()))
}
fn get_bytes(&self, column: &str) -> Result<&[u8], RowError>;
fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError>;
fn get_cow_str(&self, column: &str) -> Result<Cow<'_, str>, RowError> {
self.get_str(column).map(Cow::Borrowed)
}
fn get_datetime_utc(&self, column: &str) -> Result<chrono::DateTime<chrono::Utc>, RowError> {
Err(unsupported_get(column, "datetime_utc"))
}
fn get_datetime_utc_opt(
&self,
column: &str,
) -> Result<Option<chrono::DateTime<chrono::Utc>>, RowError> {
Err(unsupported_get(column, "datetime_utc_opt"))
}
fn get_naive_datetime(&self, column: &str) -> Result<chrono::NaiveDateTime, RowError> {
Err(unsupported_get(column, "naive_datetime"))
}
fn get_naive_datetime_opt(
&self,
column: &str,
) -> Result<Option<chrono::NaiveDateTime>, RowError> {
Err(unsupported_get(column, "naive_datetime_opt"))
}
fn get_naive_date(&self, column: &str) -> Result<chrono::NaiveDate, RowError> {
Err(unsupported_get(column, "naive_date"))
}
fn get_naive_date_opt(&self, column: &str) -> Result<Option<chrono::NaiveDate>, RowError> {
Err(unsupported_get(column, "naive_date_opt"))
}
fn get_naive_time(&self, column: &str) -> Result<chrono::NaiveTime, RowError> {
Err(unsupported_get(column, "naive_time"))
}
fn get_naive_time_opt(&self, column: &str) -> Result<Option<chrono::NaiveTime>, RowError> {
Err(unsupported_get(column, "naive_time_opt"))
}
fn get_uuid(&self, column: &str) -> Result<uuid::Uuid, RowError> {
Err(unsupported_get(column, "uuid"))
}
fn get_uuid_opt(&self, column: &str) -> Result<Option<uuid::Uuid>, RowError> {
Err(unsupported_get(column, "uuid_opt"))
}
fn get_json(&self, column: &str) -> Result<serde_json::Value, RowError> {
Err(unsupported_get(column, "json"))
}
fn get_json_opt(&self, column: &str) -> Result<Option<serde_json::Value>, RowError> {
Err(unsupported_get(column, "json_opt"))
}
fn get_decimal(&self, column: &str) -> Result<rust_decimal::Decimal, RowError> {
Err(unsupported_get(column, "decimal"))
}
fn get_decimal_opt(&self, column: &str) -> Result<Option<rust_decimal::Decimal>, RowError> {
Err(unsupported_get(column, "decimal_opt"))
}
fn get_vector(&self, column: &str) -> Result<Vec<f32>, RowError> {
Err(unsupported_get(column, "vector"))
}
fn is_null(&self, column: &str) -> Result<bool, RowError> {
self.get_str_opt(column).map(|opt| opt.is_none())
}
}
fn unsupported_get(column: &str, getter: &str) -> RowError {
RowError::TypeConversion {
column: column.to_string(),
message: format!("{getter} not supported by this row type"),
}
}
pub fn into_row_error<T, E: std::fmt::Display>(
column: &str,
res: Result<T, E>,
) -> Result<T, RowError> {
res.map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: e.to_string(),
})
}
pub trait FromRowRef<'a>: Sized {
fn from_row_ref(row: &'a impl RowRef) -> Result<Self, RowError>;
}
pub trait FromRow: Sized {
fn from_row(row: &impl RowRef) -> Result<Self, RowError>;
}
impl<T: FromRow> FromRowRef<'_> for T {
fn from_row_ref(row: &impl RowRef) -> Result<Self, RowError> {
T::from_row(row)
}
}
pub struct RowRefIter<'a, R: RowRef, T: FromRowRef<'a>> {
rows: std::slice::Iter<'a, R>,
_marker: std::marker::PhantomData<T>,
}
impl<'a, R: RowRef, T: FromRowRef<'a>> RowRefIter<'a, R, T> {
pub fn new(rows: &'a [R]) -> Self {
Self {
rows: rows.iter(),
_marker: std::marker::PhantomData,
}
}
}
impl<'a, R: RowRef, T: FromRowRef<'a>> Iterator for RowRefIter<'a, R, T> {
type Item = Result<T, RowError>;
fn next(&mut self) -> Option<Self::Item> {
self.rows.next().map(|row| T::from_row_ref(row))
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.rows.size_hint()
}
}
impl<'a, R: RowRef, T: FromRowRef<'a>> ExactSizeIterator for RowRefIter<'a, R, T> {}
#[derive(Debug, Clone)]
pub enum RowData<'a> {
Borrowed(&'a str),
Owned(String),
}
impl<'a> RowData<'a> {
pub fn as_str(&self) -> &str {
match self {
Self::Borrowed(s) => s,
Self::Owned(s) => s,
}
}
pub fn into_owned(self) -> String {
match self {
Self::Borrowed(s) => s.to_string(),
Self::Owned(s) => s,
}
}
pub const fn borrowed(s: &'a str) -> Self {
Self::Borrowed(s)
}
pub fn owned(s: impl Into<String>) -> Self {
Self::Owned(s.into())
}
}
impl<'a> From<&'a str> for RowData<'a> {
fn from(s: &'a str) -> Self {
Self::Borrowed(s)
}
}
impl From<String> for RowData<'static> {
fn from(s: String) -> Self {
Self::Owned(s)
}
}
impl<'a> AsRef<str> for RowData<'a> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[macro_export]
macro_rules! impl_from_row {
($type:ident { $($field:ident : i32),* $(,)? }) => {
impl $crate::row::FromRow for $type {
fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
Ok(Self {
$(
$field: row.get_i32(stringify!($field))?,
)*
})
}
}
};
($type:ident { $($field:ident : $field_type:ty),* $(,)? }) => {
impl $crate::row::FromRow for $type {
fn from_row(row: &impl $crate::row::RowRef) -> Result<Self, $crate::row::RowError> {
Ok(Self {
$(
$field: $crate::row::_get_typed_value::<$field_type>(row, stringify!($field))?,
)*
})
}
}
};
}
#[doc(hidden)]
pub fn _get_typed_value<T: FromColumn>(row: &impl RowRef, column: &str) -> Result<T, RowError> {
T::from_column(row, column)
}
pub trait FromColumn: Sized {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError>;
}
impl FromColumn for i32 {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_i32(column)
}
}
impl FromColumn for i64 {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_i64(column)
}
}
impl FromColumn for f64 {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_f64(column)
}
}
impl FromColumn for bool {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_bool(column)
}
}
impl FromColumn for String {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_string(column)
}
}
impl FromColumn for Vec<u8> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_bytes(column).map(|b| b.to_vec())
}
}
impl FromColumn for chrono::DateTime<chrono::Utc> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_datetime_utc(column)
}
}
impl FromColumn for chrono::NaiveDateTime {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_naive_datetime(column)
}
}
impl FromColumn for chrono::NaiveDate {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_naive_date(column)
}
}
impl FromColumn for chrono::NaiveTime {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_naive_time(column)
}
}
impl FromColumn for uuid::Uuid {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_uuid(column)
}
}
impl FromColumn for serde_json::Value {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_json(column)
}
}
impl FromColumn for rust_decimal::Decimal {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_decimal(column)
}
}
impl FromColumn for Vec<f32> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_vector(column)
}
}
impl<T: FromColumn> FromColumn for Option<T> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
if row.is_null(column)? {
Ok(None)
} else {
T::from_column(row, column).map(Some)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockRow {
data: std::collections::HashMap<String, String>,
}
impl RowRef for MockRow {
fn get_i32(&self, column: &str) -> Result<i32, RowError> {
self.data
.get(column)
.ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
.parse()
.map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: format!("{}", e),
})
}
fn get_i32_opt(&self, column: &str) -> Result<Option<i32>, RowError> {
match self.data.get(column) {
Some(v) if v == "NULL" => Ok(None),
Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: format!("{}", e),
}),
None => Ok(None),
}
}
fn get_i64(&self, column: &str) -> Result<i64, RowError> {
self.data
.get(column)
.ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
.parse()
.map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: format!("{}", e),
})
}
fn get_i64_opt(&self, column: &str) -> Result<Option<i64>, RowError> {
match self.data.get(column) {
Some(v) if v == "NULL" => Ok(None),
Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: format!("{}", e),
}),
None => Ok(None),
}
}
fn get_f64(&self, column: &str) -> Result<f64, RowError> {
self.data
.get(column)
.ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?
.parse()
.map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: format!("{}", e),
})
}
fn get_f64_opt(&self, column: &str) -> Result<Option<f64>, RowError> {
match self.data.get(column) {
Some(v) if v == "NULL" => Ok(None),
Some(v) => v.parse().map(Some).map_err(|e| RowError::TypeConversion {
column: column.to_string(),
message: format!("{}", e),
}),
None => Ok(None),
}
}
fn get_bool(&self, column: &str) -> Result<bool, RowError> {
let v = self
.data
.get(column)
.ok_or_else(|| RowError::ColumnNotFound(column.to_string()))?;
match v.as_str() {
"true" | "t" | "1" => Ok(true),
"false" | "f" | "0" => Ok(false),
_ => Err(RowError::TypeConversion {
column: column.to_string(),
message: "invalid boolean".to_string(),
}),
}
}
fn get_bool_opt(&self, column: &str) -> Result<Option<bool>, RowError> {
match self.data.get(column) {
Some(v) if v == "NULL" => Ok(None),
Some(v) => match v.as_str() {
"true" | "t" | "1" => Ok(Some(true)),
"false" | "f" | "0" => Ok(Some(false)),
_ => Err(RowError::TypeConversion {
column: column.to_string(),
message: "invalid boolean".to_string(),
}),
},
None => Ok(None),
}
}
fn get_str(&self, column: &str) -> Result<&str, RowError> {
self.data
.get(column)
.map(|s| s.as_str())
.ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
}
fn get_str_opt(&self, column: &str) -> Result<Option<&str>, RowError> {
match self.data.get(column) {
Some(v) if v == "NULL" => Ok(None),
Some(v) => Ok(Some(v.as_str())),
None => Ok(None),
}
}
fn get_bytes(&self, column: &str) -> Result<&[u8], RowError> {
self.data
.get(column)
.map(|s| s.as_bytes())
.ok_or_else(|| RowError::ColumnNotFound(column.to_string()))
}
fn get_bytes_opt(&self, column: &str) -> Result<Option<&[u8]>, RowError> {
match self.data.get(column) {
Some(v) if v == "NULL" => Ok(None),
Some(v) => Ok(Some(v.as_bytes())),
None => Ok(None),
}
}
}
#[test]
fn test_row_ref_get_i32() {
let mut data = std::collections::HashMap::new();
data.insert("id".to_string(), "42".to_string());
let row = MockRow { data };
assert_eq!(row.get_i32("id").unwrap(), 42);
}
#[test]
fn test_row_ref_get_str_zero_copy() {
let mut data = std::collections::HashMap::new();
data.insert("email".to_string(), "test@example.com".to_string());
let row = MockRow { data };
let email = row.get_str("email").unwrap();
assert_eq!(email, "test@example.com");
}
#[test]
fn test_row_data() {
let borrowed: RowData = RowData::borrowed("hello");
assert_eq!(borrowed.as_str(), "hello");
let owned: RowData = RowData::owned("world".to_string());
assert_eq!(owned.as_str(), "world");
}
#[test]
fn default_datetime_method_errors() {
let mut data = std::collections::HashMap::new();
data.insert("created_at".into(), "2026-04-27T00:00:00Z".into());
let row = MockRow { data };
assert!(matches!(
row.get_datetime_utc("created_at"),
Err(RowError::TypeConversion { .. })
));
}
}