switchy_database/
lib.rs

1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
2#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
3#![allow(clippy::multiple_crate_versions)]
4
5pub mod config;
6#[cfg(feature = "postgres-raw")]
7pub mod postgres;
8pub mod profiles;
9#[cfg(feature = "sqlite-rusqlite")]
10pub mod rusqlite;
11#[cfg(feature = "simulator")]
12pub mod simulator;
13#[cfg(feature = "sqlx")]
14pub mod sqlx;
15
16pub mod query;
17
18#[cfg(feature = "schema")]
19pub mod schema;
20
21use std::{num::TryFromIntError, sync::Arc};
22
23use async_trait::async_trait;
24use chrono::NaiveDateTime;
25use query::{
26    DeleteStatement, InsertStatement, SelectQuery, UpdateStatement, UpsertMultiStatement,
27    UpsertStatement,
28};
29use thiserror::Error;
30
31#[derive(Debug, Clone, PartialEq)]
32pub enum DatabaseValue {
33    Null,
34    String(String),
35    StringOpt(Option<String>),
36    Bool(bool),
37    BoolOpt(Option<bool>),
38    Number(i64),
39    NumberOpt(Option<i64>),
40    UNumber(u64),
41    UNumberOpt(Option<u64>),
42    Real(f64),
43    RealOpt(Option<f64>),
44    NowAdd(String),
45    Now,
46    DateTime(NaiveDateTime),
47}
48
49impl DatabaseValue {
50    #[must_use]
51    #[allow(clippy::missing_const_for_fn)]
52    pub fn as_str(&self) -> Option<&str> {
53        match self {
54            Self::String(value) | Self::StringOpt(Some(value)) => Some(value),
55            _ => None,
56        }
57    }
58}
59
60impl<T: Into<Self>> From<Option<T>> for DatabaseValue {
61    fn from(val: Option<T>) -> Self {
62        val.map_or(Self::Null, std::convert::Into::into)
63    }
64}
65
66impl From<bool> for DatabaseValue {
67    fn from(val: bool) -> Self {
68        Self::Bool(val)
69    }
70}
71
72impl From<&str> for DatabaseValue {
73    fn from(val: &str) -> Self {
74        Self::String(val.to_string())
75    }
76}
77
78impl From<&String> for DatabaseValue {
79    fn from(val: &String) -> Self {
80        Self::String(val.to_string())
81    }
82}
83
84impl From<String> for DatabaseValue {
85    fn from(val: String) -> Self {
86        Self::String(val)
87    }
88}
89
90impl From<f32> for DatabaseValue {
91    fn from(val: f32) -> Self {
92        Self::Real(f64::from(val))
93    }
94}
95
96impl From<f64> for DatabaseValue {
97    fn from(val: f64) -> Self {
98        Self::Real(val)
99    }
100}
101
102impl From<i8> for DatabaseValue {
103    fn from(val: i8) -> Self {
104        Self::Number(i64::from(val))
105    }
106}
107
108impl From<i16> for DatabaseValue {
109    fn from(val: i16) -> Self {
110        Self::Number(i64::from(val))
111    }
112}
113
114impl From<i32> for DatabaseValue {
115    fn from(val: i32) -> Self {
116        Self::Number(i64::from(val))
117    }
118}
119
120impl From<i64> for DatabaseValue {
121    fn from(val: i64) -> Self {
122        Self::Number(val)
123    }
124}
125
126impl From<isize> for DatabaseValue {
127    fn from(val: isize) -> Self {
128        Self::Number(val as i64)
129    }
130}
131
132impl From<u8> for DatabaseValue {
133    fn from(val: u8) -> Self {
134        Self::UNumber(u64::from(val))
135    }
136}
137
138impl From<u16> for DatabaseValue {
139    fn from(val: u16) -> Self {
140        Self::UNumber(u64::from(val))
141    }
142}
143
144impl From<u32> for DatabaseValue {
145    fn from(val: u32) -> Self {
146        Self::UNumber(u64::from(val))
147    }
148}
149
150impl From<u64> for DatabaseValue {
151    fn from(val: u64) -> Self {
152        Self::UNumber(val)
153    }
154}
155
156impl From<usize> for DatabaseValue {
157    fn from(val: usize) -> Self {
158        Self::UNumber(val as u64)
159    }
160}
161
162pub trait AsId {
163    fn as_id(&self) -> DatabaseValue;
164}
165
166#[derive(Debug, Error)]
167pub enum TryFromError {
168    #[error("Could not convert to type '{0}'")]
169    CouldNotConvert(String),
170    #[error(transparent)]
171    TryFromInt(#[from] TryFromIntError),
172}
173
174impl TryFrom<DatabaseValue> for u64 {
175    type Error = TryFromError;
176
177    fn try_from(value: DatabaseValue) -> Result<Self, Self::Error> {
178        match value {
179            DatabaseValue::Number(value) | DatabaseValue::NumberOpt(Some(value)) => {
180                Ok(Self::try_from(value)?)
181            }
182            DatabaseValue::UNumber(value) | DatabaseValue::UNumberOpt(Some(value)) => Ok(value),
183            _ => Err(TryFromError::CouldNotConvert("u64".into())),
184        }
185    }
186}
187
188impl TryFrom<DatabaseValue> for i32 {
189    type Error = TryFromError;
190
191    fn try_from(value: DatabaseValue) -> Result<Self, Self::Error> {
192        match value {
193            DatabaseValue::Number(value) | DatabaseValue::NumberOpt(Some(value)) => {
194                Ok(Self::try_from(value)?)
195            }
196            DatabaseValue::UNumber(value) | DatabaseValue::UNumberOpt(Some(value)) => {
197                Ok(Self::try_from(value)?)
198            }
199            _ => Err(TryFromError::CouldNotConvert("i32".into())),
200        }
201    }
202}
203
204#[derive(Debug, Error)]
205pub enum DatabaseError {
206    #[cfg(feature = "sqlite-rusqlite")]
207    #[error(transparent)]
208    Rusqlite(rusqlite::RusqliteDatabaseError),
209    #[cfg(feature = "mysql-sqlx")]
210    #[error(transparent)]
211    MysqlSqlx(sqlx::mysql::SqlxDatabaseError),
212    #[cfg(feature = "sqlite-sqlx")]
213    #[error(transparent)]
214    SqliteSqlx(sqlx::sqlite::SqlxDatabaseError),
215    #[cfg(feature = "postgres-raw")]
216    #[error(transparent)]
217    Postgres(postgres::postgres::PostgresDatabaseError),
218    #[cfg(feature = "postgres-sqlx")]
219    #[error(transparent)]
220    PostgresSqlx(sqlx::postgres::SqlxDatabaseError),
221    #[error("No row")]
222    NoRow,
223    #[cfg(feature = "schema")]
224    #[error("Invalid schema: {0}")]
225    InvalidSchema(String),
226}
227
228impl DatabaseError {
229    #[must_use]
230    #[allow(clippy::missing_const_for_fn)]
231    pub fn is_connection_error(&self) -> bool {
232        match &self {
233            #[cfg(feature = "postgres-sqlx")]
234            Self::PostgresSqlx(sqlx::postgres::SqlxDatabaseError::Sqlx(::sqlx::Error::Io(
235                _io_err,
236            ))) => true,
237            #[cfg(feature = "mysql-sqlx")]
238            Self::MysqlSqlx(sqlx::mysql::SqlxDatabaseError::Sqlx(::sqlx::Error::Io(_io_err))) => {
239                true
240            }
241            #[cfg(feature = "sqlite-sqlx")]
242            Self::SqliteSqlx(sqlx::sqlite::SqlxDatabaseError::Sqlx(::sqlx::Error::Io(_io_err))) => {
243                true
244            }
245            #[cfg(feature = "postgres-raw")]
246            Self::Postgres(postgres::postgres::PostgresDatabaseError::Postgres(pg_err)) => {
247                pg_err.to_string().as_str() == "connection closed"
248            }
249            #[cfg(feature = "sqlite-rusqlite")]
250            Self::Rusqlite(rusqlite::RusqliteDatabaseError::Rusqlite(
251                ::rusqlite::Error::SqliteFailure(_, _),
252            )) => true,
253            _ => false,
254        }
255    }
256}
257
258#[derive(Debug)]
259pub struct Row {
260    pub columns: Vec<(String, DatabaseValue)>,
261}
262
263impl Row {
264    #[must_use]
265    pub fn get(&self, column_name: &str) -> Option<DatabaseValue> {
266        self.columns
267            .iter()
268            .find(|c| c.0 == column_name)
269            .map(|c| c.1.clone())
270    }
271
272    #[must_use]
273    pub fn id(&self) -> Option<DatabaseValue> {
274        self.get("id")
275    }
276}
277
278#[async_trait]
279pub trait Database: Send + Sync + std::fmt::Debug {
280    fn select(&self, table_name: &'static str) -> SelectQuery<'static> {
281        query::select(table_name)
282    }
283    fn update<'a>(&self, table_name: &'a str) -> UpdateStatement<'a> {
284        query::update(table_name)
285    }
286    fn insert<'a>(&self, table_name: &'a str) -> InsertStatement<'a> {
287        query::insert(table_name)
288    }
289    fn upsert<'a>(&self, table_name: &'a str) -> UpsertStatement<'a> {
290        query::upsert(table_name)
291    }
292    fn upsert_first<'a>(&self, table_name: &'a str) -> UpsertStatement<'a> {
293        query::upsert(table_name)
294    }
295    fn upsert_multi<'a>(&self, table_name: &'a str) -> UpsertMultiStatement<'a> {
296        query::upsert_multi(table_name)
297    }
298    fn delete<'a>(&self, table_name: &'a str) -> DeleteStatement<'a> {
299        query::delete(table_name)
300    }
301
302    #[cfg(feature = "schema")]
303    fn create_table<'a>(&self, table_name: &'a str) -> schema::CreateTableStatement<'a> {
304        schema::create_table(table_name)
305    }
306
307    async fn query(&self, query: &SelectQuery<'_>) -> Result<Vec<Row>, DatabaseError>;
308    async fn query_first(&self, query: &SelectQuery<'_>) -> Result<Option<Row>, DatabaseError>;
309    async fn exec_update(&self, statement: &UpdateStatement<'_>)
310    -> Result<Vec<Row>, DatabaseError>;
311    async fn exec_update_first(
312        &self,
313        statement: &UpdateStatement<'_>,
314    ) -> Result<Option<Row>, DatabaseError>;
315    async fn exec_insert(&self, statement: &InsertStatement<'_>) -> Result<Row, DatabaseError>;
316    async fn exec_upsert(&self, statement: &UpsertStatement<'_>)
317    -> Result<Vec<Row>, DatabaseError>;
318    async fn exec_upsert_first(
319        &self,
320        statement: &UpsertStatement<'_>,
321    ) -> Result<Row, DatabaseError>;
322    async fn exec_upsert_multi(
323        &self,
324        statement: &UpsertMultiStatement<'_>,
325    ) -> Result<Vec<Row>, DatabaseError>;
326    async fn exec_delete(&self, statement: &DeleteStatement<'_>)
327    -> Result<Vec<Row>, DatabaseError>;
328    async fn exec_delete_first(
329        &self,
330        statement: &DeleteStatement<'_>,
331    ) -> Result<Option<Row>, DatabaseError>;
332
333    async fn exec_raw(&self, statement: &str) -> Result<(), DatabaseError>;
334
335    /// # Errors
336    ///
337    /// Will return `Err` if the close failed to trigger.
338    fn trigger_close(&self) -> Result<(), DatabaseError> {
339        Ok(())
340    }
341
342    async fn close(&self) -> Result<(), DatabaseError> {
343        self.trigger_close()
344    }
345
346    #[cfg(feature = "schema")]
347    async fn exec_create_table(
348        &self,
349        statement: &schema::CreateTableStatement<'_>,
350    ) -> Result<(), DatabaseError>;
351}
352
353#[async_trait]
354pub trait TryFromDb<T>
355where
356    Self: Sized,
357{
358    type Error;
359
360    async fn try_from_db(value: T, db: Arc<Box<dyn Database>>) -> Result<Self, Self::Error>;
361}
362
363#[async_trait]
364impl<T, U: Send + 'static> TryFromDb<Vec<U>> for Vec<T>
365where
366    T: TryFromDb<U> + Send,
367{
368    type Error = T::Error;
369
370    async fn try_from_db(value: Vec<U>, db: Arc<Box<dyn Database>>) -> Result<Self, T::Error> {
371        let mut converted = Self::with_capacity(value.len());
372
373        for x in value {
374            converted.push(T::try_from_db(x, db.clone()).await?);
375        }
376
377        Ok(converted)
378    }
379}
380
381#[async_trait]
382impl<T, U: Send + 'static> TryFromDb<Option<U>> for Option<T>
383where
384    T: TryFromDb<U>,
385{
386    type Error = T::Error;
387
388    async fn try_from_db(value: Option<U>, db: Arc<Box<dyn Database>>) -> Result<Self, T::Error> {
389        Ok(match value {
390            Some(x) => Some(T::try_from_db(x, db).await?),
391            None => None,
392        })
393    }
394}
395
396#[async_trait]
397pub trait TryIntoDb<T>
398where
399    Self: Sized,
400{
401    type Error;
402
403    async fn try_into_db(self, db: Arc<Box<dyn Database>>) -> Result<T, Self::Error>;
404}
405
406#[async_trait]
407impl<T: Send, U> TryIntoDb<U> for T
408where
409    U: TryFromDb<T>,
410{
411    type Error = U::Error;
412
413    async fn try_into_db(self, db: Arc<Box<dyn Database>>) -> Result<U, U::Error> {
414        U::try_from_db(self, db).await
415    }
416}