use async_trait::async_trait;
use sea_query::Value as SeaValue;
#[derive(Debug, Clone)]
pub struct Statement {
pub sql: String,
pub values: sea_query::Values,
}
impl Statement {
pub fn new(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
values: sea_query::Values(Vec::new()),
}
}
pub fn with_values(sql: impl Into<String>, values: Vec<SeaValue>) -> Self {
Self {
sql: sql.into(),
values: sea_query::Values(values),
}
}
pub fn render(query: &impl sea_query::QueryStatementBuilder) -> Self {
let (sql, values) = query.build_any(&sea_query::SqliteQueryBuilder);
Self { sql, values }
}
}
#[derive(Debug, Clone)]
pub struct Rows {
pub rows: Vec<Row>,
}
impl Rows {
pub fn new(rows: Vec<Row>) -> Self {
Self { rows }
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn first(&self) -> Option<&Row> {
self.rows.first()
}
}
#[derive(Debug, Clone)]
pub struct Row {
columns: Vec<(String, SeaValue)>,
}
impl Row {
pub fn new(columns: Vec<(String, SeaValue)>) -> Self {
Self { columns }
}
pub fn column_names(&self) -> impl Iterator<Item = &str> {
self.columns.iter().map(|(name, _)| name.as_str())
}
pub fn columns(&self) -> impl Iterator<Item = (&str, &SeaValue)> {
self.columns
.iter()
.map(|(name, value)| (name.as_str(), value))
}
pub fn get<T: TryFromValue>(&self, column: &str) -> Option<T> {
let value = self
.columns
.iter()
.find(|(name, _)| name == column)
.map(|(_, value)| value)?;
T::try_from_value(value)
}
}
pub trait TryFromValue: Sized {
fn try_from_value(value: &SeaValue) -> Option<Self>;
}
fn text(value: &SeaValue) -> Option<String> {
match value {
SeaValue::String(Some(s)) => Some((**s).clone()),
SeaValue::Char(Some(c)) => Some(c.to_string()),
_ => None,
}
}
macro_rules! impl_int {
($($t:ty),* $(,)?) => {
$(
impl TryFromValue for $t {
fn try_from_value(value: &SeaValue) -> Option<Self> {
let i: i64 = match value {
SeaValue::TinyInt(Some(v)) => i64::from(*v),
SeaValue::SmallInt(Some(v)) => i64::from(*v),
SeaValue::Int(Some(v)) => i64::from(*v),
SeaValue::BigInt(Some(v)) => *v,
_ => return None,
};
<$t>::try_from(i).ok()
}
}
)*
};
}
impl_int!(i8, i16, i32, i64, u8, u16, u32, u64, usize);
impl TryFromValue for String {
fn try_from_value(value: &SeaValue) -> Option<Self> {
text(value)
}
}
impl TryFromValue for Vec<u8> {
fn try_from_value(value: &SeaValue) -> Option<Self> {
match value {
SeaValue::Bytes(Some(bytes)) => Some(bytes.as_ref().clone()),
_ => None,
}
}
}
impl TryFromValue for bool {
fn try_from_value(value: &SeaValue) -> Option<Self> {
match value {
SeaValue::Bool(Some(v)) => Some(*v),
SeaValue::Int(Some(v)) => Some(*v != 0),
_ => None,
}
}
}
impl TryFromValue for f64 {
fn try_from_value(value: &SeaValue) -> Option<Self> {
match value {
SeaValue::Float(Some(v)) => Some(f64::from(*v)),
SeaValue::Double(Some(v)) => Some(*v),
_ => None,
}
}
}
impl TryFromValue for Option<String> {
fn try_from_value(value: &SeaValue) -> Option<Self> {
match value {
SeaValue::String(inner) => Some(inner.as_deref().map(String::from)),
_ => None,
}
}
}
impl TryFromValue for SeaValue {
fn try_from_value(value: &SeaValue) -> Option<Self> {
Some(value.clone())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DbError {
Execute(String),
Query(String),
Batch(String),
}
impl std::fmt::Display for DbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (label, message) = match self {
Self::Execute(message) => ("execute failed", message),
Self::Query(message) => ("query failed", message),
Self::Batch(message) => ("batch failed", message),
};
write!(f, "{label}: {}", crate::logging::scrub_text(message))
}
}
impl std::error::Error for DbError {}
#[async_trait]
pub trait Database: Send + Sync {
async fn execute(&self, stmt: &Statement) -> Result<u64, DbError>;
async fn query(&self, stmt: &Statement) -> Result<Rows, DbError>;
async fn batch_atomic(&self, stmts: &[Statement]) -> Result<(), DbError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_sanitizes_the_driver_message() {
let error = DbError::Execute(
"duplicate key value violates unique constraint \"subscribers_email_normalized_key\" \
DETAIL: Key (email_normalized)=(nick@example.com) already exists."
.to_owned(),
);
let text = error.to_string();
assert!(text.starts_with("execute failed: "), "{text}");
assert!(!text.contains('@'), "{text}");
assert!(!text.contains("nick"), "{text}");
assert!(text.contains("[subject_hash:"), "{text}");
let error = DbError::Execute(
"error connecting to postgres://venture:sup3r-s3cret@db.internal:5432/app".to_owned(),
);
let text = error.to_string();
assert!(!text.contains("sup3r-s3cret"), "{text}");
assert!(text.contains("postgres://[redacted]@db.internal"), "{text}");
assert_eq!(
DbError::Query("no such table: subscribers".to_owned()).to_string(),
"query failed: no such table: subscribers"
);
assert_eq!(
DbError::Batch("batch aborted".to_owned()).to_string(),
"batch failed: batch aborted"
);
}
}