Merge branch 'master' into ss/bakery

This commit is contained in:
Sam Samai 2021-06-28 10:30:06 +10:00
commit 59322d37d8
53 changed files with 1288 additions and 306 deletions

View File

@ -1,22 +1,33 @@
name: Rust name: sea-orm
on: on:
push: push:
branches: [ master ] branches:
- master
pull_request: pull_request:
branches: [ master ] branches:
- master
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
jobs: jobs:
build: test:
name: Unit Test
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Build
run: cargo build --verbose - uses: actions-rs/toolchain@v1
- name: Run tests with:
run: cargo test --verbose profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: build
- uses: actions-rs/cargo@v1
with:
command: test

View File

@ -6,6 +6,7 @@ members = [
"sea-orm-cli", "sea-orm-cli",
"examples/sqlx-mysql", "examples/sqlx-mysql",
"examples/codegen", "examples/codegen",
"examples/cli",
] ]
[package] [package]

View File

@ -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. 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<B<C<D>>>`). 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. You don't have to use macros when using SeaORM. We provide some derive macros for convenience, but they are entirely optional.

2
examples/cli/.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URI=mysql://sea:sea@localhost/bakery
DATABASE_SCHEMA=bakery

9
examples/cli/Cargo.toml Normal file
View File

@ -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" ] }

18
examples/cli/README.md Normal file
View File

@ -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
```

View File

@ -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<super::cake_filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::CakeFilling.def()
}
}
impl Related<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Model {
pub fn find_cake_filling(&self) -> Select<super::cake_filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -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<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl Related<super::filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::Filling.def()
}
}
impl Model {
pub fn find_cake(&self) -> Select<super::cake::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_filling(&self) -> Select<super::filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -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<super::cake_filling::Entity> for Entity {
fn to() -> RelationDef {
Relation::CakeFilling.def()
}
}
impl Model {
pub fn find_cake_filling(&self) -> Select<super::cake_filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -47,7 +47,7 @@ impl ColumnTrait for Column {
match self { match self {
Self::Id => ColumnType::Integer.def(), Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(Some(255u32)).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 { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { match self {
Self::Cake => Entity::has_one(super::cake::Entity) Self::Cake => Entity::belongs_to(super::cake::Entity)
.from(Column::CakeId) .from(Column::CakeId)
.to(super::cake::Column::Id) .to(super::cake::Column::Id)
.into(), .into(),

View File

@ -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<i32>,
}
#[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<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Model {
pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

3
examples/cli/src/main.rs Normal file
View File

@ -0,0 +1,3 @@
mod entity;
fn main() {}

View File

@ -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<i32>,
}
#[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<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
}
}
impl Related<super::vendor::Entity> for Entity {
fn to() -> RelationDef {
Relation::Vendor.def()
}
}
impl Model {
pub fn find_cake(&self) -> Select<super::cake::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_vendor(&self) -> Select<super::vendor::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -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;

View File

@ -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;

View File

@ -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<i32>,
}
#[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<super::fruit::Entity> for Entity {
fn to() -> RelationDef {
Relation::Fruit.def()
}
}
impl Model {
pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -1,4 +1,4 @@
mod out; mod entity;
use sea_orm_codegen::{EntityGenerator, Error}; use sea_orm_codegen::{EntityGenerator, Error};
@ -10,7 +10,7 @@ async fn main() -> Result<(), Error> {
let _generator = EntityGenerator::discover(uri, schema) let _generator = EntityGenerator::discover(uri, schema)
.await? .await?
.transform()? .transform()?
.generate("src/out")?; .generate("src/entity")?;
Ok(()) Ok(())
} }

View File

@ -32,15 +32,27 @@ Model { id: 2, name: "Rasberry", cake_id: Some(1) }
Model { id: 3, name: "Strawberry", cake_id: Some(2) } 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` 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" } 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" }) 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: 3, name: "Mango" }])
(Model { id: 2, name: "Chocolate Forest" }, Model { id: 2, name: "Lemon" })
(Model { id: 2, name: "Chocolate Forest" }, Model { id: 3, name: "Mango" })
find fillings for cheese cake: SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` = 1 LIMIT 1 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 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

View File

@ -51,10 +51,7 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { match self {
Self::Fruit => Entity::has_many(super::fruit::Entity) Self::Fruit => Entity::has_many(super::fruit::Entity).into(),
.from(Column::Id)
.to(super::fruit::Column::CakeId)
.into(),
} }
} }
} }
@ -75,14 +72,4 @@ impl Related<super::filling::Entity> for Entity {
} }
} }
impl Model {
pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_filling(&self) -> Select<super::filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -53,11 +53,11 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
match self { match self {
Self::Cake => Entity::has_one(super::cake::Entity) Self::Cake => Entity::belongs_to(super::cake::Entity)
.from(Column::CakeId) .from(Column::CakeId)
.to(super::cake::Column::Id) .to(super::cake::Column::Id)
.into(), .into(),
Self::Filling => Entity::has_one(super::filling::Entity) Self::Filling => Entity::belongs_to(super::filling::Entity)
.from(Column::FillingId) .from(Column::FillingId)
.to(super::filling::Column::Id) .to(super::filling::Column::Id)
.into(), .into(),

View File

@ -62,10 +62,4 @@ impl Related<super::cake::Entity> for Entity {
} }
} }
impl Model {
pub fn find_cake(&self) -> Select<super::cake::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -35,7 +35,9 @@ impl PrimaryKeyTrait for PrimaryKey {
} }
#[derive(Copy, Clone, Debug, EnumIter)] #[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {} pub enum Relation {
Cake,
}
impl ColumnTrait for Column { impl ColumnTrait for Column {
type EntityName = Entity; type EntityName = Entity;
@ -51,7 +53,18 @@ impl ColumnTrait for Column {
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { 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<super::cake::Entity> for Entity {
fn to() -> RelationDef {
Relation::Cake.def()
} }
} }

View File

@ -20,12 +20,12 @@ pub async fn insert_and_update(db: &DbConn) -> Result<(), ExecErr> {
name: Set("pear".to_owned()), name: Set("pear".to_owned()),
..Default::default() ..Default::default()
}; };
let res = Fruit::insert(pear).exec(db).await?; let res: InsertResult = Fruit::insert(pear).exec(db).await?;
println!(); 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::Model> = Fruit::find_by_id(res.last_insert_id)
.one(db) .one(db)
.await .await
.map_err(|_| ExecErr)?; .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(); let mut pear: fruit::ActiveModel = pear.unwrap().into();
pear.name = Set("Sweet pear".to_owned()); 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!();
println!("Updated: {:?}\n", res); println!("Updated: {:?}\n", pear);
Ok(()) Ok(())
} }

