diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 17819f23..fbeeb88e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -126,7 +126,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - runtime: [async-std, actix, tokio] + runtime: [async-std] tls: [native-tls, rustls] steps: - uses: actions/checkout@v2 @@ -165,7 +165,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - runtime: [async-std, actix, tokio] + runtime: [actix] tls: [native-tls, rustls] steps: - uses: actions/checkout@v2 @@ -204,7 +204,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - runtime: [async-std, actix, tokio] + runtime: [tokio] tls: [native-tls, rustls] steps: - uses: actions/checkout@v2 @@ -293,7 +293,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - path: [basic, actix_example, actix4_example, axum_example, rocket_example] + path: [basic, actix_example, actix4_example, axum_example, rocket_example, poem_example] steps: - uses: actions/checkout@v2 @@ -356,7 +356,7 @@ jobs: strategy: fail-fast: false matrix: - runtime: [async-std, actix, tokio] + runtime: [async-std] tls: [native-tls, rustls] steps: - uses: actions/checkout@v2 @@ -400,7 +400,7 @@ jobs: fail-fast: false matrix: version: [8.0, 5.7] - runtime: [async-std, actix, tokio] + runtime: [actix] tls: [native-tls] services: mysql: @@ -461,7 +461,7 @@ jobs: fail-fast: false matrix: version: [10.6, 10.5, 10.4] - runtime: [async-std, actix, tokio] + runtime: [actix] tls: [native-tls] services: mysql: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c856cce..c5407faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,40 @@ 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.6.0 - 2022-02-07 + +### New Features +* Migration Support by @billy1624 in https://github.com/SeaQL/sea-orm/pull/335 +* Support `DateTime` & `DateTime` by @billy1624 in https://github.com/SeaQL/sea-orm/pull/489 +* Add `max_lifetime` connection option by @billy1624 in https://github.com/SeaQL/sea-orm/pull/493 + +### Enhancements +* Model with Generics by @billy1624 in https://github.com/SeaQL/sea-orm/pull/400 +* Add Poem example by @sunli829 in https://github.com/SeaQL/sea-orm/pull/446 +* Codegen `column_name` proc_macro attribute by @billy1624 in https://github.com/SeaQL/sea-orm/pull/433 +* Easy joins with MockDatabase #447 by @cemoktra in https://github.com/SeaQL/sea-orm/pull/455 + +### Bug Fixes +* CLI allow generate entity with url without password by @billy1624 in https://github.com/SeaQL/sea-orm/pull/436 +* Support up to 6-ary composite primary key by @billy1624 in https://github.com/SeaQL/sea-orm/pull/423 +* Fix FromQueryResult when Result is redefined by @tasn in https://github.com/SeaQL/sea-orm/pull/495 +* Remove `r#` prefix when deriving `FromQueryResult` by @smrtrfszm in https://github.com/SeaQL/sea-orm/pull/494 + +### Breaking Changes +* Name conflict of foreign key constraints when two entities have more than one foreign keys by @billy1624 in https://github.com/SeaQL/sea-orm/pull/417 + +### Fixed Issues +* Is it possible to have 4 values Composite Key? https://github.com/SeaQL/sea-orm/issues/352 +* Support `DateTime` & `DateTime` https://github.com/SeaQL/sea-orm/issues/381 +* Codegen `column_name` proc_macro attribute if column name isn't in snake case https://github.com/SeaQL/sea-orm/issues/395 +* Model with Generics https://github.com/SeaQL/sea-orm/issues/402 +* Foreign key constraint collision when multiple keys exist between the same two tables https://github.com/SeaQL/sea-orm/issues/405 +* sea-orm-cli passwordless database user causes "No password was found in the database url" error https://github.com/SeaQL/sea-orm/issues/435 +* Testing joins with MockDatabase https://github.com/SeaQL/sea-orm/issues/447 +* Surface max_lifetime connection option https://github.com/SeaQL/sea-orm/issues/475 + +**Full Changelog**: https://github.com/SeaQL/sea-orm/compare/0.5.0...0.6.0 + ## 0.5.0 - 2022-01-01 ### Fixed Issues diff --git a/COMMUNITY.md b/COMMUNITY.md index 1f2c3b0b..d578dee7 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -4,4 +4,24 @@ If you have built an app using SeaORM and want to showcase it, feel free to open a PR and add it to the list below! -[Caido](https://caido.io/) | A lightweight web security auditing toolkit \ No newline at end of file +### Startups + +- [Caido](https://caido.io/) | A lightweight web security auditing toolkit +- [Svix](https://www.svix.com/) ([repository](https://github.com/svix/svix-webhooks)) | The enterprise ready webhooks service + +### Open Source Projects + +- [Wikijump](https://github.com/scpwiki/wikijump) ([repository](https://github.com/scpwiki/wikijump/tree/develop/deepwell)) | API service for Wikijump, a fork of Wikidot +- [aeroFans](https://github.com/naryand/aerofans) | Full stack forum-like social media platform in Rust and WebAssembly +- [thrpg](https://github.com/thrpg/thrpg) | Touhou Project's secondary creative games +- [Adta](https://github.com/aaronleopold/adta) | Adta is **A**nother **D**amn **T**odo **A**pp, fun little side project +- [Axum-Book-Management](https://github.com/lz1998/axum-book-management) | CRUD system of book-management with ORM and JWT for educational purposes +- [mediarepo](https://mediarepo.trivernis.dev) ([repository](https://github.com/Trivernis/mediarepo)) | A tag-based media management application +- [THUBurrow](https://thuburrow.com) ([repository](https://github.com/BobAnkh/THUBurrow)) | A campus forum built by Next.js and Rocket +- [Backpack](https://github.com/JSH32/Backpack) | Open source self hosted file sharing platform on crack +- [Stump](https://github.com/aaronleopold/stump) | A free and open source comics server with OPDS support +- [awto](https://github.com/awto-rs/awto) | Awtomate your πŸ¦€ microservices with awto +- [tardis](https://github.com/ideal-world/tardis) | Elegant, Clean Rust development frameworkπŸ›Έ +- [mugen](https://github.com/koopa1338/mugen-dms) | DMS written in πŸ¦€ +- [JinShu](https://github.com/gengteng/jinshu) | A cross-platform **I**nstant **M**essaging system written in πŸ¦€ +- [rust-juniper-playground](https://github.com/Yama-Tomo/rust-juniper-playground) | juniper with SeaORM example diff --git a/Cargo.toml b/Cargo.toml index 0665ae23..90313632 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "sea-orm-macros", "sea-orm-codegen"] [package] name = "sea-orm" -version = "0.5.0" +version = "0.6.0" authors = ["Chris Tsang "] edition = "2021" description = "🐚 An async & dynamic ORM for Rust" @@ -14,7 +14,7 @@ categories = ["database"] keywords = ["async", "orm", "mysql", "postgres", "sqlite"] [package.metadata.docs.rs] -features = ["default", "sqlx-all", "runtime-async-std-native-tls"] +features = ["default", "sqlx-all", "mock", "runtime-async-std-native-tls"] rustdoc-args = ["--cfg", "docsrs"] [lib] @@ -29,8 +29,8 @@ futures = { version = "^0.3" } futures-util = { version = "^0.3" } tracing = { version = "0.1", features = ["log"] } rust_decimal = { version = "^1", optional = true } -sea-orm-macros = { version = "^0.5.0", path = "sea-orm-macros", optional = true } -sea-query = { version = "^0.20.0", features = ["thread-safe"] } +sea-orm-macros = { version = "^0.6.0", path = "sea-orm-macros", optional = true } +sea-query = { version = "^0.21.0", features = ["thread-safe"] } sea-strum = { version = "^0.23", features = ["derive", "sea-orm"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1", optional = true } @@ -43,20 +43,19 @@ once_cell = "1.8" [dev-dependencies] smol = { version = "^1.2" } smol-potat = { version = "^1.1" } -async-std = { version = "^1.9", features = ["attributes"] } +async-std = { version = "^1.9", features = ["attributes", "tokio1"] } tokio = { version = "^1.6", features = ["full"] } actix-rt = { version = "2.2.0" } maplit = { version = "^1" } rust_decimal_macros = { version = "^1" } tracing-subscriber = { version = "0.3", features = ["env-filter"] } -sea-orm = { path = ".", features = ["debug-print"] } +sea-orm = { path = ".", features = ["mock", "debug-print"] } pretty_assertions = { version = "^0.7" } [features] debug-print = [] default = [ "macros", - "mock", "with-json", "with-chrono", "with-rust_decimal", diff --git a/README.md b/README.md index a182bb0b..19430c9f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ ## Getting Started +[![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/SeaQL/sea-orm/stargazers/) +If you like what we do, consider starring, commenting, sharing and contributing! + [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) Join our Discord server to chat with others in the SeaQL community! @@ -26,6 +29,7 @@ Join our Discord server to chat with others in the SeaQL community! + [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) + [Actix Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) + [Axum Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) ++ [Poem Example](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) ## Features diff --git a/build-tools/cargo-publish.sh b/build-tools/cargo-publish.sh index 1cb61c94..656c9cce 100644 --- a/build-tools/cargo-publish.sh +++ b/build-tools/cargo-publish.sh @@ -10,7 +10,7 @@ cd sea-orm-cli sed -i 's/^version.*$/'"version = \"$1\"/" Cargo.toml sed -i 's/^sea-orm-codegen [^,]*,/sea-orm-codegen = { version = "\^'$1'",/' Cargo.toml git commit -am "sea-orm-cli $1" -cargo publish +# cargo publish cd .. sleep 30 cd sea-orm-macros diff --git a/examples/actix4_example/.env b/examples/actix4_example/.env index d89d9e63..010295bc 100644 --- a/examples/actix4_example/.env +++ b/examples/actix4_example/.env @@ -1,3 +1,3 @@ HOST=127.0.0.1 PORT=8000 -DATABASE_URL="mysql://root:@localhost/actix_example" +DATABASE_URL="mysql://root:root@localhost/actix_example" diff --git a/examples/actix4_example/Cargo.toml b/examples/actix4_example/Cargo.toml index 711b437f..bbb75c96 100644 --- a/examples/actix4_example/Cargo.toml +++ b/examples/actix4_example/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" publish = false [workspace] +members = [".", "entity", "migration"] [dependencies] actix-files = "0.6.0-beta.4" @@ -19,14 +20,5 @@ dotenv = "0.15" listenfd = "0.3.3" serde = "1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.5.0" -features = ["macros", "runtime-actix-native-tls", "debug-print"] -default-features = false - -[features] -default = ["sqlx-mysql"] -sqlx-mysql = ["sea-orm/sqlx-mysql"] -sqlx-postgres = ["sea-orm/sqlx-postgres"] +entity = { path = "entity" } +migration = { path = "migration" } diff --git a/examples/actix4_example/README.md b/examples/actix4_example/README.md index 9eb73ea6..f754d132 100644 --- a/examples/actix4_example/README.md +++ b/examples/actix4_example/README.md @@ -1,13 +1,14 @@ +![screenshot](Screenshot.png) + # Actix 4 Beta with SeaORM example app -Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`. +1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -```toml -[features] -default = ["sqlx-$DATABASE"] -``` +1. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-mysql",` line) -Edit `.env` to point to your database. +1. Execute `cargo run` to start the server + +1. Visit [localhost:8000](http://localhost:8000) in browser Run server with auto-reloading: diff --git a/examples/actix4_example/Screenshot.png b/examples/actix4_example/Screenshot.png new file mode 100644 index 00000000..8846f70f Binary files /dev/null and b/examples/actix4_example/Screenshot.png differ diff --git a/examples/actix4_example/entity/Cargo.toml b/examples/actix4_example/entity/Cargo.toml new file mode 100644 index 00000000..90659cdc --- /dev/null +++ b/examples/actix4_example/entity/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dependencies.sea-orm] +# path = "../../../" # remove this line in your own project +version = "^0.6.0" +features = [ + "macros", + "debug-print", + "runtime-actix-native-tls", + "sqlx-mysql", + # "sqlx-postgres", + # "sqlx-sqlite", +] +default-features = false \ No newline at end of file diff --git a/examples/actix4_example/entity/src/lib.rs b/examples/actix4_example/entity/src/lib.rs new file mode 100644 index 00000000..263f05b4 --- /dev/null +++ b/examples/actix4_example/entity/src/lib.rs @@ -0,0 +1,3 @@ +pub mod post; + +pub use sea_orm; diff --git a/examples/actix4_example/src/post.rs b/examples/actix4_example/entity/src/post.rs similarity index 100% rename from examples/actix4_example/src/post.rs rename to examples/actix4_example/entity/src/post.rs diff --git a/examples/actix4_example/migration/Cargo.toml b/examples/actix4_example/migration/Cargo.toml new file mode 100644 index 00000000..9a7c338f --- /dev/null +++ b/examples/actix4_example/migration/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.5.0", default-features = false, features = [ "migration", "debug-print" ] } +entity = { path = "../entity" } diff --git a/examples/actix4_example/migration/README.md b/examples/actix4_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/actix4_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/actix4_example/migration/src/lib.rs b/examples/actix4_example/migration/src/lib.rs new file mode 100644 index 00000000..3679d81f --- /dev/null +++ b/examples/actix4_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/actix4_example/migration/src/m20220120_000001_create_post_table.rs b/examples/actix4_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 00000000..0fe872c4 --- /dev/null +++ b/examples/actix4_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,39 @@ +use entity::post::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220120_000001_create_post_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Column::Title).string().not_null()) + .col(ColumnDef::new(Column::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/examples/actix4_example/migration/src/main.rs b/examples/actix4_example/migration/src/main.rs new file mode 100644 index 00000000..7e5e996d --- /dev/null +++ b/examples/actix4_example/migration/src/main.rs @@ -0,0 +1,7 @@ +use migration::Migrator; +use sea_schema::migration::*; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/examples/actix4_example/src/main.rs b/examples/actix4_example/src/main.rs index 43ec4336..201d22d9 100644 --- a/examples/actix4_example/src/main.rs +++ b/examples/actix4_example/src/main.rs @@ -3,17 +3,17 @@ use actix_web::{ error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, }; +use entity::post; +use entity::post::Entity as Post; +use entity::sea_orm; use listenfd::ListenFd; +use migration::{Migrator, MigratorTrait}; use sea_orm::DatabaseConnection; use sea_orm::{entity::*, query::*}; use serde::{Deserialize, Serialize}; use std::env; use tera::Tera; -mod post; -pub use post::Entity as Post; -mod setup; - const DEFAULT_POSTS_PER_PAGE: usize = 5; #[derive(Debug, Clone)] @@ -92,7 +92,9 @@ async fn create( .await .expect("could not insert post"); - Ok(HttpResponse::Found().append_header(("location", "/")).finish()) + Ok(HttpResponse::Found() + .append_header(("location", "/")) + .finish()) } #[get("/{id}")] @@ -133,7 +135,9 @@ async fn update( .await .expect("could not edit post"); - Ok(HttpResponse::Found().append_header(("location", "/")).finish()) + Ok(HttpResponse::Found() + .append_header(("location", "/")) + .finish()) } #[post("/delete/{id}")] @@ -149,7 +153,9 @@ async fn delete(data: web::Data, id: web::Path) -> Result std::io::Result<()> { // create post table if not exists let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - let _ = setup::create_post_table(&conn).await; + Migrator::up(&conn, None).await.unwrap(); let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); let state = AppState { templates, conn }; diff --git a/examples/actix4_example/src/setup.rs b/examples/actix4_example/src/setup.rs deleted file mode 100644 index 04677af4..00000000 --- a/examples/actix4_example/src/setup.rs +++ /dev/null @@ -1,33 +0,0 @@ -use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; -use sea_orm::{error::*, sea_query, ConnectionTrait, 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/actix_example/.env b/examples/actix_example/.env index 94ec74e3..1a12a183 100644 --- a/examples/actix_example/.env +++ b/examples/actix_example/.env @@ -1,3 +1,3 @@ HOST=127.0.0.1 PORT=8000 -DATABASE_URL="sql://root:@localhost/actix_example" \ No newline at end of file +DATABASE_URL="mysql://root:root@localhost/actix_example" \ No newline at end of file diff --git a/examples/actix_example/Cargo.toml b/examples/actix_example/Cargo.toml index 0849a4d4..11d2915a 100644 --- a/examples/actix_example/Cargo.toml +++ b/examples/actix_example/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" publish = false [workspace] +members = [".", "entity", "migration"] [dependencies] actix-http = "2" @@ -19,14 +20,5 @@ dotenv = "0.15" listenfd = "0.3.3" serde = "1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.5.0" -features = ["macros", "runtime-async-std-native-tls", "debug-print"] -default-features = false - -[features] -default = ["sqlx-mysql"] -sqlx-mysql = ["sea-orm/sqlx-mysql"] -sqlx-postgres = ["sea-orm/sqlx-postgres"] +entity = { path = "entity" } +migration = { path = "migration" } diff --git a/examples/actix_example/README.md b/examples/actix_example/README.md index d1ba90bf..71b4722a 100644 --- a/examples/actix_example/README.md +++ b/examples/actix_example/README.md @@ -1,13 +1,14 @@ +![screenshot](Screenshot.png) + # Actix with SeaORM example app -Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`. +1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -```toml -[features] -default = ["sqlx-$DATABASE"] -``` +1. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-mysql",` line) -Edit `.env` to point to your database. +1. Execute `cargo run` to start the server + +1. Visit [localhost:8000](http://localhost:8000) in browser Run server with auto-reloading: diff --git a/examples/actix_example/Screenshot.png b/examples/actix_example/Screenshot.png new file mode 100644 index 00000000..8846f70f Binary files /dev/null and b/examples/actix_example/Screenshot.png differ diff --git a/examples/actix_example/entity/Cargo.toml b/examples/actix_example/entity/Cargo.toml new file mode 100644 index 00000000..e70b7d6d --- /dev/null +++ b/examples/actix_example/entity/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dependencies.sea-orm] +# path = "../../../" # remove this line in your own project +version = "^0.6.0" +features = [ + "macros", + "debug-print", + "runtime-async-std-native-tls", + "sqlx-mysql", + # "sqlx-postgres", + # "sqlx-sqlite", +] +default-features = false \ No newline at end of file diff --git a/examples/actix_example/entity/src/lib.rs b/examples/actix_example/entity/src/lib.rs new file mode 100644 index 00000000..263f05b4 --- /dev/null +++ b/examples/actix_example/entity/src/lib.rs @@ -0,0 +1,3 @@ +pub mod post; + +pub use sea_orm; diff --git a/examples/actix_example/src/post.rs b/examples/actix_example/entity/src/post.rs similarity index 100% rename from examples/actix_example/src/post.rs rename to examples/actix_example/entity/src/post.rs diff --git a/examples/actix_example/migration/Cargo.toml b/examples/actix_example/migration/Cargo.toml new file mode 100644 index 00000000..9a7c338f --- /dev/null +++ b/examples/actix_example/migration/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.5.0", default-features = false, features = [ "migration", "debug-print" ] } +entity = { path = "../entity" } diff --git a/examples/actix_example/migration/README.md b/examples/actix_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/actix_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/actix_example/migration/src/lib.rs b/examples/actix_example/migration/src/lib.rs new file mode 100644 index 00000000..3679d81f --- /dev/null +++ b/examples/actix_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs b/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 00000000..0fe872c4 --- /dev/null +++ b/examples/actix_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,39 @@ +use entity::post::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220120_000001_create_post_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Column::Title).string().not_null()) + .col(ColumnDef::new(Column::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/examples/actix_example/migration/src/main.rs b/examples/actix_example/migration/src/main.rs new file mode 100644 index 00000000..7e5e996d --- /dev/null +++ b/examples/actix_example/migration/src/main.rs @@ -0,0 +1,7 @@ +use migration::Migrator; +use sea_schema::migration::*; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/examples/actix_example/src/main.rs b/examples/actix_example/src/main.rs index d532174f..b3f44a78 100644 --- a/examples/actix_example/src/main.rs +++ b/examples/actix_example/src/main.rs @@ -2,17 +2,18 @@ use actix_files as fs; use actix_web::{ error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, }; + +use entity::post; +use entity::post::Entity as Post; +use entity::sea_orm; use listenfd::ListenFd; +use migration::{Migrator, MigratorTrait}; use sea_orm::DatabaseConnection; use sea_orm::{entity::*, query::*}; use serde::{Deserialize, Serialize}; use std::env; use tera::Tera; -mod post; -pub use post::Entity as Post; -mod setup; - const DEFAULT_POSTS_PER_PAGE: usize = 5; #[derive(Debug, Clone)] @@ -192,7 +193,7 @@ async fn main() -> std::io::Result<()> { // create post table if not exists let conn = sea_orm::Database::connect(&db_url).await.unwrap(); - let _ = setup::create_post_table(&conn).await; + Migrator::up(&conn, None).await.unwrap(); let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); let state = AppState { templates, conn }; diff --git a/examples/actix_example/src/setup.rs b/examples/actix_example/src/setup.rs deleted file mode 100644 index 04677af4..00000000 --- a/examples/actix_example/src/setup.rs +++ /dev/null @@ -1,33 +0,0 @@ -use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; -use sea_orm::{error::*, sea_query, ConnectionTrait, 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/axum_example/.env b/examples/axum_example/.env index fb7fcfb5..375cab79 100644 --- a/examples/axum_example/.env +++ b/examples/axum_example/.env @@ -1,3 +1,3 @@ HOST=127.0.0.1 PORT=8000 -DATABASE_URL="postgres://postgres:password@localhost/axum_exmaple" \ No newline at end of file +DATABASE_URL="postgres://root:root@localhost/axum_exmaple" \ No newline at end of file diff --git a/examples/axum_example/Cargo.toml b/examples/axum_example/Cargo.toml index 68248be2..3e81d995 100644 --- a/examples/axum_example/Cargo.toml +++ b/examples/axum_example/Cargo.toml @@ -7,6 +7,7 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] +members = [".", "entity", "migration"] [dependencies] tokio = { version = "1.14", features = ["full"] } @@ -20,14 +21,5 @@ serde = "1" serde_json = "1" tera = "1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.5.0" -features = ["macros", "runtime-tokio-native-tls", "debug-print"] -default-features = false - -[features] -default = ["sqlx-postgres"] -sqlx-mysql = ["sea-orm/sqlx-mysql"] -sqlx-postgres = ["sea-orm/sqlx-postgres"] +entity = { path = "entity" } +migration = { path = "migration" } diff --git a/examples/axum_example/README.md b/examples/axum_example/README.md index 3b04a53e..8ed1e789 100644 --- a/examples/axum_example/README.md +++ b/examples/axum_example/README.md @@ -1,10 +1,11 @@ +![screenshot](Screenshot.png) + # Axum with SeaORM example app -Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`. +1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database -```toml -[features] -default = ["sqlx-$DATABASE"] -``` +1. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-postgres",` line) -Edit `.env` to point to your database. \ No newline at end of file +1. Execute `cargo run` to start the server + +1. Visit [localhost:8000](http://localhost:8000) in browser diff --git a/examples/axum_example/Screenshot.png b/examples/axum_example/Screenshot.png new file mode 100644 index 00000000..478048ec Binary files /dev/null and b/examples/axum_example/Screenshot.png differ diff --git a/examples/axum_example/entity/Cargo.toml b/examples/axum_example/entity/Cargo.toml new file mode 100644 index 00000000..388df043 --- /dev/null +++ b/examples/axum_example/entity/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dependencies.sea-orm] +# path = "../../../" # remove this line in your own project +version = "^0.6.0" +features = [ + "macros", + "debug-print", + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] +default-features = false \ No newline at end of file diff --git a/examples/axum_example/entity/src/lib.rs b/examples/axum_example/entity/src/lib.rs new file mode 100644 index 00000000..263f05b4 --- /dev/null +++ b/examples/axum_example/entity/src/lib.rs @@ -0,0 +1,3 @@ +pub mod post; + +pub use sea_orm; diff --git a/examples/axum_example/src/post.rs b/examples/axum_example/entity/src/post.rs similarity index 100% rename from examples/axum_example/src/post.rs rename to examples/axum_example/entity/src/post.rs diff --git a/examples/axum_example/migration/Cargo.toml b/examples/axum_example/migration/Cargo.toml new file mode 100644 index 00000000..9a7c338f --- /dev/null +++ b/examples/axum_example/migration/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.5.0", default-features = false, features = [ "migration", "debug-print" ] } +entity = { path = "../entity" } diff --git a/examples/axum_example/migration/README.md b/examples/axum_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/axum_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/axum_example/migration/src/lib.rs b/examples/axum_example/migration/src/lib.rs new file mode 100644 index 00000000..3679d81f --- /dev/null +++ b/examples/axum_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs b/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 00000000..0fe872c4 --- /dev/null +++ b/examples/axum_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,39 @@ +use entity::post::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220120_000001_create_post_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Column::Title).string().not_null()) + .col(ColumnDef::new(Column::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/examples/axum_example/migration/src/main.rs b/examples/axum_example/migration/src/main.rs new file mode 100644 index 00000000..7e5e996d --- /dev/null +++ b/examples/axum_example/migration/src/main.rs @@ -0,0 +1,7 @@ +use migration::Migrator; +use sea_schema::migration::*; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/examples/axum_example/src/main.rs b/examples/axum_example/src/main.rs index c35e180a..35b07976 100644 --- a/examples/axum_example/src/main.rs +++ b/examples/axum_example/src/main.rs @@ -1,15 +1,16 @@ mod flash; -mod post; -mod setup; use axum::{ extract::{Extension, Form, Path, Query}, http::StatusCode, response::Html, - routing::{get, post, get_service}, + routing::{get, get_service, post}, AddExtensionLayer, Router, Server, }; +use entity::post; +use entity::sea_orm; use flash::{get_flash_cookie, post_response, PostResponse}; +use migration::{Migrator, MigratorTrait}; use post::Entity as Post; use sea_orm::{prelude::*, Database, QueryOrder, Set}; use serde::{Deserialize, Serialize}; @@ -34,7 +35,7 @@ async fn main() -> anyhow::Result<()> { let conn = Database::connect(db_url) .await .expect("Database connection failed"); - let _ = setup::create_post_table(&conn).await; + Migrator::up(&conn, None).await.unwrap(); let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")) .expect("Tera initialization failed"); // let state = AppState { templates, conn }; diff --git a/examples/axum_example/src/setup.rs b/examples/axum_example/src/setup.rs deleted file mode 100644 index 04677af4..00000000 --- a/examples/axum_example/src/setup.rs +++ /dev/null @@ -1,33 +0,0 @@ -use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; -use sea_orm::{error::*, sea_query, ConnectionTrait, 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/basic/Cargo.toml b/examples/basic/Cargo.toml index bd9f0a08..9c53df39 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" publish = false [dependencies] -async-std = { version = "^1.9", features = [ "attributes" ] } +async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] } sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-async-std-native-tls" ] } serde_json = { version = "^1" } futures = { version = "^0.3" } diff --git a/examples/poem_example/.env b/examples/poem_example/.env new file mode 100644 index 00000000..acd83c0e --- /dev/null +++ b/examples/poem_example/.env @@ -0,0 +1,4 @@ +HOST=127.0.0.1 +PORT=8000 +#DATABASE_URL="mysql://root:@localhost/poem_example" +DATABASE_URL="sqlite::memory:" diff --git a/examples/poem_example/Cargo.toml b/examples/poem_example/Cargo.toml new file mode 100644 index 00000000..7b109792 --- /dev/null +++ b/examples/poem_example/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "sea-orm-poem-example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = [".", "entity", "migration"] + +[dependencies] +tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } +poem = { version = "1.2.33", features = ["static-files"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +serde = { version = "1", features = ["derive"] } +tera = "1.8.0" +dotenv = "0.15" +entity = { path = "entity" } +migration = { path = "migration" } diff --git a/examples/poem_example/README.md b/examples/poem_example/README.md new file mode 100644 index 00000000..b9ec2d3e --- /dev/null +++ b/examples/poem_example/README.md @@ -0,0 +1,11 @@ +![screenshot](Screenshot.png) + +# Poem with SeaORM example app + +1. Modify the `DATABASE_URL` var in `.env` to point to your chosen database + +1. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-sqlite",` line) + +1. Execute `cargo run` to start the server + +1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `server started` line diff --git a/examples/poem_example/Screenshot.png b/examples/poem_example/Screenshot.png new file mode 100644 index 00000000..7ddc244a Binary files /dev/null and b/examples/poem_example/Screenshot.png differ diff --git a/examples/poem_example/entity/Cargo.toml b/examples/poem_example/entity/Cargo.toml new file mode 100644 index 00000000..7a6ac369 --- /dev/null +++ b/examples/poem_example/entity/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } + +[dependencies.sea-orm] +# path = "../../../" # remove this line in your own project +version = "^0.6.0" +features = [ + "macros", + "debug-print", + "runtime-tokio-native-tls", + "sqlx-sqlite", + # "sqlx-postgres", + # "sqlx-mysql", +] +default-features = false \ No newline at end of file diff --git a/examples/poem_example/entity/src/lib.rs b/examples/poem_example/entity/src/lib.rs new file mode 100644 index 00000000..263f05b4 --- /dev/null +++ b/examples/poem_example/entity/src/lib.rs @@ -0,0 +1,3 @@ +pub mod post; + +pub use sea_orm; diff --git a/examples/poem_example/entity/src/post.rs b/examples/poem_example/entity/src/post.rs new file mode 100644 index 00000000..37a99c2f --- /dev/null +++ b/examples/poem_example/entity/src/post.rs @@ -0,0 +1,18 @@ +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)] +#[sea_orm(table_name = "posts")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + pub id: i32, + pub title: String, + #[sea_orm(column_type = "Text")] + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/poem_example/migration/Cargo.toml b/examples/poem_example/migration/Cargo.toml new file mode 100644 index 00000000..9a7c338f --- /dev/null +++ b/examples/poem_example/migration/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.5.0", default-features = false, features = [ "migration", "debug-print" ] } +entity = { path = "../entity" } diff --git a/examples/poem_example/migration/README.md b/examples/poem_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/poem_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/poem_example/migration/src/lib.rs b/examples/poem_example/migration/src/lib.rs new file mode 100644 index 00000000..3679d81f --- /dev/null +++ b/examples/poem_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs b/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 00000000..0fe872c4 --- /dev/null +++ b/examples/poem_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,39 @@ +use entity::post::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220120_000001_create_post_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Column::Title).string().not_null()) + .col(ColumnDef::new(Column::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/examples/poem_example/migration/src/main.rs b/examples/poem_example/migration/src/main.rs new file mode 100644 index 00000000..7e5e996d --- /dev/null +++ b/examples/poem_example/migration/src/main.rs @@ -0,0 +1,7 @@ +use migration::Migrator; +use sea_schema::migration::*; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/examples/poem_example/src/main.rs b/examples/poem_example/src/main.rs new file mode 100644 index 00000000..f2a826ab --- /dev/null +++ b/examples/poem_example/src/main.rs @@ -0,0 +1,163 @@ +use std::env; + +use entity::post; +use entity::sea_orm; +use migration::{Migrator, MigratorTrait}; +use poem::endpoint::StaticFilesEndpoint; +use poem::error::{BadRequest, InternalServerError}; +use poem::http::StatusCode; +use poem::listener::TcpListener; +use poem::web::{Data, Form, Html, Path, Query}; +use poem::{get, handler, post, EndpointExt, Error, IntoResponse, Result, Route, Server}; +use sea_orm::{entity::*, query::*, DatabaseConnection}; +use serde::Deserialize; +use tera::Tera; + +const DEFAULT_POSTS_PER_PAGE: usize = 5; + +#[derive(Debug, Clone)] +struct AppState { + templates: tera::Tera, + conn: DatabaseConnection, +} + +#[derive(Deserialize)] +struct Params { + page: Option, + posts_per_page: Option, +} + +#[handler] +async fn create(state: Data<&AppState>, form: Form) -> Result { + post::ActiveModel { + title: Set(form.title.to_owned()), + text: Set(form.text.to_owned()), + ..Default::default() + } + .save(&state.conn) + .await + .map_err(InternalServerError)?; + + Ok(StatusCode::FOUND.with_header("location", "/")) +} + +#[handler] +async fn list(state: Data<&AppState>, Query(params): Query) -> Result { + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + let paginator = post::Entity::find() + .order_by_asc(post::Column::Id) + .paginate(&state.conn, posts_per_page); + let num_pages = paginator.num_pages().await.map_err(BadRequest)?; + let posts = paginator + .fetch_page(page - 1) + .await + .map_err(InternalServerError)?; + + let mut ctx = tera::Context::new(); + ctx.insert("posts", &posts); + ctx.insert("page", &page); + ctx.insert("posts_per_page", &posts_per_page); + ctx.insert("num_pages", &num_pages); + + let body = state + .templates + .render("index.html.tera", &ctx) + .map_err(InternalServerError)?; + Ok(Html(body)) +} + +#[handler] +async fn new(state: Data<&AppState>) -> Result { + let ctx = tera::Context::new(); + let body = state + .templates + .render("new.html.tera", &ctx) + .map_err(InternalServerError)?; + Ok(Html(body)) +} + +#[handler] +async fn edit(state: Data<&AppState>, Path(id): Path) -> Result { + let post: post::Model = post::Entity::find_by_id(id) + .one(&state.conn) + .await + .map_err(InternalServerError)? + .ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))?; + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = state + .templates + .render("edit.html.tera", &ctx) + .map_err(InternalServerError)?; + Ok(Html(body)) +} + +#[handler] +async fn update( + state: Data<&AppState>, + Path(id): Path, + form: Form, +) -> Result { + post::ActiveModel { + id: Set(id), + title: Set(form.title.to_owned()), + text: Set(form.text.to_owned()), + } + .save(&state.conn) + .await + .map_err(InternalServerError)?; + + Ok(StatusCode::FOUND.with_header("location", "/")) +} + +#[handler] +async fn delete(state: Data<&AppState>, Path(id): Path) -> Result { + let post: post::ActiveModel = post::Entity::find_by_id(id) + .one(&state.conn) + .await + .map_err(InternalServerError)? + .ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))? + .into(); + post.delete(&state.conn) + .await + .map_err(InternalServerError)?; + + Ok(StatusCode::FOUND.with_header("location", "/")) +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + std::env::set_var("RUST_LOG", "debug"); + tracing_subscriber::fmt::init(); + + // get env vars + dotenv::dotenv().ok(); + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); + let host = env::var("HOST").expect("HOST is not set in .env file"); + let port = env::var("PORT").expect("PORT is not set in .env file"); + let server_url = format!("{}:{}", host, port); + + // create post table if not exists + let conn = sea_orm::Database::connect(&db_url).await.unwrap(); + Migrator::up(&conn, None).await.unwrap(); + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); + let state = AppState { templates, conn }; + + println!("Starting server at {}", server_url); + + let app = Route::new() + .at("/", post(create).get(list)) + .at("/new", new) + .at("/:id", get(edit).post(update)) + .at("/delete/:id", post(delete)) + .nest( + "/static", + StaticFilesEndpoint::new(concat!(env!("CARGO_MANIFEST_DIR"), "/static")), + ) + .data(state); + let server = Server::new(TcpListener::bind(format!("{}:{}", host, port))); + server.run(app).await +} diff --git a/examples/poem_example/static/css/normalize.css b/examples/poem_example/static/css/normalize.css new file mode 100644 index 00000000..458eea1e --- /dev/null +++ b/examples/poem_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/poem_example/static/css/skeleton.css b/examples/poem_example/static/css/skeleton.css new file mode 100644 index 00000000..cdc432a4 --- /dev/null +++ b/examples/poem_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/poem_example/static/css/style.css b/examples/poem_example/static/css/style.css new file mode 100644 index 00000000..ac2720d3 --- /dev/null +++ b/examples/poem_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/poem_example/static/images/favicon.png b/examples/poem_example/static/images/favicon.png new file mode 100644 index 00000000..02b73904 Binary files /dev/null and b/examples/poem_example/static/images/favicon.png differ diff --git a/examples/poem_example/templates/edit.html.tera b/examples/poem_example/templates/edit.html.tera new file mode 100644 index 00000000..5aeb22af --- /dev/null +++ b/examples/poem_example/templates/edit.html.tera @@ -0,0 +1,49 @@ +{% extends "layout.html.tera" %} {% block content %} +
+

