use crate::error::FlozError;
use crate::value::Value;
use super::row_bound::FlozRowBound;
use super::pool::{Db, DbInner};
use super::tx::{Tx, TxInner};
pub trait Executor {
fn execute_raw(
&self,
sql: &str,
params: Vec<Value>,
) -> impl std::future::Future<Output = Result<u64, FlozError>> + Send;
fn fetch_all<T>(
&self,
sql: &str,
params: Vec<Value>,
) -> impl std::future::Future<Output = Result<Vec<T>, FlozError>> + Send
where
T: Send + Unpin + FlozRowBound;
fn fetch_one<T>(
&self,
sql: &str,
params: Vec<Value>,
) -> impl std::future::Future<Output = Result<T, FlozError>> + Send
where
T: Send + Unpin + FlozRowBound;
fn fetch_optional<T>(
&self,
sql: &str,
params: Vec<Value>,
) -> impl std::future::Future<Output = Result<Option<T>, FlozError>> + Send
where
T: Send + Unpin + FlozRowBound;
}
macro_rules! bind_params {
($query:expr, $params:expr) => {{
let mut q = $query;
for param in $params {
q = match param {
Value::Short(v) => q.bind(v),
Value::Int(v) => q.bind(v),
Value::BigInt(v) => q.bind(v),
Value::Real(v) => q.bind(v),
Value::Double(v) => q.bind(v),
Value::Bool(v) => q.bind(v),
Value::String(v) => q.bind(v),
Value::Bytes(v) => q.bind(v),
Value::Uuid(v) => q.bind(v.to_string()),
Value::DateTime(v) => q.bind(v),
Value::NaiveDateTime(v) => q.bind(v),
Value::NaiveDate(v) => q.bind(v),
Value::NaiveTime(v) => q.bind(v),
Value::Json(v) => q.bind(v.to_string()),
Value::Jsonb(v) => q.bind(v.to_string()),
Value::OptionShort(v) => q.bind(v),
Value::OptionInt(v) => q.bind(v),
Value::OptionBigInt(v) => q.bind(v),
Value::OptionReal(v) => q.bind(v),
Value::OptionDouble(v) => q.bind(v),
Value::OptionBool(v) => q.bind(v),
Value::OptionString(v) => q.bind(v),
Value::OptionBytes(v) => q.bind(v),
Value::OptionUuid(v) => q.bind(v.map(|u| u.to_string())),
Value::OptionDateTime(v) => q.bind(v),
Value::OptionNaiveDateTime(v) => q.bind(v),
Value::OptionNaiveDate(v) => q.bind(v),
Value::OptionNaiveTime(v) => q.bind(v),
Value::OptionJson(v) => q.bind(v.map(|j| j.to_string())),
Value::OptionJsonb(v) => q.bind(v.map(|j| j.to_string())),
};
}
q
}};
}
impl Executor for Db {
#[allow(unreachable_patterns)]
async fn execute_raw(&self, sql: &str, params: Vec<Value>) -> Result<u64, FlozError> {
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
DbInner::Postgres(pool) => {
let query = bind_params!(sqlx::query(&sql), params);
let result = query.execute(pool).await?;
Ok(result.rows_affected())
}
#[cfg(feature = "sqlite")]
DbInner::Sqlite(pool) => {
let query = bind_params!(sqlx::query(&sql), params);
let result = query.execute(pool).await?;
Ok(result.rows_affected())
}
_ => unreachable!("no database backend enabled"),
}
}
#[allow(unreachable_patterns)]
async fn fetch_all<T>(&self, sql: &str, params: Vec<Value>) -> Result<Vec<T>, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
DbInner::Postgres(pool) => {
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_all(pool).await?)
}
#[cfg(feature = "sqlite")]
DbInner::Sqlite(pool) => {
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_all(pool).await?)
}
_ => unreachable!("no database backend enabled"),
}
}
#[allow(unreachable_patterns)]
async fn fetch_one<T>(&self, sql: &str, params: Vec<Value>) -> Result<T, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
DbInner::Postgres(pool) => {
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
query
.fetch_optional(pool)
.await?
.ok_or(FlozError::NotFound)
}
#[cfg(feature = "sqlite")]
DbInner::Sqlite(pool) => {
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
query
.fetch_optional(pool)
.await?
.ok_or(FlozError::NotFound)
}
_ => unreachable!("no database backend enabled"),
}
}
#[allow(unreachable_patterns)]
async fn fetch_optional<T>(
&self,
sql: &str,
params: Vec<Value>,
) -> Result<Option<T>, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
DbInner::Postgres(pool) => {
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_optional(pool).await?)
}
#[cfg(feature = "sqlite")]
DbInner::Sqlite(pool) => {
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_optional(pool).await?)
}
_ => unreachable!("no database backend enabled"),
}
}
}
impl Executor for Tx {
#[allow(unreachable_patterns)]
async fn execute_raw(&self, sql: &str, params: Vec<Value>) -> Result<u64, FlozError> {
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query(&sql), params);
let result = query.execute(&mut **c).await?;
Ok(result.rows_affected())
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query(&sql), params);
let result = query.execute(&mut **c).await?;
Ok(result.rows_affected())
}
_ => unreachable!("no database backend enabled"),
}
}
#[allow(unreachable_patterns)]
async fn fetch_all<T>(&self, sql: &str, params: Vec<Value>) -> Result<Vec<T>, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_all(&mut **c).await?)
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_all(&mut **c).await?)
}
_ => unreachable!("no database backend enabled"),
}
}
#[allow(unreachable_patterns)]
async fn fetch_one<T>(&self, sql: &str, params: Vec<Value>) -> Result<T, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
query
.fetch_optional(&mut **c)
.await?
.ok_or(FlozError::NotFound)
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
query
.fetch_optional(&mut **c)
.await?
.ok_or(FlozError::NotFound)
}
_ => unreachable!("no database backend enabled"),
}
}
#[allow(unreachable_patterns)]
async fn fetch_optional<T>(
&self,
sql: &str,
params: Vec<Value>,
) -> Result<Option<T>, FlozError>
where
T: Send + Unpin + FlozRowBound,
{
let sql = self.adjust_sql(sql);
match &self.inner {
#[cfg(feature = "postgres")]
TxInner::Postgres(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_optional(&mut **c).await?)
}
#[cfg(feature = "sqlite")]
TxInner::Sqlite(conn) => {
let mut c = conn.lock().await;
let query = bind_params!(sqlx::query_as::<_, T>(&sql), params);
Ok(query.fetch_optional(&mut **c).await?)
}
_ => unreachable!("no database backend enabled"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn db_is_send_sync() {
fn _assert_send<T: Send>() {}
fn _assert_sync<T: Sync>() {}
_assert_send::<Db>();
_assert_sync::<Db>();
}
#[test]
fn tx_is_debug() {
fn _assert_debug<T: std::fmt::Debug>() {}
_assert_debug::<Tx>();
}
#[cfg(feature = "sqlite")]
mod sqlite_tests {
use super::*;
#[tokio::test]
async fn connect_memory() {
let db = Db::connect("sqlite::memory:").await.unwrap();
let affected = db.execute_raw("SELECT 1", vec![]).await.unwrap();
assert_eq!(affected, 0);
}
#[tokio::test]
async fn create_insert_fetch() {
let db = Db::connect("sqlite::memory:").await.unwrap();
db.execute_raw(
"CREATE TABLE test_users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
vec![],
)
.await
.unwrap();
let affected = db
.execute_raw(
"INSERT INTO test_users (id, name) VALUES ($1, $2)",
vec![Value::Int(1), Value::String("Alice".into())],
)
.await
.unwrap();
assert_eq!(affected, 1);
#[derive(Debug, sqlx::FromRow)]
struct User {
id: i32,
name: String,
}
let users: Vec<User> = db
.fetch_all("SELECT id, name FROM test_users ORDER BY id", vec![])
.await
.unwrap();
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Alice");
}
#[tokio::test]
async fn fetch_one_not_found() {
let db = Db::connect("sqlite::memory:").await.unwrap();
db.execute_raw("CREATE TABLE test_nf (id INTEGER)", vec![])
.await
.unwrap();
#[derive(Debug, sqlx::FromRow)]
struct Row {
id: i32,
}
let result: Result<Row, _> = db
.fetch_one(
"SELECT id FROM test_nf WHERE id = $1",
vec![Value::Int(999)],
)
.await;
assert!(matches!(result, Err(FlozError::NotFound)));
}
#[tokio::test]
async fn transaction_commit() {
let db = Db::connect("sqlite::memory:").await.unwrap();
db.execute_raw("CREATE TABLE test_tx (id INTEGER)", vec![])
.await
.unwrap();
let tx = db.begin().await.unwrap();
tx.execute_raw(
"INSERT INTO test_tx VALUES ($1)",
vec![Value::Int(1)],
)
.await
.unwrap();
tx.commit().await.unwrap();
#[derive(Debug, sqlx::FromRow)]
struct Row {
id: i32,
}
let rows: Vec<Row> = db
.fetch_all("SELECT id FROM test_tx", vec![])
.await
.unwrap();
assert_eq!(rows.len(), 1);
}
#[tokio::test]
async fn transaction_rollback() {
let db = Db::connect("sqlite::memory:").await.unwrap();
db.execute_raw("CREATE TABLE test_rb (id INTEGER)", vec![])
.await
.unwrap();
let tx = db.begin().await.unwrap();
tx.execute_raw(
"INSERT INTO test_rb VALUES ($1)",
vec![Value::Int(1)],
)
.await
.unwrap();
tx.rollback().await.unwrap();
#[derive(Debug, sqlx::FromRow)]
struct Row {
id: i32,
}
let rows: Vec<Row> = db
.fetch_all("SELECT id FROM test_rb", vec![])
.await
.unwrap();
assert_eq!(rows.len(), 0);
}
}
#[cfg(feature = "postgres")]
mod pg_tests {
use super::*;
fn test_url() -> Option<String> {
std::env::var("DATABASE_URL").ok()
}
#[tokio::test]
async fn connect_to_postgres() {
let Some(url) = test_url() else { return };
let db = Db::connect(&url).await.unwrap();
let affected = db.execute_raw("SELECT 1", vec![]).await.unwrap();
assert_eq!(affected, 0);
}
#[tokio::test]
async fn execute_with_params() {
let Some(url) = test_url() else { return };
let db = Db::connect(&url).await.unwrap();
db.execute_raw(
"CREATE TEMP TABLE floz_test_params (id INT, name TEXT)",
vec![],
)
.await
.unwrap();
let affected = db
.execute_raw(
"INSERT INTO floz_test_params (id, name) VALUES ($1, $2)",
vec![Value::Int(1), Value::String("Alice".into())],
)
.await
.unwrap();
assert_eq!(affected, 1);
}
#[tokio::test]
async fn fetch_all_rows() {
let Some(url) = test_url() else { return };
let db = Db::connect(&url).await.unwrap();
db.execute_raw(
"CREATE TEMP TABLE floz_test_fetch (id INT, name TEXT)",
vec![],
)
.await
.unwrap();
db.execute_raw(
"INSERT INTO floz_test_fetch VALUES (1, 'Alice'), (2, 'Bob')",
vec![],
)
.await
.unwrap();
#[derive(Debug, sqlx::FromRow)]
struct Row {
id: i32,
name: String,
}
let rows: Vec<Row> = db
.fetch_all(
"SELECT id, name FROM floz_test_fetch ORDER BY id",
vec![],
)
.await
.unwrap();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].name, "Alice");
assert_eq!(rows[1].name, "Bob");
}
#[tokio::test]
async fn transaction_commit() {
let Some(url) = test_url() else { return };
let db = Db::connect(&url).await.unwrap();
db.execute_raw("CREATE TEMP TABLE floz_test_tx (id INT)", vec![])
.await
.unwrap();
let tx = db.begin().await.unwrap();
tx.execute_raw(
"INSERT INTO floz_test_tx VALUES ($1)",
vec![Value::Int(1)],
)
.await
.unwrap();
tx.commit().await.unwrap();
#[derive(Debug, sqlx::FromRow)]
struct Row {
id: i32,
}
let rows: Vec<Row> = db
.fetch_all("SELECT id FROM floz_test_tx", vec![])
.await
.unwrap();
assert_eq!(rows.len(), 1);
}
}
}