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 }} 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())) 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/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) diff --git a/issues/249/Cargo.toml b/issues/249/Cargo.toml index 623ee6bb5..cadab33f1 100644 --- a/issues/249/Cargo.toml +++ b/issues/249/Cargo.toml @@ -1,11 +1,2 @@ [workspace] -# A separate workspace - -[package] -name = "sea-orm-issues-249" -version = "0.1.0" -edition = "2018" -publish = false - -[dependencies] -sea-orm = { path = "../../", default-features = false, features = ["mock"] } \ No newline at end of file +members = ["core", "app"] diff --git a/issues/249/README.md b/issues/249/README.md new file mode 100644 index 000000000..f6d18ee14 --- /dev/null +++ b/issues/249/README.md @@ -0,0 +1 @@ +# Demo of a pure logic crate depending on SeaORM with no enabled features \ No newline at end of file diff --git a/issues/249/app/Cargo.toml b/issues/249/app/Cargo.toml new file mode 100644 index 000000000..258b37c91 --- /dev/null +++ b/issues/249/app/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sea-orm-issues-249-app" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +core = { path = "../core" } +sea-orm = { path = "../../../", default-features = false, features = ["macros", "sqlx-sqlite", "runtime-async-std-native-tls"] } diff --git a/issues/249/app/src/main.rs b/issues/249/app/src/main.rs new file mode 100644 index 000000000..98fbb89db --- /dev/null +++ b/issues/249/app/src/main.rs @@ -0,0 +1,14 @@ +use core::clone_a_model; + +use sea_orm::tests_cfg::cake; + +fn main() { + let c1 = cake::Model { + id: 1, + name: "Cheese".to_owned(), + }; + + let c2 = clone_a_model(&c1); + + println!("{:?}", c2); +} \ No newline at end of file diff --git a/issues/249/core/Cargo.toml b/issues/249/core/Cargo.toml new file mode 100644 index 000000000..c9b1621d7 --- /dev/null +++ b/issues/249/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "core" +version = "0.1.0" +edition = "2018" +publish = false + +[dependencies] +sea-orm = { path = "../../../", default-features = false } + +[dev-dependencies] +sea-orm = { path = "../../../", features = ["mock"] } \ No newline at end of file diff --git a/issues/249/core/src/lib.rs b/issues/249/core/src/lib.rs new file mode 100644 index 000000000..2828977ee --- /dev/null +++ b/issues/249/core/src/lib.rs @@ -0,0 +1,17 @@ +pub use sea_orm::entity::*; + +pub fn clone_a_model(model: &M) -> M +where + M: ModelTrait { + model.clone() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + println!("OK"); + } +} \ No newline at end of file diff --git a/issues/249/src/lib.rs b/issues/249/src/lib.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/sea-orm-cli/Cargo.toml b/sea-orm-cli/Cargo.toml index 02fddcb16..6512f61cc 100644 --- a/sea-orm-cli/Cargo.toml +++ b/sea-orm-cli/Cargo.toml @@ -32,6 +32,10 @@ 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" + +[dev-dependencies] +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 528ce5f54..2529b0f12 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,6 @@ 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(); let output_dir = args.value_of("OUTPUT_DIR").unwrap(); let include_hidden_tables = args.is_present("INCLUDE_HIDDEN_TABLES"); let tables = args @@ -32,8 +32,67 @@ 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 should be a valid URL that can be parsed + // protocol://username:password@host/database_name + let url = Url::parse( + args.value_of("DATABASE_URL") + .expect("No database url could be found"), + )?; + + // Make sure we have all the required url components + // + // Missing scheme will have been caught by the Url::parse() call + // above + + let url_username = url.username(); + let url_password = url.password(); + let url_host = url.host_str(); + + // Panic on any that are missing + if url_username.is_empty() { + panic!("No username was found in the database url"); + } + if url_password.is_none() { + panic!("No password was found in the database url"); + } + if url_host.is_none() { + panic!("No host was found in the database url"); + } + + // 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() + .unwrap_or_else(|| { + panic!( + "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.len() > 0 { + if !tables.is_empty() { return tables.contains(&table); } @@ -43,49 +102,43 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> Result<(), Box = 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; - - 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 table_stmts = match url.scheme() { + "mysql" => { + use sea_schema::mysql::discovery::SchemaDiscovery; + use sqlx::MySqlPool; + + let connection = MySqlPool::connect(url.as_str()).await?; + let schema_discovery = SchemaDiscovery::new(connection, database_name); + 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.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 +152,8 @@ async fn run_generate_command(matches: &ArgMatches<'_>) -> 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"), + } + } +} diff --git a/src/database/db_connection.rs b/src/database/db_connection.rs index e8f375467..6b8e33563 100644 --- a/src/database/db_connection.rs +++ b/src/database/db_connection.rs @@ -4,6 +4,7 @@ use crate::{ }; use sea_query::{MysqlQueryBuilder, PostgresQueryBuilder, QueryBuilder, SqliteQueryBuilder}; use std::{future::Future, pin::Pin}; +use url::Url; #[cfg(feature = "sqlx-dep")] use sqlx::pool::PoolConnection; @@ -223,12 +224,13 @@ impl DatabaseConnection { impl DbBackend { pub fn is_prefix_of(self, base_url: &str) -> 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", } } 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; diff --git a/src/lib.rs b/src/lib.rs index d40db473a..836544993 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) diff --git a/tests/common/features/mod.rs b/tests/common/features/mod.rs index f0db35b7e..4c0e1418a 100644 --- a/tests/common/features/mod.rs +++ b/tests/common/features/mod.rs @@ -3,9 +3,11 @@ pub mod applog; pub mod metadata; pub mod repository; pub mod schema; +pub mod self_join; pub use active_enum::Entity as ActiveEnum; 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 07a2953b4..823ccdfd9 100644 --- a/tests/common/features/schema.rs +++ b/tests/common/features/schema.rs @@ -2,13 +2,14 @@ pub use super::super::bakery_chain::*; use super::*; use crate::common::setup::create_table; -use sea_orm::{ConnectionTrait, DatabaseConnection, DbConn, ExecResult, Statement, error::*, sea_query}; -use sea_query::{Alias, ColumnDef}; +use sea_orm::{error::*, sea_query, DatabaseConnection, DbConn, ExecResult}; +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?; create_active_enum_table(db).await?; Ok(()) @@ -77,6 +78,30 @@ 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 +} + pub async fn create_active_enum_table(db: &DbConn) -> Result { let stmt = sea_query::Table::create() .table(active_enum::Entity) 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