diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 236a668e..6b11432a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -170,7 +170,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - path: [async-std, tokio, actix_example, rocket_example] + path: [async-std, tokio, actix_example, actix4_example, rocket_example] steps: - uses: actions/checkout@v2 @@ -282,7 +282,7 @@ jobs: matrix: version: [10.6] runtime: [async-std, actix, tokio] - tls: [rustls] + tls: [native-tls] services: mysql: image: mariadb:${{ matrix.version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e5038563..0398854a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## 0.2.4 - Prerelease +## 0.2.4 - 2021-10-01 - [[#186]] [sea-orm-cli] Foreign key handling - [[#191]] [sea-orm-cli] Unique key handling +- [[#182]] `find_linked` join with alias +- [[#202]] Accept both `postgres://` and `postgresql://` +- [[#208]] Support feteching T, (T, U), (T, U, P) etc +- [[#209]] Rename column name & column enum variant +- [[#207]] Support `chrono::NaiveDate` & `chrono::NaiveTime` +- Support `Condition::not` (from sea-query) [#186]: https://github.com/SeaQL/sea-orm/issues/186 [#191]: https://github.com/SeaQL/sea-orm/issues/191 +[#182]: https://github.com/SeaQL/sea-orm/pull/182 +[#202]: https://github.com/SeaQL/sea-orm/pull/202 +[#208]: https://github.com/SeaQL/sea-orm/pull/208 +[#209]: https://github.com/SeaQL/sea-orm/pull/209 +[#207]: https://github.com/SeaQL/sea-orm/pull/207 ## 0.2.3 - 2021-09-22 diff --git a/Cargo.toml b/Cargo.toml index 37aa1b6e..a4466152 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = [".", "sea-orm-macros", "sea-orm-codegen"] [package] name = "sea-orm" -version = "0.2.3" +version = "0.2.4" authors = ["Chris Tsang "] edition = "2018" description = "🐚 An async & dynamic ORM for Rust" @@ -29,8 +29,8 @@ futures = { version = "^0.3" } futures-util = { version = "^0.3" } log = { version = "^0.4", optional = true } rust_decimal = { version = "^1", optional = true } -sea-orm-macros = { version = "^0.2.3", path = "sea-orm-macros", optional = true } -sea-query = { version = "^0.16.3", features = ["thread-safe"] } +sea-orm-macros = { version = "^0.2.4", path = "sea-orm-macros", optional = true } +sea-query = { version = "^0.16.5", features = ["thread-safe"] } sea-strum = { version = "^0.21", features = ["derive", "sea-orm"] } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1", optional = true } diff --git a/README.md b/README.md index 2285c77c..b4525b31 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,9 @@ SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. -[![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) -[![Usage Example](https://img.shields.io/badge/Usage%20Example-green)](https://github.com/SeaQL/sea-orm/tree/master/examples/async-std) +[![Getting Started](https://img.shields.io/badge/Getting%20Started-brightgreen)](https://www.sea-ql.org/SeaORM/docs/index) +[![Usage Example](https://img.shields.io/badge/Usage%20Example-yellow)](https://github.com/SeaQL/sea-orm/tree/master/examples/async-std) +[![Actix Example](https://img.shields.io/badge/Actix%20Example-blue)](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) [![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) @@ -31,114 +32,45 @@ SeaORM is a relational ORM to help you build light weight and concurrent web ser Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. -```rust -// execute multiple queries in parallel -let cakes_and_fruits: (Vec, Vec) = - futures::try_join!(Cake::find().all(&db), Fruit::find().all(&db))?; -``` - 2. Dynamic Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. -```rust -// build subquery with ease -let cakes_with_filling: Vec = cake::Entity::find() - .filter( - Condition::any().add( - cake::Column::Id.in_subquery( - Query::select() - .column(cake_filling::Column::CakeId) - .from(cake_filling::Entity) - .to_owned(), - ), - ), - ) - .all(&db) - .await?; - -``` - 3. Testable Use mock connections to write unit tests for your logic. -```rust -// Setup mock connection -let db = MockDatabase::new(DbBackend::Postgres) - .append_query_results(vec![ - vec![ - cake::Model { - id: 1, - name: "New York Cheese".to_owned(), - }, - ], - ]) - .into_connection(); - -// Perform your application logic -assert_eq!( - cake::Entity::find().one(&db).await?, - Some(cake::Model { - id: 1, - name: "New York Cheese".to_owned(), - }) -); - -// Compare it against the expected transaction log -assert_eq!( - db.into_transaction_log(), - vec![ - Transaction::from_sql_and_values( - DbBackend::Postgres, - r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, - vec![1u64.into()] - ), - ] -); -``` - 4. Service Oriented Quickly build services that join, filter, sort and paginate data in APIs. +## A quick taste of SeaORM + +### Entity ```rust -#[get("/?&")] -async fn list( - conn: Connection, - page: Option, - per_page: Option, -) -> Template { - // Set page number and items per page - let page = page.unwrap_or(1); - let per_page = per_page.unwrap_or(10); +use sea_orm::entity::prelude::*; - // Setup paginator - let paginator = Post::find() - .order_by_asc(post::Column::Id) - .paginate(&conn, per_page); - let num_pages = paginator.num_pages().await.unwrap(); +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "cake")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, +} - // Fetch paginated posts - let posts = paginator - .fetch_page(page - 1) - .await - .expect("could not retrieve posts"); +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::fruit::Entity")] + Fruit, +} - Template::render( - "index", - context! { - page: page, - per_page: per_page, - posts: posts, - num_pages: num_pages, - }, - ) +impl Related for Entity { + fn to() -> RelationDef { + Relation::Fruit.def() + } } ``` -## A quick taste of SeaORM - ### Select ```rust // find all models diff --git a/examples/actix4_example/.env b/examples/actix4_example/.env new file mode 100644 index 00000000..d89d9e63 --- /dev/null +++ b/examples/actix4_example/.env @@ -0,0 +1,3 @@ +HOST=127.0.0.1 +PORT=8000 +DATABASE_URL="mysql://root:@localhost/actix_example" diff --git a/examples/actix4_example/Cargo.toml b/examples/actix4_example/Cargo.toml new file mode 100644 index 00000000..b03b7e85 --- /dev/null +++ b/examples/actix4_example/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "sea-orm-actix-4-beta-example" +version = "0.1.0" +authors = ["Sam Samai "] +edition = "2018" +publish = false + +[workspace] + +[dependencies] +actix-files = "0.6.0-beta.4" +actix-http = "=3.0.0-beta.5" +actix-rt = "2.2.0" +actix-service = "=2.0.0-beta.5" +actix-web = "=4.0.0-beta.5" + +tera = "1.8.0" +dotenv = "0.15" +listenfd = "0.3.3" +# remove `path = ""` in your own project +sea-orm = { path = "../../", version = "^0.2.3", features = [ + "macros", + "runtime-actix-native-tls", +], default-features = false } +serde = "1" +env_logger = "0.8" + +[features] +default = ["sqlx-mysql"] +sqlx-mysql = ["sea-orm/sqlx-mysql"] +sqlx-postgres = ["sea-orm/sqlx-postgres"] diff --git a/examples/actix4_example/README.md b/examples/actix4_example/README.md new file mode 100644 index 00000000..9eb73ea6 --- /dev/null +++ b/examples/actix4_example/README.md @@ -0,0 +1,17 @@ +# Actix 4 Beta with SeaORM example app + +Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`. + +```toml +[features] +default = ["sqlx-$DATABASE"] +``` + +Edit `.env` to point to your database. + +Run server with auto-reloading: + +```bash +cargo install systemfd +systemfd --no-pid -s http::8000 -- cargo watch -x run +``` diff --git a/examples/actix4_example/src/main.rs b/examples/actix4_example/src/main.rs new file mode 100644 index 00000000..6a52b81b --- /dev/null +++ b/examples/actix4_example/src/main.rs @@ -0,0 +1,200 @@ +use actix_files::Files as Fs; +use actix_web::{ + error, get, middleware, post, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result, +}; + +use listenfd::ListenFd; +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)] +struct AppState { + templates: tera::Tera, + conn: DatabaseConnection, +} +#[derive(Debug, Deserialize)] +pub struct Params { + page: Option, + posts_per_page: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +struct FlashData { + kind: String, + message: String, +} + +#[get("/")] +async fn list(req: HttpRequest, data: web::Data) -> Result { + let template = &data.templates; + let conn = &data.conn; + + // get params + let params = web::Query::::from_query(req.query_string()).unwrap(); + + let page = params.page.unwrap_or(1); + let posts_per_page = params.posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE); + let paginator = Post::find() + .order_by_asc(post::Column::Id) + .paginate(conn, posts_per_page); + let num_pages = paginator.num_pages().await.ok().unwrap(); + + let posts = paginator + .fetch_page(page - 1) + .await + .expect("could not retrieve posts"); + 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 = template + .render("index.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[get("/new")] +async fn new(data: web::Data) -> Result { + let template = &data.templates; + let ctx = tera::Context::new(); + let body = template + .render("new.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[post("/")] +async fn create( + data: web::Data, + post_form: web::Form, +) -> Result { + let conn = &data.conn; + + let form = post_form.into_inner(); + + post::ActiveModel { + title: Set(form.title.to_owned()), + text: Set(form.text.to_owned()), + ..Default::default() + } + .save(conn) + .await + .expect("could not insert post"); + + Ok(HttpResponse::Found().append_header(("location", "/")).finish()) +} + +#[get("/{id}")] +async fn edit(data: web::Data, id: web::Path) -> Result { + let conn = &data.conn; + let template = &data.templates; + + let post: post::Model = Post::find_by_id(id.into_inner()) + .one(conn) + .await + .expect("could not find post") + .unwrap(); + + let mut ctx = tera::Context::new(); + ctx.insert("post", &post); + + let body = template + .render("edit.html.tera", &ctx) + .map_err(|_| error::ErrorInternalServerError("Template error"))?; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) +} + +#[post("/{id}")] +async fn update( + data: web::Data, + id: web::Path, + post_form: web::Form, +) -> Result { + let conn = &data.conn; + let form = post_form.into_inner(); + + post::ActiveModel { + id: Set(id.into_inner()), + title: Set(form.title.to_owned()), + text: Set(form.text.to_owned()), + } + .save(conn) + .await + .expect("could not edit post"); + + Ok(HttpResponse::Found().append_header(("location", "/")).finish()) +} + +#[post("/delete/{id}")] +async fn delete(data: web::Data, id: web::Path) -> Result { + let conn = &data.conn; + + let post: post::ActiveModel = Post::find_by_id(id.into_inner()) + .one(conn) + .await + .unwrap() + .unwrap() + .into(); + + post.delete(conn).await.unwrap(); + + Ok(HttpResponse::Found().append_header(("location", "/")).finish()) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + std::env::set_var("RUST_LOG", "debug"); + env_logger::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(); + let _ = setup::create_post_table(&conn).await; + let templates = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); + let state = AppState { templates, conn }; + + let mut listenfd = ListenFd::from_env(); + let mut server = HttpServer::new(move || { + App::new() + .service(Fs::new("/static", "./static")) + .data(state.clone()) + .wrap(middleware::Logger::default()) // enable logger + .configure(init) + }); + + server = match listenfd.take_tcp_listener(0)? { + Some(listener) => server.listen(listener)?, + None => server.bind(&server_url)?, + }; + + println!("Starting server at {}", server_url); + server.run().await?; + + Ok(()) +} + +pub fn init(cfg: &mut web::ServiceConfig) { + cfg.service(list); + cfg.service(new); + cfg.service(create); + cfg.service(edit); + cfg.service(update); + cfg.service(delete); +} diff --git a/examples/actix4_example/src/post.rs b/examples/actix4_example/src/post.rs new file mode 100644 index 00000000..37a99c2f --- /dev/null +++ b/examples/actix4_example/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/actix4_example/src/setup.rs b/examples/actix4_example/src/setup.rs new file mode 100644 index 00000000..034e8b53 --- /dev/null +++ b/examples/actix4_example/src/setup.rs @@ -0,0 +1,33 @@ +use sea_orm::sea_query::{ColumnDef, TableCreateStatement}; +use sea_orm::{error::*, sea_query, DbConn, ExecResult}; + +async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result { + let builder = db.get_database_backend(); + db.execute(builder.build(stmt)).await +} + +pub async fn create_post_table(db: &DbConn) -> Result { + let stmt = sea_query::Table::create() + .table(super::post::Entity) + .if_not_exists() + .col( + ColumnDef::new(super::post::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col( + ColumnDef::new(super::post::Column::Title) + .string() + .not_null(), + ) + .col( + ColumnDef::new(super::post::Column::Text) + .string() + .not_null(), + ) + .to_owned(); + + create_table(db, &stmt).await +} diff --git a/examples/actix4_example/static/css/normalize.css b/examples/actix4_example/static/css/normalize.css new file mode 100644 index 00000000..458eea1e --- /dev/null +++ b/examples/actix4_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/actix4_example/static/css/skeleton.css b/examples/actix4_example/static/css/skeleton.css new file mode 100644 index 00000000..cdc432a4 --- /dev/null +++ b/examples/actix4_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/actix4_example/static/css/style.css b/examples/actix4_example/static/css/style.css new file mode 100644 index 00000000..ac2720d3 --- /dev/null +++ b/examples/actix4_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/actix4_example/static/images/favicon.png b/examples/actix4_example/static/images/favicon.png new file mode 100644 index 00000000..02b73904 Binary files /dev/null and b/examples/actix4_example/static/images/favicon.png differ diff --git a/examples/actix4_example/templates/edit.html.tera b/examples/actix4_example/templates/edit.html.tera new file mode 100644 index 00000000..5aeb22af --- /dev/null +++ b/examples/actix4_example/templates/edit.html.tera @@ -0,0 +1,49 @@ +{% extends "layout.html.tera" %} {% block content %} +
+

Edit Post

+
+
+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+{% endblock content %} diff --git a/examples/actix4_example/templates/error/404.html.tera b/examples/actix4_example/templates/error/404.html.tera new file mode 100644 index 00000000..afda653d --- /dev/null +++ b/examples/actix4_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/actix4_example/templates/index.html.tera b/examples/actix4_example/templates/index.html.tera new file mode 100644 index 00000000..1dccdeba --- /dev/null +++ b/examples/actix4_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/actix4_example/templates/layout.html.tera b/examples/actix4_example/templates/layout.html.tera new file mode 100644 index 00000000..c930afcc --- /dev/null +++ b/examples/actix4_example/templates/layout.html.tera @@ -0,0 +1,26 @@ + + + + + Actix Example + + + + + + + + + + + +
+

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

New Post

+
+
+ + +
+
+
+ + + +
+
+
+ +
+
+
+
+{% endblock content %} diff --git a/examples/actix_example/.env b/examples/actix_example/.env index f6b73c71..94ec74e3 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:root@localhost/actix_example" \ No newline at end of file +DATABASE_URL="sql://root:@localhost/actix_example" \ No newline at end of file diff --git a/examples/actix_example/README.md b/examples/actix_example/README.md index 3785c036..d1ba90bf 100644 --- a/examples/actix_example/README.md +++ b/examples/actix_example/README.md @@ -1,10 +1,17 @@ # Actix with SeaORM example app +Edit `Cargo.toml` to use `sqlx-mysql` or `sqlx-postgres`. + +```toml +[features] +default = ["sqlx-$DATABASE"] +``` + Edit `.env` to point to your database. Run server with auto-reloading: ```bash cargo install systemfd -systemfd --no-pid -s http::5000 -- cargo watch -x run +systemfd --no-pid -s http::8000 -- cargo watch -x run ``` diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index 100c747c..8fe9c4a4 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -41,6 +41,15 @@ pub fn build_cli() -> App<'static, 'static> { .help("Generate entity file for hidden tables (i.e. table name starts with an underscore)") .takes_value(false), ) + .arg( + Arg::with_name("TABLES") + .long("tables") + .short("t") + .use_delimiter(true) + .help("Generate entity file for specified tables only (comma seperated)") + .takes_value(true) + .conflicts_with("INCLUDE_HIDDEN_TABLES"), + ) .arg( Arg::with_name("EXPANDED_FORMAT") .long("expanded-format") diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index 527e1264..c318a510 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -26,7 +26,15 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box>(); let expanded_format = args.is_present("EXPANDED_FORMAT"); + let filter_tables = |table: &str| -> bool { + if tables.len() > 0 { + return tables.contains(&table); + } + + true + }; let filter_hidden_tables = |table: &str| -> bool { if include_hidden_tables { true @@ -53,6 +61,7 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box) -> Result<(), Box" ] edition = "2018" description = "Derive macros for SeaORM" diff --git a/sea-orm-macros/src/derives/active_model.rs b/sea-orm-macros/src/derives/active_model.rs index 3a96860e..2227f09b 100644 --- a/sea-orm-macros/src/derives/active_model.rs +++ b/sea-orm-macros/src/derives/active_model.rs @@ -2,7 +2,7 @@ use crate::util::field_not_ignored; use heck::CamelCase; use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote, quote_spanned}; -use syn::{Data, DataStruct, Field, Fields, Type}; +use syn::{punctuated::Punctuated, token::Comma, Data, DataStruct, Field, Fields, Lit, Meta, Type}; pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result { let fields = match data { @@ -28,7 +28,36 @@ pub fn expand_derive_active_model(ident: Ident, data: Data) -> syn::Result = fields .clone() .into_iter() - .map(|Field { ident, .. }| format_ident!("{}", ident.unwrap().to_string().to_camel_case())) + .map(|field| { + let mut ident = format_ident!( + "{}", + field.ident.as_ref().unwrap().to_string().to_camel_case() + ); + for attr in field.attrs.iter() { + if let Some(ident) = attr.path.get_ident() { + if ident != "sea_orm" { + continue; + } + } else { + continue; + } + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) + { + for meta in list.iter() { + if let Meta::NameValue(nv) = meta { + if let Some(name) = nv.path.get_ident() { + if name == "enum_name" { + if let Lit::Str(litstr) = &nv.lit { + ident = syn::parse_str(&litstr.value()).unwrap(); + } + } + } + } + } + } + } + ident + }) .collect(); let ty: Vec = fields.into_iter().map(|Field { ty, .. }| ty).collect(); diff --git a/sea-orm-macros/src/derives/column.rs b/sea-orm-macros/src/derives/column.rs index 16f9bb22..5fc471e9 100644 --- a/sea-orm-macros/src/derives/column.rs +++ b/sea-orm-macros/src/derives/column.rs @@ -1,7 +1,7 @@ use heck::{MixedCase, SnakeCase}; use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; -use syn::{Data, DataEnum, Fields, Variant}; +use syn::{punctuated::Punctuated, token::Comma, Data, DataEnum, Fields, Lit, Meta, Variant}; pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result { let variants = match data { @@ -25,8 +25,31 @@ pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result = variants .iter() .map(|v| { - let ident = v.ident.to_string().to_snake_case(); - quote! { #ident } + let mut column_name = v.ident.to_string().to_snake_case(); + for attr in v.attrs.iter() { + if let Some(ident) = attr.path.get_ident() { + if ident != "sea_orm" { + continue; + } + } else { + continue; + } + if let Ok(list) = attr.parse_args_with(Punctuated::::parse_terminated) + { + for meta in list.iter() { + if let Meta::NameValue(nv) = meta { + if let Some(name) = nv.path.get_ident() { + if name == "column_name" { + if let Lit::Str(litstr) = &nv.lit { + column_name = litstr.value(); + } + } + } + } + } + } + } + quote! { #column_name } }) .collect(); diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index f0772763..5d0fa511 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -60,9 +60,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res if let Fields::Named(fields) = item_struct.fields { for field in fields.named { if let Some(ident) = &field.ident { - let field_name = + let mut field_name = Ident::new(&ident.to_string().to_case(Case::Pascal), Span::call_site()); - columns_enum.push(quote! { #field_name }); let mut nullable = false; let mut default_value = None; @@ -71,7 +70,10 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res let mut ignore = false; let mut unique = false; let mut sql_type = None; - // search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(Some(255))", default_value = "new user", default_expr = "gen_random_uuid()", nullable, indexed, unique)] + let mut column_name = None; + let mut enum_name = None; + let mut is_primary_key = false; + // search for #[sea_orm(primary_key, auto_increment = false, column_type = "String(Some(255))", default_value = "new user", default_expr = "gen_random_uuid()", column_name = "name", enum_name = "Name", nullable, indexed, unique)] for attr in field.attrs.iter() { if let Some(ident) = attr.path.get_ident() { if ident != "sea_orm" { @@ -116,6 +118,26 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res default_value = Some(nv.lit.to_owned()); } else if name == "default_expr" { default_expr = Some(nv.lit.to_owned()); + } else if name == "column_name" { + if let Lit::Str(litstr) = &nv.lit { + column_name = Some(litstr.value()); + } else { + return Err(Error::new( + field.span(), + format!("Invalid column_name {:?}", nv.lit), + )); + } + } else if name == "enum_name" { + if let Lit::Str(litstr) = &nv.lit { + let ty: Ident = + syn::parse_str(&litstr.value())?; + enum_name = Some(ty); + } else { + return Err(Error::new( + field.span(), + format!("Invalid enum_name {:?}", nv.lit), + )); + } } } } @@ -125,7 +147,7 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res ignore = true; break; } else if name == "primary_key" { - primary_keys.push(quote! { #field_name }); + is_primary_key = true; primary_key_types.push(field.ty.clone()); } else if name == "nullable" { nullable = true; @@ -142,9 +164,27 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res } } + if let Some(enum_name) = enum_name { + field_name = enum_name; + } + if ignore { - columns_enum.pop(); continue; + } else { + let variant_attrs = match &column_name { + Some(column_name) => quote! { + #[sea_orm(column_name = #column_name)] + }, + None => quote! {}, + }; + columns_enum.push(quote! { + #variant_attrs + #field_name + }); + } + + if is_primary_key { + primary_keys.push(quote! { #field_name }); } let field_type = match sql_type { @@ -170,8 +210,8 @@ pub fn expand_derive_entity_model(data: Data, attrs: Vec) -> syn::Res "f32" => quote! { Float }, "f64" => quote! { Double }, "bool" => quote! { Boolean }, - "NaiveDate" => quote! { Date }, - "NaiveTime" => quote! { Time }, + "Date" | "NaiveDate" => quote! { Date }, + "Time" | "NaiveTime" => quote! { Time }, "DateTime" | "NaiveDateTime" => { quote! { DateTime } } diff --git a/sea-orm-macros/src/derives/model.rs b/sea-orm-macros/src/derives/model.rs index 9d619991..a43b487f 100644 --- a/sea-orm-macros/src/derives/model.rs +++ b/sea-orm-macros/src/derives/model.rs @@ -3,7 +3,7 @@ use heck::CamelCase; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned}; use std::iter::FromIterator; -use syn::Ident; +use syn::{punctuated::Punctuated, token::Comma, Ident, Lit, Meta}; enum Error { InputNotStruct, @@ -43,10 +43,35 @@ impl DeriveModel { let column_idents = fields .iter() .map(|field| { - format_ident!( + let mut ident = format_ident!( "{}", field.ident.as_ref().unwrap().to_string().to_camel_case() - ) + ); + for attr in field.attrs.iter() { + if let Some(ident) = attr.path.get_ident() { + if ident != "sea_orm" { + continue; + } + } else { + continue; + } + if let Ok(list) = + attr.parse_args_with(Punctuated::::parse_terminated) + { + for meta in list.iter() { + if let Meta::NameValue(nv) = meta { + if let Some(name) = nv.path.get_ident() { + if name == "enum_name" { + if let Lit::Str(litstr) = &nv.lit { + ident = syn::parse_str(&litstr.value()).unwrap(); + } + } + } + } + } + } + } + ident }) .collect(); diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 629c5c18..c3f8a50f 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -46,7 +46,7 @@ pub fn derive_primary_key(input: TokenStream) -> TokenStream { } } -#[proc_macro_derive(DeriveColumn)] +#[proc_macro_derive(DeriveColumn, attributes(sea_orm))] pub fn derive_column(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input); diff --git a/src/docs.rs b/src/docs.rs new file mode 100644 index 00000000..bab054ef --- /dev/null +++ b/src/docs.rs @@ -0,0 +1,166 @@ +//! 1. Async +//! +//! Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. +//! +//! ``` +//! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*, DatabaseConnection, DbBackend, MockDatabase, Transaction, IntoMockRow}; +//! # let db = MockDatabase::new(DbBackend::Postgres) +//! # .append_query_results(vec![ +//! # vec![cake::Model { +//! # id: 1, +//! # name: "New York Cheese".to_owned(), +//! # } +//! # .into_mock_row()], +//! # vec![fruit::Model { +//! # id: 1, +//! # name: "Apple".to_owned(), +//! # cake_id: Some(1), +//! # } +//! # .into_mock_row()], +//! # ]) +//! # .into_connection(); +//! # let _: Result<(), DbErr> = smol::block_on(async { +//! // execute multiple queries in parallel +//! let cakes_and_fruits: (Vec, Vec) = +//! futures::try_join!(Cake::find().all(&db), Fruit::find().all(&db))?; +//! # assert_eq!( +//! # cakes_and_fruits, +//! # ( +//! # vec![cake::Model { +//! # id: 1, +//! # name: "New York Cheese".to_owned(), +//! # }], +//! # vec![fruit::Model { +//! # id: 1, +//! # 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", "cake"."name" FROM "cake""#, +//! # vec![] +//! # ), +//! # Transaction::from_sql_and_values( +//! # DbBackend::Postgres, +//! # r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit""#, +//! # vec![] +//! # ), +//! # ] +//! # ); +//! # Ok(()) +//! # }); +//! ``` +//! +//! 2. Dynamic +//! +//! Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. +//! +//! ``` +//! # use sea_query::Query; +//! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*}; +//! # async fn function(db: DbConn) -> Result<(), DbErr> { +//! // build subquery with ease +//! let cakes_with_filling: Vec = cake::Entity::find() +//! .filter( +//! Condition::any().add( +//! cake::Column::Id.in_subquery( +//! Query::select() +//! .column(cake_filling::Column::CakeId) +//! .from(cake_filling::Entity) +//! .to_owned(), +//! ), +//! ), +//! ) +//! .all(&db) +//! .await?; +//! +//! # Ok(()) +//! # } +//! ``` +//! +//! 3. Testable +//! +//! Use mock connections to write unit tests for your logic. +//! +//! ``` +//! # use sea_orm::{error::*, entity::*, query::*, tests_cfg::*, DbConn, MockDatabase, Transaction, DbBackend}; +//! # async fn function(db: DbConn) -> Result<(), DbErr> { +//! // Setup mock connection +//! let db = MockDatabase::new(DbBackend::Postgres) +//! .append_query_results(vec![ +//! vec![ +//! cake::Model { +//! id: 1, +//! name: "New York Cheese".to_owned(), +//! }, +//! ], +//! ]) +//! .into_connection(); +//! +//! // Perform your application logic +//! assert_eq!( +//! cake::Entity::find().one(&db).await?, +//! Some(cake::Model { +//! id: 1, +//! name: "New York Cheese".to_owned(), +//! }) +//! ); +//! +//! // Compare it against the expected transaction log +//! assert_eq!( +//! db.into_transaction_log(), +//! vec![ +//! Transaction::from_sql_and_values( +//! DbBackend::Postgres, +//! r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, +//! vec![1u64.into()] +//! ), +//! ] +//! ); +//! # Ok(()) +//! # } +//! ``` +//! +//! 4. Service Oriented +//! +//! Quickly build services that join, filter, sort and paginate data in APIs. +//! +//! ```ignore +//! #[get("/?&")] +//! async fn list( +//! conn: Connection, +//! page: Option, +//! per_page: Option, +//! ) -> Template { +//! // Set page number and items per page +//! let page = page.unwrap_or(1); +//! let per_page = per_page.unwrap_or(10); +//! +//! // Setup paginator +//! let paginator = Post::find() +//! .order_by_asc(post::Column::Id) +//! .paginate(&conn, per_page); +//! let num_pages = paginator.num_pages().await.unwrap(); +//! +//! // Fetch paginated posts +//! let posts = paginator +//! .fetch_page(page - 1) +//! .await +//! .expect("could not retrieve posts"); +//! +//! Template::render( +//! "index", +//! context! { +//! page: page, +//! per_page: per_page, +//! posts: posts, +//! num_pages: num_pages, +//! }, +//! ) +//! } +//! ``` \ No newline at end of file diff --git a/src/entity/column.rs b/src/entity/column.rs index 32500630..a27215e5 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -453,4 +453,353 @@ mod tests { ColumnType::Integer.def().unique().indexed().nullable() ); } + + #[test] + #[cfg(feature = "macros")] + fn column_name_1() { + use sea_query::Iden; + + mod hello { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "hello")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(column_name = "ONE")] + pub one: i32, + pub two: i32, + #[sea_orm(column_name = "3")] + pub three: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(hello::Column::One.to_string().as_str(), "ONE"); + assert_eq!(hello::Column::Two.to_string().as_str(), "two"); + assert_eq!(hello::Column::Three.to_string().as_str(), "3"); + } + + #[test] + #[cfg(feature = "macros")] + fn column_name_2() { + use sea_query::Iden; + + mod hello { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Copy, Clone, Default, Debug, DeriveEntity)] + pub struct Entity; + + impl EntityName for Entity { + fn table_name(&self) -> &str { + "hello" + } + } + + #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] + pub struct Model { + pub id: i32, + pub one: i32, + pub two: i32, + pub three: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + pub enum Column { + Id, + #[sea_orm(column_name = "ONE")] + One, + Two, + #[sea_orm(column_name = "3")] + Three, + } + + impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Column::Id => ColumnType::Integer.def(), + Column::One => ColumnType::Integer.def(), + Column::Two => ColumnType::Integer.def(), + Column::Three => ColumnType::Integer.def(), + } + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] + pub enum PrimaryKey { + Id, + } + + impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + + fn auto_increment() -> bool { + true + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(hello::Column::One.to_string().as_str(), "ONE"); + assert_eq!(hello::Column::Two.to_string().as_str(), "two"); + assert_eq!(hello::Column::Three.to_string().as_str(), "3"); + } + + #[test] + #[cfg(feature = "macros")] + fn enum_name_1() { + use sea_query::Iden; + + mod hello { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "hello")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(enum_name = "One1")] + pub one: i32, + pub two: i32, + #[sea_orm(enum_name = "Three3")] + pub three: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(hello::Column::One1.to_string().as_str(), "one1"); + assert_eq!(hello::Column::Two.to_string().as_str(), "two"); + assert_eq!(hello::Column::Three3.to_string().as_str(), "three3"); + } + + #[test] + #[cfg(feature = "macros")] + fn enum_name_2() { + use sea_query::Iden; + + mod hello { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Copy, Clone, Default, Debug, DeriveEntity)] + pub struct Entity; + + impl EntityName for Entity { + fn table_name(&self) -> &str { + "hello" + } + } + + #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] + pub struct Model { + pub id: i32, + #[sea_orm(enum_name = "One1")] + pub one: i32, + pub two: i32, + #[sea_orm(enum_name = "Three3")] + pub three: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + pub enum Column { + Id, + One1, + Two, + Three3, + } + + impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Column::Id => ColumnType::Integer.def(), + Column::One1 => ColumnType::Integer.def(), + Column::Two => ColumnType::Integer.def(), + Column::Three3 => ColumnType::Integer.def(), + } + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] + pub enum PrimaryKey { + Id, + } + + impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + + fn auto_increment() -> bool { + true + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(hello::Column::One1.to_string().as_str(), "one1"); + assert_eq!(hello::Column::Two.to_string().as_str(), "two"); + assert_eq!(hello::Column::Three3.to_string().as_str(), "three3"); + } + + #[test] + #[cfg(feature = "macros")] + fn column_name_enum_name_1() { + use sea_query::Iden; + + mod hello { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "hello")] + pub struct Model { + #[sea_orm(primary_key, column_name = "ID", enum_name = "IdentityColumn")] + pub id: i32, + #[sea_orm(column_name = "ONE", enum_name = "One1")] + pub one: i32, + pub two: i32, + #[sea_orm(column_name = "THREE", enum_name = "Three3")] + pub three: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(hello::Column::IdentityColumn.to_string().as_str(), "ID"); + assert_eq!(hello::Column::One1.to_string().as_str(), "ONE"); + assert_eq!(hello::Column::Two.to_string().as_str(), "two"); + assert_eq!(hello::Column::Three3.to_string().as_str(), "THREE"); + } + + #[test] + #[cfg(feature = "macros")] + fn column_name_enum_name_2() { + use sea_query::Iden; + + mod hello { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Copy, Clone, Default, Debug, DeriveEntity)] + pub struct Entity; + + impl EntityName for Entity { + fn table_name(&self) -> &str { + "hello" + } + } + + #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] + pub struct Model { + #[sea_orm(enum_name = "IdentityCol")] + pub id: i32, + #[sea_orm(enum_name = "One1")] + pub one: i32, + pub two: i32, + #[sea_orm(enum_name = "Three3")] + pub three: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + pub enum Column { + #[sea_orm(column_name = "ID")] + IdentityCol, + #[sea_orm(column_name = "ONE")] + One1, + Two, + #[sea_orm(column_name = "THREE")] + Three3, + } + + impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Column::IdentityCol => ColumnType::Integer.def(), + Column::One1 => ColumnType::Integer.def(), + Column::Two => ColumnType::Integer.def(), + Column::Three3 => ColumnType::Integer.def(), + } + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] + pub enum PrimaryKey { + IdentityCol, + } + + impl PrimaryKeyTrait for PrimaryKey { + type ValueType = i32; + + fn auto_increment() -> bool { + true + } + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(hello::Column::IdentityCol.to_string().as_str(), "ID"); + assert_eq!(hello::Column::One1.to_string().as_str(), "ONE"); + assert_eq!(hello::Column::Two.to_string().as_str(), "two"); + assert_eq!(hello::Column::Three3.to_string().as_str(), "THREE"); + } + + #[test] + #[cfg(feature = "macros")] + fn column_name_enum_name_3() { + use sea_query::Iden; + + mod my_entity { + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "my_entity")] + pub struct Model { + #[sea_orm(primary_key, enum_name = "IdentityColumn", column_name = "id")] + pub id: i32, + #[sea_orm(column_name = "type")] + pub type_: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} + } + + assert_eq!(my_entity::Column::IdentityColumn.to_string().as_str(), "id"); + assert_eq!(my_entity::Column::Type.to_string().as_str(), "type"); + } } diff --git a/src/entity/model.rs b/src/entity/model.rs index ce4a3a4c..318b8e70 100644 --- a/src/entity/model.rs +++ b/src/entity/model.rs @@ -44,16 +44,12 @@ pub trait FromQueryResult: Sized { /// # .append_query_results(vec![vec![ /// # maplit::btreemap! { /// # "name" => Into::::into("Chocolate Forest"), - /// # "num_of_cakes" => Into::::into(1), - /// # }, - /// # maplit::btreemap! { - /// # "name" => Into::::into("New York Cheese"), - /// # "num_of_cakes" => Into::::into(1), + /// # "num_of_cakes" => Into::::into(2), /// # }, /// # ]]) /// # .into_connection(); /// # - /// use sea_orm::{entity::*, query::*, tests_cfg::cake, FromQueryResult}; + /// use sea_orm::{query::*, FromQueryResult}; /// /// #[derive(Debug, PartialEq, FromQueryResult)] /// struct SelectResult { @@ -65,7 +61,7 @@ pub trait FromQueryResult: Sized { /// # /// let res: Vec = SelectResult::find_by_statement(Statement::from_sql_and_values( /// DbBackend::Postgres, - /// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, + /// r#"SELECT "name", COUNT(*) AS "num_of_cakes" FROM "cake" GROUP BY("name")"#, /// vec![], /// )) /// .all(&db) @@ -76,26 +72,21 @@ pub trait FromQueryResult: Sized { /// vec![ /// SelectResult { /// name: "Chocolate Forest".to_owned(), - /// num_of_cakes: 1, - /// }, - /// SelectResult { - /// name: "New York Cheese".to_owned(), - /// num_of_cakes: 1, + /// num_of_cakes: 2, /// }, /// ] /// ); /// # /// # Ok(()) /// # }); - /// - /// assert_eq!( - /// db.into_transaction_log(), - /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, - /// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, - /// vec![] - /// ),] - /// ); + /// # assert_eq!( + /// # db.into_transaction_log(), + /// # vec![Transaction::from_sql_and_values( + /// # DbBackend::Postgres, + /// # r#"SELECT "name", COUNT(*) AS "num_of_cakes" FROM "cake" GROUP BY("name")"#, + /// # vec![] + /// # ),] + /// # ); /// ``` fn find_by_statement(stmt: Statement) -> SelectorRaw> { SelectorRaw::>::from_statement(stmt) diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index be630567..1fecfa96 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -1,15 +1,25 @@ pub use crate::{ error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType, - DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, - DeriveEntityModel, DeriveModel, DerivePrimaryKey, DeriveRelation, EntityName, EntityTrait, - EnumIter, ForeignKeyAction, Iden, IdenStatic, IntoActiveModel, Linked, ModelTrait, + EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden, IdenStatic, Linked, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, PrimaryKeyValue, QueryFilter, QueryResult, Related, RelationDef, RelationTrait, Select, Value, }; +#[cfg(feature = "macros")] +pub use crate::{ + DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, + DeriveEntityModel, DeriveModel, DerivePrimaryKey, DeriveRelation, +}; + #[cfg(feature = "with-json")] pub use serde_json::Value as Json; +#[cfg(feature = "with-chrono")] +pub use chrono::NaiveDate as Date; + +#[cfg(feature = "with-chrono")] +pub use chrono::NaiveTime as Time; + #[cfg(feature = "with-chrono")] pub use chrono::NaiveDateTime as DateTime; diff --git a/src/executor/query.rs b/src/executor/query.rs index ae09f97c..0248fa5c 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -1,6 +1,6 @@ #[cfg(feature = "mock")] use crate::debug_print; -use crate::DbErr; +use crate::{DbErr, SelectGetableValue, SelectorRaw, Statement}; use std::fmt; #[derive(Debug)] @@ -241,6 +241,12 @@ try_getable_all!(Vec); #[cfg(feature = "with-json")] try_getable_all!(serde_json::Value); +#[cfg(feature = "with-chrono")] +try_getable_all!(chrono::NaiveDate); + +#[cfg(feature = "with-chrono")] +try_getable_all!(chrono::NaiveTime); + #[cfg(feature = "with-chrono")] try_getable_all!(chrono::NaiveDateTime); @@ -302,6 +308,69 @@ try_getable_all!(uuid::Uuid); pub trait TryGetableMany: Sized { fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result; + + /// ``` + /// # #[cfg(all(feature = "mock", feature = "macros"))] + /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![ + /// # maplit::btreemap! { + /// # "name" => Into::::into("Chocolate Forest"), + /// # "num_of_cakes" => Into::::into(1), + /// # }, + /// # maplit::btreemap! { + /// # "name" => Into::::into("New York Cheese"), + /// # "num_of_cakes" => Into::::into(1), + /// # }, + /// # ]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake, EnumIter, DeriveIden, TryGetableMany}; + /// + /// #[derive(EnumIter, DeriveIden)] + /// enum ResultCol { + /// Name, + /// NumOfCakes, + /// } + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let res: Vec<(String, i32)> = + /// <(String, i32)>::find_by_statement::(Statement::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, + /// vec![], + /// )) + /// .all(&db) + /// .await?; + /// + /// assert_eq!( + /// res, + /// vec![ + /// ("Chocolate Forest".to_owned(), 1), + /// ("New York Cheese".to_owned(), 1), + /// ] + /// ); + /// # + /// # Ok(()) + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, + /// vec![] + /// ),] + /// ); + /// ``` + fn find_by_statement(stmt: Statement) -> SelectorRaw> + where + C: sea_strum::IntoEnumIterator + sea_query::Iden, + { + SelectorRaw::>::with_columns(stmt) + } } impl TryGetableMany for T @@ -314,6 +383,15 @@ where } } +impl TryGetableMany for (T,) +where + T: TryGetableMany, +{ + fn try_get_many(res: &QueryResult, pre: &str, cols: &[String]) -> Result { + T::try_get_many(res, pre, cols).map(|r| (r,)) + } +} + impl TryGetableMany for (A, B) where A: TryGetable, diff --git a/src/executor/select.rs b/src/executor/select.rs index 959091c0..bb386722 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -1,7 +1,7 @@ use crate::{ error::*, DatabaseConnection, EntityTrait, FromQueryResult, IdenStatic, Iterable, JsonValue, ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo, - SelectTwoMany, Statement, + SelectTwoMany, Statement, TryGetableMany, }; use sea_query::SelectStatement; use std::marker::PhantomData; @@ -30,6 +30,16 @@ pub trait SelectorTrait { fn from_raw_query_result(res: QueryResult) -> Result; } +#[derive(Debug)] +pub struct SelectGetableValue +where + T: TryGetableMany, + C: sea_strum::IntoEnumIterator + sea_query::Iden, +{ + columns: PhantomData, + model: PhantomData, +} + #[derive(Debug)] pub struct SelectModel where @@ -47,6 +57,19 @@ where model: PhantomData<(M, N)>, } +impl SelectorTrait for SelectGetableValue +where + T: TryGetableMany, + C: sea_strum::IntoEnumIterator + sea_query::Iden, +{ + type Item = T; + + fn from_raw_query_result(res: QueryResult) -> Result { + let cols: Vec = C::iter().map(|col| col.to_string()).collect(); + T::try_get_many(&res, "", &cols).map_err(Into::into) + } +} + impl SelectorTrait for SelectModel where M: FromQueryResult + Sized, @@ -103,6 +126,115 @@ where } } + /// ``` + /// # #[cfg(all(feature = "mock", feature = "macros"))] + /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![ + /// # maplit::btreemap! { + /// # "cake_name" => Into::::into("Chocolate Forest"), + /// # }, + /// # maplit::btreemap! { + /// # "cake_name" => Into::::into("New York Cheese"), + /// # }, + /// # ]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake, DeriveColumn, EnumIter}; + /// + /// #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + /// enum QueryAs { + /// CakeName, + /// } + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let res: Vec = cake::Entity::find() + /// .select_only() + /// .column_as(cake::Column::Name, QueryAs::CakeName) + /// .into_values::<_, QueryAs>() + /// .all(&db) + /// .await?; + /// + /// assert_eq!( + /// res, + /// vec!["Chocolate Forest".to_owned(), "New York Cheese".to_owned()] + /// ); + /// # + /// # Ok(()) + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// r#"SELECT "cake"."name" AS "cake_name" FROM "cake""#, + /// vec![] + /// )] + /// ); + /// ``` + /// + /// ``` + /// # #[cfg(all(feature = "mock", feature = "macros"))] + /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres) + /// # .append_query_results(vec![vec![ + /// # maplit::btreemap! { + /// # "cake_name" => Into::::into("Chocolate Forest"), + /// # "num_of_cakes" => Into::::into(2i64), + /// # }, + /// # ]]) + /// # .into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake, DeriveColumn, EnumIter}; + /// + /// #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + /// enum QueryAs { + /// CakeName, + /// NumOfCakes, + /// } + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let res: Vec<(String, i64)> = cake::Entity::find() + /// .select_only() + /// .column_as(cake::Column::Name, QueryAs::CakeName) + /// .column_as(cake::Column::Id.count(), QueryAs::NumOfCakes) + /// .group_by(cake::Column::Name) + /// .into_values::<_, QueryAs>() + /// .all(&db) + /// .await?; + /// + /// assert_eq!( + /// res, + /// vec![("Chocolate Forest".to_owned(), 2i64)] + /// ); + /// # + /// # Ok(()) + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![Transaction::from_sql_and_values( + /// DbBackend::Postgres, + /// vec![ + /// r#"SELECT "cake"."name" AS "cake_name", COUNT("cake"."id") AS "num_of_cakes""#, + /// r#"FROM "cake" GROUP BY "cake"."name""#, + /// ].join(" ").as_str(), + /// vec![] + /// )] + /// ); + /// ``` + pub fn into_values(self) -> Selector> + where + T: TryGetableMany, + C: sea_strum::IntoEnumIterator + sea_query::Iden, + { + Selector::>::with_columns(self.query) + } + pub async fn one(self, db: &DatabaseConnection) -> Result, DbErr> { self.into_model().one(db).await } @@ -228,6 +360,22 @@ impl Selector where S: SelectorTrait, { + /// Create `Selector` from Statment and columns. Executing this `Selector` + /// will return a type `T` which implement `TryGetableMany`. + pub fn with_columns(query: SelectStatement) -> Selector> + where + T: TryGetableMany, + C: sea_strum::IntoEnumIterator + sea_query::Iden, + { + Selector { + query, + selector: SelectGetableValue { + columns: PhantomData, + model: PhantomData, + }, + } + } + pub async fn one(mut self, db: &DatabaseConnection) -> Result, DbErr> { let builder = db.get_database_backend(); self.query.limit(1); @@ -275,6 +423,22 @@ where } } + /// Create `SelectorRaw` from Statment and columns. Executing this `SelectorRaw` will + /// return a type `T` which implement `TryGetableMany`. + pub fn with_columns(stmt: Statement) -> SelectorRaw> + where + T: TryGetableMany, + C: sea_strum::IntoEnumIterator + sea_query::Iden, + { + SelectorRaw { + stmt, + selector: SelectGetableValue { + columns: PhantomData, + model: PhantomData, + }, + } + } + /// ``` /// # #[cfg(feature = "mock")] /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; diff --git a/src/lib.rs b/src/lib.rs index a1dc83f7..910044a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,8 +27,9 @@ //! //! SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. //! -//! [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) -//! [![Usage Example](https://img.shields.io/badge/Usage%20Example-green)](https://github.com/SeaQL/sea-orm/tree/master/examples/async-std) +//! [![Getting Started](https://img.shields.io/badge/Getting%20Started-brightgreen)](https://www.sea-ql.org/SeaORM/docs/index) +//! [![Usage Example](https://img.shields.io/badge/Usage%20Example-yellow)](https://github.com/SeaQL/sea-orm/tree/master/examples/async-std) +//! [![Actix Example](https://img.shields.io/badge/Actix%20Example-blue)](https://github.com/SeaQL/sea-orm/tree/master/examples/actix_example) //! [![Rocket Example](https://img.shields.io/badge/Rocket%20Example-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/rocket_example) //! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) //! @@ -38,171 +39,77 @@ //! //! Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. //! -//! ``` -//! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*, DatabaseConnection, DbBackend, MockDatabase, Transaction, IntoMockRow}; -//! # let db = MockDatabase::new(DbBackend::Postgres) -//! # .append_query_results(vec![ -//! # vec![cake::Model { -//! # id: 1, -//! # name: "New York Cheese".to_owned(), -//! # } -//! # .into_mock_row()], -//! # vec![fruit::Model { -//! # id: 1, -//! # name: "Apple".to_owned(), -//! # cake_id: Some(1), -//! # } -//! # .into_mock_row()], -//! # ]) -//! # .into_connection(); -//! # let _: Result<(), DbErr> = smol::block_on(async { -//! // execute multiple queries in parallel -//! let cakes_and_fruits: (Vec, Vec) = -//! futures::try_join!(Cake::find().all(&db), Fruit::find().all(&db))?; -//! # assert_eq!( -//! # cakes_and_fruits, -//! # ( -//! # vec![cake::Model { -//! # id: 1, -//! # name: "New York Cheese".to_owned(), -//! # }], -//! # vec![fruit::Model { -//! # id: 1, -//! # 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", "cake"."name" FROM "cake""#, -//! # vec![] -//! # ), -//! # Transaction::from_sql_and_values( -//! # DbBackend::Postgres, -//! # r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id" FROM "fruit""#, -//! # vec![] -//! # ), -//! # ] -//! # ); -//! # Ok(()) -//! # }); -//! ``` -//! //! 2. Dynamic //! //! Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. //! -//! ``` -//! # use sea_query::Query; -//! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*}; -//! # async fn function(db: DbConn) -> Result<(), DbErr> { -//! // build subquery with ease -//! let cakes_with_filling: Vec = cake::Entity::find() -//! .filter( -//! Condition::any().add( -//! cake::Column::Id.in_subquery( -//! Query::select() -//! .column(cake_filling::Column::CakeId) -//! .from(cake_filling::Entity) -//! .to_owned(), -//! ), -//! ), -//! ) -//! .all(&db) -//! .await?; -//! -//! # Ok(()) -//! # } -//! ``` -//! //! 3. Testable //! //! Use mock connections to write unit tests for your logic. //! -//! ``` -//! # use sea_orm::{error::*, entity::*, query::*, tests_cfg::*, DbConn, MockDatabase, Transaction, DbBackend}; -//! # async fn function(db: DbConn) -> Result<(), DbErr> { -//! // Setup mock connection -//! let db = MockDatabase::new(DbBackend::Postgres) -//! .append_query_results(vec![ -//! vec![ -//! cake::Model { -//! id: 1, -//! name: "New York Cheese".to_owned(), -//! }, -//! ], -//! ]) -//! .into_connection(); -//! -//! // Perform your application logic -//! assert_eq!( -//! cake::Entity::find().one(&db).await?, -//! Some(cake::Model { -//! id: 1, -//! name: "New York Cheese".to_owned(), -//! }) -//! ); -//! -//! // Compare it against the expected transaction log -//! assert_eq!( -//! db.into_transaction_log(), -//! vec![ -//! Transaction::from_sql_and_values( -//! DbBackend::Postgres, -//! r#"SELECT "cake"."id", "cake"."name" FROM "cake" LIMIT $1"#, -//! vec![1u64.into()] -//! ), -//! ] -//! ); -//! # Ok(()) -//! # } -//! ``` -//! //! 4. Service Oriented //! //! Quickly build services that join, filter, sort and paginate data in APIs. //! -//! ```ignore -//! #[get("/?&")] -//! async fn list( -//! conn: Connection, -//! page: Option, -//! per_page: Option, -//! ) -> Template { -//! // Set page number and items per page -//! let page = page.unwrap_or(1); -//! let per_page = per_page.unwrap_or(10); -//! -//! // Setup paginator -//! let paginator = Post::find() -//! .order_by_asc(post::Column::Id) -//! .paginate(&conn, per_page); -//! let num_pages = paginator.num_pages().await.unwrap(); -//! -//! // Fetch paginated posts -//! let posts = paginator -//! .fetch_page(page - 1) -//! .await -//! .expect("could not retrieve posts"); -//! -//! Template::render( -//! "index", -//! context! { -//! page: page, -//! per_page: per_page, -//! posts: posts, -//! num_pages: num_pages, -//! }, -//! ) -//! } -//! ``` -//! //! ## A quick taste of SeaORM //! +//! ### Entity +//! ``` +//! # #[cfg(feature = "macros")] +//! # mod entities { +//! # mod fruit { +//! # use sea_orm::entity::prelude::*; +//! # #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +//! # #[sea_orm(table_name = "fruit")] +//! # pub struct Model { +//! # #[sea_orm(primary_key)] +//! # pub id: i32, +//! # pub name: String, +//! # pub cake_id: Option, +//! # } +//! # #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +//! # pub enum Relation { +//! # #[sea_orm( +//! # belongs_to = "super::cake::Entity", +//! # from = "Column::CakeId", +//! # to = "super::cake::Column::Id" +//! # )] +//! # Cake, +//! # } +//! # impl Related for Entity { +//! # fn to() -> RelationDef { +//! # Relation::Cake.def() +//! # } +//! # } +//! # impl ActiveModelBehavior for ActiveModel {} +//! # } +//! # mod cake { +//! use sea_orm::entity::prelude::*; +//! +//! #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +//! #[sea_orm(table_name = "cake")] +//! pub struct Model { +//! #[sea_orm(primary_key)] +//! pub id: i32, +//! pub name: String, +//! } +//! +//! #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +//! pub enum Relation { +//! #[sea_orm(has_many = "super::fruit::Entity")] +//! Fruit, +//! } +//! +//! impl Related for Entity { +//! fn to() -> RelationDef { +//! Relation::Fruit.def() +//! } +//! } +//! # impl ActiveModelBehavior for ActiveModel {} +//! # } +//! # } +//! ``` +//! //! ### Select //! ``` //! # use sea_orm::{DbConn, error::*, entity::*, query::*, tests_cfg::*}; @@ -366,6 +273,7 @@ pub mod query; pub mod schema; #[doc(hidden)] pub mod tests_cfg; +mod docs; mod util; pub use database::*; @@ -376,6 +284,7 @@ pub use executor::*; pub use query::*; pub use schema::*; +#[cfg(feature = "macros")] pub use sea_orm_macros::{ DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, DeriveEntityModel, DeriveModel, DerivePrimaryKey, DeriveRelation, FromQueryResult, @@ -383,5 +292,9 @@ pub use sea_orm_macros::{ pub use sea_query; pub use sea_query::Iden; +#[cfg(feature = "macros")] +pub use sea_query::Iden as DeriveIden; + pub use sea_strum; +#[cfg(feature = "macros")] pub use sea_strum::EnumIter; diff --git a/src/query/helper.rs b/src/query/helper.rs index 43fed28a..7efe0ca1 100644 --- a/src/query/helper.rs +++ b/src/query/helper.rs @@ -276,7 +276,9 @@ pub trait QueryFilter: Sized { /// struct Input { /// name: Option, /// } - /// let input = Input { name: Some("cheese".to_owned()) }; + /// let input = Input { + /// name: Some("cheese".to_owned()), + /// }; /// /// let mut conditions = Condition::all(); /// if let Some(name) = input.name { @@ -298,17 +300,44 @@ pub trait QueryFilter: Sized { /// struct Input { /// name: Option, /// } - /// let input = Input { name: Some("cheese".to_owned()) }; + /// let input = Input { + /// name: Some("cheese".to_owned()), + /// }; + /// + /// assert_eq!( + /// cake::Entity::find() + /// .filter( + /// Condition::all().add_option(input.name.map(|n| cake::Column::Name.contains(&n))) + /// ) + /// .build(DbBackend::MySql) + /// .to_string(), + /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`name` LIKE '%cheese%'" + /// ); + /// ``` + /// + /// A slightly more complex example. + /// ``` + /// use sea_orm::{entity::*, query::*, tests_cfg::cake, sea_query::Expr, DbBackend}; /// /// assert_eq!( /// cake::Entity::find() /// .filter( /// Condition::all() - /// .add_option(input.name.map(|n| cake::Column::Name.contains(&n))) + /// .add( + /// Condition::all() + /// .not() + /// .add(Expr::val(1).eq(1)) + /// .add(Expr::val(2).eq(2)) + /// ) + /// .add( + /// Condition::any() + /// .add(Expr::val(3).eq(3)) + /// .add(Expr::val(4).eq(4)) + /// ) /// ) - /// .build(DbBackend::MySql) + /// .build(DbBackend::Postgres) /// .to_string(), - /// "SELECT `cake`.`id`, `cake`.`name` FROM `cake` WHERE `cake`.`name` LIKE '%cheese%'" + /// r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE (NOT (1 = 1 AND 2 = 2)) AND (3 = 3 OR 4 = 4)"# /// ); /// ``` fn filter(mut self, filter: F) -> Self diff --git a/src/tests_cfg/cake.rs b/src/tests_cfg/cake.rs index 8628492b..8c01bf16 100644 --- a/src/tests_cfg/cake.rs +++ b/src/tests_cfg/cake.rs @@ -6,6 +6,7 @@ use crate::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: i32, + #[sea_orm(column_name = "name", enum_name = "Name")] pub name: String, } diff --git a/tests/common/bakery_chain/metadata.rs b/tests/common/bakery_chain/metadata.rs index 95a7a48b..de513a22 100644 --- a/tests/common/bakery_chain/metadata.rs +++ b/tests/common/bakery_chain/metadata.rs @@ -5,9 +5,13 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub uuid: Uuid, + #[sea_orm(column_name = "type", enum_name = "Type")] + pub ty: String, pub key: String, pub value: String, pub bytes: Vec, + pub date: Date, + pub time: Time, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/tests/common/setup/schema.rs b/tests/common/setup/schema.rs index 64f31dfe..b39f1e77 100644 --- a/tests/common/setup/schema.rs +++ b/tests/common/setup/schema.rs @@ -283,9 +283,12 @@ pub async fn create_metadata_table(db: &DbConn) -> Result { .not_null() .primary_key(), ) + .col(ColumnDef::new(metadata::Column::Type).string().not_null()) .col(ColumnDef::new(metadata::Column::Key).string().not_null()) .col(ColumnDef::new(metadata::Column::Value).string().not_null()) .col(ColumnDef::new(metadata::Column::Bytes).binary().not_null()) + .col(ColumnDef::new(metadata::Column::Date).date().not_null()) + .col(ColumnDef::new(metadata::Column::Time).time().not_null()) .to_owned(); create_table(db, &stmt, Metadata).await diff --git a/tests/parallel_tests.rs b/tests/parallel_tests.rs index 0ac09fd6..a0ef869b 100644 --- a/tests/parallel_tests.rs +++ b/tests/parallel_tests.rs @@ -22,21 +22,30 @@ pub async fn crud_in_parallel(db: &DatabaseConnection) -> Result<(), DbErr> { let metadata = vec![ metadata::Model { uuid: Uuid::new_v4(), + ty: "Type".to_owned(), key: "markup".to_owned(), value: "1.18".to_owned(), bytes: vec![1, 2, 3], + date: Date::from_ymd(2021, 9, 27), + time: Time::from_hms(11, 32, 55), }, metadata::Model { uuid: Uuid::new_v4(), + ty: "Type".to_owned(), key: "exchange_rate".to_owned(), value: "0.78".to_owned(), bytes: vec![1, 2, 3], + date: Date::from_ymd(2021, 9, 27), + time: Time::from_hms(11, 32, 55), }, metadata::Model { uuid: Uuid::new_v4(), + ty: "Type".to_owned(), key: "service_charge".to_owned(), value: "1.1".to_owned(), bytes: vec![1, 2, 3], + date: Date::from_ymd(2021, 9, 27), + time: Time::from_hms(11, 32, 55), }, ]; diff --git a/tests/uuid_tests.rs b/tests/uuid_tests.rs index b896097b..3a30a7f6 100644 --- a/tests/uuid_tests.rs +++ b/tests/uuid_tests.rs @@ -20,9 +20,12 @@ async fn main() -> Result<(), DbErr> { pub async fn create_metadata(db: &DatabaseConnection) -> Result<(), DbErr> { let metadata = metadata::Model { uuid: Uuid::new_v4(), + ty: "Type".to_owned(), key: "markup".to_owned(), value: "1.18".to_owned(), bytes: vec![1, 2, 3], + date: Date::from_ymd(2021, 9, 27), + time: Time::from_hms(11, 32, 55), }; let res = Metadata::insert(metadata.clone().into_active_model())