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