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:
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

View File

@ -6,6 +6,7 @@ members = [
"sea-orm-cli",
"examples/sqlx-mysql",
"examples/codegen",
"examples/cli",
]
[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.
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.

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 {
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(),

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};
@ -10,7 +10,7 @@ async fn main() -> Result<(), Error> {
let _generator = EntityGenerator::discover(uri, schema)
.await?
.transform()?
.generate("src/out")?;
.generate("src/entity")?;
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: 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

View File

@ -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<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 {}

View File

@ -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(),

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 {}

View File

@ -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<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()),
..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::Model> = 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(())
}

View File

@ -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::Model> = 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<filling::Model>)> = 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<filling::Model> = cheese.find_filling().all(db).await?;
let fillings: Vec<filling::Model> = 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<cake::Model> = lemon.find_cake().all(db).await?;
let cakes: Vec<cake::Model> = 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?;

View File

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

View File

@ -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")

View File

@ -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<dyn Error>> {
async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box<dyn Error>> {
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();

View File

@ -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<bool> = 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,
}
}
}

View File

@ -86,6 +86,10 @@ impl Entity {
.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> {
self.relations
.iter()

View File

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

View File

@ -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<Column> = table_create
.get_columns()
.iter()
.map(|col_def| col_def.into())
.collect();
let unique_columns: Vec<String> = 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 {

View File

@ -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! {

View File

@ -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<bool> {
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 {

View File

@ -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<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
pub fn one(stmt: Statement) -> Self {
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)]
pub fn unchanged_active_value_not_intended_for_public_use<V>(value: V) -> ActiveValue<V>
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.
/// 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>

View File

@ -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<Self> {
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<V>(values: V) -> Select<Self>
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<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};
/// # 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<A>(model: A) -> Insert<A>
fn insert<A>(model: A) -> Insert<A>
where
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 {
/// 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<A, I>(models: I) -> Insert<A>
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<A>(model: A) -> UpdateOne<A>
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 {
/// 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<A>(model: A) -> DeleteOne<A>
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<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;
use std::fmt::Debug;
@ -8,10 +8,25 @@ pub trait ModelTrait: Clone + Debug {
fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> 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 {
fn from_query_result(res: &QueryResult, pre: &str) -> Result<Self, TypeErr>
where
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)),
];
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(())
}
}

View File

@ -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<N>);
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, TypeErr> {
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<Option<(E::Model, F::Model)>, QueryErr> {
) -> 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, 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
}
}
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>
where
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 driver;
pub mod entity;
mod executor;
pub mod query;
#[doc(hidden)]
pub mod tests_cfg;
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;
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<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>
@ -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 <F::Column as Iterable>::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::<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() {
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(" ")
);
}
}

View File

@ -258,11 +258,11 @@ pub trait QueryFilter: Sized {
}
/// 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
E: EntityTrait,
M: ModelTrait,
{
for key in E::PrimaryKey::iter() {
for key in <M::Entity as EntityTrait>::PrimaryKey::iter() {
let col = key.into_column();
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;
impl<E> Select<E>
@ -41,19 +41,28 @@ where
}
/// 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
R: EntityTrait,
E: Related<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)]
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`",

View File

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

View File

@ -23,6 +23,16 @@ where
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 {
fn into_simple_expr(self) -> SimpleExpr;
}
@ -51,6 +61,18 @@ macro_rules! impl_trait {
&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,19 +140,26 @@ where
}
}
impl<E, F> QueryTrait for SelectTwo<E, F>
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<E, F> QueryTrait for $selector<E, F>
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);

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 {}

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 {}

View File

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