SelectTwo cursor by, cursorTrait revamp (#1826)
* WIP * WIP * test cases for cursor with SelectTwo * fmt, fix test * WIP, missing test for also_linked cursor_by * completed many_to_many relationship * test fixup * fmt * extra order_by to fix test * WIP * fixed cursor_by to do ordering implicitly base on both cursor column and primary key from other table * change from map to for loop to eagerly do order_by * fix tests
This commit is contained in:
parent
9d033d01a8
commit
27ca745208
@ -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<S>
|
||||
where
|
||||
S: SelectorTrait,
|
||||
{
|
||||
pub(crate) query: SelectStatement,
|
||||
pub(crate) table: DynIden,
|
||||
pub(crate) order_columns: Identity,
|
||||
pub(crate) last: bool,
|
||||
pub(crate) phantom: PhantomData<S>,
|
||||
query: SelectStatement,
|
||||
table: DynIden,
|
||||
order_columns: Identity,
|
||||
secondary_order_by: Vec<(DynIden, Identity)>,
|
||||
last: bool,
|
||||
phantom: PhantomData<S>,
|
||||
}
|
||||
|
||||
impl<S> Cursor<S>
|
||||
@ -29,7 +32,12 @@ where
|
||||
S: SelectorTrait,
|
||||
{
|
||||
/// Initialize a cursor
|
||||
pub fn new<C>(query: SelectStatement, table: DynIden, order_columns: C) -> Self
|
||||
pub fn new<C>(
|
||||
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<F>(&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<S> QuerySelect for Cursor<S>
|
||||
@ -281,11 +298,6 @@ where
|
||||
pub trait CursorTrait {
|
||||
/// Select operation
|
||||
type Selector: SelectorTrait + Send + Sync;
|
||||
|
||||
/// Convert current type into a cursor
|
||||
fn cursor_by<C>(self, order_columns: C) -> Cursor<Self::Selector>
|
||||
where
|
||||
C: IntoIdentity;
|
||||
}
|
||||
|
||||
impl<E, M> CursorTrait for Select<E>
|
||||
@ -294,12 +306,79 @@ where
|
||||
M: FromQueryResult + Sized + Send + Sync,
|
||||
{
|
||||
type Selector = SelectModel<M>;
|
||||
}
|
||||
|
||||
fn cursor_by<C>(self, order_columns: C) -> Cursor<Self::Selector>
|
||||
impl<E, M> Select<E>
|
||||
where
|
||||
E: EntityTrait<Model = M>,
|
||||
M: FromQueryResult + Sized + Send + Sync,
|
||||
{
|
||||
/// Convert into a cursor
|
||||
pub fn cursor_by<C>(self, order_columns: C) -> Cursor<SelectModel<M>>
|
||||
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<E, F, M, N> CursorTrait for SelectTwo<E, F>
|
||||
where
|
||||
E: EntityTrait<Model = M>,
|
||||
F: EntityTrait<Model = N>,
|
||||
M: FromQueryResult + Sized + Send + Sync,
|
||||
N: FromQueryResult + Sized + Send + Sync,
|
||||
{
|
||||
type Selector = SelectTwoModel<M, N>;
|
||||
}
|
||||
|
||||
impl<E, F, M, N> SelectTwo<E, F>
|
||||
where
|
||||
E: EntityTrait<Model = M>,
|
||||
F: EntityTrait<Model = N>,
|
||||
M: FromQueryResult + Sized + Send + Sync,
|
||||
N: FromQueryResult + Sized + Send + Sync,
|
||||
{
|
||||
/// Convert into a cursor using column of first entity
|
||||
pub fn cursor_by<C>(self, order_columns: C) -> Cursor<SelectTwoModel<M, N>>
|
||||
where
|
||||
C: IdentityOf<E>,
|
||||
{
|
||||
let primary_keys: Vec<(DynIden, Identity)> = <F::PrimaryKey as Iterable>::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<C>(self, order_columns: C) -> Cursor<SelectTwoModel<M, N>>
|
||||
where
|
||||
C: IdentityOf<F>,
|
||||
{
|
||||
let primary_keys: Vec<(DynIden, Identity)> = <E::PrimaryKey as Iterable>::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<super::test_related_entity::Entity> 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<super::test_base_entity::Entity> 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(())
|
||||
}
|
||||
}
|
||||
|
@ -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<bakery::ActiveModel> = 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<baker::ActiveModel> = vec![];
|
||||
let mut cakes: Vec<cake::ActiveModel> = vec![];
|
||||
let mut cakes_bakers: Vec<cakes_bakers::ActiveModel> = 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::<CakeBakerlite>()
|
||||
.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::<CakeBakerlite>()
|
||||
.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::<CakeBakerlite>()
|
||||
.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::<CakeBakerlite>()
|
||||
.all(db)
|
||||
.await?,
|
||||
vec![
|
||||
cakebaker('a', 'A'),
|
||||
cakebaker('b', 'A'),
|
||||
cakebaker('a', 'B'),
|
||||
cakebaker('b', 'B'),
|
||||
cakebaker('c', 'C'),
|
||||
cakebaker('d', 'D'),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user