diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8853cca3..4ce00b59 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -31,3 +31,4 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test + args: --all diff --git a/Cargo.toml b/Cargo.toml index 1f11248f..d404bc02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,23 +37,27 @@ name = "sea_orm" path = "src/lib.rs" [dependencies] -async-stream = { version="^0.3" } -chrono = { version="^0", optional=true } -futures = { version="^0.3" } -futures-util = { version="^0.3" } -rust_decimal = { version="^1", optional=true } -sea-query = { version="^0.12" } -sea-orm-macros = { path="sea-orm-macros", optional=true } -serde = { version="^1.0", features=["derive"] } -sqlx = { version="^0.5", optional=true } -strum = { git="https://github.com/SeaQL/strum.git", branch="sea-orm", version="^0.21", features=["derive", "sea-orm"] } -serde_json = { version="^1", optional=true } +async-stream = { version = "^0.3" } +chrono = { version = "^0", optional = true } +futures = { version = "^0.3" } +futures-util = { version = "^0.3" } +rust_decimal = { version = "^1", optional = true } +sea-query = { version = "^0.12" } +sea-orm-macros = { path = "sea-orm-macros", optional = true } +sea-orm-codegen = { path = "sea-orm-codegen", optional = true } +serde = { version = "^1.0", features = ["derive"] } +sqlx = { version = "^0.5", optional = true } +strum = { git = "https://github.com/SeaQL/strum.git", branch = "sea-orm", version = "^0.21", features = [ + "derive", + "sea-orm", +] } +serde_json = { version = "^1", optional = true } [dev-dependencies] -async-std = { version="^1.9", features=["attributes"] } -maplit = { version="^1" } -rust_decimal_macros = { version="^1" } -sea-orm = { path=".", features=[ +async-std = { version = "^1.9", features = ["attributes"] } +maplit = { version = "^1" } +rust_decimal_macros = { version = "^1" } +sea-orm = { path = ".", features = [ "sqlx-sqlite", "sqlx-json", "sqlx-chrono", @@ -64,8 +68,16 @@ sea-orm = { path=".", features=[ [features] debug-print = [] -default = ["macros", "with-json", "with-chrono", "with-rust_decimal", "mock"] +default = [ + "macros", + "codegen", + "with-json", + "with-chrono", + "with-rust_decimal", + "mock", +] macros = ["sea-orm-macros"] +codegen = ["sea-orm-codegen"] mock = [] with-json = ["serde_json", "sea-query/with-json"] with-chrono = ["chrono", "sea-query/with-chrono"] diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml index a72a366a..100314c9 100644 --- a/sea-orm-codegen/Cargo.toml +++ b/sea-orm-codegen/Cargo.toml @@ -23,3 +23,7 @@ syn = { version = "^1", default-features = false, features = [ "derive", "parsin quote = "^1" heck = "^0.3" proc-macro2 = "^1" + +[dev-dependencies] +async-std = { version = "^1.9", features = [ "attributes" ] } +sea-orm = { path = "../", features = ["mock", "sqlx-json", "sqlx-chrono", "runtime-async-std-native-tls"] } diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index e077c0a6..2dbeab52 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -22,7 +22,7 @@ impl Column { } pub fn get_rs_type(&self) -> TokenStream { - let ident = match self.col_type { + let ident: TokenStream = match self.col_type { ColumnType::Char(_) | ColumnType::String(_) | ColumnType::Text @@ -32,18 +32,18 @@ impl Column { | ColumnType::Date | ColumnType::Json | ColumnType::JsonBinary - | ColumnType::Custom(_) => format_ident!("String"), - ColumnType::TinyInteger(_) => format_ident!("i8"), - ColumnType::SmallInteger(_) => format_ident!("i16"), - ColumnType::Integer(_) => format_ident!("i32"), - ColumnType::BigInteger(_) => format_ident!("i64"), - ColumnType::Float(_) | ColumnType::Decimal(_) | ColumnType::Money(_) => { - format_ident!("f32") - } - ColumnType::Double(_) => format_ident!("f64"), - ColumnType::Binary(_) => format_ident!("Vec"), - ColumnType::Boolean => format_ident!("bool"), - }; + | ColumnType::Custom(_) => "String", + ColumnType::TinyInteger(_) => "i8", + ColumnType::SmallInteger(_) => "i16", + ColumnType::Integer(_) => "i32", + ColumnType::BigInteger(_) => "i64", + ColumnType::Float(_) | ColumnType::Decimal(_) | ColumnType::Money(_) => "f32", + ColumnType::Double(_) => "f64", + ColumnType::Binary(_) => "Vec", + ColumnType::Boolean => "bool", + } + .parse() + .unwrap(); match self.not_null { true => quote! { #ident }, false => quote! { Option<#ident> }, @@ -102,6 +102,12 @@ impl Column { } } +impl From for Column { + fn from(col_def: ColumnDef) -> Self { + (&col_def).into() + } +} + impl From<&ColumnDef> for Column { fn from(col_def: &ColumnDef) -> Self { let name = col_def.get_column_name(); @@ -145,3 +151,164 @@ impl From<&ColumnDef> for Column { } } } + +#[cfg(test)] +mod tests { + use crate::Column; + use proc_macro2::TokenStream; + use quote::quote; + use sea_query::{Alias, ColumnDef, ColumnType, SeaRc}; + + fn setup() -> Vec { + macro_rules! make_col { + ($name:expr, $col_type:expr) => { + Column { + name: $name.to_owned(), + col_type: $col_type, + auto_increment: false, + not_null: false, + unique: false, + } + }; + } + vec![ + make_col!("id", ColumnType::String(Some(255))), + make_col!( + "cake_id", + ColumnType::Custom(SeaRc::new(Alias::new("cus_col"))) + ), + make_col!("CakeId", ColumnType::TinyInteger(None)), + make_col!("CakeId", ColumnType::SmallInteger(None)), + make_col!("CakeId", ColumnType::Integer(Some(11))), + make_col!("CakeFillingId", ColumnType::BigInteger(None)), + make_col!("cake-filling-id", ColumnType::Float(None)), + make_col!("CAKE_FILLING_ID", ColumnType::Double(None)), + make_col!("CAKE-FILLING-ID", ColumnType::Binary(None)), + make_col!("CAKE", ColumnType::Boolean), + ] + } + + #[test] + fn test_get_name_snake_case() { + let columns = setup(); + let snack_cases = vec![ + "id", + "cake_id", + "cake_id", + "cake_id", + "cake_id", + "cake_filling_id", + "cake_filling_id", + "cake_filling_id", + "cake_filling_id", + "cake", + ]; + for (col, snack_case) in columns.into_iter().zip(snack_cases) { + assert_eq!(col.get_name_snake_case().to_string(), snack_case); + } + } + + #[test] + fn test_get_name_camel_case() { + let columns = setup(); + let camel_cases = vec![ + "Id", + "CakeId", + "CakeId", + "CakeId", + "CakeId", + "CakeFillingId", + "CakeFillingId", + "CakeFillingId", + "CakeFillingId", + "Cake", + ]; + for (col, camel_case) in columns.into_iter().zip(camel_cases) { + assert_eq!(col.get_name_camel_case().to_string(), camel_case); + } + } + + #[test] + fn test_get_rs_type() { + let columns = setup(); + let rs_types = vec![ + "String", "String", "i8", "i16", "i32", "i64", "f32", "f64", "Vec", "bool", + ]; + for (mut col, rs_type) in columns.into_iter().zip(rs_types) { + let rs_type: TokenStream = rs_type.parse().unwrap(); + + col.not_null = true; + assert_eq!(col.get_rs_type().to_string(), quote!(#rs_type).to_string()); + + col.not_null = false; + assert_eq!( + col.get_rs_type().to_string(), + quote!(Option<#rs_type>).to_string() + ); + } + } + + #[test] + fn test_get_def() { + let columns = setup(); + let col_defs = vec![ + "ColumnType::String(Some(255u32)).def()", + "ColumnType::Custom(\"cus_col\".to_owned()).def()", + "ColumnType::TinyInteger.def()", + "ColumnType::SmallInteger.def()", + "ColumnType::Integer.def()", + "ColumnType::BigInteger.def()", + "ColumnType::Float.def()", + "ColumnType::Double.def()", + "ColumnType::Binary.def()", + "ColumnType::Boolean.def()", + ]; + for (mut col, col_def) in columns.into_iter().zip(col_defs) { + let mut col_def: TokenStream = col_def.parse().unwrap(); + + col.not_null = true; + assert_eq!(col.get_def().to_string(), col_def.to_string()); + + col.not_null = false; + col_def.extend(quote!(.null())); + assert_eq!(col.get_def().to_string(), col_def.to_string()); + + col.unique = true; + col_def.extend(quote!(.unique())); + assert_eq!(col.get_def().to_string(), col_def.to_string()); + } + } + + #[test] + fn test_from_column_def() { + let column: Column = ColumnDef::new(Alias::new("id")).string().into(); + assert_eq!( + column.get_def().to_string(), + quote! { + ColumnType::String(None).def().null() + } + .to_string() + ); + + let column: Column = ColumnDef::new(Alias::new("id")).string().not_null().into(); + assert!(column.not_null); + + let column: Column = ColumnDef::new(Alias::new("id")) + .string() + .unique_key() + .not_null() + .into(); + assert!(column.unique); + assert!(column.not_null); + + let column: Column = ColumnDef::new(Alias::new("id")) + .string() + .auto_increment() + .unique_key() + .not_null() + .into(); + assert!(column.auto_increment); + assert!(column.unique); + assert!(column.not_null); + } +} diff --git a/sea-orm-codegen/src/entity/entity.rs b/sea-orm-codegen/src/entity/entity.rs index 2abb4a7a..aeaed68d 100644 --- a/sea-orm-codegen/src/entity/entity.rs +++ b/sea-orm-codegen/src/entity/entity.rs @@ -111,15 +111,242 @@ impl Entity { .collect() } - pub fn get_relation_rel_find_helpers(&self) -> Vec { - self.relations - .iter() - .map(|rel| rel.get_rel_find_helper()) - .collect() - } - pub fn get_primary_key_auto_increment(&self) -> Ident { let auto_increment = self.columns.iter().any(|col| col.auto_increment); format_ident!("{}", auto_increment) } } + +#[cfg(test)] +mod tests { + use crate::{Column, Entity, PrimaryKey, Relation, RelationType}; + use quote::format_ident; + use sea_query::ColumnType; + + fn setup() -> Entity { + Entity { + table_name: "special_cake".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::String(None), + auto_increment: false, + not_null: false, + unique: false, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(None), + auto_increment: false, + not_null: false, + unique: false, + }, + ], + relations: vec![ + Relation { + ref_table: "fruit".to_owned(), + columns: vec!["id".to_owned()], + ref_columns: vec!["cake_id".to_owned()], + rel_type: RelationType::HasOne, + }, + Relation { + ref_table: "filling".to_owned(), + columns: vec!["id".to_owned()], + ref_columns: vec!["cake_id".to_owned()], + rel_type: RelationType::HasOne, + }, + ], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + } + } + + #[test] + fn test_get_table_name_snake_case() { + let entity = setup(); + + assert_eq!( + entity.get_table_name_snake_case(), + "special_cake".to_owned() + ); + } + + #[test] + fn test_get_table_name_camel_case() { + let entity = setup(); + + assert_eq!(entity.get_table_name_camel_case(), "SpecialCake".to_owned()); + } + + #[test] + fn test_get_table_name_snake_case_ident() { + let entity = setup(); + + assert_eq!( + entity.get_table_name_snake_case_ident(), + format_ident!("{}", "special_cake") + ); + } + + #[test] + fn test_get_table_name_camel_case_ident() { + let entity = setup(); + + assert_eq!( + entity.get_table_name_camel_case_ident(), + format_ident!("{}", "SpecialCake") + ); + } + + #[test] + fn test_get_column_names_snake_case() { + let entity = setup(); + + for (i, elem) in entity.get_column_names_snake_case().into_iter().enumerate() { + assert_eq!(elem, entity.columns[i].get_name_snake_case()); + } + } + + #[test] + fn test_get_column_names_camel_case() { + let entity = setup(); + + for (i, elem) in entity.get_column_names_camel_case().into_iter().enumerate() { + assert_eq!(elem, entity.columns[i].get_name_camel_case()); + } + } + + #[test] + fn test_get_column_rs_types() { + let entity = setup(); + + for (i, elem) in entity.get_column_rs_types().into_iter().enumerate() { + assert_eq!( + elem.to_string(), + entity.columns[i].get_rs_type().to_string() + ); + } + } + + #[test] + fn test_get_column_defs() { + let entity = setup(); + + for (i, elem) in entity.get_column_defs().into_iter().enumerate() { + assert_eq!(elem.to_string(), entity.columns[i].get_def().to_string()); + } + } + + #[test] + fn test_get_primary_key_names_snake_case() { + let entity = setup(); + + for (i, elem) in entity + .get_primary_key_names_snake_case() + .into_iter() + .enumerate() + { + assert_eq!(elem, entity.primary_keys[i].get_name_snake_case()); + } + } + + #[test] + fn test_get_primary_key_names_camel_case() { + let entity = setup(); + + for (i, elem) in entity + .get_primary_key_names_camel_case() + .into_iter() + .enumerate() + { + assert_eq!(elem, entity.primary_keys[i].get_name_camel_case()); + } + } + + #[test] + fn test_get_relation_ref_tables_snake_case() { + let entity = setup(); + + for (i, elem) in entity + .get_relation_ref_tables_snake_case() + .into_iter() + .enumerate() + { + assert_eq!(elem, entity.relations[i].get_ref_table_snake_case()); + } + } + + #[test] + fn test_get_relation_ref_tables_camel_case() { + let entity = setup(); + + for (i, elem) in entity + .get_relation_ref_tables_camel_case() + .into_iter() + .enumerate() + { + assert_eq!(elem, entity.relations[i].get_ref_table_camel_case()); + } + } + + #[test] + fn test_get_relation_defs() { + let entity = setup(); + + for (i, elem) in entity.get_relation_defs().into_iter().enumerate() { + assert_eq!(elem.to_string(), entity.relations[i].get_def().to_string()); + } + } + + #[test] + fn test_get_relation_rel_types() { + let entity = setup(); + + for (i, elem) in entity.get_relation_rel_types().into_iter().enumerate() { + assert_eq!(elem, entity.relations[i].get_rel_type()); + } + } + + #[test] + fn test_get_relation_columns_camel_case() { + let entity = setup(); + + for (i, elem) in entity + .get_relation_columns_camel_case() + .into_iter() + .enumerate() + { + assert_eq!(elem, entity.relations[i].get_column_camel_case()); + } + } + + #[test] + fn test_get_relation_ref_columns_camel_case() { + let entity = setup(); + + for (i, elem) in entity + .get_relation_ref_columns_camel_case() + .into_iter() + .enumerate() + { + assert_eq!(elem, entity.relations[i].get_ref_column_camel_case()); + } + } + + #[test] + fn test_get_primary_key_auto_increment() { + let mut entity = setup(); + + assert_eq!( + entity.get_primary_key_auto_increment(), + format_ident!("{}", false) + ); + + entity.columns[0].auto_increment = true; + assert_eq!( + entity.get_primary_key_auto_increment(), + format_ident!("{}", true) + ); + } +} diff --git a/sea-orm-codegen/src/entity/primary_key.rs b/sea-orm-codegen/src/entity/primary_key.rs index 5efbc038..3a7d6b17 100644 --- a/sea-orm-codegen/src/entity/primary_key.rs +++ b/sea-orm-codegen/src/entity/primary_key.rs @@ -16,3 +16,28 @@ impl PrimaryKey { format_ident!("{}", self.name.to_camel_case()) } } + +#[cfg(test)] +mod tests { + use crate::PrimaryKey; + + fn setup() -> PrimaryKey { + PrimaryKey { + name: "cake_id".to_owned(), + } + } + + #[test] + fn test_get_name_snake_case() { + let primary_key = setup(); + + assert_eq!(primary_key.get_name_snake_case(), "cake_id".to_owned()); + } + + #[test] + fn test_get_name_camel_case() { + let primary_key = setup(); + + assert_eq!(primary_key.get_name_camel_case(), "CakeId".to_owned()); + } +} diff --git a/sea-orm-codegen/src/entity/relation.rs b/sea-orm-codegen/src/entity/relation.rs index 8eab372d..d675137a 100644 --- a/sea-orm-codegen/src/entity/relation.rs +++ b/sea-orm-codegen/src/entity/relation.rs @@ -64,10 +64,6 @@ impl Relation { pub fn get_ref_column_camel_case(&self) -> Ident { format_ident!("{}", self.ref_columns[0].to_camel_case()) } - - pub fn get_rel_find_helper(&self) -> Ident { - format_ident!("find_{}", self.ref_table.to_snake_case()) - } } impl From<&TableForeignKey> for Relation { @@ -87,3 +83,95 @@ impl From<&TableForeignKey> for Relation { } } } + +#[cfg(test)] +mod tests { + use crate::{Relation, RelationType}; + use proc_macro2::TokenStream; + + fn setup() -> Vec { + vec![ + Relation { + ref_table: "fruit".to_owned(), + columns: vec!["id".to_owned()], + ref_columns: vec!["cake_id".to_owned()], + rel_type: RelationType::HasOne, + }, + Relation { + ref_table: "filling".to_owned(), + columns: vec!["filling_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + }, + Relation { + ref_table: "filling".to_owned(), + columns: vec!["filling_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::HasMany, + }, + ] + } + + #[test] + fn test_get_ref_table_snake_case() { + let relations = setup(); + let snake_cases = vec!["fruit", "filling", "filling"]; + for (rel, snake_case) in relations.into_iter().zip(snake_cases) { + assert_eq!(rel.get_ref_table_snake_case().to_string(), snake_case); + } + } + + #[test] + fn test_get_ref_table_camel_case() { + let relations = setup(); + let camel_cases = vec!["Fruit", "Filling", "Filling"]; + for (rel, camel_case) in relations.into_iter().zip(camel_cases) { + assert_eq!(rel.get_ref_table_camel_case().to_string(), camel_case); + } + } + + #[test] + fn test_get_def() { + let relations = setup(); + let rel_defs = vec![ + "Entity::has_one(super::fruit::Entity).into()", + "Entity::belongs_to(super::filling::Entity) \ + .from(Column::FillingId) \ + .to(super::filling::Column::Id) \ + .into()", + "Entity::has_many(super::filling::Entity).into()", + ]; + for (rel, rel_def) in relations.into_iter().zip(rel_defs) { + let rel_def: TokenStream = rel_def.parse().unwrap(); + + assert_eq!(rel.get_def().to_string(), rel_def.to_string()); + } + } + + #[test] + fn test_get_rel_type() { + let relations = setup(); + let rel_types = vec!["has_one", "belongs_to", "has_many"]; + for (rel, rel_type) in relations.into_iter().zip(rel_types) { + assert_eq!(rel.get_rel_type(), rel_type); + } + } + + #[test] + fn test_get_column_camel_case() { + let relations = setup(); + let cols = vec!["Id", "FillingId", "FillingId"]; + for (rel, col) in relations.into_iter().zip(cols) { + assert_eq!(rel.get_column_camel_case(), col); + } + } + + #[test] + fn test_get_ref_column_camel_case() { + let relations = setup(); + let ref_cols = vec!["CakeId", "Id", "Id"]; + for (rel, ref_col) in relations.into_iter().zip(ref_cols) { + assert_eq!(rel.get_ref_column_camel_case(), ref_col); + } + } +} diff --git a/sea-orm-codegen/src/entity/transformer.rs b/sea-orm-codegen/src/entity/transformer.rs index c49a871e..ad48d518 100644 --- a/sea-orm-codegen/src/entity/transformer.rs +++ b/sea-orm-codegen/src/entity/transformer.rs @@ -98,6 +98,7 @@ impl EntityTransformer { } } } + println!("{:#?}", entities); Ok(EntityWriter { entities }) } } diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 3bd8fb59..267b00d6 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -116,9 +116,7 @@ impl EntityWriter { Self::gen_impl_relation_trait(entity), ]; code_blocks.extend(Self::gen_impl_related(entity)); - code_blocks.extend(vec![ - Self::gen_impl_active_model_behavior(), - ]); + code_blocks.extend(vec![Self::gen_impl_active_model_behavior()]); code_blocks } @@ -152,7 +150,7 @@ impl EntityWriter { quote! { #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] pub struct Model { - #(pub #column_names_snake_case: #column_rs_types),* + #(pub #column_names_snake_case: #column_rs_types,)* } } } @@ -162,7 +160,7 @@ impl EntityWriter { quote! { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] pub enum Column { - #(#column_names_camel_case),* + #(#column_names_camel_case,)* } } } @@ -172,7 +170,7 @@ impl EntityWriter { quote! { #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] pub enum PrimaryKey { - #(#primary_key_names_camel_case),* + #(#primary_key_names_camel_case,)* } } } @@ -193,7 +191,7 @@ impl EntityWriter { quote! { #[derive(Copy, Clone, Debug, EnumIter)] pub enum Relation { - #(#relation_ref_tables_camel_case),* + #(#relation_ref_tables_camel_case,)* } } } @@ -207,7 +205,7 @@ impl EntityWriter { fn def(&self) -> ColumnDef { match self { - #(Self::#column_names_camel_case => #column_defs),* + #(Self::#column_names_camel_case => #column_defs,)* } } } @@ -223,7 +221,7 @@ impl EntityWriter { } } else { quote! { - #(Self::#relation_ref_tables_camel_case => #relation_defs),* + #(Self::#relation_ref_tables_camel_case => #relation_defs,)* } }; quote! { @@ -276,3 +274,239 @@ impl EntityWriter { } } } + +#[cfg(test)] +mod tests { + use crate::{Column, Entity, EntityWriter, PrimaryKey, Relation, RelationType}; + use proc_macro2::TokenStream; + use sea_query::ColumnType; + use std::io::{self, BufRead, BufReader}; + + const ENTITY_FILES: [&'static str; 5] = [ + include_str!("../../tests/entity/cake.rs"), + include_str!("../../tests/entity/cake_filling.rs"), + include_str!("../../tests/entity/filling.rs"), + include_str!("../../tests/entity/fruit.rs"), + include_str!("../../tests/entity/vendor.rs"), + ]; + + fn setup() -> Vec { + vec![ + Entity { + table_name: "cake".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: true, + not_null: true, + unique: false, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(Some(255)), + auto_increment: false, + not_null: true, + unique: false, + }, + ], + relations: vec![ + Relation { + ref_table: "cake_filling".to_owned(), + columns: vec![], + ref_columns: vec![], + rel_type: RelationType::HasMany, + }, + Relation { + ref_table: "fruit".to_owned(), + columns: vec![], + ref_columns: vec![], + rel_type: RelationType::HasMany, + }, + ], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "cake_filling".to_owned(), + columns: vec![ + Column { + name: "cake_id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: false, + not_null: true, + unique: false, + }, + Column { + name: "filling_id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: false, + not_null: true, + unique: false, + }, + ], + relations: vec![ + Relation { + ref_table: "cake".to_owned(), + columns: vec!["cake_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + }, + Relation { + ref_table: "filling".to_owned(), + columns: vec!["filling_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + }, + ], + primary_keys: vec![ + PrimaryKey { + name: "cake_id".to_owned(), + }, + PrimaryKey { + name: "filling_id".to_owned(), + }, + ], + }, + Entity { + table_name: "filling".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: true, + not_null: true, + unique: false, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(Some(255)), + auto_increment: false, + not_null: true, + unique: false, + }, + ], + relations: vec![Relation { + ref_table: "cake_filling".to_owned(), + columns: vec![], + ref_columns: vec![], + rel_type: RelationType::HasMany, + }], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "fruit".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: true, + not_null: true, + unique: false, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(Some(255)), + auto_increment: false, + not_null: true, + unique: false, + }, + Column { + name: "cake_id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: false, + not_null: false, + unique: false, + }, + ], + relations: vec![ + Relation { + ref_table: "cake".to_owned(), + columns: vec!["cake_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + }, + Relation { + ref_table: "vendor".to_owned(), + columns: vec![], + ref_columns: vec![], + rel_type: RelationType::HasMany, + }, + ], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + Entity { + table_name: "vendor".to_owned(), + columns: vec![ + Column { + name: "id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: true, + not_null: true, + unique: false, + }, + Column { + name: "name".to_owned(), + col_type: ColumnType::String(Some(255)), + auto_increment: false, + not_null: true, + unique: false, + }, + Column { + name: "fruit_id".to_owned(), + col_type: ColumnType::Integer(Some(11)), + auto_increment: false, + not_null: false, + unique: false, + }, + ], + relations: vec![Relation { + ref_table: "fruit".to_owned(), + columns: vec!["fruit_id".to_owned()], + ref_columns: vec!["id".to_owned()], + rel_type: RelationType::BelongsTo, + }], + primary_keys: vec![PrimaryKey { + name: "id".to_owned(), + }], + }, + ] + } + + #[test] + fn test_gen_code_blocks() -> io::Result<()> { + let entities = setup(); + + assert_eq!(entities.len(), ENTITY_FILES.len()); + + for (i, entity) in entities.iter().enumerate() { + let mut reader = BufReader::new(ENTITY_FILES[i].as_bytes()); + let mut lines: Vec = Vec::new(); + + reader.read_until(b';', &mut Vec::new())?; + + let mut line = String::new(); + while reader.read_line(&mut line)? > 0 { + lines.push(line.to_owned()); + line.clear(); + } + let content = lines.join(""); + let expected: TokenStream = content.parse().unwrap(); + let generated = EntityWriter::gen_code_blocks(entity) + .into_iter() + .skip(1) + .fold(TokenStream::new(), |mut acc, tok| { + acc.extend(tok); + acc + }); + assert_eq!(expected.to_string(), generated.to_string()); + } + + Ok(()) + } +} diff --git a/sea-orm-codegen/tests/entity/cake.rs b/sea-orm-codegen/tests/entity/cake.rs new file mode 100644 index 00000000..12e7c4ab --- /dev/null +++ b/sea-orm-codegen/tests/entity/cake.rs @@ -0,0 +1,74 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "cake" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + CakeFilling, + Fruit, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::String(Some(255u32)).def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::CakeFilling => Entity::has_many(super::cake_filling::Entity).into(), + Self::Fruit => Entity::has_many(super::fruit::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/entity/cake_filling.rs b/sea-orm-codegen/tests/entity/cake_filling.rs new file mode 100644 index 00000000..1ac64920 --- /dev/null +++ b/sea-orm-codegen/tests/entity/cake_filling.rs @@ -0,0 +1,81 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "cake_filling" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + CakeId, + FillingId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + CakeId, + FillingId, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Cake, + Filling, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::CakeId => ColumnType::Integer.def(), + Self::FillingId => ColumnType::Integer.def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Cake => Entity::belongs_to(super::cake::Entity) + .from(Column::CakeId) + .to(super::cake::Column::Id) + .into(), + Self::Filling => Entity::belongs_to(super::filling::Entity) + .from(Column::FillingId) + .to(super::filling::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Filling.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/entity/filling.rs b/sea-orm-codegen/tests/entity/filling.rs new file mode 100644 index 00000000..752317a4 --- /dev/null +++ b/sea-orm-codegen/tests/entity/filling.rs @@ -0,0 +1,66 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "filling" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + CakeFilling, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::String(Some(255u32)).def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::CakeFilling => Entity::has_many(super::cake_filling::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/entity/fruit.rs b/sea-orm-codegen/tests/entity/fruit.rs new file mode 100644 index 00000000..7ff7d9b8 --- /dev/null +++ b/sea-orm-codegen/tests/entity/fruit.rs @@ -0,0 +1,80 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "fruit" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: String, + pub cake_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, + CakeId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Cake, + Vendor, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::String(Some(255u32)).def(), + Self::CakeId => ColumnType::Integer.def().null(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Cake => Entity::belongs_to(super::cake::Entity) + .from(Column::CakeId) + .to(super::cake::Column::Id) + .into(), + Self::Vendor => Entity::has_many(super::vendor::Entity).into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Vendor.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/entity/mod.rs b/sea-orm-codegen/tests/entity/mod.rs new file mode 100644 index 00000000..395d29f9 --- /dev/null +++ b/sea-orm-codegen/tests/entity/mod.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub mod cake; +pub mod cake_filling; +pub mod filling; +pub mod fruit; +pub mod vendor; diff --git a/sea-orm-codegen/tests/entity/prelude.rs b/sea-orm-codegen/tests/entity/prelude.rs new file mode 100644 index 00000000..b4e85c78 --- /dev/null +++ b/sea-orm-codegen/tests/entity/prelude.rs @@ -0,0 +1,7 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +pub use super::cake::Entity as Cake; +pub use super::cake_filling::Entity as CakeFilling; +pub use super::filling::Entity as Filling; +pub use super::fruit::Entity as Fruit; +pub use super::vendor::Entity as Vendor; diff --git a/sea-orm-codegen/tests/entity/vendor.rs b/sea-orm-codegen/tests/entity/vendor.rs new file mode 100644 index 00000000..2262519f --- /dev/null +++ b/sea-orm-codegen/tests/entity/vendor.rs @@ -0,0 +1,72 @@ +//! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 + +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "vendor" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: String, + pub fruit_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, + FruitId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Fruit, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::String(Some(255u32)).def(), + Self::FruitId => ColumnType::Integer.def().null(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Fruit => Entity::belongs_to(super::fruit::Entity) + .from(Column::FruitId) + .to(super::fruit::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-codegen/tests/mod.rs b/sea-orm-codegen/tests/mod.rs new file mode 100644 index 00000000..12c18066 --- /dev/null +++ b/sea-orm-codegen/tests/mod.rs @@ -0,0 +1,127 @@ +mod entity; + +use entity::*; + +use sea_orm::{entity::*, error::*, MockDatabase, MockExecResult, Transaction}; + +#[async_std::test] +async fn test_insert() -> Result<(), DbErr> { + let exec_result = MockExecResult { + last_insert_id: 1, + rows_affected: 1, + }; + + let db = MockDatabase::new() + .append_exec_results(vec![exec_result.clone()]) + .into_connection(); + + let apple = cake::ActiveModel { + name: Set("Apple Pie".to_owned()), + ..Default::default() + }; + + let insert_result = cake::Entity::insert(apple).exec(&db).await?; + + assert_eq!(insert_result.last_insert_id, exec_result.last_insert_id); + + assert_eq!( + db.into_transaction_log(), + vec![Transaction::from_sql_and_values( + r#"INSERT INTO "cake" ("name") VALUES ($1)"#, + vec!["Apple Pie".into()] + )] + ); + + Ok(()) +} + +#[async_std::test] +async fn test_select() -> Result<(), DbErr> { + let query_results = vec![cake_filling::Model { + cake_id: 2, + filling_id: 3, + }]; + + let db = MockDatabase::new() + .append_query_results(vec![query_results.clone()]) + .into_connection(); + + let selected_models = cake_filling::Entity::find_by_id((2, 3)).all(&db).await?; + + assert_eq!(selected_models, query_results); + + assert_eq!( + db.into_transaction_log(), + vec![Transaction::from_sql_and_values([ + r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#, + r#"WHERE "cake_filling"."cake_id" = $1 AND "cake_filling"."filling_id" = $2"#, + ].join(" ").as_str(), + vec![2i32.into(), 3i32.into()] + )] + ); + + Ok(()) +} + +#[async_std::test] +async fn test_update() -> Result<(), DbErr> { + let exec_result = MockExecResult { + last_insert_id: 1, + rows_affected: 1, + }; + + let db = MockDatabase::new() + .append_exec_results(vec![exec_result.clone()]) + .into_connection(); + + let orange = fruit::ActiveModel { + id: Set(1), + name: Set("Orange".to_owned()), + ..Default::default() + }; + + let updated_model = fruit::Entity::update(orange.clone()).exec(&db).await?; + + assert_eq!(updated_model, orange); + + assert_eq!( + db.into_transaction_log(), + vec![Transaction::from_sql_and_values( + r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2"#, + vec!["Orange".into(), 1i32.into()] + )] + ); + + Ok(()) +} + +#[async_std::test] +async fn test_delete() -> Result<(), DbErr> { + let exec_result = MockExecResult { + last_insert_id: 1, + rows_affected: 1, + }; + + let db = MockDatabase::new() + .append_exec_results(vec![exec_result.clone()]) + .into_connection(); + + let orange = fruit::ActiveModel { + id: Set(3), + ..Default::default() + }; + + let delete_result = fruit::Entity::delete(orange).exec(&db).await?; + + assert_eq!(delete_result.rows_affected, exec_result.rows_affected); + + assert_eq!( + db.into_transaction_log(), + vec![Transaction::from_sql_and_values( + r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, + vec![3i32.into()] + )] + ); + + Ok(()) +} diff --git a/sea-orm-macros/src/derives/active_model.rs b/sea-orm-macros/src/derives/active_model.rs index 76e80a02..4d18a169 100644 --- a/sea-orm-macros/src/derives/active_model.rs +++ b/sea-orm-macros/src/derives/active_model.rs @@ -71,7 +71,11 @@ pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result::Column) -> sea_orm::ActiveValue { match c { - #(::Column::#name => std::mem::take(&mut self.#field).into_wrapped_value(),)* + #(::Column::#name => { + let mut value = sea_orm::ActiveValue::unset(); + std::mem::swap(&mut value, &mut self.#field); + value.into_wrapped_value() + },)* _ => sea_orm::ActiveValue::unset(), } } diff --git a/src/database/connection.rs b/src/database/connection.rs index 58a5bba8..4f880b7f 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -1,4 +1,4 @@ -use crate::{error::*, ExecResult, QueryResult, Statement}; +use crate::{error::*, ExecResult, QueryResult, Statement, Syntax}; use sea_query::{ MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementBuilder, SchemaStatementBuilder, SqliteQueryBuilder, @@ -134,29 +134,49 @@ impl DatabaseConnection { } impl QueryBuilderBackend { + pub fn syntax(&self) -> Syntax { + match self { + Self::MySql => Syntax::MySql, + Self::Postgres => Syntax::Postgres, + Self::Sqlite => Syntax::Sqlite, + } + } + pub fn build(&self, statement: &S) -> Statement where S: QueryStatementBuilder, { - match self { - Self::MySql => statement.build(MysqlQueryBuilder), - Self::Postgres => statement.build(PostgresQueryBuilder), - Self::Sqlite => statement.build(SqliteQueryBuilder), - } - .into() + Statement::from_string_values_tuple( + self.syntax(), + match self { + Self::MySql => statement.build(MysqlQueryBuilder), + Self::Postgres => statement.build(PostgresQueryBuilder), + Self::Sqlite => statement.build(SqliteQueryBuilder), + }, + ) } } impl SchemaBuilderBackend { + pub fn syntax(&self) -> Syntax { + match self { + Self::MySql => Syntax::MySql, + Self::Postgres => Syntax::Postgres, + Self::Sqlite => Syntax::Sqlite, + } + } + pub fn build(&self, statement: &S) -> Statement where S: SchemaStatementBuilder, { - match self { - Self::MySql => statement.build(MysqlQueryBuilder), - Self::Postgres => statement.build(PostgresQueryBuilder), - Self::Sqlite => statement.build(SqliteQueryBuilder), - } - .into() + Statement::from_string( + self.syntax(), + match self { + Self::MySql => statement.build(MysqlQueryBuilder), + Self::Postgres => statement.build(PostgresQueryBuilder), + Self::Sqlite => statement.build(SqliteQueryBuilder), + }, + ) } } diff --git a/src/database/mock.rs b/src/database/mock.rs index 7b208a72..ab002acb 100644 --- a/src/database/mock.rs +++ b/src/database/mock.rs @@ -79,11 +79,7 @@ impl MockDatabaseTrait for MockDatabase { } } - fn query( - &mut self, - counter: usize, - statement: Statement, - ) -> Result, DbErr> { + fn query(&mut self, counter: usize, statement: Statement) -> Result, DbErr> { self.transaction_log.push(Transaction::one(statement)); if counter < self.query_results.len() { Ok(std::mem::take(&mut self.query_results[counter]) diff --git a/src/database/mod.rs b/src/database/mod.rs index 4ad1b347..a5182a34 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -29,6 +29,9 @@ impl Database { if crate::MockDatabaseConnector::accepts(string) { return crate::MockDatabaseConnector::connect(string).await; } - Err(DbErr::Conn(format!("The connection string '{}' has no supporting driver.", string))) + Err(DbErr::Conn(format!( + "The connection string '{}' has no supporting driver.", + string + ))) } } diff --git a/src/database/statement.rs b/src/database/statement.rs index 8b265433..34ece553 100644 --- a/src/database/statement.rs +++ b/src/database/statement.rs @@ -1,26 +1,38 @@ -use sea_query::{inject_parameters, MySqlQueryBuilder, Values}; +use crate::QueryBuilderWithSyntax; +use sea_query::{ + inject_parameters, MysqlQueryBuilder, PostgresQueryBuilder, QueryBuilder, SqliteQueryBuilder, + Values, +}; use std::fmt; +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Syntax { + MySql, + Postgres, + Sqlite, +} + #[derive(Debug, Clone, PartialEq)] pub struct Statement { pub sql: String, pub values: Option, + pub syntax: Syntax, } -impl From for Statement { - fn from(stmt: String) -> Statement { +impl Statement { + pub fn from_string(syntax: Syntax, stmt: String) -> Statement { Statement { sql: stmt, values: None, + syntax, } } -} -impl From<(String, Values)> for Statement { - fn from(stmt: (String, Values)) -> Statement { + pub fn from_string_values_tuple(syntax: Syntax, stmt: (String, Values)) -> Statement { Statement { sql: stmt.0, values: Some(stmt.1), + syntax, } } } @@ -29,8 +41,11 @@ impl fmt::Display for Statement { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.values { Some(values) => { - let string = - inject_parameters(&self.sql, values.0.clone(), &MySqlQueryBuilder::default()); + let string = inject_parameters( + &self.sql, + values.0.clone(), + self.syntax.get_query_builder().as_ref(), + ); write!(f, "{}", &string) } None => { @@ -39,3 +54,31 @@ impl fmt::Display for Statement { } } } + +impl Syntax { + pub fn get_query_builder(&self) -> Box { + match self { + Self::MySql => Box::new(MysqlQueryBuilder), + Self::Postgres => Box::new(PostgresQueryBuilder), + Self::Sqlite => Box::new(SqliteQueryBuilder), + } + } +} + +impl QueryBuilderWithSyntax for MysqlQueryBuilder { + fn syntax(&self) -> Syntax { + Syntax::MySql + } +} + +impl QueryBuilderWithSyntax for PostgresQueryBuilder { + fn syntax(&self) -> Syntax { + Syntax::Postgres + } +} + +impl QueryBuilderWithSyntax for SqliteQueryBuilder { + fn syntax(&self) -> Syntax { + Syntax::Sqlite + } +} diff --git a/src/database/transaction.rs b/src/database/transaction.rs index ff9bba4f..0c49a4e8 100644 --- a/src/database/transaction.rs +++ b/src/database/transaction.rs @@ -1,4 +1,4 @@ -use crate::Statement; +use crate::{Statement, Syntax}; use sea_query::{Value, Values}; #[derive(Debug, Clone, PartialEq)] @@ -11,10 +11,10 @@ impl Transaction { where I: IntoIterator, { - Self::one(Statement { - sql: sql.to_owned(), - values: Some(Values(values.into_iter().collect())), - }) + Self::one(Statement::from_string_values_tuple( + Syntax::Postgres, + (sql.to_string(), Values(values.into_iter().collect())), + )) } /// Create a Transaction with one statement diff --git a/src/driver/sqlx_mysql.rs b/src/driver/sqlx_mysql.rs index ebea0a32..48da88a0 100644 --- a/src/driver/sqlx_mysql.rs +++ b/src/driver/sqlx_mysql.rs @@ -49,7 +49,9 @@ impl SqlxMySqlPoolConnection { Err(err) => Err(sqlx_error_to_exec_err(err)), } } else { - Err(DbErr::Exec("Failed to acquire connection from pool.".to_owned())) + Err(DbErr::Exec( + "Failed to acquire connection from pool.".to_owned(), + )) } } @@ -66,7 +68,9 @@ impl SqlxMySqlPoolConnection { }, } } else { - Err(DbErr::Query("Failed to acquire connection from pool.".to_owned())) + Err(DbErr::Query( + "Failed to acquire connection from pool.".to_owned(), + )) } } @@ -80,7 +84,9 @@ impl SqlxMySqlPoolConnection { Err(err) => Err(sqlx_error_to_query_err(err)), } } else { - Err(DbErr::Query("Failed to acquire connection from pool.".to_owned())) + Err(DbErr::Query( + "Failed to acquire connection from pool.".to_owned(), + )) } } } diff --git a/src/driver/sqlx_sqlite.rs b/src/driver/sqlx_sqlite.rs index ecd0ba4f..ac275b72 100644 --- a/src/driver/sqlx_sqlite.rs +++ b/src/driver/sqlx_sqlite.rs @@ -49,7 +49,9 @@ impl SqlxSqlitePoolConnection { Err(err) => Err(sqlx_error_to_exec_err(err)), } } else { - Err(DbErr::Exec("Failed to acquire connection from pool.".to_owned())) + Err(DbErr::Exec( + "Failed to acquire connection from pool.".to_owned(), + )) } } @@ -66,7 +68,9 @@ impl SqlxSqlitePoolConnection { }, } } else { - Err(DbErr::Query("Failed to acquire connection from pool.".to_owned())) + Err(DbErr::Query( + "Failed to acquire connection from pool.".to_owned(), + )) } } @@ -80,7 +84,9 @@ impl SqlxSqlitePoolConnection { Err(err) => Err(sqlx_error_to_query_err(err)), } } else { - Err(DbErr::Query("Failed to acquire connection from pool.".to_owned())) + Err(DbErr::Query( + "Failed to acquire connection from pool.".to_owned(), + )) } } } diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index 5ab7be19..54bf037b 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -227,7 +227,11 @@ where let model: Option = found?; match model { Some(model) => Ok(model.into_active_model()), - None => Err(DbErr::Exec(format!("Failed to find inserted item: {} {}", E::default().to_string(), res.last_insert_id))), + None => Err(DbErr::Exec(format!( + "Failed to find inserted item: {} {}", + E::default().to_string(), + res.last_insert_id + ))), } } else { Ok(A::default()) @@ -243,6 +247,7 @@ where exec.await } +/// Delete an active model by its primary key pub async fn delete_active_model( mut am: A, db: &DatabaseConnection, diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index 1717ce19..bec0c854 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -18,7 +18,18 @@ pub trait EntityName: IdenStatic + Default { Self::table_name(self) } } - +/// Each table in database correspond to a Entity implemented [`EntityTrait`]. +/// +/// This trait provides an API for you to inspect it's properties +/// - Column (implemented [`ColumnTrait`]) +/// - Relation (implemented [`RelationTrait`]) +/// - Primary Key (implemented [`PrimaryKeyTrait`] and [`PrimaryKeyToColumn`]) +/// +/// This trait also provides an API for CRUD actions +/// - Select: `find`, `find_*` +/// - Insert: `insert`, `insert_*` +/// - Update: `update`, `update_*` +/// - Delete: `delete`, `delete_*` pub trait EntityTrait: EntityName { type Model: ModelTrait + FromQueryResult; @@ -49,16 +60,66 @@ pub trait EntityTrait: EntityName { RelationBuilder::from_rel(RelationType::HasMany, R::to().rev()) } + /// Construct select statement to find one / all models + /// + /// - To select columns, join tables and group by expressions, see [`QuerySelect`](crate::query::QuerySelect) + /// - To apply where conditions / filters, see [`QueryFilter`](crate::query::QueryFilter) + /// - To apply order by expressions, see [`QueryOrder`](crate::query::QueryOrder) + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_query_results(vec![ + /// # vec![ + /// # cake::Model { + /// # id: 1, + /// # name: "New York Cheese".to_owned(), + /// # }, + /// # ], + /// # vec![ + /// # cake::Model { + /// # id: 1, + /// # name: "New York Cheese".to_owned(), + /// # }, + /// # cake::Model { + /// # id: 2, + /// # name: "Chocolate Forest".to_owned(), + /// # }, + /// # ], + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # async_std::task::block_on(async { - /// cake::Entity::find().one(&db).await; - /// cake::Entity::find().all(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// assert_eq!( + /// cake::Entity::find().one(&db).await?, + /// Some(cake::Model { + /// id: 1, + /// name: "New York Cheese".to_owned(), + /// }) + /// ); + /// + /// assert_eq!( + /// cake::Entity::find().all(&db).await?, + /// vec![ + /// cake::Model { + /// id: 1, + /// name: "New York Cheese".to_owned(), + /// }, + /// cake::Model { + /// id: 2, + /// name: "Chocolate Forest".to_owned(), + /// }, + /// ] + /// ); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -77,15 +138,37 @@ pub trait EntityTrait: EntityName { } /// Find a model by primary key + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_query_results(vec![ + /// # vec![ + /// # cake::Model { + /// # id: 11, + /// # name: "Sponge Cake".to_owned(), + /// # }, + /// # ], + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # async_std::task::block_on(async { - /// cake::Entity::find_by_id(11).all(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// assert_eq!( + /// cake::Entity::find_by_id(11).all(&db).await?, + /// vec![cake::Model { + /// id: 11, + /// name: "Sponge Cake".to_owned(), + /// }] + /// ); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -97,13 +180,32 @@ pub trait EntityTrait: EntityName { /// Find by composite key /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_query_results(vec![ + /// # vec![ + /// # cake_filling::Model { + /// # cake_id: 2, + /// # filling_id: 3, + /// # }, + /// # ], + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake_filling}; /// - /// # async_std::task::block_on(async { - /// cake_filling::Entity::find_by_id((2, 3)).all(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// assert_eq!( + /// cake_filling::Entity::find_by_id((2, 3)).all(&db).await?, + /// vec![cake_filling::Model { + /// cake_id: 2, + /// filling_id: 3, + /// }] + /// ); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -135,10 +237,22 @@ pub trait EntityTrait: EntityName { select } + /// Insert an model into database + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 15, + /// # rows_affected: 1, + /// # }, + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// @@ -147,8 +261,14 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # async_std::task::block_on(async { - /// cake::Entity::insert(apple).exec(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// let insert_result = cake::Entity::insert(apple).exec(&db).await?; + /// + /// assert_eq!(insert_result.last_insert_id, 15); + /// // assert_eq!(insert_result.rows_affected, 1); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -164,10 +284,22 @@ pub trait EntityTrait: EntityName { Insert::one(model) } + /// Insert many models into database + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 28, + /// # rows_affected: 2, + /// # }, + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// @@ -180,8 +312,14 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # async_std::task::block_on(async { - /// cake::Entity::insert_many(vec![apple, orange]).exec(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// let insert_result = cake::Entity::insert_many(vec![apple, orange]).exec(&db).await?; + /// + /// assert_eq!(insert_result.last_insert_id, 28); + /// // assert_eq!(insert_result.rows_affected, 2); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -199,10 +337,24 @@ pub trait EntityTrait: EntityName { Insert::many(models) } + /// Update an model in database + /// + /// - To apply where conditions / filters, see [`QueryFilter`](crate::query::QueryFilter) + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 0, + /// # rows_affected: 1, + /// # }, + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// @@ -212,8 +364,14 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # async_std::task::block_on(async { - /// fruit::Entity::update(orange).exec(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// assert_eq!( + /// fruit::Entity::update(orange.clone()).exec(&db).await?, // Clone here because we need to assert_eq + /// orange + /// ); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -229,19 +387,38 @@ pub trait EntityTrait: EntityName { Update::one(model) } + /// Update many models in database + /// + /// - To apply where conditions / filters, see [`QueryFilter`](crate::query::QueryFilter) + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 0, + /// # rows_affected: 5, + /// # }, + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::{Expr, Value}}; /// - /// # async_std::task::block_on(async { - /// fruit::Entity::update_many() + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// let update_result = fruit::Entity::update_many() /// .col_expr(fruit::Column::CakeId, Expr::value(Value::Null)) /// .filter(fruit::Column::Name.contains("Apple")) /// .exec(&db) - /// .await; + /// .await?; + /// + /// assert_eq!(update_result.rows_affected, 5); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -254,10 +431,24 @@ pub trait EntityTrait: EntityName { Update::many(Self::default()) } + /// Delete an model from database + /// + /// - To apply where conditions / filters, see [`QueryFilter`](crate::query::QueryFilter) + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 0, + /// # rows_affected: 1, + /// # }, + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// @@ -266,8 +457,13 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # async_std::task::block_on(async { - /// fruit::Entity::delete(orange).exec(&db).await; + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// let delete_result = fruit::Entity::delete(orange).exec(&db).await?; + /// + /// assert_eq!(delete_result.rows_affected, 1); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( @@ -283,18 +479,37 @@ pub trait EntityTrait: EntityName { Delete::one(model) } + /// Delete many models from database + /// + /// - To apply where conditions / filters, see [`QueryFilter`](crate::query::QueryFilter) + /// + /// # Example + /// /// ``` /// # #[cfg(feature = "mock")] - /// # use sea_orm::{MockDatabase, Transaction}; - /// # let db = MockDatabase::new().into_connection(); + /// # use sea_orm::{error::*, MockDatabase, MockExecResult, Transaction, tests_cfg::*}; + /// # + /// # let db = MockDatabase::new() + /// # .append_exec_results(vec![ + /// # MockExecResult { + /// # last_insert_id: 0, + /// # rows_affected: 5, + /// # }, + /// # ]) + /// # .into_connection(); /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// - /// # async_std::task::block_on(async { - /// fruit::Entity::delete_many() + /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # + /// let delete_result = fruit::Entity::delete_many() /// .filter(fruit::Column::Name.contains("Apple")) /// .exec(&db) - /// .await; + /// .await?; + /// + /// assert_eq!(delete_result.rows_affected, 5); + /// # + /// # Ok(()) /// # }); /// /// assert_eq!( diff --git a/src/entity/column.rs b/src/entity/column.rs index de98c0d7..d6580e8f 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -66,6 +66,7 @@ macro_rules! bind_vec_func { } // LINT: when the operand value does not match column type +/// Wrapper of the identically named method in [`sea_query::Expr`] pub trait ColumnTrait: IdenStatic + Iterable { type EntityName: EntityName; diff --git a/src/entity/model.rs b/src/entity/model.rs index 73dd66ae..15ebdb58 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -1,4 +1,4 @@ -use crate::{EntityTrait, DbErr, QueryFilter, QueryResult, Related, Select}; +use crate::{DbErr, EntityTrait, QueryFilter, QueryResult, Related, Select}; pub use sea_query::Value; use std::fmt::Debug; diff --git a/src/executor/delete.rs b/src/executor/delete.rs index eebd0fc9..bbdc3bb5 100644 --- a/src/executor/delete.rs +++ b/src/executor/delete.rs @@ -62,10 +62,7 @@ async fn exec_delete_only( } // Only Statement impl Send -async fn exec_delete( - statement: Statement, - db: &DatabaseConnection, -) -> Result { +async fn exec_delete(statement: Statement, db: &DatabaseConnection) -> Result { let result = db.execute(statement).await?; Ok(DeleteResult { rows_affected: result.rows_affected(), diff --git a/src/executor/insert.rs b/src/executor/insert.rs index cf36d5ff..784d95a0 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -40,10 +40,7 @@ impl Inserter { } // Only Statement impl Send -async fn exec_insert( - statement: Statement, - db: &DatabaseConnection, -) -> Result { +async fn exec_insert(statement: Statement, db: &DatabaseConnection) -> Result { let result = db.execute(statement).await?; // TODO: Postgres instead use query_one + returning clause Ok(InsertResult { diff --git a/src/executor/query.rs b/src/executor/query.rs index 2c10d0b6..75a73463 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -1,4 +1,5 @@ use crate::DbErr; +use chrono::NaiveDateTime; use std::fmt; #[derive(Debug)] @@ -56,12 +57,14 @@ macro_rules! try_getable_all { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get(column.as_str()).map_err(crate::sqlx_error_to_query_err) + row.try_get(column.as_str()) + .map_err(crate::sqlx_error_to_query_err) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; - row.try_get(column.as_str()).map_err(crate::sqlx_error_to_query_err) + row.try_get(column.as_str()) + .map_err(crate::sqlx_error_to_query_err) } #[cfg(feature = "mock")] QueryResultRow::Mock(row) => Ok(row.try_get(column.as_str())?), @@ -109,7 +112,8 @@ macro_rules! try_getable_mysql { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get(column.as_str()).map_err(crate::sqlx_error_to_query_err) + row.try_get(column.as_str()) + .map_err(crate::sqlx_error_to_query_err) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(_) => { @@ -160,6 +164,7 @@ try_getable_mysql!(u64); try_getable_all!(f32); try_getable_all!(f64); try_getable_all!(String); +try_getable_all!(NaiveDateTime); #[cfg(feature = "with-rust_decimal")] use rust_decimal::Decimal; @@ -172,14 +177,18 @@ impl TryGetable for Decimal { #[cfg(feature = "sqlx-mysql")] QueryResultRow::SqlxMySql(row) => { use sqlx::Row; - row.try_get(column.as_str()).map_err(crate::sqlx_error_to_query_err) + row.try_get(column.as_str()) + .map_err(crate::sqlx_error_to_query_err) } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(row) => { use sqlx::Row; - let val: f64 = row.try_get(column.as_str()).map_err(crate::sqlx_error_to_query_err)?; + let val: f64 = row + .try_get(column.as_str()) + .map_err(crate::sqlx_error_to_query_err)?; use rust_decimal::prelude::FromPrimitive; - Decimal::from_f64(val).ok_or_else(|| DbErr::Query("Failed to convert f64 into Decimal".to_owned())) + Decimal::from_f64(val) + .ok_or_else(|| DbErr::Query("Failed to convert f64 into Decimal".to_owned())) } #[cfg(feature = "mock")] QueryResultRow::Mock(row) => Ok(row.try_get(column.as_str())?), diff --git a/src/executor/update.rs b/src/executor/update.rs index 0a486afc..0f33c914 100644 --- a/src/executor/update.rs +++ b/src/executor/update.rs @@ -18,10 +18,7 @@ impl<'a, A: 'a> UpdateOne where A: ActiveModelTrait, { - pub fn exec( - self, - db: &'a DatabaseConnection, - ) -> impl Future> + 'a { + pub fn exec(self, db: &'a DatabaseConnection) -> impl Future> + 'a { // so that self is dropped before entering await exec_update_and_return_original(self.query, self.model, db) } @@ -74,10 +71,7 @@ where } // Only Statement impl Send -async fn exec_update( - statement: Statement, - db: &DatabaseConnection, -) -> Result { +async fn exec_update(statement: Statement, db: &DatabaseConnection) -> Result { let result = db.execute(statement).await?; Ok(UpdateResult { rows_affected: result.rows_affected(), diff --git a/src/lib.rs b/src/lib.rs index ae71656e..e80fb521 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,18 +173,18 @@ //! # } //! ``` //! ## License -//! +//! //! Licensed under either of -//! +//! //! - Apache License, Version 2.0 -//! ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) +//! ([LICENSE-APACHE](LICENSE-APACHE) or ) //! - MIT license -//! ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) -//! +//! ([LICENSE-MIT](LICENSE-MIT) or ) +//! //! at your option. -//! +//! //! ## Contribution -//! +//! //! Unless you explicitly state otherwise, any contribution intentionally submitted //! for inclusion in the work by you, as defined in the Apache-2.0 license, shall be //! dual licensed as above, without any additional terms or conditions. @@ -215,5 +215,5 @@ pub use sea_orm_macros::{ }; pub use sea_query; pub use sea_query::Iden; -pub use strum::EnumIter; pub use strum; +pub use strum::EnumIter; diff --git a/src/query/json.rs b/src/query/json.rs index aa3d9c6c..98cf49b9 100644 --- a/src/query/json.rs +++ b/src/query/json.rs @@ -1,4 +1,4 @@ -use crate::{FromQueryResult, DbErr, QueryResult, QueryResultRow}; +use crate::{DbErr, FromQueryResult, QueryResult, QueryResultRow}; use serde_json::Map; pub use serde_json::Value as JsonValue; diff --git a/src/query/traits.rs b/src/query/traits.rs index e4e6c77d..1d1d2914 100644 --- a/src/query/traits.rs +++ b/src/query/traits.rs @@ -1,4 +1,4 @@ -use crate::Statement; +use crate::{Statement, Syntax}; use sea_query::{QueryBuilder, QueryStatementBuilder}; pub trait QueryTrait { @@ -16,8 +16,12 @@ pub trait QueryTrait { /// Build the query as [`Statement`] fn build(&self, builder: B) -> Statement where - B: QueryBuilder, + B: QueryBuilderWithSyntax, { - self.as_query().build(builder).into() + Statement::from_string_values_tuple(builder.syntax(), self.as_query().build(builder)) } } + +pub trait QueryBuilderWithSyntax: QueryBuilder { + fn syntax(&self) -> Syntax; +} diff --git a/tests/bakery_chain/order.rs b/tests/bakery_chain/order.rs index de38df60..41bfb03f 100644 --- a/tests/bakery_chain/order.rs +++ b/tests/bakery_chain/order.rs @@ -1,3 +1,4 @@ +use chrono::NaiveDateTime; use rust_decimal::prelude::*; use sea_orm::entity::prelude::*; @@ -16,7 +17,7 @@ pub struct Model { pub total: Decimal, pub bakery_id: Option, pub customer_id: Option, - pub placed_at: String, + pub placed_at: NaiveDateTime, } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/tests/basic.rs b/tests/basic.rs index 641fc1aa..620fe41d 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,4 +1,4 @@ -use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, DbConn}; +use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, DbConn, Statement, Syntax}; mod setup; @@ -27,7 +27,9 @@ async fn setup_schema(db: &DbConn) { .col(ColumnDef::new(cake::Column::Name).string()) .build(SqliteQueryBuilder); - let result = db.execute(stmt.into()).await; + let result = db + .execute(Statement::from_string(Syntax::Sqlite, stmt)) + .await; println!("Create table cake: {:?}", result); } diff --git a/tests/crud/create_lineitem.rs b/tests/crud/create_lineitem.rs index ac7af1ca..46411801 100644 --- a/tests/crud/create_lineitem.rs +++ b/tests/crud/create_lineitem.rs @@ -1,4 +1,5 @@ pub use super::*; +use chrono::offset::Utc; use rust_decimal_macros::dec; pub async fn test_create_lineitem(db: &DbConn) { @@ -64,7 +65,7 @@ pub async fn test_create_lineitem(db: &DbConn) { let order_1 = order::ActiveModel { bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)), customer_id: Set(Some(customer_insert_res.last_insert_id as i32)), - placed_at: Set("placeholder".to_string()), + placed_at: Set(Utc::now().naive_utc()), ..Default::default() }; let order_insert_res: InsertResult = Order::insert(order_1) diff --git a/tests/crud/create_order.rs b/tests/crud/create_order.rs index d3769203..5b0cd42d 100644 --- a/tests/crud/create_order.rs +++ b/tests/crud/create_order.rs @@ -1,4 +1,5 @@ pub use super::*; +use chrono::offset::Utc; use rust_decimal_macros::dec; pub async fn test_create_order(db: &DbConn) { @@ -65,7 +66,7 @@ pub async fn test_create_order(db: &DbConn) { bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)), customer_id: Set(Some(customer_insert_res.last_insert_id as i32)), total: Set(dec!(15.10)), - placed_at: Set("placeholder".to_string()), + placed_at: Set(Utc::now().naive_utc()), ..Default::default() }; let order_insert_res: InsertResult = Order::insert(order_1) diff --git a/tests/schema/mod.rs b/tests/schema/mod.rs index 6ebcc381..3fe859eb 100644 --- a/tests/schema/mod.rs +++ b/tests/schema/mod.rs @@ -4,208 +4,208 @@ use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, TableCreateState pub use super::bakery_chain::*; async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result { - let builder = db.get_schema_builder_backend(); - db.execute(builder.build(stmt)).await + let builder = db.get_schema_builder_backend(); + db.execute(builder.build(stmt)).await } pub async fn create_bakery_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(bakery::Entity) - .if_not_exists() - .col( - ColumnDef::new(bakery::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(bakery::Column::Name).string()) - .col(ColumnDef::new(bakery::Column::ProfitMargin).float()) - .to_owned(); + let stmt = sea_query::Table::create() + .table(bakery::Entity) + .if_not_exists() + .col( + ColumnDef::new(bakery::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(bakery::Column::Name).string()) + .col(ColumnDef::new(bakery::Column::ProfitMargin).float()) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } pub async fn create_baker_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(baker::Entity) - .if_not_exists() - .col( - ColumnDef::new(baker::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(baker::Column::Name).string()) - .col(ColumnDef::new(baker::Column::BakeryId).integer()) - .foreign_key( - ForeignKey::create() - .name("FK_baker_bakery") - .from(baker::Entity, baker::Column::BakeryId) - .to(bakery::Entity, bakery::Column::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .to_owned(); + let stmt = sea_query::Table::create() + .table(baker::Entity) + .if_not_exists() + .col( + ColumnDef::new(baker::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(baker::Column::Name).string()) + .col(ColumnDef::new(baker::Column::BakeryId).integer()) + .foreign_key( + ForeignKey::create() + .name("FK_baker_bakery") + .from(baker::Entity, baker::Column::BakeryId) + .to(bakery::Entity, bakery::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } pub async fn create_customer_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(customer::Entity) - .if_not_exists() - .col( - ColumnDef::new(customer::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(customer::Column::Name).string()) - .col(ColumnDef::new(customer::Column::Notes).text()) - .to_owned(); + let stmt = sea_query::Table::create() + .table(customer::Entity) + .if_not_exists() + .col( + ColumnDef::new(customer::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(customer::Column::Name).string()) + .col(ColumnDef::new(customer::Column::Notes).text()) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } pub async fn create_order_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(order::Entity) - .if_not_exists() - .col( - ColumnDef::new(order::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(order::Column::Total).float()) - .col(ColumnDef::new(order::Column::BakeryId).integer().not_null()) - .col( - ColumnDef::new(order::Column::CustomerId) - .integer() - .not_null(), - ) - .col( - ColumnDef::new(order::Column::PlacedAt) - .date_time() - .not_null(), - ) - .foreign_key( - ForeignKey::create() - .name("FK_order_bakery") - .from(order::Entity, order::Column::BakeryId) - .to(bakery::Entity, bakery::Column::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .foreign_key( - ForeignKey::create() - .name("FK_order_customer") - .from(order::Entity, order::Column::CustomerId) - .to(customer::Entity, customer::Column::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .to_owned(); + let stmt = sea_query::Table::create() + .table(order::Entity) + .if_not_exists() + .col( + ColumnDef::new(order::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(order::Column::Total).float()) + .col(ColumnDef::new(order::Column::BakeryId).integer().not_null()) + .col( + ColumnDef::new(order::Column::CustomerId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(order::Column::PlacedAt) + .date_time() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("FK_order_bakery") + .from(order::Entity, order::Column::BakeryId) + .to(bakery::Entity, bakery::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("FK_order_customer") + .from(order::Entity, order::Column::CustomerId) + .to(customer::Entity, customer::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } pub async fn create_lineitem_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(lineitem::Entity) - .if_not_exists() - .col( - ColumnDef::new(lineitem::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(lineitem::Column::Price).decimal()) - .col(ColumnDef::new(lineitem::Column::Quantity).integer()) - .col( - ColumnDef::new(lineitem::Column::OrderId) - .integer() - .not_null(), - ) - .col( - ColumnDef::new(lineitem::Column::CakeId) - .integer() - .not_null(), - ) - .foreign_key( - ForeignKey::create() - .name("FK_lineitem_order") - .from(lineitem::Entity, lineitem::Column::OrderId) - .to(order::Entity, order::Column::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .foreign_key( - ForeignKey::create() - .name("FK_lineitem_cake") - .from(lineitem::Entity, lineitem::Column::CakeId) - .to(cake::Entity, cake::Column::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .to_owned(); + let stmt = sea_query::Table::create() + .table(lineitem::Entity) + .if_not_exists() + .col( + ColumnDef::new(lineitem::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(lineitem::Column::Price).decimal()) + .col(ColumnDef::new(lineitem::Column::Quantity).integer()) + .col( + ColumnDef::new(lineitem::Column::OrderId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(lineitem::Column::CakeId) + .integer() + .not_null(), + ) + .foreign_key( + ForeignKey::create() + .name("FK_lineitem_order") + .from(lineitem::Entity, lineitem::Column::OrderId) + .to(order::Entity, order::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .name("FK_lineitem_cake") + .from(lineitem::Entity, lineitem::Column::CakeId) + .to(cake::Entity, cake::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } pub async fn create_cakes_bakers_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(cakes_bakers::Entity) - .if_not_exists() - .col( - ColumnDef::new(cakes_bakers::Column::CakeId) - .integer() - .not_null(), - ) - .col( - ColumnDef::new(cakes_bakers::Column::BakerId) - .integer() - .not_null(), - ) - .primary_key( - Index::create() - .col(cakes_bakers::Column::CakeId) - .col(cakes_bakers::Column::BakerId), - ) - .to_owned(); + let stmt = sea_query::Table::create() + .table(cakes_bakers::Entity) + .if_not_exists() + .col( + ColumnDef::new(cakes_bakers::Column::CakeId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(cakes_bakers::Column::BakerId) + .integer() + .not_null(), + ) + .primary_key( + Index::create() + .col(cakes_bakers::Column::CakeId) + .col(cakes_bakers::Column::BakerId), + ) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await } pub async fn create_cake_table(db: &DbConn) -> Result { - let stmt = sea_query::Table::create() - .table(cake::Entity) - .if_not_exists() - .col( - ColumnDef::new(cake::Column::Id) - .integer() - .not_null() - .auto_increment() - .primary_key(), - ) - .col(ColumnDef::new(cake::Column::Name).string()) - .col(ColumnDef::new(cake::Column::Price).float()) - .col(ColumnDef::new(cake::Column::BakeryId).integer().not_null()) - .foreign_key( - ForeignKey::create() - .name("FK_cake_bakery") - .from(cake::Entity, cake::Column::BakeryId) - .to(bakery::Entity, bakery::Column::Id) - .on_delete(ForeignKeyAction::Cascade) - .on_update(ForeignKeyAction::Cascade), - ) - .col(ColumnDef::new(cake::Column::GlutenFree).boolean()) - .to_owned(); + let stmt = sea_query::Table::create() + .table(cake::Entity) + .if_not_exists() + .col( + ColumnDef::new(cake::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(cake::Column::Name).string()) + .col(ColumnDef::new(cake::Column::Price).float()) + .col(ColumnDef::new(cake::Column::BakeryId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("FK_cake_bakery") + .from(cake::Entity, cake::Column::BakeryId) + .to(bakery::Entity, bakery::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .on_update(ForeignKeyAction::Cascade), + ) + .col(ColumnDef::new(cake::Column::GlutenFree).boolean()) + .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt).await }