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)
}
}
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 Option<i32> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_i32_opt(column)
}
}
impl FromColumn for Option<i64> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_i64_opt(column)
}
}
impl FromColumn for Option<f64> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_f64_opt(column)
}
}
impl FromColumn for Option<bool> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_bool_opt(column)
}
}
impl FromColumn for Option<String> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_string_opt(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 Option<Vec<u8>> {
fn from_column(row: &impl RowRef, column: &str) -> Result<Self, RowError> {
row.get_bytes_opt(column).map(|opt| opt.map(|b| b.to_vec()))
}
}
#[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");
}
}