From bcc5b5066a095b2821329337aea1babf3ab79fe5 Mon Sep 17 00:00:00 2001 From: Forest Anderson Date: Sat, 16 Oct 2021 23:19:48 -0400 Subject: [PATCH 01/13] Changed manual url parsing to use Url crate --- Cargo.toml | 1 + sea-orm-cli/Cargo.toml | 1 + sea-orm-cli/src/main.rs | 76 +++++++++++++++++++---------------- src/database/db_connection.rs | 8 ++-- 4 files changed, 49 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7dfb7cb82..716903748 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ serde_json = { version = "^1", optional = true } sqlx = { version = "^0.5", optional = true } uuid = { version = "0.8", features = ["serde", "v4"], optional = true } ouroboros = "0.11" +url = "^2.2" [dev-dependencies] smol = { version = "^1.2" } diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 02fddcb16..320b35cf3 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -32,6 +32,7 @@ sea-schema = { version = "^0.2.9", default-features = false, features = [ sqlx = { version = "^0.5", default-features = false, features = [ "mysql", "postgres" ] } env_logger = { version = "^0.9" } log = { version = "^0.4" } +url = "^2.2" [features] default = [ "runtime-async-std-native-tls" ] diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index 528ce5f54..6ce8cc315 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -3,6 +3,7 @@ use dotenv::dotenv; use log::LevelFilter; use sea_orm_codegen::{EntityTransformer, OutputFile, WithSerde}; use std::{error::Error, fmt::Display, fs, io::Write, path::Path, process::Command, str::FromStr}; +use url::Url; mod cli; @@ -23,7 +24,8 @@ async fn main() { async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box> { match matches.subcommand() { ("entity", Some(args)) => { - let url = args.value_of("DATABASE_URL").unwrap(); + // The database should be a valid URL that can be parsed + let url = Url::parse(args.value_of("DATABASE_URL").unwrap())?; let output_dir = args.value_of("OUTPUT_DIR").unwrap(); let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); let tables = args @@ -33,7 +35,7 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box bool { - if tables.len() > 0 { + if !tables.is_empty() { return tables.contains(&table); } @@ -43,7 +45,7 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box) -> Result<(), Box { + 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_tables(&schema.info.name)) - .filter(|schema| filter_hidden_tables(&schema.info.name)) - .map(|schema| schema.write()) - .collect() - } else if url.starts_with("postgres://") || url.starts_with("postgresql://") { - use sea_schema::postgres::discovery::SchemaDiscovery; - use sqlx::PgPool; + // TODO: as far as I can tell, this used to be the last + // value of the url, which should have been the database + // name? + let schema = url.path_segments().unwrap().last().unwrap(); + let connection = MySqlPool::connect(url.as_str()).await?; + let schema_discovery = SchemaDiscovery::new(connection, schema); + let schema = schema_discovery.discover().await; + schema + .tables + .into_iter() + .filter(|schema| filter_tables(&schema.info.name)) + .filter(|schema| filter_hidden_tables(&schema.info.name)) + .map(|schema| schema.write()) + .collect() + } + "postgres" | "postgresql" => { + 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_tables(&schema.info.name)) - .filter(|schema| filter_hidden_tables(&schema.info.name)) - .map(|schema| schema.write()) - .collect() - } else { - panic!("This database is not supported ({})", url) + let schema = args.value_of("DATABASE_SCHEMA").unwrap_or("public"); + let connection = PgPool::connect(url.as_str()).await?; + let schema_discovery = SchemaDiscovery::new(connection, schema); + let schema = schema_discovery.discover().await; + schema + .tables + .into_iter() + .filter(|schema| filter_tables(&schema.info.name)) + .filter(|schema| filter_hidden_tables(&schema.info.name)) + .map(|schema| schema.write()) + .collect() + } + _ => unimplemented!("{} is not supported", url.scheme()), }; let output = EntityTransformer::transform(table_stmts)? @@ -99,6 +105,8 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box bool { + let base_url_parsed = Url::parse(base_url).unwrap(); match self { Self::Postgres => { - base_url.starts_with("postgres://") || base_url.starts_with("postgresql://") + base_url_parsed.scheme() == "postgres" || base_url_parsed.scheme() == "postgresql" } - Self::MySql => base_url.starts_with("mysql://"), - Self::Sqlite => base_url.starts_with("sqlite:"), + Self::MySql => base_url_parsed.scheme() == "mysql", + Self::Sqlite => base_url_parsed.scheme() == "sqlite", } } From a346b3429921513828eb83d2a49d551a76a8d3c6 Mon Sep 17 00:00:00 2001 From: Forest Anderson Date: Mon, 18 Oct 2021 14:28:36 -0400 Subject: [PATCH 02/13] Improve errors --- sea-orm-cli/src/main.rs | 52 +++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index 6ce8cc315..f5c595290 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -25,7 +25,15 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box { // The database should be a valid URL that can be parsed - let url = Url::parse(args.value_of("DATABASE_URL").unwrap())?; + // protocol://username:password@host/database_name + let url = match Url::parse( + args.value_of("DATABASE_URL") + .expect("No database url could be found"), + ) { + Ok(url) => url, + Err(e) => panic!("Invalid database url: {}", e), + }; + let output_dir = args.value_of("OUTPUT_DIR").unwrap(); let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); let tables = args @@ -34,6 +42,36 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box>(); let expanded_format = args.is_present("EXPANDED_FORMAT"); let with_serde = args.value_of("WITH_SERDE").unwrap(); + if args.is_present("VERBOSE") { + let _ = ::env_logger::builder() + .filter_level(LevelFilter::Debug) + .is_test(true) + .try_init(); + } + + // The database name should be the first element of the path string + // + // Throwing an error if there is no database name since it might be + // accepted by the database without it, while we're looking to dump + // information from a particular database + let database_name = url + .path_segments() + .expect(&format!( + "There is no database name as part of the url path: {}", + url.as_str() + )) + .next() + .unwrap(); + + // An empty string as the database name is also an error + if database_name.is_empty() { + panic!( + "There is no database name as part of the url path: {}", + url.as_str() + ); + } + + // Closures for filtering tables let filter_tables = |table: &str| -> bool { if !tables.is_empty() { return tables.contains(&table); @@ -48,24 +86,14 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box { use sea_schema::mysql::discovery::SchemaDiscovery; use sqlx::MySqlPool; - // TODO: as far as I can tell, this used to be the last - // value of the url, which should have been the database - // name? - let schema = url.path_segments().unwrap().last().unwrap(); let connection = MySqlPool::connect(url.as_str()).await?; - let schema_discovery = SchemaDiscovery::new(connection, schema); + let schema_discovery = SchemaDiscovery::new(connection, database_name); let schema = schema_discovery.discover().await; schema .tables From 8d06aea00b17149cf58e3c0fc459f79429d1815b Mon Sep 17 00:00:00 2001 From: Forest Anderson Date: Tue, 19 Oct 2021 01:11:54 -0400 Subject: [PATCH 03/13] Added tests --- sea-orm-cli/Cargo.toml | 1 + sea-orm-cli/src/main.rs | 169 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 156 insertions(+), 14 deletions(-) diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 320b35cf3..aead20df2 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -33,6 +33,7 @@ sqlx = { version = "^0.5", default-features = false, features = [ "mysql", "post env_logger = { version = "^0.9" } log = { version = "^0.4" } url = "^2.2" +smol = "1.2.5" [features] default = [ "runtime-async-std-native-tls" ] diff --git a/sea-orm-cli/src/main.rs b/sea-orm-cli/src/main.rs index f5c595290..2529b0f12 100644 --- a/sea-orm-cli/src/main.rs +++ b/sea-orm-cli/src/main.rs @@ -24,16 +24,6 @@ async fn main() { async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box> { match matches.subcommand() { ("entity", Some(args)) => { - // The database should be a valid URL that can be parsed - // protocol://username:password@host/database_name - let url = match Url::parse( - args.value_of("DATABASE_URL") - .expect("No database url could be found"), - ) { - Ok(url) => url, - Err(e) => panic!("Invalid database url: {}", e), - }; - let output_dir = args.value_of("OUTPUT_DIR").unwrap(); let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); let tables = args @@ -49,6 +39,33 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box) -> Result<(), Box match e.downcast::() { + Ok(_) => (), + Err(e) => panic!("Expected ParseError but got: {:?}", e), + }, + _ => panic!("Should have panicked"), + } + } + + #[test] + #[should_panic] + fn test_generate_entity_no_database_section() { + let matches = cli::build_cli() + .setting(AppSettings::NoBinaryName) + .get_matches_from(vec![ + "generate", + "entity", + "--database-url", + "postgresql://root:root@localhost:3306", + ]); + + smol::block_on(run_generate_command(matches.subcommand().1.unwrap())) + .unwrap_or_else(handle_error); + } + + #[test] + #[should_panic] + fn test_generate_entity_no_database_path() { + let matches = cli::build_cli() + .setting(AppSettings::NoBinaryName) + .get_matches_from(vec![ + "generate", + "entity", + "--database-url", + "mysql://root:root@localhost:3306/", + ]); + + smol::block_on(run_generate_command(matches.subcommand().1.unwrap())) + .unwrap_or_else(handle_error); + } + + #[test] + #[should_panic] + fn test_generate_entity_no_username() { + let matches = cli::build_cli() + .setting(AppSettings::NoBinaryName) + .get_matches_from(vec![ + "generate", + "entity", + "--database-url", + "mysql://:root@localhost:3306/database", + ]); + + smol::block_on(run_generate_command(matches.subcommand().1.unwrap())) + .unwrap_or_else(handle_error); + } + + #[test] + #[should_panic] + fn test_generate_entity_no_password() { + let matches = cli::build_cli() + .setting(AppSettings::NoBinaryName) + .get_matches_from(vec![ + "generate", + "entity", + "--database-url", + "mysql://root:@localhost:3306/database", + ]); + + smol::block_on(run_generate_command(matches.subcommand().1.unwrap())) + .unwrap_or_else(handle_error); + } + + #[async_std::test] + async fn test_generate_entity_no_host() { + let matches = cli::build_cli() + .setting(AppSettings::NoBinaryName) + .get_matches_from(vec![ + "generate", + "entity", + "--database-url", + "postgres://root:root@/database", + ]); + + let result = std::panic::catch_unwind(|| { + smol::block_on(run_generate_command(matches.subcommand().1.unwrap())) + }); + + // Make sure result is a ParseError + match result { + Ok(Err(e)) => match e.downcast::() { + Ok(_) => (), + Err(e) => panic!("Expected ParseError but got: {:?}", e), + }, + _ => panic!("Should have panicked"), + } + } +} From fb1694b09608a5740d323ac1d37bf496c18033fd Mon Sep 17 00:00:00 2001 From: Forest Anderson Date: Tue, 19 Oct 2021 01:27:27 -0400 Subject: [PATCH 04/13] Updated actions with test --- .github/workflows/rust.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 28e73ad62..c9073f5ce 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -145,7 +145,7 @@ jobs: with: command: test args: > - --all + --workspace - uses: actions-rs/cargo@v1 with: @@ -153,6 +153,12 @@ jobs: args: > --manifest-path sea-orm-rocket/Cargo.toml + - uses: actions-rs/cargo@v1 + with: + command: test + args: > + --manifest-path sea-orm-cli/Cargo.toml + cli: name: CLI runs-on: ${{ matrix.os }} From ec9163ab8e28fb89bab5c480b8438aa5bce3dcf9 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 17 Oct 2021 20:18:07 +0800 Subject: [PATCH 05/13] Notes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a57051f9a..cbe7f64e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ assert_eq!( )) ); +// update many remains the same assert_eq!( Update::many(cake::Entity) .col_expr(cake::Column::Name, Expr::value("Cheese Cake".to_owned())) From c0761a949b9e089d4aa9645207e6bbdd790e2902 Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Sun, 17 Oct 2021 20:18:32 +0800 Subject: [PATCH 06/13] cargo fmt --- src/entity/active_model.rs | 5 +---- src/executor/select.rs | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/entity/active_model.rs b/src/entity/active_model.rs index 9451a1143..b17ade72d 100644 --- a/src/entity/active_model.rs +++ b/src/entity/active_model.rs @@ -460,10 +460,7 @@ mod tests { ); assert_eq!( - my_fruit::UpdateFruit { - cake_id: None, - } - .into_active_model(), + my_fruit::UpdateFruit { cake_id: None }.into_active_model(), fruit::ActiveModel { id: Unset(None), name: Unset(None), diff --git a/src/executor/select.rs b/src/executor/select.rs index c2a2e43bd..78786b761 100644 --- a/src/executor/select.rs +++ b/src/executor/select.rs @@ -1,7 +1,7 @@ use crate::{ - error::*, ConnectionTrait, EntityTrait, FromQueryResult, IdenStatic, Iterable, - ModelTrait, Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo, - SelectTwoMany, Statement, TryGetableMany, + error::*, ConnectionTrait, EntityTrait, FromQueryResult, IdenStatic, Iterable, ModelTrait, + Paginator, PrimaryKeyToColumn, QueryResult, Select, SelectA, SelectB, SelectTwo, SelectTwoMany, + Statement, TryGetableMany, }; use futures::{Stream, TryStreamExt}; use sea_query::SelectStatement; From f506ad658166042f656dce44cecdee68aaa3d88c Mon Sep 17 00:00:00 2001 From: Chris Tsang Date: Tue, 19 Oct 2021 18:51:56 +0800 Subject: [PATCH 07/13] Tweak --- sea-orm-cli/Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index aead20df2..6512f61cc 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -33,6 +33,8 @@ sqlx = { version = "^0.5", default-features = false, features = [ "mysql", "post env_logger = { version = "^0.9" } log = { version = "^0.4" } url = "^2.2" + +[dev-dependencies] smol = "1.2.5" [features] From dd15d264bf8f1c1345a93b0257d9cdca868de37b Mon Sep 17 00:00:00 2001 From: Billy Chan <30400950+billy1624@users.noreply.github.com> Date: Wed, 20 Oct 2021 10:45:02 +0800 Subject: [PATCH 08/13] README SEO --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c2e461925..0532c0809 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,7 @@

SeaORM

-

- 🐚 An async & dynamic ORM for Rust -

+

🐚 An async & dynamic ORM for Rust

[![crate](https://img.shields.io/crates/v/sea-orm.svg)](https://crates.io/crates/sea-orm) [![docs](https://docs.rs/sea-orm/badge.svg)](https://docs.rs/sea-orm) @@ -18,7 +16,7 @@ # SeaORM -SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. +#### 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-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/basic) From bd564851805d3a045c607e94131a745cb0edfbfa Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Wed, 20 Oct 2021 12:28:51 +0800 Subject: [PATCH 09/13] lib.rs SEO --- src/lib.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 745692b92..8cb1d981b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,9 +11,7 @@ //! //!

SeaORM

//! -//!

-//! 🐚 An async & dynamic ORM for Rust -//!

+//!

🐚 An async & dynamic ORM for Rust

//! //! [![crate](https://img.shields.io/crates/v/sea-orm.svg)](https://crates.io/crates/sea-orm) //! [![docs](https://docs.rs/sea-orm/badge.svg)](https://docs.rs/sea-orm) @@ -25,7 +23,7 @@ //! //! # SeaORM //! -//! SeaORM is a relational ORM to help you build light weight and concurrent web services in Rust. +//! #### 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-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/basic) From 32f82a0c9b8722048a0439f250b20728526b3fb2 Mon Sep 17 00:00:00 2001 From: Billy Chan Date: Mon, 18 Oct 2021 17:39:30 +0800 Subject: [PATCH 10/13] Test self referencing relation --- tests/common/features/mod.rs | 2 + tests/common/features/schema.rs | 27 ++++++++- tests/common/features/self_join.rs | 77 ++++++++++++++++++++++++++ tests/self_join_tests.rs | 89 ++++++++++++++++++++++++++++++ 4 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/common/features/self_join.rs create mode 100644 tests/self_join_tests.rs diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index ff716f010..1e709edfc 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -2,8 +2,10 @@ pub mod applog; pub mod metadata; pub mod repository; pub mod schema; +pub mod self_join; pub use applog::Entity as Applog; pub use metadata::Entity as Metadata; pub use repository::Entity as Repository; pub use schema::*; +pub use self_join::Entity as SelfJoin; diff --git a/tests/common/features/schema.rs b/tests/common/features/schema.rs index 44b011c5c..7e6f6424b 100644 --- a/tests/common/features/schema.rs +++ b/tests/common/features/schema.rs @@ -3,12 +3,13 @@ pub use super::super::bakery_chain::*; use super::*; use crate::common::setup::create_table; use sea_orm::{error::*, sea_query, DatabaseConnection, DbConn, ExecResult}; -use sea_query::ColumnDef; +use sea_query::{ColumnDef, ForeignKeyCreateStatement}; pub async fn create_tables(db: &DatabaseConnection) -> Result<(), DbErr> { create_log_table(db).await?; create_metadata_table(db).await?; create_repository_table(db).await?; + create_self_join_table(db).await?; Ok(()) } @@ -75,3 +76,27 @@ pub async fn create_repository_table(db: &DbConn) -> Result { create_table(db, &stmt, Repository).await } + +pub async fn create_self_join_table(db: &DbConn) -> Result { + let stmt = sea_query::Table::create() + .table(self_join::Entity) + .col( + ColumnDef::new(self_join::Column::Uuid) + .uuid() + .not_null() + .primary_key(), + ) + .col(ColumnDef::new(self_join::Column::UuidRef).uuid()) + .col(ColumnDef::new(self_join::Column::Time).time()) + .foreign_key( + ForeignKeyCreateStatement::new() + .name("fk-self_join-self_join") + .from_tbl(SelfJoin) + .from_col(self_join::Column::UuidRef) + .to_tbl(SelfJoin) + .to_col(self_join::Column::Uuid), + ) + .to_owned(); + + create_table(db, &stmt, SelfJoin).await +} diff --git a/tests/common/features/self_join.rs b/tests/common/features/self_join.rs new file mode 100644 index 000000000..de491a09a --- /dev/null +++ b/tests/common/features/self_join.rs @@ -0,0 +1,77 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "self_join")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub uuid: Uuid, + pub uuid_ref: Option, + pub time: Option