diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4db1bdd7..2aa1f90d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,10 +1,8 @@ name: tests on: - push: - branches: - - master pull_request: + push: branches: - master @@ -12,12 +10,15 @@ env: CARGO_TERM_COLOR: always jobs: - test: - name: Unit Test + + compile: + name: Compile runs-on: ubuntu-20.04 strategy: matrix: - runtime: [async-std-native-tls, async-std-rustls, actix-native-tls, actix-rustls, tokio-native-tls, tokio-rustls] + database: [sqlite, mysql, postgres] + runtime: [async-std, actix, tokio] + tls: [native-tls, rustls] steps: - uses: actions/checkout@v2 @@ -27,32 +28,98 @@ jobs: toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 + - uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + Cargo.lock + target + key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-${{ matrix.database }}-${{ matrix.runtime }}-${{ matrix.tls }} - uses: actions-rs/cargo@v1 with: - command: build + command: test args: > - --all - --features default + --features default,sqlx-${{ matrix.database }},runtime-${{ matrix.runtime }}-${{ matrix.tls }} + --no-run + + test: + name: Unit Test + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true - uses: actions-rs/cargo@v1 with: command: test args: > --all - --exclude 'sea-orm-example-*' - --features default + + cli: + name: CLI + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions-rs/cargo@v1 + with: + command: install + args: > + --path sea-orm-cli + + examples: + name: Examples + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions-rs/cargo@v1 + with: + command: build + args: > + --manifest-path examples/async-std/Cargo.toml + + - uses: actions-rs/cargo@v1 + with: + command: build + args: > + --manifest-path examples/tokio/Cargo.toml sqlite: name: SQLite runs-on: ubuntu-20.04 + needs: compile env: DATABASE_URL: "sqlite::memory:" strategy: matrix: - # runtime: [async-std-native-tls, async-std-rustls, actix-native-tls, actix-rustls, tokio-native-tls, tokio-rustls] - runtime: [async-std-native-tls] + runtime: [async-std, actix, tokio] + tls: [native-tls, rustls] steps: - uses: actions/checkout@v2 @@ -62,29 +129,32 @@ jobs: toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 - - - uses: actions-rs/cargo@v1 + - uses: actions/cache@v2 with: - command: build - args: > - --features default,runtime-${{ matrix.runtime }} + path: | + ~/.cargo/registry + ~/.cargo/git + Cargo.lock + target + key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-sqlite-${{ matrix.runtime }}-${{ matrix.tls }} - uses: actions-rs/cargo@v1 with: command: test args: > - --features default,sqlx-sqlite,runtime-${{ matrix.runtime }} + --features default,sqlx-sqlite,runtime-${{ matrix.runtime }}-${{ matrix.tls }} mysql: name: MySQL runs-on: ubuntu-20.04 + needs: compile env: DATABASE_URL: "mysql://root:@localhost" strategy: matrix: version: [8.0, 5.7] - runtime: [async-std-native-tls] + runtime: [async-std, actix, tokio] + tls: [native-tls] services: mysql: image: mysql:${{ matrix.version }} @@ -111,29 +181,32 @@ jobs: toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 - - - uses: actions-rs/cargo@v1 + - uses: actions/cache@v2 with: - command: build - args: > - --features default,runtime-${{ matrix.runtime }} + path: | + ~/.cargo/registry + ~/.cargo/git + Cargo.lock + target + key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }} - uses: actions-rs/cargo@v1 with: command: test args: > - --features default,sqlx-mysql,runtime-${{ matrix.runtime }} + --features default,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} mariadb: name: MariaDB runs-on: ubuntu-20.04 + needs: compile env: DATABASE_URL: "mysql://root:@localhost" strategy: matrix: version: [10.6] - runtime: [async-std-native-tls] + runtime: [async-std, actix, tokio] + tls: [rustls] services: mysql: image: mariadb:${{ matrix.version }} @@ -160,29 +233,32 @@ jobs: toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 - - - uses: actions-rs/cargo@v1 + - uses: actions/cache@v2 with: - command: build - args: > - --features default,runtime-${{ matrix.runtime }} + path: | + ~/.cargo/registry + ~/.cargo/git + Cargo.lock + target + key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-mysql-${{ matrix.runtime }}-${{ matrix.tls }} - uses: actions-rs/cargo@v1 with: command: test args: > - --features default,sqlx-mysql,runtime-${{ matrix.runtime }} + --features default,sqlx-mysql,runtime-${{ matrix.runtime }}-${{ matrix.tls }} postgres: name: Postgres runs-on: ubuntu-20.04 + needs: compile env: DATABASE_URL: "postgres://root:root@localhost" strategy: matrix: version: [13.3, 12.7, 11.12, 10.17, 9.6.22] - runtime: [async-std-native-tls] + runtime: [tokio] + tls: [native-tls] services: postgres: image: postgres:${{ matrix.version }} @@ -206,16 +282,17 @@ jobs: toolchain: stable override: true - - uses: Swatinem/rust-cache@v1 - - - uses: actions-rs/cargo@v1 + - uses: actions/cache@v2 with: - command: build - args: > - --features default,runtime-${{ matrix.runtime }} + path: | + ~/.cargo/registry + ~/.cargo/git + Cargo.lock + target + key: ${{ github.sha }}-${{ github.run_id }}-${{ runner.os }}-postgres-${{ matrix.runtime }}-${{ matrix.tls }} - uses: actions-rs/cargo@v1 with: command: test args: > - --features default,sqlx-postgres,runtime-${{ matrix.runtime }} + --features default,sqlx-postgres,runtime-${{ matrix.runtime }}-${{ matrix.tls }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..1ef38d62 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +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.1.2 - 2021-08-23 + +- [[#68]] Added `DateTimeWithTimeZone` as supported attribute type +- [[#70]] Generate arbitrary named entity +- [[#80]] Custom column name +- [[#81]] Support join on multiple columns +- [[#99]] Implement FromStr for ColumnTrait + +[#68]: https://github.com/SeaQL/sea-orm/issues/68 +[#70]: https://github.com/SeaQL/sea-orm/issues/70 +[#80]: https://github.com/SeaQL/sea-orm/issues/80 +[#81]: https://github.com/SeaQL/sea-orm/issues/81 +[#99]: https://github.com/SeaQL/sea-orm/issues/99 + +## 0.1.1 - 2021-08-08 + +- Early release of SeaORM diff --git a/Cargo.toml b/Cargo.toml index 6f8cadbb..ca61c819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,11 @@ members = [ ".", "sea-orm-macros", "sea-orm-codegen", - "sea-orm-cli", - "examples/sqlx", ] [package] name = "sea-orm" -version = "0.1.1" +version = "0.1.2" authors = ["Chris Tsang "] edition = "2018" description = "🐚 An async & dynamic ORM for Rust" @@ -20,11 +18,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", "runtime-async-std-native-tls"] rustdoc-args = ["--cfg", "docsrs"] [lib] @@ -37,8 +31,8 @@ chrono = { version = "^0", optional = true } futures = { version = "^0.3" } futures-util = { version = "^0.3" } rust_decimal = { version = "^1", optional = true } -sea-orm-macros = { version = "^0.1", optional = true } -sea-query = { version = "^0.12.8" } +sea-orm-macros = { version = "^0.1.1", optional = true } +sea-query = { version = "^0.15", features = ["thread-safe"] } sea-strum = { version = "^0.21", features = ["derive", "sea-orm"] } serde = { version = "^1.0", features = ["derive"] } sqlx = { version = "^0.5", optional = true } @@ -48,6 +42,8 @@ serde_json = { version = "^1", optional = true } uuid = { version = "0.8", features = ["serde", "v4"], optional = true } [dev-dependencies] +smol = { version = "^1.2" } +smol-potat = { version = "^1.1" } async-std = { version = "^1.9", features = ["attributes"] } tokio = { version = "^1.6", features = ["full"] } actix-rt = { version = "2.2.0" } @@ -69,14 +65,8 @@ macros = ["sea-orm-macros"] mock = [] with-json = ["serde_json", "sea-query/with-json"] with-chrono = ["chrono", "sea-query/with-chrono"] -with-rust_decimal = [ - "rust_decimal", - "sea-query/with-rust_decimal", -] -with-uuid = [ - "uuid", - "sea-query/with-uuid", -] +with-rust_decimal = ["rust_decimal", "sea-query/with-rust_decimal"] +with-uuid = ["uuid", "sea-query/with-uuid"] sqlx-all = ["sqlx-mysql", "sqlx-postgres", "sqlx-sqlite"] sqlx-dep = ["sqlx-json", "sqlx-chrono", "sqlx-decimal", "sqlx-uuid"] sqlx-json = ["sqlx/json", "with-json"] diff --git a/Design.md b/Design.md index 860afcef..73e88227 100644 --- a/Design.md +++ b/Design.md @@ -1,5 +1,7 @@ # Design Goals +We are heavily inspired by ActiveRecord, Eloquent and TypeORM. + 1. Intuitive and ergonomic API should state the intention clearly. Provide syntax sugar for common things. diff --git a/README.md b/README.md index 3ced0629..f2d262e9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- +

