From c5adb9d1d1627b5bd1fe9a5016f45e4a7587398e Mon Sep 17 00:00:00 2001 From: Maksim Zaborovskiy Date: Sun, 14 Jan 2024 16:23:55 +0100 Subject: [PATCH] Add cursor pagination desc order (#2037) * feat: add cursor pagination desc order * feat: add cursor pagination desc order * test: add cursor integration tests * test: add cursor integration tests * test: add query cursor_by tests * test: add query cursor_by tests * test: add query cursor_by tests * test: add query cursor_by tests * test: add query cursor_by tests * test: add query cursor_by sort tests * chore: remove unused import --- src/executor/cursor.rs | 1038 ++++++++++++++++++++++++++++++++++++++-- tests/cursor_tests.rs | 413 +++++++++++++++- 2 files changed, 1420 insertions(+), 31 deletions(-) diff --git a/src/executor/cursor.rs b/src/executor/cursor.rs index 50f282ef..78d86a7a 100644 --- a/src/executor/cursor.rs +++ b/src/executor/cursor.rs @@ -23,7 +23,12 @@ where table: DynIden, order_columns: Identity, secondary_order_by: Vec<(DynIden, Identity)>, - last: bool, + first: Option, + last: Option, + before: Option, + after: Option, + sort_asc: bool, + is_result_reversed: bool, phantom: PhantomData, } @@ -40,7 +45,12 @@ where query, table, order_columns: order_columns.into_identity(), - last: false, + last: None, + first: None, + after: None, + before: None, + sort_asc: true, + is_result_reversed: false, phantom: PhantomData, secondary_order_by: Default::default(), } @@ -51,10 +61,7 @@ where where V: IntoValueTuple, { - let condition = self.apply_filter(values, |c, v| { - Expr::col((SeaRc::clone(&self.table), SeaRc::clone(c))).lt(v) - }); - self.query.cond_where(condition); + self.before = Some(values.into_value_tuple()); self } @@ -63,19 +70,43 @@ where where V: IntoValueTuple, { - let condition = self.apply_filter(values, |c, v| { - Expr::col((SeaRc::clone(&self.table), SeaRc::clone(c))).gt(v) - }); - self.query.cond_where(condition); + self.after = Some(values.into_value_tuple()); self } - fn apply_filter(&self, values: V, f: F) -> Condition + fn apply_filters(&mut self) -> &mut Self { + if let Some(values) = self.after.clone() { + let condition = self.apply_filter(values, |c, v| { + let exp = Expr::col((SeaRc::clone(&self.table), SeaRc::clone(c))); + if self.sort_asc { + exp.gt(v) + } else { + exp.lt(v) + } + }); + self.query.cond_where(condition); + } + + if let Some(values) = self.before.clone() { + let condition = self.apply_filter(values, |c, v| { + let exp = Expr::col((SeaRc::clone(&self.table), SeaRc::clone(c))); + if self.sort_asc { + exp.lt(v) + } else { + exp.gt(v) + } + }); + self.query.cond_where(condition); + } + + self + } + + fn apply_filter(&self, values: ValueTuple, f: F) -> Condition where - V: IntoValueTuple, F: Fn(&DynIden, Value) -> SimpleExpr, { - match (&self.order_columns, values.into_value_tuple()) { + match (&self.order_columns, values) { (Identity::Unary(c1), ValueTuple::One(v1)) => Condition::all().add(f(c1, v1)), (Identity::Binary(c1, c2), ValueTuple::Two(v1, v2)) => Condition::any() .add( @@ -159,26 +190,65 @@ where } } + /// Use ascending sort order + pub fn asc(&mut self) -> &mut Self { + self.sort_asc = true; + self + } + + /// Use descending sort order + pub fn desc(&mut self) -> &mut Self { + self.sort_asc = false; + self + } + /// 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(); - self.apply_order_by(self.table.clone(), Order::Asc); - self.last = false; + self.last = None; + self.first = Some(num_rows); self } /// 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(); - self.apply_order_by(self.table.clone(), Order::Desc); - self.last = true; + self.first = None; + self.last = Some(num_rows); self } - fn apply_order_by(&mut self, table: DynIden, ord: Order) { + fn resolve_sort_order(&mut self) -> Order { + let should_reverse_order = self.last.is_some(); + + if should_reverse_order { + self.is_result_reversed = true; + } else { + self.is_result_reversed = false; + } + + if (self.sort_asc && !should_reverse_order) || (!self.sort_asc && should_reverse_order) { + Order::Asc + } else { + Order::Desc + } + } + + fn apply_limit(&mut self) -> &mut Self { + if let Some(num_rows) = self.first { + self.query.limit(num_rows); + } else if let Some(num_rows) = self.last { + self.query.limit(num_rows); + } + + self + } + + fn apply_order_by(&mut self) -> &mut Self { + self.query.clear_order_by(); + let ord = self.resolve_sort_order(); + let query = &mut self.query; let order = |query: &mut SelectStatement, col| { - query.order_by((SeaRc::clone(&table), SeaRc::clone(col)), ord.clone()); + query.order_by((SeaRc::clone(&self.table), SeaRc::clone(col)), ord.clone()); }; match &self.order_columns { Identity::Unary(c1) => { @@ -205,6 +275,8 @@ where query.order_by((tbl, c1), ord.clone()); }; } + + self } /// Fetch the paginated result @@ -212,13 +284,17 @@ where where C: ConnectionTrait, { + self.apply_limit(); + self.apply_order_by(); + self.apply_filters(); + let stmt = db.get_database_backend().build(&self.query); let rows = db.query_all(stmt).await?; let mut buffer = Vec::with_capacity(rows.len()); for row in rows.into_iter() { buffer.push(S::from_raw_query_result(row)?); } - if self.last { + if self.is_result_reversed { buffer.reverse() } Ok(buffer) @@ -234,6 +310,11 @@ where table: self.table, order_columns: self.order_columns, last: self.last, + first: self.first, + after: self.after, + before: self.before, + sort_asc: self.sort_asc, + is_result_reversed: self.is_result_reversed, phantom: PhantomData, secondary_order_by: self.secondary_order_by, } @@ -255,6 +336,11 @@ where table: self.table, order_columns: self.order_columns, last: self.last, + first: self.first, + after: self.after, + before: self.before, + sort_asc: self.sort_asc, + is_result_reversed: self.is_result_reversed, phantom: PhantomData, secondary_order_by: self.secondary_order_by, } @@ -438,6 +524,60 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn last_2_after_10_desc() -> Result<(), DbErr> { + use fruit::*; + + let mut models = [ + Model { + id: 1, + name: "Blueberry".into(), + cake_id: Some(1), + }, + Model { + id: 2, + name: "Rasberry".into(), + cake_id: Some(1), + }, + ]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + models.reverse(); + + assert_eq!( + Entity::find() + .cursor_by(Column::Id) + .after(10) + .last(2) + .desc() + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id""#, + r#"FROM "fruit""#, + r#"WHERE "fruit"."id" < $1"#, + r#"ORDER BY "fruit"."id" ASC"#, + r#"LIMIT $2"#, + ] + .join(" ") + .as_str(), + [10_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + #[smol_potat::test] async fn first_2_before_10_also_related_select() -> Result<(), DbErr> { let models = [ @@ -501,6 +641,72 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn last_2_after_10_also_related_select_desc() -> Result<(), DbErr> { + let mut models = [ + ( + cake::Model { + id: 2, + name: "Rasberry Cheese Cake".into(), + }, + Some(fruit::Model { + id: 10, + name: "Rasberry".into(), + cake_id: Some(1), + }), + ), + ( + 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(); + + models.reverse(); + + assert_eq!( + cake::Entity::find() + .find_also_related(Fruit) + .cursor_by(cake::Column::Id) + .after(10) + .last(2) + .desc() + .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 = [( @@ -551,6 +757,57 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn last_2_after_10_also_related_select_cursor_other_desc() -> 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) + .after(10) + .last(2) + .desc() + .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 = [ @@ -613,6 +870,71 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn last_2_after_10_also_linked_select_desc() -> Result<(), DbErr> { + let mut models = [ + ( + cake::Model { + id: 2, + name: "Rasberry Cheese Cake".into(), + }, + Some(vendor::Model { + id: 10, + name: "Rasberry".into(), + }), + ), + ( + 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(); + + models.reverse(); + + assert_eq!( + cake::Entity::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .cursor_by(cake::Column::Id) + .after(10) + .last(2) + .desc() + .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 = [( @@ -663,6 +985,59 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn last_2_after_10_also_linked_select_cursor_other_desc() -> Result<(), DbErr> { + let mut 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(); + + models.reverse(); + + assert_eq!( + cake::Entity::find() + .find_also_linked(entity_linked::CakeToFillingVendor) + .cursor_by_other(vendor::Column::Id) + .after(10) + .last(2) + .desc() + .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::*; @@ -723,6 +1098,58 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn first_2_before_10_desc() -> Result<(), DbErr> { + use fruit::*; + + let models = [ + Model { + id: 22, + name: "Rasberry".into(), + cake_id: Some(1), + }, + Model { + id: 21, + name: "Blueberry".into(), + cake_id: Some(1), + }, + ]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + assert_eq!( + Entity::find() + .cursor_by(Column::Id) + .before(10) + .first(2) + .desc() + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id""#, + r#"FROM "fruit""#, + r#"WHERE "fruit"."id" > $1"#, + r#"ORDER BY "fruit"."id" DESC"#, + r#"LIMIT $2"#, + ] + .join(" ") + .as_str(), + [10_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + #[smol_potat::test] async fn last_2_after_25_before_30() -> Result<(), DbErr> { use fruit::*; @@ -785,6 +1212,60 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn first_2_after_30_before_25_desc() -> Result<(), DbErr> { + use fruit::*; + + let models = [ + Model { + id: 27, + name: "Rasberry".into(), + cake_id: Some(1), + }, + Model { + id: 26, + name: "Blueberry".into(), + cake_id: Some(1), + }, + ]; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([models.clone()]) + .into_connection(); + + assert_eq!( + Entity::find() + .cursor_by(Column::Id) + .after(30) + .before(25) + .first(2) + .desc() + .all(&db) + .await?, + models + ); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "fruit"."id", "fruit"."name", "fruit"."cake_id""#, + r#"FROM "fruit""#, + r#"WHERE "fruit"."id" < $1"#, + r#"AND "fruit"."id" > $2"#, + r#"ORDER BY "fruit"."id" DESC"#, + r#"LIMIT $3"#, + ] + .join(" ") + .as_str(), + [30_i32.into(), 25_i32.into(), 2_u64.into()] + ),])] + ); + + Ok(()) + } + mod test_entity { use crate as sea_orm; use crate::entity::prelude::*; @@ -862,6 +1343,44 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn composite_keys_1_desc() -> Result<(), DbErr> { + use test_entity::*; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[Model { + id: 1, + category: "CAT".into(), + }]]) + .into_connection(); + + assert!(!Entity::find() + .cursor_by((Column::Category, Column::Id)) + .last(3) + .desc() + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "example"."id", "example"."category""#, + r#"FROM "example""#, + r#"ORDER BY "example"."category" ASC, "example"."id" ASC"#, + r#"LIMIT $1"#, + ] + .join(" ") + .as_str(), + [3_u64.into()] + ),])] + ); + + Ok(()) + } + #[smol_potat::test] async fn composite_keys_2() -> Result<(), DbErr> { use test_entity::*; @@ -907,6 +1426,52 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn composite_keys_2_desc() -> Result<(), DbErr> { + use test_entity::*; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[Model { + id: 1, + category: "CAT".into(), + }]]) + .into_connection(); + + assert!(!Entity::find() + .cursor_by((Column::Category, Column::Id)) + .before(("A".to_owned(), 2)) + .last(3) + .desc() + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "example"."id", "example"."category""#, + r#"FROM "example""#, + r#"WHERE ("example"."category" = $1 AND "example"."id" > $2)"#, + r#"OR "example"."category" > $3"#, + r#"ORDER BY "example"."category" ASC, "example"."id" ASC"#, + r#"LIMIT $4"#, + ] + .join(" ") + .as_str(), + [ + "A".to_string().into(), + 2i32.into(), + "A".to_string().into(), + 3_u64.into(), + ] + )])] + ); + + Ok(()) + } + #[smol_potat::test] async fn composite_keys_3() -> Result<(), DbErr> { use test_entity::*; @@ -952,6 +1517,52 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn composite_keys_3_desc() -> Result<(), DbErr> { + use test_entity::*; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[Model { + id: 1, + category: "CAT".into(), + }]]) + .into_connection(); + + assert!(!Entity::find() + .cursor_by((Column::Category, Column::Id)) + .after(("A".to_owned(), 2)) + .first(3) + .desc() + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "example"."id", "example"."category""#, + r#"FROM "example""#, + r#"WHERE ("example"."category" = $1 AND "example"."id" < $2)"#, + r#"OR "example"."category" < $3"#, + r#"ORDER BY "example"."category" DESC, "example"."id" DESC"#, + r#"LIMIT $4"#, + ] + .join(" ") + .as_str(), + [ + "A".to_string().into(), + 2i32.into(), + "A".to_string().into(), + 3_u64.into(), + ] + )])] + ); + + Ok(()) + } + #[smol_potat::test] async fn composite_keys_4() -> Result<(), DbErr> { use xyz_entity::*; @@ -990,6 +1601,45 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn composite_keys_4_desc() -> Result<(), DbErr> { + use xyz_entity::*; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[Model { + x: 'x' as i32, + y: "y".into(), + z: 'z' as i64, + }]]) + .into_connection(); + + assert!(!Entity::find() + .cursor_by((Column::X, Column::Y, Column::Z)) + .last(4) + .desc() + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "m"."x", "m"."y", "m"."z""#, + r#"FROM "m""#, + r#"ORDER BY "m"."x" ASC, "m"."y" ASC, "m"."z" ASC"#, + r#"LIMIT $1"#, + ] + .join(" ") + .as_str(), + [4_u64.into()] + ),])] + ); + + Ok(()) + } + #[smol_potat::test] async fn composite_keys_5() -> Result<(), DbErr> { use xyz_entity::*; @@ -1040,6 +1690,57 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn composite_keys_5_desc() -> Result<(), DbErr> { + use xyz_entity::*; + + let db = MockDatabase::new(DbBackend::Postgres) + .append_query_results([[Model { + x: 'x' as i32, + y: "y".into(), + z: 'z' as i64, + }]]) + .into_connection(); + + assert!(!Entity::find() + .cursor_by((Column::X, Column::Y, Column::Z)) + .before(('x' as i32, "y".to_owned(), 'z' as i64)) + .last(4) + .desc() + .all(&db) + .await? + .is_empty()); + + assert_eq!( + db.into_transaction_log(), + [Transaction::many([Statement::from_sql_and_values( + DbBackend::Postgres, + [ + r#"SELECT "m"."x", "m"."y", "m"."z""#, + r#"FROM "m""#, + r#"WHERE ("m"."x" = $1 AND "m"."y" = $2 AND "m"."z" > $3)"#, + r#"OR ("m"."x" = $4 AND "m"."y" > $5)"#, + r#"OR "m"."x" > $6"#, + r#"ORDER BY "m"."x" ASC, "m"."y" ASC, "m"."z" ASC"#, + r#"LIMIT $7"#, + ] + .join(" ") + .as_str(), + [ + ('x' as i32).into(), + "y".into(), + ('z' as i64).into(), + ('x' as i32).into(), + "y".into(), + ('x' as i32).into(), + 4_u64.into(), + ] + ),])] + ); + + Ok(()) + } + mod composite_entity { use crate as sea_orm; use crate::entity::prelude::*; @@ -1092,14 +1793,14 @@ mod tests { DbBackend::Postgres.build(& Entity::find() .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4)) - .after(("val_1", "val_2", "val_3", "val_4")) - .query + .after(("val_1", "val_2", "val_3", "val_4")).apply_limit().apply_order_by().apply_filters().query ).to_string(), format!("{base_sql} {}", [ r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" > 'val_4')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" > 'val_3')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" > 'val_2')"#, r#"OR "t"."col_1" > 'val_1'"#, + r#"ORDER BY "t"."col_1" ASC, "t"."col_2" ASC, "t"."col_3" ASC, "t"."col_4" ASC"#, ].join(" ")) ); @@ -1107,7 +1808,7 @@ mod tests { DbBackend::Postgres.build(& Entity::find() .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5)) - .after(("val_1", "val_2", "val_3", "val_4", "val_5")) + .after(("val_1", "val_2", "val_3", "val_4", "val_5")).apply_limit().apply_order_by().apply_filters() .query ).to_string(), format!("{base_sql} {}", [ @@ -1116,6 +1817,7 @@ mod tests { r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" > 'val_3')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" > 'val_2')"#, r#"OR "t"."col_1" > 'val_1'"#, + r#"ORDER BY "t"."col_1" ASC, "t"."col_2" ASC, "t"."col_3" ASC, "t"."col_4" ASC, "t"."col_5" ASC"#, ].join(" ")) ); @@ -1123,7 +1825,7 @@ mod tests { DbBackend::Postgres.build(& Entity::find() .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6)) - .after(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6")) + .after(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6")).apply_limit().apply_order_by().apply_filters() .query ).to_string(), format!("{base_sql} {}", [ @@ -1133,6 +1835,7 @@ mod tests { r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" > 'val_3')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" > 'val_2')"#, r#"OR "t"."col_1" > 'val_1'"#, + r#"ORDER BY "t"."col_1" ASC, "t"."col_2" ASC, "t"."col_3" ASC, "t"."col_4" ASC, "t"."col_5" ASC, "t"."col_6" ASC"#, ].join(" ")) ); @@ -1140,7 +1843,7 @@ mod tests { DbBackend::Postgres.build(& Entity::find() .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6, Column::Col7)) - .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7")) + .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7")).apply_limit().apply_order_by().apply_filters() .query ).to_string(), format!("{base_sql} {}", [ @@ -1151,6 +1854,7 @@ mod tests { r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" < 'val_3')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" < 'val_2')"#, r#"OR "t"."col_1" < 'val_1'"#, + r#"ORDER BY "t"."col_1" ASC, "t"."col_2" ASC, "t"."col_3" ASC, "t"."col_4" ASC, "t"."col_5" ASC, "t"."col_6" ASC, "t"."col_7" ASC"#, ].join(" ")) ); @@ -1158,7 +1862,7 @@ mod tests { DbBackend::Postgres.build(& Entity::find() .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6, Column::Col7, Column::Col8)) - .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7", "val_8")) + .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7", "val_8")).apply_limit().apply_order_by().apply_filters() .query ).to_string(), format!("{base_sql} {}", [ @@ -1170,6 +1874,7 @@ mod tests { r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" < 'val_3')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" < 'val_2')"#, r#"OR "t"."col_1" < 'val_1'"#, + r#"ORDER BY "t"."col_1" ASC, "t"."col_2" ASC, "t"."col_3" ASC, "t"."col_4" ASC, "t"."col_5" ASC, "t"."col_6" ASC, "t"."col_7" ASC, "t"."col_8" ASC"#, ].join(" ")) ); @@ -1177,7 +1882,7 @@ mod tests { DbBackend::Postgres.build(& Entity::find() .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6, Column::Col7, Column::Col8, Column::Col9)) - .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7", "val_8", "val_9")) + .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7", "val_8", "val_9")).apply_limit().apply_order_by().apply_filters() .query ).to_string(), format!("{base_sql} {}", [ @@ -1190,6 +1895,129 @@ mod tests { r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" < 'val_3')"#, r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" < 'val_2')"#, r#"OR "t"."col_1" < 'val_1'"#, + r#"ORDER BY "t"."col_1" ASC, "t"."col_2" ASC, "t"."col_3" ASC, "t"."col_4" ASC, "t"."col_5" ASC, "t"."col_6" ASC, "t"."col_7" ASC, "t"."col_8" ASC, "t"."col_9" ASC"#, + ].join(" ")) + ); + + Ok(()) + } + + #[smol_potat::test] + async fn cursor_by_many_desc() -> Result<(), DbErr> { + use composite_entity::*; + + let base_sql = [ + r#"SELECT "t"."col_1", "t"."col_2", "t"."col_3", "t"."col_4", "t"."col_5", "t"."col_6", "t"."col_7", "t"."col_8", "t"."col_9", "t"."col_10", "t"."col_11", "t"."col_12""#, + r#"FROM "t" WHERE"#, + ].join(" "); + + assert_eq!( + DbBackend::Postgres.build(& + Entity::find() + .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4)) + .before(("val_1", "val_2", "val_3", "val_4")).desc().apply_limit().apply_order_by().apply_filters().query + ).to_string(), + format!("{base_sql} {}", [ + r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" > 'val_4')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" > 'val_3')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" > 'val_2')"#, + r#"OR "t"."col_1" > 'val_1'"#, + r#"ORDER BY "t"."col_1" DESC, "t"."col_2" DESC, "t"."col_3" DESC, "t"."col_4" DESC"#, + ].join(" ")) + ); + + assert_eq!( + DbBackend::Postgres.build(& + Entity::find() + .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5)) + .before(("val_1", "val_2", "val_3", "val_4", "val_5")).desc().apply_limit().apply_order_by().apply_filters() + .query + ).to_string(), + format!("{base_sql} {}", [ + r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" > 'val_5')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" > 'val_4')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" > 'val_3')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" > 'val_2')"#, + r#"OR "t"."col_1" > 'val_1'"#, + r#"ORDER BY "t"."col_1" DESC, "t"."col_2" DESC, "t"."col_3" DESC, "t"."col_4" DESC, "t"."col_5" DESC"#, + ].join(" ")) + ); + + assert_eq!( + DbBackend::Postgres.build(& + Entity::find() + .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6)) + .before(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6")).desc().apply_limit().apply_order_by().apply_filters() + .query + ).to_string(), + format!("{base_sql} {}", [ + r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" > 'val_6')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" > 'val_5')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" > 'val_4')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" > 'val_3')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" > 'val_2')"#, + r#"OR "t"."col_1" > 'val_1'"#, + r#"ORDER BY "t"."col_1" DESC, "t"."col_2" DESC, "t"."col_3" DESC, "t"."col_4" DESC, "t"."col_5" DESC, "t"."col_6" DESC"#, + ].join(" ")) + ); + + assert_eq!( + DbBackend::Postgres.build(& + Entity::find() + .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6, Column::Col7)) + .after(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7")).desc().apply_limit().apply_order_by().apply_filters() + .query + ).to_string(), + format!("{base_sql} {}", [ + r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" = 'val_6' AND "t"."col_7" < 'val_7')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" < 'val_6')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" < 'val_5')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" < 'val_4')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" < 'val_3')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" < 'val_2')"#, + r#"OR "t"."col_1" < 'val_1'"#, + r#"ORDER BY "t"."col_1" DESC, "t"."col_2" DESC, "t"."col_3" DESC, "t"."col_4" DESC, "t"."col_5" DESC, "t"."col_6" DESC, "t"."col_7" DESC"#, + ].join(" ")) + ); + + assert_eq!( + DbBackend::Postgres.build(& + Entity::find() + .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6, Column::Col7, Column::Col8)) + .after(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7", "val_8")).desc().apply_limit().apply_order_by().apply_filters() + .query + ).to_string(), + format!("{base_sql} {}", [ + r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" = 'val_6' AND "t"."col_7" = 'val_7' AND "t"."col_8" < 'val_8')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" = 'val_6' AND "t"."col_7" < 'val_7')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" < 'val_6')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" < 'val_5')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" < 'val_4')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" < 'val_3')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" < 'val_2')"#, + r#"OR "t"."col_1" < 'val_1'"#, + r#"ORDER BY "t"."col_1" DESC, "t"."col_2" DESC, "t"."col_3" DESC, "t"."col_4" DESC, "t"."col_5" DESC, "t"."col_6" DESC, "t"."col_7" DESC, "t"."col_8" DESC"#, + ].join(" ")) + ); + + assert_eq!( + DbBackend::Postgres.build(& + Entity::find() + .cursor_by((Column::Col1, Column::Col2, Column::Col3, Column::Col4, Column::Col5, Column::Col6, Column::Col7, Column::Col8, Column::Col9)) + .after(("val_1", "val_2", "val_3", "val_4", "val_5", "val_6", "val_7", "val_8", "val_9")).desc().apply_limit().apply_order_by().apply_filters() + .query + ).to_string(), + format!("{base_sql} {}", [ + r#"("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" = 'val_6' AND "t"."col_7" = 'val_7' AND "t"."col_8" = 'val_8' AND "t"."col_9" < 'val_9')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" = 'val_6' AND "t"."col_7" = 'val_7' AND "t"."col_8" < 'val_8')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" = 'val_6' AND "t"."col_7" < 'val_7')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" = 'val_5' AND "t"."col_6" < 'val_6')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" = 'val_4' AND "t"."col_5" < 'val_5')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" = 'val_3' AND "t"."col_4" < 'val_4')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" = 'val_2' AND "t"."col_3" < 'val_3')"#, + r#"OR ("t"."col_1" = 'val_1' AND "t"."col_2" < 'val_2')"#, + r#"OR "t"."col_1" < 'val_1'"#, + r#"ORDER BY "t"."col_1" DESC, "t"."col_2" DESC, "t"."col_3" DESC, "t"."col_4" DESC, "t"."col_5" DESC, "t"."col_6" DESC, "t"."col_7" DESC, "t"."col_8" DESC, "t"."col_9" DESC"#, ].join(" ")) ); @@ -1302,6 +2130,51 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn related_composite_keys_1_desc() -> 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)) + .last(1) + .desc() + .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) @@ -1353,6 +2226,58 @@ mod tests { Ok(()) } + #[smol_potat::test] + async fn related_composite_keys_2_desc() -> 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)) + .before((1, "C".to_string())) + .last(2) + .desc() + .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) @@ -1406,4 +2331,59 @@ mod tests { Ok(()) } + + #[smol_potat::test] + async fn related_composite_keys_3_desc() -> 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 + )) + .before((1, "CAT".to_string())) + .last(2) + .desc() + .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 6b90c34f..e8430868 100644 --- a/tests/cursor_tests.rs +++ b/tests/cursor_tests.rs @@ -11,7 +11,7 @@ use serde_json::json; feature = "sqlx-sqlite", feature = "sqlx-postgres" ))] -async fn main() -> Result<(), DbErr> { +async fn cursor_tests() -> Result<(), DbErr> { let ctx = TestContext::new("cursor_tests").await; create_tables(&ctx.db).await?; create_insert_default(&ctx.db).await?; @@ -103,6 +103,54 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { ] ); + // Before 5 DESC, i.e. id > 5 + + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.before(5).desc(); + + assert_eq!( + cursor.first(4).all(db).await?, + [ + Model { id: 10 }, + Model { id: 9 }, + Model { id: 8 }, + Model { id: 7 }, + ] + ); + + assert_eq!( + cursor.first(5).all(db).await?, + [ + Model { id: 10 }, + Model { id: 9 }, + Model { id: 8 }, + Model { id: 7 }, + Model { id: 6 }, + ] + ); + + assert_eq!( + cursor.last(4).all(db).await?, + [ + Model { id: 9 }, + Model { id: 8 }, + Model { id: 7 }, + Model { id: 6 }, + ] + ); + + assert_eq!( + cursor.last(5).all(db).await?, + [ + Model { id: 10 }, + Model { id: 9 }, + Model { id: 8 }, + Model { id: 7 }, + Model { id: 6 }, + ] + ); + // After 5, i.e. id > 5 let mut cursor = Entity::find().cursor_by(Column::Id); @@ -173,6 +221,62 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { ] ); + // After 5 DESC, i.e. id < 5 + + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.after(5).desc(); + + assert_eq!( + cursor.first(3).all(db).await?, + [Model { id: 4 }, Model { id: 3 }, Model { id: 2 },] + ); + + assert_eq!( + cursor.first(4).all(db).await?, + [ + Model { id: 4 }, + Model { id: 3 }, + Model { id: 2 }, + Model { id: 1 }, + ] + ); + + assert_eq!( + cursor.first(5).all(db).await?, + [ + Model { id: 4 }, + Model { id: 3 }, + Model { id: 2 }, + Model { id: 1 }, + ] + ); + + assert_eq!( + cursor.last(3).all(db).await?, + [Model { id: 3 }, Model { id: 2 }, Model { id: 1 },] + ); + + assert_eq!( + cursor.last(4).all(db).await?, + [ + Model { id: 4 }, + Model { id: 3 }, + Model { id: 2 }, + Model { id: 1 }, + ] + ); + + assert_eq!( + cursor.last(5).all(db).await?, + [ + Model { id: 4 }, + Model { id: 3 }, + Model { id: 2 }, + Model { id: 1 }, + ] + ); + // Between 5 and 8, i.e. id > 5 AND id < 8 let mut cursor = Entity::find().cursor_by(Column::Id); @@ -203,6 +307,64 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { [Model { id: 6 }, Model { id: 7 }] ); + // Between 8 and 5 DESC, i.e. id < 8 AND id > 5 + + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.after(8).before(5).desc(); + + assert_eq!(cursor.first(1).all(db).await?, [Model { id: 7 }]); + + assert_eq!( + cursor.first(2).all(db).await?, + [Model { id: 7 }, Model { id: 6 }] + ); + + assert_eq!( + cursor.first(3).all(db).await?, + [Model { id: 7 }, Model { id: 6 }] + ); + + assert_eq!(cursor.last(1).all(db).await?, [Model { id: 6 }]); + + assert_eq!( + cursor.last(2).all(db).await?, + [Model { id: 7 }, Model { id: 6 }] + ); + + assert_eq!( + cursor.last(3).all(db).await?, + [Model { id: 7 }, Model { id: 6 }] + ); + + // Ensure asc/desc order can be changed + + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.first(2); + + assert_eq!(cursor.all(db).await?, [Model { id: 1 }, Model { id: 2 },]); + + assert_eq!( + cursor.asc().all(db).await?, + [Model { id: 1 }, Model { id: 2 },] + ); + + assert_eq!( + cursor.desc().all(db).await?, + [Model { id: 10 }, Model { id: 9 },] + ); + + assert_eq!( + cursor.asc().all(db).await?, + [Model { id: 1 }, Model { id: 2 },] + ); + + assert_eq!( + cursor.desc().all(db).await?, + [Model { id: 10 }, Model { id: 9 },] + ); + // Fetch custom struct #[derive(FromQueryResult, Debug, PartialEq, Clone)] @@ -210,6 +372,10 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { id: i32, } + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.after(5).before(8); + let mut cursor = cursor.into_model::(); assert_eq!( @@ -222,8 +388,30 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { [Row { id: 6 }, Row { id: 7 }] ); + // Fetch custom struct desc + + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.after(8).before(5).desc(); + + let mut cursor = cursor.into_model::(); + + assert_eq!( + cursor.first(2).all(db).await?, + [Row { id: 7 }, Row { id: 6 }] + ); + + assert_eq!( + cursor.first(3).all(db).await?, + [Row { id: 7 }, Row { id: 6 }] + ); + // Fetch JSON value + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.after(5).before(8); + let mut cursor = cursor.into_json(); assert_eq!( @@ -275,6 +463,54 @@ pub async fn cursor_pagination(db: &DatabaseConnection) -> Result<(), DbErr> { ] ); + // Fetch JSON value desc + + let mut cursor = Entity::find().cursor_by(Column::Id); + + cursor.after(8).before(5).desc(); + + let mut cursor = cursor.into_json(); + + assert_eq!( + cursor.first(2).all(db).await?, + [json!({ "id": 7 }), json!({ "id": 6 })] + ); + + assert_eq!( + cursor.first(3).all(db).await?, + [json!({ "id": 7 }), json!({ "id": 6 })] + ); + + let mut cursor = cursor.into_partial_model::(); + + assert_eq!( + cursor.first(2).all(db).await?, + [ + PartialRow { + id: 7, + id_shifted: 1007, + }, + PartialRow { + id: 6, + id_shifted: 1006, + } + ] + ); + + assert_eq!( + cursor.first(3).all(db).await?, + [ + PartialRow { + id: 7, + id_shifted: 1007, + }, + PartialRow { + id: 6, + id_shifted: 1006, + }, + ] + ); + Ok(()) } @@ -387,6 +623,17 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db [bakery(1), bakery(2), bakery(3), bakery(4),] ); + assert_eq!( + bakery::Entity::find() + .cursor_by(bakery::Column::Id) + .before(5) + .first(4) + .desc() + .all(db) + .await?, + [bakery(10), bakery(9), bakery(8), bakery(7),] + ); + assert_eq!( bakery::Entity::find() .find_also_related(Baker) @@ -411,6 +658,31 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db ] ); + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Id) + .after(5) + .last(20) + .desc() + .all(db) + .await?, + [ + (bakery(4), Some(baker('X'))), + (bakery(4), Some(baker('N'))), + (bakery(4), Some(baker('D'))), + (bakery(3), Some(baker('W'))), + (bakery(3), Some(baker('M'))), + (bakery(3), Some(baker('C'))), + (bakery(2), Some(baker('V'))), + (bakery(2), Some(baker('L'))), + (bakery(2), Some(baker('B'))), + (bakery(1), Some(baker('U'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('A'))), + ] + ); + assert_eq!( bakery::Entity::find() .find_also_related(Baker) @@ -427,6 +699,41 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db ] ); + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Id) + .after(5) + .last(4) + .desc() + .all(db) + .await?, + [ + (bakery(2), Some(baker('B'))), + (bakery(1), Some(baker('U'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('A'))), + ] + ); + + // since "10" is before "2" lexicologically, it return that first + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Name) + .after("3") + .last(4) + .desc() + .all(db) + .await?, + [ + (bakery(10), Some(baker('J'))), + (bakery(1), Some(baker('U'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('A'))), + ] + ); + // since "10" is before "2" lexicologically, it return that first assert_eq!( bakery::Entity::find() @@ -444,6 +751,24 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db ] ); + // since "10" is before "2" lexicologically, it return that first + assert_eq!( + bakery::Entity::find() + .find_also_related(Baker) + .cursor_by(bakery::Column::Name) + .after("3") + .last(4) + .desc() + .all(db) + .await?, + [ + (bakery(10), Some(baker('J'))), + (bakery(1), Some(baker('U'))), + (bakery(1), Some(baker('K'))), + (bakery(1), Some(baker('A'))), + ] + ); + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryAs { CakeId, @@ -473,6 +798,29 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db ] ); + 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) + .after("e") + .last(4) + .desc() + .clone() + .into_model::() + .all(db) + .await?, + vec![ + cakebaker('b', 'B'), + cakebaker('b', 'A'), + cakebaker('a', 'B'), + cakebaker('a', 'A') + ] + ); + assert_eq!( cake::Entity::find() .find_also_related(Baker) @@ -487,7 +835,25 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db .into_model::() .all(db) .await?, - vec![cakebaker('a', 'A'), cakebaker('a', 'B'),] + 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(cake::Column::Name) + .after("b") + .last(4) + .desc() + .clone() + .into_model::() + .all(db) + .await?, + vec![cakebaker('a', 'B'), cakebaker('a', 'A')] ); assert_eq!( @@ -507,6 +873,24 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db 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) + .after("B") + .last(4) + .desc() + .clone() + .into_model::() + .all(db) + .await?, + vec![cakebaker('b', 'A'), cakebaker('a', 'A'),] + ); + assert_eq!( cake::Entity::find() .find_also_related(Baker) @@ -531,5 +915,30 @@ pub async fn cursor_related_pagination(db: &DatabaseConnection) -> Result<(), Db ] ); + 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) + .after("E") + .last(20) + .desc() + .clone() + .into_model::() + .all(db) + .await?, + vec![ + cakebaker('d', 'D'), + cakebaker('c', 'C'), + cakebaker('b', 'B'), + cakebaker('a', 'B'), + cakebaker('b', 'A'), + cakebaker('a', 'A'), + ] + ); + Ok(()) }