diff --git a/sea-orm-cli/src/commands/migrate.rs b/sea-orm-cli/src/commands/migrate.rs index ec2d03af2..63414f36f 100644 --- a/sea-orm-cli/src/commands/migrate.rs +++ b/sea-orm-cli/src/commands/migrate.rs @@ -178,7 +178,7 @@ fn get_full_migration_dir(migration_dir: &str) -> PathBuf { fn create_new_migration(migration_name: &str, migration_dir: &str) -> Result<(), Box> { let migration_filepath = - get_full_migration_dir(migration_dir).join(format!("{}.rs", &migration_name)); + get_full_migration_dir(migration_dir).join(format!("{}.rs", migration_name)); println!("Creating migration file `{}`", migration_filepath.display()); let migration_template = fmt_migration_template(migration_name); diff --git a/sea-orm-macros/src/derives/active_model_ex.rs b/sea-orm-macros/src/derives/active_model_ex.rs index 7367a027b..b095a2836 100644 --- a/sea-orm-macros/src/derives/active_model_ex.rs +++ b/sea-orm-macros/src/derives/active_model_ex.rs @@ -2,51 +2,29 @@ use super::active_model::DeriveActiveModel; use super::attributes::compound_attr; use super::model_ex::infer_relation_name_from_entity; use super::util::{ - CardinalityKind, CompoundKind, CompoundType, async_token, await_token, consume_meta, - escape_rust_keyword, is_self_entity, trim_starting_raw_identifier, + CardinalityKind, CompoundKind, CompoundType, Junction, RelationColumns, async_token, + await_token, consume_meta, escape_rust_keyword, is_self_entity, trim_starting_raw_identifier, }; use heck::ToUpperCamelCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; -use syn::{ - Attribute, Data, LitStr, Path, PathArguments, Type, TypePath, Visibility, parse::Parser, - punctuated::Punctuated, token::Comma, -}; +use syn::{Attribute, Data, LitStr, PathArguments, Type, TypePath, Visibility}; enum RelationAttr { BelongsTo { - cardinality: CardinalityKind, - from: LitStr, - /// Explicit relation disambiguator, present when more than one relation - /// targets the same entity. Routes the FK write through the - /// relation-keyed `*_parent_key_for` helpers instead of the entity-keyed ones. + from: RelationColumns, relation_enum: Option, }, - BelongsToSelf { - relation_enum: LitStr, - cardinality: CardinalityKind, - from: LitStr, - }, - /// Legacy `belongs_to` declared with the `HasOne` field type instead of - /// `BelongsTo`. Backward-compatible path; `BelongsTo` is recommended. - BelongsToLegacy { - /// Explicit relation disambiguator (see `BelongsTo::relation_enum`). - relation_enum: Option, - }, - /// Legacy self-referential `belongs_to` declared with `HasOne`. - BelongsToSelfLegacy { - relation_enum: LitStr, - }, HasOne, HasMany, HasManySelf { relation_enum: LitStr, }, - HasManyVia { - via: LitStr, + ManyToMany { + junction: Junction, }, - HasManyViaSelf { - via: LitStr, + ManyToManySelf { + junction_module: Ident, reverse: bool, }, } @@ -54,8 +32,8 @@ enum RelationAttr { impl RelationAttr { fn from_attr( attrs: &compound_attr::SeaOrm, - ident: &Ident, - compound: &CompoundType, + field_ident: &Ident, + compound_type: &CompoundType, ) -> syn::Result> { match attrs { compound_attr::SeaOrm { @@ -63,14 +41,23 @@ impl RelationAttr { via: Some(via), .. } => { - if compound.kind != CompoundKind::HasMany || !is_self_entity(&compound.entity) { + if compound_type.kind != CompoundKind::HasMany + || !is_self_entity(&compound_type.entity) + { return Err(syn::Error::new_spanned( via, "self_ref + via field type must be `HasMany`", )); } - Ok(Some(Self::HasManyViaSelf { - via: via.clone(), + let junction = Junction::from_lit(via)?; + if junction.relation.is_some() { + return Err(syn::Error::new( + via.span(), + "`self_ref` via must name a junction entity", + )); + } + Ok(Some(Self::ManyToManySelf { + junction_module: junction.module, reverse: attrs.reverse.is_some(), })) } @@ -82,21 +69,19 @@ impl RelationAttr { to: Some(_), .. } => { - match compound.kind { - CompoundKind::BelongsTo(cardinality) => Ok(Some(Self::BelongsToSelf { - relation_enum: relation_enum.clone(), - cardinality, - from: from.clone(), - })), - // Legacy: self-referential belongs_to with a `HasOne` field type. - CompoundKind::HasOne => Ok(Some(Self::BelongsToSelfLegacy { - relation_enum: relation_enum.clone(), - })), - _ => Err(syn::Error::new_spanned( - ident, + if !matches!( + compound_type.kind, + CompoundKind::BelongsTo(_) | CompoundKind::HasOne + ) { + return Err(syn::Error::new_spanned( + field_ident, "self_ref belongs_to must be paired with BelongsTo or HasOne", - )), + )); } + Ok(Some(Self::BelongsTo { + from: RelationColumns::from_lit(from.clone())?, + relation_enum: Some(relation_enum.clone()), + })) } compound_attr::SeaOrm { relation_enum: Some(relation_enum), @@ -107,9 +92,11 @@ impl RelationAttr { to: None, .. } => { - if compound.kind != CompoundKind::HasMany || !is_self_entity(&compound.entity) { + if compound_type.kind != CompoundKind::HasMany + || !is_self_entity(&compound_type.entity) + { return Err(syn::Error::new_spanned( - ident, + field_ident, "self_ref has_many field type must be `HasMany`", )); } @@ -123,7 +110,7 @@ impl RelationAttr { relation_enum: None, .. } => Err(syn::Error::new_spanned( - ident, + field_ident, "Please specify `relation_enum` for `self_ref`", )), compound_attr::SeaOrm { @@ -132,26 +119,23 @@ impl RelationAttr { compound_attr::SeaOrm { belongs_to: Some(_), .. - } => match compound.kind { - CompoundKind::BelongsTo(cardinality) => Ok(Some(Self::BelongsTo { - cardinality, - from: attrs.from.clone().ok_or_else(|| { - syn::Error::new_spanned(ident, "belongs_to must specify `from`") - })?, - // Captured so multiple belongs_to targeting the same entity can be - // disambiguated via their relation enum on save. - relation_enum: attrs.relation_enum.clone(), - })), - // Legacy: belongs_to declared with a `HasOne` field type instead of - // `BelongsTo`. Backward compatible; `BelongsTo` is recommended. - CompoundKind::HasOne => Ok(Some(Self::BelongsToLegacy { + } => { + if !matches!( + compound_type.kind, + CompoundKind::BelongsTo(_) | CompoundKind::HasOne + ) { + return Err(syn::Error::new_spanned( + field_ident, + "belongs_to must be paired with BelongsTo or HasOne", + )); + } + Ok(Some(Self::BelongsTo { + from: RelationColumns::from_lit(attrs.from.clone().ok_or_else(|| { + syn::Error::new_spanned(field_ident, "belongs_to must specify `from`") + })?)?, relation_enum: attrs.relation_enum.clone(), - })), - _ => Err(syn::Error::new_spanned( - ident, - "belongs_to must be paired with BelongsTo or HasOne", - )), - }, + })) + } compound_attr::SeaOrm { relation_enum: Some(_), .. @@ -159,9 +143,9 @@ impl RelationAttr { compound_attr::SeaOrm { has_one: Some(_), .. } => { - if compound.kind != CompoundKind::HasOne { + if compound_type.kind != CompoundKind::HasOne { return Err(syn::Error::new_spanned( - ident, + field_ident, "#[sea_orm(has_one)] must be paired with HasOne", )); } @@ -172,9 +156,9 @@ impl RelationAttr { via: None, .. } => { - if compound.kind != CompoundKind::HasMany { + if compound_type.kind != CompoundKind::HasMany { return Err(syn::Error::new_spanned( - ident, + field_ident, "has_many must be paired with HasMany", )); } @@ -185,13 +169,15 @@ impl RelationAttr { via: Some(via), .. } => { - if compound.kind != CompoundKind::HasMany { + if compound_type.kind != CompoundKind::HasMany { return Err(syn::Error::new_spanned( - ident, + field_ident, "has_many via must be paired with HasMany", )); } - Ok(Some(Self::HasManyVia { via: via.clone() })) + Ok(Some(Self::ManyToMany { + junction: Junction::from_lit(via)?, + })) } _ => Ok(None), } @@ -228,6 +214,16 @@ impl<'a> ScalarField<'a> { column, }) } + + fn is_option(&self) -> bool { + if let Type::Path(type_path) = self.ty + && let Some(segment) = type_path.path.segments.last() + { + segment.ident == "Option" + } else { + false + } + } } #[derive(Clone, Copy)] @@ -237,12 +233,12 @@ enum FieldParseMode { } struct CompoundField { - compound: CompoundType, + compound_type: CompoundType, } struct RelationField { - compound: CompoundType, - relation: RelationAttr, + compound_type: CompoundType, + attr: RelationAttr, } enum FieldKind<'a> { @@ -281,20 +277,23 @@ impl<'a> Field<'a> { let kind = if ignored { FieldKind::Ignored } else if let Type::Path(type_path) = &field.ty - && let Some(compound) = CompoundType::from_type(type_path)? + && let Some(compound_type) = CompoundType::from_type(type_path)? { - let relation = match mode { + let relation_attr = match mode { FieldParseMode::Compact => None, FieldParseMode::Dense => { let attrs = compound_attr::SeaOrm::try_from_attributes(&field.attrs)? .unwrap_or_default(); - RelationAttr::from_attr(&attrs, ident, &compound)? + RelationAttr::from_attr(&attrs, ident, &compound_type)? } }; - if let Some(relation) = relation { - FieldKind::Relation(RelationField { compound, relation }) + if let Some(attr) = relation_attr { + FieldKind::Relation(RelationField { + compound_type, + attr, + }) } else { - FieldKind::Compound(CompoundField { compound }) + FieldKind::Compound(CompoundField { compound_type }) } } else { FieldKind::Scalar(ScalarField::from_field(field, ident)?) @@ -302,7 +301,7 @@ impl<'a> Field<'a> { Ok(Self { ident, kind }) } - fn expand_into(&'a self, fields: &'a Fields<'a>, output: &mut Output<'a>) -> syn::Result<()> { + fn expand_into(&'a self, output: &mut Output<'a>) { let ident = self.ident; match &self.kind { FieldKind::Ignored => { @@ -314,33 +313,19 @@ impl<'a> Field<'a> { #[doc = " Generated by sea-orm-macros"] pub #ident: sea_orm::ActiveValue<#field_type> }); - output.active_model_setters.extend( - ActiveModelSetter { - field: self, - fields, - } - .expand()?, - ); output.scalar_fields.push(ident); } - FieldKind::Compound(CompoundField { compound }) - | FieldKind::Relation(RelationField { compound, .. }) => { - self.expand_compound_into(fields, output, compound)?; + FieldKind::Compound(CompoundField { compound_type }) + | FieldKind::Relation(RelationField { compound_type, .. }) => { + self.expand_compound_into(output, compound_type); } } - - Ok(()) } - fn expand_compound_into( - &'a self, - fields: &'a Fields<'a>, - output: &mut Output<'a>, - compound: &CompoundType, - ) -> syn::Result<()> { + fn expand_compound_into(&'a self, output: &mut Output<'a>, compound_type: &CompoundType) { let ident = self.ident; - let entity_type = &compound.entity; - let field_type = match compound.kind { + let entity_type = &compound_type.entity; + let field_type = match compound_type.kind { CompoundKind::BelongsTo(cardinality) => { let target_type = match cardinality { CardinalityKind::Required => quote!(#entity_type), @@ -355,16 +340,7 @@ impl<'a> Field<'a> { #[doc = " Generated by sea-orm-macros"] pub #ident: #field_type }); - output.active_model_setters.extend( - ActiveModelSetter { - field: self, - fields, - } - .expand()?, - ); output.compound_fields.push(ident); - - Ok(()) } } @@ -390,69 +366,46 @@ impl<'a> Fields<'a> { Ok(Self(fields)) } - /// Return the fields corresponding to the `from` columns of a relation. - /// For example: - /// #[sea_orm(belogns_to, from = "cake_id")], #[sea_orm(belongs_to, from = "Column::CakeId")] will return the field corresponding to `cake_id`. - /// #[sea_orm(belongs_to, from = "(user_id, post_id)")] will return the fields corresponding to `user_id` and `post_id`. - fn get_nullable_from_fields(&'a self, columns_lit: &LitStr) -> syn::Result> { - let columns = if columns_lit.value().starts_with('(') { - let parser = Punctuated::::parse_terminated; - columns_lit.parse_with(|input: syn::parse::ParseStream<'_>| { - let content; - syn::parenthesized!(content in input); - parser.parse2(content.parse()?) - })? - } else { - let mut columns = Punctuated::new(); - columns.push(columns_lit.parse()?); - columns - }; - - columns.iter().try_fold(Vec::new(), |mut fields, column| { - let Some(column) = column.segments.last() else { - return Err(syn::Error::new_spanned(column, "expected column path")); - }; - let column = Ident::new( - &escape_rust_keyword(column.ident.to_string().to_upper_camel_case()), - column.ident.span(), - ); - let (field, scalar) = self - .0 - .iter() - .find_map(|field| { - if let FieldKind::Scalar(scalar) = &field.kind - && scalar.column == column - { - Some((field.ident, scalar)) - } else { - None - } - }) - .ok_or_else(|| { - syn::Error::new( - columns_lit.span(), - format!("unknown `from` column `{column}`"), - ) - })?; - if let Type::Path(type_path) = scalar.ty - && type_path - .path - .segments - .last() - .is_some_and(|segment| segment.ident == "Option") - { - fields.push(field); - } - Ok(fields) - }) + fn optional_fields_for_columns( + &'a self, + columns: &RelationColumns, + ) -> syn::Result> { + columns + .columns + .iter() + .try_fold(Vec::new(), |mut optional_fields, column| { + let (ident, scalar) = self + .0 + .iter() + .find_map(|field| { + if let FieldKind::Scalar(scalar) = &field.kind + && scalar.column == *column + { + Some((field.ident, scalar)) + } else { + None + } + }) + .ok_or_else(|| { + syn::Error::new(columns.span, format!("unknown `from` column `{column}`")) + })?; + if scalar.is_option() { + optional_fields.push(ident); + } + Ok(optional_fields) + }) } - fn entity_count(&self, entity: &TypePath) -> usize { + fn relation_target_count(&self, entity: &TypePath) -> usize { self.0 .iter() .filter(|field| match &field.kind { - FieldKind::Compound(compound_field) => &compound_field.compound.entity == entity, - FieldKind::Relation(relation_field) => &relation_field.compound.entity == entity, + FieldKind::Compound(compound_field) => { + &compound_field.compound_type.entity == entity + } + FieldKind::Relation(relation_field) => { + &relation_field.compound_type.entity == entity + } _ => false, }) .count() @@ -482,21 +435,17 @@ impl<'a> ActiveModelSetter<'a> { }) } FieldKind::Compound(compound_field) => { - self.expand_compound(&compound_field.compound, None) + self.expand_compound(&compound_field.compound_type) } FieldKind::Relation(relation_field) => { - self.expand_compound(&relation_field.compound, Some(&relation_field.relation)) + self.expand_compound(&relation_field.compound_type) } } } - fn expand_compound( - &self, - compound: &CompoundType, - relation: Option<&RelationAttr>, - ) -> syn::Result { + fn expand_compound(&self, compound_type: &CompoundType) -> syn::Result { let field_ident = self.field.ident; - let entity_path = &compound.entity; + let entity_path = &compound_type.entity; let mut active_model_type = entity_path.path.clone(); let Some(segment) = active_model_type.segments.last_mut() else { return Err(syn::Error::new_spanned(entity_path, "expected entity path")); @@ -504,9 +453,9 @@ impl<'a> ActiveModelSetter<'a> { segment.ident = format_ident!("ActiveModelEx"); segment.arguments = PathArguments::None; - match compound.kind { + match compound_type.kind { CompoundKind::BelongsTo(cardinality) => { - let (target_entity, optional) = match cardinality { + let (target_entity, maybe_some) = match cardinality { CardinalityKind::Required => (quote!(#entity_path), quote!()), CardinalityKind::Optional => (quote!(Option<#entity_path>), quote!(Some)), }; @@ -514,13 +463,22 @@ impl<'a> ActiveModelSetter<'a> { let optional_setters = if matches!(cardinality, CardinalityKind::Optional) { let optional_setter = format_ident!("set_{}_option", field_ident); let clear_method = format_ident!("clear_{}", field_ident); - let clear_parent_keys = self.clear_parent_key_setters(relation)?; + let optional_foreign_key_fields = match &self.field.kind { + FieldKind::Relation(RelationField { + attr: RelationAttr::BelongsTo { from, .. }, + .. + }) => self.fields.optional_fields_for_columns(from)?, + _ => Vec::new(), + }; + let clear_foreign_keys = quote! { + #(self.#optional_foreign_key_fields = sea_orm::Set(None);)* + }; quote! { #[doc = " Generated by sea-orm-macros"] pub fn #optional_setter(mut self, v: Option>) -> Self { if v.is_none() { - #clear_parent_keys + #clear_foreign_keys } self.#field_ident = sea_orm::ActiveBelongsTo::<#target_entity>::set(v); self @@ -528,7 +486,7 @@ impl<'a> ActiveModelSetter<'a> { #[doc = " Generated by sea-orm-macros"] pub fn #clear_method(mut self) -> Self { - #clear_parent_keys + #clear_foreign_keys self.#field_ident = sea_orm::ActiveBelongsTo::Set(None); self } @@ -540,7 +498,7 @@ impl<'a> ActiveModelSetter<'a> { Ok(quote! { #[doc = " Generated by sea-orm-macros"] pub fn #setter(mut self, v: impl Into<#active_model_type>) -> Self { - self.#field_ident = sea_orm::ActiveBelongsTo::<#target_entity>::set(#optional(v.into())); + self.#field_ident = sea_orm::ActiveBelongsTo::<#target_entity>::set(#maybe_some(v.into())); self } @@ -589,34 +547,21 @@ impl<'a> ActiveModelSetter<'a> { } } } +} - fn clear_parent_key_setters( - &self, - relation: Option<&RelationAttr>, - ) -> syn::Result { - let from = match relation { - Some( - RelationAttr::BelongsTo { from, .. } | RelationAttr::BelongsToSelf { from, .. }, - ) => from, - _ => return Ok(quote!()), - }; - let nullable_from_fields = self.fields.get_nullable_from_fields(from)?; - - Ok(quote! { - #(self.#nullable_from_fields = sea_orm::Set(None);)* - }) - } +// Resolves the belongs-to `RelationDef` used for FK assignment via +// `Related` or a specific `Relation` variant. +enum RelationLookup { + ByRelatedEntity, + ByRelationVariant(Ident), } enum Relation<'a> { BelongsTo(BelongsToField<'a>), - BelongsToSelf(BelongsToSelfField<'a>), - BelongsToLegacy(LegacyBelongsToField<'a>), HasOne(HasOneField<'a>), HasMany(HasManyField<'a>), - HasManySelf(SelfRelationField<'a>), - HasManyVia(ViaField<'a>), - HasManyViaSelf(SelfViaField<'a>), + HasManySelf(HasManySelfField<'a>), + ManyToMany(ManyToManyField<'a>), } #[derive(Default)] @@ -629,14 +574,14 @@ struct ActiveModelActionTokens { has_many_before_action: TokenStream, has_many_action: TokenStream, has_many_delete: TokenStream, - has_many_via_before_action: TokenStream, - has_many_via_action: TokenStream, - has_many_via_delete: TokenStream, + many_to_many_before_action: TokenStream, + many_to_many_action: TokenStream, + many_to_many_delete: TokenStream, } impl ActiveModelActionTokens { fn from_fields(fields: &Fields<'_>) -> syn::Result { - let relations = fields + fields .0 .iter() .filter_map(|field| { @@ -645,125 +590,87 @@ impl ActiveModelActionTokens { }; Some((field.ident, relation_field)) }) - .try_fold(Vec::new(), |mut relations, (ident, relation_field)| { - let compound = &relation_field.compound; - let relation_attr = &relation_field.relation; - let is_unique_entity = fields.entity_count(&compound.entity) == 1; + .try_fold(Self::default(), |mut output, (ident, relation_field)| { + let compound_type = &relation_field.compound_type; + let relation_attr = &relation_field.attr; + let is_unique_relation_target = + fields.relation_target_count(&compound_type.entity) == 1; let relation = match relation_attr { - RelationAttr::BelongsToSelf { - relation_enum, - cardinality, - from, - } => Some(Relation::BelongsToSelf(BelongsToSelfField { - ident, - relation_enum: relation_enum.clone(), - cardinality: *cardinality, - nullable_from_fields: fields.get_nullable_from_fields(from)?, - })), RelationAttr::HasManySelf { relation_enum } => { - Some(Relation::HasManySelf(SelfRelationField { + Some(Relation::HasManySelf(HasManySelfField { ident, - relation_enum: relation_enum.clone(), + relation_variant: relation_enum.clone(), })) } RelationAttr::BelongsTo { - cardinality, - relation_enum, from, + relation_enum, } => { // Always generate. Pick the FK-write path: an explicit // relation_enum, or a non-unique target, is keyed by relation // variant (there is no canonical `Related` to key by); // a unique target with no explicit enum uses the entity-keyed path. - let relation_variant = match (relation_enum.as_ref(), is_unique_entity) { - (Some(relation_enum), _) => Some(Ident::new( + let relation_lookup = match relation_enum { + Some(relation_enum) => RelationLookup::ByRelationVariant(Ident::new( &relation_enum.value().to_upper_camel_case(), relation_enum.span(), )), - (None, false) => Some(Ident::new( - &infer_relation_name_from_entity(&compound.entity) - .to_upper_camel_case(), - Span::call_site(), - )), - (None, true) => None, + None => { + if is_unique_relation_target { + RelationLookup::ByRelatedEntity + } else { + RelationLookup::ByRelationVariant(Ident::new( + &infer_relation_name_from_entity(&compound_type.entity) + .to_upper_camel_case(), + Span::call_site(), + )) + } + } }; Some(Relation::BelongsTo(BelongsToField { ident, - entity: &compound.entity, - cardinality: *cardinality, - relation_enum: relation_variant, - nullable_from_fields: fields.get_nullable_from_fields(from)?, + compound_type, + relation_lookup, + from, + fields, })) } - RelationAttr::BelongsToLegacy { relation_enum } => { - // Same FK-write keying as `BelongsTo`: explicit relation_enum, or a - // non-unique target, is keyed by relation variant; a unique target - // uses the entity-keyed path. - let relation_variant = match (relation_enum.as_ref(), is_unique_entity) { - (Some(relation_enum), _) => Some(Ident::new( - &relation_enum.value().to_upper_camel_case(), - relation_enum.span(), - )), - (None, false) => Some(Ident::new( - &infer_relation_name_from_entity(&compound.entity) - .to_upper_camel_case(), - Span::call_site(), - )), - (None, true) => None, - }; - Some(Relation::BelongsToLegacy(LegacyBelongsToField { - ident, - relation_enum: relation_variant, - })) - } - RelationAttr::BelongsToSelfLegacy { relation_enum } => { - Some(Relation::BelongsToLegacy(LegacyBelongsToField { - ident, - relation_enum: Some(Ident::new( - &relation_enum.value().to_upper_camel_case(), - relation_enum.span(), - )), - })) - } - RelationAttr::HasOne if is_unique_entity => { + RelationAttr::HasOne if is_unique_relation_target => { Some(Relation::HasOne(HasOneField { ident, - entity: &compound.entity, + entity: &compound_type.entity, })) } - RelationAttr::HasMany if is_unique_entity => { + RelationAttr::HasMany if is_unique_relation_target => { Some(Relation::HasMany(HasManyField { ident, - entity: &compound.entity, - })) - } - RelationAttr::HasManyVia { via } if is_unique_entity => { - Some(Relation::HasManyVia(ViaField { - ident, - entity: via.value(), + entity: &compound_type.entity, })) } - RelationAttr::HasManyViaSelf { via, reverse } => { - Some(Relation::HasManyViaSelf(SelfViaField { + RelationAttr::ManyToMany { junction } if is_unique_relation_target => { + Some(Relation::ManyToMany(ManyToManyField { ident, - entity: via.value(), - reverse: *reverse, + junction_module: &junction.module, + kind: ManyToManyKind::Standard, })) } + RelationAttr::ManyToManySelf { + junction_module, + reverse, + } => Some(Relation::ManyToMany(ManyToManyField { + ident, + junction_module, + kind: ManyToManyKind::SelfRef { reverse: *reverse }, + })), _ => None, }; - relations.extend(relation); - Ok::<_, syn::Error>(relations) - })?; - - let mut this = Self::default(); - for relation in &relations { - relation.expand_into(&mut this); - } - - Ok(this) + if let Some(relation) = relation { + relation.expand_into(&mut output)?; + } + Ok::<_, syn::Error>(output) + }) } fn expand(self) -> TokenStream { @@ -778,9 +685,9 @@ impl ActiveModelActionTokens { has_many_before_action, has_many_action, has_many_delete, - has_many_via_before_action, - has_many_via_action, - has_many_via_delete, + many_to_many_before_action, + many_to_many_action, + many_to_many_delete, } = self; quote! { @@ -823,7 +730,7 @@ impl ActiveModelActionTokens { #has_one_delete #has_many_delete - #has_many_via_delete + #many_to_many_delete let model: ActiveModel = self.into(); deleted.merge(model.delete(db)#await_?); @@ -846,7 +753,7 @@ impl ActiveModelActionTokens { #belongs_to_action #has_one_before_action #has_many_before_action - #has_many_via_before_action + #many_to_many_before_action let model: ActiveModel = self.into(); @@ -878,7 +785,7 @@ impl ActiveModelActionTokens { #belongs_to_after_action #has_one_action #has_many_action - #has_many_via_action + #many_to_many_action txn.commit()#await_?; @@ -902,7 +809,9 @@ impl<'a> Output<'a> { let mut this = Self::default(); for field in &fields.0 { - field.expand_into(fields, &mut this)?; + field.expand_into(&mut this); + this.active_model_setters + .extend(ActiveModelSetter { field, fields }.expand()?); } Ok(this) @@ -1068,163 +977,132 @@ fn expand_active_model_ex<'a>( struct BelongsToField<'a> { ident: &'a Ident, - entity: &'a TypePath, - cardinality: CardinalityKind, - nullable_from_fields: Vec<&'a Ident>, - /// The relation-enum variant to key FK writes by (`*_parent_key_for`) when this - /// relation shares its target entity with another. `None` uses the entity-keyed - /// path (`*_parent_key`), valid only when the target has a unique `Related`. - relation_enum: Option, + compound_type: &'a CompoundType, + relation_lookup: RelationLookup, + from: &'a RelationColumns, + fields: &'a Fields<'a>, } impl BelongsToField<'_> { - fn belongs_to_action(&self) -> TokenStream { + fn save_related_model(&self) -> TokenStream { let await_ = await_token(); let box_pin = if cfg!(feature = "async") { quote!(Box::pin) } else { quote!() }; - let ident = self.ident; - let related_entity = self.entity; - let (belongs_to_target, optional) = match self.cardinality { - CardinalityKind::Required => (quote!(#related_entity), quote!()), - CardinalityKind::Optional => (quote!(Option<#related_entity>), quote!(Some)), - }; - let nullable_from_fields = &self.nullable_from_fields; - // Disambiguate the FK write: a relation that shares its target entity with - // another relation is keyed by its relation enum; otherwise by entity (the - // canonical `Related`). - let (set_parent_key, clear_parent_key) = match &self.relation_enum { - Some(relation_enum) => ( - quote!(self.set_parent_key_for(&model, Relation::#relation_enum)?), - quote!(self.clear_parent_key_for(Relation::#relation_enum)?), - ), - None => ( - quote!(self.set_parent_key(&model)?), - quote!(self.clear_parent_key::<#related_entity>()?), - ), + let set_parent_key = match &self.relation_lookup { + RelationLookup::ByRelatedEntity => quote!(self.set_parent_key(&model)?), + RelationLookup::ByRelationVariant(relation) => { + quote!(self.set_parent_key_for(&model, Relation::#relation)?) + } }; - let replace_model = quote! { - ActiveBelongsTo::Set(#optional(model)) => { - let mut model = *model; - if model.is_update() { - #set_parent_key; - if model.is_changed() { - model = #box_pin(model.action(action, db))#await_?; + let model_action = quote!(#box_pin(model.action(action, db))#await_?); + quote! { + let mut model = *model; + if model.is_update() { + #set_parent_key; + if model.is_changed() { + model = #model_action; + } + } else { + model = #model_action; + #set_parent_key; + } + } + } + + fn expand_active_belongs_to_into( + &self, + output: &mut ActiveModelActionTokens, + cardinality: CardinalityKind, + ) -> syn::Result<()> { + let ident = self.ident; + let related_entity = &self.compound_type.entity; + let save_model = self.save_related_model(); + let from_columns = self.from; + let optional_foreign_key_fields = self.fields.optional_fields_for_columns(from_columns)?; + let action_arms = match cardinality { + CardinalityKind::Required => { + if !optional_foreign_key_fields.is_empty() { + return Err(syn::Error::new( + from_columns.span, + "BelongsTo requires non-Option `from` fields", + )); + } + quote! { + ActiveBelongsTo::Set(model) => { + #save_model + ActiveBelongsTo::<#related_entity>::set(model) } - } else { - model = #box_pin(model.action(action, db))#await_?; - #set_parent_key; } - ActiveBelongsTo::<#belongs_to_target>::set(#optional(model)) } - }; - let clear_optional = if matches!(self.cardinality, CardinalityKind::Optional) { - quote! { - ActiveBelongsTo::Set(None) => { - #(self.#nullable_from_fields = sea_orm::Set(None);)* - if !#clear_parent_key { - return Err(sea_orm::DbErr::Type(format!( - "Relation {} cannot be cleared", - stringify!(#ident) - ))); + CardinalityKind::Optional => { + if optional_foreign_key_fields.is_empty() { + return Err(syn::Error::new( + from_columns.span, + "BelongsTo> requires at least one `Option` `from` field", + )); + } + quote! { + ActiveBelongsTo::Set(Some(model)) => { + #save_model + ActiveBelongsTo::>::set(Some(model)) + } + ActiveBelongsTo::Set(None) => { + #(self.#optional_foreign_key_fields = sea_orm::Set(None);)* + ActiveBelongsTo::Set(None) } - ActiveBelongsTo::Set(None) } } - } else { - quote!() }; - quote! { + output.belongs_to_action.extend(quote! { let #ident = match self.#ident.take() { ActiveBelongsTo::NotSet => ActiveBelongsTo::NotSet, - #replace_model - #clear_optional + #action_arms }; - } - } - - fn belongs_to_after_action(&self) -> TokenStream { - let ident = self.ident; - quote! { + }); + output.belongs_to_after_action.extend(quote! { if #ident.is_set() { model.#ident = #ident; } - } - } - - fn expand_into(&self, output: &mut ActiveModelActionTokens) { - output.belongs_to_action.extend(self.belongs_to_action()); - output - .belongs_to_after_action - .extend(self.belongs_to_after_action()); + }); + Ok(()) } -} -/// Legacy `belongs_to` whose field type is `HasOne` (wrapped by `ActiveHasOne`), kept -/// for backward compatibility. Writes this row's own foreign key from the related model, -/// mirroring the pre-`BelongsTo` behavior. Setting the relation to `Set(None)` leaves the -/// foreign key untouched (the legacy no-op); use `BelongsTo>` for clear/detach. -struct LegacyBelongsToField<'a> { - ident: &'a Ident, - /// Key the FK write by relation variant (`set_parent_key_for`) when the target entity - /// is shared with another relation; `None` uses the entity-keyed path (`set_parent_key`). - relation_enum: Option, -} - -impl LegacyBelongsToField<'_> { - fn belongs_to_action(&self) -> TokenStream { - let await_ = await_token(); - let box_pin = if cfg!(feature = "async") { - quote!(Box::pin) - } else { - quote!() - }; + fn expand_active_has_one_into(&self, output: &mut ActiveModelActionTokens) { let ident = self.ident; - let set_parent_key = match &self.relation_enum { - Some(relation_enum) => { - quote!(self.set_parent_key_for(&model, Relation::#relation_enum)?) - } - None => quote!(self.set_parent_key(&model)?), - }; + let save_model = self.save_related_model(); - quote! { + output.belongs_to_action.extend(quote! { let #ident = match std::mem::take(&mut self.#ident) { ActiveHasOne::Set(Some(model)) => { - let mut model = *model; - if model.is_update() { - #set_parent_key; - if model.is_changed() { - model = #box_pin(model.action(action, db))#await_?; - } - } else { - model = #box_pin(model.action(action, db))#await_?; - #set_parent_key; - } + #save_model Some(model) } // `NotSet` or `Set(None)`: leave the foreign key as-is (legacy no-op). ActiveHasOne::NotSet | ActiveHasOne::Set(None) => None, }; - } - } - - fn belongs_to_after_action(&self) -> TokenStream { - let ident = self.ident; - quote! { + }); + output.belongs_to_after_action.extend(quote! { if let Some(#ident) = #ident { model.#ident = ActiveHasOne::set(Some(#ident)); } - } + }); } - fn expand_into(&self, output: &mut ActiveModelActionTokens) { - output.belongs_to_action.extend(self.belongs_to_action()); - output - .belongs_to_after_action - .extend(self.belongs_to_after_action()); + fn expand_into(&self, output: &mut ActiveModelActionTokens) -> syn::Result<()> { + match self.compound_type.kind { + CompoundKind::BelongsTo(cardinality) => { + self.expand_active_belongs_to_into(output, cardinality) + } + CompoundKind::HasOne => { + self.expand_active_has_one_into(output); + Ok(()) + } + CompoundKind::HasMany => unreachable!("belongs_to field cannot be HasMany"), + } } } @@ -1324,96 +1202,12 @@ impl HasOneField<'_> { } } -struct BelongsToSelfField<'a> { +struct HasManySelfField<'a> { ident: &'a Ident, - relation_enum: LitStr, - cardinality: CardinalityKind, - nullable_from_fields: Vec<&'a Ident>, + relation_variant: LitStr, } -impl BelongsToSelfField<'_> { - fn belongs_to_action(&self) -> TokenStream { - let await_ = await_token(); - let box_pin = if cfg!(feature = "async") { - quote!(Box::pin) - } else { - quote!() - }; - let ident = self.ident; - let relation_enum = Ident::new(&self.relation_enum.value(), self.relation_enum.span()); - let relation_enum = quote!(Relation::#relation_enum); - let (belongs_to_target, optional) = match self.cardinality { - CardinalityKind::Required => (quote!(Entity), quote!()), - CardinalityKind::Optional => (quote!(Option), quote!(Some)), - }; - let nullable_from_fields = &self.nullable_from_fields; - let clear_parent_key_fields = quote! { - #(self.#nullable_from_fields = sea_orm::Set(None);)* - }; - let replace_model = quote! { - ActiveBelongsTo::Set(#optional(model)) => { - let mut model = *model; - if model.is_update() { - self.set_parent_key_for(&model, #relation_enum)?; - if model.is_changed() { - model = #box_pin(model.action(action, db))#await_?; - } - } else { - model = #box_pin(model.action(action, db))#await_?; - self.set_parent_key_for(&model, #relation_enum)?; - } - ActiveBelongsTo::<#belongs_to_target>::set(#optional(model)) - } - }; - let clear_optional = if matches!(self.cardinality, CardinalityKind::Optional) { - quote! { - ActiveBelongsTo::Set(None) => { - #clear_parent_key_fields - if !self.clear_parent_key_for(#relation_enum)? { - return Err(sea_orm::DbErr::Type(format!( - "Relation {} cannot be cleared", - stringify!(#ident) - ))); - } - ActiveBelongsTo::Set(None) - } - } - } else { - quote!() - }; - - quote! { - let #ident = match self.#ident.take() { - ActiveBelongsTo::NotSet => ActiveBelongsTo::NotSet, - #replace_model - #clear_optional - }; - } - } - - fn belongs_to_after_action(&self) -> TokenStream { - let ident = self.ident; - quote! { - if #ident.is_set() { - model.#ident = #ident; - } - } - } - - fn expand_into(&self, output: &mut ActiveModelActionTokens) { - output.belongs_to_action.extend(self.belongs_to_action()); - output - .belongs_to_after_action - .extend(self.belongs_to_after_action()); - } -} - -struct SelfRelationField<'a> { - ident: &'a Ident, - relation_enum: LitStr, -} - -impl SelfRelationField<'_> { +impl HasManySelfField<'_> { fn expand_into(&self, output: &mut ActiveModelActionTokens) { let await_ = await_token(); let box_pin = if cfg!(feature = "async") { @@ -1422,12 +1216,13 @@ impl SelfRelationField<'_> { quote!() }; let ident = self.ident; - let relation_enum = Ident::new(&self.relation_enum.value(), self.relation_enum.span()); - let relation_enum = quote!(Relation::#relation_enum); + let relation_variant = + Ident::new(&self.relation_variant.value(), self.relation_variant.span()); + let relation_variant = quote!(Relation::#relation_variant); let delete_associated_model = quote! { let mut item = item.into_active_model(); - if item.clear_parent_key_for_self_rev(#relation_enum)? { + if item.clear_parent_key_for_self_rev(#relation_variant)? { item.update(db)#await_?; } else { // attempt to cascade delete may lead to infinite recursion @@ -1441,7 +1236,7 @@ impl SelfRelationField<'_> { let has_many_action = quote! { if #ident.is_replace() { - for item in model.find_belongs_to_self(#relation_enum, db.get_database_backend())?.all(db)#await_? { + for item in model.find_belongs_to_self(#relation_variant, db.get_database_backend())?.all(db)#await_? { if !#ident.find(&item) { #delete_associated_model } @@ -1449,7 +1244,7 @@ impl SelfRelationField<'_> { } model.#ident = #ident.empty_holder(); for mut #ident in #ident.into_vec() { - #ident.set_parent_key_for_self_rev(&model, #relation_enum)?; + #ident.set_parent_key_for_self_rev(&model, #relation_variant)?; let #ident = if #ident.is_changed() { #box_pin(#ident.action(action, db))#await_? } else { @@ -1460,7 +1255,7 @@ impl SelfRelationField<'_> { }; let has_many_delete = quote! { - for item in self.find_belongs_to_self(#relation_enum, db.get_database_backend())?.all(db)#await_? { + for item in self.find_belongs_to_self(#relation_variant, db.get_database_backend())?.all(db)#await_? { #delete_associated_model } }; @@ -1471,68 +1266,18 @@ impl SelfRelationField<'_> { } } -struct ViaField<'a> { - ident: &'a Ident, - entity: String, +enum ManyToManyKind { + Standard, + SelfRef { reverse: bool }, } -impl ViaField<'_> { - fn expand_into(&self, output: &mut ActiveModelActionTokens) { - let await_ = await_token(); - let box_pin = if cfg!(feature = "async") { - quote!(Box::pin) - } else { - quote!() - }; - let ident = self.ident; - let mut via_entity = self.entity.as_str(); - if let Some((prefix, _)) = via_entity.split_once("::") { - via_entity = prefix; - } - - let related_entity: TokenStream = format!("super::{via_entity}::Entity").parse().unwrap(); - - let has_many_via_before_action = quote! { - let #ident = self.#ident.take(); - }; - - let has_many_via_action = quote! { - model.#ident = #ident.empty_holder(); - for item in #ident.into_vec() { - let item = if item.is_update() && !item.is_changed() { - item - } else { - #box_pin(item.action(action, db))#await_? - }; - model.#ident.push(item); - } - model.establish_links( - #related_entity, - model.#ident.as_slice(), - model.#ident.is_replace(), - db - )#await_?; - }; - - let has_many_via_delete = quote! { - deleted.merge(self.delete_links(#related_entity, db)#await_?); - }; - - output - .has_many_via_before_action - .extend(has_many_via_before_action); - output.has_many_via_action.extend(has_many_via_action); - output.has_many_via_delete.extend(has_many_via_delete); - } -} - -struct SelfViaField<'a> { +struct ManyToManyField<'a> { ident: &'a Ident, - entity: String, - reverse: bool, + junction_module: &'a Ident, + kind: ManyToManyKind, } -impl SelfViaField<'_> { +impl ManyToManyField<'_> { fn expand_into(&self, output: &mut ActiveModelActionTokens) { let await_ = await_token(); let box_pin = if cfg!(feature = "async") { @@ -1541,23 +1286,27 @@ impl SelfViaField<'_> { quote!() }; let ident = self.ident; - let related_entity: TokenStream = - format!("super::{}::Entity", self.entity).parse().unwrap(); - let establish_links = Ident::new( - if self.reverse { - "establish_links_self_rev" - } else { - "establish_links_self" - }, - ident.span(), - ); + let junction_module = self.junction_module; + let junction_entity = quote!(super::#junction_module::Entity); + let (establish_links, delete_links) = match &self.kind { + ManyToManyKind::Standard => ("establish_links", "delete_links"), + ManyToManyKind::SelfRef { reverse: false } => { + ("establish_links_self", "delete_links_self") + } + ManyToManyKind::SelfRef { reverse: true } => { + ("establish_links_self_rev", "delete_links_self") + } + }; + let establish_links = Ident::new(establish_links, ident.span()); + let delete_links = Ident::new(delete_links, ident.span()); - let has_many_via_before_action = quote! { + let many_to_many_before_action = quote! { let #ident = self.#ident.take(); }; - let has_many_via_action = quote! { + let many_to_many_action = quote! { model.#ident = #ident.empty_holder(); + // TODO: Batch save? for item in #ident.into_vec() { let item = if item.is_update() && !item.is_changed() { item @@ -1567,22 +1316,22 @@ impl SelfViaField<'_> { model.#ident.push(item); } model.#establish_links( - #related_entity, + #junction_entity, model.#ident.as_slice(), model.#ident.is_replace(), db )#await_?; }; - let has_many_via_delete = quote! { - deleted.merge(self.delete_links_self(#related_entity, db)#await_?); + let many_to_many_delete = quote! { + deleted.merge(self.#delete_links(#junction_entity, db)#await_?); }; output - .has_many_via_before_action - .extend(has_many_via_before_action); - output.has_many_via_action.extend(has_many_via_action); - output.has_many_via_delete.extend(has_many_via_delete); + .many_to_many_before_action + .extend(many_to_many_before_action); + output.many_to_many_action.extend(many_to_many_action); + output.many_to_many_delete.extend(many_to_many_delete); } } @@ -1665,17 +1414,15 @@ impl HasManyField<'_> { } impl Relation<'_> { - fn expand_into(&self, output: &mut ActiveModelActionTokens) { + fn expand_into(&self, output: &mut ActiveModelActionTokens) -> syn::Result<()> { match self { - Self::BelongsTo(field) => field.expand_into(output), - Self::BelongsToSelf(field) => field.expand_into(output), - Self::BelongsToLegacy(field) => field.expand_into(output), + Self::BelongsTo(field) => field.expand_into(output)?, Self::HasOne(field) => field.expand_into(output), Self::HasMany(field) => field.expand_into(output), Self::HasManySelf(field) => field.expand_into(output), - Self::HasManyVia(field) => field.expand_into(output), - Self::HasManyViaSelf(field) => field.expand_into(output), + Self::ManyToMany(field) => field.expand_into(output), } + Ok(()) } } diff --git a/sea-orm-macros/src/derives/entity_loader.rs b/sea-orm-macros/src/derives/entity_loader.rs index 69f03e8a7..a9bc84592 100644 --- a/sea-orm-macros/src/derives/entity_loader.rs +++ b/sea-orm-macros/src/derives/entity_loader.rs @@ -10,10 +10,14 @@ pub struct EntityLoaderSchema { pub enum EntityLoaderFieldKind { HasOne, - HasMany { via: Option }, - SelfHasOne, - SelfHasMany, - SelfHasManyVia { via: syn::LitStr, reverse: bool }, + HasOneSelf, + HasMany, + HasManySelf, + ManyToMany, + ManyToManySelf { + junction_module: Ident, + reverse: bool, + }, } pub struct EntityLoaderField { @@ -55,7 +59,9 @@ impl EntityLoaderField { fn expand_loader_nest_field_into(&self, output: &mut EntityLoaderOutput) { let field = &self.field; let nest_type = match &self.kind { - EntityLoaderFieldKind::HasOne | EntityLoaderFieldKind::HasMany { .. } => { + EntityLoaderFieldKind::HasOne + | EntityLoaderFieldKind::HasMany + | EntityLoaderFieldKind::ManyToMany => { let mut entity_module = self.entity.path.clone(); entity_module.segments.pop(); entity_module @@ -63,9 +69,9 @@ impl EntityLoaderField { .push(syn::parse_quote!(EntityLoaderWith)); quote!(#entity_module) } - EntityLoaderFieldKind::SelfHasOne - | EntityLoaderFieldKind::SelfHasMany - | EntityLoaderFieldKind::SelfHasManyVia { .. } => quote!(EntityLoaderWith), + EntityLoaderFieldKind::HasOneSelf + | EntityLoaderFieldKind::HasManySelf + | EntityLoaderFieldKind::ManyToManySelf { .. } => quote!(EntityLoaderWith), }; output.loader_nest_fields.push(quote! { @@ -103,29 +109,28 @@ impl EntityLoaderField { }); } - fn expand_self_via_with_param_into( + fn expand_many_to_many_self_with_param_into( &self, output: &mut EntityLoaderOutput, - via: &syn::LitStr, + junction_module: &Ident, reverse: bool, ) { - let via = Ident::new(&via.value(), via.span()); let target_type = if !reverse { - Ident::new("TableRef", via.span()) + Ident::new("TableRef", junction_module.span()) } else { - Ident::new("TableRefRev", via.span()) + Ident::new("TableRefRev", junction_module.span()) }; let target_entity = if !reverse { - quote!(super::#via::Entity) + quote!(super::#junction_module::Entity) } else { - quote!(super::#via::EntityReverse) + quote!(super::#junction_module::EntityReverse) }; output.with_param_impls.extend(quote! { impl EntityLoaderWithParam for #target_entity { fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { - (sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()), None) + (sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()), None) } } @@ -136,7 +141,7 @@ impl EntityLoaderField { { fn into_with_param(self) -> (sea_orm::compound::LoadTarget, Option) { ( - sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()), + sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()), Some(sea_orm::compound::LoadTarget::TableRef(self.1.table_ref())), ) } @@ -153,7 +158,9 @@ impl EntityLoaderField { let entity = &self.entity; match &self.kind { - EntityLoaderFieldKind::HasOne | EntityLoaderFieldKind::HasMany { .. } => { + EntityLoaderFieldKind::HasOne + | EntityLoaderFieldKind::HasMany + | EntityLoaderFieldKind::ManyToMany => { if !duplicate_entity { output.loader_with_set_impl.extend(quote! { if target == sea_orm::compound::LoadTarget::TableRef(#entity.table_ref()) { @@ -170,9 +177,9 @@ impl EntityLoaderField { }); } } - EntityLoaderFieldKind::SelfHasOne - | EntityLoaderFieldKind::SelfHasMany - | EntityLoaderFieldKind::SelfHasManyVia { .. } => { + EntityLoaderFieldKind::HasOneSelf + | EntityLoaderFieldKind::HasManySelf + | EntityLoaderFieldKind::ManyToManySelf { .. } => { if let Some(relation_enum) = &self.relation_enum { output.loader_with_set_impl.extend(quote! { if let sea_orm::compound::LoadTarget::Relation(relation_enum) = &target { @@ -183,16 +190,19 @@ impl EntityLoaderField { }); } - if let EntityLoaderFieldKind::SelfHasManyVia { via, reverse } = &self.kind { - let via = Ident::new(&via.value(), via.span()); + if let EntityLoaderFieldKind::ManyToManySelf { + junction_module, + reverse, + } = &self.kind + { let target_type = if !reverse { - Ident::new("TableRef", via.span()) + Ident::new("TableRef", junction_module.span()) } else { - Ident::new("TableRefRev", via.span()) + Ident::new("TableRefRev", junction_module.span()) }; output.loader_with_set_impl.extend(quote! { - if target == sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()) { + if target == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) { self.#field = true; } }); @@ -210,7 +220,9 @@ impl EntityLoaderField { let entity = &self.entity; match &self.kind { - EntityLoaderFieldKind::HasOne | EntityLoaderFieldKind::HasMany { .. } => { + EntityLoaderFieldKind::HasOne + | EntityLoaderFieldKind::HasMany + | EntityLoaderFieldKind::ManyToMany => { if !duplicate_entity { output.loader_with_2_impl.extend(quote! { if left == sea_orm::compound::LoadTarget::TableRef(#entity.table_ref()) { @@ -231,17 +243,19 @@ impl EntityLoaderField { }); } } - EntityLoaderFieldKind::SelfHasOne | EntityLoaderFieldKind::SelfHasMany => {} - EntityLoaderFieldKind::SelfHasManyVia { via, reverse } => { - let via = Ident::new(&via.value(), via.span()); + EntityLoaderFieldKind::HasOneSelf | EntityLoaderFieldKind::HasManySelf => {} + EntityLoaderFieldKind::ManyToManySelf { + junction_module, + reverse, + } => { let target_type = if !reverse { - Ident::new("TableRef", via.span()) + Ident::new("TableRef", junction_module.span()) } else { - Ident::new("TableRefRev", via.span()) + Ident::new("TableRefRev", junction_module.span()) }; output.loader_with_2_impl.extend(quote! { - if left == sea_orm::compound::LoadTarget::#target_type(super::#via::Entity.table_ref()) { + if left == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) { self.with.#field = true; self.nest.#field.set(right); return self; @@ -490,7 +504,7 @@ impl EntityLoaderField { }); } - fn expand_load_self_one_into( + fn expand_load_one_self_into( &self, output: &mut EntityLoaderOutput, relation_enum: &syn::LitStr, @@ -515,7 +529,7 @@ impl EntityLoaderField { }); } - fn expand_load_self_many_into( + fn expand_load_many_self_into( &self, output: &mut EntityLoaderOutput, relation_enum: &syn::LitStr, @@ -540,14 +554,13 @@ impl EntityLoaderField { }); } - fn expand_load_self_many_via_into( + fn expand_load_many_to_many_self_into( &self, output: &mut EntityLoaderOutput, - via: &syn::LitStr, + junction_module: &Ident, reverse: bool, ) { let field = &self.field; - let via = Ident::new(&via.value(), via.span()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -556,7 +569,7 @@ impl EntityLoaderField { output.load_many.extend(quote! { if with.#field { - let #field = models.as_slice().load_self_via_ex(super::#via::Entity, #reverse, db)#await_?; + let #field = models.as_slice().load_self_via_ex(super::#junction_module::Entity, #reverse, db)#await_?; let #field = EntityLoader::load_nest_nest(#field, &nest.#field, db)#await_?; for (model, #field) in models.iter_mut().zip(#field) { @@ -566,7 +579,7 @@ impl EntityLoaderField { }); output.load_many_nest.extend(quote! { if with.#field { - let #field = models.as_slice().load_self_via_ex(super::#via::Entity, #reverse, db)#await_?; + let #field = models.as_slice().load_self_via_ex(super::#junction_module::Entity, #reverse, db)#await_?; for (model, #field) in models.iter_mut().zip(#field) { if let Some(model) = model.as_mut() { @@ -577,7 +590,7 @@ impl EntityLoaderField { }); output.load_many_nest_nest.extend(quote! { if with.#field { - let #field = models.as_slice().load_self_via_ex(super::#via::Entity, #reverse, db)#await_?; + let #field = models.as_slice().load_self_via_ex(super::#junction_module::Entity, #reverse, db)#await_?; for (models, #field) in models.iter_mut().zip(#field) { for (model, #field) in models.iter_mut().zip(#field) { @@ -598,30 +611,35 @@ impl EntityLoaderField { self.expand_load_one_with_rel_into(output, relation_enum); } } - EntityLoaderFieldKind::HasMany { .. } if !duplicate_entity => { + EntityLoaderFieldKind::HasMany | EntityLoaderFieldKind::ManyToMany + if !duplicate_entity => + { self.expand_load_many_into(output); } - EntityLoaderFieldKind::HasMany { via: None } => { + EntityLoaderFieldKind::HasMany => { if let Some(relation_enum) = &self.relation_enum { self.expand_load_many_with_rel_into(output, relation_enum); } } - EntityLoaderFieldKind::HasMany { via: Some(_) } => {} - EntityLoaderFieldKind::SelfHasOne => { + EntityLoaderFieldKind::ManyToMany => {} + EntityLoaderFieldKind::HasOneSelf => { if let Some(relation_enum) = &self.relation_enum { - self.expand_load_self_one_into(output, relation_enum); + self.expand_load_one_self_into(output, relation_enum); } } - EntityLoaderFieldKind::SelfHasMany => { + EntityLoaderFieldKind::HasManySelf => { if let Some(relation_enum) = &self.relation_enum { - self.expand_load_self_many_into(output, relation_enum); + self.expand_load_many_self_into(output, relation_enum); } } - EntityLoaderFieldKind::SelfHasManyVia { via, reverse } => { + EntityLoaderFieldKind::ManyToManySelf { + junction_module, + reverse, + } => { if let Some(relation_enum) = &self.relation_enum { - self.expand_load_self_many_into(output, relation_enum); + self.expand_load_many_self_into(output, relation_enum); } - self.expand_load_self_many_via_into(output, via, *reverse); + self.expand_load_many_to_many_self_into(output, junction_module, *reverse); } } } @@ -650,12 +668,22 @@ impl EntityLoaderOutput { field.expand_loader_nest_field_into(&mut output); field.expand_loader_with_set_impl_into(&mut output, duplicate_entity); field.expand_loader_with_2_impl_into(&mut output, duplicate_entity); - if let EntityLoaderFieldKind::SelfHasManyVia { via, reverse } = &field.kind { - field.expand_self_via_with_param_into(&mut output, via, *reverse); + if let EntityLoaderFieldKind::ManyToManySelf { + junction_module, + reverse, + } = &field.kind + { + field.expand_many_to_many_self_with_param_into( + &mut output, + junction_module, + *reverse, + ); } if matches!( &field.kind, - EntityLoaderFieldKind::HasOne | EntityLoaderFieldKind::HasMany { .. } + EntityLoaderFieldKind::HasOne + | EntityLoaderFieldKind::HasMany + | EntityLoaderFieldKind::ManyToMany ) && duplicate_entity && field.relation_enum.is_some() && relation_tuple_impls.insert(&field.entity) diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs index 5ba61fab7..b1dc78d54 100644 --- a/sea-orm-macros/src/derives/model_ex.rs +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -4,7 +4,10 @@ use super::attributes::compound_attr; use super::entity_loader::{ EntityLoaderField, EntityLoaderFieldKind, EntityLoaderSchema, expand_entity_loader, }; -use super::util::{CardinalityKind, CompoundKind, CompoundType, combine_error, is_self_entity}; +use super::util::{ + CardinalityKind, CompoundKind, CompoundType, Junction, RelationColumns, combine_error, + is_self_entity, +}; use super::{expand_typed_column, model::DeriveModel}; use heck::ToUpperCamelCase; use proc_macro2::{Ident, Span, TokenStream}; @@ -203,6 +206,7 @@ struct ModelExField<'a> { kind: ModelExFieldKind<'a>, } +#[expect(clippy::large_enum_variant)] enum ModelExFieldKind<'a> { Scalar(ScalarField<'a>), Compound(CompoundField), @@ -306,63 +310,138 @@ struct ModelExRelationOutput { impl ModelExRelationOutput { fn from_schema(schema: &ModelExSchema<'_>) -> Self { - let fields = schema + let entity_count = schema .fields .iter() .filter_map(|field| { - let ModelExFieldKind::Compound(compound) = &field.kind else { - return None; - }; - if matches!( - &compound.kind, - CompoundFieldKind::BelongsTo(Some(_)) - | CompoundFieldKind::HasOne(Some(_)) - | CompoundFieldKind::HasMany(Some(_)) - ) { - Some(compound) + if let ModelExFieldKind::Compound(compound) = &field.kind + && compound.relation.is_some() + { + Some(&compound.compound_type.entity) } else { None } }) - .collect::>(); - let mut entity_count = HashMap::<&TypePath, usize>::new(); - for field in &fields { - *entity_count.entry(&field.entity).or_default() += 1; - } + .fold( + HashMap::<&TypePath, usize>::new(), + |mut entity_count, entity| { + *entity_count.entry(entity).or_default() += 1; + entity_count + }, + ); let mut output = Self::default(); - for field in fields { - let is_unique_entity = entity_count - .get(&field.entity) - .is_some_and(|count| *count == 1); - match &field.kind { - CompoundFieldKind::BelongsTo(Some(attr)) => { - expand_belongs_to_into(&mut output, &field.entity, attr, is_unique_entity); + for (field, relation) in schema.fields.iter().filter_map(|field| { + if let ModelExFieldKind::Compound(compound) = &field.kind + && let Some(relation) = &compound.relation + { + Some((compound, relation)) + } else { + None + } + }) { + let entity = &field.compound_type.entity; + let is_unique_entity = entity_count.get(entity).is_some_and(|count| *count == 1); + match relation { + RelationAttr::BelongsTo(attr) => { + expand_belongs_to_into(&mut output, entity, attr, is_unique_entity); } - CompoundFieldKind::HasOne(Some(attr)) => { - expand_has_one_into(&mut output, &field.entity, attr, is_unique_entity); + RelationAttr::HasOne(attr) => { + expand_has_one_into(&mut output, entity, attr, is_unique_entity); } - CompoundFieldKind::HasMany(Some(attr)) => { - expand_has_many_into(&mut output, &field.entity, attr, is_unique_entity); + RelationAttr::HasMany(attr) => { + expand_has_many_into(&mut output, entity, attr, is_unique_entity); } - CompoundFieldKind::BelongsTo(None) - | CompoundFieldKind::HasOne(None) - | CompoundFieldKind::HasMany(None) => {} } } output } } -enum CompoundFieldKind { - BelongsTo(Option), - HasOne(Option), - HasMany(Option), +enum RelationAttr { + BelongsTo(BelongsToAttr), + HasOne(HasOneAttr), + HasMany(HasManyAttr), +} + +impl RelationAttr { + fn from_attr( + attrs: compound_attr::SeaOrm, + field: &Field, + compound_type: &CompoundType, + ) -> syn::Result { + match &attrs { + compound_attr::SeaOrm { + belongs_to: Some(_), + .. + } + | compound_attr::SeaOrm { + self_ref: Some(_), + via: None, + from: Some(_), + to: Some(_), + .. + } => { + let attr = BelongsToAttr::from_attr(attrs, field)?; + if !matches!( + compound_type.kind, + CompoundKind::BelongsTo(_) | CompoundKind::HasOne + ) { + return Err(syn::Error::new_spanned( + &field.ty, + "belongs_to must be paired with BelongsTo or HasOne", + )); + } + Ok(Self::BelongsTo(attr)) + } + compound_attr::SeaOrm { + has_one: Some(_), .. + } => { + let attr = HasOneAttr::from_attr(attrs, field)?; + if compound_type.kind != CompoundKind::HasOne { + return Err(syn::Error::new_spanned( + &field.ty, + "has_one must be paired with HasOne", + )); + } + Ok(Self::HasOne(attr)) + } + compound_attr::SeaOrm { + has_many: Some(_), .. + } + | compound_attr::SeaOrm { + self_ref: Some(_), + via: Some(_), + .. + } => { + let attr = + HasManyAttr::from_attr(attrs, field, is_self_entity(&compound_type.entity))?; + if compound_type.kind != CompoundKind::HasMany { + return Err(syn::Error::new_spanned( + &field.ty, + "has_many must be paired with HasMany", + )); + } + Ok(Self::HasMany(attr)) + } + _ => match compound_type.kind { + CompoundKind::BelongsTo(_) => { + Ok(Self::BelongsTo(BelongsToAttr::from_attr(attrs, field)?)) + } + CompoundKind::HasOne => Ok(Self::HasOne(HasOneAttr::from_attr(attrs, field)?)), + CompoundKind::HasMany => Ok(Self::HasMany(HasManyAttr::from_attr( + attrs, + field, + is_self_entity(&compound_type.entity), + )?)), + }, + } + } } struct CompoundField { - entity: TypePath, - kind: CompoundFieldKind, + compound_type: CompoundType, + relation: Option, } impl CompoundField { @@ -379,108 +458,89 @@ impl CompoundField { )); } - let is_self_entity = is_self_entity(&compound.entity); - // A `belongs_to` relation may keep the legacy `HasOne` field type instead of - // opting into `BelongsTo`. Detect that so it is routed through the belongs_to - // codegen (emitting a `belongs_to` relation); only the wrapper type differs. - // `BelongsTo` remains the recommended type. - let declares_belongs_to = attrs.as_ref().is_some_and(|attrs| { - attrs.belongs_to.is_some() - || (attrs.self_ref.is_some() - && attrs.via.is_none() - && attrs.from.is_some() - && attrs.to.is_some()) - }); - let kind = match compound.kind { - CompoundKind::BelongsTo(_) => CompoundFieldKind::BelongsTo( - attrs - .map(|attrs| BelongsToAttr::from_attr(attrs, field)) - .transpose()?, - ), - CompoundKind::HasOne if declares_belongs_to => CompoundFieldKind::BelongsTo( - attrs - .map(|attrs| BelongsToAttr::from_attr(attrs, field)) - .transpose()?, - ), - CompoundKind::HasOne => CompoundFieldKind::HasOne( - attrs - .map(|attrs| HasOneAttr::from_attr(attrs, field)) - .transpose()?, - ), - CompoundKind::HasMany => CompoundFieldKind::HasMany( - attrs - .map(|attrs| HasManyAttr::from_attr(attrs, field, is_self_entity)) - .transpose()?, - ), - }; + let relation = attrs + .map(|attrs| RelationAttr::from_attr(attrs, field, &compound)) + .transpose()?; Ok(Self { - entity: compound.entity, - kind, + compound_type: compound, + relation, }) } } fn entity_loader_field(field: &Ident, compound: &CompoundField) -> EntityLoaderField { - let self_entity = is_self_entity(&compound.entity); - let (relation_enum, kind) = match &compound.kind { - CompoundFieldKind::BelongsTo(attr) => ( - attr.as_ref() - .and_then(|attr| attr.relation_variants.explicit_name()) - .cloned(), + let entity = &compound.compound_type.entity; + let self_entity = is_self_entity(entity); + let (relation_enum, kind) = match &compound.relation { + Some(RelationAttr::BelongsTo(attr)) => ( + attr.relation_variants.explicit_name().cloned(), if self_entity { - EntityLoaderFieldKind::SelfHasOne + EntityLoaderFieldKind::HasOneSelf } else { EntityLoaderFieldKind::HasOne }, ), - CompoundFieldKind::HasOne(attr) => ( - attr.as_ref() - .and_then(|attr| attr.relation_variants.explicit_name()) - .cloned(), + Some(RelationAttr::HasOne(attr)) => ( + attr.relation_variants.explicit_name().cloned(), if self_entity { - EntityLoaderFieldKind::SelfHasOne + EntityLoaderFieldKind::HasOneSelf } else { EntityLoaderFieldKind::HasOne }, ), - CompoundFieldKind::HasMany(attr) => { - let (relation_enum, via, reverse) = match attr { - None => (None, None, false), - Some(HasManyAttr::Standard { + Some(RelationAttr::HasMany(attr)) => { + let (relation_enum, junction_module, reverse) = match attr { + HasManyAttr::Standard { relation_variants, via, reverse, .. - }) => ( + } => ( relation_variants.explicit_name().cloned(), - via.clone(), + via.as_ref().map(|junction| junction.module.clone()), *reverse, ), - Some(HasManyAttr::SelfVia { + HasManyAttr::ManyToManySelf { relation_enum, - via, + junction_module, direction, - }) => ( + } => ( relation_enum.clone(), - Some(via.clone()), - matches!(direction, SelfViaDirection::Reverse), + Some(junction_module.clone()), + matches!(direction, ManyToManySelfDirection::Reverse), ), }; let kind = if self_entity { - if let Some(via) = via { - EntityLoaderFieldKind::SelfHasManyVia { via, reverse } + if let Some(junction_module) = junction_module { + EntityLoaderFieldKind::ManyToManySelf { + junction_module, + reverse, + } } else { - EntityLoaderFieldKind::SelfHasMany + EntityLoaderFieldKind::HasManySelf } + } else if junction_module.is_some() { + EntityLoaderFieldKind::ManyToMany } else { - EntityLoaderFieldKind::HasMany { via } + EntityLoaderFieldKind::HasMany }; (relation_enum, kind) } + None => ( + None, + match compound.compound_type.kind { + CompoundKind::BelongsTo(_) | CompoundKind::HasOne if self_entity => { + EntityLoaderFieldKind::HasOneSelf + } + CompoundKind::BelongsTo(_) | CompoundKind::HasOne => EntityLoaderFieldKind::HasOne, + CompoundKind::HasMany if self_entity => EntityLoaderFieldKind::HasManySelf, + CompoundKind::HasMany => EntityLoaderFieldKind::HasMany, + }, + ), }; EntityLoaderField { field: field.clone(), - entity: compound.entity.clone(), + entity: entity.clone(), relation_enum, kind, } @@ -553,23 +613,15 @@ fn expand_related_trait(entity: &TypePath, relation_variants: &RelationVariants) fn expand_related_via_trait( entity: &TypePath, relation_variants: &RelationVariants, - via: &LitStr, + junction: &Junction, ) -> TokenStream { let relation_enum = relation_variants.forward_ident(entity); - let value = via.value(); - let mut junction = value.as_str(); - let mut via_related = ""; - if let Some((prefix, suffix)) = value.split_once("::") { - junction = prefix; - via_related = suffix; - } - let junction = Ident::new(junction, via.span()); - let relation_def = quote!(super::#junction::Relation::#relation_enum.def()); - let via_relation_def = if via_related.is_empty() { - quote!(>::to().rev()) + let module = &junction.module; + let relation_def = quote!(super::#module::Relation::#relation_enum.def()); + let via_relation_def = if let Some(relation) = &junction.relation { + quote!(super::#module::Relation::#relation.def().rev()) } else { - let via_related = Ident::new(via_related, via.span()); - quote!(super::#junction::Relation::#via_related.def().rev()) + quote!(>::to().rev()) }; quote! { #[doc = " Generated by sea-orm-macros"] @@ -588,7 +640,7 @@ fn expand_related_into( output: &mut ModelExRelationOutput, entity: &TypePath, relation_variants: &RelationVariants, - via: Option<&LitStr>, + via: Option<&Junction>, impl_related: bool, ) { expand_related_entity_variants_into(entity, relation_variants, output); @@ -601,44 +653,9 @@ fn expand_related_into( } } -struct RelationColumns(Vec); - -impl RelationColumns { - fn from_lit(lit: LitStr) -> syn::Result { - let paths = if lit.value().starts_with('(') { - lit.parse_with(|input: syn::parse::ParseStream<'_>| { - let content; - syn::parenthesized!(content in input); - content.parse_terminated(syn::Path::parse_mod_style, Comma) - })? - } else { - let mut paths = Punctuated::new(); - paths.push(lit.parse()?); - paths - }; - if paths.is_empty() { - return Err(syn::Error::new(lit.span(), "expected at least one column")); - } - - paths - .into_iter() - .map(|path| { - let Some(segment) = path.segments.last() else { - return Err(syn::Error::new_spanned(path, "expected column path")); - }; - Ok(Ident::new( - &segment.ident.to_string().to_upper_camel_case(), - segment.ident.span(), - )) - }) - .collect::>>() - .map(Self) - } -} - struct BelongsToRelationAttr { declared: bool, - via: Option, + via: Option, from: RelationColumns, to: RelationColumns, on_update: Option, @@ -745,6 +762,13 @@ impl BelongsToAttr { None } }; + let via = match via.map(|via| Junction::from_lit(&via)).transpose() { + Ok(via) => via, + Err(err) => { + combine_error(&mut error, err); + None + } + }; if let Some(err) = error { return Err(err); @@ -802,7 +826,7 @@ fn expand_belongs_to_into( let belongs_to = Ident::new("belongs_to", Span::call_site()); let format_columns = |columns: &RelationColumns, prefix: &str| { let columns = columns - .0 + .columns .iter() .map(|column| { if prefix.is_empty() { @@ -867,7 +891,7 @@ struct HasOneAttr { relation_variants: RelationVariants, /// Whether `#[sea_orm(has_one)]` is present. has_one: bool, - via: Option, + via: Option, via_rel: Option, } @@ -932,6 +956,13 @@ impl HasOneAttr { ), ); } + let via = match via.map(|via| Junction::from_lit(&via)).transpose() { + Ok(via) => via, + Err(err) => { + combine_error(&mut error, err); + None + } + }; if let Some(err) = error { return Err(err); @@ -990,18 +1021,18 @@ enum HasManyAttr { Standard { relation_variants: RelationVariants, has_many: bool, - via: Option, + via: Option, via_rel: Option, reverse: bool, }, - SelfVia { + ManyToManySelf { relation_enum: Option, - via: LitStr, - direction: SelfViaDirection, + junction_module: Ident, + direction: ManyToManySelfDirection, }, } -enum SelfViaDirection { +enum ManyToManySelfDirection { Forward { from: LitStr, to: LitStr }, Reverse, Manual, @@ -1043,7 +1074,14 @@ impl HasManyAttr { syn::Error::new_spanned(&field.ty, "has_one must be paired with HasOne"), ); } - if is_self_ref && let Some(via) = via { + let junction = match via.as_ref().map(Junction::from_lit).transpose() { + Ok(junction) => junction, + Err(err) => { + combine_error(&mut error, err); + None + } + }; + if is_self_ref && let Some(via) = &via { if !is_self_entity { combine_error( &mut error, @@ -1054,20 +1092,29 @@ impl HasManyAttr { ); } + if junction + .as_ref() + .is_some_and(|junction| junction.relation.is_some()) + { + combine_error( + &mut error, + syn::Error::new(via.span(), "`self_ref` via must name a junction entity"), + ); + } if let Some(err) = error { return Err(err); } let direction = if reverse.is_some() { - SelfViaDirection::Reverse + ManyToManySelfDirection::Reverse } else if let (Some(from), Some(to)) = (from, to) { - SelfViaDirection::Forward { from, to } + ManyToManySelfDirection::Forward { from, to } } else { - SelfViaDirection::Manual + ManyToManySelfDirection::Manual }; - return Ok(Self::SelfVia { + return Ok(Self::ManyToManySelf { relation_enum, - via, + junction_module: junction.expect("validated junction").module, direction, }); } @@ -1097,7 +1144,7 @@ impl HasManyAttr { Ok(Self::Standard { relation_variants, has_many: has_many.is_some(), - via, + via: junction, via_rel, reverse: has_many.is_none() && reverse.is_some(), }) @@ -1118,10 +1165,17 @@ fn expand_has_many_into( via_rel, .. } => (relation_variants, *has_many, via, via_rel), - HasManyAttr::SelfVia { via, direction, .. } => { + HasManyAttr::ManyToManySelf { + junction_module, + direction, + .. + } => { output .impl_related_trait - .extend(expand_impl_related_self_via(via, direction)); + .extend(expand_impl_related_many_to_many_self( + junction_module, + direction, + )); return; } }; @@ -1349,21 +1403,23 @@ pub fn expand_derive_model_ex( }) } -fn expand_impl_related_self_via(via: &LitStr, direction: &SelfViaDirection) -> TokenStream { +fn expand_impl_related_many_to_many_self( + junction_module: &Ident, + direction: &ManyToManySelfDirection, +) -> TokenStream { match direction { - SelfViaDirection::Forward { from, to } => { - let junction = Ident::new(&via.value(), via.span()); + ManyToManySelfDirection::Forward { from, to } => { let from = Ident::new(&from.value(), from.span()); let to = Ident::new(&to.value(), to.span()); quote! { #[doc = " Generated by sea-orm-macros"] - impl RelatedSelfVia for Entity { + impl RelatedSelfVia for Entity { fn to() -> RelationDef { - super::#junction::Relation::#to.def() + super::#junction_module::Relation::#to.def() } fn via() -> RelationDef { - super::#junction::Relation::#from.def().rev() + super::#junction_module::Relation::#from.def().rev() } } } @@ -1379,7 +1435,7 @@ fn expand_impl_related_self_via(via: &LitStr, direction: &SelfViaDirection) -> T // } // } } - SelfViaDirection::Reverse | SelfViaDirection::Manual => quote!(), + ManyToManySelfDirection::Reverse | ManyToManySelfDirection::Manual => quote!(), } } diff --git a/sea-orm-macros/src/derives/util.rs b/sea-orm-macros/src/derives/util.rs index 0c47875d4..19a72c535 100644 --- a/sea-orm-macros/src/derives/util.rs +++ b/sea-orm-macros/src/derives/util.rs @@ -1,8 +1,8 @@ use heck::ToUpperCamelCase; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::{ - Field, GenericArgument, Ident, Meta, MetaNameValue, PathArguments, Type, TypePath, + Field, GenericArgument, LitStr, Meta, MetaNameValue, PathArguments, Type, TypePath, meta::ParseNestedMeta, punctuated::Punctuated, token::Comma, }; @@ -22,6 +22,85 @@ pub(crate) fn await_token() -> TokenStream { } } +pub(crate) struct RelationColumns { + pub(crate) columns: Vec, + pub(crate) span: Span, +} + +impl RelationColumns { + /// Parse relation columns in a `from` or `to` attribute. + /// For example: + /// `cake_id` or `Column::CakeId` -> `CakeId`; + /// `(user_id, post_id)` -> `UserId`, `PostId`. + pub(crate) fn from_lit(lit: LitStr) -> syn::Result { + let paths = if lit.value().starts_with('(') { + lit.parse_with(|input: syn::parse::ParseStream<'_>| { + let content; + syn::parenthesized!(content in input); + content.parse_terminated(syn::Path::parse_mod_style, Comma) + })? + } else { + let mut paths = Punctuated::new(); + paths.push(lit.parse()?); + paths + }; + if paths.is_empty() { + return Err(syn::Error::new(lit.span(), "expected at least one column")); + } + + let columns = paths + .into_iter() + .map(|path| { + let Some(segment) = path.segments.last() else { + return Err(syn::Error::new_spanned(path, "expected column path")); + }; + Ok(Ident::new( + &escape_rust_keyword(segment.ident.to_string().to_upper_camel_case()), + segment.ident.span(), + )) + }) + .collect::>>()?; + + Ok(Self { + columns, + span: lit.span(), + }) + } +} + +#[derive(Clone)] +pub(crate) struct Junction { + pub(crate) module: Ident, + pub(crate) relation: Option, +} + +impl Junction { + /// Parse the junction module and optional relation variant in a `via` attribute. + /// For example: `post_tag` -> module `post_tag`; + /// `cakes_bakers::Baker` -> module `cakes_bakers`, relation `Baker`. + pub(crate) fn from_lit(lit: &LitStr) -> syn::Result { + let path = lit.parse::()?; + if path.leading_colon.is_some() + || !(1..=2).contains(&path.segments.len()) + || path + .segments + .iter() + .any(|segment| !matches!(segment.arguments, PathArguments::None)) + { + return Err(syn::Error::new( + lit.span(), + "`via` must be `junction` or `junction::Relation`", + )); + } + + let mut segments = path.segments.into_iter(); + let module = segments.next().expect("validated junction path").ident; + let relation = segments.next().map(|segment| segment.ident); + + Ok(Self { module, relation }) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CardinalityKind { Required, diff --git a/sea-orm-sync/src/entity/active_model.rs b/sea-orm-sync/src/entity/active_model.rs index d8e8f64e2..c69d8052b 100644 --- a/sea-orm-sync/src/entity/active_model.rs +++ b/sea-orm-sync/src/entity/active_model.rs @@ -782,20 +782,6 @@ pub trait ActiveModelTrait: Clone + Debug { clear_key_on_active_model(&rel_def.from_col, self) } - #[doc(hidden)] - fn clear_parent_key_for( - &mut self, - rel: ::Relation, - ) -> Result { - let rel_def = rel.def(); - - if rel_def.is_owner { - return Err(DbErr::Type(format!("Relation {rel:?} is not belongs_to"))); - } - - clear_key_on_active_model(&rel_def.from_col, self) - } - #[doc(hidden)] fn clear_parent_key_for_self_rev( &mut self, diff --git a/sea-orm-sync/src/entity/active_model_ex.rs b/sea-orm-sync/src/entity/active_model_ex.rs index 9dd3b92a1..d16d03833 100644 --- a/sea-orm-sync/src/entity/active_model_ex.rs +++ b/sea-orm-sync/src/entity/active_model_ex.rs @@ -204,21 +204,6 @@ where } } -impl PartialEq> for ActiveBelongsTo> -where - E: EntityTrait, - E::ActiveModelEx: PartialEq, -{ - fn eq(&self, other: &Option) -> bool { - match (self, other) { - (Self::NotSet, None) => true, - (Self::Set(Some(a)), Some(b)) => a.as_ref() == b, - (Self::Set(None), None) => true, - _ => false, - } - } -} - impl ActiveHasOne where E: EntityTrait, @@ -285,21 +270,6 @@ where } } -impl PartialEq> for ActiveHasOne -where - E: EntityTrait, - E::ActiveModelEx: PartialEq, -{ - fn eq(&self, other: &Option) -> bool { - match (self, other) { - (Self::NotSet, None) => true, - (Self::Set(Some(a)), Some(b)) => a.as_ref() == b, - (Self::Set(None), None) => true, - _ => false, - } - } -} - impl ActiveHasMany where E: EntityTrait, diff --git a/sea-orm-sync/src/entity/compound/belongs_to.rs b/sea-orm-sync/src/entity/compound/belongs_to.rs index 0c75ceeae..68761367f 100644 --- a/sea-orm-sync/src/entity/compound/belongs_to.rs +++ b/sea-orm-sync/src/entity/compound/belongs_to.rs @@ -148,6 +148,12 @@ where matches!(self, Self::Loaded(None)) } + /// Return true if this optional relation holds no model — i.e. it is either + /// `Unloaded` or was loaded with no match (`is_not_found`). + pub fn is_unloaded_or_not_found(&self) -> bool { + matches!(self, Self::Unloaded | Self::Loaded(None)) + } + /// Get a reference, if loaded with a model pub fn as_ref(&self) -> Option<&E::ModelEx> { match self { @@ -243,54 +249,6 @@ impl From>> for BelongsTo> { } } -impl PartialEq>> for BelongsTo -where - E: EntityTrait, - E::ModelEx: PartialEq, -{ - fn eq(&self, other: &Option>) -> bool { - match (self, other) { - (Self::Loaded(a), Some(b)) => a.as_ref() == b.as_ref(), - (Self::Unloaded, None) => true, - _ => false, - } - } -} - -impl PartialEq>> for BelongsTo> -where - E: EntityTrait, - E::ModelEx: PartialEq, -{ - fn eq(&self, other: &Option>) -> bool { - match (self, other) { - (Self::Loaded(a), b) => a == b, - (Self::Unloaded, None) => true, - _ => false, - } - } -} - -impl PartialEq> for Option> -where - E: EntityTrait, - E::ModelEx: PartialEq, -{ - fn eq(&self, other: &BelongsTo) -> bool { - other == self - } -} - -impl PartialEq>> for Option> -where - E: EntityTrait, - E::ModelEx: PartialEq, -{ - fn eq(&self, other: &BelongsTo>) -> bool { - other == self - } -} - #[cfg(feature = "with-json")] impl serde::Serialize for BelongsTo where diff --git a/sea-orm-sync/src/entity/compound/has_one.rs b/sea-orm-sync/src/entity/compound/has_one.rs index e0f4cd952..c87cd8674 100644 --- a/sea-orm-sync/src/entity/compound/has_one.rs +++ b/sea-orm-sync/src/entity/compound/has_one.rs @@ -108,30 +108,6 @@ impl From>> for HasOne { } } -impl PartialEq>> for HasOne -where - E: EntityTrait, - E::ModelEx: PartialEq, -{ - fn eq(&self, other: &Option>) -> bool { - match (self, other) { - (Self::Loaded(a), b) => a == b, - (Self::Unloaded, None) => true, - _ => false, - } - } -} - -impl PartialEq> for Option> -where - E: EntityTrait, - E::ModelEx: PartialEq, -{ - fn eq(&self, other: &HasOne) -> bool { - other == self - } -} - #[cfg(feature = "with-json")] impl serde::Serialize for HasOne where diff --git a/sea-orm-sync/src/query/util.rs b/sea-orm-sync/src/query/util.rs index 3b8a26b63..9888b3d33 100644 --- a/sea-orm-sync/src/query/util.rs +++ b/sea-orm-sync/src/query/util.rs @@ -111,7 +111,7 @@ where Ok(()) } -/// Set null on the key columns. Return true if succeeded, false if column is not nullable. +/// Set key columns to null. Return false if any column is not nullable. pub fn clear_key_on_active_model( columns: &Identity, model: &mut ActiveModel, @@ -119,6 +119,7 @@ pub fn clear_key_on_active_model( where ActiveModel: ActiveModelTrait, { + let mut parsed_columns = Vec::new(); for col in columns.iter() { let col_name = col.inner(); let column = <::Column as FromStr>::from_str(&col_name) @@ -126,11 +127,26 @@ where if !column.def().is_null() { return Ok(false); } - if let Some(value) = model.get(column).into_value() { - model.set(column, value.as_null()); - } + parsed_columns.push((col_name, column)); } + let values = parsed_columns + .into_iter() + .map(|(col_name, column)| { + let value = model.get(column).into_value().ok_or_else(|| { + DbErr::AttrNotSet(format!( + "{}.{}", + ::default().as_str(), + col_name + )) + })?; + Ok((column, value.as_null())) + }) + .collect::, DbErr>>()?; + + for (column, value) in values { + model.set(column, value); + } Ok(true) } diff --git a/sea-orm-sync/src/tests_cfg/ingredient.rs b/sea-orm-sync/src/tests_cfg/ingredient.rs index 32f01e9c7..04978a099 100644 --- a/sea-orm-sync/src/tests_cfg/ingredient.rs +++ b/sea-orm-sync/src/tests_cfg/ingredient.rs @@ -15,14 +15,14 @@ pub struct Model { pub filling_id: Option, pub ingredient_id: Option, #[sea_orm(belongs_to, from = "filling_id", to = "id")] - pub filling: BelongsTo, + pub filling: BelongsTo>, #[sea_orm( self_ref, relation_enum = "Ingredient", from = "IngredientId", to = "Id" )] - pub ingredient: BelongsTo, + pub ingredient: BelongsTo>, } impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-sync/tests/active_model_ex_tests.rs b/sea-orm-sync/tests/active_model_ex_tests.rs index e69026b07..d724b9553 100644 --- a/sea-orm-sync/tests/active_model_ex_tests.rs +++ b/sea-orm-sync/tests/active_model_ex_tests.rs @@ -24,6 +24,40 @@ mod optional_self_ref { impl ActiveModelBehavior for ActiveModel {} } +mod mixed_composite_parent { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "mixed_composite_parent")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id1: i32, + #[sea_orm(primary_key, auto_increment = false)] + pub id2: i32, + } + + impl ActiveModelBehavior for ActiveModel {} +} + +mod mixed_composite_child { + use sea_orm::entity::prelude::*; + + #[sea_orm::model] + #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] + #[sea_orm(table_name = "mixed_composite_child")] + pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub parent_id1: i32, + pub parent_id2: Option, + #[sea_orm(belongs_to, from = "(parent_id1, parent_id2)", to = "(id1, id2)")] + pub parent: BelongsTo>, + } + + impl ActiveModelBehavior for ActiveModel {} +} + #[sea_orm_macros::test] fn test_active_model_ex_blog() -> Result<(), DbErr> { use common::blogger::*; @@ -720,7 +754,7 @@ fn test_active_model_ex_film_store() -> Result<(), DbErr> { .all(db)?; assert_eq!(staff[0].name, "Alan"); - assert_eq!(staff[0].reports_to, None); + assert!(staff[0].reports_to.is_unloaded_or_not_found()); assert_eq!(staff[0].manages[0].name, "Ben"); assert_eq!(staff[0].manages[1].name, "Alice"); @@ -733,7 +767,7 @@ fn test_active_model_ex_film_store() -> Result<(), DbErr> { assert!(staff[2].manages.is_empty()); assert_eq!(staff[3].name, "Elle"); - assert_eq!(staff[3].reports_to, None); + assert!(staff[3].reports_to.is_unloaded_or_not_found()); assert!(staff[3].manages.is_empty()); info!("delete alan, reports_to should be cleared"); @@ -887,15 +921,19 @@ fn test_clear_belongs_to_clears_unset_fk() -> Result<(), DbErr> { bakers: ActiveHasMany::NotSet, }; - let cleared = partial_cake(cake.id).clear_bakery().update(db)?; + let active_model: cake::ActiveModel = partial_cake(cake.id).clear_bakery().into(); + assert_eq!(active_model.bakery_id, Set(None)); + let cleared = active_model.update(db)?; assert!(cleared.bakery_id.is_none()); let row = cake::Entity::find_by_id(cake.id).one(db)?.expect("cake"); assert!(row.bakery_id.is_none()); - let cleared = partial_cake(cake_with_option.id) + let active_model: cake::ActiveModel = partial_cake(cake_with_option.id) .set_bakery_option(None::) - .update(db)?; + .into(); + assert_eq!(active_model.bakery_id, Set(None)); + let cleared = active_model.update(db)?; assert!(cleared.bakery_id.is_none()); let row = cake::Entity::find_by_id(cake_with_option.id) @@ -943,3 +981,39 @@ fn test_clear_self_ref_belongs_to_clears_unset_fk() -> Result<(), DbErr> { Ok(()) } + +#[sea_orm_macros::test] +fn test_clear_mixed_nullable_composite_belongs_to() -> Result<(), DbErr> { + let ctx = TestContext::new("test_clear_mixed_nullable_composite_belongs_to"); + let db = &ctx.db; + + db.get_schema_builder() + .register(mixed_composite_parent::Entity) + .register(mixed_composite_child::Entity) + .apply(db)?; + + mixed_composite_parent::ActiveModel::builder() + .set_id1(1) + .set_id2(2) + .insert(db)?; + let child = mixed_composite_child::ActiveModel::builder() + .set_parent_id1(1) + .set_parent_id2(Some(2)) + .insert(db)?; + + let cleared = mixed_composite_child::ActiveModelEx { + id: Unchanged(child.id), + parent_id1: NotSet, + parent_id2: NotSet, + parent: ActiveBelongsTo::NotSet, + } + .clear_parent() + .update(db)?; + + assert_eq!(cleared.parent_id1, 1); + assert_eq!(cleared.parent_id2, None); + + ctx.delete(); + + Ok(()) +} diff --git a/sea-orm-sync/tests/common/blogger/attachment.rs b/sea-orm-sync/tests/common/blogger/attachment.rs index 2f821042b..7f2c5091e 100644 --- a/sea-orm-sync/tests/common/blogger/attachment.rs +++ b/sea-orm-sync/tests/common/blogger/attachment.rs @@ -9,7 +9,7 @@ pub struct Model { pub post_id: Option, pub file: String, #[sea_orm(belongs_to, from = "post_id", to = "id")] - pub post: BelongsTo, + pub post: BelongsTo>, } impl ActiveModelBehavior for ActiveModel {} diff --git a/sea-orm-sync/tests/common/features/self_join.rs b/sea-orm-sync/tests/common/features/self_join.rs index 4aabf15fa..3774b0991 100644 --- a/sea-orm-sync/tests/common/features/self_join.rs +++ b/sea-orm-sync/tests/common/features/self_join.rs @@ -9,7 +9,7 @@ pub struct Model { pub uuid_ref: Option, pub time: Option