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}