use crate::value::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum QueryError {
ColumnCountMismatch {
expected: usize,
actual: usize,
},
TypeMismatch {
column: std::borrow::Cow<'static, str>,
expected: &'static str,
},
MissingColumn {
column: &'static str,
},
Custom(String),
}
impl std::fmt::Display for QueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QueryError::ColumnCountMismatch { expected, actual } => {
write!(f, "列数不匹配: 期望 {}, 实际 {}", expected, actual)
}
QueryError::TypeMismatch { column, expected } => {
write!(f, "列 {:?} 类型不匹配, 期望 {}", column, expected)
}
QueryError::MissingColumn { column } => {
write!(f, "缺少列: {}", column)
}
QueryError::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for QueryError {}
#[derive(Debug, Clone)]
pub struct RowDesc {
pub columns: Vec<String>,
}
impl RowDesc {
pub fn new(columns: Vec<String>) -> Self {
Self { columns }
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
pub fn index_of(&self, name: &str) -> Option<usize> {
self.columns.iter().position(|c| c == name)
}
}
pub trait Queryable: Sized {
fn from_values(values: Vec<Value>) -> Result<Self, QueryError>;
fn from_values_with_desc(values: Vec<Value>, desc: &RowDesc) -> Result<Self, QueryError> {
if values.len() != desc.len() {
return Err(QueryError::ColumnCountMismatch {
expected: desc.len(),
actual: values.len(),
});
}
Self::from_values(values)
}
}
pub trait FromRow: Sized {
fn from_row(row: HashMap<String, Value>) -> Result<Self, QueryError>;
}
impl Queryable for Value {
fn from_values(values: Vec<Value>) -> Result<Self, QueryError> {
if values.len() != 1 {
return Err(QueryError::ColumnCountMismatch {
expected: 1,
actual: values.len(),
});
}
Ok(values.into_iter().next().unwrap())
}
}
impl Queryable for (Value, Value) {
fn from_values(values: Vec<Value>) -> Result<Self, QueryError> {
if values.len() != 2 {
return Err(QueryError::ColumnCountMismatch {
expected: 2,
actual: values.len(),
});
}
let mut iter = values.into_iter();
Ok((iter.next().unwrap(), iter.next().unwrap()))
}
}
impl Queryable for (Value, Value, Value) {
fn from_values(values: Vec<Value>) -> Result<Self, QueryError> {
if values.len() != 3 {
return Err(QueryError::ColumnCountMismatch {
expected: 3,
actual: values.len(),
});
}
let mut iter = values.into_iter();
Ok((
iter.next().unwrap(),
iter.next().unwrap(),
iter.next().unwrap(),
))
}
}
pub fn value_as_i64(v: &Value) -> Option<i64> {
v.as_i64()
}
pub fn value_as_f64(v: &Value) -> Option<f64> {
v.as_f64()
}
pub fn value_as_string(v: &Value) -> Option<String> {
v.as_str().map(|s| s.to_string())
}
pub fn value_as_bool(v: &Value) -> Option<bool> {
v.as_bool()
}
pub fn value_as_nullable_i64(v: &Value) -> Option<i64> {
if v.is_null() {
None
} else {
v.as_i64()
}
}
pub fn value_as_nullable_string(v: &Value) -> Option<String> {
if v.is_null() {
None
} else {
v.as_str().map(|s| s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default, PartialEq)]
struct UserRow {
id: i64,
name: String,
}
impl Queryable for UserRow {
fn from_values(values: Vec<Value>) -> Result<Self, QueryError> {
if values.len() != 2 {
return Err(QueryError::ColumnCountMismatch {
expected: 2,
actual: values.len(),
});
}
let id = values[0].as_i64().ok_or(QueryError::TypeMismatch {
column: "0".into(),
expected: "i64",
})?;
let name = values[1]
.as_str()
.ok_or(QueryError::TypeMismatch {
column: "1".into(),
expected: "String",
})?
.to_string();
Ok(UserRow { id, name })
}
}
impl FromRow for UserRow {
fn from_row(row: HashMap<String, Value>) -> Result<Self, QueryError> {
let id = row
.get("id")
.ok_or(QueryError::MissingColumn { column: "id" })?
.as_i64()
.ok_or(QueryError::TypeMismatch {
column: "id".into(),
expected: "i64",
})?;
let name = row
.get("name")
.ok_or(QueryError::MissingColumn { column: "name" })?
.as_str()
.ok_or(QueryError::TypeMismatch {
column: "name".into(),
expected: "String",
})?
.to_string();
Ok(UserRow { id, name })
}
}
#[test]
fn test_query_error_display() {
let e = QueryError::ColumnCountMismatch {
expected: 3,
actual: 2,
};
assert!(format!("{}", e).contains("3"));
assert!(format!("{}", e).contains("2"));
let e = QueryError::TypeMismatch {
column: "age".into(),
expected: "i64",
};
assert!(format!("{}", e).contains("age"));
let e = QueryError::MissingColumn { column: "id" };
assert!(format!("{}", e).contains("id"));
let e = QueryError::Custom("custom".into());
assert_eq!(format!("{}", e), "custom");
}
#[test]
fn test_row_desc_basic() {
let desc = RowDesc::new(vec!["id".into(), "name".into(), "age".into()]);
assert_eq!(desc.len(), 3);
assert!(!desc.is_empty());
assert_eq!(desc.index_of("name"), Some(1));
assert_eq!(desc.index_of("missing"), None);
}
#[test]
fn test_row_desc_empty() {
let desc = RowDesc::new(vec![]);
assert!(desc.is_empty());
assert_eq!(desc.len(), 0);
}
#[test]
fn test_user_row_from_values_success() {
let row =
UserRow::from_values(vec![Value::I64(42), Value::String("Alice".into())]).unwrap();
assert_eq!(row.id, 42);
assert_eq!(row.name, "Alice");
}
#[test]
fn test_user_row_from_values_count_mismatch() {
let result = UserRow::from_values(vec![Value::I64(42)]);
assert!(matches!(
result,
Err(QueryError::ColumnCountMismatch {
expected: 2,
actual: 1
})
));
}
#[test]
fn test_user_row_from_values_type_mismatch() {
let result = UserRow::from_values(vec![
Value::String("not_an_int".into()),
Value::String("Alice".into()),
]);
assert!(matches!(result, Err(QueryError::TypeMismatch { .. })));
}
#[test]
fn test_user_row_from_values_with_desc() {
let desc = RowDesc::new(vec!["id".into(), "name".into()]);
let row =
UserRow::from_values_with_desc(vec![Value::I64(1), Value::String("Bob".into())], &desc)
.unwrap();
assert_eq!(row.id, 1);
assert_eq!(row.name, "Bob");
}
#[test]
fn test_user_row_from_values_with_desc_mismatch() {
let desc = RowDesc::new(vec!["id".into(), "name".into(), "age".into()]);
let result =
UserRow::from_values_with_desc(vec![Value::I64(1), Value::String("Bob".into())], &desc);
assert!(matches!(
result,
Err(QueryError::ColumnCountMismatch { .. })
));
}
#[test]
fn test_user_row_from_row_success() {
let mut map = HashMap::new();
map.insert("id".into(), Value::I64(99));
map.insert("name".into(), Value::String("Charlie".into()));
let row = UserRow::from_row(map).unwrap();
assert_eq!(row.id, 99);
assert_eq!(row.name, "Charlie");
}
#[test]
fn test_user_row_from_row_missing_column() {
let mut map = HashMap::new();
map.insert("id".into(), Value::I64(99));
let result = UserRow::from_row(map);
assert!(matches!(
result,
Err(QueryError::MissingColumn { column: "name" })
));
}
#[test]
fn test_user_row_from_row_extra_columns_ignored() {
let mut map = HashMap::new();
map.insert("id".into(), Value::I64(1));
map.insert("name".into(), Value::String("X".into()));
map.insert("extra".into(), Value::String("ignored".into()));
let row = UserRow::from_row(map).unwrap();
assert_eq!(row.id, 1);
}
#[test]
fn test_value_queryable_single() {
let v = Value::from_values(vec![Value::I64(42)]).unwrap();
assert_eq!(v.as_i64(), Some(42));
}
#[test]
fn test_value_queryable_count_mismatch() {
let result = Value::from_values(vec![Value::I64(1), Value::I64(2)]);
assert!(matches!(
result,
Err(QueryError::ColumnCountMismatch { .. })
));
}
#[test]
fn test_tuple_2_queryable() {
let (a, b) =
<(Value, Value)>::from_values(vec![Value::I64(1), Value::String("hello".into())])
.unwrap();
assert_eq!(a.as_i64(), Some(1));
assert_eq!(b.as_str(), Some("hello"));
}
#[test]
fn test_tuple_3_queryable() {
let (a, b, c) = <(Value, Value, Value)>::from_values(vec![
Value::I64(1),
Value::String("two".into()),
Value::F64(3.5),
])
.unwrap();
assert_eq!(a.as_i64(), Some(1));
assert_eq!(b.as_str(), Some("two"));
assert_eq!(c.as_f64(), Some(3.5));
}
#[test]
fn test_value_helpers() {
assert_eq!(value_as_i64(&Value::I64(42)), Some(42));
assert_eq!(value_as_i64(&Value::String("42".into())), Some(42));
assert_eq!(value_as_f64(&Value::F64(3.5)), Some(3.5));
assert_eq!(
value_as_string(&Value::String("hi".into())),
Some("hi".into())
);
assert_eq!(value_as_bool(&Value::Bool(true)), Some(true));
}
#[test]
fn test_nullable_helpers() {
assert_eq!(value_as_nullable_i64(&Value::Null), None);
assert_eq!(value_as_nullable_i64(&Value::I64(42)), Some(42));
assert_eq!(value_as_nullable_string(&Value::Null), None);
assert_eq!(
value_as_nullable_string(&Value::String("hi".into())),
Some("hi".into())
);
}
#[test]
fn test_full_flow_queryable() {
let values = vec![Value::I64(1), Value::String("Alice".into())];
let row = UserRow::from_values(values).unwrap();
assert_eq!(
row,
UserRow {
id: 1,
name: "Alice".into()
}
);
}
#[test]
fn test_full_flow_from_row_with_extra_data() {
let mut map = HashMap::new();
map.insert("id".into(), Value::I64(7));
map.insert("name".into(), Value::String("Bob".into()));
map.insert("email".into(), Value::String("bob@example.com".into()));
map.insert("created_at".into(), Value::String("2026-01-01".into()));
let row = UserRow::from_row(map).unwrap();
assert_eq!(row.id, 7);
assert_eq!(row.name, "Bob");
}
}