View File

@ -44,7 +44,7 @@ pub async fn all_about_select(db: &DbConn) -> Result<(), QueryErr> {
async fn find_all(db: &DbConn) -> Result<(), QueryErr> { async fn find_all(db: &DbConn) -> Result<(), QueryErr> {
print!("find all cakes: "); print!("find all cakes: ");
let cakes = Cake::find().all(db).await?; let cakes: Vec<cake::Model> = Cake::find().all(db).await?;
println!(); println!();
for cc in cakes.iter() { 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> { async fn find_together(db: &DbConn) -> Result<(), QueryErr> {
print!("find cakes and fruits: "); 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!(); println!();
for bb in both.iter() { for bb in both.iter() {
@ -102,7 +105,7 @@ async fn find_one(db: &DbConn) -> Result<(), QueryErr> {
print!("find models belong to: "); print!("find models belong to: ");
let fruits = cheese.find_fruit().all(db).await?; let fruits = cheese.find_related(Fruit).all(db).await?;
println!(); println!();
for ff in fruits.iter() { 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> { async fn find_many_to_many(db: &DbConn) -> Result<(), QueryErr> {
print!("find cakes and fillings: "); print!("find cakes and fillings: ");
let both = Cake::find().left_join_and_select(Filling).all(db).await?; let both: Vec<(cake::Model, Vec<filling::Model>)> = Cake::find()
.find_with_related(Filling)
.all(db)
.await?;
println!(); println!();
for bb in both.iter() { 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?; let cheese = Cake::find_by_id(1).one(db).await?;
if let Some(cheese) = cheese { if let Some(cheese) = cheese {
let fillings: Vec<filling::Model> = cheese.find_filling().all(db).await?; let fillings: Vec<filling::Model> = cheese.find_related(Filling).all(db).await?;
println!(); println!();
for ff in fillings.iter() { 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?; let lemon = Filling::find_by_id(2).one(db).await?;
if let Some(lemon) = lemon { if let Some(lemon) = lemon {
let cakes: Vec<cake::Model> = lemon.find_cake().all(db).await?; let cakes: Vec<cake::Model> = lemon.find_related(Cake).all(db).await?;
println!(); println!();
for cc in cakes.iter() { for cc in cakes.iter() {
@ -211,7 +217,7 @@ async fn find_together_json(db: &DbConn) -> Result<(), QueryErr> {
print!("find cakes and fruits: "); print!("find cakes and fruits: ");
let cakes_fruits = Cake::find() let cakes_fruits = Cake::find()
.left_join_and_select(Fruit) .find_with_related(Fruit)
.into_json() .into_json()
.all(db) .all(db)
.await?; .await?;

View File

@ -12,10 +12,11 @@ keywords = [ "orm", "database", "sql", "mysql", "postgres", "sqlite", "cli" ]
publish = false publish = false
[[bin]] [[bin]]
name = "sea-orm" name = "sea-orm-cli"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
clap = { version = "^2.33.3" } clap = { version = "^2.33.3" }
dotenv = { version = "^0.15" }
async-std = { version = "^1.9", features = [ "attributes" ] } async-std = { version = "^1.9", features = [ "attributes" ] }
sea-orm-codegen = { path = "../sea-orm-codegen" } sea-orm-codegen = { path = "../sea-orm-codegen" }

View File

@ -1,11 +1,11 @@
use clap::{App, AppSettings, Arg, SubCommand}; use clap::{App, AppSettings, Arg, SubCommand};
pub fn build_cli() -> App<'static, 'static> { pub fn build_cli() -> App<'static, 'static> {
let entity_subcommand = SubCommand::with_name("entity") let entity_subcommand = SubCommand::with_name("generate")
.about("Entity related commands") .about("Codegen related commands")
.setting(AppSettings::VersionlessSubcommands) .setting(AppSettings::VersionlessSubcommands)
.subcommand( .subcommand(
SubCommand::with_name("generate") SubCommand::with_name("entity")
.about("Generate entity") .about("Generate entity")
.arg( .arg(
Arg::with_name("DATABASE_URI") Arg::with_name("DATABASE_URI")
@ -13,7 +13,8 @@ pub fn build_cli() -> App<'static, 'static> {
.short("u") .short("u")
.help("Database URI") .help("Database URI")
.takes_value(true) .takes_value(true)
.required(true), .required(true)
.env("DATABASE_URI"),
) )
.arg( .arg(
Arg::with_name("DATABASE_SCHEMA") Arg::with_name("DATABASE_SCHEMA")
@ -21,7 +22,8 @@ pub fn build_cli() -> App<'static, 'static> {
.short("s") .short("s")
.help("Database schema") .help("Database schema")
.takes_value(true) .takes_value(true)
.required(true), .required(true)
.env("DATABASE_SCHEMA"),
) )
.arg( .arg(
Arg::with_name("OUTPUT_DIR") Arg::with_name("OUTPUT_DIR")

View File

@ -1,4 +1,5 @@
use clap::ArgMatches; use clap::ArgMatches;
use dotenv::dotenv;
use sea_orm_codegen::EntityGenerator; use sea_orm_codegen::EntityGenerator;
use std::{error::Error, fmt::Display}; use std::{error::Error, fmt::Display};
@ -6,19 +7,21 @@ mod cli;
#[async_std::main] #[async_std::main]
async fn main() { async fn main() {
dotenv().ok();
let matches = cli::build_cli().get_matches(); let matches = cli::build_cli().get_matches();
match matches.subcommand() { match matches.subcommand() {
("entity", Some(matches)) => run_entity_command(matches) ("generate", Some(matches)) => run_generate_command(matches)
.await .await
.unwrap_or_else(handle_error), .unwrap_or_else(handle_error),
_ => unreachable!("You should never see this message"), _ => unreachable!("You should never see this message"),
} }
} }
async fn run_entity_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error>> { async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error>> {
match matches.subcommand() { match matches.subcommand() {
("generate", Some(args)) => { ("entity", Some(args)) => {
let uri = args.value_of("DATABASE_URI").unwrap(); let uri = args.value_of("DATABASE_URI").unwrap();
let schema = args.value_of("DATABASE_SCHEMA").unwrap(); let schema = args.value_of("DATABASE_SCHEMA").unwrap();
let output_dir = args.value_of("OUTPUT_DIR").unwrap(); let output_dir = args.value_of("OUTPUT_DIR").unwrap();

View File

@ -9,6 +9,7 @@ pub struct Column {
pub(crate) col_type: ColumnType, pub(crate) col_type: ColumnType,
pub(crate) auto_increment: bool, pub(crate) auto_increment: bool,
pub(crate) not_null: bool, pub(crate) not_null: bool,
pub(crate) unique: bool,
} }
impl Column { impl Column {
@ -50,7 +51,7 @@ impl Column {
} }
pub fn get_def(&self) -> TokenStream { pub fn get_def(&self) -> TokenStream {
match &self.col_type { let mut col_def = match &self.col_type {
ColumnType::Char(s) => match s { ColumnType::Char(s) => match s {
Some(s) => quote! { ColumnType::Char(Some(#s)).def() }, Some(s) => quote! { ColumnType::Char(Some(#s)).def() },
None => quote! { ColumnType::Char(None).def() }, None => quote! { ColumnType::Char(None).def() },
@ -86,7 +87,18 @@ impl Column {
let s = s.to_string(); let s = s.to_string();
quote! { ColumnType::Custom(#s.to_owned()).def() } 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(); .collect();
let not_null = !not_nulls.is_empty(); let not_null = !not_nulls.is_empty();
let uniques: Vec<bool> = col_def
.get_column_spec()
.iter()
.filter_map(|spec| match spec {
ColumnSpec::UniqueKey => Some(true),
_ => None,
})
.collect();
let unique = !uniques.is_empty();
Self { Self {
name, name,
col_type, col_type,
auto_increment, auto_increment,
not_null, not_null,
unique,
} }
} }
} }

View File

@ -86,6 +86,10 @@ impl Entity {
.collect() .collect()
} }
pub fn get_relation_defs(&self) -> Vec<TokenStream> {
self.relations.iter().map(|rel| rel.get_def()).collect()
}
pub fn get_relation_rel_types(&self) -> Vec<Ident> { pub fn get_relation_rel_types(&self) -> Vec<Ident> {
self.relations self.relations
.iter() .iter()

View File

@ -1,9 +1,15 @@
use heck::{CamelCase, SnakeCase}; use heck::{CamelCase, SnakeCase};
use proc_macro2::Ident; use proc_macro2::{Ident, TokenStream};
use quote::format_ident; use quote::{format_ident, quote};
use sea_orm::RelationType;
use sea_query::TableForeignKey; use sea_query::TableForeignKey;
#[derive(Clone, Debug)]
pub enum RelationType {
HasOne,
HasMany,
BelongsTo,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Relation { pub struct Relation {
pub(crate) ref_table: String, pub(crate) ref_table: String,
@ -21,10 +27,33 @@ impl Relation {
format_ident!("{}", self.ref_table.to_camel_case()) 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 { pub fn get_rel_type(&self) -> Ident {
match self.rel_type { match self.rel_type {
RelationType::HasOne => format_ident!("has_one"), RelationType::HasOne => format_ident!("has_one"),
RelationType::HasMany => format_ident!("has_many"), 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 columns = tbl_fk.get_columns();
let ref_columns = tbl_fk.get_ref_columns(); let ref_columns = tbl_fk.get_ref_columns();
let rel_type = RelationType::HasOne; let rel_type = RelationType::BelongsTo;
Self { Self {
ref_table, ref_table,
columns, columns,

View File

@ -1,5 +1,4 @@
use crate::{Entity, EntityWriter, Error, PrimaryKey, Relation}; use crate::{Column, Entity, EntityWriter, Error, PrimaryKey, Relation, RelationType};
use sea_orm::RelationType;
use sea_query::TableStatement; use sea_query::TableStatement;
use sea_schema::mysql::def::Schema; use sea_schema::mysql::def::Schema;
use std::{collections::HashMap, mem::swap}; use std::{collections::HashMap, mem::swap};
@ -31,11 +30,16 @@ impl EntityTransformer {
)) ))
} }
}; };
let columns = table_create let columns: Vec<Column> = table_create
.get_columns() .get_columns()
.iter() .iter()
.map(|col_def| col_def.into()) .map(|col_def| col_def.into())
.collect(); .collect();
let unique_columns: Vec<String> = columns
.iter()
.filter(|col| col.unique)
.map(|col| col.name.clone())
.collect();
let relations = table_create let relations = table_create
.get_foreign_key_create_stmts() .get_foreign_key_create_stmts()
.iter() .iter()
@ -64,9 +68,22 @@ impl EntityTransformer {
entities.push(entity); entities.push(entity);
for mut rel in relations.into_iter() { for mut rel in relations.into_iter() {
let ref_table = rel.ref_table; let ref_table = rel.ref_table;
swap(&mut rel.columns, &mut rel.ref_columns); let mut unique = true;
rel.rel_type = RelationType::HasMany; 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.ref_table = table_name.clone();
rel.columns = Vec::new();
rel.ref_columns = Vec::new();
if let Some(vec) = inverse_relations.get_mut(&ref_table) { if let Some(vec) = inverse_relations.get_mut(&ref_table) {
vec.push(rel); vec.push(rel);
} else { } else {

View File

@ -217,20 +217,14 @@ impl EntityWriter {
pub fn gen_impl_relation_trait(entity: &Entity) -> TokenStream { pub fn gen_impl_relation_trait(entity: &Entity) -> TokenStream {
let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case(); let relation_ref_tables_camel_case = entity.get_relation_ref_tables_camel_case();
let relation_rel_types = entity.get_relation_rel_types(); let relation_defs = entity.get_relation_defs();
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 quoted = if relation_ref_tables_camel_case.is_empty() { let quoted = if relation_ref_tables_camel_case.is_empty() {
quote! { quote! {
_ => panic!("No RelationDef"), _ => panic!("No RelationDef"),
} }
} else { } else {
quote! { quote! {
#(Self::#relation_ref_tables_camel_case => Entity::#relation_rel_types(super::#relation_ref_tables_snake_case::Entity) #(Self::#relation_ref_tables_camel_case => #relation_defs),*
.from(Column::#relation_columns_camel_case)
.to(super::#relation_ref_tables_snake_case::Column::#relation_ref_columns_camel_case)
.into()),*
} }
}; };
quote! { quote! {

View File

@ -1,4 +1,4 @@
use crate::{ExecErr, ExecResult, QueryErr, QueryResult, Statement}; use crate::{ExecErr, ExecResult, QueryErr, QueryResult, Statement, Transaction};
use sea_query::{ use sea_query::{
MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementBuilder, SqliteQueryBuilder, MysqlQueryBuilder, PostgresQueryBuilder, QueryStatementBuilder, SqliteQueryBuilder,
}; };
@ -118,6 +118,12 @@ impl DatabaseConnection {
pub fn as_mock_connection(&self) -> Option<bool> { pub fn as_mock_connection(&self) -> Option<bool> {
None None
} }
#[cfg(feature = "mock")]
pub fn into_transaction_log(self) -> Vec<Transaction> {
let mut mocker = self.as_mock_connection().get_mocker_mutex().lock().unwrap();
mocker.drain_transaction_log()
}
} }
impl QueryBuilderBackend { impl QueryBuilderBackend {

View File

@ -1,4 +1,5 @@
use crate::Statement; use crate::Statement;
use sea_query::{Value, Values};
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Transaction { pub struct Transaction {
@ -6,6 +7,16 @@ pub struct Transaction {
} }
impl Transaction { impl Transaction {
pub fn from_sql_and_values<I>(sql: &str, values: I) -> Self
where
I: IntoIterator<Item = Value>,
{
Self::one(Statement {
sql: sql.to_owned(),
values: Some(Values(values.into_iter().collect())),
})
}
/// Create a Transaction with one statement /// Create a Transaction with one statement
pub fn one(stmt: Statement) -> Self { pub fn one(stmt: Statement) -> Self {
Self { stmts: vec![stmt] } Self { stmts: vec![stmt] }

View File

@ -42,17 +42,6 @@ impl Default for ActiveValueState {
} }
} }
pub trait OneOrManyActiveModel<A>
where
A: ActiveModelTrait,
{
fn is_one() -> bool;
fn get_one(self) -> A;
fn is_many() -> bool;
fn get_many(self) -> Vec<A>;
}
#[doc(hidden)] #[doc(hidden)]
pub fn unchanged_active_value_not_intended_for_public_use<V>(value: V) -> ActiveValue<V> pub fn unchanged_active_value_not_intended_for_public_use<V>(value: V) -> ActiveValue<V>
where where
@ -197,44 +186,6 @@ where
} }
} }
impl<A> OneOrManyActiveModel<A> 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<A> {
panic!("not many")
}
}
impl<A> OneOrManyActiveModel<A> for Vec<A>
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<A> {
self
}
}
/// Insert the model if primary key is unset, update otherwise. /// Insert the model if primary key is unset, update otherwise.
/// Only works if the entity has auto increment primary key. /// Only works if the entity has auto increment primary key.
pub async fn save_active_model<A, E>(mut am: A, db: &DatabaseConnection) -> Result<A, ExecErr> pub async fn save_active_model<A, E>(mut am: A, db: &DatabaseConnection) -> Result<A, ExecErr>

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
ActiveModelTrait, ColumnTrait, Delete, DeleteOne, FromQueryResult, Insert, ModelTrait, ActiveModelTrait, ColumnTrait, Delete, DeleteMany, DeleteOne, FromQueryResult, Insert,
OneOrManyActiveModel, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, Related, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, Related, RelationBuilder,
RelationBuilder, RelationTrait, RelationType, Select, Update, UpdateOne, RelationTrait, RelationType, Select, Update, UpdateMany, UpdateOne,
}; };
use sea_query::{Iden, IntoValueTuple}; use sea_query::{Iden, IntoValueTuple};
use std::fmt::Debug; 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!( /// assert_eq!(
/// cake::Entity::find() /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![
/// .to_string(), /// Transaction::from_sql_and_values(
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""# /// 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<Self> { fn find() -> Select<Self> {
Select::new() Select::new()
@ -65,28 +78,42 @@ pub trait EntityTrait: EntityName {
/// Find a model by primary key /// 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!( /// assert_eq!(
/// cake::Entity::find_by_id(11) /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![Transaction::from_sql_and_values(
/// .to_string(), /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()]
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = 11"# /// )]);
/// );
/// ``` /// ```
/// Find by composite key /// 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!( /// assert_eq!(
/// cake_filling::Entity::find_by_id((2, 3)) /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![Transaction::from_sql_and_values([
/// .to_string(),
/// [
/// r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#, /// r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id" FROM "cake_filling""#,
/// r#"WHERE "cake_filling"."cake_id" = 2 AND "cake_filling"."filling_id" = 3"#, /// r#"WHERE "cake_filling"."cake_id" = $1 AND "cake_filling"."filling_id" = $2"#,
/// ].join(" ") /// ].join(" ").as_str(),
/// ); /// vec![2i32.into(), 3i32.into()]
/// )]);
/// ``` /// ```
fn find_by_id<V>(values: V) -> Select<Self> fn find_by_id<V>(values: V) -> Select<Self>
where where
@ -108,69 +135,29 @@ pub trait EntityTrait: EntityName {
select 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 { /// let apple = cake::ActiveModel {
/// name: Set("Apple Pie".to_owned()), /// name: Set("Apple Pie".to_owned()),
/// ..Default::default() /// ..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 { /// # async_std::task::block_on(async {
/// name: Set("Apple Pie".to_owned()), /// cake::Entity::insert(apple).exec(&db).await;
/// ..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<A, C>(models: C) -> Insert<A>
where
A: ActiveModelTrait<Entity = Self>,
C: OneOrManyActiveModel<A>,
{
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};
/// ///
/// let apple = cake::ActiveModel {
/// name: Set("Apple Pie".to_owned()),
/// ..Default::default()
/// };
/// assert_eq!( /// assert_eq!(
/// cake::Entity::insert_one(apple) /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![Transaction::from_sql_and_values(
/// .to_string(), /// r#"INSERT INTO "cake" ("name") VALUES ($1)"#, vec!["Apple Pie".into()]
/// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#, /// )]);
/// );
/// ``` /// ```
fn insert_one<A>(model: A) -> Insert<A> fn insert<A>(model: A) -> Insert<A>
where where
A: ActiveModelTrait<Entity = Self>, A: ActiveModelTrait<Entity = Self>,
{ {
@ -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 { /// let apple = cake::ActiveModel {
/// name: Set("Apple Pie".to_owned()), /// name: Set("Apple Pie".to_owned()),
@ -188,12 +179,17 @@ pub trait EntityTrait: EntityName {
/// name: Set("Orange Scone".to_owned()), /// name: Set("Orange Scone".to_owned()),
/// ..Default::default() /// ..Default::default()
/// }; /// };
///
/// # async_std::task::block_on(async {
/// cake::Entity::insert_many(vec![apple, orange]).exec(&db).await;
/// # });
///
/// assert_eq!( /// assert_eq!(
/// cake::Entity::insert_many(vec![apple, orange]) /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![Transaction::from_sql_and_values(
/// .to_string(), /// r#"INSERT INTO "cake" ("name") VALUES ($1), ($2)"#,
/// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie'), ('Orange Scone')"#, /// vec!["Apple Pie".into(), "Orange Scone".into()]
/// ); /// )]);
/// ``` /// ```
fn insert_many<A, I>(models: I) -> Insert<A> fn insert_many<A, I>(models: I) -> Insert<A>
where 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 { /// let orange = fruit::ActiveModel {
/// id: Set(1), /// id: Set(1),
/// name: Set("Orange".to_owned()), /// name: Set("Orange".to_owned()),
/// ..Default::default() /// ..Default::default()
/// }; /// };
///
/// # async_std::task::block_on(async {
/// fruit::Entity::update(orange).exec(&db).await;
/// # });
///
/// assert_eq!( /// assert_eq!(
/// fruit::Entity::update(orange) /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![Transaction::from_sql_and_values(
/// .to_string(), /// r#"UPDATE "fruit" SET "name" = $1 WHERE "fruit"."id" = $2"#, vec!["Orange".into(), 1i32.into()]
/// r#"UPDATE "fruit" SET "name" = 'Orange' WHERE "fruit"."id" = 1"#, /// )]);
/// );
/// ``` /// ```
fn update<A>(model: A) -> UpdateOne<A> fn update<A>(model: A) -> UpdateOne<A>
where 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<Self> {
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 { /// let orange = fruit::ActiveModel {
/// id: Set(3), /// id: Set(3),
/// ..Default::default() /// ..Default::default()
/// }; /// };
///
/// # async_std::task::block_on(async {
/// fruit::Entity::delete(orange).exec(&db).await;
/// # });
///
/// assert_eq!( /// assert_eq!(
/// fruit::Entity::delete(orange) /// db.into_transaction_log(),
/// .build(PostgresQueryBuilder) /// vec![Transaction::from_sql_and_values(
/// .to_string(), /// r#"DELETE FROM "fruit" WHERE "fruit"."id" = $1"#, vec![3i32.into()]
/// r#"DELETE FROM "fruit" WHERE "fruit"."id" = 3"#, /// )]);
/// );
/// ``` /// ```
fn delete<A>(model: A) -> DeleteOne<A> fn delete<A>(model: A) -> DeleteOne<A>
where where
@ -245,4 +282,28 @@ pub trait EntityTrait: EntityName {
{ {
Delete::one(model) 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<Self> {
Delete::many(Self::default())
}
} }

View File

@ -1,4 +1,4 @@
use crate::{EntityTrait, QueryResult, TypeErr}; use crate::{EntityTrait, QueryFilter, QueryResult, Related, Select, TypeErr};
pub use sea_query::Value; pub use sea_query::Value;
use std::fmt::Debug; use std::fmt::Debug;
@ -8,10 +8,25 @@ pub trait ModelTrait: Clone + Debug {
fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> Value; fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> Value;
fn set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value); fn set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value);
fn find_related<R>(&self, _: R) -> Select<R>
where
R: EntityTrait,
Self::Entity: Related<R>,
{
<Self::Entity as Related<R>>::find_related().belongs_to(self)
}
} }
pub trait FromQueryResult { pub trait FromQueryResult {
fn from_query_result(res: &QueryResult, pre: &str) -> Result<Self, TypeErr> fn from_query_result(res: &QueryResult, pre: &str) -> Result<Self, TypeErr>
where where
Self: Sized; Self: Sized;
fn from_query_result_optional(res: &QueryResult, pre: &str) -> Result<Option<Self>, TypeErr>
where
Self: Sized,
{
Ok(Self::from_query_result(res, pre).ok())
}
} }

View File

@ -175,9 +175,7 @@ mod tests {
query_builder.build(select.offset(4).limit(2)), query_builder.build(select.offset(4).limit(2)),
]; ];
let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts));
assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts));
Ok(()) Ok(())
} }
@ -211,9 +209,7 @@ mod tests {
query_builder.build(select.offset(4).limit(2)), query_builder.build(select.offset(4).limit(2)),
]; ];
let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts));
assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts));
Ok(()) Ok(())
} }
@ -244,9 +240,8 @@ mod tests {
let query_builder = db.get_query_builder_backend(); let query_builder = db.get_query_builder_backend();
let stmts = vec![query_builder.build(&select)]; 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(()) Ok(())
} }
@ -297,9 +292,7 @@ mod tests {
query_builder.build(select.offset(4).limit(2)), query_builder.build(select.offset(4).limit(2)),
]; ];
let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts));
assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts));
Ok(()) Ok(())
} }
@ -331,9 +324,7 @@ mod tests {
query_builder.build(select.offset(4).limit(2)), query_builder.build(select.offset(4).limit(2)),
]; ];
let mut mocker = db.as_mock_connection().get_mocker_mutex().lock().unwrap(); assert_eq!(db.into_transaction_log(), Transaction::wrap(stmts));
assert_eq!(mocker.drain_transaction_log(), Transaction::wrap(stmts));
Ok(()) Ok(())
} }
} }

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
query::combine, DatabaseConnection, EntityTrait, FromQueryResult, JsonValue, Paginator, query::combine, DatabaseConnection, EntityTrait, FromQueryResult, Iterable, JsonValue,
QueryErr, QueryResult, Select, SelectTwo, TypeErr, ModelTrait, Paginator, PrimaryKeyToColumn, QueryErr, QueryResult, Select, SelectTwo,
SelectTwoMany, TypeErr,
}; };
use sea_query::SelectStatement; use sea_query::SelectStatement;
use std::marker::PhantomData; use std::marker::PhantomData;
@ -52,12 +53,12 @@ where
M: FromQueryResult + Sized, M: FromQueryResult + Sized,
N: FromQueryResult + Sized, N: FromQueryResult + Sized,
{ {
type Item = (M, N); type Item = (M, Option<N>);
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, TypeErr> { fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, TypeErr> {
Ok(( Ok((
M::from_query_result(&res, combine::SELECT_A)?, 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( pub async fn one(
self, self,
db: &DatabaseConnection, db: &DatabaseConnection,
) -> Result<Option<(E::Model, F::Model)>, QueryErr> { ) -> Result<Option<(E::Model, Option<F::Model>)>, QueryErr> {
self.into_model::<E::Model, F::Model>().one(db).await self.into_model::<E::Model, F::Model>().one(db).await
} }
pub async fn all(self, db: &DatabaseConnection) -> Result<Vec<(E::Model, F::Model)>, QueryErr> { pub async fn all(
self,
db: &DatabaseConnection,
) -> Result<Vec<(E::Model, Option<F::Model>)>, QueryErr> {
self.into_model::<E::Model, F::Model>().all(db).await self.into_model::<E::Model, F::Model>().all(db).await
} }
} }
impl<E, F> SelectTwoMany<E, F>
where
E: EntityTrait,
F: EntityTrait,
{
fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
where
M: FromQueryResult,
N: FromQueryResult,
{
Selector {
query: self.query,
selector: SelectTwoModel { model: PhantomData },
}
}
#[cfg(feature = "with-json")]
pub fn into_json(self) -> Selector<SelectTwoModel<JsonValue, JsonValue>> {
Selector {
query: self.query,
selector: SelectTwoModel { model: PhantomData },
}
}
pub async fn one(
self,
db: &DatabaseConnection,
) -> Result<Option<(E::Model, Option<F::Model>)>, QueryErr> {
self.into_model::<E::Model, F::Model>().one(db).await
}
pub async fn all(
self,
db: &DatabaseConnection,
) -> Result<Vec<(E::Model, Vec<F::Model>)>, QueryErr> {
let rows = self.into_model::<E::Model, F::Model>().all(db).await?;
Ok(consolidate_query_result::<E, F>(rows))
}
}
impl<S> Selector<S> impl<S> Selector<S>
where where
S: SelectorTrait, S: SelectorTrait,
@ -171,3 +215,39 @@ where
} }
} }
} }
fn consolidate_query_result<L, R>(
rows: Vec<(L::Model, Option<R::Model>)>,
) -> Vec<(L::Model, Vec<R::Model>)>
where
L: EntityTrait,
R: EntityTrait,
{
let mut acc: Vec<(L::Model, Vec<R::Model>)> = 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 <L::PrimaryKey as Iterable>::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
}

View File

@ -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::Model> = Cake::find().all(db).await?;
//!
//! // find and filter
//! let chocolate: Vec<cake::Model> = Cake::find()
//! .filter(cake::Column::Name.contains("chocolate"))
//! .all(db)
//! .await?;
//!
//! // find one model
//! let cheese: Option<cake::Model> = Cake::find_by_id(1).one(db).await?;
//! let cheese: cake::Model = cheese.unwrap();
//!
//! // find related models (lazy)
//! let fruits: Vec<fruit::Model> = cheese.find_related(Fruit).all(db).await?;
//!
//! // find related models (eager)
//! let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> = 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::Model> = Fruit::find_by_id(1).one(db).await?;
//! # Ok(())
//! # }
//! #
//! # async fn function2(db: &DbConn) -> Result<(), ExecErr> {
//! # let pear: Option<fruit::Model> = 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::Model> = Fruit::find_by_id(1).one(db).await?;
//! # Ok(())
//! # }
//! #
//! # async fn function2(db: &DbConn) -> Result<(), ExecErr> {
//! # let orange: Option<fruit::Model> = 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 database;
mod driver; mod driver;
pub mod entity; pub mod entity;
mod executor; mod executor;
pub mod query; pub mod query;
#[doc(hidden)]
pub mod tests_cfg; pub mod tests_cfg;
mod util; mod util;

View File

@ -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; use core::marker::PhantomData;
pub use sea_query::JoinType; 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_A: &str = "A_";
pub const SELECT_B: &str = "B_"; pub const SELECT_B: &str = "B_";
@ -40,6 +40,14 @@ where
self = self.apply_alias(SELECT_A); self = self.apply_alias(SELECT_A);
SelectTwo::new(self.into_query()) SelectTwo::new(self.into_query())
} }
pub fn select_with<F>(mut self, _: F) -> SelectTwoMany<E, F>
where
F: EntityTrait,
{
self = self.apply_alias(SELECT_A);
SelectTwoMany::new(self.into_query())
}
} }
impl<E, F> SelectTwo<E, F> impl<E, F> SelectTwo<E, F>
@ -48,23 +56,58 @@ where
F: EntityTrait, F: EntityTrait,
{ {
pub(crate) fn new(query: SelectStatement) -> Self { pub(crate) fn new(query: SelectStatement) -> Self {
let myself = Self { Self {
query, query,
entity: PhantomData, entity: PhantomData,
}; }
myself.prepare_select() .prepare_select()
} }
fn prepare_select(mut self) -> Self { fn prepare_select(mut self) -> Self {
prepare_select_two::<F, Self>(&mut self);
self
}
}
impl<E, F> SelectTwoMany<E, F>
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::<F, Self>(&mut self);
self
}
fn prepare_order_by(mut self) -> Self {
for col in <E::PrimaryKey as Iterable>::iter() {
self.query.order_by((E::default(), col), Order::Asc);
}
self
}
}
fn prepare_select_two<F, S>(selector: &mut S)
where
F: EntityTrait,
S: QueryTrait<QueryStatement = SelectStatement>,
{
for col in <F::Column as Iterable>::iter() { for col in <F::Column as Iterable>::iter() {
let alias = format!("{}{}", SELECT_B, col.to_string().as_str()); let alias = format!("{}{}", SELECT_B, col.to_string().as_str());
self.query.expr(SelectExpr { selector.query().expr(SelectExpr {
expr: col.into_simple_expr(), expr: col.into_simple_expr(),
alias: Some(SeaRc::new(Alias::new(&alias))), alias: Some(SeaRc::new(Alias::new(&alias))),
}); });
} }
self
}
} }
#[cfg(test)] #[cfg(test)]
@ -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] #[test]
fn select_also_2() { fn select_also_2() {
assert_eq!( assert_eq!(
@ -119,4 +179,24 @@ mod tests {
].join(" ") ].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(" ")
);
}
} }

View File

@ -258,11 +258,11 @@ pub trait QueryFilter: Sized {
} }
/// Apply a where condition using the model's primary key /// Apply a where condition using the model's primary key
fn belongs_to<E>(mut self, model: &E::Model) -> Self fn belongs_to<M>(mut self, model: &M) -> Self
where where
E: EntityTrait, M: ModelTrait,
{ {
for key in E::PrimaryKey::iter() { for key in <M::Entity as EntityTrait>::PrimaryKey::iter() {
let col = key.into_column(); let col = key.into_column();
self = self.filter(col.eq(model.get(col))); self = self.filter(col.eq(model.get(col)));
} }

View File

@ -1,4 +1,4 @@
use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo}; use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
pub use sea_query::JoinType; pub use sea_query::JoinType;
impl<E> Select<E> impl<E> Select<E>
@ -41,19 +41,28 @@ where
} }
/// Left Join with a Related Entity and select both Entity. /// Left Join with a Related Entity and select both Entity.
pub fn left_join_and_select<R>(self, r: R) -> SelectTwo<E, R> pub fn find_also_related<R>(self, r: R) -> SelectTwo<E, R>
where where
R: EntityTrait, R: EntityTrait,
E: Related<R>, E: Related<R>,
{ {
self.left_join(r).select_also(r) 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<R>(self, r: R) -> SelectTwoMany<E, R>
where
R: EntityTrait,
E: Related<R>,
{
self.left_join(r).select_with(r)
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests_cfg::{cake, filling, fruit}; use crate::tests_cfg::{cake, filling, fruit};
use crate::{ColumnTrait, EntityTrait, QueryFilter, QueryTrait}; use crate::{ColumnTrait, EntityTrait, ModelTrait, QueryFilter, QueryTrait};
use sea_query::MysqlQueryBuilder; use sea_query::MysqlQueryBuilder;
#[test] #[test]
@ -130,7 +139,10 @@ mod tests {
}; };
assert_eq!( 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`", "SELECT `fruit`.`id`, `fruit`.`name`, `fruit`.`cake_id` FROM `fruit`",
"INNER JOIN `cake` ON `cake`.`id` = `fruit`.`cake_id`", "INNER JOIN `cake` ON `cake`.`id` = `fruit`.`cake_id`",

View File

@ -20,4 +20,4 @@ pub use select::*;
pub use traits::*; pub use traits::*;
pub use update::*; pub use update::*;
pub use crate::executor::{ExecErr, QueryErr}; pub use crate::executor::{ExecErr, InsertResult, QueryErr, UpdateResult};

View File

@ -23,6 +23,16 @@ where
pub(crate) entity: PhantomData<(E, F)>, pub(crate) entity: PhantomData<(E, F)>,
} }
#[derive(Clone, Debug)]
pub struct SelectTwoMany<E, F>
where
E: EntityTrait,
F: EntityTrait,
{
pub(crate) query: SelectStatement,
pub(crate) entity: PhantomData<(E, F)>,
}
pub trait IntoSimpleExpr { pub trait IntoSimpleExpr {
fn into_simple_expr(self) -> SimpleExpr; fn into_simple_expr(self) -> SimpleExpr;
} }
@ -51,6 +61,18 @@ macro_rules! impl_trait {
&mut self.query &mut self.query
} }
} }
impl<E, F> $trait for SelectTwoMany<E, F>
where
E: EntityTrait,
F: EntityTrait,
{
type QueryStatement = SelectStatement;
fn query(&mut self) -> &mut SelectStatement {
&mut self.query
}
}
}; };
} }
@ -118,11 +140,13 @@ where
} }
} }
impl<E, F> QueryTrait for SelectTwo<E, F> macro_rules! select_two {
where ( $selector: ident ) => {
impl<E, F> QueryTrait for $selector<E, F>
where
E: EntityTrait, E: EntityTrait,
F: EntityTrait, F: EntityTrait,
{ {
type QueryStatement = SelectStatement; type QueryStatement = SelectStatement;
fn query(&mut self) -> &mut SelectStatement { fn query(&mut self) -> &mut SelectStatement {
&mut self.query &mut self.query
@ -133,4 +157,9 @@ where
fn into_query(self) -> SelectStatement { fn into_query(self) -> SelectStatement {
self.query self.query
} }
}
};
} }
select_two!(SelectTwo);
select_two!(SelectTwoMany);

View File

@ -73,14 +73,4 @@ impl Related<super::filling::Entity> for Entity {
} }
} }
impl Model {
pub fn find_fruit(&self) -> Select<super::fruit::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
pub fn find_filling(&self) -> Select<super::filling::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -63,10 +63,4 @@ impl Related<super::cake::Entity> for Entity {
} }
} }
impl Model {
pub fn find_cake(&self) -> Select<super::cake::Entity> {
Entity::find_related().belongs_to::<Entity>(self)
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

View File

@ -4,3 +4,8 @@ pub mod cake;
pub mod cake_filling; pub mod cake_filling;
pub mod filling; pub mod filling;
pub mod fruit; 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;