diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 03bbe23b..7928130b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -11,6 +11,24 @@ env: jobs: + clippy: + name: Clippy + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + components: clippy + override: true + + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-targets --all + compile-sqlite: name: Compile SQLite runs-on: ubuntu-20.04 diff --git a/Cargo.toml b/Cargo.toml index e9d0774e..16b4a378 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ chrono = { version = "^0", optional = true } futures = { version = "^0.3" } futures-util = { version = "^0.3" } rust_decimal = { version = "^1", optional = true } -sea-orm-macros = { version = "^0.1.1", optional = true } -sea-query = { version = "^0.15", features = ["thread-safe"] } +sea-orm-macros = { version = "^0.1.1", path = "sea-orm-macros", optional = true } +sea-query = { version = "^0.16", features = ["thread-safe"] } sea-strum = { version = "^0.21", features = ["derive", "sea-orm"] } serde = { version = "^1.0", features = ["derive"] } sqlx = { version = "^0.5", optional = true } @@ -36,8 +36,11 @@ sqlx-core = { version = "^0.5", optional = true } sqlx-macros = { version = "^0.5", optional = true } serde_json = { version = "^1", optional = true } uuid = { version = "0.8", features = ["serde", "v4"], optional = true } -rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git", features = ["sqlx_mysql"], optional = true } +rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git", features = [ + "sqlx_mysql", +], optional = true } async-trait = { version = "0.1", optional = true } +log = { version = "^0.4", optional = true } [dev-dependencies] smol = { version = "^1.2" } @@ -47,10 +50,12 @@ tokio = { version = "^1.6", features = ["full"] } actix-rt = { version = "2.2.0" } maplit = { version = "^1" } rust_decimal_macros = { version = "^1" } +env_logger = { version = "^0.9" } sea-orm = { path = ".", features = ["debug-print"] } +pretty_assertions = { version = "^0.7" } [features] -debug-print = [] +debug-print = ["log"] default = [ "macros", "mock", diff --git a/README.md b/README.md index 524aa896..82696f29 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ let pear = fruit::ActiveModel { }; // insert one -let res: InsertResult = Fruit::insert(pear).exec(db).await?; +let res = Fruit::insert(pear).exec(db).await?; println!("InsertResult: {}", res.last_insert_id); diff --git a/examples/async-std/src/example_cake.rs b/examples/async-std/src/example_cake.rs index 475315e8..114347ee 100644 --- a/examples/async-std/src/example_cake.rs +++ b/examples/async-std/src/example_cake.rs @@ -27,6 +27,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/examples/async-std/src/example_cake_filling.rs b/examples/async-std/src/example_cake_filling.rs index 19de83e4..4fa188bc 100644 --- a/examples/async-std/src/example_cake_filling.rs +++ b/examples/async-std/src/example_cake_filling.rs @@ -28,6 +28,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/examples/async-std/src/example_filling.rs b/examples/async-std/src/example_filling.rs index 925b92fc..2a39a7de 100644 --- a/examples/async-std/src/example_filling.rs +++ b/examples/async-std/src/example_filling.rs @@ -27,6 +27,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/examples/async-std/src/example_fruit.rs b/examples/async-std/src/example_fruit.rs index b875da24..8802e707 100644 --- a/examples/async-std/src/example_fruit.rs +++ b/examples/async-std/src/example_fruit.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/examples/async-std/src/operation.rs b/examples/async-std/src/operation.rs index b1273e10..3fa5cc85 100644 --- a/examples/async-std/src/operation.rs +++ b/examples/async-std/src/operation.rs @@ -20,7 +20,7 @@ pub async fn insert_and_update(db: &DbConn) -> Result<(), DbErr> { name: Set("pear".to_owned()), ..Default::default() }; - let res: InsertResult = Fruit::insert(pear).exec(db).await?; + let res = Fruit::insert(pear).exec(db).await?; println!(); println!("Inserted: last_insert_id = {}\n", res.last_insert_id); diff --git a/examples/rocket_example/src/setup.rs b/examples/rocket_example/src/setup.rs index 92c3ca8c..034e8b53 100644 --- a/examples/rocket_example/src/setup.rs +++ b/examples/rocket_example/src/setup.rs @@ -2,32 +2,32 @@ use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; use sea_orm::{error::*, sea_query, DbConn, ExecResult}; async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result { - let builder = db.get_database_backend(); - db.execute(builder.build(stmt)).await + let builder = db.get_database_backend(); + db.execute(builder.build(stmt)).await } pub async fn create_post_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(super::post::Entity) - .if_not_exists() - .col( - ColumnDef::new(super::post::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col( - ColumnDef::new(super::post::Column::Title) - .string() - .not_null(), - ) - .col( - ColumnDef::new(super::post::Column::Text) - .string() - .not_null(), - ) - .to_owned(); + let stmt = sea_query::Table::create() + .table(super::post::Entity) + .if_not_exists() + .col( + ColumnDef::new(super::post::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(super::post::Column::Title) + .string() + .not_null(), + ) + .col( + ColumnDef::new(super::post::Column::Text) + .string() + .not_null(), + ) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } diff --git a/examples/tokio/src/cake.rs b/examples/tokio/src/cake.rs index 0b1a4439..21b83331 100644 --- a/examples/tokio/src/cake.rs +++ b/examples/tokio/src/cake.rs @@ -27,6 +27,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..521706a0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +format_code_in_doc_comments=true \ No newline at end of file diff --git a/sea-orm-codegen/src/entity/base_entity.rs b/sea-orm-codegen/src/entity/base_entity.rs index f1e03b21..7b28f9e4 100644 --- a/sea-orm-codegen/src/entity/base_entity.rs +++ b/sea-orm-codegen/src/entity/base_entity.rs @@ -117,6 +117,31 @@ impl Entity { format_ident!("{}", auto_increment) } + pub fn get_primary_key_rs_type(&self) -> TokenStream { + let types = self + .primary_keys + .iter() + .map(|primary_key| { + self.columns + .iter() + .find(|col| col.name.eq(&primary_key.name)) + .unwrap() + .get_rs_type() + .to_string() + }) + .collect::>(); + if !types.is_empty() { + let value_type = if types.len() > 1 { + vec!["(".to_owned(), types.join(", "), ")".to_owned()] + } else { + types + }; + value_type.join("").parse().unwrap() + } else { + TokenStream::new() + } + } + pub fn get_conjunct_relations_via_snake_case(&self) -> Vec { self.conjunct_relations .iter() @@ -151,7 +176,7 @@ mod tests { columns: vec![ Column { name: "id".to_owned(), - col_type: ColumnType::String(None), + col_type: ColumnType::Integer(None), auto_increment: false, not_null: false, unique: false, @@ -373,6 +398,16 @@ mod tests { ); } + #[test] + fn test_get_primary_key_rs_type() { + let entity = setup(); + + assert_eq!( + entity.get_primary_key_rs_type().to_string(), + entity.columns[0].get_rs_type().to_string() + ); + } + #[test] fn test_get_conjunct_relations_via_snake_case() { let entity = setup(); diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index a2c3b4f5..0b9eb8de 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -119,33 +119,18 @@ impl From<&ColumnDef> for Column { Some(ty) => ty.clone(), None => panic!("ColumnType should not be empty"), }; - let auto_increments: Vec = col_def + let auto_increment = col_def .get_column_spec() .iter() - .filter_map(|spec| match spec { - ColumnSpec::AutoIncrement => Some(true), - _ => None, - }) - .collect(); - let auto_increment = !auto_increments.is_empty(); - let not_nulls: Vec = col_def + .any(|spec| matches!(spec, ColumnSpec::AutoIncrement)); + let not_null = col_def .get_column_spec() .iter() - .filter_map(|spec| match spec { - ColumnSpec::NotNull => Some(true), - _ => None, - }) - .collect(); - let not_null = !not_nulls.is_empty(); - let uniques: Vec = col_def + .any(|spec| matches!(spec, ColumnSpec::NotNull)); + let unique = col_def .get_column_spec() .iter() - .filter_map(|spec| match spec { - ColumnSpec::UniqueKey => Some(true), - _ => None, - }) - .collect(); - let unique = !uniques.is_empty(); + .any(|spec| matches!(spec, ColumnSpec::UniqueKey)); Self { name, col_type, diff --git a/sea-orm-codegen/src/entity/transformer.rs b/sea-orm-codegen/src/entity/transformer.rs index 75dc25c7..840eade0 100644 --- a/sea-orm-codegen/src/entity/transformer.rs +++ b/sea-orm-codegen/src/entity/transformer.rs @@ -34,11 +34,6 @@ impl EntityTransformer { .iter() .map(|col_def| col_def.into()) .collect(); - let unique_columns: Vec = columns - .iter() - .filter(|col| col.unique) - .map(|col| col.name.clone()) - .collect(); let relations = table_create .get_foreign_key_create_stmts() .iter() @@ -85,8 +80,13 @@ impl EntityTransformer { false => { let ref_table = rel.ref_table; let mut unique = true; - for col in rel.columns.iter() { - if !unique_columns.contains(col) { + for column in rel.columns.iter() { + if !entity + .columns + .iter() + .filter(|col| col.unique) + .any(|col| col.name.as_str() == column) + { unique = false; break; } diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 207864ba..19e9af1c 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -173,8 +173,11 @@ impl EntityWriter { pub fn gen_impl_primary_key(entity: &Entity) -> TokenStream { let primary_key_auto_increment = entity.get_primary_key_auto_increment(); + let value_type = entity.get_primary_key_rs_type(); quote! { impl PrimaryKeyTrait for PrimaryKey { + type ValueType = #value_type; + fn auto_increment() -> bool { #primary_key_auto_increment } @@ -305,7 +308,7 @@ mod tests { use sea_query::ColumnType; use std::io::{self, BufRead, BufReader}; - const ENTITY_FILES: [&'static str; 5] = [ + const ENTITY_FILES: [&str; 5] = [ include_str!("../../tests/entity/cake.rs"), include_str!("../../tests/entity/cake_filling.rs"), include_str!("../../tests/entity/filling.rs"), diff --git a/sea-orm-codegen/tests/entity/cake.rs b/sea-orm-codegen/tests/entity/cake.rs index 29f55ac6..55fa279f 100644 --- a/sea-orm-codegen/tests/entity/cake.rs +++ b/sea-orm-codegen/tests/entity/cake.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-codegen/tests/entity/cake_filling.rs b/sea-orm-codegen/tests/entity/cake_filling.rs index d0f00560..d100fa8c 100644 --- a/sea-orm-codegen/tests/entity/cake_filling.rs +++ b/sea-orm-codegen/tests/entity/cake_filling.rs @@ -30,6 +30,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/sea-orm-codegen/tests/entity/filling.rs b/sea-orm-codegen/tests/entity/filling.rs index bedb1ab4..73d58152 100644 --- a/sea-orm-codegen/tests/entity/filling.rs +++ b/sea-orm-codegen/tests/entity/filling.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-codegen/tests/entity/fruit.rs b/sea-orm-codegen/tests/entity/fruit.rs index 72c37c1b..12919021 100644 --- a/sea-orm-codegen/tests/entity/fruit.rs +++ b/sea-orm-codegen/tests/entity/fruit.rs @@ -31,6 +31,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-codegen/tests/entity/vendor.rs b/sea-orm-codegen/tests/entity/vendor.rs index 320400f4..ffc211a8 100644 --- a/sea-orm-codegen/tests/entity/vendor.rs +++ b/sea-orm-codegen/tests/entity/vendor.rs @@ -31,6 +31,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index a2217aa8..a9abf8b2 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -99,6 +99,10 @@ pub fn test(_: TokenStream, input: TokenStream) -> TokenStream { #[test] #(#attrs)* fn #name() #ret { + let _ = ::env_logger::builder() + .filter_level(::log::LevelFilter::Debug) + .is_test(true) + .try_init(); crate::block_on!(async { #body }) } ) diff --git a/src/database/connection.rs b/src/database/connection.rs index 3d4215f5..6a39b240 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -75,7 +75,7 @@ impl DatabaseConnection { DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.execute(stmt).await, #[cfg(feature = "mock")] DatabaseConnection::MockDatabaseConnection(conn) => conn.execute(stmt).await, - DatabaseConnection::Disconnected => panic!("Disconnected"), + DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())), } } @@ -89,7 +89,7 @@ impl DatabaseConnection { DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_one(stmt).await, #[cfg(feature = "mock")] DatabaseConnection::MockDatabaseConnection(conn) => conn.query_one(stmt).await, - DatabaseConnection::Disconnected => panic!("Disconnected"), + DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())), } } @@ -103,7 +103,7 @@ impl DatabaseConnection { DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_all(stmt).await, #[cfg(feature = "mock")] DatabaseConnection::MockDatabaseConnection(conn) => conn.query_all(stmt).await, - DatabaseConnection::Disconnected => panic!("Disconnected"), + DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())), } } diff --git a/src/driver/mock.rs b/src/driver/mock.rs index 0a1629bf..823ddb32 100644 --- a/src/driver/mock.rs +++ b/src/driver/mock.rs @@ -2,19 +2,22 @@ use crate::{ debug_print, error::*, DatabaseConnection, DbBackend, ExecResult, MockDatabase, QueryResult, Statement, Transaction, }; +use std::fmt::Debug; use std::sync::{ atomic::{AtomicUsize, Ordering}, Mutex, }; +#[derive(Debug)] pub struct MockDatabaseConnector; +#[derive(Debug)] pub struct MockDatabaseConnection { counter: AtomicUsize, mocker: Mutex>, } -pub trait MockDatabaseTrait: Send { +pub trait MockDatabaseTrait: Send + Debug { fn execute(&mut self, counter: usize, stmt: Statement) -> Result; fn query(&mut self, counter: usize, stmt: Statement) -> Result, DbErr>; diff --git a/src/driver/sqlx_mysql.rs b/src/driver/sqlx_mysql.rs index 48da88a0..e91df037 100644 --- a/src/driver/sqlx_mysql.rs +++ b/src/driver/sqlx_mysql.rs @@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement}; use super::sqlx_common::*; +#[derive(Debug)] pub struct SqlxMySqlConnector; +#[derive(Debug)] pub struct SqlxMySqlPoolConnection { pool: MySqlPool, } diff --git a/src/driver/sqlx_postgres.rs b/src/driver/sqlx_postgres.rs index a283ddf3..086dc995 100644 --- a/src/driver/sqlx_postgres.rs +++ b/src/driver/sqlx_postgres.rs @@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement}; use super::sqlx_common::*; +#[derive(Debug)] pub struct SqlxPostgresConnector; +#[derive(Debug)] pub struct SqlxPostgresPoolConnection { pool: PgPool, } @@ -102,24 +104,11 @@ impl From for QueryResult { impl From for ExecResult { fn from(result: PgQueryResult) -> ExecResult { ExecResult { - result: ExecResultHolder::SqlxPostgres { - last_insert_id: 0, - rows_affected: result.rows_affected(), - }, + result: ExecResultHolder::SqlxPostgres(result), } } } -pub(crate) fn query_result_into_exec_result(res: QueryResult) -> Result { - let last_insert_id: i32 = res.try_get("", "last_insert_id")?; - Ok(ExecResult { - result: ExecResultHolder::SqlxPostgres { - last_insert_id: last_insert_id as u64, - rows_affected: 0, - }, - }) -} - fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Postgres, PgArguments> { let mut query = sqlx::query(&stmt.sql); if let Some(values) = &stmt.values { diff --git a/src/driver/sqlx_sqlite.rs b/src/driver/sqlx_sqlite.rs index ac275b72..5fa4bdcd 100644 --- a/src/driver/sqlx_sqlite.rs +++ b/src/driver/sqlx_sqlite.rs @@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement}; use super::sqlx_common::*; +#[derive(Debug)] pub struct SqlxSqliteConnector; +#[derive(Debug)] pub struct SqlxSqlitePoolConnection { pool: SqlitePool, } diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index 54bf037b..f823411c 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -221,16 +221,18 @@ where let exec = E::insert(am).exec(db); let res = exec.await?; // TODO: if the entity does not have auto increment primary key, then last_insert_id is a wrong value - if ::auto_increment() && res.last_insert_id != 0 { + // FIXME: Assumed valid last_insert_id is not equals to Default::default() + if ::auto_increment() + && res.last_insert_id != ::ValueType::default() + { let find = E::find_by_id(res.last_insert_id).one(db); let found = find.await; let model: Option = found?; match model { Some(model) => Ok(model.into_active_model()), None => Err(DbErr::Exec(format!( - "Failed to find inserted item: {} {}", + "Failed to find inserted item: {}", E::default().to_string(), - res.last_insert_id ))), } } else { diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index d0740307..67f817a7 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -55,21 +55,21 @@ pub trait EntityTrait: EntityName { where R: EntityTrait, { - RelationBuilder::new(RelationType::HasOne, Self::default(), related) + RelationBuilder::new(RelationType::HasOne, Self::default(), related, false) } fn has_one(_: R) -> RelationBuilder where R: EntityTrait + Related, { - RelationBuilder::from_rel(RelationType::HasOne, R::to().rev()) + RelationBuilder::from_rel(RelationType::HasOne, R::to().rev(), true) } fn has_many(_: R) -> RelationBuilder where R: EntityTrait + Related, { - RelationBuilder::from_rel(RelationType::HasMany, R::to().rev()) + RelationBuilder::from_rel(RelationType::HasMany, R::to().rev(), true) } /// Construct select statement to find one / all models @@ -137,13 +137,18 @@ pub trait EntityTrait: EntityName { /// assert_eq!( /// db.into_transaction_log(), /// vec![ - /// Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, vec![1u64.into()] - /// ), - /// Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] - /// ), - /// ]); + /// Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, + /// vec![1u64.into()] + /// ), + /// Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + /// vec![] + /// ), + /// ] + /// ); /// ``` fn find() -> Select { Select::new() @@ -186,8 +191,11 @@ pub trait EntityTrait: EntityName { /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()] - /// )]); + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, + /// vec![11i32.into()] + /// )] + /// ); /// ``` /// Find by composite key /// ``` diff --git a/src/entity/column.rs b/src/entity/column.rs index 611950f5..baaf190c 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -103,7 +103,7 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr { /// /// assert_eq!( /// cake::Entity::find() - /// .filter(cake::Column::Id.between(2,3)) + /// .filter(cake::Column::Id.between(2, 3)) /// .build(DbBackend::MySql) /// .to_string(), /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` BETWEEN 2 AND 3" @@ -121,7 +121,7 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr { /// /// assert_eq!( /// cake::Entity::find() - /// .filter(cake::Column::Id.not_between(2,3)) + /// .filter(cake::Column::Id.not_between(2, 3)) /// .build(DbBackend::MySql) /// .to_string(), /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` NOT BETWEEN 2 AND 3" diff --git a/src/entity/identity.rs b/src/entity/identity.rs index d1cc3170..d8623b7d 100644 --- a/src/entity/identity.rs +++ b/src/entity/identity.rs @@ -1,5 +1,6 @@ use crate::{ColumnTrait, EntityTrait, IdenStatic}; -use sea_query::{DynIden, IntoIden}; +use sea_query::{Alias, DynIden, Iden, IntoIden, SeaRc}; +use std::fmt; #[derive(Debug, Clone)] pub enum Identity { @@ -8,6 +9,25 @@ pub enum Identity { Ternary(DynIden, DynIden, DynIden), } +impl Iden for Identity { + fn unquoted(&self, s: &mut dyn fmt::Write) { + match self { + Identity::Unary(iden) => { + write!(s, "{}", iden.to_string()).unwrap(); + } + Identity::Binary(iden1, iden2) => { + write!(s, "{}", iden1.to_string()).unwrap(); + write!(s, "{}", iden2.to_string()).unwrap(); + } + Identity::Ternary(iden1, iden2, iden3) => { + write!(s, "{}", iden1.to_string()).unwrap(); + write!(s, "{}", iden2.to_string()).unwrap(); + write!(s, "{}", iden3.to_string()).unwrap(); + } + } + } +} + pub trait IntoIdentity { fn into_identity(self) -> Identity; } @@ -19,6 +39,18 @@ where fn identity_of(self) -> Identity; } +impl IntoIdentity for String { + fn into_identity(self) -> Identity { + self.as_str().into_identity() + } +} + +impl IntoIdentity for &str { + fn into_identity(self) -> Identity { + Identity::Unary(SeaRc::new(Alias::new(self))) + } +} + impl IntoIdentity for T where T: IdenStatic, diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 6da5e1b6..47eb4fa5 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -6,6 +6,7 @@ mod model; pub mod prelude; mod primary_key; mod relation; +mod schema; pub use active_model::*; pub use base_entity::*; @@ -15,3 +16,4 @@ pub use model::*; // pub use prelude::*; pub use primary_key::*; pub use relation::*; +pub use schema::*; diff --git a/src/entity/model.rs b/src/entity/model.rs index 15ebdb58..4774e1dc 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -1,4 +1,4 @@ -use crate::{DbErr, EntityTrait, QueryFilter, QueryResult, Related, Select}; +use crate::{DbErr, EntityTrait, Linked, QueryFilter, QueryResult, Related, Select}; pub use sea_query::Value; use std::fmt::Debug; @@ -16,6 +16,13 @@ pub trait ModelTrait: Clone + Debug { { >::find_related().belongs_to(self) } + + fn find_linked(&self, l: L) -> Select + where + L: Linked, + { + l.find_linked() + } } pub trait FromQueryResult { diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 447117b7..6720f916 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -1,9 +1,9 @@ pub use crate::{ error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType, DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, - DeriveModel, DerivePrimaryKey, EntityName, EntityTrait, EnumIter, Iden, IdenStatic, ModelTrait, - PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef, - RelationTrait, Select, Value, + DeriveModel, DerivePrimaryKey, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden, + IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, + Related, RelationDef, RelationTrait, Select, Value, }; #[cfg(feature = "with-json")] diff --git a/src/entity/primary_key.rs b/src/entity/primary_key.rs index 81a28915..b9c381d4 100644 --- a/src/entity/primary_key.rs +++ b/src/entity/primary_key.rs @@ -1,7 +1,18 @@ use super::{ColumnTrait, IdenStatic, Iterable}; +use crate::{TryFromU64, TryGetableMany}; +use sea_query::IntoValueTuple; +use std::fmt::Debug; //LINT: composite primary key cannot auto increment pub trait PrimaryKeyTrait: IdenStatic + Iterable { + type ValueType: Sized + + Default + + Debug + + PartialEq + + IntoValueTuple + + TryGetableMany + + TryFromU64; + fn auto_increment() -> bool; } diff --git a/src/entity/relation.rs b/src/entity/relation.rs index 955660e2..7b2d7b4d 100644 --- a/src/entity/relation.rs +++ b/src/entity/relation.rs @@ -9,6 +9,8 @@ pub enum RelationType { HasMany, } +pub type ForeignKeyAction = sea_query::ForeignKeyAction; + pub trait RelationTrait: Iterable + Debug + 'static { fn def(&self) -> RelationDef; } @@ -28,14 +30,35 @@ where } } +pub trait Linked { + type FromEntity: EntityTrait; + + type ToEntity: EntityTrait; + + fn link(&self) -> Vec; + + fn find_linked(&self) -> Select { + let mut select = Select::new(); + for rel in self.link().into_iter().rev() { + select = select.join_rev(JoinType::InnerJoin, rel); + } + select + } +} + +#[derive(Debug)] pub struct RelationDef { pub rel_type: RelationType, pub from_tbl: TableRef, pub to_tbl: TableRef, pub from_col: Identity, pub to_col: Identity, + pub is_owner: bool, + pub on_delete: Option, + pub on_update: Option, } +#[derive(Debug)] pub struct RelationBuilder where E: EntityTrait, @@ -47,6 +70,9 @@ where to_tbl: TableRef, from_col: Option, to_col: Option, + is_owner: bool, + on_delete: Option, + on_update: Option, } impl RelationDef { @@ -58,6 +84,9 @@ impl RelationDef { to_tbl: self.from_tbl, from_col: self.to_col, to_col: self.from_col, + is_owner: !self.is_owner, + on_delete: self.on_delete, + on_update: self.on_update, } } } @@ -67,7 +96,7 @@ where E: EntityTrait, R: EntityTrait, { - pub(crate) fn new(rel_type: RelationType, from: E, to: R) -> Self { + pub(crate) fn new(rel_type: RelationType, from: E, to: R, is_owner: bool) -> Self { Self { entities: PhantomData, rel_type, @@ -75,10 +104,13 @@ where to_tbl: to.table_ref(), from_col: None, to_col: None, + is_owner, + on_delete: None, + on_update: None, } } - pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef) -> Self { + pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef, is_owner: bool) -> Self { Self { entities: PhantomData, rel_type, @@ -86,6 +118,9 @@ where to_tbl: rel.to_tbl, from_col: Some(rel.from_col), to_col: Some(rel.to_col), + is_owner, + on_delete: None, + on_update: None, } } @@ -104,6 +139,16 @@ where self.to_col = Some(identifier.identity_of()); self } + + pub fn on_delete(mut self, action: ForeignKeyAction) -> Self { + self.on_delete = Some(action); + self + } + + pub fn on_update(mut self, action: ForeignKeyAction) -> Self { + self.on_update = Some(action); + self + } } impl From> for RelationDef @@ -118,6 +163,9 @@ where to_tbl: b.to_tbl, from_col: b.from_col.unwrap(), to_col: b.to_col.unwrap(), + is_owner: b.is_owner, + on_delete: b.on_delete, + on_update: b.on_update, } } } diff --git a/src/entity/schema.rs b/src/entity/schema.rs new file mode 100644 index 00000000..06c688a8 --- /dev/null +++ b/src/entity/schema.rs @@ -0,0 +1,161 @@ +use crate::{ + unpack_table_ref, ColumnTrait, EntityTrait, Identity, Iterable, PrimaryKeyToColumn, + PrimaryKeyTrait, RelationTrait, +}; +use sea_query::{ColumnDef, ForeignKeyCreateStatement, Iden, Index, TableCreateStatement}; + +pub fn entity_to_table_create_statement(entity: E) -> TableCreateStatement +where + E: EntityTrait, +{ + let mut stmt = TableCreateStatement::new(); + + for column in E::Column::iter() { + let orm_column_def = column.def(); + let types = orm_column_def.col_type.into(); + let mut column_def = ColumnDef::new_with_type(column, types); + if !orm_column_def.null { + column_def.not_null(); + } + if orm_column_def.unique { + column_def.unique_key(); + } + for primary_key in E::PrimaryKey::iter() { + if column.to_string() == primary_key.into_column().to_string() { + if E::PrimaryKey::auto_increment() { + column_def.auto_increment(); + } + if E::PrimaryKey::iter().count() == 1 { + column_def.primary_key(); + } + } + } + if orm_column_def.indexed { + stmt.index( + Index::create() + .name(&format!( + "idx-{}-{}", + entity.to_string(), + column.to_string() + )) + .table(entity) + .col(column), + ); + } + stmt.col(&mut column_def); + } + + if E::PrimaryKey::iter().count() > 1 { + let mut idx_pk = Index::create(); + for primary_key in E::PrimaryKey::iter() { + idx_pk.col(primary_key); + } + stmt.primary_key(idx_pk.name(&format!("pk-{}", entity.to_string())).primary()); + } + + for relation in E::Relation::iter() { + let relation = relation.def(); + if relation.is_owner { + continue; + } + let mut foreign_key_stmt = ForeignKeyCreateStatement::new(); + let from_tbl = unpack_table_ref(&relation.from_tbl); + let to_tbl = unpack_table_ref(&relation.to_tbl); + match relation.from_col { + Identity::Unary(o1) => { + foreign_key_stmt.from_col(o1); + } + Identity::Binary(o1, o2) => { + foreign_key_stmt.from_col(o1); + foreign_key_stmt.from_col(o2); + } + Identity::Ternary(o1, o2, o3) => { + foreign_key_stmt.from_col(o1); + foreign_key_stmt.from_col(o2); + foreign_key_stmt.from_col(o3); + } + } + match relation.to_col { + Identity::Unary(o1) => { + foreign_key_stmt.to_col(o1); + } + Identity::Binary(o1, o2) => { + foreign_key_stmt.to_col(o1); + foreign_key_stmt.to_col(o2); + } + crate::Identity::Ternary(o1, o2, o3) => { + foreign_key_stmt.to_col(o1); + foreign_key_stmt.to_col(o2); + foreign_key_stmt.to_col(o3); + } + } + if let Some(action) = relation.on_delete { + foreign_key_stmt.on_delete(action); + } + if let Some(action) = relation.on_update { + foreign_key_stmt.on_update(action); + } + stmt.foreign_key( + foreign_key_stmt + .name(&format!( + "fk-{}-{}", + from_tbl.to_string(), + to_tbl.to_string() + )) + .from_tbl(from_tbl) + .to_tbl(to_tbl), + ); + } + + stmt.table(entity).if_not_exists().take() +} + +#[cfg(test)] +mod tests { + use crate::{entity_to_table_create_statement, tests_cfg::*}; + use pretty_assertions::assert_eq; + use sea_query::*; + + #[test] + fn test_entity_to_table_create_statement() { + assert_eq!( + entity_to_table_create_statement(CakeFillingPrice).to_string(MysqlQueryBuilder), + Table::create() + .table(CakeFillingPrice) + .if_not_exists() + .col( + ColumnDef::new(cake_filling_price::Column::CakeId) + .integer() + .not_null() + ) + .col( + ColumnDef::new(cake_filling_price::Column::FillingId) + .integer() + .not_null() + ) + .col( + ColumnDef::new(cake_filling_price::Column::Price) + .decimal() + .not_null() + ) + .primary_key( + Index::create() + .name("pk-cake_filling_price") + .col(cake_filling_price::Column::CakeId) + .col(cake_filling_price::Column::FillingId) + .primary() + ) + .foreign_key( + ForeignKeyCreateStatement::new() + .name("fk-cake_filling_price-cake_filling") + .from_tbl(CakeFillingPrice) + .from_col(cake_filling_price::Column::CakeId) + .from_col(cake_filling_price::Column::FillingId) + .to_tbl(CakeFilling) + .to_col(cake_filling::Column::CakeId) + .to_col(cake_filling::Column::FillingId) + ) + .to_string(MysqlQueryBuilder) + ); + } +} diff --git a/src/error.rs b/src/error.rs index 8a695dac..09f80b0a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum DbErr { Conn(String), Exec(String), diff --git a/src/executor/execute.rs b/src/executor/execute.rs index 00375bb7..46ba2d69 100644 --- a/src/executor/execute.rs +++ b/src/executor/execute.rs @@ -8,10 +8,7 @@ pub(crate) enum ExecResultHolder { #[cfg(feature = "sqlx-mysql")] SqlxMySql(sqlx::mysql::MySqlQueryResult), #[cfg(feature = "sqlx-postgres")] - SqlxPostgres { - last_insert_id: u64, - rows_affected: u64, - }, + SqlxPostgres(sqlx::postgres::PgQueryResult), #[cfg(feature = "sqlx-sqlite")] SqlxSqlite(sqlx::sqlite::SqliteQueryResult), #[cfg(feature = "mock")] @@ -26,7 +23,9 @@ impl ExecResult { #[cfg(feature = "sqlx-mysql")] ExecResultHolder::SqlxMySql(result) => result.last_insert_id(), #[cfg(feature = "sqlx-postgres")] - ExecResultHolder::SqlxPostgres { last_insert_id, .. } => last_insert_id.to_owned(), + ExecResultHolder::SqlxPostgres(_) => { + panic!("Should not retrieve last_insert_id this way") + } #[cfg(feature = "sqlx-sqlite")] ExecResultHolder::SqlxSqlite(result) => { let last_insert_rowid = result.last_insert_rowid(); @@ -46,7 +45,7 @@ impl ExecResult { #[cfg(feature = "sqlx-mysql")] ExecResultHolder::SqlxMySql(result) => result.rows_affected(), #[cfg(feature = "sqlx-postgres")] - ExecResultHolder::SqlxPostgres { rows_affected, .. } => rows_affected.to_owned(), + ExecResultHolder::SqlxPostgres(result) => result.rows_affected(), #[cfg(feature = "sqlx-sqlite")] ExecResultHolder::SqlxSqlite(result) => result.rows_affected(), #[cfg(feature = "mock")] diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 9414fea6..90065675 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -1,15 +1,25 @@ -use crate::{error::*, ActiveModelTrait, DatabaseConnection, Insert, Statement}; +use crate::{ + error::*, ActiveModelTrait, DatabaseConnection, EntityTrait, Insert, PrimaryKeyTrait, + Statement, TryFromU64, +}; use sea_query::InsertStatement; -use std::future::Future; +use std::{future::Future, marker::PhantomData}; #[derive(Clone, Debug)] -pub struct Inserter { +pub struct Inserter +where + A: ActiveModelTrait, +{ query: InsertStatement, + model: PhantomData, } -#[derive(Clone, Debug)] -pub struct InsertResult { - pub last_insert_id: u64, +#[derive(Debug)] +pub struct InsertResult +where + A: ActiveModelTrait, +{ + pub last_insert_id: <<::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType, } impl Insert @@ -17,54 +27,79 @@ where A: ActiveModelTrait, { #[allow(unused_mut)] - pub fn exec( + pub fn exec<'a>( self, - db: &DatabaseConnection, - ) -> impl Future> + '_ { + db: &'a DatabaseConnection, + ) -> impl Future, DbErr>> + 'a + where + A: 'a, + { // so that self is dropped before entering await let mut query = self.query; #[cfg(feature = "sqlx-postgres")] if let DatabaseConnection::SqlxPostgresPoolConnection(_) = db { - use crate::{EntityTrait, Iterable}; - use sea_query::{Alias, Expr, Query}; - for key in ::PrimaryKey::iter() { + use crate::{sea_query::Query, Iterable}; + if ::PrimaryKey::iter().count() > 0 { query.returning( Query::select() - .expr_as(Expr::col(key), Alias::new("last_insert_id")) - .to_owned(), + .columns(::PrimaryKey::iter()) + .take(), ); } } - Inserter::new(query).exec(db) + Inserter::::new(query).exec(db) } } -impl Inserter { +impl Inserter +where + A: ActiveModelTrait, +{ pub fn new(query: InsertStatement) -> Self { - Self { query } + Self { + query, + model: PhantomData, + } } - pub fn exec( + pub fn exec<'a>( self, - db: &DatabaseConnection, - ) -> impl Future> + '_ { + db: &'a DatabaseConnection, + ) -> impl Future, DbErr>> + 'a + where + A: 'a, + { let builder = db.get_database_backend(); exec_insert(builder.build(&self.query), db) } } // Only Statement impl Send -async fn exec_insert(statement: Statement, db: &DatabaseConnection) -> Result { - // TODO: Postgres instead use query_one + returning clause - let result = match db { +async fn exec_insert( + statement: Statement, + db: &DatabaseConnection, +) -> Result, DbErr> +where + A: ActiveModelTrait, +{ + type PrimaryKey = <::Entity as EntityTrait>::PrimaryKey; + type ValueTypeOf = as PrimaryKeyTrait>::ValueType; + let last_insert_id = match db { #[cfg(feature = "sqlx-postgres")] DatabaseConnection::SqlxPostgresPoolConnection(conn) => { + use crate::{sea_query::Iden, Iterable}; + let cols = PrimaryKey::::iter() + .map(|col| col.to_string()) + .collect::>(); let res = conn.query_one(statement).await?.unwrap(); - crate::query_result_into_exec_result(res)? + res.try_get_many("", cols.as_ref()).unwrap_or_default() + } + _ => { + let last_insert_id = db.execute(statement).await?.last_insert_id(); + ValueTypeOf::::try_from_u64(last_insert_id) + .ok() + .unwrap_or_default() } - _ => db.execute(statement).await?, }; - Ok(InsertResult { - last_insert_id: result.last_insert_id(), - }) + Ok(InsertResult { last_insert_id }) } diff --git a/src/executor/query.rs b/src/executor/query.rs index d880063d..c30daf48 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -17,6 +17,11 @@ pub(crate) enum QueryResultRow { Mock(crate::MockRow), } +pub trait TryGetable: Sized { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result; +} + +#[derive(Debug)] pub enum TryGetError { DbErr(DbErr), Null, @@ -31,12 +36,6 @@ impl From for DbErr { } } -pub trait TryGetable { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result - where - Self: Sized; -} - // QueryResult // impl QueryResult { @@ -46,6 +45,13 @@ impl QueryResult { { Ok(T::try_get(self, pre, col)?) } + + pub fn try_get_many(&self, pre: &str, cols: &[String]) -> Result + where + T: TryGetableMany, + { + Ok(T::try_get_many(self, pre, cols)?) + } } impl fmt::Debug for QueryResultRow { @@ -54,9 +60,9 @@ impl fmt::Debug for QueryResultRow { #[cfg(feature = "sqlx-mysql")] Self::SqlxMySql(row) => write!(f, "{:?}", row), #[cfg(feature = "sqlx-postgres")] - Self::SqlxPostgres(_) => panic!("QueryResultRow::SqlxPostgres cannot be inspected"), + Self::SqlxPostgres(_) => write!(f, "QueryResultRow::SqlxPostgres cannot be inspected"), #[cfg(feature = "sqlx-sqlite")] - Self::SqlxSqlite(_) => panic!("QueryResultRow::SqlxSqlite cannot be inspected"), + Self::SqlxSqlite(_) => write!(f, "QueryResultRow::SqlxSqlite cannot be inspected"), #[cfg(feature = "mock")] Self::Mock(row) => write!(f, "{:?}", row), } @@ -288,3 +294,135 @@ impl TryGetable for Decimal { #[cfg(feature = "with-uuid")] try_getable_all!(uuid::Uuid); + +// TryGetableMany // + +pub trait TryGetableMany: Sized { + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result; +} + +impl TryGetableMany for T +where + T: TryGetable, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + try_get_many_with_slice_len_of(1, cols)?; + T::try_get(res, pre, &cols[0]) + } +} + +impl TryGetableMany for (T, T) +where + T: TryGetable, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + try_get_many_with_slice_len_of(2, cols)?; + Ok(( + T::try_get(res, pre, &cols[0])?, + T::try_get(res, pre, &cols[1])?, + )) + } +} + +impl TryGetableMany for (T, T, T) +where + T: TryGetable, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + try_get_many_with_slice_len_of(3, cols)?; + Ok(( + T::try_get(res, pre, &cols[0])?, + T::try_get(res, pre, &cols[1])?, + T::try_get(res, pre, &cols[2])?, + )) + } +} + +fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> { + if cols.len() < len { + Err(TryGetError::DbErr(DbErr::Query(format!( + "Expect {} column names supplied but got slice of length {}", + len, + cols.len() + )))) + } else { + Ok(()) + } +} + +// TryFromU64 // + +pub trait TryFromU64: Sized { + fn try_from_u64(n: u64) -> Result; +} + +macro_rules! try_from_u64_err { + ( $type: ty ) => { + impl TryFromU64 for $type { + fn try_from_u64(_: u64) -> Result { + Err(DbErr::Exec(format!( + "{} cannot be converted from u64", + stringify!($type) + ))) + } + } + }; +} + +macro_rules! try_from_u64_tuple { + ( $type: ty ) => { + try_from_u64_err!(($type, $type)); + try_from_u64_err!(($type, $type, $type)); + }; +} + +macro_rules! try_from_u64_numeric { + ( $type: ty ) => { + impl TryFromU64 for $type { + fn try_from_u64(n: u64) -> Result { + use std::convert::TryInto; + n.try_into().map_err(|_| { + DbErr::Exec(format!( + "fail to convert '{}' into '{}'", + n, + stringify!($type) + )) + }) + } + } + try_from_u64_tuple!($type); + }; +} + +try_from_u64_numeric!(i8); +try_from_u64_numeric!(i16); +try_from_u64_numeric!(i32); +try_from_u64_numeric!(i64); +try_from_u64_numeric!(u8); +try_from_u64_numeric!(u16); +try_from_u64_numeric!(u32); +try_from_u64_numeric!(u64); + +macro_rules! try_from_u64_string { + ( $type: ty ) => { + impl TryFromU64 for $type { + fn try_from_u64(n: u64) -> Result { + Ok(n.to_string()) + } + } + try_from_u64_tuple!($type); + }; +} + +try_from_u64_string!(String); + +macro_rules! try_from_u64_dummy { + ( $type: ty ) => { + try_from_u64_err!($type); + try_from_u64_err!(($type, $type)); + try_from_u64_err!(($type, $type, $type)); + }; +} + +#[cfg(feature = "with-uuid")] +try_from_u64_dummy!(uuid::Uuid); diff --git a/src/executor/select.rs b/src/executor/select.rs index 6196ec73..ef30bb3d 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -1,6 +1,6 @@ use crate::{ - error::*, query::combine, DatabaseConnection, EntityTrait, FromQueryResult, Iterable, - JsonValue, ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectTwo, + error::*, DatabaseConnection, EntityTrait, FromQueryResult, IdenStatic, Iterable, JsonValue, + ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo, SelectTwoMany, Statement, }; use sea_query::SelectStatement; @@ -30,6 +30,7 @@ pub trait SelectorTrait { fn from_raw_query_result(res: QueryResult) -> Result; } +#[derive(Debug)] pub struct SelectModel where M: FromQueryResult, @@ -66,8 +67,8 @@ where fn from_raw_query_result(res: QueryResult) -> Result { Ok(( - M::from_query_result(&res, combine::SELECT_A)?, - N::from_query_result_optional(&res, combine::SELECT_B)?, + M::from_query_result(&res, SelectA.as_str())?, + N::from_query_result_optional(&res, SelectB.as_str())?, )) } } @@ -128,7 +129,7 @@ where E: EntityTrait, F: EntityTrait, { - fn into_model(self) -> Selector> + pub fn into_model(self) -> Selector> where M: FromQueryResult, N: FromQueryResult, @@ -289,14 +290,15 @@ where /// /// # let _: Result<(), DbErr> = smol::block_on(async { /// # - /// let res: Vec = cake::Entity::find().from_raw_sql( - /// Statement::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![] - /// ) - /// ) - /// .into_model::() - /// .all(&db) - /// .await?; + /// let res: Vec = cake::Entity::find() + /// .from_raw_sql(Statement::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, + /// vec![], + /// )) + /// .into_model::() + /// .all(&db) + /// .await?; /// /// assert_eq!( /// res, @@ -317,11 +319,12 @@ where /// /// assert_eq!( /// db.into_transaction_log(), - /// vec![ - /// Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![] - /// ), - /// ]); + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, + /// vec![] + /// ),] + /// ); /// ``` pub fn into_model(self) -> SelectorRaw> where @@ -406,22 +409,26 @@ where /// /// # let _: Result<(), DbErr> = smol::block_on(async { /// # - /// let _: Option = cake::Entity::find().from_raw_sql( - /// Statement::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()] - /// ) - /// ).one(&db).await?; + /// let _: Option = cake::Entity::find() + /// .from_raw_sql(Statement::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, + /// vec![1.into()], + /// )) + /// .one(&db) + /// .await?; /// # /// # Ok(()) /// # }); /// /// assert_eq!( /// db.into_transaction_log(), - /// vec![ - /// Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()] - /// ), - /// ]); + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, + /// vec![1.into()] + /// ),] + /// ); /// ``` pub async fn one(self, db: &DatabaseConnection) -> Result, DbErr> { let row = db.query_one(self.stmt).await?; @@ -441,22 +448,26 @@ where /// /// # let _: Result<(), DbErr> = smol::block_on(async { /// # - /// let _: Vec = cake::Entity::find().from_raw_sql( - /// Statement::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] - /// ) - /// ).all(&db).await?; + /// let _: Vec = cake::Entity::find() + /// .from_raw_sql(Statement::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + /// vec![], + /// )) + /// .all(&db) + /// .await?; /// # /// # Ok(()) /// # }); /// /// assert_eq!( /// db.into_transaction_log(), - /// vec![ - /// Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] - /// ), - /// ]); + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, + /// vec![] + /// ),] + /// ); /// ``` pub async fn all(self, db: &DatabaseConnection) -> Result, DbErr> { let rows = db.query_all(self.stmt).await?; diff --git a/src/lib.rs b/src/lib.rs index 7a20d851..24022090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![deny( + missing_debug_implementations, + clippy::print_stderr, + clippy::print_stdout +)] + //!
//! //! @@ -70,10 +77,8 @@ //! let fruits: Vec = cheese.find_related(Fruit).all(db).await?; //! //! // find related models (eager) -//! let cake_with_fruits: Vec<(cake::Model, Vec)> = Cake::find() -//! .find_with_related(Fruit) -//! .all(db) -//! .await?; +//! let cake_with_fruits: Vec<(cake::Model, Vec)> = +//! Cake::find().find_with_related(Fruit).all(db).await?; //! //! # Ok(()) //! # } @@ -93,7 +98,7 @@ //! }; //! //! // insert one -//! let res: InsertResult = Fruit::insert(pear).exec(db).await?; +//! let res = Fruit::insert(pear).exec(db).await?; //! //! println!("InsertResult: {}", res.last_insert_id); //! # Ok(()) diff --git a/src/query/combine.rs b/src/query/combine.rs index 8cce0510..0c0f151f 100644 --- a/src/query/combine.rs +++ b/src/query/combine.rs @@ -1,10 +1,31 @@ -use crate::{EntityTrait, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany}; +use crate::{ + EntityTrait, IdenStatic, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany, +}; use core::marker::PhantomData; pub use sea_query::JoinType; use sea_query::{Alias, ColumnRef, Iden, Order, SeaRc, SelectExpr, SelectStatement, SimpleExpr}; -pub const SELECT_A: &str = "A_"; -pub const SELECT_B: &str = "B_"; +macro_rules! select_def { + ( $ident: ident, $str: expr ) => { + #[derive(Debug, Clone, Copy)] + pub struct $ident; + + impl Iden for $ident { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!(s, "{}", self.as_str()).unwrap(); + } + } + + impl IdenStatic for $ident { + fn as_str(&self) -> &str { + $str + } + } + }; +} + +select_def!(SelectA, "A_"); +select_def!(SelectB, "B_"); impl Select where @@ -37,7 +58,7 @@ where where F: EntityTrait, { - self = self.apply_alias(SELECT_A); + self = self.apply_alias(SelectA.as_str()); SelectTwo::new(self.into_query()) } @@ -45,7 +66,7 @@ where where F: EntityTrait, { - self = self.apply_alias(SELECT_A); + self = self.apply_alias(SelectA.as_str()); SelectTwoMany::new(self.into_query()) } } @@ -102,7 +123,7 @@ where S: QueryTrait, { for col in ::iter() { - let alias = format!("{}{}", SELECT_B, col.to_string().as_str()); + let alias = format!("{}{}", SelectB.as_str(), col.as_str()); selector.query().expr(SelectExpr { expr: col.into_simple_expr(), alias: Some(SeaRc::new(Alias::new(&alias))), diff --git a/src/query/helper.rs b/src/query/helper.rs index 6ade581a..cb1ca28d 100644 --- a/src/query/helper.rs +++ b/src/query/helper.rs @@ -1,11 +1,9 @@ use crate::{ - ColumnTrait, EntityTrait, Identity, IntoSimpleExpr, Iterable, ModelTrait, PrimaryKeyToColumn, - RelationDef, -}; -use sea_query::{ - Alias, Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef, + ColumnTrait, EntityTrait, Identity, IntoIdentity, IntoSimpleExpr, Iterable, ModelTrait, + PrimaryKeyToColumn, RelationDef, }; pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement}; +use sea_query::{Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef}; // LINT: when the column does not appear in tables selected from // LINT: when there is a group by clause, but some columns don't have aggregate functions @@ -55,13 +53,14 @@ pub trait QuerySelect: Sized { /// r#"SELECT COUNT("cake"."id") AS "count" FROM "cake""# /// ); /// ``` - fn column_as(mut self, col: C, alias: &str) -> Self + fn column_as(mut self, col: C, alias: I) -> Self where C: IntoSimpleExpr, + I: IntoIdentity, { self.query().expr(SelectExpr { expr: col.into_simple_expr(), - alias: Some(SeaRc::new(Alias::new(alias))), + alias: Some(SeaRc::new(alias.into_identity())), }); self } @@ -295,7 +294,7 @@ fn join_condition(rel: RelationDef) -> SimpleExpr { } } -fn unpack_table_ref(table_ref: &TableRef) -> DynIden { +pub(crate) fn unpack_table_ref(table_ref: &TableRef) -> DynIden { match table_ref { TableRef::Table(tbl) => SeaRc::clone(tbl), TableRef::SchemaTable(_, tbl) => SeaRc::clone(tbl), diff --git a/src/query/insert.rs b/src/query/insert.rs index abd52b4b..a65071e1 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -43,11 +43,11 @@ where /// /// assert_eq!( /// Insert::one(cake::Model { - /// id: 1, - /// name: "Apple Pie".to_owned(), - /// }) - /// .build(DbBackend::Postgres) - /// .to_string(), + /// id: 1, + /// name: "Apple Pie".to_owned(), + /// }) + /// .build(DbBackend::Postgres) + /// .to_string(), /// r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"#, /// ); /// ``` @@ -57,11 +57,11 @@ where /// /// assert_eq!( /// Insert::one(cake::ActiveModel { - /// id: Unset(None), - /// name: Set("Apple Pie".to_owned()), - /// }) - /// .build(DbBackend::Postgres) - /// .to_string(), + /// id: Unset(None), + /// name: Set("Apple Pie".to_owned()), + /// }) + /// .build(DbBackend::Postgres) + /// .to_string(), /// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#, /// ); /// ``` @@ -79,17 +79,17 @@ where /// /// assert_eq!( /// Insert::many(vec![ - /// cake::Model { - /// id: 1, - /// name: "Apple Pie".to_owned(), - /// }, - /// cake::Model { - /// id: 2, - /// name: "Orange Scone".to_owned(), - /// } - /// ]) - /// .build(DbBackend::Postgres) - /// .to_string(), + /// cake::Model { + /// id: 1, + /// name: "Apple Pie".to_owned(), + /// }, + /// cake::Model { + /// id: 2, + /// name: "Orange Scone".to_owned(), + /// } + /// ]) + /// .build(DbBackend::Postgres) + /// .to_string(), /// r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie'), (2, 'Orange Scone')"#, /// ); /// ``` diff --git a/src/query/join.rs b/src/query/join.rs index 72726d14..42bc993c 100644 --- a/src/query/join.rs +++ b/src/query/join.rs @@ -1,4 +1,4 @@ -use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo, SelectTwoMany}; +use crate::{EntityTrait, Linked, QuerySelect, Related, Select, SelectTwo, SelectTwoMany}; pub use sea_query::JoinType; impl Select @@ -57,6 +57,19 @@ where { self.left_join(r).select_with(r) } + + /// Left Join with a Linked Entity and select both Entity. + pub fn find_also_linked(self, l: L) -> SelectTwo + where + L: Linked, + T: EntityTrait, + { + let mut slf = self; + for rel in l.link() { + slf = slf.join(JoinType::LeftJoin, rel); + } + slf.select_also(T::default()) + } } #[cfg(test)] @@ -220,4 +233,44 @@ mod tests { .join(" ") ); } + + #[test] + fn join_10() { + let cake_model = cake::Model { + id: 12, + name: "".to_owned(), + }; + + assert_eq!( + cake_model + .find_linked(cake::CakeToFilling) + .build(DbBackend::MySql) + .to_string(), + [ + r#"SELECT `filling`.`id`, `filling`.`name`"#, + r#"FROM `filling`"#, + r#"INNER JOIN `cake_filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#, + r#"INNER JOIN `cake` ON `cake`.`id` = `cake_filling`.`cake_id`"#, + ] + .join(" ") + ); + } + + #[test] + fn join_11() { + assert_eq!( + cake::Entity::find() + .find_also_linked(cake::CakeToFilling) + .build(DbBackend::MySql) + .to_string(), + [ + r#"SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,"#, + r#"`filling`.`id` AS `B_id`, `filling`.`name` AS `B_name`"#, + r#"FROM `cake`"#, + r#"LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id`"#, + r#"LEFT JOIN `filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#, + ] + .join(" ") + ); + } } diff --git a/src/query/mod.rs b/src/query/mod.rs index 899882ba..54cc12dd 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -9,7 +9,7 @@ mod select; mod traits; mod update; -// pub use combine::*; +pub use combine::{SelectA, SelectB}; pub use delete::*; pub use helper::*; pub use insert::*; diff --git a/src/query/update.rs b/src/query/update.rs index 21fd39cb..8cd66c37 100644 --- a/src/query/update.rs +++ b/src/query/update.rs @@ -59,7 +59,7 @@ impl Update { /// Update many ActiveModel /// /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::Expr, DbBackend}; + /// use sea_orm::{entity::*, query::*, sea_query::Expr, tests_cfg::fruit, DbBackend}; /// /// assert_eq!( /// Update::many(fruit::Entity) diff --git a/src/tests_cfg/cake.rs b/src/tests_cfg/cake.rs index f8a35d6c..0eeb0738 100644 --- a/src/tests_cfg/cake.rs +++ b/src/tests_cfg/cake.rs @@ -28,6 +28,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -73,4 +75,20 @@ impl Related for Entity { } } +#[derive(Debug)] +pub struct CakeToFilling; + +impl Linked for CakeToFilling { + type FromEntity = Entity; + + type ToEntity = super::filling::Entity; + + fn link(&self) -> Vec { + vec![ + super::cake_filling::Relation::Cake.def().rev(), + super::cake_filling::Relation::Filling.def(), + ] + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/src/tests_cfg/cake_filling.rs b/src/tests_cfg/cake_filling.rs index b1151ee4..ed6d0af0 100644 --- a/src/tests_cfg/cake_filling.rs +++ b/src/tests_cfg/cake_filling.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/src/tests_cfg/cake_filling_price.rs b/src/tests_cfg/cake_filling_price.rs index c0bcbea1..e820ae0f 100644 --- a/src/tests_cfg/cake_filling_price.rs +++ b/src/tests_cfg/cake_filling_price.rs @@ -35,6 +35,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/src/tests_cfg/filling.rs b/src/tests_cfg/filling.rs index b439af7b..5e691980 100644 --- a/src/tests_cfg/filling.rs +++ b/src/tests_cfg/filling.rs @@ -41,6 +41,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/src/tests_cfg/fruit.rs b/src/tests_cfg/fruit.rs index 0511ae58..b0b8bc79 100644 --- a/src/tests_cfg/fruit.rs +++ b/src/tests_cfg/fruit.rs @@ -30,6 +30,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/src/util.rs b/src/util.rs index f6ddfd54..20cc8eb3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,7 @@ #[macro_export] #[cfg(feature = "debug-print")] macro_rules! debug_print { - ($( $args:expr ),*) => { println!( $( $args ),* ); } + ($( $args:expr ),*) => { log::debug!( $( $args ),* ); } } #[macro_export] diff --git a/tests/basic.rs b/tests/basic.rs index 78e3f25c..e62f1ad6 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,7 +1,6 @@ pub mod common; -#[allow(unused_imports)] -use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn}; +pub use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn}; // DATABASE_URL="sqlite::memory:" cargo test --features sqlx-sqlit,runtime-async-std --test basic #[sea_orm_macros::test] diff --git a/tests/common/bakery_chain/baker.rs b/tests/common/bakery_chain/baker.rs index 0c63e721..9ab45905 100644 --- a/tests/common/bakery_chain/baker.rs +++ b/tests/common/bakery_chain/baker.rs @@ -31,6 +31,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -49,7 +51,7 @@ impl ColumnTrait for Column { Self::Id => ColumnType::Integer.def(), Self::Name => ColumnType::String(None).def(), Self::ContactDetails => ColumnType::Json.def(), - Self::BakeryId => ColumnType::Integer.def(), + Self::BakeryId => ColumnType::Integer.def().null(), } } } @@ -60,6 +62,8 @@ impl RelationTrait for Relation { Self::Bakery => Entity::belongs_to(super::bakery::Entity) .from(Column::BakeryId) .to(super::bakery::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), } } @@ -81,4 +85,22 @@ impl Related for Entity { } } +pub struct BakedForCustomer; + +impl Linked for BakedForCustomer { + type FromEntity = Entity; + + type ToEntity = super::customer::Entity; + + fn link(&self) -> Vec { + vec![ + super::cakes_bakers::Relation::Baker.def().rev(), + super::cakes_bakers::Relation::Cake.def(), + super::lineitem::Relation::Cake.def().rev(), + super::lineitem::Relation::Order.def(), + super::order::Relation::Customer.def(), + ] + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/bakery_chain/bakery.rs b/tests/common/bakery_chain/bakery.rs index 61803329..d03298b3 100644 --- a/tests/common/bakery_chain/bakery.rs +++ b/tests/common/bakery_chain/bakery.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -48,7 +50,7 @@ impl ColumnTrait for Column { match self { Self::Id => ColumnType::Integer.def(), Self::Name => ColumnType::String(None).def(), - Self::ProfitMargin => ColumnType::Float.def(), + Self::ProfitMargin => ColumnType::Double.def(), } } } diff --git a/tests/common/bakery_chain/cake.rs b/tests/common/bakery_chain/cake.rs index 72e649ce..cb1895a6 100644 --- a/tests/common/bakery_chain/cake.rs +++ b/tests/common/bakery_chain/cake.rs @@ -35,6 +35,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -54,9 +56,9 @@ impl ColumnTrait for Column { Self::Id => ColumnType::Integer.def(), Self::Name => ColumnType::String(None).def(), Self::Price => ColumnType::Decimal(Some((19, 4))).def(), - Self::BakeryId => ColumnType::Integer.def(), + Self::BakeryId => ColumnType::Integer.def().null(), Self::GlutenFree => ColumnType::Boolean.def(), - Self::Serial => ColumnType::String(None).def(), + Self::Serial => ColumnType::Uuid.def(), } } } @@ -67,6 +69,8 @@ impl RelationTrait for Relation { Self::Bakery => Entity::belongs_to(super::bakery::Entity) .from(Column::BakeryId) .to(super::bakery::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), Self::Lineitem => Entity::has_many(super::lineitem::Entity).into(), } diff --git a/tests/common/bakery_chain/cakes_bakers.rs b/tests/common/bakery_chain/cakes_bakers.rs index 8106bbdf..b0f52ec8 100644 --- a/tests/common/bakery_chain/cakes_bakers.rs +++ b/tests/common/bakery_chain/cakes_bakers.rs @@ -28,6 +28,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } @@ -56,10 +58,14 @@ impl RelationTrait for Relation { Self::Cake => Entity::belongs_to(super::cake::Entity) .from(Column::CakeId) .to(super::cake::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), Self::Baker => Entity::belongs_to(super::baker::Entity) .from(Column::BakerId) .to(super::baker::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), } } diff --git a/tests/common/bakery_chain/customer.rs b/tests/common/bakery_chain/customer.rs index ce4319ff..2cf1b9ec 100644 --- a/tests/common/bakery_chain/customer.rs +++ b/tests/common/bakery_chain/customer.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -46,7 +48,7 @@ impl ColumnTrait for Column { match self { Self::Id => ColumnType::Integer.def(), Self::Name => ColumnType::String(None).def(), - Self::Notes => ColumnType::Text.def(), + Self::Notes => ColumnType::Text.def().null(), } } } diff --git a/tests/common/bakery_chain/lineitem.rs b/tests/common/bakery_chain/lineitem.rs index 45a6037f..c89b44b8 100644 --- a/tests/common/bakery_chain/lineitem.rs +++ b/tests/common/bakery_chain/lineitem.rs @@ -33,6 +33,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -64,10 +66,14 @@ impl RelationTrait for Relation { Self::Order => Entity::belongs_to(super::order::Entity) .from(Column::OrderId) .to(super::order::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), Self::Cake => Entity::belongs_to(super::cake::Entity) .from(Column::CakeId) .to(super::cake::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), } } diff --git a/tests/common/bakery_chain/metadata.rs b/tests/common/bakery_chain/metadata.rs new file mode 100644 index 00000000..ecef26c3 --- /dev/null +++ b/tests/common/bakery_chain/metadata.rs @@ -0,0 +1,61 @@ +use sea_orm::entity::prelude::*; +use uuid::Uuid; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "metadata" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub uuid: Uuid, + pub key: String, + pub value: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Uuid, + Key, + Value, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Uuid, +} + +impl PrimaryKeyTrait for PrimaryKey { + type ValueType = Uuid; + + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Self::Uuid => ColumnType::Uuid.def(), + Self::Key => ColumnType::String(None).def(), + Self::Value => ColumnType::String(None).def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + unreachable!() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/bakery_chain/mod.rs b/tests/common/bakery_chain/mod.rs index 89028aab..3282766d 100644 --- a/tests/common/bakery_chain/mod.rs +++ b/tests/common/bakery_chain/mod.rs @@ -4,6 +4,7 @@ pub mod cake; pub mod cakes_bakers; pub mod customer; pub mod lineitem; +pub mod metadata; pub mod order; pub use super::baker::Entity as Baker; @@ -12,4 +13,5 @@ pub use super::cake::Entity as Cake; pub use super::cakes_bakers::Entity as CakesBakers; pub use super::customer::Entity as Customer; pub use super::lineitem::Entity as Lineitem; +pub use super::metadata::Entity as Metadata; pub use super::order::Entity as Order; diff --git a/tests/common/bakery_chain/order.rs b/tests/common/bakery_chain/order.rs index 82b02dee..f9561b92 100644 --- a/tests/common/bakery_chain/order.rs +++ b/tests/common/bakery_chain/order.rs @@ -33,6 +33,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } @@ -65,10 +67,14 @@ impl RelationTrait for Relation { Self::Bakery => Entity::belongs_to(super::bakery::Entity) .from(Column::BakeryId) .to(super::bakery::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), Self::Customer => Entity::belongs_to(super::customer::Entity) .from(Column::CustomerId) .to(super::customer::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade) .into(), Self::Lineitem => Entity::has_many(super::lineitem::Entity).into(), } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 54630999..2392ff72 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -19,7 +19,7 @@ impl TestContext { let db: DatabaseConnection = setup::setup(&base_url, test_name).await; Self { - base_url: base_url, + base_url, db_name: test_name.to_string(), db, } diff --git a/tests/common/setup/mod.rs b/tests/common/setup/mod.rs index 7528cf6c..74e35b45 100644 --- a/tests/common/setup/mod.rs +++ b/tests/common/setup/mod.rs @@ -45,13 +45,14 @@ pub async fn setup(base_url: &str, db_name: &str) -> DatabaseConnection { Database::connect(base_url).await.unwrap() }; - assert!(schema::create_bakery_table(&db).await.is_ok()); - assert!(schema::create_baker_table(&db).await.is_ok()); - assert!(schema::create_customer_table(&db).await.is_ok()); - assert!(schema::create_order_table(&db).await.is_ok()); - assert!(schema::create_cake_table(&db).await.is_ok()); - assert!(schema::create_cakes_bakers_table(&db).await.is_ok()); - assert!(schema::create_lineitem_table(&db).await.is_ok()); + schema::create_bakery_table(&db).await.unwrap(); + schema::create_baker_table(&db).await.unwrap(); + schema::create_customer_table(&db).await.unwrap(); + schema::create_order_table(&db).await.unwrap(); + schema::create_cake_table(&db).await.unwrap(); + schema::create_cakes_bakers_table(&db).await.unwrap(); + schema::create_lineitem_table(&db).await.unwrap(); + schema::create_metadata_table(&db).await.unwrap(); db } diff --git a/tests/common/setup/schema.rs b/tests/common/setup/schema.rs index 4eba40ab..e3844dd2 100644 --- a/tests/common/setup/schema.rs +++ b/tests/common/setup/schema.rs @@ -1,15 +1,29 @@ -use sea_orm::{error::*, sea_query, DbConn, ExecResult}; -use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, TableCreateStatement}; - pub use super::super::bakery_chain::*; +use pretty_assertions::assert_eq; +use sea_orm::{ + entity_to_table_create_statement, error::*, sea_query, DbConn, EntityTrait, ExecResult, +}; +use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, Table, TableCreateStatement}; -async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result { +async fn create_table( + db: &DbConn, + stmt: &TableCreateStatement, + entity: E, +) -> Result +where + E: EntityTrait, +{ let builder = db.get_database_backend(); - db.execute(builder.build(stmt)).await + let stmt = builder.build(stmt); + assert_eq!( + builder.build(&entity_to_table_create_statement(entity)), + stmt + ); + db.execute(stmt).await } pub async fn create_bakery_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(bakery::Entity) .if_not_exists() .col( @@ -27,16 +41,17 @@ pub async fn create_bakery_table(db: &DbConn) -> Result { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Bakery).await } pub async fn create_baker_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(baker::Entity) .if_not_exists() .col( ColumnDef::new(baker::Column::Id) .integer() + .not_null() .auto_increment() .primary_key(), ) @@ -49,7 +64,7 @@ pub async fn create_baker_table(db: &DbConn) -> Result { .col(ColumnDef::new(baker::Column::BakeryId).integer()) .foreign_key( ForeignKey::create() - .name("FK_baker_bakery") + .name("fk-baker-bakery") .from(baker::Entity, baker::Column::BakeryId) .to(bakery::Entity, bakery::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -57,11 +72,11 @@ pub async fn create_baker_table(db: &DbConn) -> Result { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Baker).await } pub async fn create_customer_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(customer::Entity) .if_not_exists() .col( @@ -75,11 +90,11 @@ pub async fn create_customer_table(db: &DbConn) -> Result { .col(ColumnDef::new(customer::Column::Notes).text()) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Customer).await } pub async fn create_order_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(order::Entity) .if_not_exists() .col( @@ -107,7 +122,7 @@ pub async fn create_order_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("FK_order_bakery") + .name("fk-order-bakery") .from(order::Entity, order::Column::BakeryId) .to(bakery::Entity, bakery::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -115,7 +130,7 @@ pub async fn create_order_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("FK_order_customer") + .name("fk-order-customer") .from(order::Entity, order::Column::CustomerId) .to(customer::Entity, customer::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -123,11 +138,11 @@ pub async fn create_order_table(db: &DbConn) -> Result { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Order).await } pub async fn create_lineitem_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(lineitem::Entity) .if_not_exists() .col( @@ -159,7 +174,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("FK_lineitem_order") + .name("fk-lineitem-order") .from(lineitem::Entity, lineitem::Column::OrderId) .to(order::Entity, order::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -167,7 +182,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("FK_lineitem_cake") + .name("fk-lineitem-cake") .from(lineitem::Entity, lineitem::Column::CakeId) .to(cake::Entity, cake::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -175,11 +190,11 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Lineitem).await } pub async fn create_cakes_bakers_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(cakes_bakers::Entity) .if_not_exists() .col( @@ -194,12 +209,13 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result ) .primary_key( Index::create() + .name("pk-cakes_bakers") .col(cakes_bakers::Column::CakeId) .col(cakes_bakers::Column::BakerId), ) .foreign_key( ForeignKey::create() - .name("FK_cakes_bakers_cake") + .name("fk-cakes_bakers-cake") .from(cakes_bakers::Entity, cakes_bakers::Column::CakeId) .to(cake::Entity, cake::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -207,7 +223,7 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result ) .foreign_key( ForeignKey::create() - .name("FK_cakes_bakers_baker") + .name("fk-cakes_bakers-baker") .from(cakes_bakers::Entity, cakes_bakers::Column::BakerId) .to(baker::Entity, baker::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -215,11 +231,11 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, CakesBakers).await } pub async fn create_cake_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() + let stmt = Table::create() .table(cake::Entity) .if_not_exists() .col( @@ -238,7 +254,7 @@ pub async fn create_cake_table(db: &DbConn) -> Result { .col(ColumnDef::new(cake::Column::BakeryId).integer()) .foreign_key( ForeignKey::create() - .name("FK_cake_bakery") + .name("fk-cake-bakery") .from(cake::Entity, cake::Column::BakeryId) .to(bakery::Entity, bakery::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -252,5 +268,22 @@ pub async fn create_cake_table(db: &DbConn) -> Result { .col(ColumnDef::new(cake::Column::Serial).uuid().not_null()) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Cake).await +} + +pub async fn create_metadata_table(db: &DbConn) -> Result { + let stmt = sea_query::Table::create() + .table(metadata::Entity) + .if_not_exists() + .col( + ColumnDef::new(metadata::Column::Uuid) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(metadata::Column::Key).string().not_null()) + .col(ColumnDef::new(metadata::Column::Value).string().not_null()) + .to_owned(); + + create_table(db, &stmt, Metadata).await } diff --git a/tests/crud/create_baker.rs b/tests/crud/create_baker.rs index 8653b692..085b4005 100644 --- a/tests/crud/create_baker.rs +++ b/tests/crud/create_baker.rs @@ -7,7 +7,7 @@ pub async fn test_create_baker(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -30,7 +30,7 @@ pub async fn test_create_baker(db: &DbConn) { bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)), ..Default::default() }; - let res: InsertResult = Baker::insert(baker_bob) + let res = Baker::insert(baker_bob) .exec(db) .await .expect("could not insert baker"); diff --git a/tests/crud/create_cake.rs b/tests/crud/create_cake.rs index 07f9c4b3..4fa914a5 100644 --- a/tests/crud/create_cake.rs +++ b/tests/crud/create_cake.rs @@ -8,7 +8,7 @@ pub async fn test_create_cake(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -23,7 +23,7 @@ pub async fn test_create_cake(db: &DbConn) { bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)), ..Default::default() }; - let baker_insert_res: InsertResult = Baker::insert(baker_bob) + let baker_insert_res = Baker::insert(baker_bob) .exec(db) .await .expect("could not insert baker"); @@ -38,7 +38,7 @@ pub async fn test_create_cake(db: &DbConn) { ..Default::default() }; - let cake_insert_res: InsertResult = Cake::insert(mud_cake) + let cake_insert_res = Cake::insert(mud_cake) .exec(db) .await .expect("could not insert cake"); @@ -51,18 +51,25 @@ pub async fn test_create_cake(db: &DbConn) { let cake_baker = cakes_bakers::ActiveModel { cake_id: Set(cake_insert_res.last_insert_id as i32), baker_id: Set(baker_insert_res.last_insert_id as i32), - ..Default::default() }; - let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker) + let cake_baker_res = CakesBakers::insert(cake_baker.clone()) .exec(db) .await .expect("could not insert cake_baker"); + assert_eq!( + cake_baker_res.last_insert_id, + if cfg!(feature = "sqlx-postgres") { + (cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap()) + } else { + Default::default() + } + ); assert!(cake.is_some()); let cake_model = cake.unwrap(); assert_eq!(cake_model.name, "Mud Cake"); assert_eq!(cake_model.price, dec!(10.25)); - assert_eq!(cake_model.gluten_free, false); + assert!(!cake_model.gluten_free); assert_eq!( cake_model .find_related(Bakery) diff --git a/tests/crud/create_lineitem.rs b/tests/crud/create_lineitem.rs index c82958f9..da82cc82 100644 --- a/tests/crud/create_lineitem.rs +++ b/tests/crud/create_lineitem.rs @@ -10,7 +10,7 @@ pub async fn test_create_lineitem(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -26,7 +26,7 @@ pub async fn test_create_lineitem(db: &DbConn) { bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)), ..Default::default() }; - let baker_insert_res: InsertResult = Baker::insert(baker_bob) + let baker_insert_res = Baker::insert(baker_bob) .exec(db) .await .expect("could not insert baker"); @@ -41,7 +41,7 @@ pub async fn test_create_lineitem(db: &DbConn) { ..Default::default() }; - let cake_insert_res: InsertResult = Cake::insert(mud_cake) + let cake_insert_res = Cake::insert(mud_cake) .exec(db) .await .expect("could not insert cake"); @@ -50,12 +50,19 @@ pub async fn test_create_lineitem(db: &DbConn) { let cake_baker = cakes_bakers::ActiveModel { cake_id: Set(cake_insert_res.last_insert_id as i32), baker_id: Set(baker_insert_res.last_insert_id as i32), - ..Default::default() }; - let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker) + let cake_baker_res = CakesBakers::insert(cake_baker.clone()) .exec(db) .await .expect("could not insert cake_baker"); + assert_eq!( + cake_baker_res.last_insert_id, + if cfg!(feature = "sqlx-postgres") { + (cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap()) + } else { + Default::default() + } + ); // Customer let customer_kate = customer::ActiveModel { @@ -63,7 +70,7 @@ pub async fn test_create_lineitem(db: &DbConn) { notes: Set(Some("Loves cheese cake".to_owned())), ..Default::default() }; - let customer_insert_res: InsertResult = Customer::insert(customer_kate) + let customer_insert_res = Customer::insert(customer_kate) .exec(db) .await .expect("could not insert customer"); @@ -76,7 +83,7 @@ pub async fn test_create_lineitem(db: &DbConn) { placed_at: Set(Utc::now().naive_utc()), ..Default::default() }; - let order_insert_res: InsertResult = Order::insert(order_1) + let order_insert_res = Order::insert(order_1) .exec(db) .await .expect("could not insert order"); @@ -89,7 +96,7 @@ pub async fn test_create_lineitem(db: &DbConn) { quantity: Set(1), ..Default::default() }; - let lineitem_insert_res: InsertResult = Lineitem::insert(lineitem_1) + let lineitem_insert_res = Lineitem::insert(lineitem_1) .exec(db) .await .expect("could not insert lineitem"); @@ -105,7 +112,7 @@ pub async fn test_create_lineitem(db: &DbConn) { assert_eq!(lineitem_model.price, dec!(7.55)); - let cake: Option = Cake::find_by_id(lineitem_model.cake_id as u64) + let cake: Option = Cake::find_by_id(lineitem_model.cake_id) .one(db) .await .expect("could not find cake"); diff --git a/tests/crud/create_order.rs b/tests/crud/create_order.rs index 46ebcf09..ba8ff09b 100644 --- a/tests/crud/create_order.rs +++ b/tests/crud/create_order.rs @@ -10,7 +10,7 @@ pub async fn test_create_order(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -26,7 +26,7 @@ pub async fn test_create_order(db: &DbConn) { bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)), ..Default::default() }; - let baker_insert_res: InsertResult = Baker::insert(baker_bob) + let baker_insert_res = Baker::insert(baker_bob) .exec(db) .await .expect("could not insert baker"); @@ -41,7 +41,7 @@ pub async fn test_create_order(db: &DbConn) { ..Default::default() }; - let cake_insert_res: InsertResult = Cake::insert(mud_cake) + let cake_insert_res = Cake::insert(mud_cake) .exec(db) .await .expect("could not insert cake"); @@ -50,12 +50,19 @@ pub async fn test_create_order(db: &DbConn) { let cake_baker = cakes_bakers::ActiveModel { cake_id: Set(cake_insert_res.last_insert_id as i32), baker_id: Set(baker_insert_res.last_insert_id as i32), - ..Default::default() }; - let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker) + let cake_baker_res = CakesBakers::insert(cake_baker.clone()) .exec(db) .await .expect("could not insert cake_baker"); + assert_eq!( + cake_baker_res.last_insert_id, + if cfg!(feature = "sqlx-postgres") { + (cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap()) + } else { + Default::default() + } + ); // Customer let customer_kate = customer::ActiveModel { @@ -63,7 +70,7 @@ pub async fn test_create_order(db: &DbConn) { notes: Set(Some("Loves cheese cake".to_owned())), ..Default::default() }; - let customer_insert_res: InsertResult = Customer::insert(customer_kate) + let customer_insert_res = Customer::insert(customer_kate) .exec(db) .await .expect("could not insert customer"); @@ -76,7 +83,7 @@ pub async fn test_create_order(db: &DbConn) { placed_at: Set(Utc::now().naive_utc()), ..Default::default() }; - let order_insert_res: InsertResult = Order::insert(order_1) + let order_insert_res = Order::insert(order_1) .exec(db) .await .expect("could not insert order"); @@ -89,7 +96,7 @@ pub async fn test_create_order(db: &DbConn) { quantity: Set(2), ..Default::default() }; - let _lineitem_insert_res: InsertResult = Lineitem::insert(lineitem_1) + let _lineitem_insert_res = Lineitem::insert(lineitem_1) .exec(db) .await .expect("could not insert lineitem"); @@ -103,7 +110,7 @@ pub async fn test_create_order(db: &DbConn) { let order_model = order.unwrap(); assert_eq!(order_model.total, dec!(15.10)); - let customer: Option = Customer::find_by_id(order_model.customer_id as u64) + let customer: Option = Customer::find_by_id(order_model.customer_id) .one(db) .await .expect("could not find customer"); @@ -111,7 +118,7 @@ pub async fn test_create_order(db: &DbConn) { let customer_model = customer.unwrap(); assert_eq!(customer_model.name, "Kate"); - let bakery: Option = Bakery::find_by_id(order_model.bakery_id as i64) + let bakery: Option = Bakery::find_by_id(order_model.bakery_id) .one(db) .await .expect("could not find bakery"); diff --git a/tests/crud/deletes.rs b/tests/crud/deletes.rs index ebff4b5f..4c34d36b 100644 --- a/tests/crud/deletes.rs +++ b/tests/crud/deletes.rs @@ -10,7 +10,7 @@ pub async fn test_delete_cake(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 457f639d..916878c8 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -1,7 +1,3 @@ -use sea_orm::{entity::*, DbConn, InsertResult}; - -pub use super::common::bakery_chain::*; - pub mod create_baker; pub mod create_cake; pub mod create_lineitem; @@ -9,13 +5,23 @@ pub mod create_order; pub mod deletes; pub mod updates; +pub use create_baker::*; +pub use create_cake::*; +pub use create_lineitem::*; +pub use create_order::*; +pub use deletes::*; +pub use updates::*; + +pub use super::common::bakery_chain::*; +use sea_orm::{entity::*, DbConn}; + pub async fn test_create_bakery(db: &DbConn) { let seaside_bakery = bakery::ActiveModel { name: Set("SeaSide Bakery".to_owned()), profit_margin: Set(10.4), ..Default::default() }; - let res: InsertResult = Bakery::insert(seaside_bakery) + let res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -28,7 +34,7 @@ pub async fn test_create_bakery(db: &DbConn) { assert!(bakery.is_some()); let bakery_model = bakery.unwrap(); assert_eq!(bakery_model.name, "SeaSide Bakery"); - assert_eq!(bakery_model.profit_margin, 10.4); + assert!((bakery_model.profit_margin - 10.4).abs() < f64::EPSILON); } pub async fn test_create_customer(db: &DbConn) { @@ -37,7 +43,7 @@ pub async fn test_create_customer(db: &DbConn) { notes: Set(Some("Loves cheese cake".to_owned())), ..Default::default() }; - let res: InsertResult = Customer::insert(customer_kate) + let res = Customer::insert(customer_kate) .exec(db) .await .expect("could not insert customer"); diff --git a/tests/crud/updates.rs b/tests/crud/updates.rs index 505e4837..83b9a5d3 100644 --- a/tests/crud/updates.rs +++ b/tests/crud/updates.rs @@ -8,7 +8,7 @@ pub async fn test_update_cake(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -22,7 +22,7 @@ pub async fn test_update_cake(db: &DbConn) { ..Default::default() }; - let cake_insert_res: InsertResult = Cake::insert(mud_cake) + let cake_insert_res = Cake::insert(mud_cake) .exec(db) .await .expect("could not insert cake"); @@ -36,7 +36,7 @@ pub async fn test_update_cake(db: &DbConn) { let cake_model = cake.unwrap(); assert_eq!(cake_model.name, "Mud Cake"); assert_eq!(cake_model.price, dec!(10.25)); - assert_eq!(cake_model.gluten_free, false); + assert!(!cake_model.gluten_free); let mut cake_am: cake::ActiveModel = cake_model.into(); cake_am.name = Set("Extra chocolate mud cake".to_owned()); @@ -62,7 +62,7 @@ pub async fn test_update_bakery(db: &DbConn) { profit_margin: Set(10.4), ..Default::default() }; - let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery) + let bakery_insert_res = Bakery::insert(seaside_bakery) .exec(db) .await .expect("could not insert bakery"); @@ -75,7 +75,7 @@ pub async fn test_update_bakery(db: &DbConn) { assert!(bakery.is_some()); let bakery_model = bakery.unwrap(); assert_eq!(bakery_model.name, "SeaSide Bakery"); - assert_eq!(bakery_model.profit_margin, 10.4); + assert!((bakery_model.profit_margin - 10.40).abs() < f64::EPSILON); let mut bakery_am: bakery::ActiveModel = bakery_model.into(); bakery_am.name = Set("SeaBreeze Bakery".to_owned()); @@ -92,7 +92,7 @@ pub async fn test_update_bakery(db: &DbConn) { .expect("could not find bakery"); let bakery_model = bakery.unwrap(); assert_eq!(bakery_model.name, "SeaBreeze Bakery"); - assert_eq!(bakery_model.profit_margin, 12.00); + assert!((bakery_model.profit_margin - 12.00).abs() < f64::EPSILON); } pub async fn test_update_deleted_customer(db: &DbConn) { @@ -130,11 +130,10 @@ pub async fn test_update_deleted_customer(db: &DbConn) { assert_eq!(Customer::find().count(db).await.unwrap(), init_n_customers); - let customer: Option = - Customer::find_by_id(customer_id.clone().unwrap() as i64) - .one(db) - .await - .expect("could not find customer"); + let customer: Option = Customer::find_by_id(customer_id.clone().unwrap()) + .one(db) + .await + .expect("could not find customer"); assert_eq!(customer, None); } diff --git a/tests/crud_tests.rs b/tests/crud_tests.rs index 3c26ddfd..7edb3500 100644 --- a/tests/crud_tests.rs +++ b/tests/crud_tests.rs @@ -1,10 +1,10 @@ -use sea_orm::DatabaseConnection; - pub mod common; -pub use common::{bakery_chain::*, setup::*, TestContext}; - mod crud; +pub use common::{bakery_chain::*, setup::*, TestContext}; +pub use crud::*; +use sea_orm::DatabaseConnection; + // Run the test locally: // DATABASE_URL="mysql://root:root@localhost" cargo test --features sqlx-mysql,runtime-async-std --test crud_tests // DATABASE_URL="postgres://root:root@localhost" cargo test --features sqlx-postgres,runtime-async-std --test crud_tests @@ -20,18 +20,18 @@ async fn main() { ctx.delete().await; } -async fn create_entities(db: &DatabaseConnection) { - crud::test_create_bakery(db).await; - crud::create_baker::test_create_baker(db).await; - crud::test_create_customer(db).await; - crud::create_cake::test_create_cake(db).await; - crud::create_lineitem::test_create_lineitem(db).await; - crud::create_order::test_create_order(db).await; +pub async fn create_entities(db: &DatabaseConnection) { + test_create_bakery(db).await; + test_create_baker(db).await; + test_create_customer(db).await; + test_create_cake(db).await; + test_create_lineitem(db).await; + test_create_order(db).await; - crud::updates::test_update_cake(db).await; - crud::updates::test_update_bakery(db).await; - crud::updates::test_update_deleted_customer(db).await; + test_update_cake(db).await; + test_update_bakery(db).await; + test_update_deleted_customer(db).await; - crud::deletes::test_delete_cake(db).await; - crud::deletes::test_delete_bakery(db).await; + test_delete_cake(db).await; + test_delete_bakery(db).await; } diff --git a/tests/primary_key_tests.rs b/tests/primary_key_tests.rs new file mode 100644 index 00000000..ea8255e9 --- /dev/null +++ b/tests/primary_key_tests.rs @@ -0,0 +1,42 @@ +pub mod common; + +pub use common::{bakery_chain::*, setup::*, TestContext}; +use sea_orm::{entity::prelude::*, DatabaseConnection, Set}; +use uuid::Uuid; + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +async fn main() -> Result<(), DbErr> { + let ctx = TestContext::new("bakery_chain_schema_primary_key_tests").await; + + create_metadata(&ctx.db).await?; + + ctx.delete().await; + + Ok(()) +} + +pub async fn create_metadata(db: &DatabaseConnection) -> Result<(), DbErr> { + let metadata = metadata::ActiveModel { + uuid: Set(Uuid::new_v4()), + key: Set("markup".to_owned()), + value: Set("1.18".to_owned()), + }; + + let res = Metadata::insert(metadata.clone()).exec(db).await?; + + assert_eq!( + res.last_insert_id, + if cfg!(feature = "sqlx-postgres") { + metadata.uuid.unwrap() + } else { + Default::default() + } + ); + + Ok(()) +} diff --git a/tests/query_tests.rs b/tests/query_tests.rs index 4e905686..2b5a2295 100644 --- a/tests/query_tests.rs +++ b/tests/query_tests.rs @@ -1,8 +1,8 @@ -use sea_orm::entity::*; -use sea_orm::QueryFilter; - pub mod common; + pub use common::{bakery_chain::*, setup::*, TestContext}; +pub use sea_orm::entity::*; +pub use sea_orm::QueryFilter; // Run the test locally: // DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test query_tests diff --git a/tests/relational_tests.rs b/tests/relational_tests.rs index ae1236a2..caef9c3c 100644 --- a/tests/relational_tests.rs +++ b/tests/relational_tests.rs @@ -1,13 +1,14 @@ -use chrono::offset::Utc; -use rust_decimal::prelude::*; -use rust_decimal_macros::dec; -use sea_orm::{entity::*, query::*, FromQueryResult}; - pub mod common; + +pub use chrono::offset::Utc; pub use common::{bakery_chain::*, setup::*, TestContext}; +pub use rust_decimal::prelude::*; +pub use rust_decimal_macros::dec; +pub use sea_orm::{entity::*, query::*, DbErr, FromQueryResult}; +pub use uuid::Uuid; // Run the test locally: -// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test relational_tests +// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std-native-tls --test relational_tests #[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", @@ -474,3 +475,240 @@ pub async fn having() { ctx.delete().await; } + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +pub async fn linked() -> Result<(), DbErr> { + use common::bakery_chain::Order; + use sea_orm::{SelectA, SelectB}; + + let ctx = TestContext::new("test_linked").await; + + // SeaSide Bakery + let seaside_bakery = bakery::ActiveModel { + name: Set("SeaSide Bakery".to_owned()), + profit_margin: Set(10.4), + ..Default::default() + }; + let seaside_bakery_res = Bakery::insert(seaside_bakery).exec(&ctx.db).await?; + + // Bob's Baker, Cake & Cake Baker + let baker_bob = baker::ActiveModel { + name: Set("Baker Bob".to_owned()), + contact_details: Set(serde_json::json!({ + "mobile": "+61424000000", + "home": "0395555555", + "address": "12 Test St, Testville, Vic, Australia" + })), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)), + ..Default::default() + }; + let baker_bob_res = Baker::insert(baker_bob).exec(&ctx.db).await?; + let mud_cake = cake::ActiveModel { + name: Set("Mud Cake".to_owned()), + price: Set(dec!(10.25)), + gluten_free: Set(false), + serial: Set(Uuid::new_v4()), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)), + ..Default::default() + }; + let mud_cake_res = Cake::insert(mud_cake).exec(&ctx.db).await?; + let bob_cakes_bakers = cakes_bakers::ActiveModel { + cake_id: Set(mud_cake_res.last_insert_id as i32), + baker_id: Set(baker_bob_res.last_insert_id as i32), + ..Default::default() + }; + CakesBakers::insert(bob_cakes_bakers).exec(&ctx.db).await?; + + // Bobby's Baker, Cake & Cake Baker + let baker_bobby = baker::ActiveModel { + name: Set("Baker Bobby".to_owned()), + contact_details: Set(serde_json::json!({ + "mobile": "+85212345678", + })), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)), + ..Default::default() + }; + let baker_bobby_res = Baker::insert(baker_bobby).exec(&ctx.db).await?; + let cheese_cake = cake::ActiveModel { + name: Set("Cheese Cake".to_owned()), + price: Set(dec!(20.5)), + gluten_free: Set(false), + serial: Set(Uuid::new_v4()), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)), + ..Default::default() + }; + let cheese_cake_res = Cake::insert(cheese_cake).exec(&ctx.db).await?; + let bobby_cakes_bakers = cakes_bakers::ActiveModel { + cake_id: Set(cheese_cake_res.last_insert_id as i32), + baker_id: Set(baker_bobby_res.last_insert_id as i32), + ..Default::default() + }; + CakesBakers::insert(bobby_cakes_bakers) + .exec(&ctx.db) + .await?; + let chocolate_cake = cake::ActiveModel { + name: Set("Chocolate Cake".to_owned()), + price: Set(dec!(30.15)), + gluten_free: Set(false), + serial: Set(Uuid::new_v4()), + bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)), + ..Default::default() + }; + let chocolate_cake_res = Cake::insert(chocolate_cake).exec(&ctx.db).await?; + let bobby_cakes_bakers = cakes_bakers::ActiveModel { + cake_id: Set(chocolate_cake_res.last_insert_id as i32), + baker_id: Set(baker_bobby_res.last_insert_id as i32), + ..Default::default() + }; + CakesBakers::insert(bobby_cakes_bakers) + .exec(&ctx.db) + .await?; + + // Kate's Customer, Order & Line Item + let customer_kate = customer::ActiveModel { + name: Set("Kate".to_owned()), + notes: Set(Some("Loves cheese cake".to_owned())), + ..Default::default() + }; + let customer_kate_res = Customer::insert(customer_kate).exec(&ctx.db).await?; + let kate_order_1 = order::ActiveModel { + bakery_id: Set(seaside_bakery_res.last_insert_id as i32), + customer_id: Set(customer_kate_res.last_insert_id as i32), + total: Set(dec!(15.10)), + placed_at: Set(Utc::now().naive_utc()), + ..Default::default() + }; + let kate_order_1_res = Order::insert(kate_order_1).exec(&ctx.db).await?; + lineitem::ActiveModel { + cake_id: Set(cheese_cake_res.last_insert_id as i32), + order_id: Set(kate_order_1_res.last_insert_id as i32), + price: Set(dec!(7.55)), + quantity: Set(2), + ..Default::default() + } + .save(&ctx.db) + .await?; + let kate_order_2 = order::ActiveModel { + bakery_id: Set(seaside_bakery_res.last_insert_id as i32), + customer_id: Set(customer_kate_res.last_insert_id as i32), + total: Set(dec!(29.7)), + placed_at: Set(Utc::now().naive_utc()), + ..Default::default() + }; + let kate_order_2_res = Order::insert(kate_order_2).exec(&ctx.db).await?; + lineitem::ActiveModel { + cake_id: Set(chocolate_cake_res.last_insert_id as i32), + order_id: Set(kate_order_2_res.last_insert_id as i32), + price: Set(dec!(9.9)), + quantity: Set(3), + ..Default::default() + } + .save(&ctx.db) + .await?; + + // Kara's Customer, Order & Line Item + let customer_kara = customer::ActiveModel { + name: Set("Kara".to_owned()), + notes: Set(Some("Loves all cakes".to_owned())), + ..Default::default() + }; + let customer_kara_res = Customer::insert(customer_kara).exec(&ctx.db).await?; + let kara_order_1 = order::ActiveModel { + bakery_id: Set(seaside_bakery_res.last_insert_id as i32), + customer_id: Set(customer_kara_res.last_insert_id as i32), + total: Set(dec!(15.10)), + placed_at: Set(Utc::now().naive_utc()), + ..Default::default() + }; + let kara_order_1_res = Order::insert(kara_order_1).exec(&ctx.db).await?; + lineitem::ActiveModel { + cake_id: Set(mud_cake_res.last_insert_id as i32), + order_id: Set(kara_order_1_res.last_insert_id as i32), + price: Set(dec!(7.55)), + quantity: Set(2), + ..Default::default() + } + .save(&ctx.db) + .await?; + let kara_order_2 = order::ActiveModel { + bakery_id: Set(seaside_bakery_res.last_insert_id as i32), + customer_id: Set(customer_kara_res.last_insert_id as i32), + total: Set(dec!(29.7)), + placed_at: Set(Utc::now().naive_utc()), + ..Default::default() + }; + let kara_order_2_res = Order::insert(kara_order_2).exec(&ctx.db).await?; + lineitem::ActiveModel { + cake_id: Set(cheese_cake_res.last_insert_id as i32), + order_id: Set(kara_order_2_res.last_insert_id as i32), + price: Set(dec!(9.9)), + quantity: Set(3), + ..Default::default() + } + .save(&ctx.db) + .await?; + + #[derive(Debug, FromQueryResult, PartialEq)] + struct BakerLite { + name: String, + } + + #[derive(Debug, FromQueryResult, PartialEq)] + struct CustomerLite { + name: String, + } + + let baked_for_customers: Vec<(BakerLite, Option)> = Baker::find() + .find_also_linked(baker::BakedForCustomer) + .select_only() + .column_as(baker::Column::Name, (SelectA, baker::Column::Name)) + .column_as(customer::Column::Name, (SelectB, customer::Column::Name)) + .group_by(baker::Column::Id) + .group_by(customer::Column::Id) + .group_by(baker::Column::Name) + .group_by(customer::Column::Name) + .order_by_asc(baker::Column::Id) + .order_by_asc(customer::Column::Id) + .into_model() + .all(&ctx.db) + .await?; + + assert_eq!( + baked_for_customers, + vec![ + ( + BakerLite { + name: "Baker Bob".to_owned(), + }, + Some(CustomerLite { + name: "Kara".to_owned(), + }) + ), + ( + BakerLite { + name: "Baker Bobby".to_owned(), + }, + Some(CustomerLite { + name: "Kate".to_owned(), + }) + ), + ( + BakerLite { + name: "Baker Bobby".to_owned(), + }, + Some(CustomerLite { + name: "Kara".to_owned(), + }) + ), + ] + ); + + ctx.delete().await; + + Ok(()) +} diff --git a/tests/sequential_op_tests.rs b/tests/sequential_op_tests.rs index a854c768..28333d84 100644 --- a/tests/sequential_op_tests.rs +++ b/tests/sequential_op_tests.rs @@ -1,11 +1,11 @@ -use chrono::offset::Utc; -use rust_decimal::prelude::*; -use rust_decimal_macros::dec; -use sea_orm::{entity::*, query::*, DatabaseConnection, FromQueryResult}; -use uuid::Uuid; - pub mod common; + +pub use chrono::offset::Utc; pub use common::{bakery_chain::*, setup::*, TestContext}; +pub use rust_decimal::prelude::*; +pub use rust_decimal_macros::dec; +pub use sea_orm::{entity::*, query::*, DatabaseConnection, FromQueryResult}; +pub use uuid::Uuid; // Run the test locally: // DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test sequential_op_tests @@ -67,7 +67,7 @@ async fn init_setup(db: &DatabaseConnection) { ..Default::default() }; - let cake_insert_res: InsertResult = Cake::insert(mud_cake) + let cake_insert_res = Cake::insert(mud_cake) .exec(db) .await .expect("could not insert cake"); @@ -78,10 +78,18 @@ async fn init_setup(db: &DatabaseConnection) { ..Default::default() }; - let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker) + let cake_baker_res = CakesBakers::insert(cake_baker.clone()) .exec(db) .await .expect("could not insert cake_baker"); + assert_eq!( + cake_baker_res.last_insert_id, + if cfg!(feature = "sqlx-postgres") { + (cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap()) + } else { + Default::default() + } + ); let customer_kate = customer::ActiveModel { name: Set("Kate".to_owned()), @@ -183,7 +191,7 @@ async fn find_baker_least_sales(db: &DatabaseConnection) -> Option results.sort_by(|a, b| b.cakes_sold.cmp(&a.cakes_sold)); - Baker::find_by_id(results.last().unwrap().id as i64) + Baker::find_by_id(results.last().unwrap().id) .one(db) .await .unwrap() @@ -200,7 +208,7 @@ async fn create_cake(db: &DatabaseConnection, baker: baker::Model) -> Option Option