Generate entity for duplicated many-to-many paths (#1435)

* Generate entity for duplicated many-to-many paths

* CI: run cargo fmt with nightly channel
This commit is contained in:
Billy Chan 2023-04-13 11:16:49 +08:00 committed by GitHub
parent 8da8c0a444
commit 2c082558df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 739 additions and 58 deletions

View File

@ -250,11 +250,15 @@ jobs:
with:
toolchain: stable
components: rustfmt
- run: cargo fmt --manifest-path ${{ matrix.path }} --all -- --check
- run: cargo build --manifest-path ${{ matrix.path }}
- run: cargo test --manifest-path ${{ matrix.path }}
- if: ${{ contains(matrix.path, 'core/Cargo.toml') }}
run: cargo test --manifest-path ${{ matrix.path }} --features mock
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly
components: rustfmt
- run: cargo fmt --manifest-path ${{ matrix.path }} --all -- --check
issues-matrix:
name: Issues Matrix

View File

@ -26,3 +26,4 @@ tracing = { version = "0.1", default-features = false, features = ["log"] }
[dev-dependencies]
pretty_assertions = { version = "0.7" }
sea-orm = { path = "../", default-features = false, features = ["macros"] }

View File

@ -1,4 +1,5 @@
ignore = [
"tests/compact/*.rs",
"tests/expanded/*.rs",
"tests_cfg",
]

View File

@ -3,7 +3,7 @@ use crate::{
PrimaryKey, Relation, RelationType,
};
use sea_query::{ColumnSpec, TableCreateStatement};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
#[derive(Clone, Debug)]
pub struct EntityTransformer;
@ -12,7 +12,6 @@ impl EntityTransformer {
pub fn transform(table_create_stmts: Vec<TableCreateStatement>) -> Result<EntityWriter, Error> {
let mut enums: BTreeMap<String, ActiveEnum> = BTreeMap::new();
let mut inverse_relations: BTreeMap<String, Vec<Relation>> = BTreeMap::new();
let mut conjunct_relations: BTreeMap<String, Vec<ConjunctRelation>> = BTreeMap::new();
let mut entities = BTreeMap::new();
for table_create in table_create_stmts.into_iter() {
let table_name = match table_create.get_table_name() {
@ -125,7 +124,7 @@ impl EntityTransformer {
primary_keys,
};
entities.insert(table_name.clone(), entity.clone());
for (i, mut rel) in relations.into_iter().enumerate() {
for mut rel in relations.into_iter() {
// This will produce a duplicated relation
if rel.self_referencing {
continue;
@ -135,22 +134,6 @@ impl EntityTransformer {
if rel.num_suffix > 0 {
continue;
}
let is_conjunct_relation =
entity.relations.len() == 2 && entity.primary_keys.len() == 2;
match is_conjunct_relation {
true => {
let another_rel = entity.relations.get((i == 0) as usize).unwrap();
let conjunct_relation = ConjunctRelation {
via: table_name.clone(),
to: another_rel.ref_table.clone(),
};
if let Some(vec) = conjunct_relations.get_mut(&rel.ref_table) {
vec.push(conjunct_relation);
} else {
conjunct_relations.insert(rel.ref_table, vec![conjunct_relation]);
}
}
false => {
let ref_table = rel.ref_table;
let mut unique = true;
for column in rel.columns.iter() {
@ -180,8 +163,6 @@ impl EntityTransformer {
}
}
}
}
}
for (tbl_name, relations) in inverse_relations.into_iter() {
if let Some(entity) = entities.get_mut(&tbl_name) {
for relation in relations.into_iter() {
@ -195,26 +176,64 @@ impl EntityTransformer {
}
}
}
for (tbl_name, mut conjunct_relations) in conjunct_relations.into_iter() {
if let Some(entity) = entities.get_mut(&tbl_name) {
for relation in entity.relations.iter_mut() {
// Skip `impl Related ... { fn to() ... }` implementation block,
// if the same related entity is being referenced by a conjunct relation
if conjunct_relations
.iter()
.any(|conjunct_relation| conjunct_relation.to == relation.ref_table)
{
relation.impl_related = false;
for table_name in entities.clone().keys() {
let relations = match entities.get(table_name) {
Some(entity) => {
let is_conjunct_relation =
entity.relations.len() == 2 && entity.primary_keys.len() == 2;
if !is_conjunct_relation {
continue;
}
entity.relations.clone()
}
None => unreachable!(),
};
for (i, rel) in relations.iter().enumerate() {
let another_rel = relations.get((i == 0) as usize).unwrap();
if let Some(entity) = entities.get_mut(&rel.ref_table) {
let conjunct_relation = ConjunctRelation {
via: table_name.clone(),
to: another_rel.ref_table.clone(),
};
entity.conjunct_relations.push(conjunct_relation);
}
entity.conjunct_relations.append(&mut conjunct_relations);
}
}
Ok(EntityWriter {
entities: entities
.into_values()
.map(|mut v| {
// Filter duplicated conjunct relations
let duplicated_to: Vec<_> = v
.conjunct_relations
.iter()
.fold(HashMap::new(), |mut acc, conjunct_relation| {
acc.entry(conjunct_relation.to.clone())
.and_modify(|c| *c += 1)
.or_insert(1);
acc
})
.into_iter()
.filter(|(_, v)| v > &1)
.map(|(k, _)| k)
.collect();
v.conjunct_relations
.retain(|conjunct_relation| !duplicated_to.contains(&conjunct_relation.to));
// Skip `impl Related ... { fn to() ... }` implementation block,
// if the same related entity is being referenced by a conjunct relation
v.relations.iter_mut().for_each(|relation| {
if v.conjunct_relations
.iter()
.any(|conjunct_relation| conjunct_relation.to == relation.ref_table)
{
relation.impl_related = false;
}
});
// Sort relation vectors
v.relations.sort_by(|a, b| a.ref_table.cmp(&b.ref_table));
v.conjunct_relations.sort_by(|a, b| a.to.cmp(&b.to));
v
})
.collect(),
@ -222,3 +241,179 @@ impl EntityTransformer {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use proc_macro2::TokenStream;
use sea_orm::{DbBackend, Schema};
use std::{
error::Error,
io::{self, BufRead, BufReader},
};
#[test]
fn duplicated_many_to_many_paths() -> Result<(), Box<dyn Error>> {
use crate::tests_cfg::duplicated_many_to_many_paths::*;
let schema = Schema::new(DbBackend::Postgres);
validate_compact_entities(
vec![
schema.create_table_from_entity(bills::Entity),
schema.create_table_from_entity(users::Entity),
schema.create_table_from_entity(users_saved_bills::Entity),
schema.create_table_from_entity(users_votes::Entity),
],
vec![
(
"bills",
include_str!("../tests_cfg/duplicated_many_to_many_paths/bills.rs"),
),
(
"users",
include_str!("../tests_cfg/duplicated_many_to_many_paths/users.rs"),
),
(
"users_saved_bills",
include_str!("../tests_cfg/duplicated_many_to_many_paths/users_saved_bills.rs"),
),
(
"users_votes",
include_str!("../tests_cfg/duplicated_many_to_many_paths/users_votes.rs"),
),
],
)
}
#[test]
fn many_to_many() -> Result<(), Box<dyn Error>> {
use crate::tests_cfg::many_to_many::*;
let schema = Schema::new(DbBackend::Postgres);
validate_compact_entities(
vec![
schema.create_table_from_entity(bills::Entity),
schema.create_table_from_entity(users::Entity),
schema.create_table_from_entity(users_votes::Entity),
],
vec![
("bills", include_str!("../tests_cfg/many_to_many/bills.rs")),
("users", include_str!("../tests_cfg/many_to_many/users.rs")),
(
"users_votes",
include_str!("../tests_cfg/many_to_many/users_votes.rs"),
),
],
)
}
#[test]
fn many_to_many_multiple() -> Result<(), Box<dyn Error>> {
use crate::tests_cfg::many_to_many_multiple::*;
let schema = Schema::new(DbBackend::Postgres);
validate_compact_entities(
vec![
schema.create_table_from_entity(bills::Entity),
schema.create_table_from_entity(users::Entity),
schema.create_table_from_entity(users_votes::Entity),
],
vec![
(
"bills",
include_str!("../tests_cfg/many_to_many_multiple/bills.rs"),
),
(
"users",
include_str!("../tests_cfg/many_to_many_multiple/users.rs"),
),
(
"users_votes",
include_str!("../tests_cfg/many_to_many_multiple/users_votes.rs"),
),
],
)
}
#[test]
fn self_referencing() -> Result<(), Box<dyn Error>> {
use crate::tests_cfg::self_referencing::*;
let schema = Schema::new(DbBackend::Postgres);
validate_compact_entities(
vec![
schema.create_table_from_entity(bills::Entity),
schema.create_table_from_entity(users::Entity),
],
vec![
(
"bills",
include_str!("../tests_cfg/self_referencing/bills.rs"),
),
(
"users",
include_str!("../tests_cfg/self_referencing/users.rs"),
),
],
)
}
fn validate_compact_entities(
table_create_stmts: Vec<TableCreateStatement>,
files: Vec<(&str, &str)>,
) -> Result<(), Box<dyn Error>> {
let entities: HashMap<_, _> = EntityTransformer::transform(table_create_stmts)?
.entities
.into_iter()
.map(|entity| (entity.table_name.clone(), entity))
.collect();
for (entity_name, file_content) in files {
let entity = entities
.get(entity_name)
.expect("Forget to add entity to the list");
assert_eq!(
parse_from_file(file_content.as_bytes())?.to_string(),
EntityWriter::gen_compact_code_blocks(
entity,
&crate::WithSerde::None,
&crate::DateTimeCrate::Chrono,
&None,
false,
false,
&Default::default(),
&Default::default(),
)
.into_iter()
.skip(1)
.fold(TokenStream::new(), |mut acc, tok| {
acc.extend(tok);
acc
})
.to_string()
);
}
Ok(())
}
fn parse_from_file<R>(inner: R) -> io::Result<TokenStream>
where
R: io::Read,
{
let mut reader = BufReader::new(inner);
let mut lines: Vec<String> = Vec::new();
reader.read_until(b';', &mut Vec::new())?;
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
lines.push(line.to_owned());
line.clear();
}
let content = lines.join("");
Ok(content.parse().unwrap())
}
}

View File

@ -4,3 +4,6 @@ mod util;
pub use entity::*;
pub use error::*;
#[cfg(test)]
mod tests_cfg;

View File

@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "bills")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "NoAction",
on_delete = "NoAction",
)]
Users,
#[sea_orm(has_many = "super::users_saved_bills::Entity")]
UsersSavedBills,
#[sea_orm(has_many = "super::users_votes::Entity")]
UsersVotes,
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl Related<super::users_saved_bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersSavedBills.def()
}
}
impl Related<super::users_votes::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersVotes.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,6 @@
pub mod prelude;
pub mod bills;
pub mod users;
pub mod users_saved_bills;
pub mod users_votes;

View File

@ -0,0 +1,4 @@
pub use super::bills::Entity as Bills;
pub use super::users::Entity as Users;
pub use super::users_saved_bills::Entity as UsersSavedBills;
pub use super::users_votes::Entity as UsersVotes;

View File

@ -0,0 +1,40 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text")]
pub email: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::bills::Entity")]
Bills,
#[sea_orm(has_many = "super::users_saved_bills::Entity")]
UsersSavedBills,
#[sea_orm(has_many = "super::users_votes::Entity")]
UsersVotes,
}
impl Related<super::bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bills.def()
}
}
impl Related<super::users_saved_bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersSavedBills.def()
}
}
impl Related<super::users_votes::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersVotes.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,44 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users_saved_bills")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub bill_id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::bills::Entity",
from = "Column::BillId",
to = "super::bills::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Bills,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Users,
}
impl Related<super::bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bills.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users_votes")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub bill_id: i32,
pub vote: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::bills::Entity",
from = "Column::BillId",
to = "super::bills::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Bills,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Users,
}
impl Related<super::bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bills.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,41 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "bills")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "NoAction",
on_delete = "NoAction",
)]
Users,
#[sea_orm(has_many = "super::users_votes::Entity")]
UsersVotes,
}
impl Related<super::users_votes::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersVotes.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
super::users_votes::Relation::Users.def()
}
fn via() -> Option<RelationDef> {
Some(super::users_votes::Relation::Bills.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,5 @@
pub mod prelude;
pub mod bills;
pub mod users;
pub mod users_votes;

View File

@ -0,0 +1,3 @@
pub use super::bills::Entity as Bills;
pub use super::users::Entity as Users;
pub use super::users_votes::Entity as UsersVotes;

View File

@ -0,0 +1,36 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text")]
pub email: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::bills::Entity")]
Bills,
#[sea_orm(has_many = "super::users_votes::Entity")]
UsersVotes,
}
impl Related<super::users_votes::Entity> for Entity {
fn to() -> RelationDef {
Relation::UsersVotes.def()
}
}
impl Related<super::bills::Entity> for Entity {
fn to() -> RelationDef {
super::users_votes::Relation::Bills.def()
}
fn via() -> Option<RelationDef> {
Some(super::users_votes::Relation::Users.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users_votes")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub bill_id: i32,
pub vote: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::bills::Entity",
from = "Column::BillId",
to = "super::bills::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Bills,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "Cascade",
on_delete = "Cascade",
)]
Users,
}
impl Related<super::bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bills.def()
}
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,29 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "bills")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub user_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
on_update = "NoAction",
on_delete = "NoAction",
)]
Users,
}
impl Related<super::users::Entity> for Entity {
fn to() -> RelationDef {
Relation::Users.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,5 @@
pub mod prelude;
pub mod bills;
pub mod users;
pub mod users_votes;

View File

@ -0,0 +1,3 @@
pub use super::bills::Entity as Bills;
pub use super::users::Entity as Users;
pub use super::users_votes::Entity as UsersVotes;

View File

@ -0,0 +1,24 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(column_type = "Text")]
pub email: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::bills::Entity")]
Bills,
}
impl Related<super::bills::Entity> for Entity {
fn to() -> RelationDef {
Relation::Bills.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,43 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users_votes")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub user_id: i32,
#[sea_orm(primary_key, auto_increment = false)]
pub bill_id: i32,
pub user_idd: Option<i32> ,
pub bill_idd: Option<i32> ,
pub vote: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::bills::Entity",
from = "Column::BillIdd",
to = "super::bills::Column::Id",
)]
Bills2,
#[sea_orm(
belongs_to = "super::bills::Entity",
from = "Column::BillId",
to = "super::bills::Column::Id",
)]
Bills1,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserIdd",
to = "super::users::Column::Id",
)]
Users2,
#[sea_orm(
belongs_to = "super::users::Entity",
from = "Column::UserId",
to = "super::users::Column::Id",
)]
Users1,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,4 @@
pub mod duplicated_many_to_many_paths;
pub mod many_to_many;
pub mod many_to_many_multiple;
pub mod self_referencing;

View File

@ -0,0 +1,21 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "bills")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub self_id: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "Entity",
from = "Column::SelfId",
to = "Column::Id",
)]
SelfRef,
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,4 @@
pub mod prelude;
pub mod bills;
pub mod users;

View File

@ -0,0 +1,2 @@
pub use super::bills::Entity as Bills;
pub use super::users::Entity as Users;

View File

@ -0,0 +1,28 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub self_id: Option<i32> ,
pub self_idd: Option<i32> ,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "Entity",
from = "Column::SelfId",
to = "Column::Id",
)]
SelfRef2,
#[sea_orm(
belongs_to = "Entity",
from = "Column::SelfIdd",
to = "Column::Id",
)]
SelfRef1,
}
impl ActiveModelBehavior for ActiveModel {}