diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 426cb003..7928130b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..674b3163 --- /dev/null +++ b/ARCHITECTURE.md @@ -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. \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c9035c40..4bea7112 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/Cargo.toml b/Cargo.toml index e021b82a..d1148546 100644 --- a/Cargo.toml +++ b/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 "] 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", diff --git a/Design.md b/DESIGN.md similarity index 70% rename from Design.md rename to DESIGN.md index 73e88227..291efc02 100644 --- a/Design.md +++ b/DESIGN.md @@ -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) +where + A: IntoActiveModel, +{ + let a: ActiveModel = a.into_active_model(); + ... +} ``` \ No newline at end of file diff --git a/README.md b/README.md index f2d262e9..9696b6d3 100644 --- a/README.md +++ b/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. -``` - [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) -[![Examples](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx) -[![Starter Kit](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37) +[![Examples](https://img.shields.io/badge/Examples-green)](https://github.com/SeaQL/sea-orm/tree/master/examples) +[![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) ## Features @@ -68,10 +64,8 @@ let cheese: cake::Model = cheese.unwrap(); let fruits: Vec = cheese.find_related(Fruit).all(db).await?; // find related models (eager) -let cake_with_fruits: Vec<(cake::Model, Vec)> = Cake::find() - .find_with_related(Fruit) - .all(db) - .await?; +let cake_with_fruits: Vec<(cake::Model, Vec)> = + 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 diff --git a/examples/async-std/src/example_cake_filling.rs b/examples/async-std/src/example_cake_filling.rs index 19de83e4..4fa188bc 100644 --- a/examples/async-std/src/example_cake_filling.rs +++ b/examples/async-std/src/example_cake_filling.rs @@ -28,6 +28,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/examples/async-std/src/example_filling.rs b/examples/async-std/src/example_filling.rs index 925b92fc..2a39a7de 100644 --- a/examples/async-std/src/example_filling.rs +++ b/examples/async-std/src/example_filling.rs @@ -27,6 +27,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/examples/async-std/src/example_fruit.rs b/examples/async-std/src/example_fruit.rs index b875da24..8802e707 100644 --- a/examples/async-std/src/example_fruit.rs +++ b/examples/async-std/src/example_fruit.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/examples/async-std/src/operation.rs b/examples/async-std/src/operation.rs index b1273e10..3fa5cc85 100644 --- a/examples/async-std/src/operation.rs +++ b/examples/async-std/src/operation.rs @@ -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); diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml new file mode 100644 index 00000000..1b4303c5 --- /dev/null +++ b/examples/rocket_example/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "sea-orm-rocket-example" +version = "0.1.0" +authors = ["Sam Samai "] +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"] diff --git a/examples/rocket_example/README.md b/examples/rocket_example/README.md new file mode 100644 index 00000000..596a763a --- /dev/null +++ b/examples/rocket_example/README.md @@ -0,0 +1,11 @@ +![screenshot](Screenshot.png) + +# 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 diff --git a/examples/rocket_example/Rocket.toml b/examples/rocket_example/Rocket.toml new file mode 100644 index 00000000..b7fcc12a --- /dev/null +++ b/examples/rocket_example/Rocket.toml @@ -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" diff --git a/examples/rocket_example/Screenshot.png b/examples/rocket_example/Screenshot.png new file mode 100644 index 00000000..237c4f75 Binary files /dev/null and b/examples/rocket_example/Screenshot.png differ diff --git a/examples/rocket_example/src/main.rs b/examples/rocket_example/src/main.rs new file mode 100644 index 00000000..b5b60b4b --- /dev/null +++ b/examples/rocket_example/src/main.rs @@ -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> = std::result::Result; + +mod post; +pub use post::Entity as Post; + +#[get("/new")] +fn new() -> Template { + Template::render("new", &Context::default()) +} + +#[post("/", data = "")] +async fn create(conn: Connection, post_form: Form) -> Flash { + 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("/", data = "")] +async fn update(conn: Connection, id: i64, post_form: Form) -> Flash { + 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, flash: Option>) -> Template { + let posts = Post::find() + .order_by_asc(post::Column::Id) + .all(&conn) + .await + .expect("could not retrieve posts") + .into_iter() + .collect::>(); + let flash = flash.map(FlashMessage::into_inner); + + Template::render( + "index", + context! { + posts: posts, + flash: flash, + }, + ) +} + +#[get("/")] +async fn edit(conn: Connection, id: i64) -> Template { + let post: Option = Post::find_by_id(id) + .one(&conn) + .await + .expect("could not find post"); + + Template::render( + "edit", + context! { + post: post, + }, + ) +} + +#[delete("/")] +async fn delete(conn: Connection, id: i32) -> Flash { + 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) -> 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) -> 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()) +} diff --git a/examples/rocket_example/src/pool.rs b/examples/rocket_example/src/pool.rs new file mode 100644 index 00000000..094746ba --- /dev/null +++ b/examples/rocket_example/src/pool.rs @@ -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 { + let config = figment.extract::().unwrap(); + let db_url = config.url; + + Ok(RocketDbPool { + db_url: db_url.to_owned(), + }) + } + + async fn get(&self) -> Result { + Ok(sea_orm::Database::connect(&self.db_url).await.unwrap()) + } +} diff --git a/examples/rocket_example/src/post.rs b/examples/rocket_example/src/post.rs new file mode 100644 index 00000000..0bce0e7a --- /dev/null +++ b/examples/rocket_example/src/post.rs @@ -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, + 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 {} diff --git a/examples/rocket_example/src/setup.rs b/examples/rocket_example/src/setup.rs new file mode 100644 index 00000000..034e8b53 --- /dev/null +++ b/examples/rocket_example/src/setup.rs @@ -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 { + let builder = db.get_database_backend(); + db.execute(builder.build(stmt)).await +} + +pub async fn create_post_table(db: &DbConn) -> Result { + 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 +} diff --git a/examples/rocket_example/static/css/normalize.css b/examples/rocket_example/static/css/normalize.css new file mode 100644 index 00000000..458eea1e --- /dev/null +++ b/examples/rocket_example/static/css/normalize.css @@ -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; +} diff --git a/examples/rocket_example/static/css/skeleton.css b/examples/rocket_example/static/css/skeleton.css new file mode 100644 index 00000000..cdc432a4 --- /dev/null +++ b/examples/rocket_example/static/css/skeleton.css @@ -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) {} diff --git a/examples/rocket_example/static/css/style.css b/examples/rocket_example/static/css/style.css new file mode 100644 index 00000000..ac2720d3 --- /dev/null +++ b/examples/rocket_example/static/css/style.css @@ -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; +} diff --git a/examples/rocket_example/static/images/favicon.png b/examples/rocket_example/static/images/favicon.png new file mode 100644 index 00000000..02b73904 Binary files /dev/null and b/examples/rocket_example/static/images/favicon.png differ diff --git a/examples/rocket_example/templates/base.html.tera b/examples/rocket_example/templates/base.html.tera new file mode 100644 index 00000000..0b83d9ed --- /dev/null +++ b/examples/rocket_example/templates/base.html.tera @@ -0,0 +1,81 @@ + + + + + Rocket Todo Example + + + + + + + + + + + +
+

+ + {# +
+

Rocket Todo

+
+
+ + {% if msg %} + + {{ msg.1 }} + + {% endif %} +
+
+ +
+
+
+ +
+
+
    + {% for task in tasks %} {% if task.completed %} +
  • + {{ task.description }} +
    + + +
    +
    + + +
    +
  • + {% else %} +
  • + +
  • + {% endif %} {% endfor %} +
+
+
+ #} {% block content %}{% endblock content %} +
+ + diff --git a/examples/rocket_example/templates/edit.html.tera b/examples/rocket_example/templates/edit.html.tera new file mode 100644 index 00000000..47882e65 --- /dev/null +++ b/examples/rocket_example/templates/edit.html.tera @@ -0,0 +1,50 @@ +{% extends "base" %} {% block content %} +
+

Edit Post

+
+ +
+
+
+ + +
+
+
+
+
+{% endblock content %} diff --git a/examples/rocket_example/templates/error/404.html.tera b/examples/rocket_example/templates/error/404.html.tera new file mode 100644 index 00000000..afda653d --- /dev/null +++ b/examples/rocket_example/templates/error/404.html.tera @@ -0,0 +1,11 @@ + + + + + 404 - tera + + +

404: Hey! There's nothing here.

+ The page at {{ uri }} does not exist! + + diff --git a/examples/rocket_example/templates/index.html.tera b/examples/rocket_example/templates/index.html.tera new file mode 100644 index 00000000..621f18b8 --- /dev/null +++ b/examples/rocket_example/templates/index.html.tera @@ -0,0 +1,33 @@ +{% extends "base" %} {% block content %} +

Posts

+{% if flash %} + + {{ flash.1 }} + +{% endif %} + + + + + + + + + + {% for post in posts %} + + + + + + {% endfor %} + +
IDTitleText
{{ post.id }}{{ post.title }}{{ post.text }}
+ + + +{% endblock content %} diff --git a/examples/rocket_example/templates/new.html.tera b/examples/rocket_example/templates/new.html.tera new file mode 100644 index 00000000..57daffe2 --- /dev/null +++ b/examples/rocket_example/templates/new.html.tera @@ -0,0 +1,38 @@ +{% extends "base" %} {% block content %} +
+

New Post

+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+{% endblock content %} diff --git a/examples/tokio/src/cake.rs b/examples/tokio/src/cake.rs index 0b1a4439..21b83331 100644 --- a/examples/tokio/src/cake.rs +++ b/examples/tokio/src/cake.rs @@ -27,6 +27,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..521706a0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +format_code_in_doc_comments=true \ No newline at end of file diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 7c9ec510..46340f7b 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "sea-orm-cli" -version = "0.1.3" +version = "0.2.0" authors = [ "Billy Chan " ] 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", diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml index 9080bd2b..56cd72ae 100644 --- a/sea-orm-codegen/Cargo.toml +++ b/sea-orm-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-orm-codegen" -version = "0.1.3" +version = "0.2.0" authors = ["Billy Chan "] edition = "2018" description = "Code Generator for SeaORM" diff --git a/sea-orm-codegen/src/entity/base_entity.rs b/sea-orm-codegen/src/entity/base_entity.rs index f1e03b21..7b28f9e4 100644 --- a/sea-orm-codegen/src/entity/base_entity.rs +++ b/sea-orm-codegen/src/entity/base_entity.rs @@ -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::>(); + 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 { 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(); diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index a2c3b4f5..0b9eb8de 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -119,33 +119,18 @@ impl From<&ColumnDef> for Column { Some(ty) => ty.clone(), None => panic!("ColumnType should not be empty"), }; - let auto_increments: Vec = 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 = 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 = 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, diff --git a/sea-orm-codegen/src/entity/transformer.rs b/sea-orm-codegen/src/entity/transformer.rs index 75dc25c7..840eade0 100644 --- a/sea-orm-codegen/src/entity/transformer.rs +++ b/sea-orm-codegen/src/entity/transformer.rs @@ -34,11 +34,6 @@ impl EntityTransformer { .iter() .map(|col_def| col_def.into()) .collect(); - let unique_columns: Vec = columns - .iter() - .filter(|col| col.unique) - .map(|col| col.name.clone()) - .collect(); let relations = table_create .get_foreign_key_create_stmts() .iter() @@ -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; } diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 207864ba..19e9af1c 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -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"), diff --git a/sea-orm-codegen/tests/entity/cake.rs b/sea-orm-codegen/tests/entity/cake.rs index 29f55ac6..55fa279f 100644 --- a/sea-orm-codegen/tests/entity/cake.rs +++ b/sea-orm-codegen/tests/entity/cake.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-codegen/tests/entity/cake_filling.rs b/sea-orm-codegen/tests/entity/cake_filling.rs index d0f00560..d100fa8c 100644 --- a/sea-orm-codegen/tests/entity/cake_filling.rs +++ b/sea-orm-codegen/tests/entity/cake_filling.rs @@ -30,6 +30,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/sea-orm-codegen/tests/entity/filling.rs b/sea-orm-codegen/tests/entity/filling.rs index bedb1ab4..73d58152 100644 --- a/sea-orm-codegen/tests/entity/filling.rs +++ b/sea-orm-codegen/tests/entity/filling.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-codegen/tests/entity/fruit.rs b/sea-orm-codegen/tests/entity/fruit.rs index 72c37c1b..12919021 100644 --- a/sea-orm-codegen/tests/entity/fruit.rs +++ b/sea-orm-codegen/tests/entity/fruit.rs @@ -31,6 +31,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-codegen/tests/entity/vendor.rs b/sea-orm-codegen/tests/entity/vendor.rs index 320400f4..ffc211a8 100644 --- a/sea-orm-codegen/tests/entity/vendor.rs +++ b/sea-orm-codegen/tests/entity/vendor.rs @@ -31,6 +31,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index 06a5132b..9de45ae3 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-orm-macros" -version = "0.1.1" +version = "0.2.0" authors = [ "Billy Chan " ] edition = "2018" description = "Derive macros for SeaORM" diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index f04869b1..aa1adf8b 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -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 }) } ) diff --git a/src/database/connection.rs b/src/database/connection.rs index 3d4215f5..6a39b240 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -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())), } } diff --git a/src/driver/mock.rs b/src/driver/mock.rs index 0a1629bf..823ddb32 100644 --- a/src/driver/mock.rs +++ b/src/driver/mock.rs @@ -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>, } -pub trait MockDatabaseTrait: Send { +pub trait MockDatabaseTrait: Send + Debug { fn execute(&mut self, counter: usize, stmt: Statement) -> Result; fn query(&mut self, counter: usize, stmt: Statement) -> Result, DbErr>; diff --git a/src/driver/sqlx_mysql.rs b/src/driver/sqlx_mysql.rs index 48da88a0..e91df037 100644 --- a/src/driver/sqlx_mysql.rs +++ b/src/driver/sqlx_mysql.rs @@ -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, } diff --git a/src/driver/sqlx_postgres.rs b/src/driver/sqlx_postgres.rs index a283ddf3..086dc995 100644 --- a/src/driver/sqlx_postgres.rs +++ b/src/driver/sqlx_postgres.rs @@ -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 for QueryResult { impl From 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 { - 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 { diff --git a/src/driver/sqlx_sqlite.rs b/src/driver/sqlx_sqlite.rs index ac275b72..5fa4bdcd 100644 --- a/src/driver/sqlx_sqlite.rs +++ b/src/driver/sqlx_sqlite.rs @@ -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, } diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index 54bf037b..f823411c 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -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 ::auto_increment() && res.last_insert_id != 0 { + // FIXME: Assumed valid last_insert_id is not equals to Default::default() + if ::auto_increment() + && res.last_insert_id != ::ValueType::default() + { let find = E::find_by_id(res.last_insert_id).one(db); let found = find.await; let model: Option = 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 { diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index d0740307..67f817a7 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -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) -> RelationBuilder where R: EntityTrait + Related, { - RelationBuilder::from_rel(RelationType::HasOne, R::to().rev()) + RelationBuilder::from_rel(RelationType::HasOne, R::to().rev(), true) } fn has_many(_: R) -> RelationBuilder where R: EntityTrait + Related, { - 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 { 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 /// ``` diff --git a/src/entity/column.rs b/src/entity/column.rs index 16546057..6e871f4d 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -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 } diff --git a/src/entity/identity.rs b/src/entity/identity.rs index d1cc3170..d8623b7d 100644 --- a/src/entity/identity.rs +++ b/src/entity/identity.rs @@ -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 IntoIdentity for T where T: IdenStatic, diff --git a/src/entity/mod.rs b/src/entity/mod.rs index 6da5e1b6..47eb4fa5 100644 --- a/src/entity/mod.rs +++ b/src/entity/mod.rs @@ -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::*; diff --git a/src/entity/model.rs b/src/entity/model.rs index 15ebdb58..4774e1dc 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -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 { { >::find_related().belongs_to(self) } + + fn find_linked(&self, l: L) -> Select + where + L: Linked, + { + l.find_linked() + } } pub trait FromQueryResult { diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index a417f77a..5d69a2ed 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -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")] diff --git a/src/entity/primary_key.rs b/src/entity/primary_key.rs index 81a28915..b9c381d4 100644 --- a/src/entity/primary_key.rs +++ b/src/entity/primary_key.rs @@ -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; } diff --git a/src/entity/relation.rs b/src/entity/relation.rs index 955660e2..7b2d7b4d 100644 --- a/src/entity/relation.rs +++ b/src/entity/relation.rs @@ -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; + + fn find_linked(&self) -> Select { + 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, + pub on_update: Option, } +#[derive(Debug)] pub struct RelationBuilder where E: EntityTrait, @@ -47,6 +70,9 @@ where to_tbl: TableRef, from_col: Option, to_col: Option, + is_owner: bool, + on_delete: Option, + on_update: Option, } 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 From> 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, } } } diff --git a/src/entity/schema.rs b/src/entity/schema.rs new file mode 100644 index 00000000..06c688a8 --- /dev/null +++ b/src/entity/schema.rs @@ -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(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) + ); + } +} diff --git a/src/error.rs b/src/error.rs index 8a695dac..09f80b0a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum DbErr { Conn(String), Exec(String), diff --git a/src/executor/execute.rs b/src/executor/execute.rs index 00375bb7..46ba2d69 100644 --- a/src/executor/execute.rs +++ b/src/executor/execute.rs @@ -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")] diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 9414fea6..90065675 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -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 +where + A: ActiveModelTrait, +{ query: InsertStatement, + model: PhantomData, } -#[derive(Clone, Debug)] -pub struct InsertResult { - pub last_insert_id: u64, +#[derive(Debug)] +pub struct InsertResult +where + A: ActiveModelTrait, +{ + pub last_insert_id: <<::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType, } impl Insert @@ -17,54 +27,79 @@ where A: ActiveModelTrait, { #[allow(unused_mut)] - pub fn exec( + pub fn exec<'a>( self, - db: &DatabaseConnection, - ) -> impl Future> + '_ { + db: &'a DatabaseConnection, + ) -> impl Future, 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 ::PrimaryKey::iter() { + use crate::{sea_query::Query, Iterable}; + if ::PrimaryKey::iter().count() > 0 { query.returning( Query::select() - .expr_as(Expr::col(key), Alias::new("last_insert_id")) - .to_owned(), + .columns(::PrimaryKey::iter()) + .take(), ); } } - Inserter::new(query).exec(db) + Inserter::::new(query).exec(db) } } -impl Inserter { +impl Inserter +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> + '_ { + db: &'a DatabaseConnection, + ) -> impl Future, 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 { - // TODO: Postgres instead use query_one + returning clause - let result = match db { +async fn exec_insert( + statement: Statement, + db: &DatabaseConnection, +) -> Result, DbErr> +where + A: ActiveModelTrait, +{ + type PrimaryKey = <::Entity as EntityTrait>::PrimaryKey; + type ValueTypeOf = 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::::iter() + .map(|col| col.to_string()) + .collect::>(); 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::::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 }) } diff --git a/src/executor/query.rs b/src/executor/query.rs index 08728081..c30daf48 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -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; +} + +#[derive(Debug)] pub enum TryGetError { DbErr(DbErr), Null, @@ -31,12 +36,6 @@ impl From for DbErr { } } -pub trait TryGetable { - fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result - where - Self: Sized; -} - // QueryResult // impl QueryResult { @@ -46,6 +45,13 @@ impl QueryResult { { Ok(T::try_get(self, pre, col)?) } + + pub fn try_get_many(&self, pre: &str, cols: &[String]) -> Result + 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; +} + +impl TryGetableMany for T +where + T: TryGetable, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + try_get_many_with_slice_len_of(1, cols)?; + T::try_get(res, pre, &cols[0]) + } +} + +impl TryGetableMany for (T, T) +where + T: TryGetable, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + 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 TryGetableMany for (T, T, T) +where + T: TryGetable, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + 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; +} + +macro_rules! try_from_u64_err { + ( $type: ty ) => { + impl TryFromU64 for $type { + fn try_from_u64(_: u64) -> Result { + 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 { + 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 { + 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); diff --git a/src/executor/select.rs b/src/executor/select.rs index 6196ec73..ef30bb3d 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -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; } +#[derive(Debug)] pub struct SelectModel where M: FromQueryResult, @@ -66,8 +67,8 @@ where fn from_raw_query_result(res: QueryResult) -> Result { 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(self) -> Selector> + pub fn into_model(self) -> Selector> where M: FromQueryResult, N: FromQueryResult, @@ -289,14 +290,15 @@ where /// /// # let _: Result<(), DbErr> = smol::block_on(async { /// # - /// let res: Vec = 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::() - /// .all(&db) - /// .await?; + /// let res: Vec = 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::() + /// .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(self) -> SelectorRaw> where @@ -406,22 +409,26 @@ where /// /// # let _: Result<(), DbErr> = smol::block_on(async { /// # - /// let _: Option = 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::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, DbErr> { let row = db.query_one(self.stmt).await?; @@ -441,22 +448,26 @@ where /// /// # let _: Result<(), DbErr> = smol::block_on(async { /// # - /// let _: Vec = 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::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, DbErr> { let rows = db.query_all(self.stmt).await?; diff --git a/src/lib.rs b/src/lib.rs index d2474dc1..f6245ae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![deny( + missing_debug_implementations, + clippy::print_stderr, + clippy::print_stdout +)] + //!
//! //! @@ -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. -//! ``` -//! //! [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) -//! [![Examples](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx) -//! [![Starter Kit](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37) +//! [![Examples](https://img.shields.io/badge/Examples-green)](https://github.com/SeaQL/sea-orm/tree/master/examples) +//! [![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) //! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) //! //! ## Features @@ -70,10 +73,8 @@ //! let fruits: Vec = cheese.find_related(Fruit).all(db).await?; //! //! // find related models (eager) -//! let cake_with_fruits: Vec<(cake::Model, Vec)> = Cake::find() -//! .find_with_related(Fruit) -//! .all(db) -//! .await?; +//! let cake_with_fruits: Vec<(cake::Model, Vec)> = +//! 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 diff --git a/src/query/combine.rs b/src/query/combine.rs index 8cce0510..0c0f151f 100644 --- a/src/query/combine.rs +++ b/src/query/combine.rs @@ -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 Select 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, { for col in ::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))), diff --git a/src/query/helper.rs b/src/query/helper.rs index 6ade581a..cb1ca28d 100644 --- a/src/query/helper.rs +++ b/src/query/helper.rs @@ -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(mut self, col: C, alias: &str) -> Self + fn column_as(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), diff --git a/src/query/insert.rs b/src/query/insert.rs index abd52b4b..a65071e1 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -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')"#, /// ); /// ``` diff --git a/src/query/join.rs b/src/query/join.rs index 72726d14..42bc993c 100644 --- a/src/query/join.rs +++ b/src/query/join.rs @@ -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 Select @@ -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(self, l: L) -> SelectTwo + where + L: Linked, + 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(" ") + ); + } } diff --git a/src/query/mod.rs b/src/query/mod.rs index 899882ba..54cc12dd 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -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::*; diff --git a/src/query/update.rs b/src/query/update.rs index 21fd39cb..8cd66c37 100644 --- a/src/query/update.rs +++ b/src/query/update.rs @@ -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) diff --git a/src/tests_cfg/cake.rs b/src/tests_cfg/cake.rs index f8a35d6c..0eeb0738 100644 --- a/src/tests_cfg/cake.rs +++ b/src/tests_cfg/cake.rs @@ -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 for Entity { } } +#[derive(Debug)] +pub struct CakeToFilling; + +impl Linked for CakeToFilling { + type FromEntity = Entity; + + type ToEntity = super::filling::Entity; + + fn link(&self) -> Vec { + vec![ + super::cake_filling::Relation::Cake.def().rev(), + super::cake_filling::Relation::Filling.def(), + ] + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/src/tests_cfg/cake_filling.rs b/src/tests_cfg/cake_filling.rs index b1151ee4..ed6d0af0 100644 --- a/src/tests_cfg/cake_filling.rs +++ b/src/tests_cfg/cake_filling.rs @@ -29,6 +29,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/src/tests_cfg/cake_filling_price.rs b/src/tests_cfg/cake_filling_price.rs index c0bcbea1..e820ae0f 100644 --- a/src/tests_cfg/cake_filling_price.rs +++ b/src/tests_cfg/cake_filling_price.rs @@ -35,6 +35,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = (i32, i32); + fn auto_increment() -> bool { false } diff --git a/src/tests_cfg/filling.rs b/src/tests_cfg/filling.rs index b439af7b..5e691980 100644 --- a/src/tests_cfg/filling.rs +++ b/src/tests_cfg/filling.rs @@ -41,6 +41,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/src/tests_cfg/fruit.rs b/src/tests_cfg/fruit.rs index 0511ae58..b0b8bc79 100644 --- a/src/tests_cfg/fruit.rs +++ b/src/tests_cfg/fruit.rs @@ -30,6 +30,8 @@ pub enum PrimaryKey { } impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + fn auto_increment() -> bool { true } diff --git a/src/util.rs b/src/util.rs index f6ddfd54..20cc8eb3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -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] diff --git a/tests/basic.rs b/tests/basic.rs index 78e3f25c..e62f1ad6 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -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] diff --git a/tests/common/bakery_chain/baker.rs b/tests/common/bakery_chain/baker.rs index 0c63e721..9ab45905 100644 --- a/tests/common/bakery_chain/baker.rs +++ b/tests/common/bakery_chain/baker.rs @@ -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 for Entity { } } +pub struct BakedForCustomer; + +impl Linked for BakedForCustomer { + type FromEntity = Entity; + + type ToEntity = super::customer::Entity; + + fn link(&self) -> Vec { + 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 {} diff --git a/tests/common/bakery_chain/bakery.rs b/tests/common/bakery_chain/bakery.rs index 61803329..d03298b3 100644 --- a/tests/common/bakery_chain/bakery.rs +++ b/tests/common/bakery_chain/bakery.rs @@ -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(), } } } diff --git a/tests/common/bakery_chain/cake.rs b/tests/common/bakery_chain/cake.rs index 72e649ce..cb1895a6 100644 --- a/tests/common/bakery_chain/cake.rs +++ b/tests/common/bakery_chain/cake.rs @@ -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(), } diff --git a/tests/common/bakery_chain/cakes_bakers.rs b/tests/common/bakery_chain/cakes_bakers.rs index 8106bbdf..b0f52ec8 100644 --- a/tests/common/bakery_chain/cakes_bakers.rs +++ b/tests/common/bakery_chain/cakes_bakers.rs @@ -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(), } } diff --git a/tests/common/bakery_chain/customer.rs b/tests/common/bakery_chain/customer.rs index ce4319ff..2cf1b9ec 100644 --- a/tests/common/bakery_chain/customer.rs +++ b/tests/common/bakery_chain/customer.rs @@ -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(), } } } diff --git a/tests/common/bakery_chain/lineitem.rs b/tests/common/bakery_chain/lineitem.rs index 45a6037f..c89b44b8 100644 --- a/tests/common/bakery_chain/lineitem.rs +++ b/tests/common/bakery_chain/lineitem.rs @@ -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(), } } diff --git a/tests/common/bakery_chain/metadata.rs b/tests/common/bakery_chain/metadata.rs new file mode 100644 index 00000000..ecef26c3 --- /dev/null +++ b/tests/common/bakery_chain/metadata.rs @@ -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 {} diff --git a/tests/common/bakery_chain/mod.rs b/tests/common/bakery_chain/mod.rs index 89028aab..3282766d 100644 --- a/tests/common/bakery_chain/mod.rs +++ b/tests/common/bakery_chain/mod.rs @@ -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; diff --git a/tests/common/bakery_chain/order.rs b/tests/common/bakery_chain/order.rs index 82b02dee..f9561b92 100644 --- a/tests/common/bakery_chain/order.rs +++ b/tests/common/bakery_chain/order.rs @@ -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(), } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 54630999..2392ff72 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -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, } diff --git a/tests/common/setup/mod.rs b/tests/common/setup/mod.rs index 7528cf6c..74e35b45 100644 --- a/tests/common/setup/mod.rs +++ b/tests/common/setup/mod.rs @@ -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 } diff --git a/tests/common/setup/schema.rs b/tests/common/setup/schema.rs index 4eba40ab..e3844dd2 100644 --- a/tests/common/setup/schema.rs +++ b/tests/common/setup/schema.rs @@ -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 { +async fn create_table( + db: &DbConn, + stmt: &TableCreateStatement, + entity: E, +) -> Result +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 { - 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 { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Bakery).await } pub async fn create_baker_table(db: &DbConn) -> Result { - 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 { .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 { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Baker).await } pub async fn create_customer_table(db: &DbConn) -> Result { - 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 { .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 { - 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 { ) .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 { ) .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 { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Order).await } pub async fn create_lineitem_table(db: &DbConn) -> Result { - 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 { ) .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 { ) .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 { ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, Lineitem).await } pub async fn create_cakes_bakers_table(db: &DbConn) -> Result { - 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 ) .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 ) .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 ) .to_owned(); - create_table(db, &stmt).await + create_table(db, &stmt, CakesBakers).await } pub async fn create_cake_table(db: &DbConn) -> Result { - 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 { .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 { .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 { + 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 } diff --git a/tests/crud/create_baker.rs b/tests/crud/create_baker.rs index 8653b692..085b4005 100644 --- a/tests/crud/create_baker.rs +++ b/tests/crud/create_baker.rs @@ -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"); diff --git a/tests/crud/create_cake.rs b/tests/crud/create_cake.rs index 07f9c4b3..4fa914a5 100644 --- a/tests/crud/create_cake.rs +++ b/tests/crud/create_cake.rs @@ -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) diff --git a/tests/crud/create_lineitem.rs b/tests/crud/create_lineitem.rs index c82958f9..da82cc82 100644 --- a/tests/crud/create_lineitem.rs +++ b/tests/crud/create_lineitem.rs @@ -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::find_by_id(lineitem_model.cake_id as u64) + let cake: Option = Cake::find_by_id(lineitem_model.cake_id) .one(db) .await .expect("could not find cake"); diff --git a/tests/crud/create_order.rs b/tests/crud/create_order.rs index 46ebcf09..ba8ff09b 100644 --- a/tests/crud/create_order.rs +++ b/tests/crud/create_order.rs @@ -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::find_by_id(order_model.customer_id as u64) + let customer: Option = 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::find_by_id(order_model.bakery_id as i64) + let bakery: Option = Bakery::find_by_id(order_model.bakery_id) .one(db) .await .expect("could not find bakery"); diff --git a/tests/crud/deletes.rs b/tests/crud/deletes.rs index ebff4b5f..4c34d36b 100644 --- a/tests/crud/deletes.rs +++ b/tests/crud/deletes.rs @@ -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"); diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 457f639d..916878c8 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -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"); diff --git a/tests/crud/updates.rs b/tests/crud/updates.rs index 505e4837..83b9a5d3 100644 --- a/tests/crud/updates.rs +++ b/tests/crud/updates.rs @@ -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::find_by_id(customer_id.clone().unwrap() as i64) - .one(db) - .await - .expect("could not find customer"); + let customer: Option = Customer::find_by_id(customer_id.clone().unwrap()) + .one(db) + .await + .expect("could not find customer"); assert_eq!(customer, None); } diff --git a/tests/crud_tests.rs b/tests/crud_tests.rs index 3c26ddfd..7edb3500 100644 --- a/tests/crud_tests.rs +++ b/tests/crud_tests.rs @@ -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; } diff --git a/tests/primary_key_tests.rs b/tests/primary_key_tests.rs new file mode 100644 index 00000000..ea8255e9 --- /dev/null +++ b/tests/primary_key_tests.rs @@ -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(()) +} diff --git a/tests/query_tests.rs b/tests/query_tests.rs index 4e905686..2b5a2295 100644 --- a/tests/query_tests.rs +++ b/tests/query_tests.rs @@ -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 diff --git a/tests/relational_tests.rs b/tests/relational_tests.rs index ae1236a2..caef9c3c 100644 --- a/tests/relational_tests.rs +++ b/tests/relational_tests.rs @@ -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)> = 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(()) +} diff --git a/tests/sequential_op_tests.rs b/tests/sequential_op_tests.rs index a854c768..28333d84 100644 --- a/tests/sequential_op_tests.rs +++ b/tests/sequential_op_tests.rs @@ -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 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 Option