Edit Post

+
+
+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+{% endblock content %} diff --git a/examples/poem_example/templates/error/404.html.tera b/examples/poem_example/templates/error/404.html.tera new file mode 100644 index 00000000..afda653d --- /dev/null +++ b/examples/poem_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/poem_example/templates/index.html.tera b/examples/poem_example/templates/index.html.tera new file mode 100644 index 00000000..1dccdeba --- /dev/null +++ b/examples/poem_example/templates/index.html.tera @@ -0,0 +1,52 @@ +{% extends "layout.html.tera" %} {% block content %} +
+

+

Posts

+ {% if flash %} + + {{ flash.message }} + + {% endif %} + + + + + + + + + + {% for post in posts %} + + + + + + {% endfor %} + + + + + + + + +
IDTitleText
{{ post.id }}{{ post.title }}{{ post.text }}
+ {% if page == 1 %} Previous {% else %} + Previous + {% endif %} | {% if page == num_pages %} Next {% else %} + Next + {% endif %} +
+ +
+ + + +
+
+{% endblock content %} diff --git a/examples/poem_example/templates/layout.html.tera b/examples/poem_example/templates/layout.html.tera new file mode 100644 index 00000000..50ae5885 --- /dev/null +++ b/examples/poem_example/templates/layout.html.tera @@ -0,0 +1,26 @@ + + + + + Poem Example + + + + + + + + + + + +
+