SeaORM

@@ -18,30 +18,26 @@ # SeaORM -Inspired by ActiveRecord, Eloquent and TypeORM, SeaORM aims to provide you an intuitive and ergonomic -API to make working with databases in Rust a first-class experience. +SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. ```markdown This is an early release of SeaORM, the API is not stable yet. ``` -
- - [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) - [![Getting Started](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx) - [![Getting Started](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37) - -
+[![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) +[![Examples](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx) +[![Starter Kit](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37) +[![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) ## Features 1. Async -Relying on SQLx, SeaORM is a new library with async support from day 1. +Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. 2. Dynamic -Built upon SeaQuery, SeaORM allows you to build complex queries without 'fighting the ORM'. +Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. 3. Testable diff --git a/examples/sqlx/Cargo.toml b/examples/async-std/Cargo.toml similarity index 75% rename from examples/sqlx/Cargo.toml rename to examples/async-std/Cargo.toml index 97d1340f..c2be9d88 100644 --- a/examples/sqlx/Cargo.toml +++ b/examples/async-std/Cargo.toml @@ -1,5 +1,8 @@ +[workspace] +# A separate workspace + [package] -name = "sea-orm-example-sqlx" +name = "sea-orm-example-async-std" version = "0.1.0" edition = "2018" publish = false diff --git a/examples/sqlx/Readme.md b/examples/async-std/Readme.md similarity index 100% rename from examples/sqlx/Readme.md rename to examples/async-std/Readme.md diff --git a/examples/sqlx/bakery.sql b/examples/async-std/bakery.sql similarity index 100% rename from examples/sqlx/bakery.sql rename to examples/async-std/bakery.sql diff --git a/examples/sqlx/import.sh b/examples/async-std/import.sh similarity index 100% rename from examples/sqlx/import.sh rename to examples/async-std/import.sh diff --git a/examples/sqlx/src/entities.rs b/examples/async-std/src/entities.rs similarity index 100% rename from examples/sqlx/src/entities.rs rename to examples/async-std/src/entities.rs diff --git a/examples/sqlx/src/example_cake.rs b/examples/async-std/src/example_cake.rs similarity index 100% rename from examples/sqlx/src/example_cake.rs rename to examples/async-std/src/example_cake.rs diff --git a/examples/sqlx/src/example_cake_filling.rs b/examples/async-std/src/example_cake_filling.rs similarity index 100% rename from examples/sqlx/src/example_cake_filling.rs rename to examples/async-std/src/example_cake_filling.rs diff --git a/examples/sqlx/src/example_filling.rs b/examples/async-std/src/example_filling.rs similarity index 100% rename from examples/sqlx/src/example_filling.rs rename to examples/async-std/src/example_filling.rs diff --git a/examples/sqlx/src/example_fruit.rs b/examples/async-std/src/example_fruit.rs similarity index 100% rename from examples/sqlx/src/example_fruit.rs rename to examples/async-std/src/example_fruit.rs diff --git a/examples/sqlx/src/main.rs b/examples/async-std/src/main.rs similarity index 100% rename from examples/sqlx/src/main.rs rename to examples/async-std/src/main.rs diff --git a/examples/sqlx/src/operation.rs b/examples/async-std/src/operation.rs similarity index 100% rename from examples/sqlx/src/operation.rs rename to examples/async-std/src/operation.rs diff --git a/examples/sqlx/src/select.rs b/examples/async-std/src/select.rs similarity index 98% rename from examples/sqlx/src/select.rs rename to examples/async-std/src/select.rs index 9b2cf15c..ce26f9e2 100644 --- a/examples/sqlx/src/select.rs +++ b/examples/async-std/src/select.rs @@ -66,7 +66,8 @@ async fn find_all(db: &DbConn) -> Result<(), DbErr> { async fn find_together(db: &DbConn) -> Result<(), DbErr> { print!("find cakes and fruits: "); - let both = Cake::find().find_also_related(Fruit).all(db).await?; + let both: Vec<(cake::Model, Option)> = + Cake::find().find_also_related(Fruit).all(db).await?; println!(); for bb in both.iter() { diff --git a/examples/tokio/Cargo.toml b/examples/tokio/Cargo.toml new file mode 100644 index 00000000..173e9906 --- /dev/null +++ b/examples/tokio/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +# A separate workspace + +[package] +name = "sea-orm-example-tokio" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +sea-orm = { path = "../../", features = [ "sqlx-all", "runtime-tokio-native-tls" ] } +tokio = { version = "1", features = ["full"] } diff --git a/examples/tokio/src/cake.rs b/examples/tokio/src/cake.rs new file mode 100644 index 00000000..0b1a4439 --- /dev/null +++ b/examples/tokio/src/cake.rs @@ -0,0 +1,55 @@ +use sea_orm::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn table_name(&self) -> &str { + "cake" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub id: i32, + pub name: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + Id, + Name, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + Id, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + true + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation {} + +impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Self::Id => ColumnType::Integer.def(), + Self::Name => ColumnType::String(None).def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + unreachable!() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/examples/tokio/src/main.rs b/examples/tokio/src/main.rs new file mode 100644 index 00000000..dd81b4ab --- /dev/null +++ b/examples/tokio/src/main.rs @@ -0,0 +1,14 @@ +mod cake; +use sea_orm::*; + +#[tokio::main] +pub async fn main() { + let db = Database::connect("sql://sea:sea@localhost/bakery") + .await + .unwrap(); + + tokio::spawn(async move { + cake::Entity::find().one(&db).await.unwrap(); + }) + .await.unwrap(); +} \ No newline at end of file diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 48be929e..7c9ec510 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -1,6 +1,9 @@ +[workspace] +# A separate workspace + [package] name = "sea-orm-cli" -version = "0.1.0" +version = "0.1.3" authors = [ "Billy Chan " ] edition = "2018" description = "Command line utility for SeaORM" @@ -18,9 +21,9 @@ path = "src/main.rs" clap = { version = "^2.33.3" } dotenv = { version = "^0.15" } async-std = { version = "^1.9", features = [ "attributes" ] } -sea-orm = { version = "^0.1", features = [ "sqlx-all" ] } -sea-orm-codegen = { version = "^0.1" } -sea-schema = { version = "^0.2.4", default-features = false, features = [ +sea-orm = { version = "^0.1.2", features = [ "sqlx-all" ] } +sea-orm-codegen = { version = "^0.1.3" } +sea-schema = { version = "^0.2.7", default-features = false, features = [ "sqlx-mysql", "sqlx-postgres", "discovery", diff --git a/sea-orm-cli/README.md b/sea-orm-cli/README.md index b63a5c56..531a3c46 100644 --- a/sea-orm-cli/README.md +++ b/sea-orm-cli/README.md @@ -9,5 +9,9 @@ cargo run -- -h Running Entity Generator: ```sh -cargo run -- entity generate -url mysql://sea:sea@localhost/bakery -schema bakery -o out +# MySQL (`--database-schema` option is ignored) +cargo run -- generate entity -u mysql://sea:sea@localhost/bakery -o out + +# PostgreSQL +cargo run -- generate entity -u postgres://sea:sea@localhost/bakery -s public -o out ``` diff --git a/sea-orm-cli/src/cli.rs b/sea-orm-cli/src/cli.rs index de0313c2..8f2919fb 100644 --- a/sea-orm-cli/src/cli.rs +++ b/sea-orm-cli/src/cli.rs @@ -21,8 +21,10 @@ pub fn build_cli() -> App<'static, 'static> { .long("database-schema") .short("s") .help("Database schema") + .long_help("Database schema\n \ + - For MySQL, this argument is ignored.\n \ + - For PostgreSQL, this argument is optional with default value 'public'.") .takes_value(true) - .required(true) .env("DATABASE_SCHEMA"), ) .arg( @@ -32,6 +34,12 @@ pub fn build_cli() -> App<'static, 'static> { .help("Entity file output directory") .takes_value(true) .default_value("./"), + ) + .arg( + Arg::with_name("INCLUDE_HIDDEN_TABLES") + .long("include-hidden-tables") + .help("Generate entity file for hidden tables (i.e. table name starts with an underscore)") + .takes_value(false), ), ) .setting(AppSettings::SubcommandRequiredElseHelp); diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index 9a512050..c3cc60dd 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -23,31 +23,43 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box { let url = args.value_of("DATABASE_URL").unwrap(); - let schema = args.value_of("DATABASE_SCHEMA").unwrap(); let output_dir = args.value_of("OUTPUT_DIR").unwrap(); + let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); + let filter_hidden_tables = |table: &str| -> bool { + if include_hidden_tables { + true + } else { + !table.starts_with("_") + } + }; let table_stmts = if url.starts_with("mysql://") { use sea_schema::mysql::discovery::SchemaDiscovery; use sqlx::MySqlPool; + let url_parts: Vec<&str> = url.split("/").collect(); + let schema = url_parts.last().unwrap(); let connection = MySqlPool::connect(url).await?; let schema_discovery = SchemaDiscovery::new(connection, schema); let schema = schema_discovery.discover().await; schema .tables .into_iter() + .filter(|schema| filter_hidden_tables(&schema.info.name)) .map(|schema| schema.write()) .collect() } else if url.starts_with("postgres://") { use sea_schema::postgres::discovery::SchemaDiscovery; use sqlx::PgPool; + let schema = args.value_of("DATABASE_SCHEMA").unwrap_or("public"); let connection = PgPool::connect(url).await?; let schema_discovery = SchemaDiscovery::new(connection, schema); let schema = schema_discovery.discover().await; schema .tables .into_iter() + .filter(|schema| filter_hidden_tables(&schema.info.name)) .map(|schema| schema.write()) .collect() } else { diff --git a/sea-orm-codegen/Cargo.toml b/sea-orm-codegen/Cargo.toml index ee48571f..9080bd2b 100644 --- a/sea-orm-codegen/Cargo.toml +++ b/sea-orm-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-orm-codegen" -version = "0.1.1" +version = "0.1.3" authors = ["Billy Chan "] edition = "2018" description = "Code Generator for SeaORM" @@ -15,7 +15,7 @@ name = "sea_orm_codegen" path = "src/lib.rs" [dependencies] -sea-query = { version = "^0.12.8" } +sea-query = { version = "^0.15" } syn = { version = "^1", default-features = false, features = [ "derive", "parsing", diff --git a/sea-orm-codegen/src/entity/column.rs b/sea-orm-codegen/src/entity/column.rs index 9a6cf4dc..a2c3b4f5 100644 --- a/sea-orm-codegen/src/entity/column.rs +++ b/sea-orm-codegen/src/entity/column.rs @@ -22,6 +22,7 @@ impl Column { } pub fn get_rs_type(&self) -> TokenStream { + #[allow(unreachable_patterns)] let ident: TokenStream = match self.col_type { ColumnType::Char(_) | ColumnType::String(_) @@ -284,7 +285,7 @@ mod tests { #[test] fn test_from_column_def() { - let column: Column = ColumnDef::new(Alias::new("id")).string().into(); + let column: Column = ColumnDef::new(Alias::new("id")).string().to_owned().into(); assert_eq!( column.get_def().to_string(), quote! { @@ -293,13 +294,18 @@ mod tests { .to_string() ); - let column: Column = ColumnDef::new(Alias::new("id")).string().not_null().into(); + let column: Column = ColumnDef::new(Alias::new("id")) + .string() + .not_null() + .to_owned() + .into(); assert!(column.not_null); let column: Column = ColumnDef::new(Alias::new("id")) .string() .unique_key() .not_null() + .to_owned() .into(); assert!(column.unique); assert!(column.not_null); @@ -309,6 +315,7 @@ mod tests { .auto_increment() .unique_key() .not_null() + .to_owned() .into(); assert!(column.auto_increment); assert!(column.unique); diff --git a/sea-orm-codegen/src/entity/writer.rs b/sea-orm-codegen/src/entity/writer.rs index f4167b61..207864ba 100644 --- a/sea-orm-codegen/src/entity/writer.rs +++ b/sea-orm-codegen/src/entity/writer.rs @@ -34,7 +34,7 @@ impl EntityWriter { let code_blocks = Self::gen_code_blocks(entity); Self::write(&mut lines, code_blocks); OutputFile { - name: format!("{}.rs", entity.table_name), + name: format!("{}.rs", entity.get_table_name_snake_case()), content: lines.join("\n\n"), } }) @@ -44,11 +44,18 @@ impl EntityWriter { pub fn write_mod(&self) -> OutputFile { let mut lines = Vec::new(); Self::write_doc_comment(&mut lines); - let code_blocks = self + let code_blocks: Vec = self .entities .iter() .map(|entity| Self::gen_mod(entity)) .collect(); + Self::write( + &mut lines, + vec![quote! { + pub mod prelude; + }], + ); + lines.push("".to_owned()); Self::write(&mut lines, code_blocks); OutputFile { name: "mod.rs".to_owned(), @@ -123,11 +130,11 @@ impl EntityWriter { } pub fn gen_impl_entity_name(entity: &Entity) -> TokenStream { - let table_name_snake_case = entity.get_table_name_snake_case(); + let table_name = entity.table_name.as_str(); quote! { impl EntityName for Entity { fn table_name(&self) -> &str { - #table_name_snake_case + #table_name } } } @@ -341,7 +348,7 @@ mod tests { }], }, Entity { - table_name: "cake_filling".to_owned(), + table_name: "_cake_filling_".to_owned(), columns: vec![ Column { name: "cake_id".to_owned(), diff --git a/sea-orm-codegen/tests/entity/cake_filling.rs b/sea-orm-codegen/tests/entity/cake_filling.rs index 1ac64920..d0f00560 100644 --- a/sea-orm-codegen/tests/entity/cake_filling.rs +++ b/sea-orm-codegen/tests/entity/cake_filling.rs @@ -7,7 +7,7 @@ pub struct Entity; impl EntityName for Entity { fn table_name(&self) -> &str { - "cake_filling" + "_cake_filling_" } } diff --git a/sea-orm-codegen/tests/entity/mod.rs b/sea-orm-codegen/tests/entity/mod.rs index 395d29f9..5a8c6c21 100644 --- a/sea-orm-codegen/tests/entity/mod.rs +++ b/sea-orm-codegen/tests/entity/mod.rs @@ -1,5 +1,7 @@ //! SeaORM Entity. Generated by sea-orm-codegen 0.1.0 +pub mod prelude; + pub mod cake; pub mod cake_filling; pub mod filling; diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index 2fdfd11a..312adeaa 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sea-orm-macros" -version = "0.1.0" +version = "0.1.1" authors = [ "Billy Chan " ] edition = "2018" description = "Derive macros for SeaORM" @@ -16,7 +16,7 @@ path = "src/lib.rs" proc-macro = true [dependencies] -syn = { version = "^1", default-features = false, features = [ "derive", "clone-impls", "parsing", "proc-macro", "printing" ] } +syn = { version = "^1", default-features = false, features = [ "full", "derive", "clone-impls", "parsing", "proc-macro", "printing" ] } quote = "^1" heck = "^0.3" proc-macro2 = "^1" diff --git a/sea-orm-macros/src/derives/column.rs b/sea-orm-macros/src/derives/column.rs index 78a72259..16f9bb22 100644 --- a/sea-orm-macros/src/derives/column.rs +++ b/sea-orm-macros/src/derives/column.rs @@ -1,9 +1,9 @@ -use heck::SnakeCase; +use heck::{MixedCase, SnakeCase}; use proc_macro2::{Ident, TokenStream}; use quote::{quote, quote_spanned}; use syn::{Data, DataEnum, Fields, Variant}; -pub fn expand_derive_column(ident: Ident, data: Data) -> syn::Result { +pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result { let variants = match data { syn::Data::Enum(DataEnum { variants, .. }) => variants, _ => { @@ -31,14 +31,8 @@ pub fn expand_derive_column(ident: Ident, data: Data) -> syn::Result &str { + impl #ident { + fn default_as_str(&self) -> &str { match self { #(Self::#variant => #name),* } @@ -46,3 +40,67 @@ pub fn expand_derive_column(ident: Ident, data: Data) -> syn::Result syn::Result { + let data_enum = match data { + Data::Enum(data_enum) => data_enum, + _ => { + return Ok(quote_spanned! { + ident.span() => compile_error!("you can only derive DeriveColumn on enums"); + }) + } + }; + + let columns = data_enum.variants.iter().map(|column| { + let column_iden = column.ident.clone(); + let column_str_snake = column_iden.to_string().to_snake_case(); + let column_str_mixed = column_iden.to_string().to_mixed_case(); + quote!( + #column_str_snake | #column_str_mixed => Ok(#ident::#column_iden) + ) + }); + + Ok(quote!( + impl std::str::FromStr for #ident { + type Err = sea_orm::ColumnFromStrErr; + + fn from_str(s: &str) -> Result { + match s { + #(#columns),*, + _ => Err(sea_orm::ColumnFromStrErr(format!("Failed to parse '{}' as `{}`", s, stringify!(#ident)))), + } + } + } + )) +} + +pub fn expand_derive_column(ident: &Ident, data: &Data) -> syn::Result { + let impl_iden = expand_derive_custom_column(ident, data)?; + + Ok(quote!( + #impl_iden + + impl sea_orm::IdenStatic for #ident { + fn as_str(&self) -> &str { + self.default_as_str() + } + } + )) +} + +pub fn expand_derive_custom_column(ident: &Ident, data: &Data) -> syn::Result { + let impl_default_as_str = impl_default_as_str(ident, data)?; + let impl_col_from_str = impl_col_from_str(ident, data)?; + + Ok(quote!( + #impl_default_as_str + + #impl_col_from_str + + impl sea_orm::Iden for #ident { + fn unquoted(&self, s: &mut dyn std::fmt::Write) { + write!(s, "{}", self.as_str()).unwrap(); + } + } + )) +} diff --git a/sea-orm-macros/src/lib.rs b/sea-orm-macros/src/lib.rs index 65b8fd15..a2217aa8 100644 --- a/sea-orm-macros/src/lib.rs +++ b/sea-orm-macros/src/lib.rs @@ -29,7 +29,17 @@ pub fn derive_primary_key(input: TokenStream) -> TokenStream { pub fn derive_column(input: TokenStream) -> TokenStream { let DeriveInput { ident, data, .. } = parse_macro_input!(input); - match derives::expand_derive_column(ident, data) { + match derives::expand_derive_column(&ident, &data) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } +} + +#[proc_macro_derive(DeriveCustomColumn)] +pub fn derive_custom_column(input: TokenStream) -> TokenStream { + let DeriveInput { ident, data, .. } = parse_macro_input!(input); + + match derives::expand_derive_custom_column(&ident, &data) { Ok(ts) => ts.into(), Err(e) => e.to_compile_error().into(), } @@ -74,3 +84,23 @@ pub fn derive_from_query_result(input: TokenStream) -> TokenStream { Err(e) => e.to_compile_error().into(), } } + +#[doc(hidden)] +#[proc_macro_attribute] +pub fn test(_: TokenStream, input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::ItemFn); + + let ret = &input.sig.output; + let name = &input.sig.ident; + let body = &input.block; + let attrs = &input.attrs; + + quote::quote! ( + #[test] + #(#attrs)* + fn #name() #ret { + crate::block_on!(async { #body }) + } + ) + .into() +} diff --git a/src/driver/mock.rs b/src/driver/mock.rs index 7de6aaaf..0a1629bf 100644 --- a/src/driver/mock.rs +++ b/src/driver/mock.rs @@ -25,6 +25,7 @@ pub trait MockDatabaseTrait: Send { } impl MockDatabaseConnector { + #[allow(unused_variables)] pub fn accepts(string: &str) -> bool { #[cfg(feature = "sqlx-mysql")] if crate::SqlxMySqlConnector::accepts(string) { @@ -41,6 +42,7 @@ impl MockDatabaseConnector { false } + #[allow(unused_variables)] pub async fn connect(string: &str) -> Result { macro_rules! connect_mock_db { ( $syntax: expr ) => { diff --git a/src/entity/base_entity.rs b/src/entity/base_entity.rs index 95d5603e..d0740307 100644 --- a/src/entity/base_entity.rs +++ b/src/entity/base_entity.rs @@ -3,7 +3,7 @@ use crate::{ ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, Related, RelationBuilder, RelationTrait, RelationType, Select, Update, UpdateMany, UpdateOne, }; -use sea_query::{Iden, IntoValueTuple}; +use sea_query::{Alias, Iden, IntoIden, IntoTableRef, IntoValueTuple, TableRef}; pub use sea_strum::IntoEnumIterator as Iterable; use std::fmt::Debug; @@ -12,10 +12,21 @@ pub trait IdenStatic: Iden + Copy + Debug + 'static { } pub trait EntityName: IdenStatic + Default { + fn schema_name(&self) -> Option<&str> { + None + } + fn table_name(&self) -> &str; fn module_name(&self) -> &str { - Self::table_name(self) + self.table_name() + } + + fn table_ref(&self) -> TableRef { + match self.schema_name() { + Some(schema) => (Alias::new(schema).into_iden(), self.into_iden()).into_table_ref(), + None => self.into_table_ref(), + } } } @@ -96,7 +107,7 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// assert_eq!( /// cake::Entity::find().one(&db).await?, @@ -159,7 +170,7 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// assert_eq!( /// cake::Entity::find_by_id(11).all(&db).await?, @@ -196,7 +207,7 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake_filling}; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// assert_eq!( /// cake_filling::Entity::find_by_id((2, 3)).all(&db).await?, @@ -264,7 +275,7 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// let insert_result = cake::Entity::insert(apple).exec(&db).await?; /// @@ -315,7 +326,7 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// let insert_result = cake::Entity::insert_many(vec![apple, orange]).exec(&db).await?; /// @@ -367,7 +378,7 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// assert_eq!( /// fruit::Entity::update(orange.clone()).exec(&db).await?, // Clone here because we need to assert_eq @@ -411,10 +422,10 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit, sea_query::{Expr, Value}}; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// let update_result = fruit::Entity::update_many() - /// .col_expr(fruit::Column::CakeId, Expr::value(Value::Null)) + /// .col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None))) /// .filter(fruit::Column::Name.contains("Apple")) /// .exec(&db) /// .await?; @@ -427,7 +438,7 @@ pub trait EntityTrait: EntityName { /// assert_eq!( /// db.into_transaction_log(), /// vec![Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, vec![Value::Null, "%Apple%".into()] + /// DbBackend::Postgres, r#"UPDATE "fruit" SET "cake_id" = $1 WHERE "fruit"."name" LIKE $2"#, vec![Value::Int(None), "%Apple%".into()] /// )]); /// ``` fn update_many() -> UpdateMany { @@ -460,7 +471,7 @@ pub trait EntityTrait: EntityName { /// ..Default::default() /// }; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// let delete_result = fruit::Entity::delete(orange).exec(&db).await?; /// @@ -503,7 +514,7 @@ pub trait EntityTrait: EntityName { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::fruit}; /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// let delete_result = fruit::Entity::delete_many() /// .filter(fruit::Column::Name.contains("Apple")) diff --git a/src/entity/column.rs b/src/entity/column.rs index e9c3b759..16546057 100644 --- a/src/entity/column.rs +++ b/src/entity/column.rs @@ -1,3 +1,4 @@ +use std::str::FromStr; use crate::{EntityName, IdenStatic, Iterable}; use sea_query::{DynIden, Expr, SeaRc, SelectStatement, SimpleExpr, Value}; @@ -77,7 +78,7 @@ macro_rules! bind_subquery_func { // LINT: when the operand value does not match column type /// Wrapper of the identically named method in [`sea_query::Expr`] -pub trait ColumnTrait: IdenStatic + Iterable { +pub trait ColumnTrait: IdenStatic + Iterable + FromStr { type EntityName: EntityName; fn def(&self) -> ColumnDef; @@ -290,6 +291,7 @@ impl From for sea_query::ColumnType { impl From for ColumnType { fn from(col_type: sea_query::ColumnType) -> Self { + #[allow(unreachable_patterns)] match col_type { sea_query::ColumnType::Char(s) => Self::Char(s), sea_query::ColumnType::String(s) => Self::String(s), @@ -347,4 +349,30 @@ mod tests { .join(" ") ); } + + #[test] + fn test_col_from_str() { + use std::str::FromStr; + + assert!(matches!( + fruit::Column::from_str("id"), + Ok(fruit::Column::Id) + )); + assert!(matches!( + fruit::Column::from_str("name"), + Ok(fruit::Column::Name) + )); + assert!(matches!( + fruit::Column::from_str("cake_id"), + Ok(fruit::Column::CakeId) + )); + assert!(matches!( + fruit::Column::from_str("cakeId"), + Ok(fruit::Column::CakeId) + )); + assert!(matches!( + fruit::Column::from_str("does_not_exist"), + Err(crate::ColumnFromStrErr(_)) + )); + } } diff --git a/src/entity/identity.rs b/src/entity/identity.rs index af68978c..d1cc3170 100644 --- a/src/entity/identity.rs +++ b/src/entity/identity.rs @@ -1,17 +1,24 @@ -use crate::IdenStatic; +use crate::{ColumnTrait, EntityTrait, IdenStatic}; use sea_query::{DynIden, IntoIden}; #[derive(Debug, Clone)] pub enum Identity { Unary(DynIden), Binary(DynIden, DynIden), - // Ternary(DynIden, DynIden, DynIden), + Ternary(DynIden, DynIden, DynIden), } pub trait IntoIdentity { fn into_identity(self) -> Identity; } +pub trait IdentityOf +where + E: EntityTrait, +{ + fn identity_of(self) -> Identity; +} + impl IntoIdentity for T where T: IdenStatic, @@ -30,3 +37,44 @@ where Identity::Binary(self.0.into_iden(), self.1.into_iden()) } } + +impl IntoIdentity for (T, C, R) +where + T: IdenStatic, + C: IdenStatic, + R: IdenStatic, +{ + fn into_identity(self) -> Identity { + Identity::Ternary(self.0.into_iden(), self.1.into_iden(), self.2.into_iden()) + } +} + +impl IdentityOf for C +where + E: EntityTrait, + C: ColumnTrait, +{ + fn identity_of(self) -> Identity { + self.into_identity() + } +} + +impl IdentityOf for (C, C) +where + E: EntityTrait, + C: ColumnTrait, +{ + fn identity_of(self) -> Identity { + self.into_identity() + } +} + +impl IdentityOf for (C, C, C) +where + E: EntityTrait, + C: ColumnTrait, +{ + fn identity_of(self) -> Identity { + self.into_identity() + } +} diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 25ae5f67..447117b7 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -1,7 +1,7 @@ pub use crate::{ error::*, ActiveModelBehavior, ActiveModelTrait, ColumnDef, ColumnTrait, ColumnType, - DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveEntity, DeriveModel, - DerivePrimaryKey, EntityName, EntityTrait, EnumIter, Iden, IdenStatic, ModelTrait, + DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, + DeriveModel, DerivePrimaryKey, EntityName, EntityTrait, EnumIter, Iden, IdenStatic, ModelTrait, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, Related, RelationDef, RelationTrait, Select, Value, }; @@ -12,6 +12,9 @@ pub use serde_json::Value as Json; #[cfg(feature = "with-chrono")] pub use chrono::NaiveDateTime as DateTime; +#[cfg(feature = "with-chrono")] +pub type DateTimeWithTimeZone = 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 7c9213d3..955660e2 100644 --- a/src/entity/relation.rs +++ b/src/entity/relation.rs @@ -1,6 +1,6 @@ -use crate::{EntityTrait, Identity, IntoIdentity, Iterable, QuerySelect, Select}; +use crate::{EntityTrait, Identity, IdentityOf, Iterable, QuerySelect, Select}; use core::marker::PhantomData; -use sea_query::{DynIden, IntoIden, JoinType}; +use sea_query::{JoinType, TableRef}; use std::fmt::Debug; #[derive(Clone, Debug)] @@ -30,8 +30,8 @@ where pub struct RelationDef { pub rel_type: RelationType, - pub from_tbl: DynIden, - pub to_tbl: DynIden, + pub from_tbl: TableRef, + pub to_tbl: TableRef, pub from_col: Identity, pub to_col: Identity, } @@ -43,8 +43,8 @@ where { entities: PhantomData<(E, R)>, rel_type: RelationType, - from_tbl: DynIden, - to_tbl: DynIden, + from_tbl: TableRef, + to_tbl: TableRef, from_col: Option, to_col: Option, } @@ -71,8 +71,8 @@ where Self { entities: PhantomData, rel_type, - from_tbl: from.into_iden(), - to_tbl: to.into_iden(), + from_tbl: from.table_ref(), + to_tbl: to.table_ref(), from_col: None, to_col: None, } @@ -89,13 +89,19 @@ where } } - pub fn from(mut self, identifier: E::Column) -> Self { - self.from_col = Some(identifier.into_identity()); + pub fn from(mut self, identifier: T) -> Self + where + T: IdentityOf, + { + self.from_col = Some(identifier.identity_of()); self } - pub fn to(mut self, identifier: R::Column) -> Self { - self.to_col = Some(identifier.into_identity()); + pub fn to(mut self, identifier: T) -> Self + where + T: IdentityOf, + { + self.to_col = Some(identifier.identity_of()); self } } diff --git a/src/error.rs b/src/error.rs index eff99912..8a695dac 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,3 +16,14 @@ impl std::fmt::Display for DbErr { } } } + +#[derive(Debug, Clone)] +pub struct ColumnFromStrErr(pub String); + +impl std::error::Error for ColumnFromStrErr {} + +impl std::fmt::Display for ColumnFromStrErr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} diff --git a/src/executor/paginator.rs b/src/executor/paginator.rs index d44bbc5e..0cc7acbc 100644 --- a/src/executor/paginator.rs +++ b/src/executor/paginator.rs @@ -97,7 +97,7 @@ where /// # use sea_orm::{error::*, MockDatabase, DbBackend}; /// # let owned_db = MockDatabase::new(DbBackend::Postgres).into_connection(); /// # let db = &owned_db; - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; /// let mut cake_pages = cake::Entity::find() @@ -125,7 +125,7 @@ where /// # use sea_orm::{error::*, MockDatabase, DbBackend}; /// # let owned_db = MockDatabase::new(DbBackend::Postgres).into_connection(); /// # let db = &owned_db; - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { + /// # let _: Result<(), DbErr> = smol::block_on(async { /// # /// use futures::TryStreamExt; /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; @@ -203,7 +203,7 @@ mod tests { (db, num_items) } - #[async_std::test] + #[smol_potat::test] async fn fetch_page() -> Result<(), DbErr> { let (db, pages) = setup(); @@ -233,7 +233,7 @@ mod tests { Ok(()) } - #[async_std::test] + #[smol_potat::test] async fn fetch() -> Result<(), DbErr> { let (db, pages) = setup(); @@ -267,7 +267,7 @@ mod tests { Ok(()) } - #[async_std::test] + #[smol_potat::test] async fn num_pages() -> Result<(), DbErr> { let (db, num_items) = setup_num_items(); @@ -299,7 +299,7 @@ mod tests { Ok(()) } - #[async_std::test] + #[smol_potat::test] async fn next_and_cur_page() -> Result<(), DbErr> { let (db, _) = setup(); @@ -315,7 +315,7 @@ mod tests { Ok(()) } - #[async_std::test] + #[smol_potat::test] async fn fetch_and_next() -> Result<(), DbErr> { let (db, pages) = setup(); @@ -350,7 +350,7 @@ mod tests { Ok(()) } - #[async_std::test] + #[smol_potat::test] async fn into_stream() -> Result<(), DbErr> { let (db, pages) = setup(); diff --git a/src/executor/query.rs b/src/executor/query.rs index b58c5a2c..b7e1d12e 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -1,6 +1,4 @@ use crate::{debug_print, DbErr}; -use chrono::NaiveDateTime; -use serde_json::Value as Json; use std::fmt; #[derive(Debug)] @@ -219,8 +217,66 @@ macro_rules! try_getable_mysql { } #[cfg(feature = "sqlx-postgres")] QueryResultRow::SqlxPostgres(_) => { + panic!("{} unsupported by sqlx-postgres", stringify!($type)) + } + #[cfg(feature = "sqlx-sqlite")] + QueryResultRow::SqlxSqlite(_) => { panic!("{} unsupported by sqlx-sqlite", stringify!($type)) } + #[cfg(feature = "mock")] + QueryResultRow::Mock(row) => match row.try_get(column.as_str()) { + Ok(v) => Ok(Some(v)), + Err(e) => { + debug_print!("{:#?}", e.to_string()); + Ok(None) + } + }, + } + } + } + }; +} + +macro_rules! try_getable_postgres { + ( $type: ty ) => { + impl TryGetable for $type { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + let column = format!("{}{}", pre, col); + match &res.row { + #[cfg(feature = "sqlx-mysql")] + QueryResultRow::SqlxMySql(_) => { + panic!("{} unsupported by sqlx-mysql", stringify!($type)) + } + #[cfg(feature = "sqlx-postgres")] + QueryResultRow::SqlxPostgres(row) => { + use sqlx::Row; + row.try_get(column.as_str()) + .map_err(crate::sqlx_error_to_query_err) + } + #[cfg(feature = "sqlx-sqlite")] + QueryResultRow::SqlxSqlite(_) => { + panic!("{} unsupported by sqlx-sqlite", stringify!($type)) + } + #[cfg(feature = "mock")] + QueryResultRow::Mock(row) => Ok(row.try_get(column.as_str())?), + } + } + } + + impl TryGetable for Option<$type> { + fn try_get(res: &QueryResult, pre: &str, col: &str) -> Result { + let column = format!("{}{}", pre, col); + match &res.row { + #[cfg(feature = "sqlx-mysql")] + QueryResultRow::SqlxMySql(_) => { + panic!("{} unsupported by sqlx-mysql", stringify!($type)) + } + #[cfg(feature = "sqlx-postgres")] + QueryResultRow::SqlxPostgres(row) => { + use sqlx::Row; + row.try_get::, _>(column.as_str()) + .map_err(crate::sqlx_error_to_query_err) + } #[cfg(feature = "sqlx-sqlite")] QueryResultRow::SqlxSqlite(_) => { panic!("{} unsupported by sqlx-sqlite", stringify!($type)) @@ -251,14 +307,15 @@ try_getable_mysql!(u64); try_getable_all!(f32); try_getable_all!(f64); try_getable_all!(String); -try_getable_all!(NaiveDateTime); -try_getable_all!(Json); -#[cfg(feature = "with-uuid")] -use uuid::Uuid; +#[cfg(feature = "with-json")] +try_getable_all!(serde_json::Value); -#[cfg(feature = "with-uuid")] -try_getable_all!(Uuid); +#[cfg(feature = "with-chrono")] +try_getable_all!(chrono::NaiveDateTime); + +#[cfg(feature = "with-chrono")] +try_getable_postgres!(chrono::DateTime); #[cfg(feature = "with-rust_decimal")] use rust_decimal::Decimal; @@ -345,3 +402,6 @@ impl TryGetable for Option { } } } + +#[cfg(feature = "with-uuid")] +try_getable_all!(uuid::Uuid); diff --git a/src/executor/select.rs b/src/executor/select.rs index 647b6c95..6196ec73 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -76,33 +76,6 @@ impl Select where E: EntityTrait, { - /// ``` - /// # #[cfg(feature = "mock")] - /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; - /// # - /// # let db = MockDatabase::new(DbBackend::Postgres).into_connection(); - /// # - /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; - /// - /// # let _: Result<(), DbErr> = async_std::task::block_on(async { - /// # - /// let cheese: Option = cake::Entity::find().from_raw_sql( - /// Statement::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()] - /// ) - /// ).one(&db).await?; - /// # - /// # Ok(()) - /// # }); - /// - /// assert_eq!( - /// db.into_transaction_log(), - /// vec![ - /// Transaction::from_sql_and_values( - /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()] - /// ), - /// ]); - /// ``` #[allow(clippy::wrong_self_convention)] pub fn from_raw_sql(self, stmt: Statement) -> SelectorRaw> { SelectorRaw { @@ -289,6 +262,167 @@ impl SelectorRaw where S: SelectorTrait, { + /// ``` + /// # #[cfg(feature = "mock")] + /// # 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, FromQueryResult}; + /// + /// #[derive(Debug, PartialEq, FromQueryResult)] + /// struct SelectResult { + /// name: String, + /// num_of_cakes: i32, + /// } + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let res: Vec = cake::Entity::find().from_raw_sql( + /// Statement::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."name", count("cake"."id") AS "num_of_cakes" FROM "cake""#, vec![] + /// ) + /// ) + /// .into_model::() + /// .all(&db) + /// .await?; + /// + /// assert_eq!( + /// res, + /// vec![ + /// SelectResult { + /// name: "Chocolate Forest".to_owned(), + /// num_of_cakes: 1, + /// }, + /// SelectResult { + /// name: "New York Cheese".to_owned(), + /// num_of_cakes: 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![] + /// ), + /// ]); + /// ``` + pub fn into_model(self) -> SelectorRaw> + where + M: FromQueryResult, + { + SelectorRaw { + stmt: self.stmt, + selector: SelectModel { model: PhantomData }, + } + } + + /// ``` + /// # #[cfg(feature = "mock")] + /// # 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}; + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let res: Vec = cake::Entity::find().from_raw_sql( + /// Statement::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] + /// ) + /// ) + /// .into_json() + /// .all(&db) + /// .await?; + /// + /// assert_eq!( + /// res, + /// vec![ + /// serde_json::json!({ + /// "name": "Chocolate Forest", + /// "num_of_cakes": 1, + /// }), + /// serde_json::json!({ + /// "name": "New York Cheese", + /// "num_of_cakes": 1, + /// }), + /// ] + /// ); + /// # + /// # Ok(()) + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![ + /// Transaction::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] + /// ), + /// ]); + /// ``` + #[cfg(feature = "with-json")] + pub fn into_json(self) -> SelectorRaw> { + SelectorRaw { + stmt: self.stmt, + selector: SelectModel { model: PhantomData }, + } + } + + /// ``` + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let _: Option = cake::Entity::find().from_raw_sql( + /// Statement::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()] + /// ) + /// ).one(&db).await?; + /// # + /// # Ok(()) + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![ + /// Transaction::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake" WHERE "id" = $1"#, vec![1.into()] + /// ), + /// ]); + /// ``` pub async fn one(self, db: &DatabaseConnection) -> Result, DbErr> { let row = db.query_one(self.stmt).await?; match row { @@ -297,6 +431,33 @@ where } } + /// ``` + /// # #[cfg(feature = "mock")] + /// # use sea_orm::{error::*, tests_cfg::*, MockDatabase, Transaction, DbBackend}; + /// # + /// # let db = MockDatabase::new(DbBackend::Postgres).into_connection(); + /// # + /// use sea_orm::{entity::*, query::*, tests_cfg::cake}; + /// + /// # let _: Result<(), DbErr> = smol::block_on(async { + /// # + /// let _: Vec = cake::Entity::find().from_raw_sql( + /// Statement::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] + /// ) + /// ).all(&db).await?; + /// # + /// # Ok(()) + /// # }); + /// + /// assert_eq!( + /// db.into_transaction_log(), + /// vec![ + /// Transaction::from_sql_and_values( + /// DbBackend::Postgres, r#"SELECT "cake"."id", "cake"."name" FROM "cake""#, vec![] + /// ), + /// ]); + /// ``` pub async fn all(self, db: &DatabaseConnection) -> Result, DbErr> { let rows = db.query_all(self.stmt).await?; let mut models = Vec::new(); diff --git a/src/lib.rs b/src/lib.rs index 54ed5dc9..377bd62b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ //!
//! -//! +//! //! //!

SeaORM

//! @@ -18,30 +18,26 @@ //! //! # SeaORM //! -//! Inspired by ActiveRecord, Eloquent and TypeORM, SeaORM aims to provide you an intuitive and ergonomic -//! API to make working with databases in Rust a first-class experience. +//! SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. //! //! ```markdown //! This is an early release of SeaORM, the API is not stable yet. //! ``` //! -//!
-//! -//! [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) -//! [![Getting Started](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx) -//! [![Getting Started](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37) -//! -//!
+//! [![Getting Started](https://img.shields.io/badge/Getting%20Started-blue)](https://www.sea-ql.org/SeaORM/docs/index) +//! [![Examples](https://img.shields.io/badge/Examples-orange)](https://github.com/SeaQL/sea-orm/tree/master/examples/sqlx) +//! [![Starter Kit](https://img.shields.io/badge/Starter%20Kit-green)](https://github.com/SeaQL/sea-orm/issues/37) +//! [![Discord](https://img.shields.io/discord/873880840487206962?label=Discord)](https://discord.com/invite/uCPdDXzbdv) //! //! ## Features //! //! 1. Async //! -//! Relying on SQLx, SeaORM is a new library with async support from day 1. +//! Relying on [SQLx](https://github.com/launchbadge/sqlx), SeaORM is a new library with async support from day 1. //! //! 2. Dynamic //! -//! Built upon SeaQuery, SeaORM allows you to build complex queries without 'fighting the ORM'. +//! Built upon [SeaQuery](https://github.com/SeaQL/sea-query), SeaORM allows you to build complex queries without 'fighting the ORM'. //! //! 3. Testable //! @@ -133,7 +129,7 @@ //! //! // update many: UPDATE "fruit" SET "cake_id" = NULL WHERE "fruit"."name" LIKE '%Apple%' //! Fruit::update_many() -//! .col_expr(fruit::Column::CakeId, Expr::value(Value::Null)) +//! .col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None))) //! .filter(fruit::Column::Name.contains("Apple")) //! .exec(db) //! .await?; @@ -222,8 +218,8 @@ pub use executor::*; pub use query::*; pub use sea_orm_macros::{ - DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveEntity, DeriveModel, - DerivePrimaryKey, FromQueryResult, + DeriveActiveModel, DeriveActiveModelBehavior, DeriveColumn, DeriveCustomColumn, DeriveEntity, + DeriveModel, DerivePrimaryKey, FromQueryResult, }; pub use sea_query; diff --git a/src/query/delete.rs b/src/query/delete.rs index 8c3b2a52..e5cefdef 100644 --- a/src/query/delete.rs +++ b/src/query/delete.rs @@ -65,7 +65,7 @@ impl Delete { { let myself = DeleteOne { query: DeleteStatement::new() - .from_table(A::Entity::default().into_iden()) + .from_table(A::Entity::default().table_ref()) .to_owned(), model: model.into_active_model(), }; diff --git a/src/query/helper.rs b/src/query/helper.rs index 78eaa4e7..6ade581a 100644 --- a/src/query/helper.rs +++ b/src/query/helper.rs @@ -2,7 +2,9 @@ use crate::{ ColumnTrait, EntityTrait, Identity, IntoSimpleExpr, Iterable, ModelTrait, PrimaryKeyToColumn, RelationDef, }; -use sea_query::{Alias, Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr}; +use sea_query::{ + Alias, Expr, IntoCondition, SeaRc, SelectExpr, SelectStatement, SimpleExpr, TableRef, +}; pub use sea_query::{Condition, ConditionalStatement, DynIden, JoinType, Order, OrderedStatement}; // LINT: when the column does not appear in tables selected from @@ -269,8 +271,8 @@ pub trait QueryFilter: Sized { } fn join_condition(rel: RelationDef) -> SimpleExpr { - let from_tbl = rel.from_tbl.clone(); - let to_tbl = rel.to_tbl.clone(); + let from_tbl = unpack_table_ref(&rel.from_tbl); + let to_tbl = unpack_table_ref(&rel.to_tbl); let owner_keys = rel.from_col; let foreign_keys = rel.to_col; @@ -283,6 +285,22 @@ fn join_condition(rel: RelationDef) -> SimpleExpr { .equals(SeaRc::clone(&to_tbl), f1) .and(Expr::tbl(SeaRc::clone(&from_tbl), o2).equals(SeaRc::clone(&to_tbl), f2)) } + (Identity::Ternary(o1, o2, o3), Identity::Ternary(f1, f2, f3)) => { + Expr::tbl(SeaRc::clone(&from_tbl), o1) + .equals(SeaRc::clone(&to_tbl), f1) + .and(Expr::tbl(SeaRc::clone(&from_tbl), o2).equals(SeaRc::clone(&to_tbl), f2)) + .and(Expr::tbl(SeaRc::clone(&from_tbl), o3).equals(SeaRc::clone(&to_tbl), f3)) + } _ => panic!("Owner key and foreign key mismatch"), } } + +fn unpack_table_ref(table_ref: &TableRef) -> DynIden { + match table_ref { + TableRef::Table(tbl) => SeaRc::clone(tbl), + TableRef::SchemaTable(_, tbl) => SeaRc::clone(tbl), + TableRef::TableAlias(tbl, _) => SeaRc::clone(tbl), + TableRef::SchemaTableAlias(_, tbl, _) => SeaRc::clone(tbl), + TableRef::SubQuery(_, tbl) => SeaRc::clone(tbl), + } +} diff --git a/src/query/insert.rs b/src/query/insert.rs index 25d0a9ea..abd52b4b 100644 --- a/src/query/insert.rs +++ b/src/query/insert.rs @@ -1,6 +1,6 @@ -use crate::{ActiveModelTrait, EntityTrait, IntoActiveModel, Iterable, QueryTrait}; +use crate::{ActiveModelTrait, EntityName, EntityTrait, IntoActiveModel, Iterable, QueryTrait}; use core::marker::PhantomData; -use sea_query::{InsertStatement, IntoIden}; +use sea_query::InsertStatement; #[derive(Clone, Debug)] pub struct Insert @@ -28,7 +28,7 @@ where pub(crate) fn new() -> Self { Self { query: InsertStatement::new() - .into_table(A::Entity::default().into_iden()) + .into_table(A::Entity::default().table_ref()) .to_owned(), columns: Vec::new(), model: PhantomData, diff --git a/src/query/join.rs b/src/query/join.rs index 61c6c1b9..72726d14 100644 --- a/src/query/join.rs +++ b/src/query/join.rs @@ -61,7 +61,7 @@ where #[cfg(test)] mod tests { - use crate::tests_cfg::{cake, filling, fruit}; + use crate::tests_cfg::{cake, cake_filling, cake_filling_price, filling, fruit}; use crate::{ColumnTrait, DbBackend, EntityTrait, ModelTrait, QueryFilter, QueryTrait}; #[test] @@ -182,4 +182,42 @@ mod tests { .join(" ") ); } + + #[test] + fn join_8() { + use crate::{Related, Select}; + + let find_cake_filling_price: Select = + cake_filling::Entity::find_related(); + assert_eq!( + find_cake_filling_price.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "cake_filling_price"."cake_id", "cake_filling_price"."filling_id", "cake_filling_price"."price""#, + r#"FROM "public"."cake_filling_price""#, + r#"INNER JOIN "cake_filling" ON"#, + r#"("cake_filling"."cake_id" = "cake_filling_price"."cake_id") AND"#, + r#"("cake_filling"."filling_id" = "cake_filling_price"."filling_id")"#, + ] + .join(" ") + ); + } + + #[test] + fn join_9() { + use crate::{Related, Select}; + + let find_cake_filling: Select = + cake_filling_price::Entity::find_related(); + assert_eq!( + find_cake_filling.build(DbBackend::Postgres).to_string(), + [ + r#"SELECT "cake_filling"."cake_id", "cake_filling"."filling_id""#, + r#"FROM "cake_filling""#, + r#"INNER JOIN "public"."cake_filling_price" ON"#, + r#"("cake_filling_price"."cake_id" = "cake_filling"."cake_id") AND"#, + r#"("cake_filling_price"."filling_id" = "cake_filling"."filling_id")"#, + ] + .join(" ") + ); + } } diff --git a/src/query/json.rs b/src/query/json.rs index 95538032..589f0fad 100644 --- a/src/query/json.rs +++ b/src/query/json.rs @@ -140,11 +140,11 @@ impl FromQueryResult for JsonValue { #[cfg(feature = "mock")] mod tests { use crate::tests_cfg::cake; - use crate::{entity::*, DbBackend, MockDatabase}; + use crate::{entity::*, DbBackend, DbErr, MockDatabase}; use sea_query::Value; - #[async_std::test] - async fn to_json_1() { + #[smol_potat::test] + async fn to_json_1() -> Result<(), DbErr> { let db = MockDatabase::new(DbBackend::Postgres) .append_query_results(vec![vec![maplit::btreemap! { "id" => Into::::into(128), "name" => Into::::into("apple") @@ -158,5 +158,7 @@ mod tests { "name": "apple" })) ); + + Ok(()) } } diff --git a/src/query/select.rs b/src/query/select.rs index 91db2907..1b0c93c3 100644 --- a/src/query/select.rs +++ b/src/query/select.rs @@ -2,7 +2,7 @@ use crate::{ColumnTrait, EntityTrait, Iterable, QueryFilter, QueryOrder, QuerySe use core::fmt::Debug; use core::marker::PhantomData; pub use sea_query::JoinType; -use sea_query::{DynIden, IntoColumnRef, IntoIden, SeaRc, SelectStatement, SimpleExpr}; +use sea_query::{DynIden, IntoColumnRef, SeaRc, SelectStatement, SimpleExpr}; #[derive(Clone, Debug)] pub struct Select @@ -119,7 +119,7 @@ where } fn prepare_from(mut self) -> Self { - self.query.from(E::default().into_iden()); + self.query.from(E::default().table_ref()); self } } diff --git a/src/query/update.rs b/src/query/update.rs index ca750d88..21fd39cb 100644 --- a/src/query/update.rs +++ b/src/query/update.rs @@ -49,7 +49,7 @@ impl Update { { let myself = UpdateOne { query: UpdateStatement::new() - .table(A::Entity::default().into_iden()) + .table(A::Entity::default().table_ref()) .to_owned(), model, }; @@ -75,7 +75,7 @@ impl Update { E: EntityTrait, { UpdateMany { - query: UpdateStatement::new().table(entity.into_iden()).to_owned(), + query: UpdateStatement::new().table(entity.table_ref()).to_owned(), entity: PhantomData, } } @@ -232,7 +232,7 @@ mod tests { fn update_4() { assert_eq!( Update::many(fruit::Entity) - .col_expr(fruit::Column::CakeId, Expr::value(Value::Null)) + .col_expr(fruit::Column::CakeId, Expr::value(Value::Int(None))) .filter(fruit::Column::Id.eq(2)) .build(DbBackend::Postgres) .to_string(), diff --git a/src/tests_cfg/cake_filling.rs b/src/tests_cfg/cake_filling.rs index 6b5c20aa..b1151ee4 100644 --- a/src/tests_cfg/cake_filling.rs +++ b/src/tests_cfg/cake_filling.rs @@ -66,4 +66,10 @@ impl RelationTrait for Relation { } } +impl Related for Entity { + fn to() -> RelationDef { + super::cake_filling_price::Relation::CakeFilling.def().rev() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/src/tests_cfg/cake_filling_price.rs b/src/tests_cfg/cake_filling_price.rs new file mode 100644 index 00000000..c0bcbea1 --- /dev/null +++ b/src/tests_cfg/cake_filling_price.rs @@ -0,0 +1,80 @@ +use crate as sea_orm; +use crate::entity::prelude::*; + +#[derive(Copy, Clone, Default, Debug, DeriveEntity)] +pub struct Entity; + +impl EntityName for Entity { + fn schema_name(&self) -> Option<&str> { + Some("public") + } + + fn table_name(&self) -> &str { + "cake_filling_price" + } +} + +#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)] +pub struct Model { + pub cake_id: i32, + pub filling_id: i32, + pub price: Decimal, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +pub enum Column { + CakeId, + FillingId, + Price, +} + +#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] +pub enum PrimaryKey { + CakeId, + FillingId, +} + +impl PrimaryKeyTrait for PrimaryKey { + fn auto_increment() -> bool { + false + } +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + CakeFilling, +} + +impl ColumnTrait for Column { + type EntityName = Entity; + + fn def(&self) -> ColumnDef { + match self { + Self::CakeId => ColumnType::Integer.def(), + Self::FillingId => ColumnType::Integer.def(), + Self::Price => ColumnType::Decimal(None).def(), + } + } +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::CakeFilling => Entity::belongs_to(super::cake_filling::Entity) + .from((Column::CakeId, Column::FillingId)) + .to(( + super::cake_filling::Column::CakeId, + super::cake_filling::Column::FillingId, + )) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::CakeFilling.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/tests_cfg/filling.rs b/src/tests_cfg/filling.rs index a246d146..b439af7b 100644 --- a/src/tests_cfg/filling.rs +++ b/src/tests_cfg/filling.rs @@ -16,12 +16,25 @@ pub struct Model { pub name: String, } -#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] +// If your column names are not in snake-case, derive `DeriveCustomColumn` here. +#[derive(Copy, Clone, Debug, EnumIter, DeriveCustomColumn)] pub enum Column { Id, Name, } +// Then, customize each column names here. +impl IdenStatic for Column { + fn as_str(&self) -> &str { + match self { + // Override column names + Self::Id => "id", + // Leave all other columns using default snake-case values + _ => self.default_as_str(), + } + } +} + #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)] pub enum PrimaryKey { Id, diff --git a/src/tests_cfg/mod.rs b/src/tests_cfg/mod.rs index ca9b8a68..afb9e115 100644 --- a/src/tests_cfg/mod.rs +++ b/src/tests_cfg/mod.rs @@ -2,10 +2,12 @@ pub mod cake; pub mod cake_filling; +pub mod cake_filling_price; pub mod filling; pub mod fruit; pub use cake::Entity as Cake; pub use cake_filling::Entity as CakeFilling; +pub use cake_filling_price::Entity as CakeFillingPrice; pub use filling::Entity as Filling; pub use fruit::Entity as Fruit; diff --git a/tests/basic.rs b/tests/basic.rs index db38a7d5..78e3f25c 100644 --- a/tests/basic.rs +++ b/tests/basic.rs @@ -1,10 +1,10 @@ +pub mod common; + #[allow(unused_imports)] use sea_orm::{entity::*, error::*, sea_query, tests_cfg::*, Database, DbConn}; // DATABASE_URL="sqlite::memory:" cargo test --features sqlx-sqlit,runtime-async-std --test basic -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(feature = "sqlx-sqlite")] async fn main() { use std::env; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 5729aabb..54630999 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,7 +1,9 @@ -pub mod setup; -use sea_orm::DatabaseConnection; pub mod bakery_chain; +pub mod runtime; +pub mod setup; + pub use bakery_chain::*; +use sea_orm::DatabaseConnection; use std::env; pub struct TestContext { diff --git a/tests/common/runtime.rs b/tests/common/runtime.rs new file mode 100644 index 00000000..bc02f003 --- /dev/null +++ b/tests/common/runtime.rs @@ -0,0 +1,26 @@ +#[cfg(feature = "runtime-async-std")] +#[macro_export] +macro_rules! block_on { + ($($expr:tt)*) => { + ::async_std::task::block_on( $($expr)* ) + }; +} + +#[cfg(feature = "runtime-actix")] +#[macro_export] +macro_rules! block_on { + ($($expr:tt)*) => { + ::actix_rt::System::new() + .block_on( $($expr)* ) + }; +} + +#[cfg(feature = "runtime-tokio")] +#[macro_export] +macro_rules! block_on { + ($($expr:tt)*) => { + ::tokio::runtime::Runtime::new() + .unwrap() + .block_on( $($expr)* ) + }; +} diff --git a/tests/crud_tests.rs b/tests/crud_tests.rs index 94412a4d..3c26ddfd 100644 --- a/tests/crud_tests.rs +++ b/tests/crud_tests.rs @@ -8,9 +8,7 @@ mod crud; // Run the test locally: // DATABASE_URL="mysql://root:root@localhost" cargo test --features sqlx-mysql,runtime-async-std --test crud_tests // DATABASE_URL="postgres://root:root@localhost" cargo test --features sqlx-postgres,runtime-async-std --test crud_tests -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", diff --git a/tests/query_tests.rs b/tests/query_tests.rs index 2791f908..4e905686 100644 --- a/tests/query_tests.rs +++ b/tests/query_tests.rs @@ -6,9 +6,7 @@ pub use common::{bakery_chain::*, setup::*, TestContext}; // Run the test locally: // DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test query_tests -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -23,9 +21,7 @@ pub async fn find_one_with_no_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -50,9 +46,7 @@ pub async fn find_one_with_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -67,9 +61,7 @@ pub async fn find_by_id_with_no_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -98,9 +90,7 @@ pub async fn find_by_id_with_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -115,9 +105,7 @@ pub async fn find_all_with_no_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -151,9 +139,7 @@ pub async fn find_all_with_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -191,9 +177,7 @@ pub async fn find_all_filter_no_result() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", diff --git a/tests/relational_tests.rs b/tests/relational_tests.rs index 85fdee41..ae1236a2 100644 --- a/tests/relational_tests.rs +++ b/tests/relational_tests.rs @@ -8,9 +8,7 @@ pub use common::{bakery_chain::*, setup::*, TestContext}; // Run the test locally: // DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test relational_tests -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -91,9 +89,7 @@ pub async fn left_join() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-postgres"))] pub async fn right_join() { let ctx = TestContext::new("test_right_join").await; @@ -174,9 +170,7 @@ pub async fn right_join() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -265,9 +259,7 @@ pub async fn inner_join() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", @@ -371,9 +363,7 @@ pub async fn group_by() { ctx.delete().await; } -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any( feature = "sqlx-mysql", feature = "sqlx-sqlite", diff --git a/tests/sequential_op_tests.rs b/tests/sequential_op_tests.rs index d734ceac..a854c768 100644 --- a/tests/sequential_op_tests.rs +++ b/tests/sequential_op_tests.rs @@ -9,9 +9,7 @@ pub use common::{bakery_chain::*, setup::*, TestContext}; // Run the test locally: // DATABASE_URL="mysql://root:@localhost" cargo test --features sqlx-mysql,runtime-async-std --test sequential_op_tests -#[cfg_attr(feature = "runtime-async-std", async_std::test)] -#[cfg_attr(feature = "runtime-actix", actix_rt::test)] -#[cfg_attr(feature = "runtime-tokio", tokio::test)] +#[sea_orm_macros::test] #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-postgres"))] pub async fn test_multiple_operations() { let ctx = TestContext::new("multiple_sequential_operations").await;