Repository: SeaQL/sea-orm (Rust)

Issue:
Support relation that join on multiple columns
`.from` and `.to` in `RelationBuilder` should take `IntoIdentity` instead of `E::Column`

```rust
impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            Self::Fruit => Entity::has_many(super::fruit::Entity)
                .from(Column::Id)
                .to(super::fruit::Column::CakeId)
                .into(),
        }
    }
}
```

```rust
impl<E, R> RelationBuilder<E, R>
where
    E: EntityTrait,
    R: EntityTrait,
{
    pub(crate) fn new(rel_type: RelationType, from: E, to: R) -> Self {
        Self {
            entities: PhantomData,
            rel_type,
            from_tbl: from.into_iden(),
            to_tbl: to.into_iden(),
            from_col: None,
            to_col: None,
        }
    }

    pub fn from(mut self, identifier: E::Column) -> Self {
        self.from_col = Some(identifier.into_identity());
        self
    }

    pub fn to(mut self, identifier: R::Column) -> Self {
        self.to_col = Some(identifier.into_identity());
        self
    }
}
```


Discussion:
`E::Column` is used because I'd like to have the Column bounded to the Entity.
If we'd support composite join keys, we should create a new generic Trait `IdentityOf<E: EntityTrait>` that only accepts columns of the Entity of interest.

Tasks:
1. [ ] define `IdentityOf`
1. [ ] impl `join*` in `QuerySelect`
1. [ ] prepare `tests_cfg` and test cases



Patch:
diff --git a/src/entity/identity.rs b/src/entity/identity.rs
index af68978c0d..d1cc3170a4 100644
--- a/src/entity/identity.rs
+++ b/src/entity/identity.rs
@@ -1,17 +1,24 @@
-use crate::IdenStatic;
+use crate::{ColumnTrait, EntityTrait, IdenStatic};
 use sea_query::{DynIden, IntoIden};
 
 #[derive(Debug, Clone)]
 pub enum Identity {
     Unary(DynIden),
     Binary(DynIden, DynIden),
-    // Ternary(DynIden, DynIden, DynIden),
+    Ternary(DynIden, DynIden, DynIden),
 }
 
 pub trait IntoIdentity {
     fn into_identity(self) -> Identity;
 }
 
+pub trait IdentityOf<E>
+where
+    E: EntityTrait,
+{
+    fn identity_of(self) -> Identity;
+}
+
 impl<T> IntoIdentity for T
 where
     T: IdenStatic,
@@ -30,3 +37,44 @@ where
         Identity::Binary(self.0.into_iden(), self.1.into_iden())
     }
 }
+
+impl<T, C, R> IntoIdentity for (T, C, R)
+where
+    T: IdenStatic,
+    C: IdenStatic,
+    R: IdenStatic,
+{
+    fn into_identity(self) -> Identity {
+        Identity::Ternary(self.0.into_iden(), self.1.into_iden(), self.2.into_iden())
+    }
+}
+
+impl<E, C> IdentityOf<E> for C
+where
+    E: EntityTrait<Column = C>,
+    C: ColumnTrait,
+{
+    fn identity_of(self) -> Identity {
+        self.into_identity()
+    }
+}
+
+impl<E, C> IdentityOf<E> for (C, C)
+where
+    E: EntityTrait<Column = C>,
+    C: ColumnTrait,
+{
+    fn identity_of(self) -> Identity {
+        self.into_identity()
+    }
+}
+
+impl<E, C> IdentityOf<E> for (C, C, C)
+where
+    E: EntityTrait<Column = C>,
+    C: ColumnTrait,
+{
+    fn identity_of(self) -> Identity {
+        self.into_identity()
+    }
+}
diff --git a/src/entity/relation.rs b/src/entity/relation.rs
index 7c9213d367..7d2059e2a0 100644
--- a/src/entity/relation.rs
+++ b/src/entity/relation.rs
@@ -1,4 +1,4 @@
-use crate::{EntityTrait, Identity, IntoIdentity, Iterable, QuerySelect, Select};
+use crate::{EntityTrait, Identity, IdentityOf, Iterable, QuerySelect, Select};
 use core::marker::PhantomData;
 use sea_query::{DynIden, IntoIden, JoinType};
 use std::fmt::Debug;
@@ -89,13 +89,19 @@ where
         }
     }
 
-    pub fn from(mut self, identifier: E::Column) -> Self {
-        self.from_col = Some(identifier.into_identity());
+    pub fn from<T>(mut self, identifier: T) -> Self
+    where
+        T: IdentityOf<E>,
+    {
+        self.from_col = Some(identifier.identity_of());
         self
     }
 
-    pub fn to(mut self, identifier: R::Column) -> Self {
-        self.to_col = Some(identifier.into_identity());
+    pub fn to<T>(mut self, identifier: T) -> Self
+    where
+        T: IdentityOf<R>,
+    {
+        self.to_col = Some(identifier.identity_of());
         self
     }
 }
diff --git a/src/query/helper.rs b/src/query/helper.rs
index 78eaa4e759..9691e1abed 100644
--- a/src/query/helper.rs
+++ b/src/query/helper.rs
@@ -283,6 +283,12 @@ fn join_condition(rel: RelationDef) -> SimpleExpr {
                 .equals(SeaRc::clone(&to_tbl), f1)
                 .and(Expr::tbl(SeaRc::clone(&from_tbl), o2).equals(SeaRc::clone(&to_tbl), f2))
         }
+        (Identity::Ternary(o1, o2, o3), Identity::Ternary(f1, f2, f3)) => {
+            Expr::tbl(SeaRc::clone(&from_tbl), o1)
+                .equals(SeaRc::clone(&to_tbl), f1)
+                .and(Expr::tbl(SeaRc::clone(&from_tbl), o2).equals(SeaRc::clone(&to_tbl), f2))
+                .and(Expr::tbl(SeaRc::clone(&from_tbl), o3).equals(SeaRc::clone(&to_tbl), f3))
+        }
         _ => panic!("Owner key and foreign key mismatch"),
     }
 }