+ {% block content %}{% endblock content %} +
+ + diff --git a/examples/poem_example/templates/new.html.tera b/examples/poem_example/templates/new.html.tera new file mode 100644 index 00000000..dee19565 --- /dev/null +++ b/examples/poem_example/templates/new.html.tera @@ -0,0 +1,38 @@ +{% extends "layout.html.tera" %} {% block content %} +
+

New Post

+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+{% endblock content %} diff --git a/examples/rocket_example/Cargo.toml b/examples/rocket_example/Cargo.toml index 36eac643..7badb27c 100644 --- a/examples/rocket_example/Cargo.toml +++ b/examples/rocket_example/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" publish = false [workspace] +members = [".", "entity", "migration"] [dependencies] async-stream = { version = "^0.3" } @@ -19,18 +20,9 @@ rocket_dyn_templates = { version = "0.1.0-rc.1", features = [ "tera", ] } serde_json = { version = "^1" } - -[dependencies.sea-orm] -path = "../../" # remove this line in your own project -version = "^0.5.0" -features = ["macros", "runtime-tokio-native-tls"] -default-features = false +entity = { path = "entity" } +migration = { path = "migration" } [dependencies.sea-orm-rocket] path = "../../sea-orm-rocket/lib" # remove this line in your own project and use the git line # git = "https://github.com/SeaQL/sea-orm" - -[features] -default = ["sqlx-postgres"] -sqlx-mysql = ["sea-orm/sqlx-mysql"] -sqlx-postgres = ["sea-orm/sqlx-postgres"] diff --git a/examples/rocket_example/README.md b/examples/rocket_example/README.md index 596a763a..6ef30ea6 100644 --- a/examples/rocket_example/README.md +++ b/examples/rocket_example/README.md @@ -4,8 +4,8 @@ 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. Turn on the appropriate database feature for your chosen db in `entity/Cargo.toml` (the `"sqlx-postgres",` line) -1. `cargo run` to start the server +1. Execute `cargo run` to start the server -1. Open in browser after seeing the `πŸš€ Rocket has launched from http://localhost:8000` line +1. Visit [localhost:8000](http://localhost:8000) in browser after seeing the `πŸš€ Rocket has launched from http://localhost:8000` line diff --git a/examples/rocket_example/entity/Cargo.toml b/examples/rocket_example/entity/Cargo.toml new file mode 100644 index 00000000..0f0fbcce --- /dev/null +++ b/examples/rocket_example/entity/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "entity" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "entity" +path = "src/lib.rs" + +[dependencies] +rocket = { version = "0.5.0-rc.1", features = [ + "json", +] } + +[dependencies.sea-orm] +# path = "../../../" # remove this line in your own project +version = "^0.6.0" +features = [ + "macros", + "runtime-tokio-native-tls", + "sqlx-postgres", + # "sqlx-mysql", + # "sqlx-sqlite", +] +default-features = false \ No newline at end of file diff --git a/examples/rocket_example/entity/src/lib.rs b/examples/rocket_example/entity/src/lib.rs new file mode 100644 index 00000000..e15a5ab1 --- /dev/null +++ b/examples/rocket_example/entity/src/lib.rs @@ -0,0 +1,6 @@ +#[macro_use] +extern crate rocket; + +pub mod post; + +pub use sea_orm; diff --git a/examples/rocket_example/src/post.rs b/examples/rocket_example/entity/src/post.rs similarity index 100% rename from examples/rocket_example/src/post.rs rename to examples/rocket_example/entity/src/post.rs diff --git a/examples/rocket_example/migration/Cargo.toml b/examples/rocket_example/migration/Cargo.toml new file mode 100644 index 00000000..2d718c29 --- /dev/null +++ b/examples/rocket_example/migration/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.5.0", default-features = false, features = [ "migration", "debug-print" ] } +entity = { path = "../entity" } +rocket = { version = "0.5.0-rc.1" } diff --git a/examples/rocket_example/migration/README.md b/examples/rocket_example/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/examples/rocket_example/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/examples/rocket_example/migration/src/lib.rs b/examples/rocket_example/migration/src/lib.rs new file mode 100644 index 00000000..3679d81f --- /dev/null +++ b/examples/rocket_example/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220120_000001_create_post_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220120_000001_create_post_table::Migration)] + } +} diff --git a/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs b/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs new file mode 100644 index 00000000..0fe872c4 --- /dev/null +++ b/examples/rocket_example/migration/src/m20220120_000001_create_post_table.rs @@ -0,0 +1,39 @@ +use entity::post::*; +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220120_000001_create_post_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Entity) + .if_not_exists() + .col( + ColumnDef::new(Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(Column::Title).string().not_null()) + .col(ColumnDef::new(Column::Text).string().not_null()) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(Entity).to_owned()) + .await + } +} diff --git a/examples/rocket_example/migration/src/main.rs b/examples/rocket_example/migration/src/main.rs new file mode 100644 index 00000000..e47018eb --- /dev/null +++ b/examples/rocket_example/migration/src/main.rs @@ -0,0 +1,18 @@ +use migration::Migrator; +use sea_schema::migration::prelude::*; + +#[async_std::main] +async fn main() { + // Setting `DATABASE_URL` environment variable + let key = "DATABASE_URL"; + if std::env::var(key).is_err() { + // Getting the database URL from Rocket.toml if it's not set + let figment = rocket::Config::figment(); + let database_url: String = figment + .extract_inner("databases.sea_orm.url") + .expect("Cannot find Database URL in Rocket.toml"); + std::env::set_var(key, database_url); + } + + cli::run_cli(Migrator).await; +} diff --git a/examples/rocket_example/src/main.rs b/examples/rocket_example/src/main.rs index 5ec1e52f..13944e26 100644 --- a/examples/rocket_example/src/main.rs +++ b/examples/rocket_example/src/main.rs @@ -10,16 +10,16 @@ use rocket::{Build, Request, Rocket}; use rocket_dyn_templates::Template; use serde_json::json; +use entity::sea_orm; +use migration::MigratorTrait; use sea_orm::{entity::*, query::*}; use sea_orm_rocket::{Connection, Database}; mod pool; use pool::Db; -mod setup; - -mod post; -pub use post::Entity as Post; +pub use entity::post; +pub use entity::post::Entity as Post; const DEFAULT_POSTS_PER_PAGE: usize = 5; @@ -114,7 +114,7 @@ async fn list( "num_pages": num_pages, "posts": posts, "flash": flash.map(FlashMessage::into_inner), - }) + }), ) } @@ -131,7 +131,7 @@ async fn edit(conn: Connection<'_, Db>, id: i32) -> Template { "edit", json! ({ "post": post, - }) + }), ) } @@ -160,13 +160,13 @@ pub fn not_found(req: &Request<'_>) -> Template { "error/404", json! ({ "uri": req.uri() - }) + }), ) } async fn run_migrations(rocket: Rocket) -> fairing::Result { let conn = &Db::fetch(&rocket).unwrap().conn; - let _ = setup::create_post_table(conn).await; + let _ = migration::Migrator::up(conn, None).await; Ok(rocket) } diff --git a/examples/rocket_example/src/pool.rs b/examples/rocket_example/src/pool.rs index fe5f8c6e..adf5afb7 100644 --- a/examples/rocket_example/src/pool.rs +++ b/examples/rocket_example/src/pool.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use entity::sea_orm; use sea_orm::ConnectOptions; use sea_orm_rocket::{rocket::figment::Figment, Config, Database}; use std::time::Duration; diff --git a/examples/rocket_example/src/setup.rs b/examples/rocket_example/src/setup.rs deleted file mode 100644 index f5b5a99e..00000000 --- a/examples/rocket_example/src/setup.rs +++ /dev/null @@ -1,33 +0,0 @@ -use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; -use sea_orm::{error::*, query::*, 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/issues/262/Cargo.toml b/issues/262/Cargo.toml index f6fad508..b397fb8a 100644 --- a/issues/262/Cargo.toml +++ b/issues/262/Cargo.toml @@ -9,4 +9,4 @@ publish = false [dependencies] sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-async-std-native-tls", "debug-print" ] } -async-std = { version = "^1", features = ["attributes"] } +async-std = { version = "^1", features = ["attributes", "tokio1"] } diff --git a/issues/319/Cargo.toml b/issues/319/Cargo.toml index 3eb4adaa..b4e054c8 100644 --- a/issues/319/Cargo.toml +++ b/issues/319/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" publish = false [dependencies] -async-std = { version = "^1", features = ["attributes"] } +async-std = { version = "^1", features = ["attributes", "tokio1"] } serde = { version = "^1", features = ["derive"] } sea-orm = { path = "../../", features = [ "sqlx-mysql", diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 4c1dbb9c..3e0a96e9 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "sea-orm-cli" -version = "0.5.0" +version = "0.6.0" authors = [ "Billy Chan " ] edition = "2021" description = "Command line utility for SeaORM" @@ -20,15 +20,16 @@ path = "src/main.rs" [dependencies] clap = { version = "^2.33.3" } dotenv = { version = "^0.15" } -async-std = { version = "^1.9", features = [ "attributes" ] } -sea-orm-codegen = { version = "^0.5.0", path = "../sea-orm-codegen" } -sea-schema = { version = "0.4.0", default-features = false, features = [ +async-std = { version = "^1.9", features = [ "attributes", "tokio1" ] } +sea-orm-codegen = { version = "^0.6.0", path = "../sea-orm-codegen" } +sea-schema = { version = "^0.5.0", default-features = false, features = [ "debug-print", "sqlx-mysql", "sqlx-sqlite", "sqlx-postgres", "discovery", "writer", + "migration", ] } sqlx = { version = "^0.5", default-features = false, features = [ "mysql", "postgres" ] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/sea-orm-cli/README.md b/sea-orm-cli/README.md index bcf49100..af7f1dd3 100644 --- a/sea-orm-cli/README.md +++ b/sea-orm-cli/README.md @@ -6,7 +6,7 @@ Getting Help: cargo run -- -h ``` -Running Entity Generator: +## Running Entity Generator: ```sh # MySQL (`--database-schema` option is ignored) @@ -18,3 +18,46 @@ cargo run -- generate entity -u sqlite://bakery.db -o out # PostgreSQL cargo run -- generate entity -u postgres://sea:sea@localhost/bakery -s public -o out ``` + +## Running Migration: + +- Initialize migration directory + ```sh + cargo run -- migrate init + ``` +- Apply all pending migrations + ```sh + cargo run -- migrate + ``` + ```sh + cargo run -- migrate up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- migrate up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- migrate down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- migrate down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- migrate fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- migrate refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- migrate reset + ``` +- Check the status of all migrations + ```sh + cargo run -- migrate status + ``` + diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index f0d12b7b..ee81f7b5 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -74,10 +74,30 @@ pub fn build_cli() -> App<'static, 'static> { ) .setting(AppSettings::SubcommandRequiredElseHelp); - App::new("sea-orm") + let arg_migration_dir = Arg::with_name("MIGRATION_DIR") + .long("migration-dir") + .short("d") + .help("Migration script directory") + .takes_value(true) + .default_value("./migration"); + let mut migrate_subcommands = SubCommand::with_name("migrate") + .about("Migration related commands") + .subcommand( + SubCommand::with_name("init") + .about("Initialize migration directory") + .arg(arg_migration_dir.clone()), + ) + .arg(arg_migration_dir.clone()); + for subcommand in sea_schema::migration::get_subcommands() { + migrate_subcommands = + migrate_subcommands.subcommand(subcommand.arg(arg_migration_dir.clone())); + } + + App::new("sea-orm-cli") .version(env!("CARGO_PKG_VERSION")) .setting(AppSettings::VersionlessSubcommands) .subcommand(entity_subcommand) + .subcommand(migrate_subcommands) .arg( Arg::with_name("VERBOSE") .long("verbose") diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index 8cf9134a..23d505ec 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -16,6 +16,7 @@ async fn main() { ("generate", Some(matches)) => run_generate_command(matches) .await .unwrap_or_else(handle_error), + ("migrate", Some(matches)) => run_migrate_command(matches).unwrap_or_else(handle_error), _ => unreachable!("You should never see this message"), } } @@ -187,6 +188,81 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box) -> Result<(), Box> { + let migrate_subcommand = matches.subcommand(); + // If it's `migrate init` + if let ("init", Some(args)) = migrate_subcommand { + let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); + let migration_dir = match migration_dir.ends_with('/') { + true => migration_dir.to_string(), + false => format!("{}/", migration_dir), + }; + println!("Initializing migration directory..."); + macro_rules! write_file { + ($filename: literal) => { + write_file!($filename, $filename); + }; + ($filename: literal, $template: literal) => { + let filepath = [&migration_dir, $filename].join(""); + println!("Creating file `{}`", filepath); + let path = Path::new(&filepath); + let prefix = path.parent().unwrap(); + fs::create_dir_all(prefix).unwrap(); + let mut file = fs::File::create(path)?; + let content = include_str!(concat!("../template/migration/", $template)); + file.write_all(content.as_bytes())?; + }; + } + write_file!("src/lib.rs"); + write_file!("src/m20220101_000001_create_table.rs"); + write_file!("src/main.rs"); + write_file!("Cargo.toml", "_Cargo.toml"); + write_file!("README.md"); + println!("Done!"); + // Early exit! + return Ok(()); + } + let (subcommand, migration_dir, steps, verbose) = match migrate_subcommand { + // Catch all command with pattern `migrate xxx` + (subcommand, Some(args)) => { + let migration_dir = args.value_of("MIGRATION_DIR").unwrap(); + let steps = args.value_of("NUM_MIGRATION"); + let verbose = args.is_present("VERBOSE"); + (subcommand, migration_dir, steps, verbose) + } + // Catch command `migrate`, this will be treated as `migrate up` + _ => { + let migration_dir = matches.value_of("MIGRATION_DIR").unwrap(); + let verbose = matches.is_present("VERBOSE"); + ("up", migration_dir, None, verbose) + } + }; + // Construct the `--manifest-path` + let manifest_path = if migration_dir.ends_with('/') { + format!("{}Cargo.toml", migration_dir) + } else { + format!("{}/Cargo.toml", migration_dir) + }; + // Construct the arguments that will be supplied to `cargo` command + let mut args = vec![ + "run", + "--manifest-path", + manifest_path.as_str(), + "--", + subcommand, + ]; + if let Some(steps) = steps { + args.extend(["-n", steps]); + } + if verbose { + args.push("-v"); + } + // Run migrator CLI on user's behalf + println!("Running `cargo {}`", args.join(" ")); + Command::new("cargo").args(args).spawn()?.wait()?; + Ok(()) +} + fn handle_error(error: E) where E: Display, @@ -197,11 +273,13 @@ where #[cfg(test)] mod tests { - use clap::AppSettings; use super::*; + use clap::AppSettings; #[test] - #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase")] + #[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: RelativeUrlWithoutBase" + )] fn test_generate_entity_no_protocol() { let matches = cli::build_cli() .setting(AppSettings::NoBinaryName) @@ -216,7 +294,9 @@ mod tests { } #[test] - #[should_panic(expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306")] + #[should_panic( + expected = "There is no database name as part of the url path: postgresql://root:root@localhost:3306" + )] fn test_generate_entity_no_database_section() { let matches = cli::build_cli() .setting(AppSettings::NoBinaryName) @@ -231,7 +311,9 @@ mod tests { } #[test] - #[should_panic(expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/")] + #[should_panic( + expected = "There is no database name as part of the url path: mysql://root:root@localhost:3306/" + )] fn test_generate_entity_no_database_path() { let matches = cli::build_cli() .setting(AppSettings::NoBinaryName) diff --git a/sea-orm-cli/template/migration/README.md b/sea-orm-cli/template/migration/README.md new file mode 100644 index 00000000..963caaeb --- /dev/null +++ b/sea-orm-cli/template/migration/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/sea-orm-cli/template/migration/_Cargo.toml b/sea-orm-cli/template/migration/_Cargo.toml new file mode 100644 index 00000000..7a71863d --- /dev/null +++ b/sea-orm-cli/template/migration/_Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "migration" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" + +[dependencies] +sea-schema = { version = "^0.5.0", default-features = false, features = [ "migration", "debug-print" ] } diff --git a/sea-orm-cli/template/migration/src/lib.rs b/sea-orm-cli/template/migration/src/lib.rs new file mode 100644 index 00000000..3cabe22c --- /dev/null +++ b/sea-orm-cli/template/migration/src/lib.rs @@ -0,0 +1,12 @@ +pub use sea_schema::migration::prelude::*; + +mod m20220101_000001_create_table; + +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![Box::new(m20220101_000001_create_table::Migration)] + } +} diff --git a/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs b/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs new file mode 100644 index 00000000..a5907a28 --- /dev/null +++ b/sea-orm-cli/template/migration/src/m20220101_000001_create_table.rs @@ -0,0 +1,20 @@ +use sea_schema::migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20220101_000001_create_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + todo!() + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + todo!() + } +} diff --git a/sea-orm-cli/template/migration/src/main.rs b/sea-orm-cli/template/migration/src/main.rs new file mode 100644 index 00000000..89b349c9 --- /dev/null +++ b/sea-orm-cli/template/migration/src/main.rs @@ -0,0 +1,7 @@ +use migration::Migrator; +use sea_schema::migration::prelude::*; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml index 22ada992..2a90badb 100644 --- a/sea-orm-codegen/Cargo.toml +++ b/sea-orm-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-orm-codegen" -version = "0.5.0" +version = "0.6.0" authors = ["Billy Chan "] edition = "2021" description = "Code Generator for SeaORM" @@ -15,7 +15,7 @@ name = "sea_orm_codegen" path = "src/lib.rs" [dependencies] -sea-query = { version = "0.20.0" } +sea-query = { version = "0.21.0" } syn = { version = "^1", default-features = false, features = [ "derive", "parsing", diff --git a/sea-orm-codegen/src/entity/active_enum.rs b/sea-orm-codegen/src/entity/active_enum.rs index d92423f2..800da59e 100644 --- a/sea-orm-codegen/src/entity/active_enum.rs +++ b/sea-orm-codegen/src/entity/active_enum.rs @@ -1,3 +1,4 @@ +use crate::WithSerde; use heck::CamelCase; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -9,7 +10,7 @@ pub struct ActiveEnum { } impl ActiveEnum { - pub fn impl_active_enum(&self) -> TokenStream { + pub fn impl_active_enum(&self, with_serde: &WithSerde) -> TokenStream { let enum_name = &self.enum_name; let enum_iden = format_ident!("{}", enum_name.to_camel_case()); let values = &self.values; @@ -17,8 +18,11 @@ impl ActiveEnum { .values .iter() .map(|v| format_ident!("{}", v.to_camel_case())); + + let extra_derive = with_serde.extra_derive(); + quote! { - #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum)] + #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum #extra_derive)] #[sea_orm(rs_type = "String", db_type = "Enum", enum_name = #enum_name)] pub enum #enum_iden { #( diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index 35904600..3b374b02 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -22,6 +22,10 @@ impl Column { format_ident!("{}", escape_rust_keyword(self.name.to_camel_case())) } + pub fn is_snake_case_name(&self) -> bool { + self.name.to_snake_case() == self.name + } + pub fn get_rs_type(&self) -> TokenStream { #[allow(unreachable_patterns)] let ident: TokenStream = match &self.col_type { @@ -38,7 +42,8 @@ impl Column { ColumnType::Json | ColumnType::JsonBinary => "Json".to_owned(), ColumnType::Date => "Date".to_owned(), ColumnType::Time(_) => "Time".to_owned(), - ColumnType::DateTime(_) | ColumnType::Timestamp(_) => "DateTime".to_owned(), + ColumnType::DateTime(_) => "DateTime".to_owned(), + ColumnType::Timestamp(_) => "DateTimeUtc".to_owned(), ColumnType::TimestampWithTimeZone(_) => "DateTimeWithTimeZone".to_owned(), ColumnType::Decimal(_) | ColumnType::Money(_) => "Decimal".to_owned(), ColumnType::Uuid => "Uuid".to_owned(), @@ -271,7 +276,7 @@ mod tests { "Date", "Time", "DateTime", - "DateTime", + "DateTimeUtc", "DateTimeWithTimeZone", ]; for (mut col, rs_type) in columns.into_iter().zip(rs_types) { diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index 60a01eeb..21269cb9 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -81,25 +81,25 @@ impl FromStr for WithSerde { impl EntityWriter { pub fn generate(self, expanded_format: bool, with_serde: WithSerde) -> WriterOutput { let mut files = Vec::new(); - files.extend(self.write_entities(expanded_format, with_serde)); + files.extend(self.write_entities(expanded_format, &with_serde)); files.push(self.write_mod()); files.push(self.write_prelude()); if !self.enums.is_empty() { - files.push(self.write_sea_orm_active_enums()); + files.push(self.write_sea_orm_active_enums(&with_serde)); } WriterOutput { files } } - pub fn write_entities(&self, expanded_format: bool, with_serde: WithSerde) -> Vec { + pub fn write_entities(&self, expanded_format: bool, with_serde: &WithSerde) -> Vec { self.entities .iter() .map(|entity| { let mut lines = Vec::new(); Self::write_doc_comment(&mut lines); let code_blocks = if expanded_format { - Self::gen_expanded_code_blocks(entity, &with_serde) + Self::gen_expanded_code_blocks(entity, with_serde) } else { - Self::gen_compact_code_blocks(entity, &with_serde) + Self::gen_compact_code_blocks(entity, with_serde) }; Self::write(&mut lines, code_blocks); OutputFile { @@ -147,20 +147,18 @@ impl EntityWriter { } } - pub fn write_sea_orm_active_enums(&self) -> OutputFile { + pub fn write_sea_orm_active_enums(&self, with_serde: &WithSerde) -> OutputFile { let mut lines = Vec::new(); Self::write_doc_comment(&mut lines); Self::write( &mut lines, - vec![quote! { - use sea_orm::entity::prelude::*; - }], + vec![Self::gen_import(with_serde)], ); lines.push("".to_owned()); let code_blocks = self .enums .iter() - .map(|(_, active_enum)| active_enum.impl_active_enum()) + .map(|(_, active_enum)| active_enum.impl_active_enum(with_serde)) .collect(); Self::write(&mut lines, code_blocks); OutputFile { @@ -306,11 +304,22 @@ impl EntityWriter { } pub fn gen_column_enum(entity: &Entity) -> TokenStream { - let column_names_camel_case = entity.get_column_names_camel_case(); + let column_variants = entity.columns.iter().map(|col| { + let variant = col.get_name_camel_case(); + let mut variant = quote! { #variant }; + if !col.is_snake_case_name() { + let column_name = &col.name; + variant = quote! { + #[sea_orm(column_name = #column_name)] + #variant + }; + } + variant + }); quote! { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] pub enum Column { - #(#column_names_camel_case,)* + #(#column_variants,)* } } } @@ -474,6 +483,10 @@ impl EntityWriter { .iter() .map(|col| { let mut attrs: Punctuated<_, Comma> = Punctuated::new(); + if !col.is_snake_case_name() { + let column_name = &col.name; + attrs.push(quote! { column_name = #column_name }); + } if primary_keys.contains(&col.name) { attrs.push(quote! { primary_key }); if !col.auto_increment { @@ -724,14 +737,14 @@ mod tests { unique: false, }, Column { - name: "name".to_owned(), + name: "_name_".to_owned(), col_type: ColumnType::String(Some(255)), auto_increment: false, not_null: true, unique: false, }, Column { - name: "fruit_id".to_owned(), + name: "fruitId".to_owned(), col_type: ColumnType::Integer(Some(11)), auto_increment: false, not_null: false, @@ -740,7 +753,7 @@ mod tests { ], relations: vec![Relation { ref_table: "fruit".to_owned(), - columns: vec!["fruit_id".to_owned()], + columns: vec!["fruitId".to_owned()], ref_columns: vec!["id".to_owned()], rel_type: RelationType::BelongsTo, on_delete: None, diff --git a/sea-orm-codegen/tests/compact/vendor.rs b/sea-orm-codegen/tests/compact/vendor.rs index 314eb396..1351c227 100644 --- a/sea-orm-codegen/tests/compact/vendor.rs +++ b/sea-orm-codegen/tests/compact/vendor.rs @@ -7,7 +7,9 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: i32, + #[sea_orm(column_name = "_name_")] pub name: String, + #[sea_orm(column_name = "fruitId")] pub fruit_id: Option , } diff --git a/sea-orm-codegen/tests/expanded/vendor.rs b/sea-orm-codegen/tests/expanded/vendor.rs index ffc211a8..ab4ae39a 100644 --- a/sea-orm-codegen/tests/expanded/vendor.rs +++ b/sea-orm-codegen/tests/expanded/vendor.rs @@ -21,7 +21,9 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] pub enum Column { Id, + #[sea_orm(column_name = "_name_")] Name, + #[sea_orm(column_name = "fruitId")] FruitId, } diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index 326d48e8..12f27be2 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-orm-macros" -version = "0.5.0" +version = "0.6.0" authors = [ "Billy Chan " ] edition = "2021" description = "Derive macros for SeaORM" diff --git a/sea-orm-macros/src/attributes.rs b/sea-orm-macros/src/attributes.rs index cb47d029..3b7ed47b 100644 --- a/sea-orm-macros/src/attributes.rs +++ b/sea-orm-macros/src/attributes.rs @@ -28,5 +28,6 @@ pub mod field_attr { pub on_delete: Option, pub from: Option, pub to: Option, + pub fk_name: Option, } } diff --git a/sea-orm-macros/src/derives/active_enum.rs b/sea-orm-macros/src/derives/active_enum.rs index bafb1908..d75c9748 100644 --- a/sea-orm-macros/src/derives/active_enum.rs +++ b/sea-orm-macros/src/derives/active_enum.rs @@ -211,7 +211,7 @@ impl ActiveEnum { .to_owned() } - fn try_from_value(v: &Self::Value) -> Result { + fn try_from_value(v: &Self::Value) -> std::result::Result { match #val { #( #variant_values => Ok(Self::#variant_idents), )* _ => Err(sea_orm::DbErr::Type(format!( @@ -237,7 +237,7 @@ impl ActiveEnum { #[automatically_derived] impl sea_orm::TryGetable for #ident { - fn try_get(res: &sea_orm::QueryResult, pre: &str, col: &str) -> Result { + fn try_get(res: &sea_orm::QueryResult, pre: &str, col: &str) -> std::result::Result { let value = <::Value as sea_orm::TryGetable>::try_get(res, pre, col)?; ::try_from_value(&value).map_err(sea_orm::TryGetError::DbErr) } @@ -245,7 +245,7 @@ impl ActiveEnum { #[automatically_derived] impl sea_orm::sea_query::ValueType for #ident { - fn try_from(v: sea_orm::sea_query::Value) -> Result { + fn try_from(v: sea_orm::sea_query::Value) -> std::result::Result { let value = <::Value as sea_orm::sea_query::ValueType>::try_from(v)?; ::try_from_value(&value).map_err(|_| sea_orm::sea_query::ValueTypeErr) } diff --git a/sea-orm-macros/src/derives/column.rs b/sea-orm-macros/src/derives/column.rs index 97191250..42e1dfe1 100644 --- a/sea-orm-macros/src/derives/column.rs +++ b/sea-orm-macros/src/derives/column.rs @@ -96,7 +96,7 @@ pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result impl std::str::FromStr for #ident { type Err = sea_orm::ColumnFromStrErr; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> std::result::Result { match s { #(#columns),*, _ => Err(sea_orm::ColumnFromStrErr(format!("Failed to parse '{}' as `{}`", s, stringify!(#ident)))), diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index 1cd9134b..0bbd8bf1 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -242,7 +242,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res "DateTime" | "NaiveDateTime" => { quote! { DateTime } } - "DateTimeWithTimeZone" => { + "DateTimeUtc" | "DateTimeLocal" | "DateTimeWithTimeZone" => { quote! { TimestampWithTimeZone } } "Uuid" => quote! { Uuid }, diff --git a/sea-orm-macros/src/derives/from_query_result.rs b/sea-orm-macros/src/derives/from_query_result.rs index dc6bbc44..12b85671 100644 --- a/sea-orm-macros/src/derives/from_query_result.rs +++ b/sea-orm-macros/src/derives/from_query_result.rs @@ -1,6 +1,6 @@ use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote, quote_spanned}; -use syn::{Data, DataStruct, Field, Fields}; +use syn::{ext::IdentExt, Data, DataStruct, Field, Fields}; /// Method to derive a [QueryResult](sea_orm::QueryResult) pub fn expand_derive_from_query_result(ident: Ident, data: Data) -> syn::Result { @@ -24,7 +24,7 @@ pub fn expand_derive_from_query_result(ident: Ident, data: Data) -> syn::Result< let name: Vec = field .iter() .map(|f| { - let s = f.to_string(); + let s = f.unraw().to_string(); quote! { #s } }) .collect(); @@ -32,7 +32,7 @@ pub fn expand_derive_from_query_result(ident: Ident, data: Data) -> syn::Result< Ok(quote!( #[automatically_derived] impl sea_orm::FromQueryResult for #ident { - fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> Result { + fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { Ok(Self { #(#field: row.try_get(pre, #name)?),* }) diff --git a/sea-orm-macros/src/derives/model.rs b/sea-orm-macros/src/derives/model.rs index 97ca66a8..7858af3b 100644 --- a/sea-orm-macros/src/derives/model.rs +++ b/sea-orm-macros/src/derives/model.rs @@ -125,7 +125,7 @@ impl DeriveModel { quote!( #[automatically_derived] impl sea_orm::FromQueryResult for #ident { - fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> Result { + fn from_query_result(row: &sea_orm::QueryResult, pre: &str) -> std::result::Result { Ok(Self { #(#field_idents: #field_values),* }) diff --git a/sea-orm-macros/src/derives/relation.rs b/sea-orm-macros/src/derives/relation.rs index ee4f332c..4b6d7d61 100644 --- a/sea-orm-macros/src/derives/relation.rs +++ b/sea-orm-macros/src/derives/relation.rs @@ -136,6 +136,22 @@ impl DeriveRelation { result = quote! { #result.on_delete(sea_orm::prelude::ForeignKeyAction::#on_delete) }; } + if attr.fk_name.is_some() { + let fk_name = attr + .fk_name + .as_ref() + .map(|lit| { + match lit { + syn::Lit::Str(lit_str) => Ok(lit_str.value()), + _ => Err(syn::Error::new_spanned(lit, "attribute must be a string")), + } + }) + .ok_or_else(|| { + syn::Error::new_spanned(variant, "Missing value for 'fk_name'") + })??; + result = quote! { #result.fk_name(#fk_name) }; + } + result = quote! { #result.into() }; Result::<_, syn::Error>::Ok(result) diff --git a/src/database/connection.rs b/src/database/connection.rs index eb41fe18..46da564b 100644 --- a/src/database/connection.rs +++ b/src/database/connection.rs @@ -7,10 +7,7 @@ use std::{future::Future, pin::Pin}; /// Creates constraints for any structure that can create a database connection /// and execute SQL statements #[async_trait::async_trait] -pub trait ConnectionTrait<'a>: Sync { - /// Create a stream for the [QueryResult] - type Stream: Stream> + Send; - +pub trait ConnectionTrait: Sync { /// Fetch the database backend as specified in [DbBackend]. /// This depends on feature flags enabled. fn get_database_backend(&self) -> DbBackend; @@ -24,12 +21,34 @@ pub trait ConnectionTrait<'a>: Sync { /// Execute a [Statement] and return a collection Vec<[QueryResult]> on success async fn query_all(&self, stmt: Statement) -> Result, DbErr>; + /// Check if the connection supports `RETURNING` syntax on insert and update + fn support_returning(&self) -> bool { + let db_backend = self.get_database_backend(); + db_backend.support_returning() + } + + /// Check if the connection is a test connection for the Mock database + fn is_mock_connection(&self) -> bool { + false + } +} + +/// Stream query results +#[async_trait::async_trait] +pub trait StreamTrait<'a>: Sync { + /// Create a stream for the [QueryResult] + type Stream: Stream> + Send; + /// Execute a [Statement] and return a stream of results fn stream( &'a self, stmt: Statement, ) -> Pin> + 'a + Send>>; +} +/// Spawn database transaction +#[async_trait::async_trait] +pub trait TransactionTrait { /// Execute SQL `BEGIN` transaction. /// Returns a Transaction that can be committed or rolled back async fn begin(&self) -> Result; @@ -44,15 +63,4 @@ pub trait ConnectionTrait<'a>: Sync { + Send, T: Send, E: std::error::Error + Send; - - /// Check if the connection supports `RETURNING` syntax on insert and update - fn support_returning(&self) -> bool { - let db_backend = self.get_database_backend(); - db_backend.support_returning() - } - - /// Check if the connection is a test connection for the Mock database - fn is_mock_connection(&self) -> bool { - false - } } diff --git a/src/database/db_connection.rs b/src/database/db_connection.rs index 0183bcd8..0d4a618e 100644 --- a/src/database/db_connection.rs +++ b/src/database/db_connection.rs @@ -1,6 +1,6 @@ use crate::{ error::*, ConnectionTrait, DatabaseTransaction, ExecResult, QueryResult, Statement, - StatementBuilder, TransactionError, + StatementBuilder, StreamTrait, TransactionError, TransactionTrait, }; use sea_query::{MysqlQueryBuilder, PostgresQueryBuilder, QueryBuilder, SqliteQueryBuilder}; use std::{future::Future, pin::Pin}; @@ -89,9 +89,7 @@ impl std::fmt::Debug for DatabaseConnection { } #[async_trait::async_trait] -impl<'a> ConnectionTrait<'a> for DatabaseConnection { - type Stream = crate::QueryStream; - +impl ConnectionTrait for DatabaseConnection { fn get_database_backend(&self) -> DbBackend { match self { #[cfg(feature = "sqlx-mysql")] @@ -151,6 +149,16 @@ impl<'a> ConnectionTrait<'a> for DatabaseConnection { } } + #[cfg(feature = "mock")] + fn is_mock_connection(&self) -> bool { + matches!(self, DatabaseConnection::MockDatabaseConnection(_)) + } +} + +#[async_trait::async_trait] +impl<'a> StreamTrait<'a> for DatabaseConnection { + type Stream = crate::QueryStream; + #[instrument(level = "trace")] fn stream( &'a self, @@ -172,7 +180,10 @@ impl<'a> ConnectionTrait<'a> for DatabaseConnection { }) }) } +} +#[async_trait::async_trait] +impl TransactionTrait for DatabaseConnection { #[instrument(level = "trace")] async fn begin(&self) -> Result { match self { @@ -221,11 +232,6 @@ impl<'a> ConnectionTrait<'a> for DatabaseConnection { DatabaseConnection::Disconnected => panic!("Disconnected"), } } - - #[cfg(feature = "mock")] - fn is_mock_connection(&self) -> bool { - matches!(self, DatabaseConnection::MockDatabaseConnection(_)) - } } #[cfg(feature = "mock")] diff --git a/src/database/mock.rs b/src/database/mock.rs index 844fcbd9..ac9fe1a0 100644 --- a/src/database/mock.rs +++ b/src/database/mock.rs @@ -1,7 +1,7 @@ use crate::{ error::*, DatabaseConnection, DbBackend, EntityTrait, ExecResult, ExecResultHolder, Iden, - Iterable, MockDatabaseConnection, MockDatabaseTrait, ModelTrait, QueryResult, QueryResultRow, - Statement, + IdenStatic, Iterable, MockDatabaseConnection, MockDatabaseTrait, ModelTrait, QueryResult, + QueryResultRow, SelectA, SelectB, Statement, }; use sea_query::{Value, ValueType, Values}; use std::{collections::BTreeMap, sync::Arc}; @@ -204,6 +204,39 @@ where } } +impl IntoMockRow for (M, N) +where + M: ModelTrait, + N: ModelTrait, +{ + fn into_mock_row(self) -> MockRow { + let mut mapped_join = BTreeMap::new(); + + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectA.as_str(), column.as_str()), + self.0.get(column), + ); + } + for column in <::Entity as EntityTrait>::Column::iter() { + mapped_join.insert( + format!("{}{}", SelectB.as_str(), column.as_str()), + self.1.get(column), + ); + } + + mapped_join.into_mock_row() + } +} + +impl IntoMockRow for BTreeMap { + fn into_mock_row(self) -> MockRow { + MockRow { + values: self.into_iter().map(|(k, v)| (k, v)).collect(), + } + } +} + impl IntoMockRow for BTreeMap<&str, Value> { fn into_mock_row(self) -> MockRow { MockRow { @@ -311,8 +344,8 @@ impl OpenTransaction { #[cfg(feature = "mock")] mod tests { use crate::{ - entity::*, tests_cfg::*, ConnectionTrait, DbBackend, DbErr, MockDatabase, Statement, - Transaction, TransactionError, + entity::*, tests_cfg::*, DbBackend, DbErr, IntoMockRow, MockDatabase, Statement, + Transaction, TransactionError, TransactionTrait, }; use pretty_assertions::assert_eq; @@ -605,4 +638,73 @@ mod tests { Ok(()) } + + #[smol_potat::test] + async fn test_mocked_join() { + let row = ( + cake::Model { + id: 1, + name: "Apple Cake".to_owned(), + }, + fruit::Model { + id: 2, + name: "Apple".to_owned(), + cake_id: Some(1), + }, + ); + let mocked_row = row.into_mock_row(); + + let a_id = mocked_row.try_get::("A_id"); + assert!(a_id.is_ok()); + assert_eq!(1, a_id.unwrap()); + let b_id = mocked_row.try_get::("B_id"); + assert!(b_id.is_ok()); + assert_eq!(2, b_id.unwrap()); + } + + #[smol_potat::test] + async fn test_find_also_related_1() -> Result<(), DbErr> { + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results(vec![vec![( + cake::Model { + id: 1, + name: "Apple Cake".to_owned(), + }, + fruit::Model { + id: 2, + name: "Apple".to_owned(), + cake_id: Some(1), + }, + )]]) + .into_connection(); + + assert_eq!( + cake::Entity::find() + .find_also_related(fruit::Entity) + .all(&db) + .await?, + vec![( + cake::Model { + id: 1, + name: "Apple Cake".to_owned(), + }, + Some(fruit::Model { + id: 2, + name: "Apple".to_owned(), + cake_id: Some(1), + }) + )] + ); + + assert_eq!( + db.into_transaction_log(), + vec![Transaction::from_sql_and_values( + DbBackend::Postgres, + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name", "fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id" FROM "cake" LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + vec![] + ),] + ); + + Ok(()) + } } diff --git a/src/database/mod.rs b/src/database/mod.rs index 1a9556c7..ea00f742 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -37,6 +37,8 @@ pub struct ConnectOptions { /// Maximum idle time for a particular connection to prevent /// network resource exhaustion pub(crate) idle_timeout: Option, + /// Set the maximum lifetime of individual connections + pub(crate) max_lifetime: Option, /// Enable SQLx statement logging pub(crate) sqlx_logging: bool, } @@ -100,6 +102,7 @@ impl ConnectOptions { min_connections: None, connect_timeout: None, idle_timeout: None, + max_lifetime: None, sqlx_logging: true, } } @@ -127,6 +130,9 @@ impl ConnectOptions { if let Some(idle_timeout) = self.idle_timeout { opt = opt.idle_timeout(Some(idle_timeout)); } + if let Some(max_lifetime) = self.max_lifetime { + opt = opt.max_lifetime(Some(max_lifetime)); + } opt } @@ -179,6 +185,17 @@ impl ConnectOptions { self.idle_timeout } + /// Set the maximum lifetime of individual connections + pub fn max_lifetime(&mut self, lifetime: Duration) -> &mut Self { + self.max_lifetime = Some(lifetime); + self + } + + /// Get the maximum lifetime of individual connections, if set + pub fn get_max_lifetime(&self) -> Option { + self.max_lifetime + } + /// Enable SQLx statement logging (default true) pub fn sqlx_logging(&mut self, value: bool) -> &mut Self { self.sqlx_logging = value; diff --git a/src/database/statement.rs b/src/database/statement.rs index a1ba8126..94420c38 100644 --- a/src/database/statement.rs +++ b/src/database/statement.rs @@ -124,6 +124,10 @@ build_schema_stmt!(sea_query::TableDropStatement); build_schema_stmt!(sea_query::TableAlterStatement); build_schema_stmt!(sea_query::TableRenameStatement); build_schema_stmt!(sea_query::TableTruncateStatement); +build_schema_stmt!(sea_query::IndexCreateStatement); +build_schema_stmt!(sea_query::IndexDropStatement); +build_schema_stmt!(sea_query::ForeignKeyCreateStatement); +build_schema_stmt!(sea_query::ForeignKeyDropStatement); macro_rules! build_type_stmt { ($stmt: ty) => { diff --git a/src/database/stream/query.rs b/src/database/stream/query.rs index e0f606f4..7a8eba13 100644 --- a/src/database/stream/query.rs +++ b/src/database/stream/query.rs @@ -156,6 +156,8 @@ impl QueryStream { } #[cfg(feature = "mock")] InnerConnection::Mock(c) => c.fetch(stmt), + #[allow(unreachable_patterns)] + _ => unreachable!(), }, } .build() diff --git a/src/database/stream/transaction.rs b/src/database/stream/transaction.rs index ad1c8181..74b04421 100644 --- a/src/database/stream/transaction.rs +++ b/src/database/stream/transaction.rs @@ -83,6 +83,8 @@ impl<'a> TransactionStream<'a> { } #[cfg(feature = "mock")] InnerConnection::Mock(c) => c.fetch(stmt), + #[allow(unreachable_patterns)] + _ => unreachable!(), }, } .build() diff --git a/src/database/transaction.rs b/src/database/transaction.rs index 80db1c37..05aec165 100644 --- a/src/database/transaction.rs +++ b/src/database/transaction.rs @@ -1,6 +1,6 @@ use crate::{ debug_print, ConnectionTrait, DbBackend, DbErr, ExecResult, InnerConnection, QueryResult, - Statement, TransactionStream, + Statement, StreamTrait, TransactionStream, TransactionTrait, }; #[cfg(feature = "sqlx-dep")] use crate::{sqlx_error_to_exec_err, sqlx_error_to_query_err}; @@ -226,6 +226,8 @@ impl DatabaseTransaction { InnerConnection::Mock(c) => { c.rollback(); } + #[allow(unreachable_patterns)] + _ => unreachable!(), } } else { //this should never happen @@ -242,9 +244,7 @@ impl Drop for DatabaseTransaction { } #[async_trait::async_trait] -impl<'a> ConnectionTrait<'a> for DatabaseTransaction { - type Stream = TransactionStream<'a>; - +impl ConnectionTrait for DatabaseTransaction { fn get_database_backend(&self) -> DbBackend { // this way we don't need to lock self.backend @@ -278,6 +278,8 @@ impl<'a> ConnectionTrait<'a> for DatabaseTransaction { } #[cfg(feature = "mock")] InnerConnection::Mock(conn) => return conn.execute(stmt), + #[allow(unreachable_patterns)] + _ => unreachable!(), }; #[cfg(feature = "sqlx-dep")] _res.map_err(sqlx_error_to_exec_err) @@ -305,6 +307,8 @@ impl<'a> ConnectionTrait<'a> for DatabaseTransaction { } #[cfg(feature = "mock")] InnerConnection::Mock(conn) => return conn.query_one(stmt), + #[allow(unreachable_patterns)] + _ => unreachable!(), }; #[cfg(feature = "sqlx-dep")] if let Err(sqlx::Error::RowNotFound) = _res { @@ -345,10 +349,17 @@ impl<'a> ConnectionTrait<'a> for DatabaseTransaction { } #[cfg(feature = "mock")] InnerConnection::Mock(conn) => return conn.query_all(stmt), + #[allow(unreachable_patterns)] + _ => unreachable!(), }; #[cfg(feature = "sqlx-dep")] _res.map_err(sqlx_error_to_query_err) } +} + +#[async_trait::async_trait] +impl<'a> StreamTrait<'a> for DatabaseTransaction { + type Stream = TransactionStream<'a>; #[instrument(level = "trace")] fn stream( @@ -364,7 +375,10 @@ impl<'a> ConnectionTrait<'a> for DatabaseTransaction { )) }) } +} +#[async_trait::async_trait] +impl TransactionTrait for DatabaseTransaction { #[instrument(level = "trace")] async fn begin(&self) -> Result { DatabaseTransaction::begin( diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index 77ec517c..1fa25757 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -276,7 +276,7 @@ pub trait ActiveModelTrait: Clone + Debug { where ::Model: IntoActiveModel, Self: ActiveModelBehavior + 'a, - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let am = ActiveModelBehavior::before_save(self, true)?; let model = ::insert(am) @@ -398,7 +398,7 @@ pub trait ActiveModelTrait: Clone + Debug { where ::Model: IntoActiveModel, Self: ActiveModelBehavior + 'a, - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let am = ActiveModelBehavior::before_save(self, false)?; let model: ::Model = Self::Entity::update(am).exec(db).await?; @@ -411,7 +411,7 @@ pub trait ActiveModelTrait: Clone + Debug { where ::Model: IntoActiveModel, Self: ActiveModelBehavior + 'a, - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let mut is_update = true; for key in ::PrimaryKey::iter() { @@ -475,7 +475,7 @@ pub trait ActiveModelTrait: Clone + Debug { async fn delete<'a, C>(self, db: &'a C) -> Result where Self: ActiveModelBehavior + 'a, - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let am = ActiveModelBehavior::before_delete(self)?; let am_clone = am.clone(); @@ -632,6 +632,14 @@ impl_into_active_value!(crate::prelude::DateTime, Set); #[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] impl_into_active_value!(crate::prelude::DateTimeWithTimeZone, Set); +#[cfg(feature = "with-chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] +impl_into_active_value!(crate::prelude::DateTimeUtc, Set); + +#[cfg(feature = "with-chrono")] +#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))] +impl_into_active_value!(crate::prelude::DateTimeLocal, Set); + #[cfg(feature = "with-rust_decimal")] #[cfg_attr(docsrs, doc(cfg(feature = "with-rust_decimal")))] impl_into_active_value!(crate::prelude::Decimal, Set); @@ -644,6 +652,7 @@ impl Default for ActiveValue where V: Into, { + /// Create an [ActiveValue::NotSet] fn default() -> Self { Self::NotSet } diff --git a/src/entity/column.rs b/src/entity/column.rs index 8beb465b..c4ea545f 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -9,6 +9,7 @@ pub struct ColumnDef { pub(crate) null: bool, pub(crate) unique: bool, pub(crate) indexed: bool, + pub(crate) default_value: Option, } /// The type of column as defined in the SQL format @@ -300,6 +301,7 @@ impl ColumnType { null: false, unique: false, indexed: false, + default_value: None, } } @@ -335,6 +337,15 @@ impl ColumnDef { self } + /// Set the default value + pub fn default_value(mut self, value: T) -> Self + where + T: Into, + { + self.default_value = Some(value.into()); + self + } + /// Get [ColumnType] as reference pub fn get_column_type(&self) -> &ColumnType { &self.col_type diff --git a/src/entity/model.rs b/src/entity/model.rs index 271493ef..34520dc3 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -41,7 +41,7 @@ pub trait ModelTrait: Clone + Send + Debug { async fn delete<'a, A, C>(self, db: &'a C) -> Result where Self: IntoActiveModel, - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: ActiveModelTrait + ActiveModelBehavior + Send + 'a, { self.into_active_model().delete(db).await diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 28f5fe70..c88f9d7a 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -24,10 +24,18 @@ pub use chrono::NaiveTime as Time; #[cfg(feature = "with-chrono")] pub use chrono::NaiveDateTime as DateTime; -/// Handles the time and dates +/// Date time with fixed offset #[cfg(feature = "with-chrono")] pub type DateTimeWithTimeZone = chrono::DateTime; +/// Date time represented in UTC +#[cfg(feature = "with-chrono")] +pub type DateTimeUtc = chrono::DateTime; + +/// Date time represented in local time +#[cfg(feature = "with-chrono")] +pub type DateTimeLocal = chrono::DateTime; + #[cfg(feature = "with-rust_decimal")] pub use rust_decimal::Decimal; diff --git a/src/entity/relation.rs b/src/entity/relation.rs index 0733c982..d43c856e 100644 --- a/src/entity/relation.rs +++ b/src/entity/relation.rs @@ -62,6 +62,8 @@ pub struct RelationDef { /// Defines an operation to be performed on a Foreign Key when a /// `UPDATE` Operation is performed pub on_update: Option, + /// The name of foreign key constraint + pub fk_name: Option, } /// Defines a helper to build a relation @@ -80,6 +82,7 @@ where is_owner: bool, on_delete: Option, on_update: Option, + fk_name: Option, } impl RelationDef { @@ -94,6 +97,7 @@ impl RelationDef { is_owner: !self.is_owner, on_delete: self.on_delete, on_update: self.on_update, + fk_name: None, } } } @@ -114,6 +118,7 @@ where is_owner, on_delete: None, on_update: None, + fk_name: None, } } @@ -128,6 +133,7 @@ where is_owner, on_delete: None, on_update: None, + fk_name: None, } } @@ -160,6 +166,12 @@ where self.on_update = Some(action); self } + + /// Set the name of foreign key constraint + pub fn fk_name(mut self, fk_name: &str) -> Self { + self.fk_name = Some(fk_name.to_owned()); + self + } } impl From> for RelationDef @@ -177,6 +189,7 @@ where is_owner: b.is_owner, on_delete: b.on_delete, on_update: b.on_update, + fk_name: b.fk_name, } } } diff --git a/src/executor/delete.rs b/src/executor/delete.rs index 7577b976..25a1060e 100644 --- a/src/executor/delete.rs +++ b/src/executor/delete.rs @@ -24,7 +24,7 @@ where /// Execute a DELETE operation on one ActiveModel pub fn exec(self, db: &'a C) -> impl Future> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { // so that self is dropped before entering await exec_delete_only(self.query, db) @@ -38,7 +38,7 @@ where /// Execute a DELETE operation on many ActiveModels pub fn exec(self, db: &'a C) -> impl Future> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { // so that self is dropped before entering await exec_delete_only(self.query, db) @@ -54,7 +54,7 @@ impl Deleter { /// Execute a DELETE operation pub fn exec<'a, C>(self, db: &'a C) -> impl Future> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let builder = db.get_database_backend(); exec_delete(builder.build(&self.query), db) @@ -63,14 +63,14 @@ impl Deleter { async fn exec_delete_only<'a, C>(query: DeleteStatement, db: &'a C) -> Result where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { Deleter::new(query).exec(db).await } async fn exec_delete<'a, C>(statement: Statement, db: &'a C) -> Result where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let result = db.execute(statement).await?; Ok(DeleteResult { diff --git a/src/executor/execute.rs b/src/executor/execute.rs index f3168180..821f7bed 100644 --- a/src/executor/execute.rs +++ b/src/executor/execute.rs @@ -46,6 +46,8 @@ impl ExecResult { } #[cfg(feature = "mock")] ExecResultHolder::Mock(result) => result.last_insert_id, + #[allow(unreachable_patterns)] + _ => unreachable!(), } } @@ -60,6 +62,8 @@ impl ExecResult { ExecResultHolder::SqlxSqlite(result) => result.rows_affected(), #[cfg(feature = "mock")] ExecResultHolder::Mock(result) => result.rows_affected, + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 1d9f7442..ef5c11ea 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -36,7 +36,7 @@ where #[allow(unused_mut)] pub fn exec<'a, C>(self, db: &'a C) -> impl Future, DbErr>> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: 'a, { // so that self is dropped before entering await @@ -58,7 +58,7 @@ where ) -> impl Future::Model, DbErr>> + '_ where ::Model: IntoActiveModel, - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: 'a, { Inserter::::new(self.primary_key, self.query).exec_with_returning(db) @@ -81,7 +81,7 @@ where /// Execute an insert operation pub fn exec<'a, C>(self, db: &'a C) -> impl Future, DbErr>> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: 'a, { let builder = db.get_database_backend(); @@ -95,7 +95,7 @@ where ) -> impl Future::Model, DbErr>> + '_ where ::Model: IntoActiveModel, - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: 'a, { exec_insert_with_returning::(self.primary_key, self.query, db) @@ -108,7 +108,7 @@ async fn exec_insert<'a, A, C>( db: &'a C, ) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: ActiveModelTrait, { type PrimaryKey = <::Entity as EntityTrait>::PrimaryKey; @@ -143,7 +143,7 @@ async fn exec_insert_with_returning<'a, A, C>( ) -> Result<::Model, DbErr> where ::Model: IntoActiveModel, - C: ConnectionTrait<'a>, + C: ConnectionTrait, A: ActiveModelTrait, { let db_backend = db.get_database_backend(); diff --git a/src/executor/paginator.rs b/src/executor/paginator.rs index e8f37bea..59453ff1 100644 --- a/src/executor/paginator.rs +++ b/src/executor/paginator.rs @@ -14,7 +14,7 @@ pub type PinBoxStream<'db, Item> = Pin + 'db>>; #[derive(Clone, Debug)] pub struct Paginator<'db, C, S> where - C: ConnectionTrait<'db>, + C: ConnectionTrait, S: SelectorTrait + 'db, { pub(crate) query: SelectStatement, @@ -28,7 +28,7 @@ where impl<'db, C, S> Paginator<'db, C, S> where - C: ConnectionTrait<'db>, + C: ConnectionTrait, S: SelectorTrait + 'db, { /// Fetch a specific page; page index starts from zero @@ -184,7 +184,7 @@ where /// A Trait for any type that can paginate results pub trait PaginatorTrait<'db, C> where - C: ConnectionTrait<'db>, + C: ConnectionTrait, { /// Select operation type Selector: SelectorTrait + Send + Sync + 'db; @@ -203,7 +203,7 @@ where impl<'db, C, S> PaginatorTrait<'db, C> for Selector where - C: ConnectionTrait<'db>, + C: ConnectionTrait, S: SelectorTrait + Send + Sync + 'db, { type Selector = S; @@ -221,7 +221,7 @@ where impl<'db, C, M, E> PaginatorTrait<'db, C> for Select where - C: ConnectionTrait<'db>, + C: ConnectionTrait, E: EntityTrait, M: FromQueryResult + Sized + Send + Sync + 'db, { @@ -234,7 +234,7 @@ where impl<'db, C, M, N, E, F> PaginatorTrait<'db, C> for SelectTwo where - C: ConnectionTrait<'db>, + C: ConnectionTrait, E: EntityTrait, F: EntityTrait, M: FromQueryResult + Sized + Send + Sync + 'db, diff --git a/src/executor/query.rs b/src/executor/query.rs index 3a842fc0..4b03e312 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -75,6 +75,8 @@ impl fmt::Debug for QueryResultRow { Self::SqlxSqlite(_) => write!(f, "QueryResultRow::SqlxSqlite cannot be inspected"), #[cfg(feature = "mock")] Self::Mock(row) => write!(f, "{:?}", row), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } @@ -124,6 +126,8 @@ macro_rules! try_getable_all { debug_print!("{:#?}", e.to_string()); TryGetError::Null }), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } @@ -160,6 +164,8 @@ macro_rules! try_getable_unsigned { debug_print!("{:#?}", e.to_string()); TryGetError::Null }), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } @@ -193,6 +199,8 @@ macro_rules! try_getable_mysql { debug_print!("{:#?}", e.to_string()); TryGetError::Null }), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } @@ -236,6 +244,8 @@ macro_rules! try_getable_date_time { debug_print!("{:#?}", e.to_string()); TryGetError::Null }), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } @@ -271,6 +281,12 @@ try_getable_all!(chrono::NaiveDateTime); #[cfg(feature = "with-chrono")] try_getable_date_time!(chrono::DateTime); +#[cfg(feature = "with-chrono")] +try_getable_all!(chrono::DateTime); + +#[cfg(feature = "with-chrono")] +try_getable_all!(chrono::DateTime); + #[cfg(feature = "with-rust_decimal")] use rust_decimal::Decimal; @@ -315,6 +331,8 @@ impl TryGetable for Decimal { debug_print!("{:#?}", e.to_string()); TryGetError::Null }), + #[allow(unreachable_patterns)] + _ => unreachable!(), } } } @@ -614,6 +632,12 @@ try_from_u64_err!(chrono::NaiveDateTime); #[cfg(feature = "with-chrono")] try_from_u64_err!(chrono::DateTime); +#[cfg(feature = "with-chrono")] +try_from_u64_err!(chrono::DateTime); + +#[cfg(feature = "with-chrono")] +try_from_u64_err!(chrono::DateTime); + #[cfg(feature = "with-rust_decimal")] try_from_u64_err!(rust_decimal::Decimal); diff --git a/src/executor/select.rs b/src/executor/select.rs index 6a8bd3fa..e606d1ed 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -1,7 +1,7 @@ use crate::{ error::*, ConnectionTrait, EntityTrait, FromQueryResult, IdenStatic, Iterable, ModelTrait, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo, SelectTwoMany, Statement, - TryGetableMany, + StreamTrait, TryGetableMany, }; use futures::{Stream, TryStreamExt}; use sea_query::SelectStatement; @@ -256,7 +256,7 @@ where /// Get one Model from the SELECT query pub async fn one<'a, C>(self, db: &C) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.into_model().one(db).await } @@ -264,7 +264,7 @@ where /// Get all Models from the SELECT query pub async fn all<'a, C>(self, db: &C) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.into_model().all(db).await } @@ -275,7 +275,7 @@ where db: &'a C, ) -> Result> + 'b + Send, DbErr> where - C: ConnectionTrait<'a> + Send, + C: ConnectionTrait + StreamTrait<'a> + Send, { self.into_model().stream(db).await } @@ -310,7 +310,7 @@ where /// Get one Model from the Select query pub async fn one<'a, C>(self, db: &C) -> Result)>, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.into_model().one(db).await } @@ -318,7 +318,7 @@ where /// Get all Models from the Select query pub async fn all<'a, C>(self, db: &C) -> Result)>, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.into_model().all(db).await } @@ -329,7 +329,7 @@ where db: &'a C, ) -> Result), DbErr>> + 'b, DbErr> where - C: ConnectionTrait<'a> + Send, + C: ConnectionTrait + StreamTrait<'a> + Send, { self.into_model().stream(db).await } @@ -364,7 +364,7 @@ where /// Select one Model pub async fn one<'a, C>(self, db: &C) -> Result)>, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.into_model().one(db).await } @@ -375,7 +375,7 @@ where db: &'a C, ) -> Result), DbErr>> + 'b + Send, DbErr> where - C: ConnectionTrait<'a> + Send, + C: ConnectionTrait + StreamTrait<'a> + Send, { self.into_model().stream(db).await } @@ -383,7 +383,7 @@ where /// Get all Models from the select operation pub async fn all<'a, C>(self, db: &C) -> Result)>, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let rows = self.into_model().all(db).await?; Ok(consolidate_query_result::(rows)) @@ -421,7 +421,7 @@ where fn into_selector_raw<'a, C>(self, db: &C) -> SelectorRaw where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let builder = db.get_database_backend(); let stmt = builder.build(&self.query); @@ -434,7 +434,7 @@ where /// Get an item from the Select query pub async fn one<'a, C>(mut self, db: &C) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.query.limit(1); self.into_selector_raw(db).one(db).await @@ -443,7 +443,7 @@ where /// Get all items from the Select query pub async fn all<'a, C>(self, db: &C) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { self.into_selector_raw(db).all(db).await } @@ -454,7 +454,7 @@ where db: &'a C, ) -> Result> + 'b + Send>>, DbErr> where - C: ConnectionTrait<'a> + Send, + C: ConnectionTrait + StreamTrait<'a> + Send, S: 'b, S::Item: Send, { @@ -673,7 +673,7 @@ where /// ``` pub async fn one<'a, C>(self, db: &C) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let row = db.query_one(self.stmt).await?; match row { @@ -724,7 +724,7 @@ where /// ``` pub async fn all<'a, C>(self, db: &C) -> Result, DbErr> where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let rows = db.query_all(self.stmt).await?; let mut models = Vec::new(); @@ -740,7 +740,7 @@ where db: &'a C, ) -> Result> + 'b + Send>>, DbErr> where - C: ConnectionTrait<'a> + Send, + C: ConnectionTrait + StreamTrait<'a> + Send, S: 'b, S::Item: Send, { diff --git a/src/executor/update.rs b/src/executor/update.rs index eb997f13..70e971a8 100644 --- a/src/executor/update.rs +++ b/src/executor/update.rs @@ -1,6 +1,6 @@ use crate::{ - error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, Iterable, SelectModel, - SelectorRaw, Statement, UpdateMany, UpdateOne, + error::*, ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, IntoActiveModel, + Iterable, SelectModel, SelectorRaw, Statement, UpdateMany, UpdateOne, }; use sea_query::{Alias, Expr, FromValueTuple, Query, UpdateStatement}; use std::future::Future; @@ -26,7 +26,8 @@ where /// Execute an update operation on an ActiveModel pub async fn exec<'b, C>(self, db: &'b C) -> Result<::Model, DbErr> where - C: ConnectionTrait<'b>, + ::Model: IntoActiveModel, + C: ConnectionTrait, { // so that self is dropped before entering await exec_update_and_return_updated(self.query, self.model, db).await @@ -40,7 +41,7 @@ where /// Execute an update operation on multiple ActiveModels pub fn exec(self, db: &'a C) -> impl Future> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { // so that self is dropped before entering await exec_update_only(self.query, db) @@ -65,7 +66,7 @@ impl Updater { /// Execute an update operation pub fn exec<'a, C>(self, db: &'a C) -> impl Future> + '_ where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let builder = db.get_database_backend(); exec_update(builder.build(&self.query), db, self.check_record_exists) @@ -74,7 +75,7 @@ impl Updater { async fn exec_update_only<'a, C>(query: UpdateStatement, db: &'a C) -> Result where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { Updater::new(query).exec(db).await } @@ -86,7 +87,7 @@ async fn exec_update_and_return_updated<'a, A, C>( ) -> Result<::Model, DbErr> where A: ActiveModelTrait, - C: ConnectionTrait<'a>, + C: ConnectionTrait, { match db.support_returning() { true => { @@ -141,7 +142,7 @@ async fn exec_update<'a, C>( check_record_exists: bool, ) -> Result where - C: ConnectionTrait<'a>, + C: ConnectionTrait, { let result = db.execute(statement).await?; if check_record_exists && result.rows_affected() == 0 { diff --git a/src/lib.rs b/src/lib.rs index a44d19cd..6b17eabd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,9 @@ //! //! ## Getting Started //! +//! [![GitHub stars](https://img.shields.io/github/stars/SeaQL/sea-orm.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/SeaQL/sea-orm/stargazers/) +//! If you like what we do, consider starring, commenting, sharing and contributing! +//! //! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) //! Join our Discord server to chat with others in the SeaQL community! //! @@ -34,6 +37,7 @@ //! + [Rocket Example](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) //! + [Actix Example](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) //! + [Axum Example](https://github.com/SeaQL/sea-orm/tree/master/examples/axum_example) +//! + [Poem Example](https://github.com/SeaQL/sea-orm/tree/master/examples/poem_example) //! //! ## Features //! diff --git a/src/query/combine.rs b/src/query/combine.rs index 2baf3fa2..a96d5be1 100644 --- a/src/query/combine.rs +++ b/src/query/combine.rs @@ -45,11 +45,21 @@ where None => { let col = match &sel.expr { SimpleExpr::Column(col_ref) => match &col_ref { - ColumnRef::Column(col) | ColumnRef::TableColumn(_, col) => col, + ColumnRef::Column(col) + | ColumnRef::TableColumn(_, col) + | ColumnRef::SchemaTableColumn(_, _, col) => col, + ColumnRef::Asterisk | ColumnRef::TableAsterisk(_) => { + panic!("cannot apply alias for Column with asterisk") + } }, SimpleExpr::AsEnum(_, simple_expr) => match simple_expr.as_ref() { SimpleExpr::Column(col_ref) => match &col_ref { - ColumnRef::Column(col) | ColumnRef::TableColumn(_, col) => col, + ColumnRef::Column(col) + | ColumnRef::TableColumn(_, col) + | ColumnRef::SchemaTableColumn(_, _, col) => col, + ColumnRef::Asterisk | ColumnRef::TableAsterisk(_) => { + panic!("cannot apply alias for AsEnum with asterisk") + } }, _ => { panic!("cannot apply alias for AsEnum with expr other than Column") diff --git a/src/query/mod.rs b/src/query/mod.rs index 3168e81e..b7e304ac 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -23,5 +23,6 @@ pub use update::*; pub use util::*; pub use crate::{ - ConnectionTrait, InsertResult, PaginatorTrait, Statement, UpdateResult, Value, Values, + ConnectionTrait, InsertResult, PaginatorTrait, Statement, StreamTrait, TransactionTrait, + UpdateResult, Value, Values, }; diff --git a/src/query/util.rs b/src/query/util.rs index b23804f2..b8705845 100644 --- a/src/query/util.rs +++ b/src/query/util.rs @@ -122,7 +122,7 @@ macro_rules! debug_query_stmt { /// let raw_sql = debug_query!(&c, DbBackend::Sqlite); /// assert_eq!( /// raw_sql, -/// r#"INSERT INTO `cake` (`id`, `name`) VALUES (1, 'Apple Pie')"# +/// r#"INSERT INTO "cake" ("id", "name") VALUES (1, 'Apple Pie')"# /// ); /// ``` #[macro_export] diff --git a/src/schema/entity.rs b/src/schema/entity.rs index 9c422ed3..70f0d4ef 100644 --- a/src/schema/entity.rs +++ b/src/schema/entity.rs @@ -103,6 +103,9 @@ where if orm_column_def.unique { column_def.unique_key(); } + if let Some(value) = orm_column_def.default_value { + column_def.default(value); + } for primary_key in E::PrimaryKey::iter() { if column.to_string() == primary_key.into_column().to_string() { if E::PrimaryKey::auto_increment() { @@ -144,20 +147,18 @@ where 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); - } + let from_cols: Vec = match relation.from_col { + Identity::Unary(o1) => vec![o1], + Identity::Binary(o1, o2) => vec![o1, o2], + Identity::Ternary(o1, o2, o3) => vec![o1, o2, o3], } + .into_iter() + .map(|col| { + let col_name = col.to_string(); + foreign_key_stmt.from_col(col); + col_name + }) + .collect(); match relation.to_col { Identity::Unary(o1) => { foreign_key_stmt.to_col(o1); @@ -166,7 +167,7 @@ where foreign_key_stmt.to_col(o1); foreign_key_stmt.to_col(o2); } - crate::Identity::Ternary(o1, o2, o3) => { + Identity::Ternary(o1, o2, o3) => { foreign_key_stmt.to_col(o1); foreign_key_stmt.to_col(o2); foreign_key_stmt.to_col(o3); @@ -178,13 +179,14 @@ where if let Some(action) = relation.on_update { foreign_key_stmt.on_update(action); } + let name = if let Some(name) = relation.fk_name { + name + } else { + format!("fk-{}-{}", from_tbl.to_string(), from_cols.join("-")) + }; stmt.foreign_key( foreign_key_stmt - .name(&format!( - "fk-{}-{}", - from_tbl.to_string(), - to_tbl.to_string() - )) + .name(&name) .from_tbl(from_tbl) .to_tbl(to_tbl), ); @@ -235,7 +237,7 @@ mod tests { ) .foreign_key( ForeignKeyCreateStatement::new() - .name("fk-cake_filling_price-cake_filling") + .name("fk-cake_filling_price-cake_id-filling_id") .from_tbl(CakeFillingPrice) .from_col(cake_filling_price::Column::CakeId) .from_col(cake_filling_price::Column::FillingId) diff --git a/tests/active_enum_tests.rs b/tests/active_enum_tests.rs index 21e63016..e3c701c7 100644 --- a/tests/active_enum_tests.rs +++ b/tests/active_enum_tests.rs @@ -405,8 +405,14 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), _select.build(DbBackend::Sqlite).to_string(), + [ + r#"SELECT "active_enum_child"."id", "active_enum_child"."parent_id", "active_enum_child"."category", "active_enum_child"."color", "active_enum_child"."tea""#, + r#"FROM "active_enum_child""#, + r#"INNER JOIN "active_enum" ON "active_enum"."id" = "active_enum_child"."parent_id""#, + r#"WHERE "active_enum"."id" = 1"#, + ] + .join(" ") ); assert_eq!( _select.build(DbBackend::MySql).to_string(), @@ -435,8 +441,16 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), - _select.build(DbBackend::Sqlite).to_string(), + _select + .build(DbBackend::Sqlite) + .to_string(), + [ + r#"SELECT "active_enum"."id" AS "A_id", "active_enum"."category" AS "A_category", "active_enum"."color" AS "A_color", "active_enum"."tea" AS "A_tea","#, + r#""active_enum_child"."id" AS "B_id", "active_enum_child"."parent_id" AS "B_parent_id", "active_enum_child"."category" AS "B_category", "active_enum_child"."color" AS "B_color", "active_enum_child"."tea" AS "B_tea""#, + r#"FROM "active_enum""#, + r#"LEFT JOIN "active_enum_child" ON "active_enum"."id" = "active_enum_child"."parent_id""#, + ] + .join(" ") ); assert_eq!( _select @@ -478,8 +492,14 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), _select.build(DbBackend::Sqlite).to_string(), + [ + r#"SELECT "active_enum_child"."id", "active_enum_child"."parent_id", "active_enum_child"."category", "active_enum_child"."color", "active_enum_child"."tea""#, + r#"FROM "active_enum_child""#, + r#"INNER JOIN "active_enum" AS "r0" ON "r0"."id" = "active_enum_child"."parent_id""#, + r#"WHERE "r0"."id" = 1"#, + ] + .join(" ") ); assert_eq!( _select.build(DbBackend::MySql).to_string(), @@ -508,8 +528,16 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), - _select.build(DbBackend::Sqlite).to_string(), + _select + .build(DbBackend::Sqlite) + .to_string(), + [ + r#"SELECT "active_enum"."id" AS "A_id", "active_enum"."category" AS "A_category", "active_enum"."color" AS "A_color", "active_enum"."tea" AS "A_tea","#, + r#""r0"."id" AS "B_id", "r0"."parent_id" AS "B_parent_id", "r0"."category" AS "B_category", "r0"."color" AS "B_color", "r0"."tea" AS "B_tea""#, + r#"FROM "active_enum""#, + r#"LEFT JOIN "active_enum_child" AS "r0" ON "active_enum"."id" = "r0"."parent_id""#, + ] + .join(" ") ); assert_eq!( _select @@ -552,8 +580,14 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), _select.build(DbBackend::Sqlite).to_string(), + [ + r#"SELECT "active_enum"."id", "active_enum"."category", "active_enum"."color", "active_enum"."tea""#, + r#"FROM "active_enum""#, + r#"INNER JOIN "active_enum_child" ON "active_enum_child"."parent_id" = "active_enum"."id""#, + r#"WHERE "active_enum_child"."id" = 1"#, + ] + .join(" ") ); assert_eq!( _select.build(DbBackend::MySql).to_string(), @@ -582,8 +616,16 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), - _select.build(DbBackend::Sqlite).to_string(), + _select + .build(DbBackend::Sqlite) + .to_string(), + [ + r#"SELECT "active_enum_child"."id" AS "A_id", "active_enum_child"."parent_id" AS "A_parent_id", "active_enum_child"."category" AS "A_category", "active_enum_child"."color" AS "A_color", "active_enum_child"."tea" AS "A_tea","#, + r#""active_enum"."id" AS "B_id", "active_enum"."category" AS "B_category", "active_enum"."color" AS "B_color", "active_enum"."tea" AS "B_tea""#, + r#"FROM "active_enum_child""#, + r#"LEFT JOIN "active_enum" ON "active_enum_child"."parent_id" = "active_enum"."id""#, + ] + .join(" ") ); assert_eq!( _select @@ -626,8 +668,14 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), _select.build(DbBackend::Sqlite).to_string(), + [ + r#"SELECT "active_enum"."id", "active_enum"."category", "active_enum"."color", "active_enum"."tea""#, + r#"FROM "active_enum""#, + r#"INNER JOIN "active_enum_child" AS "r0" ON "r0"."parent_id" = "active_enum"."id""#, + r#"WHERE "r0"."id" = 1"#, + ] + .join(" ") ); assert_eq!( _select.build(DbBackend::MySql).to_string(), @@ -656,8 +704,16 @@ mod tests { #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] { assert_eq!( - _select.build(DbBackend::MySql).to_string(), - _select.build(DbBackend::Sqlite).to_string(), + _select + .build(DbBackend::Sqlite) + .to_string(), + [ + r#"SELECT "active_enum_child"."id" AS "A_id", "active_enum_child"."parent_id" AS "A_parent_id", "active_enum_child"."category" AS "A_category", "active_enum_child"."color" AS "A_color", "active_enum_child"."tea" AS "A_tea","#, + r#""r0"."id" AS "B_id", "r0"."category" AS "B_category", "r0"."color" AS "B_color", "r0"."tea" AS "B_tea""#, + r#"FROM "active_enum_child""#, + r#"LEFT JOIN "active_enum" AS "r0" ON "active_enum_child"."parent_id" = "r0"."id""#, + ] + .join(" ") ); assert_eq!( _select diff --git a/tests/common/bakery_chain/schema.rs b/tests/common/bakery_chain/schema.rs index 0d3e6c2a..94bbc649 100644 --- a/tests/common/bakery_chain/schema.rs +++ b/tests/common/bakery_chain/schema.rs @@ -55,7 +55,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_id") .from(baker::Entity, baker::Column::BakeryId) .to(bakery::Entity, bakery::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -111,7 +111,7 @@ pub async fn create_order_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("fk-order-bakery") + .name("fk-order-bakery_id") .from(order::Entity, order::Column::BakeryId) .to(bakery::Entity, bakery::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -119,7 +119,7 @@ pub async fn create_order_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("fk-order-customer") + .name("fk-order-customer_id") .from(order::Entity, order::Column::CustomerId) .to(customer::Entity, customer::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -162,7 +162,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("fk-lineitem-order") + .name("fk-lineitem-order_id") .from(lineitem::Entity, lineitem::Column::OrderId) .to(order::Entity, order::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -170,7 +170,7 @@ pub async fn create_lineitem_table(db: &DbConn) -> Result { ) .foreign_key( ForeignKey::create() - .name("fk-lineitem-cake") + .name("fk-lineitem-cake_id") .from(lineitem::Entity, lineitem::Column::CakeId) .to(cake::Entity, cake::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -202,7 +202,7 @@ pub async fn create_cakes_bakers_table(db: &DbConn) -> Result ) .foreign_key( ForeignKey::create() - .name("fk-cakes_bakers-cake") + .name("fk-cakes_bakers-cake_id") .from(cakes_bakers::Entity, cakes_bakers::Column::CakeId) .to(cake::Entity, cake::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -210,7 +210,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_id") .from(cakes_bakers::Entity, cakes_bakers::Column::BakerId) .to(baker::Entity, baker::Column::Id) .on_delete(ForeignKeyAction::Cascade) @@ -240,7 +240,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_id") .from(cake::Entity, cake::Column::BakeryId) .to(bakery::Entity, bakery::Column::Id) .on_delete(ForeignKeyAction::Cascade) diff --git a/tests/common/features/active_enum_child.rs b/tests/common/features/active_enum_child.rs index 30564a4f..1a9d0d04 100644 --- a/tests/common/features/active_enum_child.rs +++ b/tests/common/features/active_enum_child.rs @@ -16,6 +16,7 @@ pub struct Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( + fk_name = "fk-active_enum_child-active_enum", belongs_to = "super::active_enum::Entity", from = "Column::ParentId", to = "super::active_enum::Column::Id" diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index ea880fd2..18c0ae78 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -4,6 +4,7 @@ pub mod applog; pub mod byte_primary_key; pub mod metadata; pub mod repository; +pub mod satellite; pub mod schema; pub mod sea_orm_active_enums; pub mod self_join; @@ -14,6 +15,7 @@ pub use applog::Entity as Applog; pub use byte_primary_key::Entity as BytePrimaryKey; pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; +pub use satellite::Entity as Satellite; pub use schema::*; pub use sea_orm_active_enums::*; pub use self_join::Entity as SelfJoin; diff --git a/tests/common/features/satellite.rs b/tests/common/features/satellite.rs new file mode 100644 index 00000000..5db48274 --- /dev/null +++ b/tests/common/features/satellite.rs @@ -0,0 +1,18 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "satellite")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub satellite_name: String, + #[sea_orm(default_value = "2022-01-26 16:24:00")] + pub launch_date: DateTimeUtc, + #[sea_orm(default_value = "2022-01-26 16:24:00")] + pub deployment_date: DateTimeLocal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/tests/common/features/schema.rs b/tests/common/features/schema.rs index c38feb04..c1519253 100644 --- a/tests/common/features/schema.rs +++ b/tests/common/features/schema.rs @@ -16,6 +16,7 @@ pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> { create_repository_table(db).await?; create_self_join_table(db).await?; create_byte_primary_key_table(db).await?; + create_satellites_table(db).await?; let create_enum_stmts = match db_backend { DbBackend::MySql | DbBackend::Sqlite => Vec::new(), @@ -116,7 +117,7 @@ pub async fn create_self_join_table(db: &DbConn) -> Result { .col(ColumnDef::new(self_join::Column::Time).time()) .foreign_key( ForeignKeyCreateStatement::new() - .name("fk-self_join-self_join") + .name("fk-self_join-uuid_ref") .from_tbl(SelfJoin) .from_col(self_join::Column::UuidRef) .to_tbl(SelfJoin) @@ -201,3 +202,35 @@ pub async fn create_active_enum_child_table(db: &DbConn) -> Result Result { + let stmt = sea_query::Table::create() + .table(satellite::Entity) + .col( + ColumnDef::new(satellite::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(satellite::Column::SatelliteName) + .string() + .not_null(), + ) + .col( + ColumnDef::new(satellite::Column::LaunchDate) + .timestamp_with_time_zone() + .not_null() + .default("2022-01-26 16:24:00"), + ) + .col( + ColumnDef::new(satellite::Column::DeploymentDate) + .timestamp_with_time_zone() + .not_null() + .default("2022-01-26 16:24:00"), + ) + .to_owned(); + + create_table(db, &stmt, Satellite).await +} diff --git a/tests/timestamp_tests.rs b/tests/timestamp_tests.rs index cad35270..d0334a5c 100644 --- a/tests/timestamp_tests.rs +++ b/tests/timestamp_tests.rs @@ -1,5 +1,4 @@ pub mod common; - pub use common::{features::*, setup::*, TestContext}; use sea_orm::{entity::prelude::*, DatabaseConnection, IntoActiveModel}; @@ -13,6 +12,8 @@ async fn main() -> Result<(), DbErr> { let ctx = TestContext::new("bakery_chain_schema_timestamp_tests").await; create_tables(&ctx.db).await?; create_applog(&ctx.db).await?; + create_satellites_log(&ctx.db).await?; + ctx.delete().await; Ok(()) @@ -30,8 +31,26 @@ pub async fn create_applog(db: &DatabaseConnection) -> Result<(), DbErr> { .exec(db) .await?; - assert_eq!(log.id.clone(), res.last_insert_id); + assert_eq!(log.id, res.last_insert_id); assert_eq!(Applog::find().one(db).await?, Some(log.clone())); Ok(()) } + +pub async fn create_satellites_log(db: &DatabaseConnection) -> Result<(), DbErr> { + let archive = satellite::Model { + id: 1, + satellite_name: "Sea-00001-2022".to_owned(), + launch_date: "2022-01-07T12:11:23Z".parse().unwrap(), + deployment_date: "2022-01-07T12:11:23Z".parse().unwrap(), + }; + + let res = Satellite::insert(archive.clone().into_active_model()) + .exec(db) + .await?; + + assert_eq!(archive.id, res.last_insert_id); + assert_eq!(Satellite::find().one(db).await?, Some(archive.clone())); + + Ok(()) +}