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}