diff --git a/README.md b/README.md index 3129fb2a..03ab20d5 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Use mock connections to write unit tests for your logic. Quickly build search models that help you filter, sort and paginate data in APIs. -## Design goals +## Design Goals 1. Intuitive and ergonomic @@ -49,4 +49,10 @@ Balance between compile-time checking and compilation speed. 3. Avoid 'symbol soup' -Avoid macros with DSL, use derive macros where appropriate. Be friendly with IDE tools. \ No newline at end of file +Avoid macros with DSL, use derive macros where appropriate. Be friendly with IDE tools. + +## Test Time + +After some bitterness we realized it is not possible to capture everything compile time. But we don't +want to encounter problems at run time either. The solution is to perform checking at 'test time' to +uncover problems. These checks will be removed at production so there will be no run time penalty. \ No newline at end of file diff --git a/src/connector/executor.rs b/src/connector/executor.rs new file mode 100644 index 00000000..c82c3bb7 --- /dev/null +++ b/src/connector/executor.rs @@ -0,0 +1,51 @@ +use sqlx::mysql::MySqlQueryResult; +use std::{error::Error, fmt}; + +#[derive(Debug)] +pub struct ExecResult { + pub(crate) result: ExecResultHolder, +} + +#[derive(Debug)] +pub(crate) enum ExecResultHolder { + SqlxMySql(MySqlQueryResult), +} + +#[derive(Debug)] +pub struct ExecErr; + +// ExecResult // + +impl ExecResult { + pub fn last_insert_id(&self) -> u64 { + match &self.result { + ExecResultHolder::SqlxMySql(result) => { + result.last_insert_id() + } + } + } + + pub fn rows_affected(&self) -> u64 { + match &self.result { + ExecResultHolder::SqlxMySql(result) => { + result.rows_affected() + } + } + } +} + +// ExecErr // + +impl Error for ExecErr {} + +impl fmt::Display for ExecErr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for ExecErr { + fn from(_: sqlx::Error) -> ExecErr { + ExecErr + } +} diff --git a/src/connector/mod.rs b/src/connector/mod.rs index 7240ac7f..664e1665 100644 --- a/src/connector/mod.rs +++ b/src/connector/mod.rs @@ -1,10 +1,10 @@ +mod executor; mod select; - #[cfg(feature = "serialize-query-result")] mod select_json; +pub use executor::*; pub use select::*; - #[cfg(feature = "serialize-query-result")] pub use select_json::*; @@ -21,6 +21,8 @@ pub trait Connector { #[async_trait] pub trait Connection { + async fn execute(&self, stmt: Statement) -> Result; + async fn query_one(&self, stmt: Statement) -> Result; async fn query_all(&self, stmt: Statement) -> Result, QueryErr>; diff --git a/src/database/statement.rs b/src/database/statement.rs index abe5066c..92f8805d 100644 --- a/src/database/statement.rs +++ b/src/database/statement.rs @@ -3,25 +3,32 @@ use std::fmt; pub struct Statement { pub sql: String, - pub values: Values, + pub values: Option, } impl From<(String, Values)> for Statement { fn from(stmt: (String, Values)) -> Statement { Statement { sql: stmt.0, - values: stmt.1, + values: Some(stmt.1), } } } impl fmt::Display for Statement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let string = inject_parameters( - &self.sql, - self.values.0.clone(), - &MySqlQueryBuilder::default(), - ); - write!(f, "{}", &string) + match &self.values { + Some(values) => { + let string = inject_parameters( + &self.sql, + values.0.clone(), + &MySqlQueryBuilder::default(), + ); + write!(f, "{}", &string) + } + None => { + write!(f, "{}", &self.sql) + } + } } } diff --git a/src/driver/sqlx_mysql.rs b/src/driver/sqlx_mysql.rs index e7e37e29..ac510eb2 100644 --- a/src/driver/sqlx_mysql.rs +++ b/src/driver/sqlx_mysql.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use sqlx::{mysql::MySqlRow, MySqlPool}; +use sqlx::{mysql::{MySqlRow, MySqlArguments, MySqlQueryResult}, MySql, MySqlPool}; sea_query::sea_query_driver_mysql!(); use sea_query_driver_mysql::bind_query; @@ -31,10 +31,22 @@ impl Connector for SqlxMySqlConnector { #[async_trait] impl Connection for &SqlxMySqlPoolConnection { + async fn execute(&self, stmt: Statement) -> Result { + debug_print!("{}", stmt); + + let query = sqlx_query(&stmt); + if let Ok(conn) = &mut self.pool.acquire().await { + if let Ok(res) = query.execute(conn).await { + return Ok(res.into()); + } + } + Err(ExecErr) + } + async fn query_one(&self, stmt: Statement) -> Result { debug_print!("{}", stmt); - let query = bind_query(sqlx::query(&stmt.sql), &stmt.values); + let query = sqlx_query(&stmt); if let Ok(conn) = &mut self.pool.acquire().await { if let Ok(row) = query.fetch_one(conn).await { return Ok(row.into()); @@ -46,7 +58,7 @@ impl Connection for &SqlxMySqlPoolConnection { async fn query_all(&self, stmt: Statement) -> Result, QueryErr> { debug_print!("{}", stmt); - let query = bind_query(sqlx::query(&stmt.sql), &stmt.values); + let query = sqlx_query(&stmt); if let Ok(conn) = &mut self.pool.acquire().await { if let Ok(rows) = query.fetch_all(conn).await { return Ok(rows.into_iter().map(|r| r.into()).collect()); @@ -63,3 +75,19 @@ impl From for QueryResult { } } } + +impl From for ExecResult { + fn from(result: MySqlQueryResult) -> ExecResult { + ExecResult { + result: ExecResultHolder::SqlxMySql(result), + } + } +} + +fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, MySql, MySqlArguments> { + let mut query = sqlx::query(&stmt.sql); + if let Some(values) = &stmt.values { + query = bind_query(query, values); + } + query +} \ No newline at end of file diff --git a/src/entity/base.rs b/src/entity/base.rs index 84a11ea2..4283a27f 100644 --- a/src/entity/base.rs +++ b/src/entity/base.rs @@ -70,7 +70,7 @@ pub trait EntityTrait: EntityName { /// .to_string(), /// [ /// r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#, - /// r#"WHERE "cake_filling"."cake_id" = 2 AND ("cake_filling"."filling_id" = 3)"#, + /// r#"WHERE "cake_filling"."cake_id" = 2 AND "cake_filling"."filling_id" = 3"#, /// ].join(" ") /// ); /// ``` diff --git a/src/query/combine.rs b/src/query/combine.rs index 8de78c1f..757fa3ee 100644 --- a/src/query/combine.rs +++ b/src/query/combine.rs @@ -116,7 +116,7 @@ mod tests { "SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,", "`fruit`.`id` AS `B_id`, `fruit`.`name` AS `B_name`, `fruit`.`cake_id` AS `B_cake_id`", "FROM `cake` LEFT JOIN `fruit` ON `cake`.`id` = `fruit`.`cake_id`", - "WHERE `cake`.`id` = 1 AND (`fruit`.`id` = 2)", + "WHERE `cake`.`id` = 1 AND `fruit`.`id` = 2", ].join(" ") ); } diff --git a/src/query/helper.rs b/src/query/helper.rs index ccc38dfb..d973fe6e 100644 --- a/src/query/helper.rs +++ b/src/query/helper.rs @@ -1,12 +1,8 @@ use crate::{ColumnTrait, Identity, IntoSimpleExpr, RelationDef}; -use sea_query::{Alias, Expr, SelectExpr, SelectStatement, SimpleExpr}; +use sea_query::{Alias, Expr, IntoCondition, SelectExpr, SelectStatement, SimpleExpr}; pub use sea_query::{Condition, JoinType, Order}; use std::rc::Rc; -pub trait IntoCondition { - fn into_condition(self) -> Condition; -} - pub trait QueryHelper: Sized { fn query(&mut self) -> &mut SelectStatement; @@ -71,10 +67,10 @@ pub trait QueryHelper: Sized { /// .filter(cake::Column::Id.eq(5)) /// .build(MysqlQueryBuilder) /// .to_string(), - /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` = 4 AND (`cake`.`id` = 5)" + /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` = 4 AND `cake`.`id` = 5" /// ); /// ``` - /// + /// /// Add a condition tree. /// ``` /// use sea_orm::{Condition, ColumnTrait, EntityTrait, QueryHelper, tests_cfg::cake, sea_query::MysqlQueryBuilder}; @@ -92,7 +88,9 @@ pub trait QueryHelper: Sized { /// ); /// ``` fn filter(mut self, filter: F) -> Self - where F: IntoCondition { + where + F: IntoCondition, + { self.query().cond_where(filter.into_condition()); self } @@ -216,18 +214,6 @@ pub trait QueryHelper: Sized { } } -impl IntoCondition for SimpleExpr { - fn into_condition(self) -> Condition { - Condition::all().add(self) - } -} - -impl IntoCondition for Condition { - fn into_condition(self) -> Condition { - self - } -} - fn join_condition(rel: RelationDef) -> SimpleExpr { let from_tbl = rel.from_tbl.clone(); let to_tbl = rel.to_tbl.clone();