diff --git a/src/executor/cursor.rs b/src/executor/cursor.rs index c9101de6c..18031b962 100644 --- a/src/executor/cursor.rs +++ b/src/executor/cursor.rs @@ -1,12 +1,14 @@ use crate::{ - ConnectionTrait, DbErr, EntityTrait, FromQueryResult, Identity, IntoIdentity, - PartialModelTrait, QueryOrder, QuerySelect, Select, SelectModel, SelectorTrait, + ConnectionTrait, DbErr, EntityTrait, FromQueryResult, Identity, IdentityOf, IntoIdentity, + PartialModelTrait, PrimaryKeyToColumn, QueryOrder, QuerySelect, Select, SelectModel, SelectTwo, + SelectTwoModel, SelectorTrait, }; use sea_query::{ Condition, DynIden, Expr, IntoValueTuple, Order, SeaRc, SelectStatement, SimpleExpr, Value, ValueTuple, }; use std::marker::PhantomData; +use strum::IntoEnumIterator as Iterable; #[cfg(feature = "with-json")] use crate::JsonValue; @@ -17,11 +19,12 @@ pub struct Cursor where S: SelectorTrait, { - pub(crate) query: SelectStatement, - pub(crate) table: DynIden, - pub(crate) order_columns: Identity, - pub(crate) last: bool, - pub(crate) phantom: PhantomData, + query: SelectStatement, + table: DynIden, + order_columns: Identity, + secondary_order_by: Vec<(DynIden, Identity)>, + last: bool, + phantom: PhantomData, } impl Cursor @@ -29,7 +32,12 @@ where S: SelectorTrait, { /// Initialize a cursor - pub fn new(query: SelectStatement, table: DynIden, order_columns: C) -> Self + pub fn new( + query: SelectStatement, + table: DynIden, + order_columns: C, + secondary_order_by: Vec<(DynIden, Identity)>, + ) -> Self where C: IntoIdentity, { @@ -39,6 +47,7 @@ where order_columns: order_columns.into_identity(), last: false, phantom: PhantomData, + secondary_order_by, } } @@ -158,10 +167,7 @@ where /// Limit result set to only first N rows in ascending order of the order by column pub fn first(&mut self, num_rows: u64) -> &mut Self { self.query.limit(num_rows).clear_order_by(); - let table = SeaRc::clone(&self.table); - self.apply_order_by(|query, col| { - query.order_by((SeaRc::clone(&table), SeaRc::clone(col)), Order::Asc); - }); + self.apply_order_by(self.table.clone(), Order::Asc); self.last = false; self } @@ -169,38 +175,41 @@ where /// Limit result set to only last N rows in ascending order of the order by column pub fn last(&mut self, num_rows: u64) -> &mut Self { self.query.limit(num_rows).clear_order_by(); - let table = SeaRc::clone(&self.table); - self.apply_order_by(|query, col| { - query.order_by((SeaRc::clone(&table), SeaRc::clone(col)), Order::Desc); - }); + self.apply_order_by(self.table.clone(), Order::Desc); self.last = true; self } - fn apply_order_by(&mut self, f: F) - where - F: Fn(&mut SelectStatement, &DynIden), - { + fn apply_order_by(&mut self, table: DynIden, ord: Order) { let query = &mut self.query; + let order = |query: &mut SelectStatement, col| { + query.order_by((SeaRc::clone(&table), SeaRc::clone(col)), ord.clone()); + }; match &self.order_columns { Identity::Unary(c1) => { - f(query, c1); + order(query, c1); } Identity::Binary(c1, c2) => { - f(query, c1); - f(query, c2); + order(query, c1); + order(query, c2); } Identity::Ternary(c1, c2, c3) => { - f(query, c1); - f(query, c2); - f(query, c3); + order(query, c1); + order(query, c2); + order(query, c3); } Identity::Many(vec) => { for col in vec.iter() { - f(query, col); + order(query, col); } } } + + for (tbl, col) in self.secondary_order_by.clone() { + if let Identity::Unary(c1) = col { + query.order_by((tbl, c1), ord.clone()); + }; + } } /// Fetch the paginated result @@ -231,6 +240,7 @@ where order_columns: self.order_columns, last: self.last, phantom: PhantomData, + secondary_order_by: self.secondary_order_by, } } @@ -251,8 +261,15 @@ where order_columns: self.order_columns, last: self.last, phantom: PhantomData, + secondary_order_by: self.secondary_order_by, } } + + /// Set the cursor ordering for another table when dealing with SelectTwo + pub fn set_secondary_order_by(&mut self, tbl_col: Vec<(DynIden, Identity)>) -> &mut Self { + self.secondary_order_by = tbl_col; + self + } } impl QuerySelect for Cursor @@ -281,11 +298,6 @@ where pub trait CursorTrait { /// Select operation type Selector: SelectorTrait + Send + Sync; - - /// Convert current type into a cursor - fn cursor_by(self, order_columns: C) -> Cursor - where - C: IntoIdentity; } impl CursorTrait for Select @@ -294,12 +306,79 @@ where M: FromQueryResult + Sized + Send + Sync, { type Selector = SelectModel; +} - fn cursor_by(self, order_columns: C) -> Cursor +impl Select +where + E: EntityTrait, + M: FromQueryResult + Sized + Send + Sync, +{ + /// Convert into a cursor + pub fn cursor_by(self, order_columns: C) -> Cursor> where C: IntoIdentity, { - Cursor::new(self.query, SeaRc::new(E::default()), order_columns) + Cursor::new(self.query, SeaRc::new(E::default()), order_columns, vec![]) + } +} + +impl CursorTrait for SelectTwo +where + E: EntityTrait, + F: EntityTrait, + M: FromQueryResult + Sized + Send + Sync, + N: FromQueryResult + Sized + Send + Sync, +{ + type Selector = SelectTwoModel; +} + +impl SelectTwo +where + E: EntityTrait, + F: EntityTrait, + M: FromQueryResult + Sized + Send + Sync, + N: FromQueryResult + Sized + Send + Sync, +{ + /// Convert into a cursor using column of first entity + pub fn cursor_by(self, order_columns: C) -> Cursor> + where + C: IdentityOf, + { + let primary_keys: Vec<(DynIden, Identity)> = ::iter() + .map(|pk| { + ( + SeaRc::new(F::default()), + Identity::Unary(SeaRc::new(pk.into_column())), + ) + }) + .collect(); + Cursor::new( + self.query, + SeaRc::new(E::default()), + order_columns.identity_of(), + primary_keys, + ) + } + + /// Convert into a cursor using column of second entity + pub fn cursor_by_other(self, order_columns: C) -> Cursor> + where + C: IdentityOf, + { + let primary_keys: Vec<(DynIden, Identity)> = ::iter() + .map(|pk| { + ( + SeaRc::new(E::default()), + Identity::Unary(SeaRc::new(pk.into_column())), + ) + }) + .collect(); + Cursor::new( + self.query, + SeaRc::new(F::default()), + order_columns.identity_of(), + primary_keys, + ) } } @@ -363,6 +442,231 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn first_2_before_10_also_related_select() -> Result<(), DbErr> { + let models = [ + ( + cake::Model { + id: 1, + name: "Blueberry Cheese Cake".into(), + }, + Some(fruit::Model { + id: 9, + name: "Blueberry".into(), + cake_id: Some(1), + }), + ), + ( + cake::Model { + id: 2, + name: "Rasberry Cheese Cake".into(), + }, + Some(fruit::Model { + id: 10, + name: "Rasberry".into(), + cake_id: Some(1), + }), + ), + ]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + assert_eq!( + cake::Entity::find() + .find_also_related(Fruit) + .cursor_by(cake::Column::Id) + .before(10) + .first(2) + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id""#, + r#"FROM "cake""#, + r#"LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + r#"WHERE "cake"."id" < $1"#, + r#"ORDER BY "cake"."id" ASC, "fruit"."id" ASC LIMIT $2"#, + ] + .join(" ") + .as_str(), + [10_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + async fn first_2_before_10_also_related_select_cursor_other() -> Result<(), DbErr> { + let models = [( + cake::Model { + id: 1, + name: "Blueberry Cheese Cake".into(), + }, + Some(fruit::Model { + id: 9, + name: "Blueberry".into(), + cake_id: Some(1), + }), + )]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + assert_eq!( + cake::Entity::find() + .find_also_related(Fruit) + .cursor_by_other(fruit::Column::Id) + .before(10) + .first(2) + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""fruit"."id" AS "B_id", "fruit"."name" AS "B_name", "fruit"."cake_id" AS "B_cake_id""#, + r#"FROM "cake""#, + r#"LEFT JOIN "fruit" ON "cake"."id" = "fruit"."cake_id""#, + r#"WHERE "fruit"."id" < $1"#, + r#"ORDER BY "fruit"."id" ASC, "cake"."id" ASC LIMIT $2"#, + ] + .join(" ") + .as_str(), + [10_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + async fn first_2_before_10_also_linked_select() -> Result<(), DbErr> { + let models = [ + ( + cake::Model { + id: 1, + name: "Blueberry Cheese Cake".into(), + }, + Some(vendor::Model { + id: 9, + name: "Blueberry".into(), + }), + ), + ( + cake::Model { + id: 2, + name: "Rasberry Cheese Cake".into(), + }, + Some(vendor::Model { + id: 10, + name: "Rasberry".into(), + }), + ), + ]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + assert_eq!( + cake::Entity::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .cursor_by(cake::Column::Id) + .before(10) + .first(2) + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""r2"."id" AS "B_id", "r2"."name" AS "B_name""#, + r#"FROM "cake""#, + r#"LEFT JOIN "cake_filling" AS "r0" ON "cake"."id" = "r0"."cake_id""#, + r#"LEFT JOIN "filling" AS "r1" ON "r0"."filling_id" = "r1"."id""#, + r#"LEFT JOIN "vendor" AS "r2" ON "r1"."vendor_id" = "r2"."id""#, + r#"WHERE "cake"."id" < $1 ORDER BY "cake"."id" ASC, "vendor"."id" ASC LIMIT $2"#, + ] + .join(" ") + .as_str(), + [10_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + async fn first_2_before_10_also_linked_select_cursor_other() -> Result<(), DbErr> { + let models = [( + cake::Model { + id: 1, + name: "Blueberry Cheese Cake".into(), + }, + Some(vendor::Model { + id: 9, + name: "Blueberry".into(), + }), + )]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + assert_eq!( + cake::Entity::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .cursor_by_other(vendor::Column::Id) + .before(10) + .first(2) + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "cake"."id" AS "A_id", "cake"."name" AS "A_name","#, + r#""r2"."id" AS "B_id", "r2"."name" AS "B_name""#, + r#"FROM "cake""#, + r#"LEFT JOIN "cake_filling" AS "r0" ON "cake"."id" = "r0"."cake_id""#, + r#"LEFT JOIN "filling" AS "r1" ON "r0"."filling_id" = "r1"."id""#, + r#"LEFT JOIN "vendor" AS "r2" ON "r1"."vendor_id" = "r2"."id""#, + r#"WHERE "vendor"."id" < $1 ORDER BY "vendor"."id" ASC, "cake"."id" ASC LIMIT $2"#, + ] + .join(" ") + .as_str(), + [10_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + #[smol_potat::test] async fn last_2_after_10() -> Result<(), DbErr> { use fruit::*; @@ -895,4 +1199,216 @@ mod tests { Ok(()) } + + mod test_base_entity { + use super::test_related_entity; + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "base")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(primary_key)] + pub name: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation { + #[sea_orm(has_many = "super::test_related_entity::Entity")] + TestRelatedEntity, + } + + impl Related for Entity { + fn to() -> RelationDef { + Relation::TestRelatedEntity.def() + } + } + + impl ActiveModelBehavior for ActiveModel {} + } + + mod test_related_entity { + use super::test_base_entity; + use crate as sea_orm; + use crate::entity::prelude::*; + + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "related")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + #[sea_orm(primary_key)] + pub name: String, + pub test_id: i32, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation { + #[sea_orm( + belongs_to = "test_base_entity::Entity", + from = "Column::TestId", + to = "super::test_base_entity::Column::Id" + )] + TestBaseEntity, + } + + impl Related for Entity { + fn to() -> RelationDef { + Relation::TestBaseEntity.def() + } + } + + impl ActiveModelBehavior for ActiveModel {} + } + + #[smol_potat::test] + async fn related_composite_keys_1() -> Result<(), DbErr> { + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[( + test_base_entity::Model { + id: 1, + name: "CAT".into(), + }, + test_related_entity::Model { + id: 1, + name: "CATE".into(), + test_id: 1, + }, + )]]) + .into_connection(); + + assert!(!test_base_entity::Entity::find() + .find_also_related(test_related_entity::Entity) + .cursor_by((test_base_entity::Column::Id, test_base_entity::Column::Name)) + .first(1) + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "base"."id" AS "A_id", "base"."name" AS "A_name","#, + r#""related"."id" AS "B_id", "related"."name" AS "B_name", "related"."test_id" AS "B_test_id""#, + r#"FROM "base""#, + r#"LEFT JOIN "related" ON "base"."id" = "related"."test_id""#, + r#"ORDER BY "base"."id" ASC, "base"."name" ASC, "related"."id" ASC, "related"."name" ASC LIMIT $1"#, + ] + .join(" ") + .as_str(), + [1_u64.into()] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + async fn related_composite_keys_2() -> Result<(), DbErr> { + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[( + test_base_entity::Model { + id: 1, + name: "CAT".into(), + }, + test_related_entity::Model { + id: 1, + name: "CATE".into(), + test_id: 1, + }, + )]]) + .into_connection(); + + assert!(!test_base_entity::Entity::find() + .find_also_related(test_related_entity::Entity) + .cursor_by((test_base_entity::Column::Id, test_base_entity::Column::Name)) + .after((1, "C".to_string())) + .first(2) + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "base"."id" AS "A_id", "base"."name" AS "A_name","#, + r#""related"."id" AS "B_id", "related"."name" AS "B_name", "related"."test_id" AS "B_test_id""#, + r#"FROM "base""#, + r#"LEFT JOIN "related" ON "base"."id" = "related"."test_id""#, + r#"WHERE ("base"."id" = $1 AND "base"."name" > $2) OR "base"."id" > $3"#, + r#"ORDER BY "base"."id" ASC, "base"."name" ASC, "related"."id" ASC, "related"."name" ASC LIMIT $4"#, + ] + .join(" ") + .as_str(), + [ + 1_i32.into(), + "C".into(), + 1_i32.into(), + 2_u64.into(), + ] + ),])] + ); + + Ok(()) + } + + #[smol_potat::test] + async fn related_composite_keys_3() -> Result<(), DbErr> { + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[( + test_base_entity::Model { + id: 1, + name: "CAT".into(), + }, + test_related_entity::Model { + id: 1, + name: "CATE".into(), + test_id: 1, + }, + )]]) + .into_connection(); + + assert!(!test_base_entity::Entity::find() + .find_also_related(test_related_entity::Entity) + .cursor_by_other(( + test_related_entity::Column::Id, + test_related_entity::Column::Name + )) + .after((1, "CAT".to_string())) + .first(2) + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "base"."id" AS "A_id", "base"."name" AS "A_name","#, + r#""related"."id" AS "B_id", "related"."name" AS "B_name", "related"."test_id" AS "B_test_id""#, + r#"FROM "base""#, + r#"LEFT JOIN "related" ON "base"."id" = "related"."test_id""#, + r#"WHERE ("related"."id" = $1 AND "related"."name" > $2) OR "related"."id" > $3"#, + r#"ORDER BY "related"."id" ASC, "related"."name" ASC, "base"."id" ASC, "base"."name" ASC LIMIT $4"#, + ] + .join(" ") + .as_str(), + [ + 1_i32.into(), + "CAT".into(), + 1_i32.into(), + 2_u64.into(), + ] + ),])] + ); + + Ok(()) + } } diff --git a/tests/cursor_tests.rs b/tests/cursor_tests.rs index 4d9ab91e8..ef8280573 100644 --- a/tests/cursor_tests.rs +++ b/tests/cursor_tests.rs @@ -2,7 +2,9 @@ pub mod common; pub use common::{features::*, setup::*, TestContext}; use pretty_assertions::assert_eq; -use sea_orm::{entity::prelude::*, DerivePartialModel, FromQueryResult}; +use sea_orm::{ + entity::prelude::*, DerivePartialModel, FromQueryResult, QueryOrder, QuerySelect, Set, +}; use serde_json::json; #[sea_orm_macros::test] @@ -16,6 +18,9 @@ async fn main() -> Result<(), DbErr> { create_tables(&ctx.db).await?; create_insert_default(&ctx.db).await?; cursor_pagination(&ctx.db).await?; + schema::create_tables(&ctx.db).await?; + create_baker_cake(&ctx.db).await?; + cursor_related_pagination(&ctx.db).await?; ctx.delete().await; Ok(()) @@ -274,3 +279,260 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { Ok(()) } + +use common::bakery_chain::{ + baker, bakery, cake, cakes_bakers, schema, Baker, Bakery, Cake, CakesBakers, +}; + +fn bakery(i: i32) -> bakery::Model { + bakery::Model { + name: i.to_string(), + profit_margin: 10.4, + id: i, + } +} +fn baker(c: char) -> baker::Model { + baker::Model { + name: c.clone().to_string(), + contact_details: serde_json::json!({ + "mobile": "+61424000000", + }), + bakery_id: Some((c as i32 - 65) % 10 + 1), + id: c as i32 - 64, + } +} + +#[derive(Debug, FromQueryResult, PartialEq)] +pub struct CakeBakerlite { + pub cake_name: String, + pub cake_id: i32, + pub baker_name: String, +} + +fn cakebaker(cake: char, baker: char) -> CakeBakerlite { + CakeBakerlite { + cake_name: cake.to_string(), + cake_id: cake as i32 - 96, + baker_name: baker.to_string(), + } +} + +pub async fn create_baker_cake(db: &DatabaseConnection) -> Result<(), DbErr> { + use sea_orm::IntoActiveModel; + + let mut bakeries: Vec = vec![]; + // bakeries named from 1 to 10 + for i in 1..=10 { + bakeries.push(bakery::ActiveModel { + name: Set(i.to_string()), + profit_margin: Set(10.4), + ..Default::default() + }); + } + let _ = Bakery::insert_many(bakeries).exec(db).await?; + + let mut bakers: Vec = vec![]; + let mut cakes: Vec = vec![]; + let mut cakes_bakers: Vec = vec![]; + // baker and cakes named from "A" to "Z" and from "a" to "z" + for c in 'A'..='Z' { + bakers.push(baker::ActiveModel { + name: Set(c.clone().to_string()), + contact_details: Set(serde_json::json!({ + "mobile": "+61424000000", + })), + bakery_id: Set(Some((c as i32 - 65) % 10 + 1)), + ..Default::default() + }); + cakes.push(cake::ActiveModel { + name: Set(c.to_ascii_lowercase().to_string()), + price: Set(rust_decimal_macros::dec!(10.25)), + gluten_free: Set(false), + serial: Set(Uuid::new_v4()), + bakery_id: Set(Some((c as i32 - 65) % 10 + 1)), + ..Default::default() + }); + cakes_bakers.push(cakes_bakers::ActiveModel { + cake_id: Set(c as i32 - 64), + baker_id: Set(c as i32 - 64), + }) + } + cakes_bakers.append( + vec![ + cakes_bakers::ActiveModel { + cake_id: Set(2), + baker_id: Set(1), + }, + cakes_bakers::ActiveModel { + cake_id: Set(1), + baker_id: Set(2), + }, + ] + .as_mut(), + ); + Baker::insert_many(bakers).exec(db).await?; + Cake::insert_many(cakes).exec(db).await?; + CakesBakers::insert_many(cakes_bakers).exec(db).await?; + + Ok(()) +} + +pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { + use common::bakery_chain::*; + + assert_eq!( + bakery::Entity::find() + .cursor_by(bakery::Column::Id) + .before(5) + .first(4) + .all(db) + .await?, + [bakery(1), bakery(2), bakery(3), bakery(4),] + ); + + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Id) + .before(5) + .first(20) + .all(db) + .await?, + [ + (bakery(1), Some(baker('A'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('U'))), + (bakery(2), Some(baker('B'))), + (bakery(2), Some(baker('L'))), + (bakery(2), Some(baker('V'))), + (bakery(3), Some(baker('C'))), + (bakery(3), Some(baker('M'))), + (bakery(3), Some(baker('W'))), + (bakery(4), Some(baker('D'))), + (bakery(4), Some(baker('N'))), + (bakery(4), Some(baker('X'))), + ] + ); + + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Id) + .before(5) + .first(4) + .all(db) + .await?, + [ + (bakery(1), Some(baker('A'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('U'))), + (bakery(2), Some(baker('B'))), + ] + ); + + // since "10" is before "2" lexicologically, it return that first + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Name) + .before("3") + .first(4) + .all(db) + .await?, + [ + (bakery(1), Some(baker('A'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('U'))), + (bakery(10), Some(baker('J'))), + ] + ); + + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + CakeId, + CakeName, + BakerName, + } + + assert_eq!( + cake::Entity::find() + .find_also_related(Baker) + .select_only() + .column_as(cake::Column::Id, QueryAs::CakeId) + .column_as(cake::Column::Name, QueryAs::CakeName) + .column_as(baker::Column::Name, QueryAs::BakerName) + .cursor_by(cake::Column::Name) + .before("e") + .first(4) + .clone() + .into_model::() + .all(db) + .await?, + vec![ + cakebaker('a', 'A'), + cakebaker('a', 'B'), + cakebaker('b', 'A'), + cakebaker('b', 'B') + ] + ); + + assert_eq!( + cake::Entity::find() + .find_also_related(Baker) + .select_only() + .column_as(cake::Column::Id, QueryAs::CakeId) + .column_as(cake::Column::Name, QueryAs::CakeName) + .column_as(baker::Column::Name, QueryAs::BakerName) + .cursor_by(cake::Column::Name) + .before("b") + .first(4) + .clone() + .into_model::() + .all(db) + .await?, + vec![cakebaker('a', 'A'), cakebaker('a', 'B'),] + ); + + assert_eq!( + cake::Entity::find() + .find_also_related(Baker) + .select_only() + .column_as(cake::Column::Id, QueryAs::CakeId) + .column_as(cake::Column::Name, QueryAs::CakeName) + .column_as(baker::Column::Name, QueryAs::BakerName) + .cursor_by_other(baker::Column::Name) + .before("B") + .first(4) + .clone() + .into_model::() + .all(db) + .await?, + vec![cakebaker('a', 'A'), cakebaker('b', 'A'),] + ); + + assert_eq!( + cake::Entity::find() + .find_also_related(Baker) + .select_only() + .column_as(cake::Column::Id, QueryAs::CakeId) + .column_as(cake::Column::Name, QueryAs::CakeName) + .column_as(baker::Column::Name, QueryAs::BakerName) + .cursor_by_other(baker::Column::Name) + .before("E") + .first(20) + .clone() + .into_model::() + .all(db) + .await?, + vec![ + cakebaker('a', 'A'), + cakebaker('b', 'A'), + cakebaker('a', 'B'), + cakebaker('b', 'B'), + cakebaker('c', 'C'), + cakebaker('d', 'D'), + ] + ); + + Ok(()) +}