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