premix_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{Data, DeriveInput, Field, Fields, parse_macro_input};
4
5mod relations;
6
7#[proc_macro_derive(Model, attributes(has_many, belongs_to, premix))]
8pub fn derive_model(input: TokenStream) -> TokenStream {
9    let input = parse_macro_input!(input as DeriveInput);
10
11    let impl_block = match generate_generic_impl(&input) {
12        Ok(tokens) => tokens,
13        Err(err) => return TokenStream::from(err.to_compile_error()),
14    };
15
16    let rel_block = match relations::impl_relations(&input) {
17        Ok(tokens) => tokens,
18        Err(err) => return TokenStream::from(err.to_compile_error()),
19    };
20
21    TokenStream::from(quote! {
22        #impl_block
23        #rel_block
24    })
25}
26
27fn generate_generic_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
28    let struct_name = &input.ident;
29    let table_name = struct_name.to_string().to_lowercase() + "s";
30
31    let all_fields = if let Data::Struct(data) = &input.data {
32        if let Fields::Named(fields) = &data.fields {
33            &fields.named
34        } else {
35            return Err(syn::Error::new_spanned(
36                &data.fields,
37                "Premix Model only supports structs with named fields",
38            ));
39        }
40    } else {
41        return Err(syn::Error::new_spanned(
42            input,
43            "Premix Model only supports structs",
44        ));
45    };
46
47    let mut db_fields = Vec::new();
48    let mut ignored_field_idents = Vec::new();
49
50    for field in all_fields {
51        if is_ignored(field) {
52            ignored_field_idents.push(field.ident.as_ref().unwrap());
53        } else {
54            db_fields.push(field);
55        }
56    }
57
58    let field_idents: Vec<_> = db_fields
59        .iter()
60        .map(|f| f.ident.as_ref().unwrap())
61        .collect();
62    let field_types: Vec<_> = db_fields.iter().map(|f| &f.ty).collect();
63    let field_indices: Vec<_> = (0..db_fields.len()).collect();
64    let field_names: Vec<_> = field_idents.iter().map(|id| id.to_string()).collect();
65    let field_idents_len = field_idents.len();
66
67    let eager_load_body = relations::generate_eager_load_body(input)?;
68    let has_version = field_names.contains(&"version".to_string());
69    let has_soft_delete = field_names.contains(&"deleted_at".to_string());
70
71    let update_impl = if has_version {
72        quote! {
73            async fn update<'a, E>(&mut self, executor: E) -> Result<premix_core::UpdateResult, premix_core::sqlx::Error>
74            where
75                E: premix_core::IntoExecutor<'a, DB = DB>
76            {
77                let mut executor = executor.into_executor();
78                let table_name = Self::table_name();
79                let set_clause = vec![ #( format!("{} = {}", #field_names, <DB as premix_core::SqlDialect>::placeholder(1 + #field_indices)) ),* ].join(", ");
80                let id_p = <DB as premix_core::SqlDialect>::placeholder(1 + #field_idents_len);
81                let ver_p = <DB as premix_core::SqlDialect>::placeholder(2 + #field_idents_len);
82                let sql = format!(
83                    "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
84                    table_name, set_clause, id_p, ver_p
85                );
86
87                let mut query = premix_core::sqlx::query::<DB>(&sql)
88                    #( .bind(&self.#field_idents) )*
89                    .bind(&self.id)
90                    .bind(&self.version);
91
92                let result = executor.execute(query).await?;
93
94                if <DB as premix_core::SqlDialect>::rows_affected(&result) == 0 {
95                    let exists_p = <DB as premix_core::SqlDialect>::placeholder(1);
96                    let exists_sql = format!("SELECT id FROM {} WHERE id = {}", table_name, exists_p);
97                    let exists_query = premix_core::sqlx::query_as::<DB, Self>(&exists_sql).bind(&self.id);
98                    let exists = executor.fetch_optional(exists_query).await?;
99
100                    if exists.is_none() {
101                        Ok(premix_core::UpdateResult::NotFound)
102                    } else {
103                        Ok(premix_core::UpdateResult::VersionConflict)
104                    }
105                } else {
106                    self.version += 1;
107                    Ok(premix_core::UpdateResult::Success)
108                }
109            }
110        }
111    } else {
112        quote! {
113            async fn update<'a, E>(&mut self, executor: E) -> Result<premix_core::UpdateResult, premix_core::sqlx::Error>
114            where
115                E: premix_core::IntoExecutor<'a, DB = DB>
116            {
117                let mut executor = executor.into_executor();
118                let table_name = Self::table_name();
119                let set_clause = vec![ #( format!("{} = {}", #field_names, <DB as premix_core::SqlDialect>::placeholder(1 + #field_indices)) ),* ].join(", ");
120                let id_p = <DB as premix_core::SqlDialect>::placeholder(1 + #field_idents_len);
121                let sql = format!("UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
122
123                let mut query = premix_core::sqlx::query::<DB>(&sql)
124                    #( .bind(&self.#field_idents) )*
125                    .bind(&self.id);
126
127                let result = executor.execute(query).await?;
128
129                if <DB as premix_core::SqlDialect>::rows_affected(&result) == 0 {
130                    Ok(premix_core::UpdateResult::NotFound)
131                } else {
132                    Ok(premix_core::UpdateResult::Success)
133                }
134            }
135        }
136    };
137
138    let delete_impl = if has_soft_delete {
139        quote! {
140            async fn delete<'a, E>(&mut self, executor: E) -> Result<(), premix_core::sqlx::Error>
141            where
142                E: premix_core::IntoExecutor<'a, DB = DB>
143            {
144                let mut executor = executor.into_executor();
145                let table_name = Self::table_name();
146                let id_p = <DB as premix_core::SqlDialect>::placeholder(1);
147                let sql = format!("UPDATE {} SET deleted_at = {} WHERE id = {}", table_name, <DB as premix_core::SqlDialect>::current_timestamp_fn(), id_p);
148
149                let query = premix_core::sqlx::query::<DB>(&sql).bind(&self.id);
150                executor.execute(query).await?;
151
152                self.deleted_at = Some("DELETED".to_string());
153                Ok(())
154            }
155            fn has_soft_delete() -> bool { true }
156        }
157    } else {
158        quote! {
159            async fn delete<'a, E>(&mut self, executor: E) -> Result<(), premix_core::sqlx::Error>
160            where
161                E: premix_core::IntoExecutor<'a, DB = DB>
162            {
163                let mut executor = executor.into_executor();
164                let table_name = Self::table_name();
165                let id_p = <DB as premix_core::SqlDialect>::placeholder(1);
166                let sql = format!("DELETE FROM {} WHERE id = {}", table_name, id_p);
167
168                let query = premix_core::sqlx::query::<DB>(&sql).bind(&self.id);
169                executor.execute(query).await?;
170
171                Ok(())
172            }
173            fn has_soft_delete() -> bool { false }
174        }
175    };
176
177    let mut related_model_bounds = Vec::new();
178    for field in all_fields {
179        for attr in &field.attrs {
180            if (attr.path().is_ident("has_many") || attr.path().is_ident("belongs_to"))
181                && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
182            {
183                related_model_bounds.push(quote! { #related_ident: premix_core::Model<DB> });
184            }
185        }
186    }
187
188    // Generic Implementation
189    Ok(quote! {
190        impl<'r, R> premix_core::sqlx::FromRow<'r, R> for #struct_name
191        where
192            R: premix_core::sqlx::Row,
193            R::Database: premix_core::sqlx::Database,
194            #(
195                #field_types: premix_core::sqlx::Type<R::Database> + premix_core::sqlx::Decode<'r, R::Database>,
196            )*
197            for<'c> &'c str: premix_core::sqlx::ColumnIndex<R>,
198        {
199            fn from_row(row: &'r R) -> Result<Self, premix_core::sqlx::Error> {
200                use premix_core::sqlx::Row;
201                Ok(Self {
202                    #(
203                        #field_idents: row.try_get(#field_names)?,
204                    )*
205                    #(
206                        #ignored_field_idents: None,
207                    )*
208                })
209            }
210        }
211
212        #[premix_core::async_trait::async_trait]
213        impl<DB> premix_core::Model<DB> for #struct_name
214        where
215            DB: premix_core::SqlDialect,
216            for<'c> &'c str: premix_core::sqlx::ColumnIndex<DB::Row>,
217            for<'q> <DB as premix_core::sqlx::Database>::Arguments<'q>: premix_core::sqlx::IntoArguments<'q, DB>,
218            for<'c> &'c mut <DB as premix_core::sqlx::Database>::Connection: premix_core::sqlx::Executor<'c, Database = DB>,
219            i32: premix_core::sqlx::Type<DB> + for<'q> premix_core::sqlx::Encode<'q, DB> + for<'r> premix_core::sqlx::Decode<'r, DB>,
220            i64: premix_core::sqlx::Type<DB> + for<'q> premix_core::sqlx::Encode<'q, DB> + for<'r> premix_core::sqlx::Decode<'r, DB>,
221            String: premix_core::sqlx::Type<DB> + for<'q> premix_core::sqlx::Encode<'q, DB> + for<'r> premix_core::sqlx::Decode<'r, DB>,
222            bool: premix_core::sqlx::Type<DB> + for<'q> premix_core::sqlx::Encode<'q, DB> + for<'r> premix_core::sqlx::Decode<'r, DB>,
223            Option<String>: premix_core::sqlx::Type<DB> + for<'q> premix_core::sqlx::Encode<'q, DB> + for<'r> premix_core::sqlx::Decode<'r, DB>,
224            #( #related_model_bounds, )*
225        {
226            fn table_name() -> &'static str {
227                #table_name
228            }
229
230            fn create_table_sql() -> String {
231                let mut cols = vec!["id ".to_string() + <DB as premix_core::SqlDialect>::auto_increment_pk()];
232                #(
233                    if #field_names != "id" {
234                        let field_name: &str = #field_names;
235                        let sql_type = if field_name.ends_with("_id") {
236                            <DB as premix_core::SqlDialect>::int_type()
237                        } else {
238                            match field_name {
239                                "name" | "title" | "status" | "email" | "role" => <DB as premix_core::SqlDialect>::text_type(),
240                                "age" | "version" | "price" | "balance" => <DB as premix_core::SqlDialect>::int_type(),
241                                "is_active" => <DB as premix_core::SqlDialect>::bool_type(),
242                                "deleted_at" => <DB as premix_core::SqlDialect>::text_type(),
243                                _ => <DB as premix_core::SqlDialect>::text_type(),
244                            }
245                        };
246                        cols.push(format!("{} {}", #field_names, sql_type));
247                    }
248                )*
249                format!("CREATE TABLE IF NOT EXISTS {} ({})", #table_name, cols.join(", "))
250            }
251
252            fn list_columns() -> Vec<String> {
253                vec![ #( #field_names.to_string() ),* ]
254            }
255
256            async fn save<'a, E>(&mut self, executor: E) -> Result<(), premix_core::sqlx::Error>
257            where
258                E: premix_core::IntoExecutor<'a, DB = DB>
259            {
260                let mut executor = executor.into_executor();
261                use premix_core::ModelHooks;
262                self.before_save().await?;
263
264                // Filter out 'id' and 'version' for INSERT
265                let columns: Vec<&str> = vec![ #( #field_names ),* ]
266                    .into_iter()
267                    .filter(|&c| c != "id" && c != "version" && c != "deleted_at")
268                    .collect();
269
270                let placeholders = (1..=columns.len())
271                    .map(|i| <DB as premix_core::SqlDialect>::placeholder(i))
272                    .collect::<Vec<_>>()
273                    .join(", ");
274
275                let sql = format!("INSERT INTO {} ({}) VALUES ({})", #table_name, columns.join(", "), placeholders);
276
277                let mut query = premix_core::sqlx::query::<DB>(&sql);
278
279                // Bind only non-id/version fields
280                #(
281                    if #field_names != "id" && #field_names != "version" && #field_names != "deleted_at" {
282                        query = query.bind(&self.#field_idents);
283                    }
284                )*
285
286                let result = executor.execute(query).await?;
287
288                // Set the ID if it's 0 (new record)
289                if self.id == 0 {
290                    let last_id = <DB as premix_core::SqlDialect>::last_insert_id(&result);
291                    if last_id > 0 {
292                         self.id = last_id as i32;
293                    }
294                }
295
296                self.after_save().await?;
297                Ok(())
298            }
299
300            #update_impl
301            #delete_impl
302
303            async fn find_by_id<'a, E>(executor: E, id: i32) -> Result<Option<Self>, premix_core::sqlx::Error>
304            where
305                E: premix_core::IntoExecutor<'a, DB = DB>
306            {
307                let mut executor = executor.into_executor();
308                let p = <DB as premix_core::SqlDialect>::placeholder(1);
309                let mut where_clause = format!("WHERE id = {}", p);
310                if Self::has_soft_delete() {
311                    where_clause.push_str(" AND deleted_at IS NULL");
312                }
313                let sql = format!("SELECT * FROM {} {} LIMIT 1", #table_name, where_clause);
314                let query = premix_core::sqlx::query_as::<DB, Self>(&sql).bind(id);
315
316                executor.fetch_optional(query).await
317            }
318
319            async fn eager_load<'a, E>(models: &mut [Self], relation: &str, executor: E) -> Result<(), premix_core::sqlx::Error>
320            where
321                E: premix_core::IntoExecutor<'a, DB = DB>
322            {
323                let mut executor = executor.into_executor();
324                #eager_load_body
325            }
326        }
327    })
328}
329
330fn is_ignored(field: &Field) -> bool {
331    for attr in &field.attrs {
332        if attr.path().is_ident("premix")
333            && let Ok(meta) = attr.parse_args::<syn::Ident>()
334            && meta == "ignore"
335        {
336            return true;
337        }
338    }
339    false
340}