diff --git a/src/entity/prelude.rs b/src/entity/prelude.rs index 2e706df536..4827200855 100644 --- a/src/entity/prelude.rs +++ b/src/entity/prelude.rs @@ -3,7 +3,7 @@ pub use crate::{ ColumnTypeTrait, ConnectionTrait, CursorTrait, DatabaseConnection, DbConn, EntityName, EntityTrait, EnumIter, ForeignKeyAction, Iden, IdenStatic, Linked, LoaderTrait, ModelTrait, PaginatorTrait, PrimaryKeyArity, PrimaryKeyToColumn, PrimaryKeyTrait, QueryFilter, QueryResult, - Related, RelatedSelfVia, RelationDef, RelationTrait, Select, Value, + Related, RelatedSelfVia, RelationDef, RelationTrait, Select, SelectExt, Value, error::*, sea_query::{DynIden, Expr, RcOrArc, SeaRc, StringLen}, }; diff --git a/src/executor/mod.rs b/src/executor/mod.rs index a9b2c634aa..f27ba0f0f0 100644 --- a/src/executor/mod.rs +++ b/src/executor/mod.rs @@ -7,6 +7,7 @@ mod paginator; mod query; mod returning; mod select; +mod select_ext; mod update; use consolidate::*; @@ -18,4 +19,5 @@ pub use paginator::*; pub use query::*; use returning::*; pub use select::*; +pub use select_ext::*; pub use update::*; diff --git a/src/executor/paginator.rs b/src/executor/paginator.rs index 9fa85deaec..0e64feb8da 100644 --- a/src/executor/paginator.rs +++ b/src/executor/paginator.rs @@ -275,31 +275,6 @@ where { self.paginate(db, 1).num_items().await } - - /// Check if any records exist - async fn exists(self, db: &'db C) -> Result - where - Self: Send + Sized, - { - let paginator = self.paginate(db, 1); - let stmt = SelectStatement::new() - .expr(Expr::cust("1")) - .from_subquery( - paginator - .query - .clone() - .reset_limit() - .reset_offset() - .clear_order_by() - .limit(1) - .to_owned(), - "sub_query", - ) - .limit(1) - .to_owned(); - let result = db.query_one(&stmt).await?; - Ok(result.is_some()) - } } impl<'db, C, S> PaginatorTrait<'db, C> for Selector @@ -400,6 +375,7 @@ mod tests { #[cfg(feature = "sync")] use crate::util::StreamShim; use crate::{DatabaseConnection, DbBackend, MockDatabase, Transaction}; + use crate::{QueryOrder, QuerySelect}; use crate::{Statement, tests_cfg::*}; use futures_util::{TryStreamExt, stream::TryNext}; use pretty_assertions::assert_eq; diff --git a/src/executor/select_ext.rs b/src/executor/select_ext.rs new file mode 100644 index 0000000000..4ff81d310a --- /dev/null +++ b/src/executor/select_ext.rs @@ -0,0 +1,448 @@ +use crate::{ + ConnectionTrait, DbErr, EntityTrait, Select, SelectFive, SelectFour, SelectSix, SelectThree, + SelectTwo, Selector, SelectorRaw, SelectorTrait, Topology, +}; +use sea_query::{Expr, SelectStatement}; + +// TODO: Move count here +#[async_trait::async_trait] +/// Helper trait for selectors with convenient methods +pub trait SelectExt { + /// This method is unstable and is only used for internal testing. + /// It may be removed in the future. + #[doc(hidden)] + fn exists_query(self) -> SelectStatement; + /// Check if any records exist + async fn exists(self, db: &C) -> Result + where + C: ConnectionTrait, + Self: Send + Sized, + { + let stmt = self.exists_query(); + Ok(db.query_one(&stmt).await?.is_some()) + } +} + +fn into_exists_query(mut stmt: SelectStatement) -> SelectStatement { + stmt.clear_selects(); + // Expr::Custom has fewer branches, but this may not have any significant impact on performance. + stmt.expr(Expr::cust("1")); + stmt.reset_limit(); + stmt.reset_offset(); + stmt.clear_order_by(); + stmt +} + +impl SelectExt for Selector +where + S: SelectorTrait, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +#[async_trait::async_trait] +impl SelectExt for SelectorRaw +where + S: SelectorTrait, +{ + fn exists_query(self) -> SelectStatement { + let stmt = self.stmt; + let sub_query_sql = stmt.sql.trim().trim_end_matches(';').trim(); + let exists_sql = format!("1 FROM ({sub_query_sql}) AS sub_query LIMIT 1"); + + let mut query = SelectStatement::new(); + query.expr(if let Some(values) = stmt.values { + Expr::cust_with_values(exists_sql, values.0) + } else { + Expr::cust(exists_sql) + }); + query + } +} + +impl SelectExt for Select +where + E: EntityTrait, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +impl SelectExt for SelectTwo +where + E: EntityTrait, + F: EntityTrait, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +impl SelectExt for SelectThree +where + E: EntityTrait, + F: EntityTrait, + G: EntityTrait, + TOP: Topology, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +impl SelectExt for SelectFour +where + E: EntityTrait, + F: EntityTrait, + G: EntityTrait, + H: EntityTrait, + TOP: Topology, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +impl SelectExt for SelectFive +where + E: EntityTrait, + F: EntityTrait, + G: EntityTrait, + H: EntityTrait, + I: EntityTrait, + TOP: Topology, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +impl SelectExt for SelectSix +where + E: EntityTrait, + F: EntityTrait, + G: EntityTrait, + H: EntityTrait, + I: EntityTrait, + J: EntityTrait, + TOP: Topology, +{ + fn exists_query(self) -> SelectStatement { + into_exists_query(self.query) + } +} + +#[cfg(test)] +mod tests { + use super::SelectExt; + use crate::entity::prelude::*; + use crate::{DbBackend, QueryOrder, QuerySelect, Statement, tests_cfg::*}; + + #[test] + fn exists_query_select_basic() { + let stmt = fruit::Entity::find().exists_query(); + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!(sql, r#"SELECT 1 FROM "fruit""#); + } + + #[test] + fn exists_query_select_strips_limit_offset_order() { + let stmt = fruit::Entity::find() + .filter(fruit::Column::Id.gt(1)) + .order_by_asc(fruit::Column::Id) + .limit(2) + .offset(4) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!(sql, r#"SELECT 1 FROM "fruit" WHERE "fruit"."id" > 1"#); + } + + #[test] + fn exists_query_selector_basic() { + let stmt = fruit::Entity::find() + .into_model::() + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!(sql, r#"SELECT 1 FROM "fruit""#); + } + + #[test] + fn exists_query_selector_complex() { + let stmt = fruit::Entity::find() + .filter(fruit::Column::Id.gt(1)) + .order_by_desc(fruit::Column::Id) + .limit(2) + .offset(4) + .into_model::() + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!(sql, r#"SELECT 1 FROM "fruit" WHERE "fruit"."id" > 1"#); + } + + #[test] + fn exists_query_selector_raw_simple() { + let raw_stmt = + Statement::from_string(DbBackend::Postgres, r#"SELECT "fruit"."id" FROM "fruit""#); + let stmt = fruit::Entity::find().from_raw_sql(raw_stmt).exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + r#"SELECT 1 FROM (SELECT "fruit"."id" FROM "fruit") AS sub_query LIMIT 1"# + ); + } + + #[test] + fn exists_query_selector_raw_complex() { + let raw_stmt = Statement::from_string( + DbBackend::Postgres, + r#"SELECT "fruit"."id" FROM "fruit" WHERE "fruit"."id" > 1 ORDER BY "fruit"."id" DESC LIMIT 5 OFFSET 2"#, + ); + let stmt = fruit::Entity::find().from_raw_sql(raw_stmt).exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + r#"SELECT 1 FROM (SELECT "fruit"."id" FROM "fruit" WHERE "fruit"."id" > 1 ORDER BY "fruit"."id" DESC LIMIT 5 OFFSET 2) AS sub_query LIMIT 1"# + ); + } + + #[test] + fn exists_query_select_two_simple() { + let stmt = cake::Entity::find() + .find_also_related(fruit::Entity) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + r#"SELECT 1 FROM "cake" LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""# + ); + } + + #[test] + fn exists_query_select_two_complex() { + let stmt = cake::Entity::find() + .find_also_related(fruit::Entity) + .filter(cake::Column::Id.gt(1)) + .order_by_desc(cake::Column::Id) + .limit(2) + .offset(4) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake""#, + r#"LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + r#"WHERE "cake"."id" > 1"#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_three_simple() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_three_complex() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .filter(cake_filling::Column::CakeId.gt(1)) + .order_by_desc(cake_filling::Column::CakeId) + .limit(2) + .offset(4) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"WHERE "cake_filling"."cake_id" > 1"#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_four_simple() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .find_also(filling::Entity, ingredient::Entity) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"LEFT JOIN "ingredient" ON "filling"."id" = "ingredient"."filling_id""#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_four_complex() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .find_also(filling::Entity, ingredient::Entity) + .filter(cake_filling::Column::CakeId.gt(1)) + .order_by_desc(cake_filling::Column::CakeId) + .limit(2) + .offset(4) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"LEFT JOIN "ingredient" ON "filling"."id" = "ingredient"."filling_id""#, + r#"WHERE "cake_filling"."cake_id" > 1"#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_five_simple() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .find_also(filling::Entity, ingredient::Entity) + .find_also(cake_filling::Entity, cake_filling_price::Entity) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"LEFT JOIN "ingredient" ON "filling"."id" = "ingredient"."filling_id""#, + r#"LEFT JOIN "public"."cake_filling_price" ON "cake_filling"."cake_id" = "cake_filling_price"."cake_id" AND "cake_filling"."filling_id" = "cake_filling_price"."filling_id""#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_five_complex() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .find_also(filling::Entity, ingredient::Entity) + .find_also(cake_filling::Entity, cake_filling_price::Entity) + .filter(cake_filling::Column::CakeId.gt(1)) + .order_by_desc(cake_filling::Column::CakeId) + .limit(2) + .offset(4) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"LEFT JOIN "ingredient" ON "filling"."id" = "ingredient"."filling_id""#, + r#"LEFT JOIN "public"."cake_filling_price" ON "cake_filling"."cake_id" = "cake_filling_price"."cake_id" AND "cake_filling"."filling_id" = "cake_filling_price"."filling_id""#, + r#"WHERE "cake_filling"."cake_id" > 1"#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_six_simple() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .find_also(filling::Entity, ingredient::Entity) + .find_also(cake_filling::Entity, cake_filling_price::Entity) + .find_also(filling::Entity, cake_compact::Entity) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"LEFT JOIN "ingredient" ON "filling"."id" = "ingredient"."filling_id""#, + r#"LEFT JOIN "public"."cake_filling_price" ON "cake_filling"."cake_id" = "cake_filling_price"."cake_id" AND "cake_filling"."filling_id" = "cake_filling_price"."filling_id""#, + r#"LEFT JOIN "cake_filling" ON "filling"."id" = "cake_filling"."filling_id""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + ] + .join(" ") + ); + } + + #[test] + fn exists_query_select_six_complex() { + let stmt = cake_filling::Entity::find() + .find_also_related(cake::Entity) + .find_also(cake_filling::Entity, filling::Entity) + .find_also(filling::Entity, ingredient::Entity) + .find_also(cake_filling::Entity, cake_filling_price::Entity) + .find_also(filling::Entity, cake_compact::Entity) + .filter(cake_filling::Column::CakeId.gt(1)) + .order_by_desc(cake_filling::Column::CakeId) + .limit(2) + .offset(4) + .exists_query(); + + let sql = DbBackend::Postgres.build(&stmt).to_string(); + assert_eq!( + sql, + [ + r#"SELECT 1 FROM "cake_filling""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"LEFT JOIN "filling" ON "cake_filling"."filling_id" = "filling"."id""#, + r#"LEFT JOIN "ingredient" ON "filling"."id" = "ingredient"."filling_id""#, + r#"LEFT JOIN "public"."cake_filling_price" ON "cake_filling"."cake_id" = "cake_filling_price"."cake_id" AND "cake_filling"."filling_id" = "cake_filling_price"."filling_id""#, + r#"LEFT JOIN "cake_filling" ON "filling"."id" = "cake_filling"."filling_id""#, + r#"LEFT JOIN "cake" ON "cake_filling"."cake_id" = "cake"."id""#, + r#"WHERE "cake_filling"."cake_id" > 1"#, + ] + .join(" ") + ); + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs index c97a2e0f28..5d5a1dc428 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -26,7 +26,7 @@ pub use update::*; pub(crate) use util::*; pub use crate::{ - ConnectionTrait, CursorTrait, InsertResult, PaginatorTrait, Statement, StreamTrait, + ConnectionTrait, CursorTrait, InsertResult, PaginatorTrait, SelectExt, Statement, StreamTrait, TransactionTrait, UpdateResult, Value, Values, }; pub use sea_query::ExprTrait; diff --git a/tests/exists_tests.rs b/tests/exists_tests.rs index d60443ee3e..cb277316b6 100644 --- a/tests/exists_tests.rs +++ b/tests/exists_tests.rs @@ -4,7 +4,7 @@ pub mod common; pub use common::{TestContext, bakery_chain::*, setup::*}; pub use sea_orm::entity::*; -pub use sea_orm::{ConnectionTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; +pub use sea_orm::{ConnectionTrait, QueryFilter, QueryOrder, QuerySelect, SelectExt}; #[sea_orm_macros::test] pub async fn exists_with_no_result() {