From a724093395b29aa42d6f2cab3ef90a489d1321d3 Mon Sep 17 00:00:00 2001 From: Shivesh Tiwari Date: Thu, 13 Aug 2026 00:05:38 +0530 Subject: [PATCH 1/6] migrate attribute parsing from bae to darling --- sea-orm-macros/Cargo.toml | 3 +- sea-orm-macros/src/derives/attributes.rs | 199 +++++++++++++++++++---- 2 files changed, 167 insertions(+), 35 deletions(-) diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index 40c5287a87..506f55196a 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -19,6 +19,7 @@ proc-macro = true [dependencies] bae = { version = "0.2", package = "sea-bae", default-features = false, optional = true } +darling = { version = "0.23", optional = true } heck = { version = "0.5", default-features = false } itertools = "0.14" pluralizer = { version = "0.5" } @@ -44,7 +45,7 @@ serde = { version = "1.0", features = ["derive"] } [features] async = [] default = ["derive"] -derive = ["bae"] +derive = ["bae", "darling"] entity-registry = [] postgres-array = [] seaography = ["proc-macro-crate"] diff --git a/sea-orm-macros/src/derives/attributes.rs b/sea-orm-macros/src/derives/attributes.rs index 5bc8212668..aef337a0d3 100644 --- a/sea-orm-macros/src/derives/attributes.rs +++ b/sea-orm-macros/src/derives/attributes.rs @@ -1,44 +1,157 @@ +use darling::ast::NestedMeta; +use darling::FromMeta; +use syn::punctuated::Punctuated; +use syn::{Attribute, Meta, MetaList, MetaNameValue, Token}; + +fn parse_sea_orm_attrs(attrs: &[Attribute]) -> syn::Result> { + let parser = Punctuated::::parse_terminated; + let mut metas = Vec::new(); + for attr in attrs { + if attr.path().is_ident("sea_orm") { + metas.extend(attr.parse_args_with(parser)?); + } + } + Ok(metas) +} + +fn meta_ident(meta: &NestedMeta) -> Option { + let path = match meta { + NestedMeta::Meta(Meta::Path(path)) + | NestedMeta::Meta(Meta::List(MetaList { path, .. })) + | NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, .. })) => path, + _ => return None, + }; + path.get_ident().map(|ident| ident.to_string()) +} + +// Pre-filter the parsed metas before handing them to darling: +// drop any that aren't a known field, and drop bare words on value fields +// (only flag fields accept a bare word). Otherwise darling rejects the whole list. +fn filter_metas(metas: Vec, known: &[&str], flags: &[&str]) -> Vec { + metas + .into_iter() + .filter(|meta| { + let Some(name) = meta_ident(meta) else { + return false; + }; + if !known.contains(&name.as_str()) { + return false; + } + let is_bare = matches!(meta, NestedMeta::Meta(Meta::Path(_))); + if is_bare && !flags.contains(&name.as_str()) { + return false; + } + true + }) + .collect() +} + +fn darling_err(e: darling::Error) -> syn::Error { + syn::Error::new(e.span(), e) +} + +fn try_from_attrs(attrs: &[Attribute], known: &[&str], flags: &[&str]) -> syn::Result> { + let metas = filter_metas(parse_sea_orm_attrs(attrs)?, known, flags); + if metas.is_empty() { + Ok(None) + } else { + T::from_list(&metas).map(Some).map_err(darling_err) + } +} + +fn from_attrs(attrs: &[Attribute], known: &[&str], flags: &[&str]) -> syn::Result { + let metas = filter_metas(parse_sea_orm_attrs(attrs)?, known, flags); + T::from_list(&metas).map_err(darling_err) +} + pub mod derive_attr { - use bae::FromAttributes; + use super::*; + use syn::{Ident, LitStr}; + + const KNOWN: &[&str] = &[ + "column", + "entity", + "model", + "model_ex", + "active_model", + "active_model_ex", + "primary_key", + "relation", + "schema_name", + "table_name", + "comment", + "rename_all", + "table_iden", + ]; + const FLAGS: &[&str] = &["table_iden"]; /// Attributes for Models and ActiveModels - #[derive(Default, FromAttributes)] + #[derive(Default, FromMeta)] #[allow(dead_code)] pub struct SeaOrm { - pub column: Option, - pub entity: Option, - pub model: Option, - pub model_ex: Option, - pub active_model: Option, - pub active_model_ex: Option, - pub primary_key: Option, - pub relation: Option, - pub schema_name: Option, - pub table_name: Option, - pub comment: Option, + pub column: Option, + pub entity: Option, + pub model: Option, + pub model_ex: Option, + pub active_model: Option, + pub active_model_ex: Option, + pub primary_key: Option, + pub relation: Option, + pub schema_name: Option, + pub table_name: Option, + pub comment: Option, pub table_iden: Option<()>, - pub rename_all: Option, + pub rename_all: Option, + } + + impl SeaOrm { + pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { + try_from_attrs::(attrs, KNOWN, FLAGS) + } } } pub mod relation_attr { - use bae::FromAttributes; + use super::*; + use syn::Lit; + + const KNOWN: &[&str] = &[ + "belongs_to", + "has_one", + "has_many", + "via_rel", + "on_update", + "on_delete", + "on_condition", + "from", + "to", + "fk_name", + "skip_fk", + "condition_type", + ]; + const FLAGS: &[&str] = &["skip_fk"]; /// Attributes for Relation enum - #[derive(Default, FromAttributes)] + #[derive(Default, FromMeta)] pub struct SeaOrm { - pub belongs_to: Option, - pub has_one: Option, - pub has_many: Option, - pub via_rel: Option, - pub on_update: Option, - pub on_delete: Option, - pub on_condition: Option, - pub from: Option, - pub to: Option, - pub fk_name: Option, + pub belongs_to: Option, + pub has_one: Option, + pub has_many: Option, + pub via_rel: Option, + pub on_update: Option, + pub on_delete: Option, + pub on_condition: Option, + pub from: Option, + pub to: Option, + pub fk_name: Option, pub skip_fk: Option<()>, - pub condition_type: Option, + pub condition_type: Option, + } + + impl SeaOrm { + pub fn from_attributes(attrs: &[Attribute]) -> syn::Result { + from_attrs::(attrs, KNOWN, FLAGS) + } } } @@ -66,19 +179,37 @@ pub mod compound_attr { } pub mod value_type_attr { - use bae::FromAttributes; + use super::*; + use syn::LitStr; + + const KNOWN: &[&str] = &[ + "column_type", + "array_type", + "value_type", + "from_str", + "to_str", + "try_from_u64", + "try_getable_array", + ]; + const FLAGS: &[&str] = &["try_from_u64", "try_getable_array"]; /// Attributes for compound model fields - #[derive(Default, FromAttributes)] + #[derive(Default, FromMeta)] pub struct SeaOrm { - pub column_type: Option, - pub array_type: Option, - pub value_type: Option, - pub from_str: Option, - pub to_str: Option, + pub column_type: Option, + pub array_type: Option, + pub value_type: Option, + pub from_str: Option, + pub to_str: Option, pub try_from_u64: Option<()>, pub try_getable_array: Option<()>, } + + impl SeaOrm { + pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { + try_from_attrs::(attrs, KNOWN, FLAGS) + } + } } #[cfg(feature = "seaography")] From d76c4a765c83251f7341141bd27e39124ad4793f Mon Sep 17 00:00:00 2001 From: Shivesh Tiwari Date: Thu, 13 Aug 2026 01:25:44 +0530 Subject: [PATCH 2/6] remove bae in favor of darling for attribute parsing --- sea-orm-macros/Cargo.toml | 3 +- sea-orm-macros/src/derives/attributes.rs | 73 +++++++++++++++++++----- 2 files changed, 60 insertions(+), 16 deletions(-) diff --git a/sea-orm-macros/Cargo.toml b/sea-orm-macros/Cargo.toml index 506f55196a..471259efa6 100644 --- a/sea-orm-macros/Cargo.toml +++ b/sea-orm-macros/Cargo.toml @@ -18,7 +18,6 @@ path = "src/lib.rs" proc-macro = true [dependencies] -bae = { version = "0.2", package = "sea-bae", default-features = false, optional = true } darling = { version = "0.23", optional = true } heck = { version = "0.5", default-features = false } itertools = "0.14" @@ -45,7 +44,7 @@ serde = { version = "1.0", features = ["derive"] } [features] async = [] default = ["derive"] -derive = ["bae", "darling"] +derive = ["darling"] entity-registry = [] postgres-array = [] seaography = ["proc-macro-crate"] diff --git a/sea-orm-macros/src/derives/attributes.rs b/sea-orm-macros/src/derives/attributes.rs index aef337a0d3..b293c1e515 100644 --- a/sea-orm-macros/src/derives/attributes.rs +++ b/sea-orm-macros/src/derives/attributes.rs @@ -156,25 +156,57 @@ pub mod relation_attr { } pub mod compound_attr { - use bae::FromAttributes; + use super::*; + use syn::LitStr; + + const KNOWN: &[&str] = &[ + "has_one", + "has_many", + "belongs_to", + "self_ref", + "skip_fk", + "via", + "via_rel", + "from", + "to", + "relation_enum", + "relation_reverse", + "reverse", + "on_update", + "on_delete", + ]; + const FLAGS: &[&str] = &[ + "has_one", + "has_many", + "belongs_to", + "self_ref", + "skip_fk", + "reverse", + ]; /// Attributes for compound model fields - #[derive(Default, FromAttributes)] + #[derive(Default, FromMeta)] pub struct SeaOrm { pub has_one: Option<()>, pub has_many: Option<()>, pub belongs_to: Option<()>, pub self_ref: Option<()>, pub skip_fk: Option<()>, - pub via: Option, - pub via_rel: Option, - pub from: Option, - pub to: Option, - pub relation_enum: Option, - pub relation_reverse: Option, + pub via: Option, + pub via_rel: Option, + pub from: Option, + pub to: Option, + pub relation_enum: Option, + pub relation_reverse: Option, pub reverse: Option<()>, - pub on_update: Option, - pub on_delete: Option, + pub on_update: Option, + pub on_delete: Option, + } + + impl SeaOrm { + pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { + try_from_attrs::(attrs, KNOWN, FLAGS) + } } } @@ -214,10 +246,14 @@ pub mod value_type_attr { #[cfg(feature = "seaography")] pub mod related_attr { - use bae::FromAttributes; + use super::*; + use syn::Lit; + + const KNOWN: &[&str] = &["entity", "def"]; + const FLAGS: &[&str] = &[]; /// Attributes for RelatedEntity enum - #[derive(Default, FromAttributes)] + #[derive(Default, FromMeta)] pub struct SeaOrm { /// /// Allows to modify target entity @@ -227,7 +263,7 @@ pub mod related_attr { /// If used on enumeration attributes /// it allows to specify different /// Entity ident - pub entity: Option, + pub entity: Option, /// /// Allows to specify RelationDef /// @@ -235,6 +271,15 @@ pub mod related_attr { /// /// If not supplied the generated code /// will utilize `impl Related` trait - pub def: Option, + pub def: Option, + } + + impl SeaOrm { + pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { + try_from_attrs::(attrs, KNOWN, FLAGS) + } + pub fn from_attributes(attrs: &[Attribute]) -> syn::Result { + from_attrs::(attrs, KNOWN, FLAGS) + } } } From b6d247976b49f72d5ff4a0a3598197b7a72853c8 Mon Sep 17 00:00:00 2001 From: Shivesh Tiwari Date: Thu, 13 Aug 2026 02:25:54 +0530 Subject: [PATCH 3/6] replace string/Ident round-trips with format_ident! and direct Ident comparison --- sea-orm-macros/src/derives/active_model_ex.rs | 26 ++++++------- sea-orm-macros/src/derives/attributes.rs | 12 ++++-- sea-orm-macros/src/derives/case_style.rs | 2 +- sea-orm-macros/src/derives/entity_loader.rs | 38 +++++++++---------- sea-orm-macros/src/derives/entity_model.rs | 10 ++--- .../src/derives/into_active_model.rs | 2 +- sea-orm-macros/src/derives/model_ex.rs | 38 +++++++++---------- sea-orm-macros/src/derives/partial_model.rs | 11 ++---- sea-orm-macros/src/derives/related_entity.rs | 6 +-- sea-orm-macros/src/derives/typed_column.rs | 11 +++--- sea-orm-macros/src/derives/util.rs | 29 +++++++------- .../src/derives/value_type_match.rs | 12 ++---- sea-orm-macros/src/raw_sql.rs | 10 ++--- 13 files changed, 97 insertions(+), 110 deletions(-) diff --git a/sea-orm-macros/src/derives/active_model_ex.rs b/sea-orm-macros/src/derives/active_model_ex.rs index b095a28367..c23f1272f0 100644 --- a/sea-orm-macros/src/derives/active_model_ex.rs +++ b/sea-orm-macros/src/derives/active_model_ex.rs @@ -6,7 +6,7 @@ use super::util::{ await_token, consume_meta, escape_rust_keyword, is_self_entity, trim_starting_raw_identifier, }; use heck::ToUpperCamelCase; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use syn::{Attribute, Data, LitStr, PathArguments, Type, TypePath, Visibility}; @@ -192,7 +192,7 @@ struct ScalarField<'a> { impl<'a> ScalarField<'a> { fn from_field(field: &'a syn::Field, ident: &Ident) -> syn::Result { let column = trim_starting_raw_identifier(ident).to_upper_camel_case(); - let mut column = Ident::new(&escape_rust_keyword(column), ident.span()); + let mut column = format_ident!("{}", escape_rust_keyword(column)); for attr in &field.attrs { if !attr.path().is_ident("sea_orm") { @@ -612,18 +612,17 @@ impl ActiveModelActionTokens { // variant (there is no canonical `Related` to key by); // a unique target with no explicit enum uses the entity-keyed path. let relation_lookup = match relation_enum { - Some(relation_enum) => RelationLookup::ByRelationVariant(Ident::new( - &relation_enum.value().to_upper_camel_case(), - relation_enum.span(), - )), + Some(relation_enum) => RelationLookup::ByRelationVariant( + format_ident!("{}", relation_enum.value().to_upper_camel_case()), + ), 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(), + RelationLookup::ByRelationVariant(format_ident!( + "{}", + infer_relation_name_from_entity(&compound_type.entity) + .to_upper_camel_case() )) } } @@ -1216,8 +1215,7 @@ impl HasManySelfField<'_> { quote!() }; let ident = self.ident; - let relation_variant = - Ident::new(&self.relation_variant.value(), self.relation_variant.span()); + let relation_variant = format_ident!("{}", self.relation_variant.value()); let relation_variant = quote!(Relation::#relation_variant); let delete_associated_model = quote! { @@ -1297,8 +1295,8 @@ impl ManyToManyField<'_> { ("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 establish_links = format_ident!("{}", establish_links); + let delete_links = format_ident!("{}", delete_links); let many_to_many_before_action = quote! { let #ident = self.#ident.take(); diff --git a/sea-orm-macros/src/derives/attributes.rs b/sea-orm-macros/src/derives/attributes.rs index b293c1e515..4fb661e0cc 100644 --- a/sea-orm-macros/src/derives/attributes.rs +++ b/sea-orm-macros/src/derives/attributes.rs @@ -1,5 +1,5 @@ -use darling::ast::NestedMeta; use darling::FromMeta; +use darling::ast::NestedMeta; use syn::punctuated::Punctuated; use syn::{Attribute, Meta, MetaList, MetaNameValue, Token}; @@ -24,8 +24,8 @@ fn meta_ident(meta: &NestedMeta) -> Option { path.get_ident().map(|ident| ident.to_string()) } -// Pre-filter the parsed metas before handing them to darling: -// drop any that aren't a known field, and drop bare words on value fields +// Pre-filter the parsed metas before handing them to darling: +// drop any that aren't a known field, and drop bare words on value fields // (only flag fields accept a bare word). Otherwise darling rejects the whole list. fn filter_metas(metas: Vec, known: &[&str], flags: &[&str]) -> Vec { metas @@ -50,7 +50,11 @@ fn darling_err(e: darling::Error) -> syn::Error { syn::Error::new(e.span(), e) } -fn try_from_attrs(attrs: &[Attribute], known: &[&str], flags: &[&str]) -> syn::Result> { +fn try_from_attrs( + attrs: &[Attribute], + known: &[&str], + flags: &[&str], +) -> syn::Result> { let metas = filter_metas(parse_sea_orm_attrs(attrs)?, known, flags); if metas.is_empty() { Ok(None) diff --git a/sea-orm-macros/src/derives/case_style.rs b/sea-orm-macros/src/derives/case_style.rs index 917dda05af..e2510a11a8 100644 --- a/sea-orm-macros/src/derives/case_style.rs +++ b/sea-orm-macros/src/derives/case_style.rs @@ -127,7 +127,7 @@ impl TryFrom<&ParseNestedMeta<'_>> for CaseStyle { #[test] fn test_convert_case() { - let id = Ident::new("test_me", proc_macro2::Span::call_site()); + let id = quote::format_ident!("test_me"); assert_eq!("testMe", id.convert_case(Some(CaseStyle::CamelCase))); assert_eq!("TestMe", id.convert_case(Some(CaseStyle::PascalCase))); } diff --git a/sea-orm-macros/src/derives/entity_loader.rs b/sea-orm-macros/src/derives/entity_loader.rs index a9bc84592d..926bffc751 100644 --- a/sea-orm-macros/src/derives/entity_loader.rs +++ b/sea-orm-macros/src/derives/entity_loader.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use std::collections::{HashMap, HashSet}; use syn::{Ident, TypePath, Visibility, punctuated::Punctuated, token::Comma}; @@ -20,6 +20,16 @@ pub enum EntityLoaderFieldKind { }, } +/// Ident for the `LoadTarget` variant used by a self-referential many-to-many +/// junction: `TableRef` for the forward direction, `TableRefRev` for the reverse. +fn table_ref_ident(reverse: bool) -> Ident { + if reverse { + format_ident!("TableRefRev") + } else { + format_ident!("TableRef") + } +} + pub struct EntityLoaderField { pub field: Ident, /// super::bakery::Entity @@ -115,11 +125,7 @@ impl EntityLoaderField { junction_module: &Ident, reverse: bool, ) { - let target_type = if !reverse { - Ident::new("TableRef", junction_module.span()) - } else { - Ident::new("TableRefRev", junction_module.span()) - }; + let target_type = table_ref_ident(reverse); let target_entity = if !reverse { quote!(super::#junction_module::Entity) @@ -195,11 +201,7 @@ impl EntityLoaderField { reverse, } = &self.kind { - let target_type = if !reverse { - Ident::new("TableRef", junction_module.span()) - } else { - Ident::new("TableRefRev", junction_module.span()) - }; + let target_type = table_ref_ident(*reverse); output.loader_with_set_impl.extend(quote! { if target == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) { @@ -248,11 +250,7 @@ impl EntityLoaderField { junction_module, reverse, } => { - let target_type = if !reverse { - Ident::new("TableRef", junction_module.span()) - } else { - Ident::new("TableRefRev", junction_module.span()) - }; + let target_type = table_ref_ident(*reverse); output.loader_with_2_impl.extend(quote! { if left == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) { @@ -339,7 +337,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + let relation_enum = format_ident!("{}", relation_enum.value()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -448,7 +446,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + let relation_enum = format_ident!("{}", relation_enum.value()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -511,7 +509,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + let relation_enum = format_ident!("{}", relation_enum.value()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -536,7 +534,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); + let relation_enum = format_ident!("{}", relation_enum.value()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index 8a877fa618..ad042ff7c3 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -3,8 +3,8 @@ use super::util::{consume_meta, escape_rust_keyword, trim_starting_raw_identifie use heck::{ ToKebabCase, ToLowerCamelCase, ToShoutySnakeCase, ToSnakeCase, ToTitleCase, ToUpperCamelCase, }; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; +use proc_macro2::{Ident, TokenStream}; +use quote::{format_ident, quote}; use std::str::FromStr; use syn::{ Attribute, Data, Fields, Lit, LitStr, Visibility, punctuated::Punctuated, spanned::Spanned, @@ -172,7 +172,7 @@ pub fn expand_derive_entity_model( if table_iden { if let Some(table_name) = &table_name { - let table_field_name = Ident::new("Table", Span::call_site()); + let table_field_name = format_ident!("Table"); columns_enum.push(quote! { #[doc = " Generated by sea-orm-macros"] #[sea_orm(table_name=#table_name)] @@ -190,7 +190,7 @@ pub fn expand_derive_entity_model( if let Some(ident) = &field.ident { let original_field_name = trim_starting_raw_identifier(ident); let mut field_name = - Ident::new(&original_field_name.to_upper_camel_case(), ident.span()); + format_ident!("{}", original_field_name.to_upper_camel_case()); let mut nullable = false; let mut default_value = None; @@ -387,7 +387,7 @@ pub fn expand_derive_entity_model( field_name = enum_name; } - field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); + field_name = format_ident!("{}", escape_rust_keyword(field_name)); let variant_attrs = match &column_name { Some(column_name) => quote! { diff --git a/sea-orm-macros/src/derives/into_active_model.rs b/sea-orm-macros/src/derives/into_active_model.rs index 4a0a6f2c03..19f34b93d3 100644 --- a/sea-orm-macros/src/derives/into_active_model.rs +++ b/sea-orm-macros/src/derives/into_active_model.rs @@ -141,7 +141,7 @@ impl DeriveIntoActiveModel { let mut active_model_ident = active_model .clone() - .unwrap_or_else(|| syn::parse_str::("ActiveModel").unwrap()); + .unwrap_or_else(|| syn::parse_quote!(ActiveModel)); // Create a type alias for qualified types let type_alias_definition = if is_qualified_type(&active_model_ident) { diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs index b1dc78d546..a69e27292d 100644 --- a/sea-orm-macros/src/derives/model_ex.rs +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -10,7 +10,7 @@ use super::util::{ }; use super::{expand_typed_column, model::DeriveModel}; use heck::ToUpperCamelCase; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use std::collections::{BTreeMap, HashMap}; use syn::{ @@ -272,11 +272,11 @@ impl RelationVariants { fn forward_ident(&self, entity: &TypePath) -> Ident { if let Some(name) = self.explicit_name() { - Ident::new(&name.value().to_upper_camel_case(), name.span()) + format_ident!("{}", name.value().to_upper_camel_case()) } else { - Ident::new( - &infer_relation_name_from_entity(entity).to_upper_camel_case(), - Span::call_site(), + format_ident!( + "{}", + infer_relation_name_from_entity(entity).to_upper_camel_case() ) } } @@ -287,9 +287,9 @@ impl RelationVariants { } = self { Some(if let Some(reverse_variant) = reverse_variant { - Ident::new(&reverse_variant.value(), reverse_variant.span()) + format_ident!("{}", reverse_variant.value()) } else { - Ident::new(&format!("{forward}Reverse"), forward.span()) + format_ident!("{}Reverse", forward) }) } else { None @@ -823,7 +823,7 @@ fn expand_belongs_to_into( .map(|segment| segment.ident.to_string()) .collect::>() .join("::"); - let belongs_to = Ident::new("belongs_to", Span::call_site()); + let belongs_to = format_ident!("belongs_to"); let format_columns = |columns: &RelationColumns, prefix: &str| { let columns = columns .columns @@ -856,11 +856,11 @@ fn expand_belongs_to_into( }; let mut extra: Punctuated<_, Comma> = Punctuated::new(); if let Some(on_update) = &relation.on_update { - let tag = Ident::new("on_update", on_update.span()); + let tag = format_ident!("on_update"); extra.push(quote!(#tag = #on_update)); } if let Some(on_delete) = &relation.on_delete { - let tag = Ident::new("on_delete", on_delete.span()); + let tag = format_ident!("on_delete"); extra.push(quote!(#tag = #on_delete)); } if relation.skip_fk { @@ -994,10 +994,10 @@ fn expand_has_one_into( .map(|segment| segment.ident.to_string()) .collect::>() .join("::"); - let has_one = Ident::new("has_one", Span::call_site()); + let has_one = format_ident!("has_one"); let mut extra: Punctuated<_, Comma> = Punctuated::new(); if let Some(via_rel) = &attr.via_rel { - let tag = Ident::new("via_rel", via_rel.span()); + let tag = format_ident!("via_rel"); let via_rel = format!("Relation::{}", via_rel.value()); extra.push(quote!(#tag = #via_rel)); } @@ -1185,7 +1185,7 @@ fn expand_has_many_into( .. } = relation_variants { - let has_many = Ident::new("has_many", Span::call_site()); + let has_many = format_ident!("has_many"); let via_rel = format!("Relation::{}", relation_reverse.value()); output.relation_enum_variants.push(quote! { #[doc = " Generated by sea-orm-macros"] @@ -1200,10 +1200,10 @@ fn expand_has_many_into( .map(|segment| segment.ident.to_string()) .collect::>() .join("::"); - let has_many = Ident::new("has_many", Span::call_site()); + let has_many = format_ident!("has_many"); let mut extra: Punctuated<_, Comma> = Punctuated::new(); if let Some(via_rel) = via_rel { - let tag = Ident::new("via_rel", via_rel.span()); + let tag = format_ident!("via_rel"); let via_rel = format!("Relation::{}", via_rel.value()); extra.push(quote!(#tag = #via_rel)); } @@ -1409,8 +1409,8 @@ fn expand_impl_related_many_to_many_self( ) -> TokenStream { match direction { ManyToManySelfDirection::Forward { from, to } => { - let from = Ident::new(&from.value(), from.span()); - let to = Ident::new(&to.value(), to.span()); + let from = format_ident!("{}", from.value()); + let to = format_ident!("{}", to.value()); quote! { #[doc = " Generated by sea-orm-macros"] @@ -1485,7 +1485,7 @@ fn expand_find_by_unique_key(schema: &ModelExSchema<'_>) -> (TokenStream, TokenS .enumerate() .map(|(i, (col, _))| { let i = syn::Index::from(i); - let col = Ident::new(&col.to_string().to_upper_camel_case(), col.span()); + let col = format_ident!("{}", col.to_string().to_upper_camel_case()); quote!(Column::#col.eq(v.#i)) }) .collect::>(); @@ -1512,7 +1512,7 @@ fn expand_find_by_unique_key(schema: &ModelExSchema<'_>) -> (TokenStream, TokenS }); } else { let col = &columns[0].0; - let col = Ident::new(&col.to_string().to_upper_camel_case(), col.span()); + let col = format_ident!("{}", col.to_string().to_upper_camel_case()); let key_type = &columns[0].1; entity_find_by_key.extend(quote! { #[doc = " Generated by sea-orm-macros"] diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index c72882de4a..bf6b6ca07f 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -69,7 +69,6 @@ impl DerivePartialModel { }; let mut entity = None; - let mut entity_string = String::new(); let mut active_model = None; let mut model_alias = None; let mut from_query_result = true; @@ -84,7 +83,6 @@ impl DerivePartialModel { for meta in list { if let Some(s) = meta.get_as_kv("entity") { entity = Some(syn::parse_str::(&s).map_err(Error::Syn)?); - entity_string = s; } else if let Some(s) = meta.get_as_kv("alias") { model_alias = Some(s); } else if let Some(s) = meta.get_as_kv("from_query_result") { @@ -99,12 +97,9 @@ impl DerivePartialModel { } if into_active_model { - active_model = Some( - syn::parse_str::(&format!( - "<{entity_string} as EntityTrait>::ActiveModel" - )) - .map_err(Error::Syn)?, - ); + if let Some(entity) = &entity { + active_model = Some(syn::parse_quote!(<#entity as EntityTrait>::ActiveModel)); + } } let mut column_as_list = Vec::with_capacity(fields.len()); diff --git a/sea-orm-macros/src/derives/related_entity.rs b/sea-orm-macros/src/derives/related_entity.rs index 558b776b41..e5c8ad1a48 100644 --- a/sea-orm-macros/src/derives/related_entity.rs +++ b/sea-orm-macros/src/derives/related_entity.rs @@ -2,8 +2,8 @@ mod private { use heck::ToLowerCamelCase; use proc_macro_crate::{FoundCrate, crate_name}; - use proc_macro2::{Ident, Span, TokenStream}; - use quote::{quote, quote_spanned}; + use proc_macro2::TokenStream; + use quote::{format_ident, quote, quote_spanned}; use crate::derives::attributes::related_attr; @@ -94,7 +94,7 @@ mod private { let async_graphql_crate = match crate_name("async-graphql") { // if found, use application's `async-graphql` Ok(FoundCrate::Name(name)) => { - let ident = Ident::new(&name, Span::call_site()); + let ident = format_ident!("{}", name); quote! { #ident } } Ok(FoundCrate::Itself) => quote! { async_graphql }, diff --git a/sea-orm-macros/src/derives/typed_column.rs b/sea-orm-macros/src/derives/typed_column.rs index 34a1726ec7..e1bc802e3e 100644 --- a/sea-orm-macros/src/derives/typed_column.rs +++ b/sea-orm-macros/src/derives/typed_column.rs @@ -3,8 +3,8 @@ use crate::derives::util::consume_meta; use super::util::{CompoundType, escape_rust_keyword, trim_starting_raw_identifier}; use heck::ToUpperCamelCase; use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use syn::{Data, Fields, Lit, Visibility, spanned::Spanned}; +use quote::{format_ident, quote}; +use syn::{Data, Fields, Lit, Visibility}; /// First is `struct TypedColumn`, second is the `const COLUMN` pub fn expand_typed_column( @@ -21,7 +21,7 @@ pub fn expand_typed_column( for field in &fields.named { let Some(ident) = &field.ident else { continue }; let field_name = trim_starting_raw_identifier(ident); - let mut field_name = Ident::new(&field_name.to_upper_camel_case(), ident.span()); + let mut field_name = format_ident!("{}", field_name.to_upper_camel_case()); let field_ty = &field.ty; @@ -68,11 +68,10 @@ pub fn expand_typed_column( continue; } - field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); + field_name = format_ident!("{}", escape_rust_keyword(field_name)); column_fields.push(ident.clone()); - let wrapper = - super::value_type_match::column_type_wrapper(&column_type, field_ty, field.span()); + let wrapper = super::value_type_match::column_type_wrapper(&column_type, field_ty); column_types.push(if let Some(wrapper) = &wrapper { quote!(sea_orm::#wrapper) } else { diff --git a/sea-orm-macros/src/derives/util.rs b/sea-orm-macros/src/derives/util.rs index 19a72c5350..252e04de1e 100644 --- a/sea-orm-macros/src/derives/util.rs +++ b/sea-orm-macros/src/derives/util.rs @@ -1,6 +1,6 @@ use heck::ToUpperCamelCase; use proc_macro2::{Ident, Span, TokenStream}; -use quote::quote; +use quote::{format_ident, quote}; use syn::{ Field, GenericArgument, LitStr, Meta, MetaNameValue, PathArguments, Type, TypePath, meta::ParseNestedMeta, punctuated::Punctuated, token::Comma, @@ -54,9 +54,9 @@ impl RelationColumns { 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(), + Ok(format_ident!( + "{}", + escape_rust_keyword(segment.ident.to_string().to_upper_camel_case()) )) }) .collect::>>()?; @@ -125,8 +125,8 @@ impl CompoundType { pub(crate) fn matches_type(type_path: &TypePath) -> bool { last_path_segment(type_path).is_ok_and(|segment| { matches!( - segment.ident.to_string().as_str(), - "BelongsTo" | "HasOne" | "HasMany" + &segment.ident, + id if *id == "BelongsTo" || *id == "HasOne" || *id == "HasMany" ) }) } @@ -135,8 +135,8 @@ impl CompoundType { pub(crate) fn from_type(type_path: &TypePath) -> syn::Result> { let segment = last_path_segment(type_path)?; - match segment.ident.to_string().as_str() { - "BelongsTo" => { + match &segment.ident { + id if *id == "BelongsTo" => { let PathArguments::AngleBracketed(args) = &segment.arguments else { return Err(syn::Error::new_spanned( type_path, @@ -163,15 +163,12 @@ impl CompoundType { )); }; let target_segment = last_path_segment(ty_path)?; - match ( - target_segment.ident.to_string().as_str(), - &target_segment.arguments, - ) { - ("Entity", _) => Ok(Some(Self { + match (&target_segment.ident, &target_segment.arguments) { + (id, _) if *id == "Entity" => Ok(Some(Self { kind: CompoundKind::BelongsTo(CardinalityKind::Required), entity: ty_path.clone(), })), - ("Option", PathArguments::AngleBracketed(args)) => { + (id, PathArguments::AngleBracketed(args)) if *id == "Option" => { let Some(entity) = entity_generic_arg(&args.args) else { return Err(syn::Error::new_spanned( ty, @@ -189,7 +186,7 @@ impl CompoundType { )), } } - "HasOne" => { + id if *id == "HasOne" => { let PathArguments::AngleBracketed(args) = &segment.arguments else { return Err(syn::Error::new_spanned( type_path, @@ -207,7 +204,7 @@ impl CompoundType { entity, })) } - "HasMany" => { + id if *id == "HasMany" => { let PathArguments::AngleBracketed(args) = &segment.arguments else { return Err(syn::Error::new_spanned( type_path, diff --git a/sea-orm-macros/src/derives/value_type_match.rs b/sea-orm-macros/src/derives/value_type_match.rs index b09af6e071..394f7947eb 100644 --- a/sea-orm-macros/src/derives/value_type_match.rs +++ b/sea-orm-macros/src/derives/value_type_match.rs @@ -1,5 +1,5 @@ use proc_macro2::{Span, TokenStream}; -use quote::quote_spanned; +use quote::{format_ident, quote_spanned}; use syn::{GenericArgument, Ident, LitStr, PathArguments, Type, TypePath}; pub fn column_type_expr( @@ -20,11 +20,7 @@ pub fn column_type_expr( } } -pub fn column_type_wrapper( - column_type: &Option, - field_type: &Type, - field_span: Span, -) -> Option { +pub fn column_type_wrapper(column_type: &Option, field_type: &Type) -> Option { let (nullable, field_type) = if let Type::Path(type_path) = field_type && let Some(inner) = generic_type_arg(type_path, "Option") { @@ -66,7 +62,7 @@ pub fn column_type_wrapper( "Array" => Some("GenericArrayColumn"), _ => None, } - .map(|ty| Ident::new(ty, field_span)); + .map(|ty| format_ident!("{}", ty)); if value_type.is_some() { return value_type; @@ -123,7 +119,7 @@ pub fn column_type_wrapper( None }; - value_type.map(|ty| Ident::new(ty, field_span)) + value_type.map(|ty| format_ident!("{}", ty)) } fn generic_type_arg<'a>(type_path: &'a TypePath, ident: &str) -> Option<&'a Type> { diff --git a/sea-orm-macros/src/raw_sql.rs b/sea-orm-macros/src/raw_sql.rs index 5a77622873..217840a279 100644 --- a/sea-orm-macros/src/raw_sql.rs +++ b/sea-orm-macros/src/raw_sql.rs @@ -28,11 +28,11 @@ pub fn expand(input: proc_macro::TokenStream) -> syn::Result { .. } = syn::parse(input)?; - let builder = match backend.to_string().as_str() { - "MySql" => quote!(MysqlQueryBuilder), - "Postgres" => quote!(PostgresQueryBuilder), - "Sqlite" => quote!(SqliteQueryBuilder), - backend => panic!("Unsupported backend {backend}"), + let builder = match &backend { + id if *id == "MySql" => quote!(MysqlQueryBuilder), + id if *id == "Postgres" => quote!(PostgresQueryBuilder), + id if *id == "Sqlite" => quote!(SqliteQueryBuilder), + _ => panic!("Unsupported backend {backend}"), }; Ok(quote! {{ From 5f7c80e5e8c2289fb3eaac9dfa8ec7911fca8968 Mon Sep 17 00:00:00 2001 From: Shivesh Tiwari Date: Thu, 13 Aug 2026 14:00:44 +0530 Subject: [PATCH 4/6] replace manual attribute parsing layer with darling FromAttributes and restore spans --- sea-orm-macros/src/derives/active_model_ex.rs | 15 +- sea-orm-macros/src/derives/attributes.rs | 193 +++++------------- sea-orm-macros/src/derives/entity.rs | 5 +- sea-orm-macros/src/derives/entity_loader.rs | 23 ++- sea-orm-macros/src/derives/entity_model.rs | 4 +- sea-orm-macros/src/derives/model_ex.rs | 20 +- sea-orm-macros/src/derives/typed_column.rs | 6 +- .../src/derives/value_type_match.rs | 10 +- 8 files changed, 92 insertions(+), 184 deletions(-) diff --git a/sea-orm-macros/src/derives/active_model_ex.rs b/sea-orm-macros/src/derives/active_model_ex.rs index c23f1272f0..a0b7a26d4f 100644 --- a/sea-orm-macros/src/derives/active_model_ex.rs +++ b/sea-orm-macros/src/derives/active_model_ex.rs @@ -192,7 +192,7 @@ struct ScalarField<'a> { impl<'a> ScalarField<'a> { fn from_field(field: &'a syn::Field, ident: &Ident) -> syn::Result { let column = trim_starting_raw_identifier(ident).to_upper_camel_case(); - let mut column = format_ident!("{}", escape_rust_keyword(column)); + let mut column = Ident::new(&escape_rust_keyword(column), ident.span()); for attr in &field.attrs { if !attr.path().is_ident("sea_orm") { @@ -612,9 +612,10 @@ impl ActiveModelActionTokens { // variant (there is no canonical `Related` to key by); // a unique target with no explicit enum uses the entity-keyed path. let relation_lookup = match relation_enum { - Some(relation_enum) => RelationLookup::ByRelationVariant( - format_ident!("{}", relation_enum.value().to_upper_camel_case()), - ), + Some(relation_enum) => RelationLookup::ByRelationVariant(Ident::new( + &relation_enum.value().to_upper_camel_case(), + relation_enum.span(), + )), None => { if is_unique_relation_target { RelationLookup::ByRelatedEntity @@ -1215,7 +1216,7 @@ impl HasManySelfField<'_> { quote!() }; let ident = self.ident; - let relation_variant = format_ident!("{}", self.relation_variant.value()); + 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! { @@ -1295,8 +1296,8 @@ impl ManyToManyField<'_> { ("establish_links_self_rev", "delete_links_self") } }; - let establish_links = format_ident!("{}", establish_links); - let delete_links = format_ident!("{}", delete_links); + let establish_links = Ident::new(establish_links, ident.span()); + let delete_links = Ident::new(delete_links, ident.span()); let many_to_many_before_action = quote! { let #ident = self.#ident.take(); diff --git a/sea-orm-macros/src/derives/attributes.rs b/sea-orm-macros/src/derives/attributes.rs index 4fb661e0cc..26ac19acc7 100644 --- a/sea-orm-macros/src/derives/attributes.rs +++ b/sea-orm-macros/src/derives/attributes.rs @@ -1,102 +1,56 @@ -use darling::FromMeta; -use darling::ast::NestedMeta; -use syn::punctuated::Punctuated; -use syn::{Attribute, Meta, MetaList, MetaNameValue, Token}; - -fn parse_sea_orm_attrs(attrs: &[Attribute]) -> syn::Result> { - let parser = Punctuated::::parse_terminated; - let mut metas = Vec::new(); - for attr in attrs { - if attr.path().is_ident("sea_orm") { - metas.extend(attr.parse_args_with(parser)?); - } + use darling::{FromAttributes, FromMeta, Result}; + use syn::{Attribute, Expr, Ident, Lit}; + +/// An optional `syn::Ident` attribute value. +/// +/// Darling has no `from_word` for `syn::Ident`, so a bare-word usage such as +/// `#[sea_orm(model_ex)]` would be rejected for an `Option` field. Several +/// `sea_orm` attributes are emitted both as bare words (e.g. `#[sea_orm(model_ex)]`) +/// and as name-value paths (e.g. `#[sea_orm(model_ex = ModelEx)]`); this wrapper +/// treats the bare-word form as absent so both usages parse cleanly. +#[derive(Debug, Clone, Default)] +pub struct OptionalIdent(pub Option); + +impl FromMeta for OptionalIdent { + fn from_word() -> Result { + Ok(Self(None)) + } + fn from_value(value: &Lit) -> Result { + Ident::from_value(value).map(|id| Self(Some(id))) + } + fn from_expr(expr: &Expr) -> Result { + Ident::from_expr(expr).map(|id| Self(Some(id))) } - Ok(metas) -} - -fn meta_ident(meta: &NestedMeta) -> Option { - let path = match meta { - NestedMeta::Meta(Meta::Path(path)) - | NestedMeta::Meta(Meta::List(MetaList { path, .. })) - | NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, .. })) => path, - _ => return None, - }; - path.get_ident().map(|ident| ident.to_string()) -} - -// Pre-filter the parsed metas before handing them to darling: -// drop any that aren't a known field, and drop bare words on value fields -// (only flag fields accept a bare word). Otherwise darling rejects the whole list. -fn filter_metas(metas: Vec, known: &[&str], flags: &[&str]) -> Vec { - metas - .into_iter() - .filter(|meta| { - let Some(name) = meta_ident(meta) else { - return false; - }; - if !known.contains(&name.as_str()) { - return false; - } - let is_bare = matches!(meta, NestedMeta::Meta(Meta::Path(_))); - if is_bare && !flags.contains(&name.as_str()) { - return false; - } - true - }) - .collect() -} - -fn darling_err(e: darling::Error) -> syn::Error { - syn::Error::new(e.span(), e) } -fn try_from_attrs( - attrs: &[Attribute], - known: &[&str], - flags: &[&str], -) -> syn::Result> { - let metas = filter_metas(parse_sea_orm_attrs(attrs)?, known, flags); - if metas.is_empty() { - Ok(None) +fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { + if attrs.iter().any(|a| a.path().is_ident("sea_orm")) { + T::from_attributes(attrs) + .map(Some) + .map_err(syn::Error::from) } else { - T::from_list(&metas).map(Some).map_err(darling_err) + Ok(None) } } -fn from_attrs(attrs: &[Attribute], known: &[&str], flags: &[&str]) -> syn::Result { - let metas = filter_metas(parse_sea_orm_attrs(attrs)?, known, flags); - T::from_list(&metas).map_err(darling_err) +fn from_attributes(attrs: &[Attribute]) -> syn::Result { + T::from_attributes(attrs).map_err(syn::Error::from) } pub mod derive_attr { use super::*; use syn::{Ident, LitStr}; - const KNOWN: &[&str] = &[ - "column", - "entity", - "model", - "model_ex", - "active_model", - "active_model_ex", - "primary_key", - "relation", - "schema_name", - "table_name", - "comment", - "rename_all", - "table_iden", - ]; - const FLAGS: &[&str] = &["table_iden"]; - /// Attributes for Models and ActiveModels - #[derive(Default, FromMeta)] + #[derive(Default, FromAttributes)] + #[darling(attributes(sea_orm), allow_unknown_fields)] #[allow(dead_code)] pub struct SeaOrm { pub column: Option, pub entity: Option, pub model: Option, - pub model_ex: Option, + #[darling(default)] + pub model_ex: OptionalIdent, pub active_model: Option, pub active_model_ex: Option, pub primary_key: Option, @@ -110,7 +64,7 @@ pub mod derive_attr { impl SeaOrm { pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { - try_from_attrs::(attrs, KNOWN, FLAGS) + super::try_from_attributes(attrs) } } } @@ -119,24 +73,9 @@ pub mod relation_attr { use super::*; use syn::Lit; - const KNOWN: &[&str] = &[ - "belongs_to", - "has_one", - "has_many", - "via_rel", - "on_update", - "on_delete", - "on_condition", - "from", - "to", - "fk_name", - "skip_fk", - "condition_type", - ]; - const FLAGS: &[&str] = &["skip_fk"]; - /// Attributes for Relation enum - #[derive(Default, FromMeta)] + #[derive(Default, FromAttributes)] + #[darling(attributes(sea_orm), allow_unknown_fields)] pub struct SeaOrm { pub belongs_to: Option, pub has_one: Option, @@ -154,7 +93,7 @@ pub mod relation_attr { impl SeaOrm { pub fn from_attributes(attrs: &[Attribute]) -> syn::Result { - from_attrs::(attrs, KNOWN, FLAGS) + super::from_attributes(attrs) } } } @@ -163,33 +102,9 @@ pub mod compound_attr { use super::*; use syn::LitStr; - const KNOWN: &[&str] = &[ - "has_one", - "has_many", - "belongs_to", - "self_ref", - "skip_fk", - "via", - "via_rel", - "from", - "to", - "relation_enum", - "relation_reverse", - "reverse", - "on_update", - "on_delete", - ]; - const FLAGS: &[&str] = &[ - "has_one", - "has_many", - "belongs_to", - "self_ref", - "skip_fk", - "reverse", - ]; - /// Attributes for compound model fields - #[derive(Default, FromMeta)] + #[derive(Default, FromAttributes)] + #[darling(attributes(sea_orm), allow_unknown_fields)] pub struct SeaOrm { pub has_one: Option<()>, pub has_many: Option<()>, @@ -209,7 +124,7 @@ pub mod compound_attr { impl SeaOrm { pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { - try_from_attrs::(attrs, KNOWN, FLAGS) + super::try_from_attributes(attrs) } } } @@ -218,19 +133,9 @@ pub mod value_type_attr { use super::*; use syn::LitStr; - const KNOWN: &[&str] = &[ - "column_type", - "array_type", - "value_type", - "from_str", - "to_str", - "try_from_u64", - "try_getable_array", - ]; - const FLAGS: &[&str] = &["try_from_u64", "try_getable_array"]; - /// Attributes for compound model fields - #[derive(Default, FromMeta)] + #[derive(Default, FromAttributes)] + #[darling(attributes(sea_orm), allow_unknown_fields)] pub struct SeaOrm { pub column_type: Option, pub array_type: Option, @@ -243,7 +148,7 @@ pub mod value_type_attr { impl SeaOrm { pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { - try_from_attrs::(attrs, KNOWN, FLAGS) + super::try_from_attributes(attrs) } } } @@ -253,11 +158,9 @@ pub mod related_attr { use super::*; use syn::Lit; - const KNOWN: &[&str] = &["entity", "def"]; - const FLAGS: &[&str] = &[]; - /// Attributes for RelatedEntity enum - #[derive(Default, FromMeta)] + #[derive(Default, FromAttributes)] + #[darling(attributes(sea_orm), allow_unknown_fields)] pub struct SeaOrm { /// /// Allows to modify target entity @@ -280,10 +183,10 @@ pub mod related_attr { impl SeaOrm { pub fn try_from_attributes(attrs: &[Attribute]) -> syn::Result> { - try_from_attrs::(attrs, KNOWN, FLAGS) + super::try_from_attributes(attrs) } pub fn from_attributes(attrs: &[Attribute]) -> syn::Result { - from_attrs::(attrs, KNOWN, FLAGS) + super::from_attributes(attrs) } } } diff --git a/sea-orm-macros/src/derives/entity.rs b/sea-orm-macros/src/derives/entity.rs index bf896bf81e..20b1c85afb 100644 --- a/sea-orm-macros/src/derives/entity.rs +++ b/sea-orm-macros/src/derives/entity.rs @@ -25,7 +25,10 @@ impl DeriveEntity { let ident = input.ident; let column_ident = sea_attr.column.unwrap_or_else(|| format_ident!("Column")); let model_ident = sea_attr.model.unwrap_or_else(|| format_ident!("Model")); - let model_ex_ident = sea_attr.model_ex.unwrap_or_else(|| format_ident!("Model")); + let model_ex_ident = sea_attr + .model_ex + .0 + .unwrap_or_else(|| format_ident!("Model")); let active_model_ident = sea_attr .active_model .unwrap_or_else(|| format_ident!("ActiveModel")); diff --git a/sea-orm-macros/src/derives/entity_loader.rs b/sea-orm-macros/src/derives/entity_loader.rs index 926bffc751..00a44224c8 100644 --- a/sea-orm-macros/src/derives/entity_loader.rs +++ b/sea-orm-macros/src/derives/entity_loader.rs @@ -1,5 +1,6 @@ use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use proc_macro2::Span; +use quote::quote; use std::collections::{HashMap, HashSet}; use syn::{Ident, TypePath, Visibility, punctuated::Punctuated, token::Comma}; @@ -22,11 +23,11 @@ pub enum EntityLoaderFieldKind { /// Ident for the `LoadTarget` variant used by a self-referential many-to-many /// junction: `TableRef` for the forward direction, `TableRefRev` for the reverse. -fn table_ref_ident(reverse: bool) -> Ident { +fn table_ref_ident(reverse: bool, span: Span) -> Ident { if reverse { - format_ident!("TableRefRev") + Ident::new("TableRefRev", span) } else { - format_ident!("TableRef") + Ident::new("TableRef", span) } } @@ -125,7 +126,7 @@ impl EntityLoaderField { junction_module: &Ident, reverse: bool, ) { - let target_type = table_ref_ident(reverse); + let target_type = table_ref_ident(reverse, junction_module.span()); let target_entity = if !reverse { quote!(super::#junction_module::Entity) @@ -201,7 +202,7 @@ impl EntityLoaderField { reverse, } = &self.kind { - let target_type = table_ref_ident(*reverse); + let target_type = table_ref_ident(*reverse, junction_module.span()); output.loader_with_set_impl.extend(quote! { if target == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) { @@ -250,7 +251,7 @@ impl EntityLoaderField { junction_module, reverse, } => { - let target_type = table_ref_ident(*reverse); + let target_type = table_ref_ident(*reverse, junction_module.span()); output.loader_with_2_impl.extend(quote! { if left == sea_orm::compound::LoadTarget::#target_type(super::#junction_module::Entity.table_ref()) { @@ -337,7 +338,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = format_ident!("{}", relation_enum.value()); + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -446,7 +447,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = format_ident!("{}", relation_enum.value()); + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -509,7 +510,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = format_ident!("{}", relation_enum.value()); + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { @@ -534,7 +535,7 @@ impl EntityLoaderField { ) { let field = &self.field; let entity = &self.entity; - let relation_enum = format_ident!("{}", relation_enum.value()); + let relation_enum = Ident::new(&relation_enum.value(), relation_enum.span()); let await_ = if cfg!(feature = "async") { quote!(.await) } else { diff --git a/sea-orm-macros/src/derives/entity_model.rs b/sea-orm-macros/src/derives/entity_model.rs index ad042ff7c3..dd7926704b 100644 --- a/sea-orm-macros/src/derives/entity_model.rs +++ b/sea-orm-macros/src/derives/entity_model.rs @@ -190,7 +190,7 @@ pub fn expand_derive_entity_model( if let Some(ident) = &field.ident { let original_field_name = trim_starting_raw_identifier(ident); let mut field_name = - format_ident!("{}", original_field_name.to_upper_camel_case()); + Ident::new(&original_field_name.to_upper_camel_case(), ident.span()); let mut nullable = false; let mut default_value = None; @@ -387,7 +387,7 @@ pub fn expand_derive_entity_model( field_name = enum_name; } - field_name = format_ident!("{}", escape_rust_keyword(field_name)); + field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); let variant_attrs = match &column_name { Some(column_name) => quote! { diff --git a/sea-orm-macros/src/derives/model_ex.rs b/sea-orm-macros/src/derives/model_ex.rs index a69e27292d..0b795844d5 100644 --- a/sea-orm-macros/src/derives/model_ex.rs +++ b/sea-orm-macros/src/derives/model_ex.rs @@ -272,7 +272,7 @@ impl RelationVariants { fn forward_ident(&self, entity: &TypePath) -> Ident { if let Some(name) = self.explicit_name() { - format_ident!("{}", name.value().to_upper_camel_case()) + Ident::new(&name.value().to_upper_camel_case(), name.span()) } else { format_ident!( "{}", @@ -287,9 +287,9 @@ impl RelationVariants { } = self { Some(if let Some(reverse_variant) = reverse_variant { - format_ident!("{}", reverse_variant.value()) + Ident::new(&reverse_variant.value(), reverse_variant.span()) } else { - format_ident!("{}Reverse", forward) + Ident::new(&format!("{forward}Reverse"), forward.span()) }) } else { None @@ -856,11 +856,11 @@ fn expand_belongs_to_into( }; let mut extra: Punctuated<_, Comma> = Punctuated::new(); if let Some(on_update) = &relation.on_update { - let tag = format_ident!("on_update"); + let tag = Ident::new("on_update", on_update.span()); extra.push(quote!(#tag = #on_update)); } if let Some(on_delete) = &relation.on_delete { - let tag = format_ident!("on_delete"); + let tag = Ident::new("on_delete", on_delete.span()); extra.push(quote!(#tag = #on_delete)); } if relation.skip_fk { @@ -997,7 +997,7 @@ fn expand_has_one_into( let has_one = format_ident!("has_one"); let mut extra: Punctuated<_, Comma> = Punctuated::new(); if let Some(via_rel) = &attr.via_rel { - let tag = format_ident!("via_rel"); + let tag = Ident::new("via_rel", via_rel.span()); let via_rel = format!("Relation::{}", via_rel.value()); extra.push(quote!(#tag = #via_rel)); } @@ -1409,8 +1409,8 @@ fn expand_impl_related_many_to_many_self( ) -> TokenStream { match direction { ManyToManySelfDirection::Forward { from, to } => { - let from = format_ident!("{}", from.value()); - let to = format_ident!("{}", to.value()); + let from = Ident::new(&from.value(), from.span()); + let to = Ident::new(&to.value(), to.span()); quote! { #[doc = " Generated by sea-orm-macros"] @@ -1485,7 +1485,7 @@ fn expand_find_by_unique_key(schema: &ModelExSchema<'_>) -> (TokenStream, TokenS .enumerate() .map(|(i, (col, _))| { let i = syn::Index::from(i); - let col = format_ident!("{}", col.to_string().to_upper_camel_case()); + let col = Ident::new(&col.to_string().to_upper_camel_case(), col.span()); quote!(Column::#col.eq(v.#i)) }) .collect::>(); @@ -1512,7 +1512,7 @@ fn expand_find_by_unique_key(schema: &ModelExSchema<'_>) -> (TokenStream, TokenS }); } else { let col = &columns[0].0; - let col = format_ident!("{}", col.to_string().to_upper_camel_case()); + let col = Ident::new(&col.to_string().to_upper_camel_case(), col.span()); let key_type = &columns[0].1; entity_find_by_key.extend(quote! { #[doc = " Generated by sea-orm-macros"] diff --git a/sea-orm-macros/src/derives/typed_column.rs b/sea-orm-macros/src/derives/typed_column.rs index e1bc802e3e..efd69c652d 100644 --- a/sea-orm-macros/src/derives/typed_column.rs +++ b/sea-orm-macros/src/derives/typed_column.rs @@ -3,7 +3,7 @@ use crate::derives::util::consume_meta; use super::util::{CompoundType, escape_rust_keyword, trim_starting_raw_identifier}; use heck::ToUpperCamelCase; use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote}; +use quote::quote; use syn::{Data, Fields, Lit, Visibility}; /// First is `struct TypedColumn`, second is the `const COLUMN` @@ -21,7 +21,7 @@ pub fn expand_typed_column( for field in &fields.named { let Some(ident) = &field.ident else { continue }; let field_name = trim_starting_raw_identifier(ident); - let mut field_name = format_ident!("{}", field_name.to_upper_camel_case()); + let mut field_name = Ident::new(&field_name.to_upper_camel_case(), ident.span()); let field_ty = &field.ty; @@ -68,7 +68,7 @@ pub fn expand_typed_column( continue; } - field_name = format_ident!("{}", escape_rust_keyword(field_name)); + field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); column_fields.push(ident.clone()); let wrapper = super::value_type_match::column_type_wrapper(&column_type, field_ty); diff --git a/sea-orm-macros/src/derives/value_type_match.rs b/sea-orm-macros/src/derives/value_type_match.rs index 394f7947eb..239b701940 100644 --- a/sea-orm-macros/src/derives/value_type_match.rs +++ b/sea-orm-macros/src/derives/value_type_match.rs @@ -1,6 +1,6 @@ -use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote_spanned}; -use syn::{GenericArgument, Ident, LitStr, PathArguments, Type, TypePath}; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::quote_spanned; +use syn::{GenericArgument, LitStr, PathArguments, Type, TypePath}; pub fn column_type_expr( column_type: Option, @@ -62,7 +62,7 @@ pub fn column_type_wrapper(column_type: &Option, field_type: &Type) -> O "Array" => Some("GenericArrayColumn"), _ => None, } - .map(|ty| format_ident!("{}", ty)); + .map(|ty| Ident::new(ty, Span::call_site())); if value_type.is_some() { return value_type; @@ -119,7 +119,7 @@ pub fn column_type_wrapper(column_type: &Option, field_type: &Type) -> O None }; - value_type.map(|ty| format_ident!("{}", ty)) + value_type.map(|ty| Ident::new(ty, Span::call_site())) } fn generic_type_arg<'a>(type_path: &'a TypePath, ident: &str) -> Option<&'a Type> { From 884e55d44ccb1b7776b0c0c0fc3d20c0e9b608f9 Mon Sep 17 00:00:00 2001 From: Shivesh Tiwari Date: Thu, 13 Aug 2026 14:00:44 +0530 Subject: [PATCH 5/6] replace manual attribute parsing layer with darling FromAttributes and restore spans --- sea-orm-macros/src/derives/active_model_ex.rs | 3 ++- sea-orm-macros/src/derives/attributes.rs | 4 ++-- sea-orm-macros/src/derives/entity_loader.rs | 2 +- sea-orm-macros/src/derives/typed_column.rs | 3 ++- sea-orm-macros/src/derives/value_type_match.rs | 10 +++++++--- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/sea-orm-macros/src/derives/active_model_ex.rs b/sea-orm-macros/src/derives/active_model_ex.rs index a0b7a26d4f..94c11f60b4 100644 --- a/sea-orm-macros/src/derives/active_model_ex.rs +++ b/sea-orm-macros/src/derives/active_model_ex.rs @@ -1216,7 +1216,8 @@ impl HasManySelfField<'_> { quote!() }; let ident = self.ident; - let relation_variant = Ident::new(&self.relation_variant.value(), self.relation_variant.span()); + 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! { diff --git a/sea-orm-macros/src/derives/attributes.rs b/sea-orm-macros/src/derives/attributes.rs index 26ac19acc7..bb07473d75 100644 --- a/sea-orm-macros/src/derives/attributes.rs +++ b/sea-orm-macros/src/derives/attributes.rs @@ -1,5 +1,5 @@ - use darling::{FromAttributes, FromMeta, Result}; - use syn::{Attribute, Expr, Ident, Lit}; +use darling::{FromAttributes, FromMeta, Result}; +use syn::{Attribute, Expr, Ident, Lit}; /// An optional `syn::Ident` attribute value. /// diff --git a/sea-orm-macros/src/derives/entity_loader.rs b/sea-orm-macros/src/derives/entity_loader.rs index 00a44224c8..9080cf591a 100644 --- a/sea-orm-macros/src/derives/entity_loader.rs +++ b/sea-orm-macros/src/derives/entity_loader.rs @@ -1,5 +1,5 @@ -use proc_macro2::TokenStream; use proc_macro2::Span; +use proc_macro2::TokenStream; use quote::quote; use std::collections::{HashMap, HashSet}; use syn::{Ident, TypePath, Visibility, punctuated::Punctuated, token::Comma}; diff --git a/sea-orm-macros/src/derives/typed_column.rs b/sea-orm-macros/src/derives/typed_column.rs index efd69c652d..a9a44ab11d 100644 --- a/sea-orm-macros/src/derives/typed_column.rs +++ b/sea-orm-macros/src/derives/typed_column.rs @@ -71,7 +71,8 @@ pub fn expand_typed_column( field_name = Ident::new(&escape_rust_keyword(field_name), ident.span()); column_fields.push(ident.clone()); - let wrapper = super::value_type_match::column_type_wrapper(&column_type, field_ty); + let wrapper = + super::value_type_match::column_type_wrapper(&column_type, field_ty, ident.span()); column_types.push(if let Some(wrapper) = &wrapper { quote!(sea_orm::#wrapper) } else { diff --git a/sea-orm-macros/src/derives/value_type_match.rs b/sea-orm-macros/src/derives/value_type_match.rs index 239b701940..c1843faf22 100644 --- a/sea-orm-macros/src/derives/value_type_match.rs +++ b/sea-orm-macros/src/derives/value_type_match.rs @@ -20,7 +20,11 @@ pub fn column_type_expr( } } -pub fn column_type_wrapper(column_type: &Option, field_type: &Type) -> Option { +pub fn column_type_wrapper( + column_type: &Option, + field_type: &Type, + field_span: Span, +) -> Option { let (nullable, field_type) = if let Type::Path(type_path) = field_type && let Some(inner) = generic_type_arg(type_path, "Option") { @@ -62,7 +66,7 @@ pub fn column_type_wrapper(column_type: &Option, field_type: &Type) -> O "Array" => Some("GenericArrayColumn"), _ => None, } - .map(|ty| Ident::new(ty, Span::call_site())); + .map(|ty| Ident::new(ty, field_span)); if value_type.is_some() { return value_type; @@ -119,7 +123,7 @@ pub fn column_type_wrapper(column_type: &Option, field_type: &Type) -> O None }; - value_type.map(|ty| Ident::new(ty, Span::call_site())) + value_type.map(|ty| Ident::new(ty, field_span)) } fn generic_type_arg<'a>(type_path: &'a TypePath, ident: &str) -> Option<&'a Type> { From 5ce4172a824fce480ebae28d0209423768b7ea50 Mon Sep 17 00:00:00 2001 From: Shivesh Tiwari Date: Thu, 13 Aug 2026 15:28:08 +0530 Subject: [PATCH 6/6] enforce entity requirement for into_active_model in derive partial model --- sea-orm-macros/src/derives/partial_model.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/sea-orm-macros/src/derives/partial_model.rs b/sea-orm-macros/src/derives/partial_model.rs index bf6b6ca07f..dcd109b4b6 100644 --- a/sea-orm-macros/src/derives/partial_model.rs +++ b/sea-orm-macros/src/derives/partial_model.rs @@ -97,9 +97,8 @@ impl DerivePartialModel { } if into_active_model { - if let Some(entity) = &entity { - active_model = Some(syn::parse_quote!(<#entity as EntityTrait>::ActiveModel)); - } + let entity = entity.clone().ok_or(Error::EntityNotSpecified)?; + active_model = Some(syn::parse_quote!(<#entity as EntityTrait>::ActiveModel)); } let mut column_as_list = Vec::with_capacity(fields.len()); @@ -560,6 +559,21 @@ mod test { assert!(DerivePartialModel::new(input).is_err()); } + #[test] + fn test_into_active_model_requires_entity() { + let input: DeriveInput = parse_str( + r#" + #[sea_orm(into_active_model)] + struct PartialModel { + #[sea_orm(from_expr = "Expr::val(1).add(1)")] + total: i32, + } + "#, + ) + .unwrap(); + assert!(DerivePartialModel::new(input).is_err()); + } + const CODE_SNIPPET_5: &str = r#" struct PartialModel { #[sea_orm(nested)]