Merge branch 'entity-format' into entity_model_macro
This commit is contained in:
commit
a4d70dfa55
20
.github/workflows/rust.yml
vendored
20
.github/workflows/rust.yml
vendored
@ -11,6 +11,24 @@ env:
|
||||
|
||||
jobs:
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-targets --all
|
||||
|
||||
compile-sqlite:
|
||||
name: Compile SQLite
|
||||
runs-on: ubuntu-20.04
|
||||
@ -152,7 +170,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
path: [async-std, tokio]
|
||||
path: [async-std, tokio, rocket_example]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
|
39
ARCHITECTURE.md
Normal file
39
ARCHITECTURE.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Architecture
|
||||
|
||||
To understand the architecture of SeaORM, let's discuss what is an ORM. ORM exists to provide abstractions over common operations you would do against a database and hide the implementation details like the actual SQL queries.
|
||||
|
||||
With a good ORM, you shouldn't bother to look under the API surface. Until you do. I hear you say *'abstraction leaks'*, and yes, it does.
|
||||
|
||||
The approach SeaORM takes is **'layered abstraction'**, where you'd dig one layer beneath if you want to. That's why we made SeaQuery into a standalone repository. It's useful on its own, and with a public API surface and a separate namespace, it's far more difficult to create confusing internal APIs than a monolithic approach.
|
||||
|
||||
The central idea in SeaORM is nearly everything is runtime configurable. At compile time, it does not know what the underlying database is.
|
||||
|
||||
What benefits does database-agnostic bring? For example, you can:
|
||||
|
||||
1. Make your app work on any database, depending on runtime configuration
|
||||
1. Use the same models and transfer them across different databases
|
||||
1. Share entities across different projects by creating a 'data structure crate', where the database is chosen by downstream 'behaviour crates'
|
||||
|
||||
The API of SeaORM is not a thin shell, but consist of layers, with each layer underneath being less abstract.
|
||||
|
||||
There are different stages when the API is being utilized.
|
||||
|
||||
So there are two dimensions to navigate the SeaORM code base, **'stage'** and **'abstractness'**.
|
||||
|
||||
First is the declaration stage. Entities and relations among them are being defined with the `EntityTrait`, `ColumnTrait` & `RelationTrait` etc.
|
||||
|
||||
Second is the query building stage.
|
||||
|
||||
The top most layer is `Entity`'s `find*`, `insert`, `update` & `delete` methods, where you can intuitively perform basic CRUD operations.
|
||||
|
||||
One layer down, is the `Select`, `Insert`, `Update` & `Delete` structs, where they each have their own API for more advanced operations.
|
||||
|
||||
One layer down, is the SeaQuery `SelectStatement`, `InsertStatement`, `UpdateStatement` & `DeleteStatement`, where they have a rich API for you to fiddle with the SQL syntax tree.
|
||||
|
||||
Third is the execution stage. A separate set of structs, `Selector`, `Inserter`, `Updater` & `Deleter`, are responsible for executing the statements against a database connection.
|
||||
|
||||
Finally is the resolution stage, when query results are converted into Rust structs for consumption.
|
||||
|
||||
Because only the execution and resolution stages are database specific, we have the possibility to use a different driver by replacing those.
|
||||
|
||||
I imagine some day, we will support a number of databases, with a matrix of different syntaxes, protocols and form-factors.
|
18
CHANGELOG.md
18
CHANGELOG.md
@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.2.1 - 2021-09-04
|
||||
|
||||
- Update dependencies
|
||||
|
||||
## 0.2.0 - 2021-09-03
|
||||
|
||||
- [[#37]] Rocket example
|
||||
- [[#114]] `log` crate and `env-logger`
|
||||
- [[#103]] `InsertResult` to return the primary key's type
|
||||
- [[#89]] Represent several relations between same types by `Linked`
|
||||
- [[#59]] Transforming an Entity into `TableCreateStatement`
|
||||
|
||||
[#37]: https://github.com/SeaQL/sea-orm/issues/37
|
||||
[#114]: https://github.com/SeaQL/sea-orm/issues/114
|
||||
[#103]: https://github.com/SeaQL/sea-orm/issues/103
|
||||
[#89]: https://github.com/SeaQL/sea-orm/issues/89
|
||||
[#59]: https://github.com/SeaQL/sea-orm/issues/59
|
||||
|
||||
## 0.1.3 - 2021-08-30
|
||||
|
||||
- [[#108]] Remove impl TryGetable for Option<T>
|
||||
|
21
Cargo.toml
21
Cargo.toml
@ -1,13 +1,9 @@
|
||||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
"sea-orm-macros",
|
||||
"sea-orm-codegen",
|
||||
]
|
||||
members = [".", "sea-orm-macros", "sea-orm-codegen"]
|
||||
|
||||
[package]
|
||||
name = "sea-orm"
|
||||
version = "0.1.3"
|
||||
version = "0.2.1"
|
||||
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "🐚 An async & dynamic ORM for Rust"
|
||||
@ -30,15 +26,14 @@ async-stream = { version = "^0.3" }
|
||||
chrono = { version = "^0", optional = true }
|
||||
futures = { version = "^0.3" }
|
||||
futures-util = { version = "^0.3" }
|
||||
log = { version = "^0.4", optional = true }
|
||||
rust_decimal = { version = "^1", optional = true }
|
||||
sea-orm-macros = { version = "^0.1.1", optional = true }
|
||||
sea-query = { version = "^0.15", features = ["thread-safe"] }
|
||||
sea-orm-macros = { version = "^0.2", path = "sea-orm-macros", optional = true }
|
||||
sea-query = { version = "^0.16", features = ["thread-safe"] }
|
||||
sea-strum = { version = "^0.21", features = ["derive", "sea-orm"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
sqlx = { version = "^0.5", optional = true }
|
||||
sqlx-core = { version = "^0.5", optional = true }
|
||||
sqlx-macros = { version = "^0.5", optional = true }
|
||||
serde_json = { version = "^1", optional = true }
|
||||
sqlx = { version = "^0.5", optional = true }
|
||||
uuid = { version = "0.8", features = ["serde", "v4"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
@ -49,10 +44,12 @@ tokio = { version = "^1.6", features = ["full"] }
|
||||
actix-rt = { version = "2.2.0" }
|
||||
maplit = { version = "^1" }
|
||||
rust_decimal_macros = { version = "^1" }
|
||||
env_logger = { version = "^0.9" }
|
||||
sea-orm = { path = ".", features = ["debug-print"] }
|
||||
pretty_assertions = { version = "^0.7" }
|
||||
|
||||
[features]
|
||||
debug-print = []
|
||||
debug-print = ["log"]
|
||||
default = [
|
||||
"macros",
|
||||
"mock",
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Design Goals
|
||||
# Design
|
||||
|
||||
We are heavily inspired by ActiveRecord, Eloquent and TypeORM.
|
||||
|
||||
@ -20,7 +20,7 @@ After some bitterness we realized it is not possible to capture everything compi
|
||||
want to encounter problems at run time either. The solution is to perform checking at 'test time' to
|
||||
uncover problems. These checks will be removed at production so there will be no run time penalty.
|
||||
|
||||
## Readability
|
||||
## API style
|
||||
|
||||
### Turbofish and inference
|
||||
|
||||
@ -65,4 +65,35 @@ has_many(cake::Entity, cake::Column::Id, fruit::Column::CakeId)
|
||||
we'd prefer having a builder and stating the params explicitly:
|
||||
```rust
|
||||
has_many(cake::Entity).from(cake::Column::Id).to(fruit::Column::CakeId)
|
||||
```
|
||||
|
||||
### Method overloading
|
||||
|
||||
Consider the following two methods, which accept the same parameter but in different forms:
|
||||
|
||||
```rust
|
||||
fn method_with_model(m: Model) { ... }
|
||||
fn method_with_active_model(a: ActiveModel) { ... }
|
||||
```
|
||||
|
||||
We would define a trait
|
||||
|
||||
```rust
|
||||
pub trait IntoActiveModel {
|
||||
fn into_active_model(self) -> ActiveModel;
|
||||
}
|
||||
```
|
||||
|
||||
Such that `Model` and `ActiveModel` both impl this trait.
|
||||
|
||||
In this way, we can overload the two methods:
|
||||
|
||||
```rust
|
||||
pub fn method<A>(a: A)
|
||||
where
|
||||
A: IntoActiveModel,
|
||||
{
|
||||
let a: ActiveModel = a.into_active_model();
|
||||
...
|
||||
}
|
||||
```
|
25
README.md
25
README.md
@ -20,13 +20,9 @@
|
||||
|
||||
SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust.
|
||||
|
||||
```markdown
|
||||
This is an early release of SeaORM, the API is not stable yet.
|
||||
```
|
||||
|
||||
[](https://www.sea-ql.org/SeaORM/docs/index)
|
||||
[](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx)
|
||||
[](https://github.com/SeaQL/sea-orm/issues/37)
|
||||
[](https://github.com/SeaQL/sea-orm/tree/master/examples)
|
||||
[](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example)
|
||||
[](https://discord.com/invite/uCPdDXzbdv)
|
||||
|
||||
## Features
|
||||
@ -68,10 +64,8 @@ let cheese: cake::Model = cheese.unwrap();
|
||||
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?;
|
||||
let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> =
|
||||
Cake::find().find_with_related(Fruit).all(db).await?;
|
||||
|
||||
```
|
||||
### Insert
|
||||
@ -87,7 +81,7 @@ let pear = fruit::ActiveModel {
|
||||
};
|
||||
|
||||
// insert one
|
||||
let res: InsertResult = Fruit::insert(pear).exec(db).await?;
|
||||
let res = Fruit::insert(pear).exec(db).await?;
|
||||
|
||||
println!("InsertResult: {}", res.last_insert_id);
|
||||
|
||||
@ -108,7 +102,7 @@ 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))
|
||||
.col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None)))
|
||||
.filter(fruit::Column::Name.contains("Apple"))
|
||||
.exec(db)
|
||||
.await?;
|
||||
@ -148,6 +142,13 @@ fruit::Entity::delete_many()
|
||||
.await?;
|
||||
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
|
||||
1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.md)
|
||||
1. [Compare with Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel)
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
@ -28,6 +28,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = (i32, i32);
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ pub async fn insert_and_update(db: &DbConn) -> Result<(), DbErr> {
|
||||
name: Set("pear".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
let res: InsertResult = Fruit::insert(pear).exec(db).await?;
|
||||
let res = Fruit::insert(pear).exec(db).await?;
|
||||
|
||||
println!();
|
||||
println!("Inserted: last_insert_id = {}\n", res.last_insert_id);
|
||||
|
34
examples/rocket_example/Cargo.toml
Normal file
34
examples/rocket_example/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "sea-orm-rocket-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Sam Samai <sam@studio2pi.com.au>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
async-stream = { version = "^0.3" }
|
||||
async-trait = { version = "0.1" }
|
||||
futures = { version = "^0.3" }
|
||||
futures-util = { version = "^0.3" }
|
||||
rocket = { git = "https://github.com/SergioBenitez/Rocket.git", features = [
|
||||
"json",
|
||||
] }
|
||||
rocket_db_pools = { git = "https://github.com/SergioBenitez/Rocket.git" }
|
||||
rocket_dyn_templates = { git = "https://github.com/SergioBenitez/Rocket.git", features = [
|
||||
"tera",
|
||||
] }
|
||||
# remove `path = ""` in your own project
|
||||
sea-orm = { path = "../../", version = "^0.2" }
|
||||
serde_json = { version = "^1" }
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "^0.5"
|
||||
default-features = false
|
||||
features = ["macros", "offline", "migrate"]
|
||||
|
||||
[features]
|
||||
default = ["sqlx-postgres"]
|
||||
sqlx-mysql = ["sea-orm/sqlx-mysql", "rocket_db_pools/sqlx_mysql"]
|
||||
sqlx-postgres = ["sea-orm/sqlx-postgres", "rocket_db_pools/sqlx_postgres"]
|
11
examples/rocket_example/README.md
Normal file
11
examples/rocket_example/README.md
Normal file
@ -0,0 +1,11 @@
|
||||

|
||||
|
||||
# Rocket with SeaORM example app
|
||||
|
||||
1. Modify the `url` var in `Rocket.toml` to point to your chosen database
|
||||
|
||||
1. Turn on the appropriate database feature for your chosen db in `Cargo.toml` (the `default = ["sqlx-postgres"]` line)
|
||||
|
||||
1. `cargo run` to start the server
|
||||
|
||||
1. Open in browser after seeing the `🚀 Rocket has launched from http://localhost:8000` line
|
11
examples/rocket_example/Rocket.toml
Normal file
11
examples/rocket_example/Rocket.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[default]
|
||||
template_dir = "templates/"
|
||||
|
||||
[default.databases.rocket_example]
|
||||
# Mysql
|
||||
# make sure to enable "sqlx-mysql" feature in Cargo.toml, i.e default = ["sqlx-mysql"]
|
||||
# url = "mysql://root:@localhost/rocket_example"
|
||||
|
||||
# Postgres
|
||||
# make sure to enable "sqlx-postgres" feature in Cargo.toml, i.e default = ["sqlx-postgres"]
|
||||
url = "postgres://root:root@localhost/rocket_example"
|
BIN
examples/rocket_example/Screenshot.png
Normal file
BIN
examples/rocket_example/Screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
157
examples/rocket_example/src/main.rs
Normal file
157
examples/rocket_example/src/main.rs
Normal file
@ -0,0 +1,157 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
use rocket::fairing::{self, AdHoc};
|
||||
use rocket::form::{Context, Form};
|
||||
use rocket::fs::{relative, FileServer};
|
||||
use rocket::request::FlashMessage;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::{Build, Request, Rocket};
|
||||
use rocket_db_pools::{sqlx, Connection, Database};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
|
||||
use sea_orm::entity::*;
|
||||
use sea_orm::QueryOrder;
|
||||
|
||||
mod pool;
|
||||
use pool::RocketDbPool;
|
||||
|
||||
mod setup;
|
||||
|
||||
#[derive(Database, Debug)]
|
||||
#[database("rocket_example")]
|
||||
struct Db(RocketDbPool);
|
||||
|
||||
type Result<T, E = rocket::response::Debug<sqlx::Error>> = std::result::Result<T, E>;
|
||||
|
||||
mod post;
|
||||
pub use post::Entity as Post;
|
||||
|
||||
#[get("/new")]
|
||||
fn new() -> Template {
|
||||
Template::render("new", &Context::default())
|
||||
}
|
||||
|
||||
#[post("/", data = "<post_form>")]
|
||||
async fn create(conn: Connection<Db>, post_form: Form<post::Model>) -> Flash<Redirect> {
|
||||
let post = post_form.into_inner();
|
||||
|
||||
let _post = post::ActiveModel {
|
||||
title: Set(post.title.to_owned()),
|
||||
text: Set(post.text.to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&conn)
|
||||
.await
|
||||
.expect("could not insert post");
|
||||
|
||||
Flash::success(Redirect::to("/"), "Post successfully added.")
|
||||
}
|
||||
|
||||
#[post("/<id>", data = "<post_form>")]
|
||||
async fn update(conn: Connection<Db>, id: i64, post_form: Form<post::Model>) -> Flash<Redirect> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(&conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
let post_data = post_form.into_inner();
|
||||
|
||||
let _edited_post = post::ActiveModel {
|
||||
id: post.id,
|
||||
title: Set(post_data.title.to_owned()),
|
||||
text: Set(post_data.text.to_owned()),
|
||||
}
|
||||
.save(&conn)
|
||||
.await
|
||||
.expect("could not edit post");
|
||||
|
||||
Flash::success(Redirect::to("/"), "Post successfully edited.")
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn list(conn: Connection<Db>, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let posts = Post::find()
|
||||
.order_by_asc(post::Column::Id)
|
||||
.all(&conn)
|
||||
.await
|
||||
.expect("could not retrieve posts")
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
let flash = flash.map(FlashMessage::into_inner);
|
||||
|
||||
Template::render(
|
||||
"index",
|
||||
context! {
|
||||
posts: posts,
|
||||
flash: flash,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/<id>")]
|
||||
async fn edit(conn: Connection<Db>, id: i64) -> Template {
|
||||
let post: Option<post::Model> = Post::find_by_id(id)
|
||||
.one(&conn)
|
||||
.await
|
||||
.expect("could not find post");
|
||||
|
||||
Template::render(
|
||||
"edit",
|
||||
context! {
|
||||
post: post,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[delete("/<id>")]
|
||||
async fn delete(conn: Connection<Db>, id: i32) -> Flash<Redirect> {
|
||||
let post: post::ActiveModel = Post::find_by_id(id)
|
||||
.one(&conn)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.into();
|
||||
let _result = post.delete(&conn).await.unwrap();
|
||||
|
||||
Flash::success(Redirect::to("/"), "Post successfully deleted.")
|
||||
}
|
||||
|
||||
#[delete("/")]
|
||||
async fn destroy(conn: Connection<Db>) -> Result<()> {
|
||||
let _result = Post::delete_many().exec(&conn).await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub fn not_found(req: &Request<'_>) -> Template {
|
||||
Template::render(
|
||||
"error/404",
|
||||
context! {
|
||||
uri: req.uri()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn run_migrations(rocket: Rocket<Build>) -> fairing::Result {
|
||||
let db_url = Db::fetch(&rocket).unwrap().db_url.clone();
|
||||
let conn = sea_orm::Database::connect(&db_url).await.unwrap();
|
||||
let _create_post_table = setup::create_post_table(&conn).await;
|
||||
Ok(rocket)
|
||||
}
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
rocket::build()
|
||||
.attach(Db::init())
|
||||
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
|
||||
.mount("/", FileServer::from(relative!("/static")))
|
||||
.mount(
|
||||
"/",
|
||||
routes![new, create, delete, destroy, list, edit, update],
|
||||
)
|
||||
.register("/", catchers![not_found])
|
||||
.attach(Template::fairing())
|
||||
}
|
27
examples/rocket_example/src/pool.rs
Normal file
27
examples/rocket_example/src/pool.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use async_trait::async_trait;
|
||||
use rocket_db_pools::{rocket::figment::Figment, Config};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RocketDbPool {
|
||||
pub db_url: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl rocket_db_pools::Pool for RocketDbPool {
|
||||
type Error = sea_orm::DbErr;
|
||||
|
||||
type Connection = sea_orm::DatabaseConnection;
|
||||
|
||||
async fn init(figment: &Figment) -> Result<Self, Self::Error> {
|
||||
let config = figment.extract::<Config>().unwrap();
|
||||
let db_url = config.url;
|
||||
|
||||
Ok(RocketDbPool {
|
||||
db_url: db_url.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get(&self) -> Result<Self::Connection, Self::Error> {
|
||||
Ok(sea_orm::Database::connect(&self.db_url).await.unwrap())
|
||||
}
|
||||
}
|
65
examples/rocket_example/src/post.rs
Normal file
65
examples/rocket_example/src/post.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, DeriveEntity, Deserialize, Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Entity;
|
||||
|
||||
impl EntityName for Entity {
|
||||
fn table_name(&self) -> &str {
|
||||
"posts"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel, Deserialize, Serialize, FromForm,
|
||||
)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Model {
|
||||
#[serde(skip_deserializing, skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<i32>,
|
||||
pub title: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
pub enum Column {
|
||||
Id,
|
||||
Title,
|
||||
Text,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
|
||||
pub enum PrimaryKey {
|
||||
Id,
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ColumnTrait for Column {
|
||||
type EntityName = Entity;
|
||||
|
||||
fn def(&self) -> ColumnDef {
|
||||
match self {
|
||||
Self::Id => ColumnType::Integer.def(),
|
||||
Self::Title => ColumnType::String(None).def(),
|
||||
Self::Text => ColumnType::String(None).def(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
33
examples/rocket_example/src/setup.rs
Normal file
33
examples/rocket_example/src/setup.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
|
||||
use sea_orm::{error::*, sea_query, DbConn, ExecResult};
|
||||
|
||||
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
|
||||
let builder = db.get_database_backend();
|
||||
db.execute(builder.build(stmt)).await
|
||||
}
|
||||
|
||||
pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
.table(super::post::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(super::post::Column::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(super::post::Column::Title)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(super::post::Column::Text)
|
||||
.string()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
}
|
427
examples/rocket_example/static/css/normalize.css
vendored
Normal file
427
examples/rocket_example/static/css/normalize.css
vendored
Normal file
@ -0,0 +1,427 @@
|
||||
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||
* user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability when focused and also mouse hovered in all browsers.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||
input[type="reset"],
|
||||
input[type="submit"] {
|
||||
-webkit-appearance: button; /* 2 */
|
||||
cursor: pointer; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-set default cursor for disabled elements.
|
||||
*/
|
||||
|
||||
button[disabled],
|
||||
html input[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and border in Firefox 4+.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
input::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||
* the UA stylesheet.
|
||||
*/
|
||||
|
||||
input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's recommended that you don't attempt to style these elements.
|
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||
*
|
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||
* 2. Remove excess padding in IE 8/9/10.
|
||||
*/
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||
* `font-size` values of the `input`, it causes the cursor style of the
|
||||
* decrement button to change from `default` to `text`.
|
||||
*/
|
||||
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||
* (include `-moz` to future-proof).
|
||||
*/
|
||||
|
||||
input[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
-moz-box-sizing: content-box;
|
||||
-webkit-box-sizing: content-box; /* 2 */
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||
* padding (and `textfield` appearance).
|
||||
*/
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define consistent border, margin, and padding.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
border: 1px solid #c0c0c0;
|
||||
margin: 0 2px;
|
||||
padding: 0.35em 0.625em 0.75em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||
*/
|
||||
|
||||
legend {
|
||||
border: 0; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't inherit the `font-weight` (applied by a rule above).
|
||||
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||
*/
|
||||
|
||||
optgroup {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tables
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove most spacing between table cells.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0;
|
||||
}
|
421
examples/rocket_example/static/css/skeleton.css
vendored
Normal file
421
examples/rocket_example/static/css/skeleton.css
vendored
Normal file
@ -0,0 +1,421 @@
|
||||
/*
|
||||
* Skeleton V2.0.4
|
||||
* Copyright 2014, Dave Gamache
|
||||
* www.getskeleton.com
|
||||
* Free to use under the MIT license.
|
||||
* https://opensource.org/licenses/mit-license.php
|
||||
* 12/29/2014
|
||||
*/
|
||||
|
||||
|
||||
/* Table of contents
|
||||
––––––––––––––––––––––––––––––––––––––––––––––––––
|
||||
- Grid
|
||||
- Base Styles
|
||||
- Typography
|
||||
- Links
|
||||
- Buttons
|
||||
- Forms
|
||||
- Lists
|
||||
- Code
|
||||
- Tables
|
||||
- Spacing
|
||||
- Utilities
|
||||
- Clearing
|
||||
- Media Queries
|
||||
*/
|
||||
|
||||
|
||||
/* Grid
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box; }
|
||||
.column,
|
||||
.columns {
|
||||
width: 100%;
|
||||
float: left;
|
||||
box-sizing: border-box; }
|
||||
|
||||
/* For devices larger than 400px */
|
||||
@media (min-width: 400px) {
|
||||
.container {
|
||||
width: 85%;
|
||||
padding: 0; }
|
||||
}
|
||||
|
||||
/* For devices larger than 550px */
|
||||
@media (min-width: 550px) {
|
||||
.container {
|
||||
width: 80%; }
|
||||
.column,
|
||||
.columns {
|
||||
margin-left: 4%; }
|
||||
.column:first-child,
|
||||
.columns:first-child {
|
||||
margin-left: 0; }
|
||||
|
||||
.one.column,
|
||||
.one.columns { width: 4.66666666667%; }
|
||||
.two.columns { width: 13.3333333333%; }
|
||||
.three.columns { width: 22%; }
|
||||
.four.columns { width: 30.6666666667%; }
|
||||
.five.columns { width: 39.3333333333%; }
|
||||
.six.columns { width: 48%; }
|
||||
.seven.columns { width: 56.6666666667%; }
|
||||
.eight.columns { width: 65.3333333333%; }
|
||||
.nine.columns { width: 74.0%; }
|
||||
.ten.columns { width: 82.6666666667%; }
|
||||
.eleven.columns { width: 91.3333333333%; }
|
||||
.twelve.columns { width: 100%; margin-left: 0; }
|
||||
|
||||
.one-third.column { width: 30.6666666667%; }
|
||||
.two-thirds.column { width: 65.3333333333%; }
|
||||
|
||||
.one-half.column { width: 48%; }
|
||||
|
||||
/* Offsets */
|
||||
.offset-by-one.column,
|
||||
.offset-by-one.columns { margin-left: 8.66666666667%; }
|
||||
.offset-by-two.column,
|
||||
.offset-by-two.columns { margin-left: 17.3333333333%; }
|
||||
.offset-by-three.column,
|
||||
.offset-by-three.columns { margin-left: 26%; }
|
||||
.offset-by-four.column,
|
||||
.offset-by-four.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-five.column,
|
||||
.offset-by-five.columns { margin-left: 43.3333333333%; }
|
||||
.offset-by-six.column,
|
||||
.offset-by-six.columns { margin-left: 52%; }
|
||||
.offset-by-seven.column,
|
||||
.offset-by-seven.columns { margin-left: 60.6666666667%; }
|
||||
.offset-by-eight.column,
|
||||
.offset-by-eight.columns { margin-left: 69.3333333333%; }
|
||||
.offset-by-nine.column,
|
||||
.offset-by-nine.columns { margin-left: 78.0%; }
|
||||
.offset-by-ten.column,
|
||||
.offset-by-ten.columns { margin-left: 86.6666666667%; }
|
||||
.offset-by-eleven.column,
|
||||
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
|
||||
|
||||
.offset-by-one-third.column,
|
||||
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
|
||||
.offset-by-two-thirds.column,
|
||||
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
|
||||
|
||||
.offset-by-one-half.column,
|
||||
.offset-by-one-half.columns { margin-left: 52%; }
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Base Styles
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/* NOTE
|
||||
html is set to 62.5% so that all the REM measurements throughout Skeleton
|
||||
are based on 10px sizing. So basically 1.5rem = 15px :) */
|
||||
html {
|
||||
font-size: 62.5%; }
|
||||
body {
|
||||
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
|
||||
line-height: 1.6;
|
||||
font-weight: 400;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #222; }
|
||||
|
||||
|
||||
/* Typography
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 300; }
|
||||
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
|
||||
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
|
||||
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
|
||||
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
|
||||
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
|
||||
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
|
||||
|
||||
/* Larger than phablet */
|
||||
@media (min-width: 550px) {
|
||||
h1 { font-size: 5.0rem; }
|
||||
h2 { font-size: 4.2rem; }
|
||||
h3 { font-size: 3.6rem; }
|
||||
h4 { font-size: 3.0rem; }
|
||||
h5 { font-size: 2.4rem; }
|
||||
h6 { font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0; }
|
||||
|
||||
|
||||
/* Links
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
a {
|
||||
color: #1EAEDB; }
|
||||
a:hover {
|
||||
color: #0FA0CE; }
|
||||
|
||||
|
||||
/* Buttons
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.button,
|
||||
button,
|
||||
input[type="submit"],
|
||||
input[type="reset"],
|
||||
input[type="button"] {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 30px;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
letter-spacing: .1rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box; }
|
||||
.button:hover,
|
||||
button:hover,
|
||||
input[type="submit"]:hover,
|
||||
input[type="reset"]:hover,
|
||||
input[type="button"]:hover,
|
||||
.button:focus,
|
||||
button:focus,
|
||||
input[type="submit"]:focus,
|
||||
input[type="reset"]:focus,
|
||||
input[type="button"]:focus {
|
||||
color: #333;
|
||||
border-color: #888;
|
||||
outline: 0; }
|
||||
.button.button-primary,
|
||||
button.button-primary,
|
||||
button.primary,
|
||||
input[type="submit"].button-primary,
|
||||
input[type="reset"].button-primary,
|
||||
input[type="button"].button-primary {
|
||||
color: #FFF;
|
||||
background-color: #33C3F0;
|
||||
border-color: #33C3F0; }
|
||||
.button.button-primary:hover,
|
||||
button.button-primary:hover,
|
||||
button.primary:hover,
|
||||
input[type="submit"].button-primary:hover,
|
||||
input[type="reset"].button-primary:hover,
|
||||
input[type="button"].button-primary:hover,
|
||||
.button.button-primary:focus,
|
||||
button.button-primary:focus,
|
||||
button.primary:focus,
|
||||
input[type="submit"].button-primary:focus,
|
||||
input[type="reset"].button-primary:focus,
|
||||
input[type="button"].button-primary:focus {
|
||||
color: #FFF;
|
||||
background-color: #1EAEDB;
|
||||
border-color: #1EAEDB; }
|
||||
|
||||
|
||||
/* Forms
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea,
|
||||
select {
|
||||
height: 38px;
|
||||
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
background-color: #fff;
|
||||
border: 1px solid #D1D1D1;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box; }
|
||||
/* Removes awkward default styles on some inputs for iOS */
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="search"],
|
||||
input[type="text"],
|
||||
input[type="tel"],
|
||||
input[type="url"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none; }
|
||||
textarea {
|
||||
min-height: 65px;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px; }
|
||||
input[type="email"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="search"]:focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="tel"]:focus,
|
||||
input[type="url"]:focus,
|
||||
input[type="password"]:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
border: 1px solid #33C3F0;
|
||||
outline: 0; }
|
||||
label,
|
||||
legend {
|
||||
display: block;
|
||||
margin-bottom: .5rem;
|
||||
font-weight: 600; }
|
||||
fieldset {
|
||||
padding: 0;
|
||||
border-width: 0; }
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline; }
|
||||
label > .label-body {
|
||||
display: inline-block;
|
||||
margin-left: .5rem;
|
||||
font-weight: normal; }
|
||||
|
||||
|
||||
/* Lists
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
ul {
|
||||
list-style: circle inside; }
|
||||
ol {
|
||||
list-style: decimal inside; }
|
||||
ol, ul {
|
||||
padding-left: 0;
|
||||
margin-top: 0; }
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
font-size: 90%; }
|
||||
li {
|
||||
margin-bottom: 1rem; }
|
||||
|
||||
|
||||
/* Code
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
code {
|
||||
padding: .2rem .5rem;
|
||||
margin: 0 .2rem;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
background: #F1F1F1;
|
||||
border: 1px solid #E1E1E1;
|
||||
border-radius: 4px; }
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre; }
|
||||
|
||||
|
||||
/* Tables
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #E1E1E1; }
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0; }
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0; }
|
||||
|
||||
|
||||
/* Spacing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
button,
|
||||
.button {
|
||||
margin-bottom: 1rem; }
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
fieldset {
|
||||
margin-bottom: 1.5rem; }
|
||||
pre,
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
table,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
form {
|
||||
margin-bottom: 2.5rem; }
|
||||
|
||||
|
||||
/* Utilities
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
.u-full-width {
|
||||
width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-max-full-width {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box; }
|
||||
.u-pull-right {
|
||||
float: right; }
|
||||
.u-pull-left {
|
||||
float: left; }
|
||||
|
||||
|
||||
/* Misc
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
hr {
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 3.5rem;
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1; }
|
||||
|
||||
|
||||
/* Clearing
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
|
||||
/* Self Clearing Goodness */
|
||||
.container:after,
|
||||
.row:after,
|
||||
.u-cf {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both; }
|
||||
|
||||
|
||||
/* Media Queries
|
||||
–––––––––––––––––––––––––––––––––––––––––––––––––– */
|
||||
/*
|
||||
Note: The best way to structure the use of media queries is to create the queries
|
||||
near the relevant code. For example, if you wanted to change the styles for buttons
|
||||
on small devices, paste the mobile query code up in the buttons section and style it
|
||||
there.
|
||||
*/
|
||||
|
||||
|
||||
/* Larger than mobile */
|
||||
@media (min-width: 400px) {}
|
||||
|
||||
/* Larger than phablet (also point when grid becomes active) */
|
||||
@media (min-width: 550px) {}
|
||||
|
||||
/* Larger than tablet */
|
||||
@media (min-width: 750px) {}
|
||||
|
||||
/* Larger than desktop */
|
||||
@media (min-width: 1000px) {}
|
||||
|
||||
/* Larger than Desktop HD */
|
||||
@media (min-width: 1200px) {}
|
73
examples/rocket_example/static/css/style.css
Normal file
73
examples/rocket_example/static/css/style.css
Normal file
@ -0,0 +1,73 @@
|
||||
.field-error {
|
||||
border: 1px solid #ff0000 !important;
|
||||
}
|
||||
|
||||
.field-error-flash {
|
||||
color: #ff0000;
|
||||
display: block;
|
||||
margin: -10px 0 10px 0;
|
||||
}
|
||||
|
||||
.field-success {
|
||||
border: 1px solid #5ab953 !important;
|
||||
}
|
||||
|
||||
.field-success-flash {
|
||||
color: #5ab953;
|
||||
display: block;
|
||||
margin: -10px 0 10px 0;
|
||||
}
|
||||
|
||||
span.completed {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
form.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
form.link,
|
||||
button.link {
|
||||
display: inline;
|
||||
color: #1eaedb;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 0 0 0 0;
|
||||
height: inherit;
|
||||
text-decoration: underline;
|
||||
font-size: inherit;
|
||||
text-transform: none;
|
||||
font-weight: normal;
|
||||
line-height: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
form.link:hover,
|
||||
button.link:hover {
|
||||
color: #0fa0ce;
|
||||
}
|
||||
|
||||
button.small {
|
||||
height: 20px;
|
||||
padding: 0 10px;
|
||||
font-size: 10px;
|
||||
line-height: 20px;
|
||||
margin: 0 2.5px;
|
||||
}
|
||||
|
||||
.post:hover {
|
||||
background-color: #bce2ee;
|
||||
}
|
||||
|
||||
.post td {
|
||||
padding: 5px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#delete-button {
|
||||
color: red;
|
||||
border-color: red;
|
||||
}
|
BIN
examples/rocket_example/static/images/favicon.png
Normal file
BIN
examples/rocket_example/static/images/favicon.png
Normal file
Binary file not shown.
81
examples/rocket_example/templates/base.html.tera
Normal file
81
examples/rocket_example/templates/base.html.tera
Normal file
@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Rocket Todo Example</title>
|
||||
<meta name="description" content="A todo application written in Rocket." />
|
||||
<meta name="author" content="Sergio Benitez" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link
|
||||
href="//fonts.googleapis.com/css?family=Raleway:400,300,600"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<link rel="stylesheet" href="/css/normalize.css" />
|
||||
<link rel="stylesheet" href="/css/skeleton.css" />
|
||||
<link rel="stylesheet" href="/css/style.css" />
|
||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<p><!--Nothing to see here --></p>
|
||||
|
||||
{#
|
||||
<div class="row">
|
||||
<h4>Rocket Todo</h4>
|
||||
<form action="/todo" method="post">
|
||||
<div class="ten columns">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter a task description..."
|
||||
name="description"
|
||||
id="description"
|
||||
value=""
|
||||
autofocus
|
||||
class="u-full-width {% if msg %}field-{{msg.0}}{% endif %}"
|
||||
/>
|
||||
{% if msg %}
|
||||
<small class="field-{{msg.0}}-msg">
|
||||
{{ msg.1 }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="two columns">
|
||||
<input type="submit" value="add task" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="twelve columns">
|
||||
<ul>
|
||||
{% for task in tasks %} {% if task.completed %}
|
||||
<li>
|
||||
<span class="completed">{{ task.description }}</span>
|
||||
<form class="inline" action="/todo/{{ task.id }}" method="post">
|
||||
<input type="hidden" name="_method" value="put" />
|
||||
<button class="small" type="submit">undo</button>
|
||||
</form>
|
||||
<form class="inline" action="/todo/{{ task.id }}" method="post">
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<button class="primary small" type="submit">delete</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<form class="link" action="/todo/{{ task.id }}" method="post">
|
||||
<input type="hidden" name="_method" value="put" />
|
||||
<button class="link" type="submit">
|
||||
{{ task.description }}
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %} {% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
#} {% block content %}{% endblock content %}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
50
examples/rocket_example/templates/edit.html.tera
Normal file
50
examples/rocket_example/templates/edit.html.tera
Normal file
@ -0,0 +1,50 @@
|
||||
{% extends "base" %} {% block content %}
|
||||
<div class="row">
|
||||
<h4>Edit Post</h4>
|
||||
<div class="twelve columns">
|
||||
<div class="ten columns">
|
||||
<form action="/{{ post.id }}" method="post">
|
||||
<div class="twelve columns">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="title"
|
||||
name="title"
|
||||
id="title"
|
||||
value="{{ post.title }}"
|
||||
autofocus
|
||||
class="u-full-width"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="content"
|
||||
name="text"
|
||||
id="text"
|
||||
value="{{ post.text }}"
|
||||
autofocus
|
||||
class="u-full-width"
|
||||
/>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<div class="two columns">
|
||||
<a href="/">
|
||||
<input type="button" value="cancel" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="eight columns"></div>
|
||||
<div class="two columns">
|
||||
<input type="submit" value="save post" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="two columns">
|
||||
<form action="/{{ post.id }}" method="post">
|
||||
<div class="two columns">
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<input id="delete-button" type="submit" value="delete post" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
11
examples/rocket_example/templates/error/404.html.tera
Normal file
11
examples/rocket_example/templates/error/404.html.tera
Normal file
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>404 - tera</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>404: Hey! There's nothing here.</h1>
|
||||
The page at {{ uri }} does not exist!
|
||||
</body>
|
||||
</html>
|
33
examples/rocket_example/templates/index.html.tera
Normal file
33
examples/rocket_example/templates/index.html.tera
Normal file
@ -0,0 +1,33 @@
|
||||
{% extends "base" %} {% block content %}
|
||||
<h1>Posts</h1>
|
||||
{% if flash %}
|
||||
<small class="field-{{ flash.0 }}-flash">
|
||||
{{ flash.1 }}
|
||||
</small>
|
||||
{% endif %}
|
||||
<table>
|
||||
<tbody>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Title</th>
|
||||
<th>Text</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for post in posts %}
|
||||
<tr class="post" onclick="window.location='/{{ post.id }}';">
|
||||
<td>{{ post.id }}</td>
|
||||
<td>{{ post.title }}</td>
|
||||
<td>{{ post.text }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="twelve columns">
|
||||
<a href="/new">
|
||||
<input type="button" value="add post" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endblock content %}
|
38
examples/rocket_example/templates/new.html.tera
Normal file
38
examples/rocket_example/templates/new.html.tera
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends "base" %} {% block content %}
|
||||
<div class="row">
|
||||
<h4>New Post</h4>
|
||||
<form action="/" method="post">
|
||||
<div class="twelve columns">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter title"
|
||||
name="title"
|
||||
id="title"
|
||||
value=""
|
||||
autofocus
|
||||
class="u-full-width"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="enter content"
|
||||
name="text"
|
||||
id="text"
|
||||
value=""
|
||||
autofocus
|
||||
class="u-full-width"
|
||||
/>
|
||||
</div>
|
||||
<div class="twelve columns">
|
||||
<div class="two columns">
|
||||
<a href="/">
|
||||
<input type="button" value="cancel" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="eight columns"></div>
|
||||
<div class="two columns">
|
||||
<input type="submit" value="save post" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -27,6 +27,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
1
rustfmt.toml
Normal file
1
rustfmt.toml
Normal file
@ -0,0 +1 @@
|
||||
format_code_in_doc_comments=true
|
@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "sea-orm-cli"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
|
||||
edition = "2018"
|
||||
description = "Command line utility for SeaORM"
|
||||
@ -22,7 +22,7 @@ clap = { version = "^2.33.3" }
|
||||
dotenv = { version = "^0.15" }
|
||||
async-std = { version = "^1.9", features = [ "attributes" ] }
|
||||
sea-orm = { version = "^0.1.2", features = [ "sqlx-all" ] }
|
||||
sea-orm-codegen = { version = "^0.1.3" }
|
||||
sea-orm-codegen = { version = "^0.2.0" }
|
||||
sea-schema = { version = "^0.2.7", default-features = false, features = [
|
||||
"sqlx-mysql",
|
||||
"sqlx-postgres",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sea-orm-codegen"
|
||||
version = "0.1.3"
|
||||
version = "0.2.0"
|
||||
authors = ["Billy Chan <ccw.billy.123@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Code Generator for SeaORM"
|
||||
|
@ -117,6 +117,31 @@ impl Entity {
|
||||
format_ident!("{}", auto_increment)
|
||||
}
|
||||
|
||||
pub fn get_primary_key_rs_type(&self) -> TokenStream {
|
||||
let types = self
|
||||
.primary_keys
|
||||
.iter()
|
||||
.map(|primary_key| {
|
||||
self.columns
|
||||
.iter()
|
||||
.find(|col| col.name.eq(&primary_key.name))
|
||||
.unwrap()
|
||||
.get_rs_type()
|
||||
.to_string()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !types.is_empty() {
|
||||
let value_type = if types.len() > 1 {
|
||||
vec!["(".to_owned(), types.join(", "), ")".to_owned()]
|
||||
} else {
|
||||
types
|
||||
};
|
||||
value_type.join("").parse().unwrap()
|
||||
} else {
|
||||
TokenStream::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_conjunct_relations_via_snake_case(&self) -> Vec<Ident> {
|
||||
self.conjunct_relations
|
||||
.iter()
|
||||
@ -151,7 +176,7 @@ mod tests {
|
||||
columns: vec![
|
||||
Column {
|
||||
name: "id".to_owned(),
|
||||
col_type: ColumnType::String(None),
|
||||
col_type: ColumnType::Integer(None),
|
||||
auto_increment: false,
|
||||
not_null: false,
|
||||
unique: false,
|
||||
@ -373,6 +398,16 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_primary_key_rs_type() {
|
||||
let entity = setup();
|
||||
|
||||
assert_eq!(
|
||||
entity.get_primary_key_rs_type().to_string(),
|
||||
entity.columns[0].get_rs_type().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_conjunct_relations_via_snake_case() {
|
||||
let entity = setup();
|
||||
|
@ -119,33 +119,18 @@ impl From<&ColumnDef> for Column {
|
||||
Some(ty) => ty.clone(),
|
||||
None => panic!("ColumnType should not be empty"),
|
||||
};
|
||||
let auto_increments: Vec<bool> = col_def
|
||||
let auto_increment = col_def
|
||||
.get_column_spec()
|
||||
.iter()
|
||||
.filter_map(|spec| match spec {
|
||||
ColumnSpec::AutoIncrement => Some(true),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let auto_increment = !auto_increments.is_empty();
|
||||
let not_nulls: Vec<bool> = col_def
|
||||
.any(|spec| matches!(spec, ColumnSpec::AutoIncrement));
|
||||
let not_null = col_def
|
||||
.get_column_spec()
|
||||
.iter()
|
||||
.filter_map(|spec| match spec {
|
||||
ColumnSpec::NotNull => Some(true),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let not_null = !not_nulls.is_empty();
|
||||
let uniques: Vec<bool> = col_def
|
||||
.any(|spec| matches!(spec, ColumnSpec::NotNull));
|
||||
let unique = col_def
|
||||
.get_column_spec()
|
||||
.iter()
|
||||
.filter_map(|spec| match spec {
|
||||
ColumnSpec::UniqueKey => Some(true),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let unique = !uniques.is_empty();
|
||||
.any(|spec| matches!(spec, ColumnSpec::UniqueKey));
|
||||
Self {
|
||||
name,
|
||||
col_type,
|
||||
|
@ -34,11 +34,6 @@ impl EntityTransformer {
|
||||
.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()
|
||||
@ -85,8 +80,13 @@ impl EntityTransformer {
|
||||
false => {
|
||||
let ref_table = rel.ref_table;
|
||||
let mut unique = true;
|
||||
for col in rel.columns.iter() {
|
||||
if !unique_columns.contains(col) {
|
||||
for column in rel.columns.iter() {
|
||||
if !entity
|
||||
.columns
|
||||
.iter()
|
||||
.filter(|col| col.unique)
|
||||
.any(|col| col.name.as_str() == column)
|
||||
{
|
||||
unique = false;
|
||||
break;
|
||||
}
|
||||
|
@ -173,8 +173,11 @@ impl EntityWriter {
|
||||
|
||||
pub fn gen_impl_primary_key(entity: &Entity) -> TokenStream {
|
||||
let primary_key_auto_increment = entity.get_primary_key_auto_increment();
|
||||
let value_type = entity.get_primary_key_rs_type();
|
||||
quote! {
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = #value_type;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
#primary_key_auto_increment
|
||||
}
|
||||
@ -305,7 +308,7 @@ mod tests {
|
||||
use sea_query::ColumnType;
|
||||
use std::io::{self, BufRead, BufReader};
|
||||
|
||||
const ENTITY_FILES: [&'static str; 5] = [
|
||||
const ENTITY_FILES: [&str; 5] = [
|
||||
include_str!("../../tests/entity/cake.rs"),
|
||||
include_str!("../../tests/entity/cake_filling.rs"),
|
||||
include_str!("../../tests/entity/filling.rs"),
|
||||
|
@ -29,6 +29,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = (i32, i32);
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sea-orm-macros"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
authors = [ "Billy Chan <ccw.billy.123@gmail.com>" ]
|
||||
edition = "2018"
|
||||
description = "Derive macros for SeaORM"
|
||||
|
@ -99,6 +99,10 @@ pub fn test(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
#[test]
|
||||
#(#attrs)*
|
||||
fn #name() #ret {
|
||||
let _ = ::env_logger::builder()
|
||||
.filter_level(::log::LevelFilter::Debug)
|
||||
.is_test(true)
|
||||
.try_init();
|
||||
crate::block_on!(async { #body })
|
||||
}
|
||||
)
|
||||
|
@ -75,7 +75,7 @@ impl DatabaseConnection {
|
||||
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.execute(stmt).await,
|
||||
#[cfg(feature = "mock")]
|
||||
DatabaseConnection::MockDatabaseConnection(conn) => conn.execute(stmt).await,
|
||||
DatabaseConnection::Disconnected => panic!("Disconnected"),
|
||||
DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +89,7 @@ impl DatabaseConnection {
|
||||
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_one(stmt).await,
|
||||
#[cfg(feature = "mock")]
|
||||
DatabaseConnection::MockDatabaseConnection(conn) => conn.query_one(stmt).await,
|
||||
DatabaseConnection::Disconnected => panic!("Disconnected"),
|
||||
DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ impl DatabaseConnection {
|
||||
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_all(stmt).await,
|
||||
#[cfg(feature = "mock")]
|
||||
DatabaseConnection::MockDatabaseConnection(conn) => conn.query_all(stmt).await,
|
||||
DatabaseConnection::Disconnected => panic!("Disconnected"),
|
||||
DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,19 +2,22 @@ use crate::{
|
||||
debug_print, error::*, DatabaseConnection, DbBackend, ExecResult, MockDatabase, QueryResult,
|
||||
Statement, Transaction,
|
||||
};
|
||||
use std::fmt::Debug;
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Mutex,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockDatabaseConnector;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockDatabaseConnection {
|
||||
counter: AtomicUsize,
|
||||
mocker: Mutex<Box<dyn MockDatabaseTrait>>,
|
||||
}
|
||||
|
||||
pub trait MockDatabaseTrait: Send {
|
||||
pub trait MockDatabaseTrait: Send + Debug {
|
||||
fn execute(&mut self, counter: usize, stmt: Statement) -> Result<ExecResult, DbErr>;
|
||||
|
||||
fn query(&mut self, counter: usize, stmt: Statement) -> Result<Vec<QueryResult>, DbErr>;
|
||||
|
@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement};
|
||||
|
||||
use super::sqlx_common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlxMySqlConnector;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlxMySqlPoolConnection {
|
||||
pool: MySqlPool,
|
||||
}
|
||||
|
@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement};
|
||||
|
||||
use super::sqlx_common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlxPostgresConnector;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlxPostgresPoolConnection {
|
||||
pool: PgPool,
|
||||
}
|
||||
@ -102,24 +104,11 @@ impl From<PgRow> for QueryResult {
|
||||
impl From<PgQueryResult> for ExecResult {
|
||||
fn from(result: PgQueryResult) -> ExecResult {
|
||||
ExecResult {
|
||||
result: ExecResultHolder::SqlxPostgres {
|
||||
last_insert_id: 0,
|
||||
rows_affected: result.rows_affected(),
|
||||
},
|
||||
result: ExecResultHolder::SqlxPostgres(result),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn query_result_into_exec_result(res: QueryResult) -> Result<ExecResult, DbErr> {
|
||||
let last_insert_id: i32 = res.try_get("", "last_insert_id")?;
|
||||
Ok(ExecResult {
|
||||
result: ExecResultHolder::SqlxPostgres {
|
||||
last_insert_id: last_insert_id as u64,
|
||||
rows_affected: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn sqlx_query(stmt: &Statement) -> sqlx::query::Query<'_, Postgres, PgArguments> {
|
||||
let mut query = sqlx::query(&stmt.sql);
|
||||
if let Some(values) = &stmt.values {
|
||||
|
@ -10,8 +10,10 @@ use crate::{debug_print, error::*, executor::*, DatabaseConnection, Statement};
|
||||
|
||||
use super::sqlx_common::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlxSqliteConnector;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SqlxSqlitePoolConnection {
|
||||
pool: SqlitePool,
|
||||
}
|
||||
|
@ -221,16 +221,18 @@ where
|
||||
let exec = E::insert(am).exec(db);
|
||||
let res = exec.await?;
|
||||
// TODO: if the entity does not have auto increment primary key, then last_insert_id is a wrong value
|
||||
if <E::PrimaryKey as PrimaryKeyTrait>::auto_increment() && res.last_insert_id != 0 {
|
||||
// FIXME: Assumed valid last_insert_id is not equals to Default::default()
|
||||
if <E::PrimaryKey as PrimaryKeyTrait>::auto_increment()
|
||||
&& res.last_insert_id != <E::PrimaryKey as PrimaryKeyTrait>::ValueType::default()
|
||||
{
|
||||
let find = E::find_by_id(res.last_insert_id).one(db);
|
||||
let found = find.await;
|
||||
let model: Option<E::Model> = found?;
|
||||
match model {
|
||||
Some(model) => Ok(model.into_active_model()),
|
||||
None => Err(DbErr::Exec(format!(
|
||||
"Failed to find inserted item: {} {}",
|
||||
"Failed to find inserted item: {}",
|
||||
E::default().to_string(),
|
||||
res.last_insert_id
|
||||
))),
|
||||
}
|
||||
} else {
|
||||
|
@ -55,21 +55,21 @@ pub trait EntityTrait: EntityName {
|
||||
where
|
||||
R: EntityTrait,
|
||||
{
|
||||
RelationBuilder::new(RelationType::HasOne, Self::default(), related)
|
||||
RelationBuilder::new(RelationType::HasOne, Self::default(), related, false)
|
||||
}
|
||||
|
||||
fn has_one<R>(_: R) -> RelationBuilder<Self, R>
|
||||
where
|
||||
R: EntityTrait + Related<Self>,
|
||||
{
|
||||
RelationBuilder::from_rel(RelationType::HasOne, R::to().rev())
|
||||
RelationBuilder::from_rel(RelationType::HasOne, R::to().rev(), true)
|
||||
}
|
||||
|
||||
fn has_many<R>(_: R) -> RelationBuilder<Self, R>
|
||||
where
|
||||
R: EntityTrait + Related<Self>,
|
||||
{
|
||||
RelationBuilder::from_rel(RelationType::HasMany, R::to().rev())
|
||||
RelationBuilder::from_rel(RelationType::HasMany, R::to().rev(), true)
|
||||
}
|
||||
|
||||
/// Construct select statement to find one / all models
|
||||
@ -137,13 +137,18 @@ pub trait EntityTrait: EntityName {
|
||||
/// assert_eq!(
|
||||
/// db.into_transaction_log(),
|
||||
/// vec![
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, vec![1u64.into()]
|
||||
/// ),
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
|
||||
/// ),
|
||||
/// ]);
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#,
|
||||
/// vec![1u64.into()]
|
||||
/// ),
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#,
|
||||
/// vec![]
|
||||
/// ),
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
fn find() -> Select<Self> {
|
||||
Select::new()
|
||||
@ -186,8 +191,11 @@ pub trait EntityTrait: EntityName {
|
||||
/// assert_eq!(
|
||||
/// db.into_transaction_log(),
|
||||
/// vec![Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#, vec![11i32.into()]
|
||||
/// )]);
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "cake"."id" = $1"#,
|
||||
/// vec![11i32.into()]
|
||||
/// )]
|
||||
/// );
|
||||
/// ```
|
||||
/// Find by composite key
|
||||
/// ```
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::str::FromStr;
|
||||
use crate::{EntityName, IdenStatic, Iterable};
|
||||
use sea_query::{DynIden, Expr, SeaRc, SelectStatement, SimpleExpr, Value};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ColumnDef {
|
||||
@ -103,7 +103,7 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// cake::Entity::find()
|
||||
/// .filter(cake::Column::Id.between(2,3))
|
||||
/// .filter(cake::Column::Id.between(2, 3))
|
||||
/// .build(DbBackend::MySql)
|
||||
/// .to_string(),
|
||||
/// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` BETWEEN 2 AND 3"
|
||||
@ -121,7 +121,7 @@ pub trait ColumnTrait: IdenStatic + Iterable + FromStr {
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// cake::Entity::find()
|
||||
/// .filter(cake::Column::Id.not_between(2,3))
|
||||
/// .filter(cake::Column::Id.not_between(2, 3))
|
||||
/// .build(DbBackend::MySql)
|
||||
/// .to_string(),
|
||||
/// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`id` NOT BETWEEN 2 AND 3"
|
||||
@ -248,7 +248,11 @@ impl ColumnDef {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn null(mut self) -> Self {
|
||||
pub fn null(self) -> Self {
|
||||
self.nullable()
|
||||
}
|
||||
|
||||
pub fn nullable(mut self) -> Self {
|
||||
self.null = true;
|
||||
self
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{ColumnTrait, EntityTrait, IdenStatic};
|
||||
use sea_query::{DynIden, IntoIden};
|
||||
use sea_query::{Alias, DynIden, Iden, IntoIden, SeaRc};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Identity {
|
||||
@ -8,6 +9,25 @@ pub enum Identity {
|
||||
Ternary(DynIden, DynIden, DynIden),
|
||||
}
|
||||
|
||||
impl Iden for Identity {
|
||||
fn unquoted(&self, s: &mut dyn fmt::Write) {
|
||||
match self {
|
||||
Identity::Unary(iden) => {
|
||||
write!(s, "{}", iden.to_string()).unwrap();
|
||||
}
|
||||
Identity::Binary(iden1, iden2) => {
|
||||
write!(s, "{}", iden1.to_string()).unwrap();
|
||||
write!(s, "{}", iden2.to_string()).unwrap();
|
||||
}
|
||||
Identity::Ternary(iden1, iden2, iden3) => {
|
||||
write!(s, "{}", iden1.to_string()).unwrap();
|
||||
write!(s, "{}", iden2.to_string()).unwrap();
|
||||
write!(s, "{}", iden3.to_string()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoIdentity {
|
||||
fn into_identity(self) -> Identity;
|
||||
}
|
||||
@ -19,6 +39,18 @@ where
|
||||
fn identity_of(self) -> Identity;
|
||||
}
|
||||
|
||||
impl IntoIdentity for String {
|
||||
fn into_identity(self) -> Identity {
|
||||
self.as_str().into_identity()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIdentity for &str {
|
||||
fn into_identity(self) -> Identity {
|
||||
Identity::Unary(SeaRc::new(Alias::new(self)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIdentity for T
|
||||
where
|
||||
T: IdenStatic,
|
||||
|
@ -6,6 +6,7 @@ mod model;
|
||||
pub mod prelude;
|
||||
mod primary_key;
|
||||
mod relation;
|
||||
mod schema;
|
||||
|
||||
pub use active_model::*;
|
||||
pub use base_entity::*;
|
||||
@ -15,3 +16,4 @@ pub use model::*;
|
||||
// pub use prelude::*;
|
||||
pub use primary_key::*;
|
||||
pub use relation::*;
|
||||
pub use schema::*;
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{DbErr, EntityTrait, QueryFilter, QueryResult, Related, Select};
|
||||
use crate::{DbErr, EntityTrait, Linked, QueryFilter, QueryResult, Related, Select};
|
||||
pub use sea_query::Value;
|
||||
use std::fmt::Debug;
|
||||
|
||||
@ -16,6 +16,13 @@ pub trait ModelTrait: Clone + Debug {
|
||||
{
|
||||
<Self::Entity as Related<R>>::find_related().belongs_to(self)
|
||||
}
|
||||
|
||||
fn find_linked<L>(&self, l: L) -> Select<L::ToEntity>
|
||||
where
|
||||
L: Linked<FromEntity = Self::Entity>,
|
||||
{
|
||||
l.find_linked()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FromQueryResult {
|
||||
|
@ -1,9 +1,9 @@
|
||||
pub use crate::{
|
||||
error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType,
|
||||
DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity,
|
||||
DeriveModel, DerivePrimaryKey, EntityModel, EntityName, EntityTrait, EnumIter, Iden, IdenStatic, ModelTrait,
|
||||
PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef,
|
||||
RelationTrait, Select, Value,
|
||||
DeriveModel, DerivePrimaryKey, EntityModel, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden,
|
||||
IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult,
|
||||
Related, RelationDef, RelationTrait, Select, Value,
|
||||
};
|
||||
|
||||
#[cfg(feature = "with-json")]
|
||||
|
@ -1,7 +1,18 @@
|
||||
use super::{ColumnTrait, IdenStatic, Iterable};
|
||||
use crate::{TryFromU64, TryGetableMany};
|
||||
use sea_query::IntoValueTuple;
|
||||
use std::fmt::Debug;
|
||||
|
||||
//LINT: composite primary key cannot auto increment
|
||||
pub trait PrimaryKeyTrait: IdenStatic + Iterable {
|
||||
type ValueType: Sized
|
||||
+ Default
|
||||
+ Debug
|
||||
+ PartialEq
|
||||
+ IntoValueTuple
|
||||
+ TryGetableMany
|
||||
+ TryFromU64;
|
||||
|
||||
fn auto_increment() -> bool;
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ pub enum RelationType {
|
||||
HasMany,
|
||||
}
|
||||
|
||||
pub type ForeignKeyAction = sea_query::ForeignKeyAction;
|
||||
|
||||
pub trait RelationTrait: Iterable + Debug + 'static {
|
||||
fn def(&self) -> RelationDef;
|
||||
}
|
||||
@ -28,14 +30,35 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Linked {
|
||||
type FromEntity: EntityTrait;
|
||||
|
||||
type ToEntity: EntityTrait;
|
||||
|
||||
fn link(&self) -> Vec<RelationDef>;
|
||||
|
||||
fn find_linked(&self) -> Select<Self::ToEntity> {
|
||||
let mut select = Select::new();
|
||||
for rel in self.link().into_iter().rev() {
|
||||
select = select.join_rev(JoinType::InnerJoin, rel);
|
||||
}
|
||||
select
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RelationDef {
|
||||
pub rel_type: RelationType,
|
||||
pub from_tbl: TableRef,
|
||||
pub to_tbl: TableRef,
|
||||
pub from_col: Identity,
|
||||
pub to_col: Identity,
|
||||
pub is_owner: bool,
|
||||
pub on_delete: Option<ForeignKeyAction>,
|
||||
pub on_update: Option<ForeignKeyAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RelationBuilder<E, R>
|
||||
where
|
||||
E: EntityTrait,
|
||||
@ -47,6 +70,9 @@ where
|
||||
to_tbl: TableRef,
|
||||
from_col: Option<Identity>,
|
||||
to_col: Option<Identity>,
|
||||
is_owner: bool,
|
||||
on_delete: Option<ForeignKeyAction>,
|
||||
on_update: Option<ForeignKeyAction>,
|
||||
}
|
||||
|
||||
impl RelationDef {
|
||||
@ -58,6 +84,9 @@ impl RelationDef {
|
||||
to_tbl: self.from_tbl,
|
||||
from_col: self.to_col,
|
||||
to_col: self.from_col,
|
||||
is_owner: !self.is_owner,
|
||||
on_delete: self.on_delete,
|
||||
on_update: self.on_update,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,7 +96,7 @@ where
|
||||
E: EntityTrait,
|
||||
R: EntityTrait,
|
||||
{
|
||||
pub(crate) fn new(rel_type: RelationType, from: E, to: R) -> Self {
|
||||
pub(crate) fn new(rel_type: RelationType, from: E, to: R, is_owner: bool) -> Self {
|
||||
Self {
|
||||
entities: PhantomData,
|
||||
rel_type,
|
||||
@ -75,10 +104,13 @@ where
|
||||
to_tbl: to.table_ref(),
|
||||
from_col: None,
|
||||
to_col: None,
|
||||
is_owner,
|
||||
on_delete: None,
|
||||
on_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef) -> Self {
|
||||
pub(crate) fn from_rel(rel_type: RelationType, rel: RelationDef, is_owner: bool) -> Self {
|
||||
Self {
|
||||
entities: PhantomData,
|
||||
rel_type,
|
||||
@ -86,6 +118,9 @@ where
|
||||
to_tbl: rel.to_tbl,
|
||||
from_col: Some(rel.from_col),
|
||||
to_col: Some(rel.to_col),
|
||||
is_owner,
|
||||
on_delete: None,
|
||||
on_update: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +139,16 @@ where
|
||||
self.to_col = Some(identifier.identity_of());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_delete(mut self, action: ForeignKeyAction) -> Self {
|
||||
self.on_delete = Some(action);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_update(mut self, action: ForeignKeyAction) -> Self {
|
||||
self.on_update = Some(action);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, R> From<RelationBuilder<E, R>> for RelationDef
|
||||
@ -118,6 +163,9 @@ where
|
||||
to_tbl: b.to_tbl,
|
||||
from_col: b.from_col.unwrap(),
|
||||
to_col: b.to_col.unwrap(),
|
||||
is_owner: b.is_owner,
|
||||
on_delete: b.on_delete,
|
||||
on_update: b.on_update,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
161
src/entity/schema.rs
Normal file
161
src/entity/schema.rs
Normal file
@ -0,0 +1,161 @@
|
||||
use crate::{
|
||||
unpack_table_ref, ColumnTrait, EntityTrait, Identity, Iterable, PrimaryKeyToColumn,
|
||||
PrimaryKeyTrait, RelationTrait,
|
||||
};
|
||||
use sea_query::{ColumnDef, ForeignKeyCreateStatement, Iden, Index, TableCreateStatement};
|
||||
|
||||
pub fn entity_to_table_create_statement<E>(entity: E) -> TableCreateStatement
|
||||
where
|
||||
E: EntityTrait,
|
||||
{
|
||||
let mut stmt = TableCreateStatement::new();
|
||||
|
||||
for column in E::Column::iter() {
|
||||
let orm_column_def = column.def();
|
||||
let types = orm_column_def.col_type.into();
|
||||
let mut column_def = ColumnDef::new_with_type(column, types);
|
||||
if !orm_column_def.null {
|
||||
column_def.not_null();
|
||||
}
|
||||
if orm_column_def.unique {
|
||||
column_def.unique_key();
|
||||
}
|
||||
for primary_key in E::PrimaryKey::iter() {
|
||||
if column.to_string() == primary_key.into_column().to_string() {
|
||||
if E::PrimaryKey::auto_increment() {
|
||||
column_def.auto_increment();
|
||||
}
|
||||
if E::PrimaryKey::iter().count() == 1 {
|
||||
column_def.primary_key();
|
||||
}
|
||||
}
|
||||
}
|
||||
if orm_column_def.indexed {
|
||||
stmt.index(
|
||||
Index::create()
|
||||
.name(&format!(
|
||||
"idx-{}-{}",
|
||||
entity.to_string(),
|
||||
column.to_string()
|
||||
))
|
||||
.table(entity)
|
||||
.col(column),
|
||||
);
|
||||
}
|
||||
stmt.col(&mut column_def);
|
||||
}
|
||||
|
||||
if E::PrimaryKey::iter().count() > 1 {
|
||||
let mut idx_pk = Index::create();
|
||||
for primary_key in E::PrimaryKey::iter() {
|
||||
idx_pk.col(primary_key);
|
||||
}
|
||||
stmt.primary_key(idx_pk.name(&format!("pk-{}", entity.to_string())).primary());
|
||||
}
|
||||
|
||||
for relation in E::Relation::iter() {
|
||||
let relation = relation.def();
|
||||
if relation.is_owner {
|
||||
continue;
|
||||
}
|
||||
let mut foreign_key_stmt = ForeignKeyCreateStatement::new();
|
||||
let from_tbl = unpack_table_ref(&relation.from_tbl);
|
||||
let to_tbl = unpack_table_ref(&relation.to_tbl);
|
||||
match relation.from_col {
|
||||
Identity::Unary(o1) => {
|
||||
foreign_key_stmt.from_col(o1);
|
||||
}
|
||||
Identity::Binary(o1, o2) => {
|
||||
foreign_key_stmt.from_col(o1);
|
||||
foreign_key_stmt.from_col(o2);
|
||||
}
|
||||
Identity::Ternary(o1, o2, o3) => {
|
||||
foreign_key_stmt.from_col(o1);
|
||||
foreign_key_stmt.from_col(o2);
|
||||
foreign_key_stmt.from_col(o3);
|
||||
}
|
||||
}
|
||||
match relation.to_col {
|
||||
Identity::Unary(o1) => {
|
||||
foreign_key_stmt.to_col(o1);
|
||||
}
|
||||
Identity::Binary(o1, o2) => {
|
||||
foreign_key_stmt.to_col(o1);
|
||||
foreign_key_stmt.to_col(o2);
|
||||
}
|
||||
crate::Identity::Ternary(o1, o2, o3) => {
|
||||
foreign_key_stmt.to_col(o1);
|
||||
foreign_key_stmt.to_col(o2);
|
||||
foreign_key_stmt.to_col(o3);
|
||||
}
|
||||
}
|
||||
if let Some(action) = relation.on_delete {
|
||||
foreign_key_stmt.on_delete(action);
|
||||
}
|
||||
if let Some(action) = relation.on_update {
|
||||
foreign_key_stmt.on_update(action);
|
||||
}
|
||||
stmt.foreign_key(
|
||||
foreign_key_stmt
|
||||
.name(&format!(
|
||||
"fk-{}-{}",
|
||||
from_tbl.to_string(),
|
||||
to_tbl.to_string()
|
||||
))
|
||||
.from_tbl(from_tbl)
|
||||
.to_tbl(to_tbl),
|
||||
);
|
||||
}
|
||||
|
||||
stmt.table(entity).if_not_exists().take()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{entity_to_table_create_statement, tests_cfg::*};
|
||||
use pretty_assertions::assert_eq;
|
||||
use sea_query::*;
|
||||
|
||||
#[test]
|
||||
fn test_entity_to_table_create_statement() {
|
||||
assert_eq!(
|
||||
entity_to_table_create_statement(CakeFillingPrice).to_string(MysqlQueryBuilder),
|
||||
Table::create()
|
||||
.table(CakeFillingPrice)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(cake_filling_price::Column::CakeId)
|
||||
.integer()
|
||||
.not_null()
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(cake_filling_price::Column::FillingId)
|
||||
.integer()
|
||||
.not_null()
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(cake_filling_price::Column::Price)
|
||||
.decimal()
|
||||
.not_null()
|
||||
)
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.name("pk-cake_filling_price")
|
||||
.col(cake_filling_price::Column::CakeId)
|
||||
.col(cake_filling_price::Column::FillingId)
|
||||
.primary()
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKeyCreateStatement::new()
|
||||
.name("fk-cake_filling_price-cake_filling")
|
||||
.from_tbl(CakeFillingPrice)
|
||||
.from_col(cake_filling_price::Column::CakeId)
|
||||
.from_col(cake_filling_price::Column::FillingId)
|
||||
.to_tbl(CakeFilling)
|
||||
.to_col(cake_filling::Column::CakeId)
|
||||
.to_col(cake_filling::Column::FillingId)
|
||||
)
|
||||
.to_string(MysqlQueryBuilder)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum DbErr {
|
||||
Conn(String),
|
||||
Exec(String),
|
||||
|
@ -8,10 +8,7 @@ pub(crate) enum ExecResultHolder {
|
||||
#[cfg(feature = "sqlx-mysql")]
|
||||
SqlxMySql(sqlx::mysql::MySqlQueryResult),
|
||||
#[cfg(feature = "sqlx-postgres")]
|
||||
SqlxPostgres {
|
||||
last_insert_id: u64,
|
||||
rows_affected: u64,
|
||||
},
|
||||
SqlxPostgres(sqlx::postgres::PgQueryResult),
|
||||
#[cfg(feature = "sqlx-sqlite")]
|
||||
SqlxSqlite(sqlx::sqlite::SqliteQueryResult),
|
||||
#[cfg(feature = "mock")]
|
||||
@ -26,7 +23,9 @@ impl ExecResult {
|
||||
#[cfg(feature = "sqlx-mysql")]
|
||||
ExecResultHolder::SqlxMySql(result) => result.last_insert_id(),
|
||||
#[cfg(feature = "sqlx-postgres")]
|
||||
ExecResultHolder::SqlxPostgres { last_insert_id, .. } => last_insert_id.to_owned(),
|
||||
ExecResultHolder::SqlxPostgres(_) => {
|
||||
panic!("Should not retrieve last_insert_id this way")
|
||||
}
|
||||
#[cfg(feature = "sqlx-sqlite")]
|
||||
ExecResultHolder::SqlxSqlite(result) => {
|
||||
let last_insert_rowid = result.last_insert_rowid();
|
||||
@ -46,7 +45,7 @@ impl ExecResult {
|
||||
#[cfg(feature = "sqlx-mysql")]
|
||||
ExecResultHolder::SqlxMySql(result) => result.rows_affected(),
|
||||
#[cfg(feature = "sqlx-postgres")]
|
||||
ExecResultHolder::SqlxPostgres { rows_affected, .. } => rows_affected.to_owned(),
|
||||
ExecResultHolder::SqlxPostgres(result) => result.rows_affected(),
|
||||
#[cfg(feature = "sqlx-sqlite")]
|
||||
ExecResultHolder::SqlxSqlite(result) => result.rows_affected(),
|
||||
#[cfg(feature = "mock")]
|
||||
|
@ -1,15 +1,25 @@
|
||||
use crate::{error::*, ActiveModelTrait, DatabaseConnection, Insert, Statement};
|
||||
use crate::{
|
||||
error::*, ActiveModelTrait, DatabaseConnection, EntityTrait, Insert, PrimaryKeyTrait,
|
||||
Statement, TryFromU64,
|
||||
};
|
||||
use sea_query::InsertStatement;
|
||||
use std::future::Future;
|
||||
use std::{future::Future, marker::PhantomData};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Inserter {
|
||||
pub struct Inserter<A>
|
||||
where
|
||||
A: ActiveModelTrait,
|
||||
{
|
||||
query: InsertStatement,
|
||||
model: PhantomData<A>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InsertResult {
|
||||
pub last_insert_id: u64,
|
||||
#[derive(Debug)]
|
||||
pub struct InsertResult<A>
|
||||
where
|
||||
A: ActiveModelTrait,
|
||||
{
|
||||
pub last_insert_id: <<<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType,
|
||||
}
|
||||
|
||||
impl<A> Insert<A>
|
||||
@ -17,54 +27,79 @@ where
|
||||
A: ActiveModelTrait,
|
||||
{
|
||||
#[allow(unused_mut)]
|
||||
pub fn exec(
|
||||
pub fn exec<'a>(
|
||||
self,
|
||||
db: &DatabaseConnection,
|
||||
) -> impl Future<Output = Result<InsertResult, DbErr>> + '_ {
|
||||
db: &'a DatabaseConnection,
|
||||
) -> impl Future<Output = Result<InsertResult<A>, DbErr>> + 'a
|
||||
where
|
||||
A: 'a,
|
||||
{
|
||||
// so that self is dropped before entering await
|
||||
let mut query = self.query;
|
||||
#[cfg(feature = "sqlx-postgres")]
|
||||
if let DatabaseConnection::SqlxPostgresPoolConnection(_) = db {
|
||||
use crate::{EntityTrait, Iterable};
|
||||
use sea_query::{Alias, Expr, Query};
|
||||
for key in <A::Entity as EntityTrait>::PrimaryKey::iter() {
|
||||
use crate::{sea_query::Query, Iterable};
|
||||
if <A::Entity as EntityTrait>::PrimaryKey::iter().count() > 0 {
|
||||
query.returning(
|
||||
Query::select()
|
||||
.expr_as(Expr::col(key), Alias::new("last_insert_id"))
|
||||
.to_owned(),
|
||||
.columns(<A::Entity as EntityTrait>::PrimaryKey::iter())
|
||||
.take(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Inserter::new(query).exec(db)
|
||||
Inserter::<A>::new(query).exec(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl Inserter {
|
||||
impl<A> Inserter<A>
|
||||
where
|
||||
A: ActiveModelTrait,
|
||||
{
|
||||
pub fn new(query: InsertStatement) -> Self {
|
||||
Self { query }
|
||||
Self {
|
||||
query,
|
||||
model: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec(
|
||||
pub fn exec<'a>(
|
||||
self,
|
||||
db: &DatabaseConnection,
|
||||
) -> impl Future<Output = Result<InsertResult, DbErr>> + '_ {
|
||||
db: &'a DatabaseConnection,
|
||||
) -> impl Future<Output = Result<InsertResult<A>, DbErr>> + 'a
|
||||
where
|
||||
A: 'a,
|
||||
{
|
||||
let builder = db.get_database_backend();
|
||||
exec_insert(builder.build(&self.query), db)
|
||||
}
|
||||
}
|
||||
|
||||
// Only Statement impl Send
|
||||
async fn exec_insert(statement: Statement, db: &DatabaseConnection) -> Result<InsertResult, DbErr> {
|
||||
// TODO: Postgres instead use query_one + returning clause
|
||||
let result = match db {
|
||||
async fn exec_insert<A>(
|
||||
statement: Statement,
|
||||
db: &DatabaseConnection,
|
||||
) -> Result<InsertResult<A>, DbErr>
|
||||
where
|
||||
A: ActiveModelTrait,
|
||||
{
|
||||
type PrimaryKey<A> = <<A as ActiveModelTrait>::Entity as EntityTrait>::PrimaryKey;
|
||||
type ValueTypeOf<A> = <PrimaryKey<A> as PrimaryKeyTrait>::ValueType;
|
||||
let last_insert_id = match db {
|
||||
#[cfg(feature = "sqlx-postgres")]
|
||||
DatabaseConnection::SqlxPostgresPoolConnection(conn) => {
|
||||
use crate::{sea_query::Iden, Iterable};
|
||||
let cols = PrimaryKey::<A>::iter()
|
||||
.map(|col| col.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let res = conn.query_one(statement).await?.unwrap();
|
||||
crate::query_result_into_exec_result(res)?
|
||||
res.try_get_many("", cols.as_ref()).unwrap_or_default()
|
||||
}
|
||||
_ => {
|
||||
let last_insert_id = db.execute(statement).await?.last_insert_id();
|
||||
ValueTypeOf::<A>::try_from_u64(last_insert_id)
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
_ => db.execute(statement).await?,
|
||||
};
|
||||
Ok(InsertResult {
|
||||
last_insert_id: result.last_insert_id(),
|
||||
})
|
||||
Ok(InsertResult { last_insert_id })
|
||||
}
|
||||
|
@ -17,6 +17,11 @@ pub(crate) enum QueryResultRow {
|
||||
Mock(crate::MockRow),
|
||||
}
|
||||
|
||||
pub trait TryGetable: Sized {
|
||||
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TryGetError {
|
||||
DbErr(DbErr),
|
||||
Null,
|
||||
@ -31,12 +36,6 @@ impl From<TryGetError> for DbErr {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TryGetable {
|
||||
fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result<Self, TryGetError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
// QueryResult //
|
||||
|
||||
impl QueryResult {
|
||||
@ -46,6 +45,13 @@ impl QueryResult {
|
||||
{
|
||||
Ok(T::try_get(self, pre, col)?)
|
||||
}
|
||||
|
||||
pub fn try_get_many<T>(&self, pre: &str, cols: &[String]) -> Result<T, DbErr>
|
||||
where
|
||||
T: TryGetableMany,
|
||||
{
|
||||
Ok(T::try_get_many(self, pre, cols)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for QueryResultRow {
|
||||
@ -54,9 +60,9 @@ impl fmt::Debug for QueryResultRow {
|
||||
#[cfg(feature = "sqlx-mysql")]
|
||||
Self::SqlxMySql(row) => write!(f, "{:?}", row),
|
||||
#[cfg(feature = "sqlx-postgres")]
|
||||
Self::SqlxPostgres(_) => panic!("QueryResultRow::SqlxPostgres cannot be inspected"),
|
||||
Self::SqlxPostgres(_) => write!(f, "QueryResultRow::SqlxPostgres cannot be inspected"),
|
||||
#[cfg(feature = "sqlx-sqlite")]
|
||||
Self::SqlxSqlite(_) => panic!("QueryResultRow::SqlxSqlite cannot be inspected"),
|
||||
Self::SqlxSqlite(_) => write!(f, "QueryResultRow::SqlxSqlite cannot be inspected"),
|
||||
#[cfg(feature = "mock")]
|
||||
Self::Mock(row) => write!(f, "{:?}", row),
|
||||
}
|
||||
@ -103,6 +109,7 @@ macro_rules! try_getable_all {
|
||||
.and_then(|opt| opt.ok_or(TryGetError::Null))
|
||||
}
|
||||
#[cfg(feature = "mock")]
|
||||
#[allow(unused_variables)]
|
||||
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
|
||||
debug_print!("{:#?}", e.to_string());
|
||||
TryGetError::Null
|
||||
@ -138,6 +145,7 @@ macro_rules! try_getable_unsigned {
|
||||
.and_then(|opt| opt.ok_or(TryGetError::Null))
|
||||
}
|
||||
#[cfg(feature = "mock")]
|
||||
#[allow(unused_variables)]
|
||||
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
|
||||
debug_print!("{:#?}", e.to_string());
|
||||
TryGetError::Null
|
||||
@ -170,6 +178,7 @@ macro_rules! try_getable_mysql {
|
||||
panic!("{} unsupported by sqlx-sqlite", stringify!($type))
|
||||
}
|
||||
#[cfg(feature = "mock")]
|
||||
#[allow(unused_variables)]
|
||||
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
|
||||
debug_print!("{:#?}", e.to_string());
|
||||
TryGetError::Null
|
||||
@ -202,6 +211,7 @@ macro_rules! try_getable_postgres {
|
||||
panic!("{} unsupported by sqlx-sqlite", stringify!($type))
|
||||
}
|
||||
#[cfg(feature = "mock")]
|
||||
#[allow(unused_variables)]
|
||||
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
|
||||
debug_print!("{:#?}", e.to_string());
|
||||
TryGetError::Null
|
||||
@ -264,12 +274,16 @@ impl TryGetable for Decimal {
|
||||
.map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?;
|
||||
use rust_decimal::prelude::FromPrimitive;
|
||||
match val {
|
||||
Some(v) => Decimal::from_f64(v)
|
||||
.ok_or_else(|| TryGetError::DbErr(DbErr::Query("Failed to convert f64 into Decimal".to_owned()))),
|
||||
None => Err(TryGetError::Null)
|
||||
Some(v) => Decimal::from_f64(v).ok_or_else(|| {
|
||||
TryGetError::DbErr(DbErr::Query(
|
||||
"Failed to convert f64 into Decimal".to_owned(),
|
||||
))
|
||||
}),
|
||||
None => Err(TryGetError::Null),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "mock")]
|
||||
#[allow(unused_variables)]
|
||||
QueryResultRow::Mock(row) => row.try_get(column.as_str()).map_err(|e| {
|
||||
debug_print!("{:#?}", e.to_string());
|
||||
TryGetError::Null
|
||||
@ -280,3 +294,135 @@ impl TryGetable for Decimal {
|
||||
|
||||
#[cfg(feature = "with-uuid")]
|
||||
try_getable_all!(uuid::Uuid);
|
||||
|
||||
// TryGetableMany //
|
||||
|
||||
pub trait TryGetableMany: Sized {
|
||||
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError>;
|
||||
}
|
||||
|
||||
impl<T> TryGetableMany for T
|
||||
where
|
||||
T: TryGetable,
|
||||
{
|
||||
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError> {
|
||||
try_get_many_with_slice_len_of(1, cols)?;
|
||||
T::try_get(res, pre, &cols[0])
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryGetableMany for (T, T)
|
||||
where
|
||||
T: TryGetable,
|
||||
{
|
||||
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError> {
|
||||
try_get_many_with_slice_len_of(2, cols)?;
|
||||
Ok((
|
||||
T::try_get(res, pre, &cols[0])?,
|
||||
T::try_get(res, pre, &cols[1])?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TryGetableMany for (T, T, T)
|
||||
where
|
||||
T: TryGetable,
|
||||
{
|
||||
fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result<Self, TryGetError> {
|
||||
try_get_many_with_slice_len_of(3, cols)?;
|
||||
Ok((
|
||||
T::try_get(res, pre, &cols[0])?,
|
||||
T::try_get(res, pre, &cols[1])?,
|
||||
T::try_get(res, pre, &cols[2])?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> {
|
||||
if cols.len() < len {
|
||||
Err(TryGetError::DbErr(DbErr::Query(format!(
|
||||
"Expect {} column names supplied but got slice of length {}",
|
||||
len,
|
||||
cols.len()
|
||||
))))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TryFromU64 //
|
||||
|
||||
pub trait TryFromU64: Sized {
|
||||
fn try_from_u64(n: u64) -> Result<Self, DbErr>;
|
||||
}
|
||||
|
||||
macro_rules! try_from_u64_err {
|
||||
( $type: ty ) => {
|
||||
impl TryFromU64 for $type {
|
||||
fn try_from_u64(_: u64) -> Result<Self, DbErr> {
|
||||
Err(DbErr::Exec(format!(
|
||||
"{} cannot be converted from u64",
|
||||
stringify!($type)
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! try_from_u64_tuple {
|
||||
( $type: ty ) => {
|
||||
try_from_u64_err!(($type, $type));
|
||||
try_from_u64_err!(($type, $type, $type));
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! try_from_u64_numeric {
|
||||
( $type: ty ) => {
|
||||
impl TryFromU64 for $type {
|
||||
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||
use std::convert::TryInto;
|
||||
n.try_into().map_err(|_| {
|
||||
DbErr::Exec(format!(
|
||||
"fail to convert '{}' into '{}'",
|
||||
n,
|
||||
stringify!($type)
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
try_from_u64_tuple!($type);
|
||||
};
|
||||
}
|
||||
|
||||
try_from_u64_numeric!(i8);
|
||||
try_from_u64_numeric!(i16);
|
||||
try_from_u64_numeric!(i32);
|
||||
try_from_u64_numeric!(i64);
|
||||
try_from_u64_numeric!(u8);
|
||||
try_from_u64_numeric!(u16);
|
||||
try_from_u64_numeric!(u32);
|
||||
try_from_u64_numeric!(u64);
|
||||
|
||||
macro_rules! try_from_u64_string {
|
||||
( $type: ty ) => {
|
||||
impl TryFromU64 for $type {
|
||||
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||
Ok(n.to_string())
|
||||
}
|
||||
}
|
||||
try_from_u64_tuple!($type);
|
||||
};
|
||||
}
|
||||
|
||||
try_from_u64_string!(String);
|
||||
|
||||
macro_rules! try_from_u64_dummy {
|
||||
( $type: ty ) => {
|
||||
try_from_u64_err!($type);
|
||||
try_from_u64_err!(($type, $type));
|
||||
try_from_u64_err!(($type, $type, $type));
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-uuid")]
|
||||
try_from_u64_dummy!(uuid::Uuid);
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
error::*, query::combine, DatabaseConnection, EntityTrait, FromQueryResult, Iterable,
|
||||
JsonValue, ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectTwo,
|
||||
error::*, DatabaseConnection, EntityTrait, FromQueryResult, IdenStatic, Iterable, JsonValue,
|
||||
ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo,
|
||||
SelectTwoMany, Statement,
|
||||
};
|
||||
use sea_query::SelectStatement;
|
||||
@ -30,6 +30,7 @@ pub trait SelectorTrait {
|
||||
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SelectModel<M>
|
||||
where
|
||||
M: FromQueryResult,
|
||||
@ -66,8 +67,8 @@ where
|
||||
|
||||
fn from_raw_query_result(res: QueryResult) -> Result<Self::Item, DbErr> {
|
||||
Ok((
|
||||
M::from_query_result(&res, combine::SELECT_A)?,
|
||||
N::from_query_result_optional(&res, combine::SELECT_B)?,
|
||||
M::from_query_result(&res, SelectA.as_str())?,
|
||||
N::from_query_result_optional(&res, SelectB.as_str())?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -128,7 +129,7 @@ where
|
||||
E: EntityTrait,
|
||||
F: EntityTrait,
|
||||
{
|
||||
fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
|
||||
pub fn into_model<M, N>(self) -> Selector<SelectTwoModel<M, N>>
|
||||
where
|
||||
M: FromQueryResult,
|
||||
N: FromQueryResult,
|
||||
@ -289,14 +290,15 @@ where
|
||||
///
|
||||
/// # let _: Result<(), DbErr> = smol::block_on(async {
|
||||
/// #
|
||||
/// let res: Vec<SelectResult> = cake::Entity::find().from_raw_sql(
|
||||
/// Statement::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![]
|
||||
/// )
|
||||
/// )
|
||||
/// .into_model::<SelectResult>()
|
||||
/// .all(&db)
|
||||
/// .await?;
|
||||
/// let res: Vec<SelectResult> = cake::Entity::find()
|
||||
/// .from_raw_sql(Statement::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#,
|
||||
/// vec![],
|
||||
/// ))
|
||||
/// .into_model::<SelectResult>()
|
||||
/// .all(&db)
|
||||
/// .await?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// res,
|
||||
@ -317,11 +319,12 @@ where
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// db.into_transaction_log(),
|
||||
/// vec![
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![]
|
||||
/// ),
|
||||
/// ]);
|
||||
/// vec![Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#,
|
||||
/// vec![]
|
||||
/// ),]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn into_model<M>(self) -> SelectorRaw<SelectModel<M>>
|
||||
where
|
||||
@ -406,22 +409,26 @@ where
|
||||
///
|
||||
/// # let _: Result<(), DbErr> = smol::block_on(async {
|
||||
/// #
|
||||
/// let _: Option<cake::Model> = cake::Entity::find().from_raw_sql(
|
||||
/// Statement::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()]
|
||||
/// )
|
||||
/// ).one(&db).await?;
|
||||
/// let _: Option<cake::Model> = cake::Entity::find()
|
||||
/// .from_raw_sql(Statement::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#,
|
||||
/// vec![1.into()],
|
||||
/// ))
|
||||
/// .one(&db)
|
||||
/// .await?;
|
||||
/// #
|
||||
/// # Ok(())
|
||||
/// # });
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// db.into_transaction_log(),
|
||||
/// vec![
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()]
|
||||
/// ),
|
||||
/// ]);
|
||||
/// vec![Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#,
|
||||
/// vec![1.into()]
|
||||
/// ),]
|
||||
/// );
|
||||
/// ```
|
||||
pub async fn one(self, db: &DatabaseConnection) -> Result<Option<S::Item>, DbErr> {
|
||||
let row = db.query_one(self.stmt).await?;
|
||||
@ -441,22 +448,26 @@ where
|
||||
///
|
||||
/// # let _: Result<(), DbErr> = smol::block_on(async {
|
||||
/// #
|
||||
/// let _: Vec<cake::Model> = cake::Entity::find().from_raw_sql(
|
||||
/// Statement::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
|
||||
/// )
|
||||
/// ).all(&db).await?;
|
||||
/// let _: Vec<cake::Model> = cake::Entity::find()
|
||||
/// .from_raw_sql(Statement::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#,
|
||||
/// vec![],
|
||||
/// ))
|
||||
/// .all(&db)
|
||||
/// .await?;
|
||||
/// #
|
||||
/// # Ok(())
|
||||
/// # });
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// db.into_transaction_log(),
|
||||
/// vec![
|
||||
/// Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![]
|
||||
/// ),
|
||||
/// ]);
|
||||
/// vec![Transaction::from_sql_and_values(
|
||||
/// DbBackend::Postgres,
|
||||
/// r#"SELECT "cake"."id", "cake"."name" FROM "cake""#,
|
||||
/// vec![]
|
||||
/// ),]
|
||||
/// );
|
||||
/// ```
|
||||
pub async fn all(self, db: &DatabaseConnection) -> Result<Vec<S::Item>, DbErr> {
|
||||
let rows = db.query_all(self.stmt).await?;
|
||||
|
30
src/lib.rs
30
src/lib.rs
@ -1,3 +1,10 @@
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
clippy::print_stderr,
|
||||
clippy::print_stdout
|
||||
)]
|
||||
|
||||
//! <div align="center">
|
||||
//!
|
||||
//! <img src="https://www.sea-ql.org/SeaORM/img/SeaORM banner.png"/>
|
||||
@ -20,13 +27,9 @@
|
||||
//!
|
||||
//! SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust.
|
||||
//!
|
||||
//! ```markdown
|
||||
//! This is an early release of SeaORM, the API is not stable yet.
|
||||
//! ```
|
||||
//!
|
||||
//! [](https://www.sea-ql.org/SeaORM/docs/index)
|
||||
//! [](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx)
|
||||
//! [](https://github.com/SeaQL/sea-orm/issues/37)
|
||||
//! [](https://github.com/SeaQL/sea-orm/tree/master/examples)
|
||||
//! [](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example)
|
||||
//! [](https://discord.com/invite/uCPdDXzbdv)
|
||||
//!
|
||||
//! ## Features
|
||||
@ -70,10 +73,8 @@
|
||||
//! 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?;
|
||||
//! let cake_with_fruits: Vec<(cake::Model, Vec<fruit::Model>)> =
|
||||
//! Cake::find().find_with_related(Fruit).all(db).await?;
|
||||
//!
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
@ -93,7 +94,7 @@
|
||||
//! };
|
||||
//!
|
||||
//! // insert one
|
||||
//! let res: InsertResult = Fruit::insert(pear).exec(db).await?;
|
||||
//! let res = Fruit::insert(pear).exec(db).await?;
|
||||
//!
|
||||
//! println!("InsertResult: {}", res.last_insert_id);
|
||||
//! # Ok(())
|
||||
@ -180,6 +181,13 @@
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Learn More
|
||||
//!
|
||||
//! 1. [Design](https://github.com/SeaQL/sea-orm/tree/master/DESIGN.md)
|
||||
//! 1. [Architecture](https://github.com/SeaQL/sea-orm/tree/master/ARCHITECTURE.md)
|
||||
//! 1. [Compare with Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel)
|
||||
//!
|
||||
//! ## License
|
||||
//!
|
||||
//! Licensed under either of
|
||||
|
@ -1,10 +1,31 @@
|
||||
use crate::{EntityTrait, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany};
|
||||
use crate::{
|
||||
EntityTrait, IdenStatic, IntoSimpleExpr, Iterable, QueryTrait, Select, SelectTwo, SelectTwoMany,
|
||||
};
|
||||
use core::marker::PhantomData;
|
||||
pub use sea_query::JoinType;
|
||||
use sea_query::{Alias, ColumnRef, Iden, Order, SeaRc, SelectExpr, SelectStatement, SimpleExpr};
|
||||
|
||||
pub const SELECT_A: &str = "A_";
|
||||
pub const SELECT_B: &str = "B_";
|
||||
macro_rules! select_def {
|
||||
( $ident: ident, $str: expr ) => {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct $ident;
|
||||
|
||||
impl Iden for $ident {
|
||||
fn unquoted(&self, s: &mut dyn std::fmt::Write) {
|
||||
write!(s, "{}", self.as_str()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl IdenStatic for $ident {
|
||||
fn as_str(&self) -> &str {
|
||||
$str
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
select_def!(SelectA, "A_");
|
||||
select_def!(SelectB, "B_");
|
||||
|
||||
impl<E> Select<E>
|
||||
where
|
||||
@ -37,7 +58,7 @@ where
|
||||
where
|
||||
F: EntityTrait,
|
||||
{
|
||||
self = self.apply_alias(SELECT_A);
|
||||
self = self.apply_alias(SelectA.as_str());
|
||||
SelectTwo::new(self.into_query())
|
||||
}
|
||||
|
||||
@ -45,7 +66,7 @@ where
|
||||
where
|
||||
F: EntityTrait,
|
||||
{
|
||||
self = self.apply_alias(SELECT_A);
|
||||
self = self.apply_alias(SelectA.as_str());
|
||||
SelectTwoMany::new(self.into_query())
|
||||
}
|
||||
}
|
||||
@ -102,7 +123,7 @@ where
|
||||
S: QueryTrait<QueryStatement = SelectStatement>,
|
||||
{
|
||||
for col in <F::Column as Iterable>::iter() {
|
||||
let alias = format!("{}{}", SELECT_B, col.to_string().as_str());
|
||||
let alias = format!("{}{}", SelectB.as_str(), col.as_str());
|
||||
selector.query().expr(SelectExpr {
|
||||
expr: col.into_simple_expr(),
|
||||
alias: Some(SeaRc::new(Alias::new(&alias))),
|
||||
|
@ -1,11 +1,9 @@
|
||||
use crate::{
|
||||
ColumnTrait, EntityTrait, Identity, IntoSimpleExpr, Iterable, ModelTrait, PrimaryKeyToColumn,
|
||||
RelationDef,
|
||||
};
|
||||
use sea_query::{
|
||||
Alias, Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef,
|
||||
ColumnTrait, EntityTrait, Identity, IntoIdentity, IntoSimpleExpr, Iterable, ModelTrait,
|
||||
PrimaryKeyToColumn, RelationDef,
|
||||
};
|
||||
pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement};
|
||||
use sea_query::{Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef};
|
||||
|
||||
// LINT: when the column does not appear in tables selected from
|
||||
// LINT: when there is a group by clause, but some columns don't have aggregate functions
|
||||
@ -55,13 +53,14 @@ pub trait QuerySelect: Sized {
|
||||
/// r#"SELECT COUNT("cake"."id") AS "count" FROM "cake""#
|
||||
/// );
|
||||
/// ```
|
||||
fn column_as<C>(mut self, col: C, alias: &str) -> Self
|
||||
fn column_as<C, I>(mut self, col: C, alias: I) -> Self
|
||||
where
|
||||
C: IntoSimpleExpr,
|
||||
I: IntoIdentity,
|
||||
{
|
||||
self.query().expr(SelectExpr {
|
||||
expr: col.into_simple_expr(),
|
||||
alias: Some(SeaRc::new(Alias::new(alias))),
|
||||
alias: Some(SeaRc::new(alias.into_identity())),
|
||||
});
|
||||
self
|
||||
}
|
||||
@ -295,7 +294,7 @@ fn join_condition(rel: RelationDef) -> SimpleExpr {
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_table_ref(table_ref: &TableRef) -> DynIden {
|
||||
pub(crate) fn unpack_table_ref(table_ref: &TableRef) -> DynIden {
|
||||
match table_ref {
|
||||
TableRef::Table(tbl) => SeaRc::clone(tbl),
|
||||
TableRef::SchemaTable(_, tbl) => SeaRc::clone(tbl),
|
||||
|
@ -43,11 +43,11 @@ where
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Insert::one(cake::Model {
|
||||
/// id: 1,
|
||||
/// name: "Apple Pie".to_owned(),
|
||||
/// })
|
||||
/// .build(DbBackend::Postgres)
|
||||
/// .to_string(),
|
||||
/// id: 1,
|
||||
/// name: "Apple Pie".to_owned(),
|
||||
/// })
|
||||
/// .build(DbBackend::Postgres)
|
||||
/// .to_string(),
|
||||
/// r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"#,
|
||||
/// );
|
||||
/// ```
|
||||
@ -57,11 +57,11 @@ where
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Insert::one(cake::ActiveModel {
|
||||
/// id: Unset(None),
|
||||
/// name: Set("Apple Pie".to_owned()),
|
||||
/// })
|
||||
/// .build(DbBackend::Postgres)
|
||||
/// .to_string(),
|
||||
/// id: Unset(None),
|
||||
/// name: Set("Apple Pie".to_owned()),
|
||||
/// })
|
||||
/// .build(DbBackend::Postgres)
|
||||
/// .to_string(),
|
||||
/// r#"INSERT INTO "cake" ("name") VALUES ('Apple Pie')"#,
|
||||
/// );
|
||||
/// ```
|
||||
@ -79,17 +79,17 @@ where
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Insert::many(vec![
|
||||
/// cake::Model {
|
||||
/// id: 1,
|
||||
/// name: "Apple Pie".to_owned(),
|
||||
/// },
|
||||
/// cake::Model {
|
||||
/// id: 2,
|
||||
/// name: "Orange Scone".to_owned(),
|
||||
/// }
|
||||
/// ])
|
||||
/// .build(DbBackend::Postgres)
|
||||
/// .to_string(),
|
||||
/// cake::Model {
|
||||
/// id: 1,
|
||||
/// name: "Apple Pie".to_owned(),
|
||||
/// },
|
||||
/// cake::Model {
|
||||
/// id: 2,
|
||||
/// name: "Orange Scone".to_owned(),
|
||||
/// }
|
||||
/// ])
|
||||
/// .build(DbBackend::Postgres)
|
||||
/// .to_string(),
|
||||
/// r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie'), (2, 'Orange Scone')"#,
|
||||
/// );
|
||||
/// ```
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::{EntityTrait, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
|
||||
use crate::{EntityTrait, Linked, QuerySelect, Related, Select, SelectTwo, SelectTwoMany};
|
||||
pub use sea_query::JoinType;
|
||||
|
||||
impl<E> Select<E>
|
||||
@ -57,6 +57,19 @@ where
|
||||
{
|
||||
self.left_join(r).select_with(r)
|
||||
}
|
||||
|
||||
/// Left Join with a Linked Entity and select both Entity.
|
||||
pub fn find_also_linked<L, T>(self, l: L) -> SelectTwo<E, T>
|
||||
where
|
||||
L: Linked<FromEntity = E, ToEntity = T>,
|
||||
T: EntityTrait,
|
||||
{
|
||||
let mut slf = self;
|
||||
for rel in l.link() {
|
||||
slf = slf.join(JoinType::LeftJoin, rel);
|
||||
}
|
||||
slf.select_also(T::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -220,4 +233,44 @@ mod tests {
|
||||
.join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_10() {
|
||||
let cake_model = cake::Model {
|
||||
id: 12,
|
||||
name: "".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
cake_model
|
||||
.find_linked(cake::CakeToFilling)
|
||||
.build(DbBackend::MySql)
|
||||
.to_string(),
|
||||
[
|
||||
r#"SELECT `filling`.`id`, `filling`.`name`"#,
|
||||
r#"FROM `filling`"#,
|
||||
r#"INNER JOIN `cake_filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#,
|
||||
r#"INNER JOIN `cake` ON `cake`.`id` = `cake_filling`.`cake_id`"#,
|
||||
]
|
||||
.join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_11() {
|
||||
assert_eq!(
|
||||
cake::Entity::find()
|
||||
.find_also_linked(cake::CakeToFilling)
|
||||
.build(DbBackend::MySql)
|
||||
.to_string(),
|
||||
[
|
||||
r#"SELECT `cake`.`id` AS `A_id`, `cake`.`name` AS `A_name`,"#,
|
||||
r#"`filling`.`id` AS `B_id`, `filling`.`name` AS `B_name`"#,
|
||||
r#"FROM `cake`"#,
|
||||
r#"LEFT JOIN `cake_filling` ON `cake`.`id` = `cake_filling`.`cake_id`"#,
|
||||
r#"LEFT JOIN `filling` ON `cake_filling`.`filling_id` = `filling`.`id`"#,
|
||||
]
|
||||
.join(" ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ mod select;
|
||||
mod traits;
|
||||
mod update;
|
||||
|
||||
// pub use combine::*;
|
||||
pub use combine::{SelectA, SelectB};
|
||||
pub use delete::*;
|
||||
pub use helper::*;
|
||||
pub use insert::*;
|
||||
|
@ -59,7 +59,7 @@ impl Update {
|
||||
/// Update many ActiveModel
|
||||
///
|
||||
/// ```
|
||||
/// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::Expr, DbBackend};
|
||||
/// use sea_orm::{entity::*, query::*, sea_query::Expr, tests_cfg::fruit, DbBackend};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// Update::many(fruit::Entity)
|
||||
|
@ -28,6 +28,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -73,4 +75,20 @@ impl Related<super::filling::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CakeToFilling;
|
||||
|
||||
impl Linked for CakeToFilling {
|
||||
type FromEntity = Entity;
|
||||
|
||||
type ToEntity = super::filling::Entity;
|
||||
|
||||
fn link(&self) -> Vec<RelationDef> {
|
||||
vec![
|
||||
super::cake_filling::Relation::Cake.def().rev(),
|
||||
super::cake_filling::Relation::Filling.def(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -29,6 +29,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = (i32, i32);
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = (i32, i32);
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -41,6 +41,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
#[macro_export]
|
||||
#[cfg(feature = "debug-print")]
|
||||
macro_rules! debug_print {
|
||||
($( $args:expr ),*) => { println!( $( $args ),* ); }
|
||||
($( $args:expr ),*) => { log::debug!( $( $args ),* ); }
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
|
@ -1,7 +1,6 @@
|
||||
pub mod common;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn};
|
||||
pub use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn};
|
||||
|
||||
// DATABASE_URL="sqlite::memory:" cargo test --features sqlx-sqlit,runtime-async-std --test basic
|
||||
#[sea_orm_macros::test]
|
||||
|
@ -31,6 +31,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -49,7 +51,7 @@ impl ColumnTrait for Column {
|
||||
Self::Id => ColumnType::Integer.def(),
|
||||
Self::Name => ColumnType::String(None).def(),
|
||||
Self::ContactDetails => ColumnType::Json.def(),
|
||||
Self::BakeryId => ColumnType::Integer.def(),
|
||||
Self::BakeryId => ColumnType::Integer.def().null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +62,8 @@ impl RelationTrait for Relation {
|
||||
Self::Bakery => Entity::belongs_to(super::bakery::Entity)
|
||||
.from(Column::BakeryId)
|
||||
.to(super::bakery::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
@ -81,4 +85,22 @@ impl Related<super::cake::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BakedForCustomer;
|
||||
|
||||
impl Linked for BakedForCustomer {
|
||||
type FromEntity = Entity;
|
||||
|
||||
type ToEntity = super::customer::Entity;
|
||||
|
||||
fn link(&self) -> Vec<RelationDef> {
|
||||
vec![
|
||||
super::cakes_bakers::Relation::Baker.def().rev(),
|
||||
super::cakes_bakers::Relation::Cake.def(),
|
||||
super::lineitem::Relation::Cake.def().rev(),
|
||||
super::lineitem::Relation::Order.def(),
|
||||
super::order::Relation::Customer.def(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
@ -29,6 +29,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -48,7 +50,7 @@ impl ColumnTrait for Column {
|
||||
match self {
|
||||
Self::Id => ColumnType::Integer.def(),
|
||||
Self::Name => ColumnType::String(None).def(),
|
||||
Self::ProfitMargin => ColumnType::Float.def(),
|
||||
Self::ProfitMargin => ColumnType::Double.def(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -54,9 +56,9 @@ impl ColumnTrait for Column {
|
||||
Self::Id => ColumnType::Integer.def(),
|
||||
Self::Name => ColumnType::String(None).def(),
|
||||
Self::Price => ColumnType::Decimal(Some((19, 4))).def(),
|
||||
Self::BakeryId => ColumnType::Integer.def(),
|
||||
Self::BakeryId => ColumnType::Integer.def().null(),
|
||||
Self::GlutenFree => ColumnType::Boolean.def(),
|
||||
Self::Serial => ColumnType::String(None).def(),
|
||||
Self::Serial => ColumnType::Uuid.def(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -67,6 +69,8 @@ impl RelationTrait for Relation {
|
||||
Self::Bakery => Entity::belongs_to(super::bakery::Entity)
|
||||
.from(Column::BakeryId)
|
||||
.to(super::bakery::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
Self::Lineitem => Entity::has_many(super::lineitem::Entity).into(),
|
||||
}
|
||||
|
@ -28,6 +28,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = (i32, i32);
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
false
|
||||
}
|
||||
@ -56,10 +58,14 @@ impl RelationTrait for Relation {
|
||||
Self::Cake => Entity::belongs_to(super::cake::Entity)
|
||||
.from(Column::CakeId)
|
||||
.to(super::cake::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
Self::Baker => Entity::belongs_to(super::baker::Entity)
|
||||
.from(Column::BakerId)
|
||||
.to(super::baker::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -46,7 +48,7 @@ impl ColumnTrait for Column {
|
||||
match self {
|
||||
Self::Id => ColumnType::Integer.def(),
|
||||
Self::Name => ColumnType::String(None).def(),
|
||||
Self::Notes => ColumnType::Text.def(),
|
||||
Self::Notes => ColumnType::Text.def().null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -64,10 +66,14 @@ impl RelationTrait for Relation {
|
||||
Self::Order => Entity::belongs_to(super::order::Entity)
|
||||
.from(Column::OrderId)
|
||||
.to(super::order::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
Self::Cake => Entity::belongs_to(super::cake::Entity)
|
||||
.from(Column::CakeId)
|
||||
.to(super::cake::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
|
61
tests/common/bakery_chain/metadata.rs
Normal file
61
tests/common/bakery_chain/metadata.rs
Normal file
@ -0,0 +1,61 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
|
||||
pub struct Entity;
|
||||
|
||||
impl EntityName for Entity {
|
||||
fn table_name(&self) -> &str {
|
||||
"metadata"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
|
||||
pub struct Model {
|
||||
pub uuid: Uuid,
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
pub enum Column {
|
||||
Uuid,
|
||||
Key,
|
||||
Value,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
|
||||
pub enum PrimaryKey {
|
||||
Uuid,
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = Uuid;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ColumnTrait for Column {
|
||||
type EntityName = Entity;
|
||||
|
||||
fn def(&self) -> ColumnDef {
|
||||
match self {
|
||||
Self::Uuid => ColumnType::Uuid.def(),
|
||||
Self::Key => ColumnType::String(None).def(),
|
||||
Self::Value => ColumnType::String(None).def(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RelationTrait for Relation {
|
||||
fn def(&self) -> RelationDef {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
@ -4,6 +4,7 @@ pub mod cake;
|
||||
pub mod cakes_bakers;
|
||||
pub mod customer;
|
||||
pub mod lineitem;
|
||||
pub mod metadata;
|
||||
pub mod order;
|
||||
|
||||
pub use super::baker::Entity as Baker;
|
||||
@ -12,4 +13,5 @@ pub use super::cake::Entity as Cake;
|
||||
pub use super::cakes_bakers::Entity as CakesBakers;
|
||||
pub use super::customer::Entity as Customer;
|
||||
pub use super::lineitem::Entity as Lineitem;
|
||||
pub use super::metadata::Entity as Metadata;
|
||||
pub use super::order::Entity as Order;
|
||||
|
@ -33,6 +33,8 @@ pub enum PrimaryKey {
|
||||
}
|
||||
|
||||
impl PrimaryKeyTrait for PrimaryKey {
|
||||
type ValueType = i32;
|
||||
|
||||
fn auto_increment() -> bool {
|
||||
true
|
||||
}
|
||||
@ -65,10 +67,14 @@ impl RelationTrait for Relation {
|
||||
Self::Bakery => Entity::belongs_to(super::bakery::Entity)
|
||||
.from(Column::BakeryId)
|
||||
.to(super::bakery::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
Self::Customer => Entity::belongs_to(super::customer::Entity)
|
||||
.from(Column::CustomerId)
|
||||
.to(super::customer::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
.on_update(ForeignKeyAction::Cascade)
|
||||
.into(),
|
||||
Self::Lineitem => Entity::has_many(super::lineitem::Entity).into(),
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ impl TestContext {
|
||||
let db: DatabaseConnection = setup::setup(&base_url, test_name).await;
|
||||
|
||||
Self {
|
||||
base_url: base_url,
|
||||
base_url,
|
||||
db_name: test_name.to_string(),
|
||||
db,
|
||||
}
|
||||
|
@ -45,13 +45,14 @@ pub async fn setup(base_url: &str, db_name: &str) -> DatabaseConnection {
|
||||
Database::connect(base_url).await.unwrap()
|
||||
};
|
||||
|
||||
assert!(schema::create_bakery_table(&db).await.is_ok());
|
||||
assert!(schema::create_baker_table(&db).await.is_ok());
|
||||
assert!(schema::create_customer_table(&db).await.is_ok());
|
||||
assert!(schema::create_order_table(&db).await.is_ok());
|
||||
assert!(schema::create_cake_table(&db).await.is_ok());
|
||||
assert!(schema::create_cakes_bakers_table(&db).await.is_ok());
|
||||
assert!(schema::create_lineitem_table(&db).await.is_ok());
|
||||
schema::create_bakery_table(&db).await.unwrap();
|
||||
schema::create_baker_table(&db).await.unwrap();
|
||||
schema::create_customer_table(&db).await.unwrap();
|
||||
schema::create_order_table(&db).await.unwrap();
|
||||
schema::create_cake_table(&db).await.unwrap();
|
||||
schema::create_cakes_bakers_table(&db).await.unwrap();
|
||||
schema::create_lineitem_table(&db).await.unwrap();
|
||||
schema::create_metadata_table(&db).await.unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,29 @@
|
||||
use sea_orm::{error::*, sea_query, DbConn, ExecResult};
|
||||
use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, TableCreateStatement};
|
||||
|
||||
pub use super::super::bakery_chain::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use sea_orm::{
|
||||
entity_to_table_create_statement, error::*, sea_query, DbConn, EntityTrait, ExecResult,
|
||||
};
|
||||
use sea_query::{ColumnDef, ForeignKey, ForeignKeyAction, Index, Table, TableCreateStatement};
|
||||
|
||||
async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
|
||||
async fn create_table<E>(
|
||||
db: &DbConn,
|
||||
stmt: &TableCreateStatement,
|
||||
entity: E,
|
||||
) -> Result<ExecResult, DbErr>
|
||||
where
|
||||
E: EntityTrait,
|
||||
{
|
||||
let builder = db.get_database_backend();
|
||||
db.execute(builder.build(stmt)).await
|
||||
let stmt = builder.build(stmt);
|
||||
assert_eq!(
|
||||
builder.build(&entity_to_table_create_statement(entity)),
|
||||
stmt
|
||||
);
|
||||
db.execute(stmt).await
|
||||
}
|
||||
|
||||
pub async fn create_bakery_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(bakery::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
@ -27,16 +41,17 @@ pub async fn create_bakery_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, Bakery).await
|
||||
}
|
||||
|
||||
pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(baker::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(baker::Column::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
@ -49,7 +64,7 @@ pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
.col(ColumnDef::new(baker::Column::BakeryId).integer())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_baker_bakery")
|
||||
.name("fk-baker-bakery")
|
||||
.from(baker::Entity, baker::Column::BakeryId)
|
||||
.to(bakery::Entity, bakery::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -57,11 +72,11 @@ pub async fn create_baker_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, Baker).await
|
||||
}
|
||||
|
||||
pub async fn create_customer_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(customer::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
@ -75,11 +90,11 @@ pub async fn create_customer_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
.col(ColumnDef::new(customer::Column::Notes).text())
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, Customer).await
|
||||
}
|
||||
|
||||
pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(order::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
@ -107,7 +122,7 @@ pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_order_bakery")
|
||||
.name("fk-order-bakery")
|
||||
.from(order::Entity, order::Column::BakeryId)
|
||||
.to(bakery::Entity, bakery::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -115,7 +130,7 @@ pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_order_customer")
|
||||
.name("fk-order-customer")
|
||||
.from(order::Entity, order::Column::CustomerId)
|
||||
.to(customer::Entity, customer::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -123,11 +138,11 @@ pub async fn create_order_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, Order).await
|
||||
}
|
||||
|
||||
pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(lineitem::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
@ -159,7 +174,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_lineitem_order")
|
||||
.name("fk-lineitem-order")
|
||||
.from(lineitem::Entity, lineitem::Column::OrderId)
|
||||
.to(order::Entity, order::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -167,7 +182,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_lineitem_cake")
|
||||
.name("fk-lineitem-cake")
|
||||
.from(lineitem::Entity, lineitem::Column::CakeId)
|
||||
.to(cake::Entity, cake::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -175,11 +190,11 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, Lineitem).await
|
||||
}
|
||||
|
||||
pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(cakes_bakers::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
@ -194,12 +209,13 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr>
|
||||
)
|
||||
.primary_key(
|
||||
Index::create()
|
||||
.name("pk-cakes_bakers")
|
||||
.col(cakes_bakers::Column::CakeId)
|
||||
.col(cakes_bakers::Column::BakerId),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_cakes_bakers_cake")
|
||||
.name("fk-cakes_bakers-cake")
|
||||
.from(cakes_bakers::Entity, cakes_bakers::Column::CakeId)
|
||||
.to(cake::Entity, cake::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -207,7 +223,7 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr>
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_cakes_bakers_baker")
|
||||
.name("fk-cakes_bakers-baker")
|
||||
.from(cakes_bakers::Entity, cakes_bakers::Column::BakerId)
|
||||
.to(baker::Entity, baker::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -215,11 +231,11 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result<ExecResult, DbErr>
|
||||
)
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, CakesBakers).await
|
||||
}
|
||||
|
||||
pub async fn create_cake_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
let stmt = Table::create()
|
||||
.table(cake::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
@ -238,7 +254,7 @@ pub async fn create_cake_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
.col(ColumnDef::new(cake::Column::BakeryId).integer())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_cake_bakery")
|
||||
.name("fk-cake-bakery")
|
||||
.from(cake::Entity, cake::Column::BakeryId)
|
||||
.to(bakery::Entity, bakery::Column::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade)
|
||||
@ -252,5 +268,22 @@ pub async fn create_cake_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
.col(ColumnDef::new(cake::Column::Serial).uuid().not_null())
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt).await
|
||||
create_table(db, &stmt, Cake).await
|
||||
}
|
||||
|
||||
pub async fn create_metadata_table(db: &DbConn) -> Result<ExecResult, DbErr> {
|
||||
let stmt = sea_query::Table::create()
|
||||
.table(metadata::Entity)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(metadata::Column::Uuid)
|
||||
.uuid()
|
||||
.not_null()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(metadata::Column::Key).string().not_null())
|
||||
.col(ColumnDef::new(metadata::Column::Value).string().not_null())
|
||||
.to_owned();
|
||||
|
||||
create_table(db, &stmt, Metadata).await
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ pub async fn test_create_baker(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -30,7 +30,7 @@ pub async fn test_create_baker(db: &DbConn) {
|
||||
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let res: InsertResult = Baker::insert(baker_bob)
|
||||
let res = Baker::insert(baker_bob)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert baker");
|
||||
|
@ -8,7 +8,7 @@ pub async fn test_create_cake(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -23,7 +23,7 @@ pub async fn test_create_cake(db: &DbConn) {
|
||||
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let baker_insert_res: InsertResult = Baker::insert(baker_bob)
|
||||
let baker_insert_res = Baker::insert(baker_bob)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert baker");
|
||||
@ -38,7 +38,7 @@ pub async fn test_create_cake(db: &DbConn) {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
|
||||
let cake_insert_res = Cake::insert(mud_cake)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake");
|
||||
@ -51,18 +51,25 @@ pub async fn test_create_cake(db: &DbConn) {
|
||||
let cake_baker = cakes_bakers::ActiveModel {
|
||||
cake_id: Set(cake_insert_res.last_insert_id as i32),
|
||||
baker_id: Set(baker_insert_res.last_insert_id as i32),
|
||||
..Default::default()
|
||||
};
|
||||
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
|
||||
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake_baker");
|
||||
assert_eq!(
|
||||
cake_baker_res.last_insert_id,
|
||||
if cfg!(feature = "sqlx-postgres") {
|
||||
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
assert!(cake.is_some());
|
||||
let cake_model = cake.unwrap();
|
||||
assert_eq!(cake_model.name, "Mud Cake");
|
||||
assert_eq!(cake_model.price, dec!(10.25));
|
||||
assert_eq!(cake_model.gluten_free, false);
|
||||
assert!(!cake_model.gluten_free);
|
||||
assert_eq!(
|
||||
cake_model
|
||||
.find_related(Bakery)
|
||||
|
@ -10,7 +10,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -26,7 +26,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let baker_insert_res: InsertResult = Baker::insert(baker_bob)
|
||||
let baker_insert_res = Baker::insert(baker_bob)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert baker");
|
||||
@ -41,7 +41,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
|
||||
let cake_insert_res = Cake::insert(mud_cake)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake");
|
||||
@ -50,12 +50,19 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
let cake_baker = cakes_bakers::ActiveModel {
|
||||
cake_id: Set(cake_insert_res.last_insert_id as i32),
|
||||
baker_id: Set(baker_insert_res.last_insert_id as i32),
|
||||
..Default::default()
|
||||
};
|
||||
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
|
||||
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake_baker");
|
||||
assert_eq!(
|
||||
cake_baker_res.last_insert_id,
|
||||
if cfg!(feature = "sqlx-postgres") {
|
||||
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Customer
|
||||
let customer_kate = customer::ActiveModel {
|
||||
@ -63,7 +70,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
notes: Set(Some("Loves cheese cake".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let customer_insert_res: InsertResult = Customer::insert(customer_kate)
|
||||
let customer_insert_res = Customer::insert(customer_kate)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert customer");
|
||||
@ -76,7 +83,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
placed_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
};
|
||||
let order_insert_res: InsertResult = Order::insert(order_1)
|
||||
let order_insert_res = Order::insert(order_1)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert order");
|
||||
@ -89,7 +96,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
quantity: Set(1),
|
||||
..Default::default()
|
||||
};
|
||||
let lineitem_insert_res: InsertResult = Lineitem::insert(lineitem_1)
|
||||
let lineitem_insert_res = Lineitem::insert(lineitem_1)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert lineitem");
|
||||
@ -105,7 +112,7 @@ pub async fn test_create_lineitem(db: &DbConn) {
|
||||
|
||||
assert_eq!(lineitem_model.price, dec!(7.55));
|
||||
|
||||
let cake: Option<cake::Model> = Cake::find_by_id(lineitem_model.cake_id as u64)
|
||||
let cake: Option<cake::Model> = Cake::find_by_id(lineitem_model.cake_id)
|
||||
.one(db)
|
||||
.await
|
||||
.expect("could not find cake");
|
||||
|
@ -10,7 +10,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -26,7 +26,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
bakery_id: Set(Some(bakery_insert_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let baker_insert_res: InsertResult = Baker::insert(baker_bob)
|
||||
let baker_insert_res = Baker::insert(baker_bob)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert baker");
|
||||
@ -41,7 +41,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
|
||||
let cake_insert_res = Cake::insert(mud_cake)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake");
|
||||
@ -50,12 +50,19 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
let cake_baker = cakes_bakers::ActiveModel {
|
||||
cake_id: Set(cake_insert_res.last_insert_id as i32),
|
||||
baker_id: Set(baker_insert_res.last_insert_id as i32),
|
||||
..Default::default()
|
||||
};
|
||||
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
|
||||
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake_baker");
|
||||
assert_eq!(
|
||||
cake_baker_res.last_insert_id,
|
||||
if cfg!(feature = "sqlx-postgres") {
|
||||
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
// Customer
|
||||
let customer_kate = customer::ActiveModel {
|
||||
@ -63,7 +70,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
notes: Set(Some("Loves cheese cake".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let customer_insert_res: InsertResult = Customer::insert(customer_kate)
|
||||
let customer_insert_res = Customer::insert(customer_kate)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert customer");
|
||||
@ -76,7 +83,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
placed_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
};
|
||||
let order_insert_res: InsertResult = Order::insert(order_1)
|
||||
let order_insert_res = Order::insert(order_1)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert order");
|
||||
@ -89,7 +96,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
quantity: Set(2),
|
||||
..Default::default()
|
||||
};
|
||||
let _lineitem_insert_res: InsertResult = Lineitem::insert(lineitem_1)
|
||||
let _lineitem_insert_res = Lineitem::insert(lineitem_1)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert lineitem");
|
||||
@ -103,7 +110,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
let order_model = order.unwrap();
|
||||
assert_eq!(order_model.total, dec!(15.10));
|
||||
|
||||
let customer: Option<customer::Model> = Customer::find_by_id(order_model.customer_id as u64)
|
||||
let customer: Option<customer::Model> = Customer::find_by_id(order_model.customer_id)
|
||||
.one(db)
|
||||
.await
|
||||
.expect("could not find customer");
|
||||
@ -111,7 +118,7 @@ pub async fn test_create_order(db: &DbConn) {
|
||||
let customer_model = customer.unwrap();
|
||||
assert_eq!(customer_model.name, "Kate");
|
||||
|
||||
let bakery: Option<bakery::Model> = Bakery::find_by_id(order_model.bakery_id as i64)
|
||||
let bakery: Option<bakery::Model> = Bakery::find_by_id(order_model.bakery_id)
|
||||
.one(db)
|
||||
.await
|
||||
.expect("could not find bakery");
|
||||
|
@ -10,7 +10,7 @@ pub async fn test_delete_cake(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
|
@ -1,7 +1,3 @@
|
||||
use sea_orm::{entity::*, DbConn, InsertResult};
|
||||
|
||||
pub use super::common::bakery_chain::*;
|
||||
|
||||
pub mod create_baker;
|
||||
pub mod create_cake;
|
||||
pub mod create_lineitem;
|
||||
@ -9,13 +5,23 @@ pub mod create_order;
|
||||
pub mod deletes;
|
||||
pub mod updates;
|
||||
|
||||
pub use create_baker::*;
|
||||
pub use create_cake::*;
|
||||
pub use create_lineitem::*;
|
||||
pub use create_order::*;
|
||||
pub use deletes::*;
|
||||
pub use updates::*;
|
||||
|
||||
pub use super::common::bakery_chain::*;
|
||||
use sea_orm::{entity::*, DbConn};
|
||||
|
||||
pub async fn test_create_bakery(db: &DbConn) {
|
||||
let seaside_bakery = bakery::ActiveModel {
|
||||
name: Set("SeaSide Bakery".to_owned()),
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -28,7 +34,7 @@ pub async fn test_create_bakery(db: &DbConn) {
|
||||
assert!(bakery.is_some());
|
||||
let bakery_model = bakery.unwrap();
|
||||
assert_eq!(bakery_model.name, "SeaSide Bakery");
|
||||
assert_eq!(bakery_model.profit_margin, 10.4);
|
||||
assert!((bakery_model.profit_margin - 10.4).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
pub async fn test_create_customer(db: &DbConn) {
|
||||
@ -37,7 +43,7 @@ pub async fn test_create_customer(db: &DbConn) {
|
||||
notes: Set(Some("Loves cheese cake".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let res: InsertResult = Customer::insert(customer_kate)
|
||||
let res = Customer::insert(customer_kate)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert customer");
|
||||
|
@ -8,7 +8,7 @@ pub async fn test_update_cake(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -22,7 +22,7 @@ pub async fn test_update_cake(db: &DbConn) {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
|
||||
let cake_insert_res = Cake::insert(mud_cake)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake");
|
||||
@ -36,7 +36,7 @@ pub async fn test_update_cake(db: &DbConn) {
|
||||
let cake_model = cake.unwrap();
|
||||
assert_eq!(cake_model.name, "Mud Cake");
|
||||
assert_eq!(cake_model.price, dec!(10.25));
|
||||
assert_eq!(cake_model.gluten_free, false);
|
||||
assert!(!cake_model.gluten_free);
|
||||
|
||||
let mut cake_am: cake::ActiveModel = cake_model.into();
|
||||
cake_am.name = Set("Extra chocolate mud cake".to_owned());
|
||||
@ -62,7 +62,7 @@ pub async fn test_update_bakery(db: &DbConn) {
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let bakery_insert_res: InsertResult = Bakery::insert(seaside_bakery)
|
||||
let bakery_insert_res = Bakery::insert(seaside_bakery)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert bakery");
|
||||
@ -75,7 +75,7 @@ pub async fn test_update_bakery(db: &DbConn) {
|
||||
assert!(bakery.is_some());
|
||||
let bakery_model = bakery.unwrap();
|
||||
assert_eq!(bakery_model.name, "SeaSide Bakery");
|
||||
assert_eq!(bakery_model.profit_margin, 10.4);
|
||||
assert!((bakery_model.profit_margin - 10.40).abs() < f64::EPSILON);
|
||||
|
||||
let mut bakery_am: bakery::ActiveModel = bakery_model.into();
|
||||
bakery_am.name = Set("SeaBreeze Bakery".to_owned());
|
||||
@ -92,7 +92,7 @@ pub async fn test_update_bakery(db: &DbConn) {
|
||||
.expect("could not find bakery");
|
||||
let bakery_model = bakery.unwrap();
|
||||
assert_eq!(bakery_model.name, "SeaBreeze Bakery");
|
||||
assert_eq!(bakery_model.profit_margin, 12.00);
|
||||
assert!((bakery_model.profit_margin - 12.00).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
pub async fn test_update_deleted_customer(db: &DbConn) {
|
||||
@ -130,11 +130,10 @@ pub async fn test_update_deleted_customer(db: &DbConn) {
|
||||
|
||||
assert_eq!(Customer::find().count(db).await.unwrap(), init_n_customers);
|
||||
|
||||
let customer: Option<customer::Model> =
|
||||
Customer::find_by_id(customer_id.clone().unwrap() as i64)
|
||||
.one(db)
|
||||
.await
|
||||
.expect("could not find customer");
|
||||
let customer: Option<customer::Model> = Customer::find_by_id(customer_id.clone().unwrap())
|
||||
.one(db)
|
||||
.await
|
||||
.expect("could not find customer");
|
||||
|
||||
assert_eq!(customer, None);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
pub mod common;
|
||||
pub use common::{bakery_chain::*, setup::*, TestContext};
|
||||
|
||||
mod crud;
|
||||
|
||||
pub use common::{bakery_chain::*, setup::*, TestContext};
|
||||
pub use crud::*;
|
||||
use sea_orm::DatabaseConnection;
|
||||
|
||||
// Run the test locally:
|
||||
// DATABASE_URL="mysql://root:root@localhost" cargo test --features sqlx-mysql,runtime-async-std --test crud_tests
|
||||
// DATABASE_URL="postgres://root:root@localhost" cargo test --features sqlx-postgres,runtime-async-std --test crud_tests
|
||||
@ -20,18 +20,18 @@ async fn main() {
|
||||
ctx.delete().await;
|
||||
}
|
||||
|
||||
async fn create_entities(db: &DatabaseConnection) {
|
||||
crud::test_create_bakery(db).await;
|
||||
crud::create_baker::test_create_baker(db).await;
|
||||
crud::test_create_customer(db).await;
|
||||
crud::create_cake::test_create_cake(db).await;
|
||||
crud::create_lineitem::test_create_lineitem(db).await;
|
||||
crud::create_order::test_create_order(db).await;
|
||||
pub async fn create_entities(db: &DatabaseConnection) {
|
||||
test_create_bakery(db).await;
|
||||
test_create_baker(db).await;
|
||||
test_create_customer(db).await;
|
||||
test_create_cake(db).await;
|
||||
test_create_lineitem(db).await;
|
||||
test_create_order(db).await;
|
||||
|
||||
crud::updates::test_update_cake(db).await;
|
||||
crud::updates::test_update_bakery(db).await;
|
||||
crud::updates::test_update_deleted_customer(db).await;
|
||||
test_update_cake(db).await;
|
||||
test_update_bakery(db).await;
|
||||
test_update_deleted_customer(db).await;
|
||||
|
||||
crud::deletes::test_delete_cake(db).await;
|
||||
crud::deletes::test_delete_bakery(db).await;
|
||||
test_delete_cake(db).await;
|
||||
test_delete_bakery(db).await;
|
||||
}
|
||||
|
42
tests/primary_key_tests.rs
Normal file
42
tests/primary_key_tests.rs
Normal file
@ -0,0 +1,42 @@
|
||||
pub mod common;
|
||||
|
||||
pub use common::{bakery_chain::*, setup::*, TestContext};
|
||||
use sea_orm::{entity::prelude::*, DatabaseConnection, Set};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[sea_orm_macros::test]
|
||||
#[cfg(any(
|
||||
feature = "sqlx-mysql",
|
||||
feature = "sqlx-sqlite",
|
||||
feature = "sqlx-postgres"
|
||||
))]
|
||||
async fn main() -> Result<(), DbErr> {
|
||||
let ctx = TestContext::new("bakery_chain_schema_primary_key_tests").await;
|
||||
|
||||
create_metadata(&ctx.db).await?;
|
||||
|
||||
ctx.delete().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_metadata(db: &DatabaseConnection) -> Result<(), DbErr> {
|
||||
let metadata = metadata::ActiveModel {
|
||||
uuid: Set(Uuid::new_v4()),
|
||||
key: Set("markup".to_owned()),
|
||||
value: Set("1.18".to_owned()),
|
||||
};
|
||||
|
||||
let res = Metadata::insert(metadata.clone()).exec(db).await?;
|
||||
|
||||
assert_eq!(
|
||||
res.last_insert_id,
|
||||
if cfg!(feature = "sqlx-postgres") {
|
||||
metadata.uuid.unwrap()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
use sea_orm::entity::*;
|
||||
use sea_orm::QueryFilter;
|
||||
|
||||
pub mod common;
|
||||
|
||||
pub use common::{bakery_chain::*, setup::*, TestContext};
|
||||
pub use sea_orm::entity::*;
|
||||
pub use sea_orm::QueryFilter;
|
||||
|
||||
// Run the test locally:
|
||||
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test query_tests
|
||||
|
@ -1,13 +1,14 @@
|
||||
use chrono::offset::Utc;
|
||||
use rust_decimal::prelude::*;
|
||||
use rust_decimal_macros::dec;
|
||||
use sea_orm::{entity::*, query::*, FromQueryResult};
|
||||
|
||||
pub mod common;
|
||||
|
||||
pub use chrono::offset::Utc;
|
||||
pub use common::{bakery_chain::*, setup::*, TestContext};
|
||||
pub use rust_decimal::prelude::*;
|
||||
pub use rust_decimal_macros::dec;
|
||||
pub use sea_orm::{entity::*, query::*, DbErr, FromQueryResult};
|
||||
pub use uuid::Uuid;
|
||||
|
||||
// Run the test locally:
|
||||
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test relational_tests
|
||||
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std-native-tls --test relational_tests
|
||||
#[sea_orm_macros::test]
|
||||
#[cfg(any(
|
||||
feature = "sqlx-mysql",
|
||||
@ -474,3 +475,240 @@ pub async fn having() {
|
||||
|
||||
ctx.delete().await;
|
||||
}
|
||||
|
||||
#[sea_orm_macros::test]
|
||||
#[cfg(any(
|
||||
feature = "sqlx-mysql",
|
||||
feature = "sqlx-sqlite",
|
||||
feature = "sqlx-postgres"
|
||||
))]
|
||||
pub async fn linked() -> Result<(), DbErr> {
|
||||
use common::bakery_chain::Order;
|
||||
use sea_orm::{SelectA, SelectB};
|
||||
|
||||
let ctx = TestContext::new("test_linked").await;
|
||||
|
||||
// SeaSide Bakery
|
||||
let seaside_bakery = bakery::ActiveModel {
|
||||
name: Set("SeaSide Bakery".to_owned()),
|
||||
profit_margin: Set(10.4),
|
||||
..Default::default()
|
||||
};
|
||||
let seaside_bakery_res = Bakery::insert(seaside_bakery).exec(&ctx.db).await?;
|
||||
|
||||
// Bob's Baker, Cake & Cake Baker
|
||||
let baker_bob = baker::ActiveModel {
|
||||
name: Set("Baker Bob".to_owned()),
|
||||
contact_details: Set(serde_json::json!({
|
||||
"mobile": "+61424000000",
|
||||
"home": "0395555555",
|
||||
"address": "12 Test St, Testville, Vic, Australia"
|
||||
})),
|
||||
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let baker_bob_res = Baker::insert(baker_bob).exec(&ctx.db).await?;
|
||||
let mud_cake = cake::ActiveModel {
|
||||
name: Set("Mud Cake".to_owned()),
|
||||
price: Set(dec!(10.25)),
|
||||
gluten_free: Set(false),
|
||||
serial: Set(Uuid::new_v4()),
|
||||
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let mud_cake_res = Cake::insert(mud_cake).exec(&ctx.db).await?;
|
||||
let bob_cakes_bakers = cakes_bakers::ActiveModel {
|
||||
cake_id: Set(mud_cake_res.last_insert_id as i32),
|
||||
baker_id: Set(baker_bob_res.last_insert_id as i32),
|
||||
..Default::default()
|
||||
};
|
||||
CakesBakers::insert(bob_cakes_bakers).exec(&ctx.db).await?;
|
||||
|
||||
// Bobby's Baker, Cake & Cake Baker
|
||||
let baker_bobby = baker::ActiveModel {
|
||||
name: Set("Baker Bobby".to_owned()),
|
||||
contact_details: Set(serde_json::json!({
|
||||
"mobile": "+85212345678",
|
||||
})),
|
||||
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let baker_bobby_res = Baker::insert(baker_bobby).exec(&ctx.db).await?;
|
||||
let cheese_cake = cake::ActiveModel {
|
||||
name: Set("Cheese Cake".to_owned()),
|
||||
price: Set(dec!(20.5)),
|
||||
gluten_free: Set(false),
|
||||
serial: Set(Uuid::new_v4()),
|
||||
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let cheese_cake_res = Cake::insert(cheese_cake).exec(&ctx.db).await?;
|
||||
let bobby_cakes_bakers = cakes_bakers::ActiveModel {
|
||||
cake_id: Set(cheese_cake_res.last_insert_id as i32),
|
||||
baker_id: Set(baker_bobby_res.last_insert_id as i32),
|
||||
..Default::default()
|
||||
};
|
||||
CakesBakers::insert(bobby_cakes_bakers)
|
||||
.exec(&ctx.db)
|
||||
.await?;
|
||||
let chocolate_cake = cake::ActiveModel {
|
||||
name: Set("Chocolate Cake".to_owned()),
|
||||
price: Set(dec!(30.15)),
|
||||
gluten_free: Set(false),
|
||||
serial: Set(Uuid::new_v4()),
|
||||
bakery_id: Set(Some(seaside_bakery_res.last_insert_id as i32)),
|
||||
..Default::default()
|
||||
};
|
||||
let chocolate_cake_res = Cake::insert(chocolate_cake).exec(&ctx.db).await?;
|
||||
let bobby_cakes_bakers = cakes_bakers::ActiveModel {
|
||||
cake_id: Set(chocolate_cake_res.last_insert_id as i32),
|
||||
baker_id: Set(baker_bobby_res.last_insert_id as i32),
|
||||
..Default::default()
|
||||
};
|
||||
CakesBakers::insert(bobby_cakes_bakers)
|
||||
.exec(&ctx.db)
|
||||
.await?;
|
||||
|
||||
// Kate's Customer, Order & Line Item
|
||||
let customer_kate = customer::ActiveModel {
|
||||
name: Set("Kate".to_owned()),
|
||||
notes: Set(Some("Loves cheese cake".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let customer_kate_res = Customer::insert(customer_kate).exec(&ctx.db).await?;
|
||||
let kate_order_1 = order::ActiveModel {
|
||||
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
|
||||
customer_id: Set(customer_kate_res.last_insert_id as i32),
|
||||
total: Set(dec!(15.10)),
|
||||
placed_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
};
|
||||
let kate_order_1_res = Order::insert(kate_order_1).exec(&ctx.db).await?;
|
||||
lineitem::ActiveModel {
|
||||
cake_id: Set(cheese_cake_res.last_insert_id as i32),
|
||||
order_id: Set(kate_order_1_res.last_insert_id as i32),
|
||||
price: Set(dec!(7.55)),
|
||||
quantity: Set(2),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&ctx.db)
|
||||
.await?;
|
||||
let kate_order_2 = order::ActiveModel {
|
||||
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
|
||||
customer_id: Set(customer_kate_res.last_insert_id as i32),
|
||||
total: Set(dec!(29.7)),
|
||||
placed_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
};
|
||||
let kate_order_2_res = Order::insert(kate_order_2).exec(&ctx.db).await?;
|
||||
lineitem::ActiveModel {
|
||||
cake_id: Set(chocolate_cake_res.last_insert_id as i32),
|
||||
order_id: Set(kate_order_2_res.last_insert_id as i32),
|
||||
price: Set(dec!(9.9)),
|
||||
quantity: Set(3),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&ctx.db)
|
||||
.await?;
|
||||
|
||||
// Kara's Customer, Order & Line Item
|
||||
let customer_kara = customer::ActiveModel {
|
||||
name: Set("Kara".to_owned()),
|
||||
notes: Set(Some("Loves all cakes".to_owned())),
|
||||
..Default::default()
|
||||
};
|
||||
let customer_kara_res = Customer::insert(customer_kara).exec(&ctx.db).await?;
|
||||
let kara_order_1 = order::ActiveModel {
|
||||
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
|
||||
customer_id: Set(customer_kara_res.last_insert_id as i32),
|
||||
total: Set(dec!(15.10)),
|
||||
placed_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
};
|
||||
let kara_order_1_res = Order::insert(kara_order_1).exec(&ctx.db).await?;
|
||||
lineitem::ActiveModel {
|
||||
cake_id: Set(mud_cake_res.last_insert_id as i32),
|
||||
order_id: Set(kara_order_1_res.last_insert_id as i32),
|
||||
price: Set(dec!(7.55)),
|
||||
quantity: Set(2),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&ctx.db)
|
||||
.await?;
|
||||
let kara_order_2 = order::ActiveModel {
|
||||
bakery_id: Set(seaside_bakery_res.last_insert_id as i32),
|
||||
customer_id: Set(customer_kara_res.last_insert_id as i32),
|
||||
total: Set(dec!(29.7)),
|
||||
placed_at: Set(Utc::now().naive_utc()),
|
||||
..Default::default()
|
||||
};
|
||||
let kara_order_2_res = Order::insert(kara_order_2).exec(&ctx.db).await?;
|
||||
lineitem::ActiveModel {
|
||||
cake_id: Set(cheese_cake_res.last_insert_id as i32),
|
||||
order_id: Set(kara_order_2_res.last_insert_id as i32),
|
||||
price: Set(dec!(9.9)),
|
||||
quantity: Set(3),
|
||||
..Default::default()
|
||||
}
|
||||
.save(&ctx.db)
|
||||
.await?;
|
||||
|
||||
#[derive(Debug, FromQueryResult, PartialEq)]
|
||||
struct BakerLite {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromQueryResult, PartialEq)]
|
||||
struct CustomerLite {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let baked_for_customers: Vec<(BakerLite, Option<CustomerLite>)> = Baker::find()
|
||||
.find_also_linked(baker::BakedForCustomer)
|
||||
.select_only()
|
||||
.column_as(baker::Column::Name, (SelectA, baker::Column::Name))
|
||||
.column_as(customer::Column::Name, (SelectB, customer::Column::Name))
|
||||
.group_by(baker::Column::Id)
|
||||
.group_by(customer::Column::Id)
|
||||
.group_by(baker::Column::Name)
|
||||
.group_by(customer::Column::Name)
|
||||
.order_by_asc(baker::Column::Id)
|
||||
.order_by_asc(customer::Column::Id)
|
||||
.into_model()
|
||||
.all(&ctx.db)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
baked_for_customers,
|
||||
vec![
|
||||
(
|
||||
BakerLite {
|
||||
name: "Baker Bob".to_owned(),
|
||||
},
|
||||
Some(CustomerLite {
|
||||
name: "Kara".to_owned(),
|
||||
})
|
||||
),
|
||||
(
|
||||
BakerLite {
|
||||
name: "Baker Bobby".to_owned(),
|
||||
},
|
||||
Some(CustomerLite {
|
||||
name: "Kate".to_owned(),
|
||||
})
|
||||
),
|
||||
(
|
||||
BakerLite {
|
||||
name: "Baker Bobby".to_owned(),
|
||||
},
|
||||
Some(CustomerLite {
|
||||
name: "Kara".to_owned(),
|
||||
})
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
ctx.delete().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
use chrono::offset::Utc;
|
||||
use rust_decimal::prelude::*;
|
||||
use rust_decimal_macros::dec;
|
||||
use sea_orm::{entity::*, query::*, DatabaseConnection, FromQueryResult};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub mod common;
|
||||
|
||||
pub use chrono::offset::Utc;
|
||||
pub use common::{bakery_chain::*, setup::*, TestContext};
|
||||
pub use rust_decimal::prelude::*;
|
||||
pub use rust_decimal_macros::dec;
|
||||
pub use sea_orm::{entity::*, query::*, DatabaseConnection, FromQueryResult};
|
||||
pub use uuid::Uuid;
|
||||
|
||||
// Run the test locally:
|
||||
// DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test sequential_op_tests
|
||||
@ -67,7 +67,7 @@ async fn init_setup(db: &DatabaseConnection) {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cake_insert_res: InsertResult = Cake::insert(mud_cake)
|
||||
let cake_insert_res = Cake::insert(mud_cake)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake");
|
||||
@ -78,10 +78,18 @@ async fn init_setup(db: &DatabaseConnection) {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
|
||||
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake_baker");
|
||||
assert_eq!(
|
||||
cake_baker_res.last_insert_id,
|
||||
if cfg!(feature = "sqlx-postgres") {
|
||||
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
let customer_kate = customer::ActiveModel {
|
||||
name: Set("Kate".to_owned()),
|
||||
@ -183,7 +191,7 @@ async fn find_baker_least_sales(db: &DatabaseConnection) -> Option<baker::Model>
|
||||
|
||||
results.sort_by(|a, b| b.cakes_sold.cmp(&a.cakes_sold));
|
||||
|
||||
Baker::find_by_id(results.last().unwrap().id as i64)
|
||||
Baker::find_by_id(results.last().unwrap().id)
|
||||
.one(db)
|
||||
.await
|
||||
.unwrap()
|
||||
@ -200,7 +208,7 @@ async fn create_cake(db: &DatabaseConnection, baker: baker::Model) -> Option<cak
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cake_insert_res: InsertResult = Cake::insert(new_cake)
|
||||
let cake_insert_res = Cake::insert(new_cake)
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake");
|
||||
@ -211,10 +219,18 @@ async fn create_cake(db: &DatabaseConnection, baker: baker::Model) -> Option<cak
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _cake_baker_res: InsertResult = CakesBakers::insert(cake_baker)
|
||||
let cake_baker_res = CakesBakers::insert(cake_baker.clone())
|
||||
.exec(db)
|
||||
.await
|
||||
.expect("could not insert cake_baker");
|
||||
assert_eq!(
|
||||
cake_baker_res.last_insert_id,
|
||||
if cfg!(feature = "sqlx-postgres") {
|
||||
(cake_baker.cake_id.unwrap(), cake_baker.baker_id.unwrap())
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
Cake::find_by_id(cake_insert_res.last_insert_id)
|
||||
.one(db)
|
||||
|
Loading…
x
Reference in New Issue
Block a user