diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a94a83e3..8853cca3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,22 +1,33 @@ -name: Rust +name: sea-orm on: push: - branches: [ master ] + branches: + - master pull_request: - branches: [ master ] + branches: + - master env: CARGO_TERM_COLOR: always jobs: - build: - + test: + name: Unit Test runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions-rs/cargo@v1 + with: + command: build + + - uses: actions-rs/cargo@v1 + with: + command: test diff --git a/Cargo.toml b/Cargo.toml index 43488456..02a11051 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "sea-orm-cli", "examples/sqlx-mysql", "examples/codegen", + "examples/cli", ] [package] diff --git a/Diesel.md b/Diesel.md index d0680179..080c7154 100644 --- a/Diesel.md +++ b/Diesel.md @@ -20,7 +20,7 @@ SeaSchema is our schema discovery library, but it is not sealed inside SeaORM. S In addition to the sync vs async foundation, the biggest distinction between Diesel and SeaORM is static vs dynamic. Diesel has an everything-compile-time design which has its pros and cons. SeaORM is dynamic, in which things are established runtime. It offers more flexibility. While you loses some compile-time guarantee, SeaORM helps you to prove correctness by unit testing instead. -Both libraries make heavy use of traits and generics, but SeaORM generate less types. (Each column in Diesel is a struct, while each column in SeaORM is a enum variant). That probably means looser type/lifetime constraints and faster compilation. +Both libraries make heavy use of traits and generics, but SeaORM generate less types (each column in Diesel is a struct, while each column in SeaORM is a enum variant) and less depthness (there won't be `A>>`). That probably means looser type/lifetime constraints and faster compilation. You don't have to use macros when using SeaORM. We provide some derive macros for convenience, but they are entirely optional. diff --git a/examples/cli/.env b/examples/cli/.env new file mode 100644 index 00000000..e08d4810 --- /dev/null +++ b/examples/cli/.env @@ -0,0 +1,2 @@ +DATABASE_URI=mysql://sea:sea@localhost/bakery +DATABASE_SCHEMA=bakery \ No newline at end of file diff --git a/examples/cli/Cargo.toml b/examples/cli/Cargo.toml new file mode 100644 index 00000000..979e847a --- /dev/null +++ b/examples/cli/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sea-orm-cli-example" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +sea-orm = { path = "../../", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "debug-print" ] } +strum = { version = "^0.20", features = [ "derive" ] } diff --git a/examples/cli/README.md b/examples/cli/README.md new file mode 100644 index 00000000..1d34c06e --- /dev/null +++ b/examples/cli/README.md @@ -0,0 +1,18 @@ +# SeaORM CLI Example + +Prepare: + +Setup a test database and configure the connection string in `.env`. +Run `bakery.sql` to setup the test table and data. + +Building sea-orm-cli: + +```sh +(cd ../../sea-orm-cli ; cargo build) +``` + +Generating entity: + +```sh +../../target/debug/sea-orm-cli generate entity -o src/entity +``` diff --git a/examples/cli/src/entity/cake.rs b/examples/cli/src/entity/cake.rs new file mode 100644 index 00000000..9b786a5f --- /dev/null +++ b/examples/cli/src/entity/cake.rs @@ -0,0 +1,83 @@ +//! 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 Model { + pub fn find_cake_filling(&self) -> Select { + Entity::find_related().belongs_to::(self) + } + pub fn find_fruit(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/cli/src/entity/cake_filling.rs b/examples/cli/src/entity/cake_filling.rs new file mode 100644 index 00000000..d5b4b8b6 --- /dev/null +++ b/examples/cli/src/entity/cake_filling.rs @@ -0,0 +1,90 @@ +//! 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 Model { + pub fn find_cake(&self) -> Select { + Entity::find_related().belongs_to::(self) + } + pub fn find_filling(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/cli/src/entity/filling.rs b/examples/cli/src/entity/filling.rs new file mode 100644 index 00000000..e4563e55 --- /dev/null +++ b/examples/cli/src/entity/filling.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 { + "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 Model { + pub fn find_cake_filling(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/out/fruit.rs b/examples/cli/src/entity/fruit.rs similarity index 92% rename from examples/codegen/src/out/fruit.rs rename to examples/cli/src/entity/fruit.rs index 6e190749..f65d5b38 100644 --- a/examples/codegen/src/out/fruit.rs +++ b/examples/cli/src/entity/fruit.rs @@ -47,7 +47,7 @@ impl ColumnTrait for Column { match self { Self::Id => ColumnType::Integer.def(), Self::Name => ColumnType::String(Some(255u32)).def(), - Self::CakeId => ColumnType::Integer.def(), + Self::CakeId => ColumnType::Integer.def().null(), } } } @@ -55,7 +55,7 @@ impl ColumnTrait for Column { impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Cake => Entity::has_one(super::cake::Entity) + Self::Cake => Entity::belongs_to(super::cake::Entity) .from(Column::CakeId) .to(super::cake::Column::Id) .into(), diff --git a/examples/codegen/src/out/mod.rs b/examples/cli/src/entity/mod.rs similarity index 100% rename from examples/codegen/src/out/mod.rs rename to examples/cli/src/entity/mod.rs diff --git a/examples/codegen/src/out/prelude.rs b/examples/cli/src/entity/prelude.rs similarity index 100% rename from examples/codegen/src/out/prelude.rs rename to examples/cli/src/entity/prelude.rs diff --git a/examples/cli/src/entity/vendor.rs b/examples/cli/src/entity/vendor.rs new file mode 100644 index 00000000..9c4ca7dd --- /dev/null +++ b/examples/cli/src/entity/vendor.rs @@ -0,0 +1,78 @@ +//! 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(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Fruit => Entity::has_one(super::fruit::Entity) + .from(Column::FruitId) + .to(super::fruit::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl Model { + pub fn find_fruit(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs new file mode 100644 index 00000000..1a7baf89 --- /dev/null +++ b/examples/cli/src/main.rs @@ -0,0 +1,3 @@ +mod entity; + +fn main() {} diff --git a/examples/codegen/src/out/cake.rs b/examples/codegen/src/entity/cake.rs similarity index 100% rename from examples/codegen/src/out/cake.rs rename to examples/codegen/src/entity/cake.rs diff --git a/examples/codegen/src/out/cake_filling.rs b/examples/codegen/src/entity/cake_filling.rs similarity index 100% rename from examples/codegen/src/out/cake_filling.rs rename to examples/codegen/src/entity/cake_filling.rs diff --git a/examples/codegen/src/out/filling.rs b/examples/codegen/src/entity/filling.rs similarity index 100% rename from examples/codegen/src/out/filling.rs rename to examples/codegen/src/entity/filling.rs diff --git a/examples/codegen/src/entity/fruit.rs b/examples/codegen/src/entity/fruit.rs new file mode 100644 index 00000000..d3f21bf9 --- /dev/null +++ b/examples/codegen/src/entity/fruit.rs @@ -0,0 +1,92 @@ +//! 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(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Cake => Entity::has_one(super::cake::Entity) + .from(Column::CakeId) + .to(super::cake::Column::Id) + .into(), + Self::Vendor => Entity::has_many(super::vendor::Entity) + .from(Column::Id) + .to(super::vendor::Column::FruitId) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Vendor.def() + } +} + +impl Model { + pub fn find_cake(&self) -> Select { + Entity::find_related().belongs_to::(self) + } + pub fn find_vendor(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/entity/mod.rs b/examples/codegen/src/entity/mod.rs new file mode 100644 index 00000000..395d29f9 --- /dev/null +++ b/examples/codegen/src/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/examples/codegen/src/entity/prelude.rs b/examples/codegen/src/entity/prelude.rs new file mode 100644 index 00000000..b4e85c78 --- /dev/null +++ b/examples/codegen/src/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/examples/codegen/src/entity/vendor.rs b/examples/codegen/src/entity/vendor.rs new file mode 100644 index 00000000..9c4ca7dd --- /dev/null +++ b/examples/codegen/src/entity/vendor.rs @@ -0,0 +1,78 @@ +//! 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(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Fruit => Entity::has_one(super::fruit::Entity) + .from(Column::FruitId) + .to(super::fruit::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } +} + +impl Model { + pub fn find_fruit(&self) -> Select { + Entity::find_related().belongs_to::(self) + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/codegen/src/main.rs b/examples/codegen/src/main.rs index dcc034b2..f15463dd 100644 --- a/examples/codegen/src/main.rs +++ b/examples/codegen/src/main.rs @@ -1,4 +1,4 @@ -mod out; +mod entity; use sea_orm_codegen::{EntityGenerator, Error}; @@ -10,7 +10,7 @@ async fn main() -> Result<(), Error> { let _generator = EntityGenerator::discover(uri, schema) .await? .transform()? - .generate("src/out")?; + .generate("src/entity")?; Ok(()) } diff --git a/examples/sqlx-mysql/Readme.md b/examples/sqlx-mysql/Readme.md index f7e5d7bc..77a90bd6 100644 --- a/examples/sqlx-mysql/Readme.md +++ b/examples/sqlx-mysql/Readme.md @@ -32,15 +32,27 @@ Model { id: 2, name: "Rasberry", cake_id: Some(1) } Model { id: 3, name: "Strawberry", cake_id: Some(2) } +Model { id: 4, name: "Apple", cake_id: None } + +Model { id: 5, name: "Banana", cake_id: None } + +Model { id: 6, name: "Cherry", cake_id: None } + +Model { id: 7, name: "Lemon", cake_id: None } + +Model { id: 8, name: "Orange", cake_id: None } + +Model { id: 9, name: "Pineapple", cake_id: None } + ===== ===== find cakes and fruits: SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`, `fruit`.`id` AS `B_id`, `fruit`.`name` AS `B_name`, `fruit`.`cake_id` AS `B_cake_id` FROM `cake` LEFT JOIN `fruit` ON `cake`.`id` = `fruit`.`cake_id` -(Model { id: 1, name: "New York Cheese" }, Model { id: 2, name: "Rasberry", cake_id: Some(1) }) +(Model { id: 1, name: "New York Cheese" }, Some(Model { id: 1, name: "Blueberry", cake_id: Some(1) })) -(Model { id: 1, name: "New York Cheese" }, Model { id: 1, name: "Blueberry", cake_id: Some(1) }) +(Model { id: 1, name: "New York Cheese" }, Some(Model { id: 2, name: "Rasberry", cake_id: Some(1) })) -(Model { id: 2, name: "Chocolate Forest" }, Model { id: 3, name: "Strawberry", cake_id: Some(2) }) +(Model { id: 2, name: "Chocolate Forest" }, Some(Model { id: 3, name: "Strawberry", cake_id: Some(2) })) ===== ===== @@ -48,7 +60,7 @@ find one by primary key: SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `ca Model { id: 1, name: "New York Cheese" } -find one by like: SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`name` LIKE '%chocolate%' LIMIT 1 +find one by name: SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`name` LIKE '%chocolate%' LIMIT 1 Some(Model { id: 2, name: "Chocolate Forest" }) @@ -68,15 +80,11 @@ SelectResult { name: "Chocolate Forest", num_of_fruits: 1 } ===== ===== -find cakes and fillings: SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`, `filling`.`id` AS `B_id`, `filling`.`name` AS `B_name` FROM `cake` LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id` LEFT JOIN `filling` ON `cake_filling`.`filling_id` = `filling`.`id` +find cakes and fillings: SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`, `filling`.`id` AS `B_id`, `filling`.`name` AS `B_name` FROM `cake` LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id` LEFT JOIN `filling` ON `cake_filling`.`filling_id` = `filling`.`id` ORDER BY `cake`.`id` ASC -(Model { id: 1, name: "New York Cheese" }, Model { id: 1, name: "Vanilla" }) +(Model { id: 1, name: "New York Cheese" }, [Model { id: 1, name: "Vanilla" }, Model { id: 2, name: "Lemon" }]) -(Model { id: 1, name: "New York Cheese" }, Model { id: 2, name: "Lemon" }) - -(Model { id: 2, name: "Chocolate Forest" }, Model { id: 2, name: "Lemon" }) - -(Model { id: 2, name: "Chocolate Forest" }, Model { id: 3, name: "Mango" }) +(Model { id: 2, name: "Chocolate Forest" }, [Model { id: 2, name: "Lemon" }, Model { id: 3, name: "Mango" }]) find fillings for cheese cake: SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` = 1 LIMIT 1 SELECT `filling`.`id`, `filling`.`name` FROM `filling` INNER JOIN `cake_filling` ON `cake_filling`.`filling_id` = `filling`.`id` INNER JOIN `cake` ON `cake`.`id` = `cake_filling`.`cake_id` WHERE `cake`.`id` = 1 diff --git a/examples/sqlx-mysql/src/example_cake.rs b/examples/sqlx-mysql/src/example_cake.rs index 003d700c..475315e8 100644 --- a/examples/sqlx-mysql/src/example_cake.rs +++ b/examples/sqlx-mysql/src/example_cake.rs @@ -51,10 +51,7 @@ impl ColumnTrait for Column { impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Fruit => Entity::has_many(super::fruit::Entity) - .from(Column::Id) - .to(super::fruit::Column::CakeId) - .into(), + Self::Fruit => Entity::has_many(super::fruit::Entity).into(), } } } @@ -75,14 +72,4 @@ impl Related for Entity { } } -impl Model { - pub fn find_fruit(&self) -> Select { - Entity::find_related().belongs_to::(self) - } - - pub fn find_filling(&self) -> Select { - Entity::find_related().belongs_to::(self) - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/sqlx-mysql/src/example_cake_filling.rs b/examples/sqlx-mysql/src/example_cake_filling.rs index d1974a04..19de83e4 100644 --- a/examples/sqlx-mysql/src/example_cake_filling.rs +++ b/examples/sqlx-mysql/src/example_cake_filling.rs @@ -53,11 +53,11 @@ impl ColumnTrait for Column { impl RelationTrait for Relation { fn def(&self) -> RelationDef { match self { - Self::Cake => Entity::has_one(super::cake::Entity) + Self::Cake => Entity::belongs_to(super::cake::Entity) .from(Column::CakeId) .to(super::cake::Column::Id) .into(), - Self::Filling => Entity::has_one(super::filling::Entity) + Self::Filling => Entity::belongs_to(super::filling::Entity) .from(Column::FillingId) .to(super::filling::Column::Id) .into(), diff --git a/examples/sqlx-mysql/src/example_filling.rs b/examples/sqlx-mysql/src/example_filling.rs index d2dece8a..925b92fc 100644 --- a/examples/sqlx-mysql/src/example_filling.rs +++ b/examples/sqlx-mysql/src/example_filling.rs @@ -62,10 +62,4 @@ impl Related for Entity { } } -impl Model { - pub fn find_cake(&self) -> Select { - Entity::find_related().belongs_to::(self) - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/sqlx-mysql/src/example_fruit.rs b/examples/sqlx-mysql/src/example_fruit.rs index 48daac55..b875da24 100644 --- a/examples/sqlx-mysql/src/example_fruit.rs +++ b/examples/sqlx-mysql/src/example_fruit.rs @@ -35,7 +35,9 @@ impl PrimaryKeyTrait for PrimaryKey { } #[derive(Copy, Clone, Debug, EnumIter)] -pub enum Relation {} +pub enum Relation { + Cake, +} impl ColumnTrait for Column { type EntityName = Entity; @@ -51,7 +53,18 @@ impl ColumnTrait for Column { impl RelationTrait for Relation { fn def(&self) -> RelationDef { - panic!() + match self { + Self::Cake => Entity::belongs_to(super::cake::Entity) + .from(Column::CakeId) + .to(super::cake::Column::Id) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Cake.def() } } diff --git a/examples/sqlx-mysql/src/operation.rs b/examples/sqlx-mysql/src/operation.rs index 2eb95607..d7a5ed78 100644 --- a/examples/sqlx-mysql/src/operation.rs +++ b/examples/sqlx-mysql/src/operation.rs @@ -20,12 +20,12 @@ pub async fn insert_and_update(db: &DbConn) -> Result<(), ExecErr> { name: Set("pear".to_owned()), ..Default::default() }; - let res = Fruit::insert(pear).exec(db).await?; + let res: InsertResult = Fruit::insert(pear).exec(db).await?; println!(); - println!("Inserted: {:?}\n", res); + println!("Inserted: last_insert_id = {}\n", res.last_insert_id); - let pear = Fruit::find_by_id(res.last_insert_id) + let pear: Option = Fruit::find_by_id(res.last_insert_id) .one(db) .await .map_err(|_| ExecErr)?; @@ -36,10 +36,10 @@ pub async fn insert_and_update(db: &DbConn) -> Result<(), ExecErr> { let mut pear: fruit::ActiveModel = pear.unwrap().into(); pear.name = Set("Sweet pear".to_owned()); - let res = Fruit::update(pear).exec(db).await?; + let pear: fruit::ActiveModel = Fruit::update(pear).exec(db).await?; println!(); - println!("Updated: {:?}\n", res); + println!("Updated: {:?}\n", pear); Ok(()) } diff --git a/examples/sqlx-mysql/src/select.rs b/examples/sqlx-mysql/src/select.rs index d624476d..0fe9c921 100644 --- a/examples/sqlx-mysql/src/select.rs +++ b/examples/sqlx-mysql/src/select.rs @@ -44,7 +44,7 @@ pub async fn all_about_select(db: &DbConn) -> Result<(), QueryErr> { async fn find_all(db: &DbConn) -> Result<(), QueryErr> { print!("find all cakes: "); - let cakes = Cake::find().all(db).await?; + let cakes: Vec = Cake::find().all(db).await?; println!(); for cc in cakes.iter() { @@ -66,7 +66,10 @@ async fn find_all(db: &DbConn) -> Result<(), QueryErr> { async fn find_together(db: &DbConn) -> Result<(), QueryErr> { print!("find cakes and fruits: "); - let both = Cake::find().left_join_and_select(Fruit).all(db).await?; + let both = Cake::find() + .find_also_related(Fruit) + .all(db) + .await?; println!(); for bb in both.iter() { @@ -102,7 +105,7 @@ async fn find_one(db: &DbConn) -> Result<(), QueryErr> { print!("find models belong to: "); - let fruits = cheese.find_fruit().all(db).await?; + let fruits = cheese.find_related(Fruit).all(db).await?; println!(); for ff in fruits.iter() { @@ -141,7 +144,10 @@ async fn count_fruits_by_cake(db: &DbConn) -> Result<(), QueryErr> { async fn find_many_to_many(db: &DbConn) -> Result<(), QueryErr> { print!("find cakes and fillings: "); - let both = Cake::find().left_join_and_select(Filling).all(db).await?; + let both: Vec<(cake::Model, Vec)> = Cake::find() + .find_with_related(Filling) + .all(db) + .await?; println!(); for bb in both.iter() { @@ -153,7 +159,7 @@ async fn find_many_to_many(db: &DbConn) -> Result<(), QueryErr> { let cheese = Cake::find_by_id(1).one(db).await?; if let Some(cheese) = cheese { - let fillings: Vec = cheese.find_filling().all(db).await?; + let fillings: Vec = cheese.find_related(Filling).all(db).await?; println!(); for ff in fillings.iter() { @@ -166,7 +172,7 @@ async fn find_many_to_many(db: &DbConn) -> Result<(), QueryErr> { let lemon = Filling::find_by_id(2).one(db).await?; if let Some(lemon) = lemon { - let cakes: Vec = lemon.find_cake().all(db).await?; + let cakes: Vec = lemon.find_related(Cake).all(db).await?; println!(); for cc in cakes.iter() { @@ -211,7 +217,7 @@ async fn find_together_json(db: &DbConn) -> Result<(), QueryErr> { print!("find cakes and fruits: "); let cakes_fruits = Cake::find() - .left_join_and_select(Fruit) + .find_with_related(Fruit) .into_json() .all(db) .await?; diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 88a69588..677954f6 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -12,10 +12,11 @@ keywords = [ "orm", "database", "sql", "mysql", "postgres", "sqlite", "cli" ] publish = false [[bin]] -name = "sea-orm" +name = "sea-orm-cli" path = "src/main.rs" [dependencies] clap = { version = "^2.33.3" } +dotenv = { version = "^0.15" } async-std = { version = "^1.9", features = [ "attributes" ] } sea-orm-codegen = { path = "../sea-orm-codegen" } diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index 7bedbb90..474dec5f 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -1,11 +1,11 @@ use clap::{App, AppSettings, Arg, SubCommand}; pub fn build_cli() -> App<'static, 'static> { - let entity_subcommand = SubCommand::with_name("entity") - .about("Entity related commands") + let entity_subcommand = SubCommand::with_name("generate") + .about("Codegen related commands") .setting(AppSettings::VersionlessSubcommands) .subcommand( - SubCommand::with_name("generate") + SubCommand::with_name("entity") .about("Generate entity") .arg( Arg::with_name("DATABASE_URI") @@ -13,7 +13,8 @@ pub fn build_cli() -> App<'static, 'static> { .short("u") .help("Database URI") .takes_value(true) - .required(true), + .required(true) + .env("DATABASE_URI"), ) .arg( Arg::with_name("DATABASE_SCHEMA") @@ -21,7 +22,8 @@ pub fn build_cli() -> App<'static, 'static> { .short("s") .help("Database schema") .takes_value(true) - .required(true), + .required(true) + .env("DATABASE_SCHEMA"), ) .arg( Arg::with_name("OUTPUT_DIR") diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index ad0451ad..0726e17b 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -1,4 +1,5 @@ use clap::ArgMatches; +use dotenv::dotenv; use sea_orm_codegen::EntityGenerator; use std::{error::Error, fmt::Display}; @@ -6,19 +7,21 @@ mod cli; #[async_std::main] async fn main() { + dotenv().ok(); + let matches = cli::build_cli().get_matches(); match matches.subcommand() { - ("entity", Some(matches)) => run_entity_command(matches) + ("generate", Some(matches)) => run_generate_command(matches) .await .unwrap_or_else(handle_error), _ => unreachable!("You should never see this message"), } } -async fn run_entity_command(matches: &ArgMatches<'_>) -> Result<(), Box> { +async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box> { match matches.subcommand() { - ("generate", Some(args)) => { + ("entity", Some(args)) => { let uri = args.value_of("DATABASE_URI").unwrap(); let schema = args.value_of("DATABASE_SCHEMA").unwrap(); let output_dir = args.value_of("OUTPUT_DIR").unwrap(); diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index b575057f..e8a0b543 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -9,6 +9,7 @@ pub struct Column { pub(crate) col_type: ColumnType, pub(crate) auto_increment: bool, pub(crate) not_null: bool, + pub(crate) unique: bool, } impl Column { @@ -50,7 +51,7 @@ impl Column { } pub fn get_def(&self) -> TokenStream { - match &self.col_type { + let mut col_def = match &self.col_type { ColumnType::Char(s) => match s { Some(s) => quote! { ColumnType::Char(Some(#s)).def() }, None => quote! { ColumnType::Char(None).def() }, @@ -86,7 +87,18 @@ impl Column { let s = s.to_string(); quote! { ColumnType::Custom(#s.to_owned()).def() } } + }; + if !self.not_null { + col_def.extend(quote! { + .null() + }); } + if self.unique { + col_def.extend(quote! { + .unique() + }); + } + col_def } } @@ -115,11 +127,21 @@ impl From<&ColumnDef> for Column { }) .collect(); let not_null = !not_nulls.is_empty(); + let uniques: Vec = col_def + .get_column_spec() + .iter() + .filter_map(|spec| match spec { + ColumnSpec::UniqueKey => Some(true), + _ => None, + }) + .collect(); + let unique = !uniques.is_empty(); Self { name, col_type, auto_increment, not_null, + unique, } } } diff --git a/sea-orm-codegen/src/entity/entity.rs b/sea-orm-codegen/src/entity/entity.rs index 788002ce..2abb4a7a 100644 --- a/sea-orm-codegen/src/entity/entity.rs +++ b/sea-orm-codegen/src/entity/entity.rs @@ -86,6 +86,10 @@ impl Entity { .collect() } + pub fn get_relation_defs(&self) -> Vec { + self.relations.iter().map(|rel| rel.get_def()).collect() + } + pub fn get_relation_rel_types(&self) -> Vec { self.relations .iter() diff --git a/sea-orm-codegen/src/entity/relation.rs b/sea-orm-codegen/src/entity/relation.rs index bbcfd155..8eab372d 100644 --- a/sea-orm-codegen/src/entity/relation.rs +++ b/sea-orm-codegen/src/entity/relation.rs @@ -1,9 +1,15 @@ use heck::{CamelCase, SnakeCase}; -use proc_macro2::Ident; -use quote::format_ident; -use sea_orm::RelationType; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; use sea_query::TableForeignKey; +#[derive(Clone, Debug)] +pub enum RelationType { + HasOne, + HasMany, + BelongsTo, +} + #[derive(Clone, Debug)] pub struct Relation { pub(crate) ref_table: String, @@ -21,10 +27,33 @@ impl Relation { format_ident!("{}", self.ref_table.to_camel_case()) } + pub fn get_def(&self) -> TokenStream { + let rel_type = self.get_rel_type(); + let ref_table_snake_case = self.get_ref_table_snake_case(); + match self.rel_type { + RelationType::HasOne | RelationType::HasMany => { + quote! { + Entity::#rel_type(super::#ref_table_snake_case::Entity).into() + } + } + RelationType::BelongsTo => { + let column_camel_case = self.get_column_camel_case(); + let ref_column_camel_case = self.get_ref_column_camel_case(); + quote! { + Entity::#rel_type(super::#ref_table_snake_case::Entity) + .from(Column::#column_camel_case) + .to(super::#ref_table_snake_case::Column::#ref_column_camel_case) + .into() + } + } + } + } + pub fn get_rel_type(&self) -> Ident { match self.rel_type { RelationType::HasOne => format_ident!("has_one"), RelationType::HasMany => format_ident!("has_many"), + RelationType::BelongsTo => format_ident!("belongs_to"), } } @@ -49,7 +78,7 @@ impl From<&TableForeignKey> for Relation { }; let columns = tbl_fk.get_columns(); let ref_columns = tbl_fk.get_ref_columns(); - let rel_type = RelationType::HasOne; + let rel_type = RelationType::BelongsTo; Self { ref_table, columns, diff --git a/sea-orm-codegen/src/entity/transformer.rs b/sea-orm-codegen/src/entity/transformer.rs index d437d0f3..02c7fb82 100644 --- a/sea-orm-codegen/src/entity/transformer.rs +++ b/sea-orm-codegen/src/entity/transformer.rs @@ -1,5 +1,4 @@ -use crate::{Entity, EntityWriter, Error, PrimaryKey, Relation}; -use sea_orm::RelationType; +use crate::{Column, Entity, EntityWriter, Error, PrimaryKey, Relation, RelationType}; use sea_query::TableStatement; use sea_schema::mysql::def::Schema; use std::{collections::HashMap, mem::swap}; @@ -31,11 +30,16 @@ impl EntityTransformer { )) } }; - let columns = table_create + let columns: Vec = table_create .get_columns() .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() @@ -64,9 +68,22 @@ impl EntityTransformer { entities.push(entity); for mut rel in relations.into_iter() { let ref_table = rel.ref_table; - swap(&mut rel.columns, &mut rel.ref_columns); - rel.rel_type = RelationType::HasMany; + let mut unique = true; + for col in rel.columns.iter() { + if !unique_columns.contains(col) { + unique = false; + break; + } + } + let rel_type = if unique { + RelationType::HasOne + } else { + RelationType::HasMany + }; + rel.rel_type = rel_type; rel.ref_table = table_name.clone(); + rel.columns = Vec::new(); + rel.ref_columns = Vec::new(); if let Some(vec) = inverse_relations.get_mut(&ref_table) { vec.push(rel); } else { diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 73e12d07..01dd4a4c 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -217,20 +217,14 @@ impl EntityWriter { pub fn gen_impl_relation_trait(entity: &Entity) -> TokenStream { let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case(); - let relation_rel_types = entity.get_relation_rel_types(); - let relation_ref_tables_snake_case = entity.get_relation_ref_tables_snake_case(); - let relation_columns_camel_case = entity.get_relation_columns_camel_case(); - let relation_ref_columns_camel_case = entity.get_relation_ref_columns_camel_case(); + let relation_defs = entity.get_relation_defs(); let quoted = if relation_ref_tables_camel_case.is_empty() { quote! { _ => panic!("No RelationDef"), } } else { quote! { - #(Self::#relation_ref_tables_camel_case => Entity::#relation_rel_types(super::#relation_ref_tables_snake_case::Entity) - .from(Column::#relation_columns_camel_case) - .to(super::#relation_ref_tables_snake_case::Column::#relation_ref_columns_camel_case) - .into()),* + #(Self::#relation_ref_tables_camel_case => #relation_defs),* } }; quote! { diff --git a/src/database/connection.rs b/src/database/connection.rs index e2dfbb9b..95911c29 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -1,4 +1,4 @@ -use crate::{ExecErr, ExecResult, QueryErr, QueryResult, Statement}; +use crate::{ExecErr, ExecResult, QueryErr, QueryResult, Statement, Transaction}; use sea_query::{ MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementBuilder, SqliteQueryBuilder, }; @@ -118,6 +118,12 @@ impl DatabaseConnection { pub fn as_mock_connection(&self) -> Option { None } + + #[cfg(feature = "mock")] + pub fn into_transaction_log(self) -> Vec { + let mut mocker = self.as_mock_connection().get_mocker_mutex().lock().unwrap(); + mocker.drain_transaction_log() + } } impl QueryBuilderBackend { diff --git a/src/database/transaction.rs b/src/database/transaction.rs index d31a9303..ff9bba4f 100644 --- a/src/database/transaction.rs +++ b/src/database/transaction.rs @@ -1,4 +1,5 @@ use crate::Statement; +use sea_query::{Value, Values}; #[derive(Debug, Clone, PartialEq)] pub struct Transaction { @@ -6,6 +7,16 @@ pub struct Transaction { } impl Transaction { + pub fn from_sql_and_values(sql: &str, values: I) -> Self + where + I: IntoIterator, + { + Self::one(Statement { + sql: sql.to_owned(), + values: Some(Values(values.into_iter().collect())), + }) + } + /// Create a Transaction with one statement pub fn one(stmt: Statement) -> Self { Self { stmts: vec![stmt] } diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index f306cb6a..dd35a423 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -42,17 +42,6 @@ impl Default for ActiveValueState { } } -pub trait OneOrManyActiveModel -where - A: ActiveModelTrait, -{ - fn is_one() -> bool; - fn get_one(self) -> A; - - fn is_many() -> bool; - fn get_many(self) -> Vec; -} - #[doc(hidden)] pub fn unchanged_active_value_not_intended_for_public_use(value: V) -> ActiveValue where @@ -197,44 +186,6 @@ where } } -impl OneOrManyActiveModel for A -where - A: ActiveModelTrait, -{ - fn is_one() -> bool { - true - } - fn get_one(self) -> A { - self - } - - fn is_many() -> bool { - false - } - fn get_many(self) -> Vec { - panic!("not many") - } -} - -impl OneOrManyActiveModel for Vec -where - A: ActiveModelTrait, -{ - fn is_one() -> bool { - false - } - fn get_one(self) -> A { - panic!("not one") - } - - fn is_many() -> bool { - true - } - fn get_many(self) -> Vec { - self - } -} - /// Insert the model if primary key is unset, update otherwise. /// Only works if the entity has auto increment primary key. pub async fn save_active_model(mut am: A, db: &DatabaseConnection) -> Result diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index 1553acd7..1717ce19 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -1,7 +1,7 @@ use crate::{ - ActiveModelTrait, ColumnTrait, Delete, DeleteOne, FromQueryResult, Insert, ModelTrait, - OneOrManyActiveModel, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, Related, - RelationBuilder, RelationTrait, RelationType, Select, Update, UpdateOne, + ActiveModelTrait, ColumnTrait, Delete, DeleteMany, DeleteOne, FromQueryResult, Insert, + ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, Related, RelationBuilder, + RelationTrait, RelationType, Select, Update, UpdateMany, UpdateOne, }; use sea_query::{Iden, IntoValueTuple}; use std::fmt::Debug; @@ -50,14 +50,27 @@ pub trait EntityTrait: EntityName { } /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().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; + /// # }); /// /// assert_eq!( - /// cake::Entity::find() - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"SELECT "cake"."id", "cake"."name" FROM "cake""# - /// ); + /// db.into_transaction_log(), + /// vec![ + /// Transaction::from_sql_and_values( + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, vec![1u64.into()] + /// ), + /// Transaction::from_sql_and_values( + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] + /// ), + /// ]); /// ``` fn find() -> Select { Select::new() @@ -65,28 +78,42 @@ pub trait EntityTrait: EntityName { /// Find a model by primary key /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().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; + /// # }); /// /// assert_eq!( - /// cake::Entity::find_by_id(11) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = 11"# - /// ); + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()] + /// )]); /// ``` /// Find by composite key /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake_filling, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().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; + /// # }); /// /// assert_eq!( - /// cake_filling::Entity::find_by_id((2, 3)) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// [ - /// r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#, - /// r#"WHERE "cake_filling"."cake_id" = 2 AND "cake_filling"."filling_id" = 3"#, - /// ].join(" ") - /// ); + /// 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()] + /// )]); /// ``` fn find_by_id(values: V) -> Select where @@ -108,69 +135,29 @@ pub trait EntityTrait: EntityName { select } - /// Insert one /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// /// let apple = cake::ActiveModel { /// name: Set("Apple Pie".to_owned()), /// ..Default::default() /// }; - /// assert_eq!( - /// cake::Entity::insert(apple) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#, - /// ); - /// ``` - /// Insert many - /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::PostgresQueryBuilder}; /// - /// let apple = cake::ActiveModel { - /// name: Set("Apple Pie".to_owned()), - /// ..Default::default() - /// }; - /// let orange = cake::ActiveModel { - /// name: Set("Orange Scone".to_owned()), - /// ..Default::default() - /// }; - /// assert_eq!( - /// cake::Entity::insert(vec![apple, orange]) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie'), ('Orange Scone')"#, - /// ); - /// ``` - fn insert(models: C) -> Insert - where - A: ActiveModelTrait, - C: OneOrManyActiveModel, - { - if C::is_one() { - Self::insert_one(models.get_one()) - } else if C::is_many() { - Self::insert_many(models.get_many()) - } else { - unreachable!() - } - } - - /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::PostgresQueryBuilder}; + /// # async_std::task::block_on(async { + /// cake::Entity::insert(apple).exec(&db).await; + /// # }); /// - /// let apple = cake::ActiveModel { - /// name: Set("Apple Pie".to_owned()), - /// ..Default::default() - /// }; /// assert_eq!( - /// cake::Entity::insert_one(apple) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#, - /// ); + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// r#"INSERT INTO "cake" ("name") VALUES ($1)"#, vec!["Apple Pie".into()] + /// )]); /// ``` - fn insert_one(model: A) -> Insert + fn insert(model: A) -> Insert where A: ActiveModelTrait, { @@ -178,7 +165,11 @@ pub trait EntityTrait: EntityName { } /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// /// let apple = cake::ActiveModel { /// name: Set("Apple Pie".to_owned()), @@ -188,12 +179,17 @@ pub trait EntityTrait: EntityName { /// name: Set("Orange Scone".to_owned()), /// ..Default::default() /// }; + /// + /// # async_std::task::block_on(async { + /// cake::Entity::insert_many(vec![apple, orange]).exec(&db).await; + /// # }); + /// /// assert_eq!( - /// cake::Entity::insert_many(vec![apple, orange]) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie'), ('Orange Scone')"#, - /// ); + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// r#"INSERT INTO "cake" ("name") VALUES ($1), ($2)"#, + /// vec!["Apple Pie".into(), "Orange Scone".into()] + /// )]); /// ``` fn insert_many(models: I) -> Insert where @@ -204,19 +200,27 @@ pub trait EntityTrait: EntityName { } /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// /// let orange = fruit::ActiveModel { /// id: Set(1), /// name: Set("Orange".to_owned()), /// ..Default::default() /// }; + /// + /// # async_std::task::block_on(async { + /// fruit::Entity::update(orange).exec(&db).await; + /// # }); + /// /// assert_eq!( - /// fruit::Entity::update(orange) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"UPDATE "fruit" SET "name" = 'Orange' WHERE "fruit"."id" = 1"#, - /// ); + /// 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()] + /// )]); /// ``` fn update(model: A) -> UpdateOne where @@ -226,18 +230,51 @@ pub trait EntityTrait: EntityName { } /// ``` - /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::PostgresQueryBuilder}; + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::{Expr, Value}}; + /// + /// # async_std::task::block_on(async { + /// fruit::Entity::update_many() + /// .col_expr(fruit::Column::CakeId, Expr::value(Value::Null)) + /// .filter(fruit::Column::Name.contains("Apple")) + /// .exec(&db) + /// .await; + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, vec![Value::Null, "%Apple%".into()] + /// )]); + /// ``` + fn update_many() -> UpdateMany { + Update::many(Self::default()) + } + + /// ``` + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// /// let orange = fruit::ActiveModel { /// id: Set(3), /// ..Default::default() /// }; + /// + /// # async_std::task::block_on(async { + /// fruit::Entity::delete(orange).exec(&db).await; + /// # }); + /// /// assert_eq!( - /// fruit::Entity::delete(orange) - /// .build(PostgresQueryBuilder) - /// .to_string(), - /// r#"DELETE FROM "fruit" WHERE "fruit"."id" = 3"#, - /// ); + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, vec![3i32.into()] + /// )]); /// ``` fn delete(model: A) -> DeleteOne where @@ -245,4 +282,28 @@ pub trait EntityTrait: EntityName { { Delete::one(model) } + + /// ``` + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{MockDatabase, Transaction}; + /// # let db = MockDatabase::new().into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; + /// + /// # async_std::task::block_on(async { + /// fruit::Entity::delete_many() + /// .filter(fruit::Column::Name.contains("Apple")) + /// .exec(&db) + /// .await; + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// r#"DELETE FROM "fruit" WHERE "fruit"."name" LIKE $1"#, vec!["%Apple%".into()] + /// )]); + /// ``` + fn delete_many() -> DeleteMany { + Delete::many(Self::default()) + } } diff --git a/src/entity/model.rs b/src/entity/model.rs index eab2e533..6161538b 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -1,4 +1,4 @@ -use crate::{EntityTrait, QueryResult, TypeErr}; +use crate::{EntityTrait, QueryFilter, QueryResult, Related, Select, TypeErr}; pub use sea_query::Value; use std::fmt::Debug; @@ -8,10 +8,25 @@ pub trait ModelTrait: Clone + Debug { fn get(&self, c: ::Column) -> Value; fn set(&mut self, c: ::Column, v: Value); + + fn find_related(&self, _: R) -> Select + where + R: EntityTrait, + Self::Entity: Related, + { + >::find_related().belongs_to(self) + } } pub trait FromQueryResult { fn from_query_result(res: &QueryResult, pre: &str) -> Result where Self: Sized; + + fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result, TypeErr> + where + Self: Sized, + { + Ok(Self::from_query_result(res, pre).ok()) + } } diff --git a/src/executor/paginator.rs b/src/executor/paginator.rs index e40c4dc4..ead5114b 100644 --- a/src/executor/paginator.rs +++ b/src/executor/paginator.rs @@ -175,9 +175,7 @@ mod tests { query_builder.build(select.offset(4).limit(2)), ]; - let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); - - assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts)); + assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts)); Ok(()) } @@ -211,9 +209,7 @@ mod tests { query_builder.build(select.offset(4).limit(2)), ]; - let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); - - assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts)); + assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts)); Ok(()) } @@ -244,9 +240,8 @@ mod tests { let query_builder = db.get_query_builder_backend(); let stmts = vec![query_builder.build(&select)]; - let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); - assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts)); + assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts)); Ok(()) } @@ -297,9 +292,7 @@ mod tests { query_builder.build(select.offset(4).limit(2)), ]; - let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); - - assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts)); + assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts)); Ok(()) } @@ -331,9 +324,7 @@ mod tests { query_builder.build(select.offset(4).limit(2)), ]; - let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); - - assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts)); + assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts)); Ok(()) } } diff --git a/src/executor/select.rs b/src/executor/select.rs index d4d5830f..6c74c8cb 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -1,6 +1,7 @@ use crate::{ - query::combine, DatabaseConnection, EntityTrait, FromQueryResult, JsonValue, Paginator, - QueryErr, QueryResult, Select, SelectTwo, TypeErr, + query::combine, DatabaseConnection, EntityTrait, FromQueryResult, Iterable, JsonValue, + ModelTrait, Paginator, PrimaryKeyToColumn, QueryErr, QueryResult, Select, SelectTwo, + SelectTwoMany, TypeErr, }; use sea_query::SelectStatement; use std::marker::PhantomData; @@ -52,12 +53,12 @@ where M: FromQueryResult + Sized, N: FromQueryResult + Sized, { - type Item = (M, N); + type Item = (M, Option); fn from_raw_query_result(res: QueryResult) -> Result { Ok(( M::from_query_result(&res, combine::SELECT_A)?, - N::from_query_result(&res, combine::SELECT_B)?, + N::from_query_result_optional(&res, combine::SELECT_B)?, )) } } @@ -128,15 +129,58 @@ where pub async fn one( self, db: &DatabaseConnection, - ) -> Result, QueryErr> { + ) -> Result)>, QueryErr> { self.into_model::().one(db).await } - pub async fn all(self, db: &DatabaseConnection) -> Result, QueryErr> { + pub async fn all( + self, + db: &DatabaseConnection, + ) -> Result)>, QueryErr> { self.into_model::().all(db).await } } +impl SelectTwoMany +where + E: EntityTrait, + F: EntityTrait, +{ + fn into_model(self) -> Selector> + where + M: FromQueryResult, + N: FromQueryResult, + { + Selector { + query: self.query, + selector: SelectTwoModel { model: PhantomData }, + } + } + + #[cfg(feature = "with-json")] + pub fn into_json(self) -> Selector> { + Selector { + query: self.query, + selector: SelectTwoModel { model: PhantomData }, + } + } + + pub async fn one( + self, + db: &DatabaseConnection, + ) -> Result)>, QueryErr> { + self.into_model::().one(db).await + } + + pub async fn all( + self, + db: &DatabaseConnection, + ) -> Result)>, QueryErr> { + let rows = self.into_model::().all(db).await?; + Ok(consolidate_query_result::(rows)) + } +} + impl Selector where S: SelectorTrait, @@ -171,3 +215,39 @@ where } } } + +fn consolidate_query_result( + rows: Vec<(L::Model, Option)>, +) -> Vec<(L::Model, Vec)> +where + L: EntityTrait, + R: EntityTrait, +{ + let mut acc: Vec<(L::Model, Vec)> = Vec::new(); + for (l, r) in rows { + if let Some((last_l, last_r)) = acc.last_mut() { + let mut same_l = true; + for pk_col in ::iter() { + let col = pk_col.into_column(); + let val = l.get(col); + let last_val = last_l.get(col); + if !val.eq(&last_val) { + same_l = false; + break; + } + } + if same_l { + if let Some(r) = r { + last_r.push(r); + continue; + } + } + } + if r.is_some() { + acc.push((l, vec![r.unwrap()])); + } else { + acc.push((l, vec![])); + } + } + acc +} diff --git a/src/lib.rs b/src/lib.rs index 89b83a4d..35571f42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,134 @@ +//! # Select +//! ``` +//! # use sea_orm::{DbConn, entity::*, query::*, tests_cfg::*}; +//! # async fn function(db: &DbConn) -> Result<(), QueryErr> { +//! # +//! // find all models +//! let cakes: Vec = Cake::find().all(db).await?; +//! +//! // find and filter +//! let chocolate: Vec = Cake::find() +//! .filter(cake::Column::Name.contains("chocolate")) +//! .all(db) +//! .await?; +//! +//! // find one model +//! let cheese: Option = Cake::find_by_id(1).one(db).await?; +//! let cheese: cake::Model = cheese.unwrap(); +//! +//! // find related models (lazy) +//! 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?; +//! +//! # Ok(()) +//! # } +//! ``` +//! # Insert +//! ``` +//! # use sea_orm::{DbConn, entity::*, query::*, tests_cfg::*}; +//! # async fn function(db: &DbConn) -> Result<(), ExecErr> { +//! # +//! let apple = fruit::ActiveModel { +//! name: Set("Apple".to_owned()), +//! ..Default::default() // no need to set primary key +//! }; +//! +//! let pear = fruit::ActiveModel { +//! name: Set("Pear".to_owned()), +//! ..Default::default() +//! }; +//! +//! // insert one +//! let res: InsertResult = Fruit::insert(pear).exec(db).await?; +//! +//! println!("InsertResult: {}", res.last_insert_id); +//! # +//! # Ok(()) +//! # } +//! # +//! # async fn function2(db: &DbConn) -> Result<(), ExecErr> { +//! # let apple = fruit::ActiveModel { +//! # name: Set("Apple".to_owned()), +//! # ..Default::default() // no need to set primary key +//! # }; +//! # +//! # let pear = fruit::ActiveModel { +//! # name: Set("Pear".to_owned()), +//! # ..Default::default() +//! # }; +//! +//! // insert many +//! Fruit::insert_many(vec![apple, pear]).exec(db).await?; +//! # +//! # Ok(()) +//! # } +//! ``` +//! # Update +//! ``` +//! # use sea_orm::{DbConn, entity::*, query::*, tests_cfg::*}; +//! # +//! use sea_orm::sea_query::{Expr, Value}; +//! +//! # async fn function(db: &DbConn) -> Result<(), QueryErr> { +//! let pear: Option = Fruit::find_by_id(1).one(db).await?; +//! # Ok(()) +//! # } +//! # +//! # async fn function2(db: &DbConn) -> Result<(), ExecErr> { +//! # let pear: Option = Fruit::find_by_id(1).one(db).await.unwrap(); +//! +//! let mut pear: fruit::ActiveModel = pear.unwrap().into(); +//! pear.name = Set("Sweet pear".to_owned()); +//! +//! // update one +//! let pear: fruit::ActiveModel = Fruit::update(pear).exec(db).await?; +//! +//! // update many: UPDATE "fruit" SET "cake_id" = NULL WHERE "fruit"."name" LIKE '%Apple%' +//! Fruit::update_many() +//! .col_expr(fruit::Column::CakeId, Expr::value(Value::Null)) +//! .filter(fruit::Column::Name.contains("Apple")) +//! .exec(db) +//! .await?; +//! +//! # Ok(()) +//! # } +//! ``` +//! # Delete +//! ``` +//! # use sea_orm::{DbConn, entity::*, query::*, tests_cfg::*}; +//! # +//! # async fn function(db: &DbConn) -> Result<(), QueryErr> { +//! let orange: Option = Fruit::find_by_id(1).one(db).await?; +//! # Ok(()) +//! # } +//! # +//! # async fn function2(db: &DbConn) -> Result<(), ExecErr> { +//! # let orange: Option = Fruit::find_by_id(1).one(db).await.unwrap(); +//! let orange: fruit::ActiveModel = orange.unwrap().into(); +//! +//! // delete one +//! fruit::Entity::delete(orange).exec(db).await?; +//! +//! // delete many: DELETE FROM "fruit" WHERE "fruit"."name" LIKE 'Orange' +//! fruit::Entity::delete_many() +//! .filter(fruit::Column::Name.contains("Orange")) +//! .exec(db) +//! .await?; +//! +//! # Ok(()) +//! # } +//! ``` mod database; mod driver; pub mod entity; mod executor; pub mod query; +#[doc(hidden)] pub mod tests_cfg; mod util; diff --git a/src/query/combine.rs b/src/query/combine.rs index 2cde583f..f52b7925 100644 --- a/src/query/combine.rs +++ b/src/query/combine.rs @@ -1,7 +1,7 @@ -use crate::{EntityTrait, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo}; +use crate::{EntityTrait, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany}; use core::marker::PhantomData; pub use sea_query::JoinType; -use sea_query::{Alias, ColumnRef, Iden, SeaRc, SelectExpr, SelectStatement, SimpleExpr}; +use sea_query::{Alias, ColumnRef, Iden, Order, SeaRc, SelectExpr, SelectStatement, SimpleExpr}; pub const SELECT_A: &str = "A_"; pub const SELECT_B: &str = "B_"; @@ -40,6 +40,14 @@ where self = self.apply_alias(SELECT_A); SelectTwo::new(self.into_query()) } + + pub fn select_with(mut self, _: F) -> SelectTwoMany + where + F: EntityTrait, + { + self = self.apply_alias(SELECT_A); + SelectTwoMany::new(self.into_query()) + } } impl SelectTwo @@ -48,25 +56,60 @@ where F: EntityTrait, { pub(crate) fn new(query: SelectStatement) -> Self { - let myself = Self { + Self { query, entity: PhantomData, - }; - myself.prepare_select() + } + .prepare_select() } fn prepare_select(mut self) -> Self { - for col in ::iter() { - let alias = format!("{}{}", SELECT_B, col.to_string().as_str()); - self.query.expr(SelectExpr { - expr: col.into_simple_expr(), - alias: Some(SeaRc::new(Alias::new(&alias))), - }); + prepare_select_two::(&mut self); + self + } +} + +impl SelectTwoMany +where + E: EntityTrait, + F: EntityTrait, +{ + pub(crate) fn new(query: SelectStatement) -> Self { + Self { + query, + entity: PhantomData, + } + .prepare_select() + .prepare_order_by() + } + + fn prepare_select(mut self) -> Self { + prepare_select_two::(&mut self); + self + } + + fn prepare_order_by(mut self) -> Self { + for col in ::iter() { + self.query.order_by((E::default(), col), Order::Asc); } self } } +fn prepare_select_two(selector: &mut S) +where + F: EntityTrait, + S: QueryTrait, +{ + for col in ::iter() { + let alias = format!("{}{}", SELECT_B, col.to_string().as_str()); + selector.query().expr(SelectExpr { + expr: col.into_simple_expr(), + alias: Some(SeaRc::new(Alias::new(&alias))), + }); + } +} + #[cfg(test)] mod tests { use crate::tests_cfg::{cake, fruit}; @@ -101,6 +144,23 @@ mod tests { ); } + #[test] + fn select_with_1() { + assert_eq!( + cake::Entity::find() + .left_join(fruit::Entity) + .select_with(fruit::Entity) + .build(MysqlQueryBuilder) + .to_string(), + [ + "SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,", + "`fruit`.`id` AS `B_id`, `fruit`.`name` AS `B_name`, `fruit`.`cake_id` AS `B_cake_id`", + "FROM `cake` LEFT JOIN `fruit` ON `cake`.`id` = `fruit`.`cake_id`", + "ORDER BY `cake`.`id` ASC", + ].join(" ") + ); + } + #[test] fn select_also_2() { assert_eq!( @@ -119,4 +179,24 @@ mod tests { ].join(" ") ); } + + #[test] + fn select_with_2() { + assert_eq!( + cake::Entity::find() + .left_join(fruit::Entity) + .select_with(fruit::Entity) + .filter(cake::Column::Id.eq(1)) + .filter(fruit::Column::Id.eq(2)) + .build(MysqlQueryBuilder) + .to_string(), + [ + "SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,", + "`fruit`.`id` AS `B_id`, `fruit`.`name` AS `B_name`, `fruit`.`cake_id` AS `B_cake_id`", + "FROM `cake` LEFT JOIN `fruit` ON `cake`.`id` = `fruit`.`cake_id`", + "WHERE `cake`.`id` = 1 AND `fruit`.`id` = 2", + "ORDER BY `cake`.`id` ASC", + ].join(" ") + ); + } } diff --git a/src/query/helper.rs b/src/query/helper.rs index da01fab4..7044c63d 100644 --- a/src/query/helper.rs +++ b/src/query/helper.rs @@ -258,11 +258,11 @@ pub trait QueryFilter: Sized { } /// Apply a where condition using the model's primary key - fn belongs_to(mut self, model: &E::Model) -> Self + fn belongs_to(mut self, model: &M) -> Self where - E: EntityTrait, + M: ModelTrait, { - for key in E::PrimaryKey::iter() { + for key in ::PrimaryKey::iter() { let col = key.into_column(); self = self.filter(col.eq(model.get(col))); } diff --git a/src/query/join.rs b/src/query/join.rs index 4302b2e7..4481f5af 100644 --- a/src/query/join.rs +++ b/src/query/join.rs @@ -1,4 +1,4 @@ -use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo}; +use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo, SelectTwoMany}; pub use sea_query::JoinType; impl Select @@ -41,19 +41,28 @@ where } /// Left Join with a Related Entity and select both Entity. - pub fn left_join_and_select(self, r: R) -> SelectTwo + pub fn find_also_related(self, r: R) -> SelectTwo where R: EntityTrait, E: Related, { self.left_join(r).select_also(r) } + + /// Left Join with a Related Entity and select the related Entity as a `Vec` + pub fn find_with_related(self, r: R) -> SelectTwoMany + where + R: EntityTrait, + E: Related, + { + self.left_join(r).select_with(r) + } } #[cfg(test)] mod tests { use crate::tests_cfg::{cake, filling, fruit}; - use crate::{ColumnTrait, EntityTrait, QueryFilter, QueryTrait}; + use crate::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryTrait}; use sea_query::MysqlQueryBuilder; #[test] @@ -130,7 +139,10 @@ mod tests { }; assert_eq!( - cake_model.find_fruit().build(MysqlQueryBuilder).to_string(), + cake_model + .find_related(fruit::Entity) + .build(MysqlQueryBuilder) + .to_string(), [ "SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit`", "INNER JOIN `cake` ON `cake`.`id` = `fruit`.`cake_id`", diff --git a/src/query/mod.rs b/src/query/mod.rs index 7f32edc5..51060a1e 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -20,4 +20,4 @@ pub use select::*; pub use traits::*; pub use update::*; -pub use crate::executor::{ExecErr, QueryErr}; +pub use crate::executor::{ExecErr, InsertResult, QueryErr, UpdateResult}; diff --git a/src/query/select.rs b/src/query/select.rs index af483908..91db2907 100644 --- a/src/query/select.rs +++ b/src/query/select.rs @@ -23,6 +23,16 @@ where pub(crate) entity: PhantomData<(E, F)>, } +#[derive(Clone, Debug)] +pub struct SelectTwoMany +where + E: EntityTrait, + F: EntityTrait, +{ + pub(crate) query: SelectStatement, + pub(crate) entity: PhantomData<(E, F)>, +} + pub trait IntoSimpleExpr { fn into_simple_expr(self) -> SimpleExpr; } @@ -51,6 +61,18 @@ macro_rules! impl_trait { &mut self.query } } + + impl $trait for SelectTwoMany + where + E: EntityTrait, + F: EntityTrait, + { + type QueryStatement = SelectStatement; + + fn query(&mut self) -> &mut SelectStatement { + &mut self.query + } + } }; } @@ -118,19 +140,26 @@ where } } -impl QueryTrait for SelectTwo -where - E: EntityTrait, - F: EntityTrait, -{ - type QueryStatement = SelectStatement; - fn query(&mut self) -> &mut SelectStatement { - &mut self.query - } - fn as_query(&self) -> &SelectStatement { - &self.query - } - fn into_query(self) -> SelectStatement { - self.query - } +macro_rules! select_two { + ( $selector: ident ) => { + impl QueryTrait for $selector + where + E: EntityTrait, + F: EntityTrait, + { + type QueryStatement = SelectStatement; + fn query(&mut self) -> &mut SelectStatement { + &mut self.query + } + fn as_query(&self) -> &SelectStatement { + &self.query + } + fn into_query(self) -> SelectStatement { + self.query + } + } + }; } + +select_two!(SelectTwo); +select_two!(SelectTwoMany); diff --git a/src/tests_cfg/cake.rs b/src/tests_cfg/cake.rs index c5ba7348..f8a35d6c 100644 --- a/src/tests_cfg/cake.rs +++ b/src/tests_cfg/cake.rs @@ -73,14 +73,4 @@ impl Related for Entity { } } -impl Model { - pub fn find_fruit(&self) -> Select { - Entity::find_related().belongs_to::(self) - } - - pub fn find_filling(&self) -> Select { - Entity::find_related().belongs_to::(self) - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/src/tests_cfg/filling.rs b/src/tests_cfg/filling.rs index 9dbe78f1..a246d146 100644 --- a/src/tests_cfg/filling.rs +++ b/src/tests_cfg/filling.rs @@ -63,10 +63,4 @@ impl Related for Entity { } } -impl Model { - pub fn find_cake(&self) -> Select { - Entity::find_related().belongs_to::(self) - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/src/tests_cfg/mod.rs b/src/tests_cfg/mod.rs index e39a16f3..ca9b8a68 100644 --- a/src/tests_cfg/mod.rs +++ b/src/tests_cfg/mod.rs @@ -4,3 +4,8 @@ pub mod cake; pub mod cake_filling; pub mod filling; pub mod fruit; + +pub use cake::Entity as Cake; +pub use cake_filling::Entity as CakeFilling; +pub use filling::Entity as Filling; +pub use fruit::Entity as Fruit;