sqlorm_macros/
lib.rs

1#![cfg(any(feature = "postgres", feature = "sqlite"))]
2use proc_macro::TokenStream;
3use quote::format_ident;
4use syn::parse_macro_input;
5use syn::{Field, Fields, ItemStruct};
6
7mod naming;
8mod traits;
9
10use crate::entity::EntityStruct;
11mod entity;
12mod qb;
13mod sql;
14
15mod attrs;
16mod gen_columns;
17mod relations;
18
19#[proc_macro_derive(Entity, attributes(sql))]
20pub fn entity(input: TokenStream) -> TokenStream {
21    let es = parse_macro_input!(input as EntityStruct);
22    entity::handle(es).into()
23}
24
25/// Transforms a struct into a database entity with ORM capabilities.
26///
27/// This is the primary way to define database entities in SQLOrm. The macro automatically
28/// generates all necessary database operations like `save()`, `find_by_id()`, and query methods.
29///
30/// # Basic Usage
31///
32/// ```rust,ignore
33/// use sqlorm::table;
34///
35/// #[table]
36/// struct User {
37///     #[sql(pk)]
38///     id: i64,
39///     email: String,
40///     name: String,
41/// }
42/// ```
43///
44/// # Custom Table Name
45///
46/// ```rust,ignore
47/// #[table(name = "app_users")]
48/// struct User {
49///     #[sql(pk)]
50///     id: i64,
51///     email: String,
52/// }
53/// ```
54///
55/// # **⚠️ Important:**
56/// [`sqlorm::table`] attribute must go before any other attributes, otherwise code won't compile.
57/// Incorrect usage:
58/// ```rust,ignore
59/// #[derive(Debug)]
60/// #[table(name = "app_users")] // incorrect: placed after derive attr
61/// struct User {
62///     #[sql(pk)]
63///     id: i64,
64///     email: String,
65/// }
66/// ```
67/// Correct usage:
68/// ```rust,ignore
69/// #[table(name = "app_users")] // correct: placed before derive attr
70/// #[derive(Debug)]
71/// struct User {
72///     #[sql(pk)]
73///     id: i64,
74///     email: String,
75/// }
76/// ```
77/// After applying this macro, you can use standard ORM operations:
78/// - `user.save(&pool).await`
79/// - `User::query().filter(...).fetch_all(&pool).await`
80///
81/// With feature `extra-traits` enable
82/// - `User::find_by_id(&pool, 1).await`
83/// - `User::find_by_email(&pool, "user@example.com".to_string()).await`
84///
85/// # Supported Field Attributes
86///
87/// ## `#[sql(...)]` Attributes
88///
89/// - **`pk`** - Mark field as primary key (required, exactly one per struct)
90/// - **`unique`** - Mark field as unique (generates `find_by_*` methods)
91/// - **`skip`** - Exclude field from SQL operations
92/// - **`timestamp(field_name, factory)`** - Automatic timestamp management:
93///   - `created_at` - Set on insert
94///   - `updated_at` - Set on insert and update  
95///   - `deleted_at` - For soft deletes
96/// - **`relation(...)`** - Define relationships:
97///   - `belongs_to -> SomeOtherStruct, relation = "some_other_struct", on = field`
98///   - `has_many -> SomeOtherStruct, relation = "some_other_structs", on = field`
99///   - `has_one -> SomeOtherStruct, relation = "some_other_struct", on = field`
100///
101///
102/// # Complete Example
103///
104/// ```rust,ignore
105/// use chrono::{DateTime, Utc};
106/// use sqlorm::table;
107///
108/// #[derive(Debug, Clone, Default)]
109/// #[table(name = "users")]
110/// pub struct User {
111///     #[sql(pk)]
112///     #[sql(relation(has_many -> Post, relation = "posts", on = user_id))]
113///     pub id: i64,
114///     
115///     #[sql(unique)]
116///     pub email: String,
117///     
118///     #[sql(unique)]
119///     pub username: String,
120///     
121///     pub first_name: String,
122///     pub last_name: String,
123///     
124///     #[sql(skip)]
125///     pub posts: Option<Vec<Post>>,
126///     
127///     #[sql(timestamp(created_at, chrono::Utc::now()))]
128///     pub created_at: DateTime<Utc>,
129///     
130///     #[sql(timestamp(updated_at, chrono::Utc::now()))]
131///     pub updated_at: DateTime<Utc>,
132/// }
133///
134/// #[derive(Debug, Clone, Default)]
135/// #[table(name = "posts")]
136/// pub struct Post {
137///     #[sql(pk)]
138///     pub id: i64,
139///     
140///     pub title: String,
141///     pub content: String,
142///     
143///     #[sql(relation(belongs_to -> User, relation = "author", on = id))]
144///     pub user_id: i64,
145///     
146///     #[sql(skip)]
147///     pub author: Option<User>,
148/// }
149/// ```
150///
151#[proc_macro_attribute]
152pub fn table(args: TokenStream, input: TokenStream) -> TokenStream {
153    let mut model = parse_macro_input!(input as ItemStruct);
154
155    let mut existing_derives = Vec::new();
156    model.attrs.retain(|attr| {
157        if attr.path().is_ident("derive") {
158            existing_derives.push(attr.clone());
159            false
160        } else {
161            true
162        }
163    });
164
165    let table_name = if args.is_empty() {
166        model.ident.to_string().to_lowercase()
167    } else {
168        let meta_list: syn::punctuated::Punctuated<syn::MetaNameValue, syn::Token![,]> =
169            syn::parse_macro_input!(args with syn::punctuated::Punctuated::parse_terminated);
170
171        let mut table_name = model.ident.to_string().to_lowercase();
172        for meta in meta_list {
173            if meta.path.is_ident("name") {
174                if let syn::Expr::Lit(syn::ExprLit {
175                    lit: syn::Lit::Str(lit_str),
176                    ..
177                }) = meta.value
178                {
179                    table_name = lit_str.value();
180                    break;
181                }
182            }
183        }
184        table_name
185    };
186
187    inject_relation_fields(&mut model).expect("Failed to inject relation fields");
188
189    // reapply the derive attributes after field injection
190    quote::quote! {
191        #(#existing_derives)*
192        #[derive(::sqlorm::sqlx::FromRow,::sqlorm::Entity)]
193        #[sql(name = #table_name)]
194        #model
195    }
196    .into()
197}
198
199/// Scans struct fields for relation attributes and automatically injects
200/// corresponding relation fields (e.g., posts: Option<Vec<Post>>) with proper attributes.
201/// Throws compile errors if the relation field names are already used.
202/// Uses existing attribute parsing logic to extract relation information.
203fn inject_relation_fields(model: &mut ItemStruct) -> syn::Result<()> {
204    use crate::attrs::parse_entity_field;
205    use crate::relations::RelationType;
206
207    let mut relations_to_inject = Vec::new();
208
209    if let Fields::Named(ref fields) = model.fields {
210        for field in fields.named.iter() {
211            let entity_field = parse_entity_field(field)?;
212            if let Some(field_relations) = entity_field.relations {
213                relations_to_inject.extend(field_relations);
214            }
215        }
216    }
217
218    if let Fields::Named(ref mut fields) = model.fields {
219        for relation in &relations_to_inject {
220            for existing_field in fields.named.iter() {
221                if let Some(field_name) = &existing_field.ident {
222                    if field_name.to_string() == relation.relation_name {
223                        return Err(syn::Error::new_spanned(
224                            field_name,
225                            format!(
226                                "Field '{}' is reserved for auto-generated relation field. Remove this field as it will be injected automatically based on relation attributes.",
227                                relation.relation_name
228                            ),
229                        ));
230                    }
231                }
232            }
233        }
234
235        for relation in relations_to_inject {
236            let field_ident = format_ident!("{}", relation.relation_name);
237            let field_type: syn::Type = match relation.kind {
238                RelationType::HasMany => {
239                    let other_type = &relation.other;
240                    syn::parse_quote! { Option<Vec<#other_type>> }
241                }
242                RelationType::BelongsTo | RelationType::HasOne => {
243                    let other_type = &relation.other;
244                    syn::parse_quote! { Option<#other_type> }
245                }
246            };
247
248            let new_field: Field = syn::parse_quote! {
249                #[sql(skip)]
250                #[sqlx(skip)]
251                pub #field_ident: #field_type
252            };
253            fields.named.push(new_field);
254        }
255    }
256    Ok(())
257}