diff --git a/src/query/join.rs b/src/query/join.rs
index 61c6c1b916..d6e003015b 100644
--- a/src/query/join.rs
+++ b/src/query/join.rs
@@ -61,7 +61,7 @@ where
 
 #[cfg(test)]
 mod tests {
-    use crate::tests_cfg::{cake, filling, fruit};
+    use crate::tests_cfg::{cake, cake_filling, cake_filling_price, filling, fruit};
     use crate::{ColumnTrait, DbBackend, EntityTrait, ModelTrait, QueryFilter, QueryTrait};
 
     #[test]
@@ -182,4 +182,23 @@ mod tests {
             .join(" ")
         );
     }
+
+    #[test]
+    fn join_8() {
+        use crate::{Related, Select};
+
+        let find_cake_filling_price: Select<cake_filling_price::Entity> =
+            cake_filling::Entity::find_related();
+        assert_eq!(
+            find_cake_filling_price.build(DbBackend::MySql).to_string(),
+            [
+                "SELECT `cake_filling_price`.`cake_id`, `cake_filling_price`.`filling_id`, `cake_filling_price`.`price`",
+                "FROM `cake_filling_price`",
+                "INNER JOIN `cake_filling` ON",
+                "(`cake_filling`.`cake_id` = `cake_filling_price`.`cake_id`) AND",
+                "(`cake_filling`.`filling_id` = `cake_filling_price`.`filling_id`)",
+            ]
+            .join(" ")
+        );
+    }
 }


Test patch:
diff --git a/src/tests_cfg/cake_filling.rs b/src/tests_cfg/cake_filling.rs
index 6b5c20aa4a..b1151ee44d 100644
--- a/src/tests_cfg/cake_filling.rs
+++ b/src/tests_cfg/cake_filling.rs
@@ -66,4 +66,10 @@ impl RelationTrait for Relation {
     }
 }
 
+impl Related<super::cake_filling_price::Entity> for Entity {
+    fn to() -> RelationDef {
+        super::cake_filling_price::Relation::CakeFilling.def().rev()
+    }
+}
+
 impl ActiveModelBehavior for ActiveModel {}
diff --git a/src/tests_cfg/cake_filling_price.rs b/src/tests_cfg/cake_filling_price.rs
new file mode 100644
index 0000000000..eae078718f
--- /dev/null
+++ b/src/tests_cfg/cake_filling_price.rs
@@ -0,0 +1,76 @@
+use crate as sea_orm;
+use crate::entity::prelude::*;
+
+#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
+pub struct Entity;
+
+impl EntityName for Entity {
+    fn table_name(&self) -> &str {
+        "cake_filling_price"
+    }
+}
+
+#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
+pub struct Model {
+    pub cake_id: i32,
+    pub filling_id: i32,
+    pub price: Decimal,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
+pub enum Column {
+    CakeId,
+    FillingId,
+    Price,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
+pub enum PrimaryKey {
+    CakeId,
+    FillingId,
+}
+
+impl PrimaryKeyTrait for PrimaryKey {
+    fn auto_increment() -> bool {
+        false
+    }
+}
+
+#[derive(Copy, Clone, Debug, EnumIter)]
+pub enum Relation {
+    CakeFilling,
+}
+
+impl ColumnTrait for Column {
+    type EntityName = Entity;
+
+    fn def(&self) -> ColumnDef {
+        match self {
+            Self::CakeId => ColumnType::Integer.def(),
+            Self::FillingId => ColumnType::Integer.def(),
+            Self::Price => ColumnType::Decimal(None).def(),
+        }
+    }
+}
+
+impl RelationTrait for Relation {
+    fn def(&self) -> RelationDef {
+        match self {
+            Self::CakeFilling => Entity::belongs_to(super::cake_filling::Entity)
+                .from((Column::CakeId, Column::FillingId))
+                .to((
+                    super::cake_filling::Column::CakeId,
+                    super::cake_filling::Column::FillingId,
+                ))
+                .into(),
+        }
+    }
+}
+
+impl Related<super::cake_filling::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::CakeFilling.def()
+    }
+}
+
+impl ActiveModelBehavior for ActiveModel {}
diff --git a/src/tests_cfg/mod.rs b/src/tests_cfg/mod.rs
index ca9b8a6838..afb9e1150b 100644
--- a/src/tests_cfg/mod.rs
+++ b/src/tests_cfg/mod.rs
@@ -2,10 +2,12 @@
 
 pub mod cake;
 pub mod cake_filling;
+pub mod cake_filling_price;
 pub mod filling;
 pub mod fruit;
 
 pub use cake::Entity as Cake;
 pub use cake_filling::Entity as CakeFilling;
+pub use cake_filling_price::Entity as CakeFillingPrice;
 pub use filling::Entity as Filling;
 pub use fruit::Entity as Fruit;