use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SqlValue {
Null,
Bool(bool),
Integer(i64),
Float(f64),
Text(String),
Blob(Vec<u8>),
Json(Value),
Uuid(Uuid),
Timestamp(DateTime<Utc>),
}
impl SqlValue {
pub fn from_opt_text(value: Option<&str>) -> SqlValue {
match value {
Some(text) => SqlValue::Text(text.to_owned()),
None => SqlValue::Null,
}
}
pub fn from_opt_i64(value: Option<i64>) -> SqlValue {
match value {
Some(number) => SqlValue::Integer(number),
None => SqlValue::Null,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SqlStatement {
pub sql: String,
pub params: Vec<SqlValue>,
pub label: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SqlColumn {
pub name: String,
pub value: SqlValue,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SqlRow {
pub columns: Vec<SqlColumn>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SqlColumnError {
pub column: String,
pub found: Option<&'static str>,
}
impl std::fmt::Display for SqlColumnError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.found {
Some(found) => write!(f, "SQL column {} has value {found}", self.column),
None => write!(f, "SQL column {} is absent", self.column),
}
}
}
impl std::error::Error for SqlColumnError {}
impl SqlRow {
pub fn get(&self, name: &str) -> Option<&SqlValue> {
self.columns
.iter()
.find(|c| c.name == name)
.map(|c| &c.value)
}
pub fn text(&self, name: &str) -> Result<&str, SqlColumnError> {
match self.get(name) {
Some(SqlValue::Text(value)) => Ok(value),
found => Err(column_error(name, found)),
}
}
pub fn i64(&self, name: &str) -> Result<i64, SqlColumnError> {
match self.get(name) {
Some(SqlValue::Integer(value)) => Ok(*value),
found => Err(column_error(name, found)),
}
}
pub fn opt_text(&self, name: &str) -> Result<Option<&str>, SqlColumnError> {
match self.get(name) {
Some(SqlValue::Null) => Ok(None),
Some(SqlValue::Text(value)) => Ok(Some(value)),
found => Err(column_error(name, found)),
}
}
pub fn opt_i64(&self, name: &str) -> Result<Option<i64>, SqlColumnError> {
match self.get(name) {
Some(SqlValue::Null) => Ok(None),
Some(SqlValue::Integer(value)) => Ok(Some(*value)),
found => Err(column_error(name, found)),
}
}
pub fn opt_text_or_absent(&self, name: &str) -> Result<Option<&str>, SqlColumnError> {
match self.get(name) {
None => Ok(None),
_ => self.opt_text(name),
}
}
pub fn opt_i64_or_absent(&self, name: &str) -> Result<Option<i64>, SqlColumnError> {
match self.get(name) {
None => Ok(None),
_ => self.opt_i64(name),
}
}
}
fn column_error(name: &str, found: Option<&SqlValue>) -> SqlColumnError {
SqlColumnError {
column: name.to_owned(),
found: found.map(|value| match value {
SqlValue::Null => "Null",
SqlValue::Bool(_) => "Bool",
SqlValue::Integer(_) => "Integer",
SqlValue::Float(_) => "Float",
SqlValue::Text(_) => "Text",
SqlValue::Blob(_) => "Blob",
SqlValue::Json(_) => "Json",
SqlValue::Uuid(_) => "Uuid",
SqlValue::Timestamp(_) => "Timestamp",
}),
}
}