premix_macros/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Attribute, Data, DeriveInput, Field, Fields, Ident, LitStr, Token, parse_macro_input,
5    punctuated::Punctuated,
6};
7
8mod relations;
9
10#[proc_macro_derive(Model, attributes(has_many, belongs_to, premix))]
11pub fn derive_model(input: TokenStream) -> TokenStream {
12    let input = parse_macro_input!(input as DeriveInput);
13    match derive_model_impl(&input) {
14        Ok(tokens) => TokenStream::from(tokens),
15        Err(err) => TokenStream::from(err.to_compile_error()),
16    }
17}
18
19fn derive_model_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
20    let impl_block = generate_generic_impl(input)?;
21    let rel_block = relations::impl_relations(input)?;
22    Ok(quote! {
23        #impl_block
24        #rel_block
25    })
26}
27
28#[cfg(test)]
29mod tests {
30    use syn::parse_quote;
31
32    use super::*;
33
34    #[test]
35    fn generate_generic_impl_includes_table_and_columns() {
36        let input: DeriveInput = parse_quote! {
37            struct User {
38                id: i32,
39                name: String,
40                version: i32,
41                deleted_at: Option<String>,
42            }
43        };
44        let tokens = generate_generic_impl(&input).unwrap().to_string();
45        assert!(tokens.contains("CREATE TABLE IF NOT EXISTS"));
46        assert!(tokens.contains("users"));
47        assert!(tokens.contains("deleted_at"));
48        assert!(tokens.contains("version"));
49    }
50
51    #[test]
52    fn generate_generic_impl_rejects_tuple_struct() {
53        let input: DeriveInput = parse_quote! {
54            struct User(i32, String);
55        };
56        let err = generate_generic_impl(&input).unwrap_err();
57        assert!(err.to_string().contains("named fields"));
58    }
59
60    #[test]
61    fn generate_generic_impl_rejects_non_struct() {
62        let input: DeriveInput = parse_quote! {
63            enum User {
64                A,
65                B,
66            }
67        };
68        let err = generate_generic_impl(&input).unwrap_err();
69        assert!(err.to_string().contains("only supports structs"));
70    }
71
72    #[test]
73    fn generate_generic_impl_version_update_branch() {
74        let input: DeriveInput = parse_quote! {
75            struct User {
76                id: i32,
77                version: i32,
78                name: String,
79            }
80        };
81        let tokens = generate_generic_impl(&input).unwrap().to_string();
82        assert!(tokens.contains("version = version + 1"));
83    }
84
85    #[test]
86    fn generate_generic_impl_no_version_branch() {
87        let input: DeriveInput = parse_quote! {
88            struct User {
89                id: i32,
90                name: String,
91            }
92        };
93        let tokens = generate_generic_impl(&input).unwrap().to_string();
94        assert!(!tokens.contains("version = version + 1"));
95    }
96
97    #[test]
98    fn generate_generic_impl_includes_default_hooks_and_validation() {
99        let input: DeriveInput = parse_quote! {
100            struct User {
101                id: i32,
102                name: String,
103            }
104        };
105        let tokens = generate_generic_impl(&input).unwrap().to_string();
106        assert!(tokens.contains("ModelHooks"));
107        assert!(tokens.contains("ModelValidation"));
108    }
109
110    #[test]
111    fn generate_generic_impl_includes_schema_impl() {
112        let input: DeriveInput = parse_quote! {
113            struct User {
114                id: i32,
115                name: String,
116            }
117        };
118        let tokens = generate_generic_impl(&input).unwrap().to_string();
119        assert!(tokens.contains("ModelSchema"));
120        assert!(tokens.contains("SchemaColumn"));
121    }
122
123    #[test]
124    fn generate_generic_impl_includes_index_and_foreign_key_metadata() {
125        let input: DeriveInput = parse_quote! {
126            struct User {
127                id: i32,
128                #[premix(index)]
129                name: String,
130                #[premix(unique(name = "users_email_uidx"))]
131                email: String,
132                #[premix(foreign_key(table = "accounts", column = "id"))]
133                account_id: i32,
134            }
135        };
136        let tokens = generate_generic_impl(&input).unwrap().to_string();
137        assert!(tokens.contains("SchemaIndex"));
138        assert!(tokens.contains("idx_users_name"));
139        assert!(tokens.contains("users_email_uidx"));
140        assert!(tokens.contains("SchemaForeignKey"));
141        assert!(tokens.contains("accounts"));
142        assert!(tokens.contains("account_id"));
143    }
144
145    #[test]
146    fn generate_generic_impl_includes_sensitive_fields() {
147        let input: DeriveInput = parse_quote! {
148            struct User {
149                id: i32,
150                #[premix(sensitive)]
151                email: String,
152            }
153        };
154        let tokens = generate_generic_impl(&input).unwrap().to_string();
155        assert!(tokens.contains("sensitive_fields"));
156        assert!(tokens.contains("\"email\""));
157    }
158
159    #[test]
160    fn generate_generic_impl_skips_custom_hooks_and_validation() {
161        let input: DeriveInput = parse_quote! {
162            #[premix(custom_hooks, custom_validation)]
163            struct User {
164                id: i32,
165                name: String,
166            }
167        };
168        let tokens = generate_generic_impl(&input).unwrap().to_string();
169        assert!(!tokens.contains("impl premix_orm :: ModelHooks"));
170        assert!(!tokens.contains("impl premix_orm :: ModelValidation"));
171    }
172
173    #[test]
174    fn is_ignored_detects_attribute() {
175        let field: Field = parse_quote! {
176            #[premix(ignore)]
177            ignored: Option<String>
178        };
179        assert!(is_ignored(&field));
180    }
181
182    #[test]
183    fn is_ignored_false_for_other_attrs() {
184        let field: Field = parse_quote! {
185            #[serde(skip)]
186            name: String
187        };
188        assert!(!is_ignored(&field));
189    }
190
191    #[test]
192    fn is_ignored_false_for_premix_other_arg() {
193        let field: Field = parse_quote! {
194            #[premix(skip)]
195            name: String
196        };
197        assert!(!is_ignored(&field));
198    }
199
200    #[test]
201    fn is_sensitive_detects_attribute() {
202        let field: Field = parse_quote! {
203            #[premix(sensitive)]
204            secret: String
205        };
206        assert!(is_sensitive(&field));
207    }
208
209    #[test]
210    fn is_sensitive_false_for_other_attrs() {
211        let field: Field = parse_quote! {
212            #[serde(skip)]
213            secret: String
214        };
215        assert!(!is_sensitive(&field));
216    }
217
218    #[test]
219    fn is_ignored_false_when_premix_has_no_args() {
220        let field: Field = parse_quote! {
221            #[premix]
222            name: String
223        };
224        assert!(!is_ignored(&field));
225    }
226
227    #[test]
228    fn derive_model_impl_emits_tokens() {
229        let input: DeriveInput = parse_quote! {
230            struct User {
231                id: i32,
232                name: String,
233            }
234        };
235        let tokens = derive_model_impl(&input).unwrap().to_string();
236        assert!(tokens.contains("impl"));
237    }
238
239    #[test]
240    fn derive_model_impl_propagates_error() {
241        let input: DeriveInput = parse_quote! {
242            enum User {
243                A,
244            }
245        };
246        let err = derive_model_impl(&input).unwrap_err();
247        assert!(err.to_string().contains("only supports structs"));
248    }
249
250    #[test]
251    fn generate_generic_impl_includes_soft_delete_delete_impl() {
252        let input: DeriveInput = parse_quote! {
253            struct AuditLog {
254                id: i32,
255                deleted_at: Option<String>,
256            }
257        };
258        let tokens = generate_generic_impl(&input).unwrap().to_string();
259        assert!(tokens.contains("deleted_at ="));
260        assert!(tokens.contains("has_soft_delete"));
261    }
262
263    #[test]
264    fn generate_generic_impl_ignores_marked_fields() {
265        let input: DeriveInput = parse_quote! {
266            struct User {
267                id: i32,
268                name: String,
269                #[premix(ignore)]
270                temp: Option<String>,
271            }
272        };
273        let tokens = generate_generic_impl(&input).unwrap().to_string();
274        assert!(tokens.contains("temp : None"));
275        assert!(!tokens.contains("\"temp\""));
276    }
277
278    #[test]
279    fn generate_generic_impl_adds_relation_bounds() {
280        let input: DeriveInput = parse_quote! {
281            struct User {
282                id: i32,
283                #[has_many(Post)]
284                posts: Vec<Post>,
285            }
286        };
287        let tokens = generate_generic_impl(&input).unwrap().to_string();
288        assert!(tokens.contains("Post : premix_orm :: Model < DB >"));
289    }
290
291    #[test]
292    fn generate_generic_impl_records_field_names() {
293        let input: DeriveInput = parse_quote! {
294            struct Account {
295                id: i32,
296                user_id: i32,
297                is_active: bool,
298            }
299        };
300        let tokens = generate_generic_impl(&input).unwrap().to_string();
301        assert!(tokens.contains("\"user_id\""));
302        assert!(tokens.contains("\"is_active\""));
303    }
304}
305
306fn generate_generic_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
307    let struct_name = &input.ident;
308    let table_name = struct_name.to_string().to_lowercase() + "s";
309    let custom_hooks = has_premix_flag(&input.attrs, "custom_hooks");
310    let custom_validation = has_premix_flag(&input.attrs, "custom_validation");
311
312    let all_fields = if let Data::Struct(data) = &input.data {
313        if let Fields::Named(fields) = &data.fields {
314            &fields.named
315        } else {
316            return Err(syn::Error::new_spanned(
317                &data.fields,
318                "Premix Model only supports structs with named fields",
319            ));
320        }
321    } else {
322        return Err(syn::Error::new_spanned(
323            input,
324            "Premix Model only supports structs",
325        ));
326    };
327
328    let mut db_fields = Vec::new();
329    let mut ignored_field_idents = Vec::new();
330
331    for field in all_fields {
332        if is_ignored(field) {
333            ignored_field_idents.push(field.ident.as_ref().unwrap());
334        } else {
335            db_fields.push(field);
336        }
337    }
338
339    let field_idents: Vec<_> = db_fields
340        .iter()
341        .map(|f| f.ident.as_ref().unwrap())
342        .collect();
343    let field_types: Vec<_> = db_fields.iter().map(|f| &f.ty).collect();
344    let _field_indices: Vec<_> = (0..db_fields.len()).collect();
345    let field_names: Vec<_> = field_idents.iter().map(|id| id.to_string()).collect();
346    let field_names_no_id: Vec<_> = field_names
347        .iter()
348        .filter(|name| *name != "id")
349        .cloned()
350        .collect();
351    let field_names_no_id_len = field_names_no_id.len();
352    let all_columns_joined = field_names.join(", ");
353    let no_id_columns_joined = field_names_no_id.join(", ");
354    let field_idents_len = field_idents.len();
355    let field_nullables: Vec<_> = db_fields.iter().map(|f| is_option_type(&f.ty)).collect();
356    let field_primary_keys: Vec<_> = field_names.iter().map(|n| n == "id").collect();
357    let field_sql_types: Vec<_> = db_fields
358        .iter()
359        .map(|field| {
360            let name = field.ident.as_ref().unwrap().to_string();
361            sql_type_for_field(&name, &field.ty).to_string()
362        })
363        .collect();
364    let field_sql_type_exprs: Vec<_> = db_fields
365        .iter()
366        .map(|field| {
367            let name = field.ident.as_ref().unwrap().to_string();
368            sql_type_expr_for_field(&name, &field.ty)
369        })
370        .collect();
371    let sensitive_field_literals: Vec<LitStr> = db_fields
372        .iter()
373        .filter(|f| is_sensitive(f))
374        .map(|f| {
375            LitStr::new(
376                &f.ident.as_ref().unwrap().to_string(),
377                f.ident.as_ref().unwrap().span(),
378            )
379        })
380        .collect();
381
382    let eager_load_body = relations::generate_eager_load_body(input)?;
383    let (index_specs, foreign_key_specs) = collect_schema_specs(all_fields, &table_name)?;
384    let index_tokens: Vec<_> = index_specs
385        .iter()
386        .map(|spec| {
387            let name = &spec.name;
388            let columns = &spec.columns;
389            let unique = spec.unique;
390            quote! {
391                premix_orm::schema::SchemaIndex {
392                    name: #name.to_string(),
393                    columns: vec![#(#columns.to_string()),*],
394                    unique: #unique,
395                }
396            }
397        })
398        .collect();
399    let foreign_key_tokens: Vec<_> = foreign_key_specs
400        .iter()
401        .map(|spec| {
402            let column = &spec.column;
403            let ref_table = &spec.ref_table;
404            let ref_column = &spec.ref_column;
405            quote! {
406                premix_orm::schema::SchemaForeignKey {
407                    column: #column.to_string(),
408                    ref_table: #ref_table.to_string(),
409                    ref_column: #ref_column.to_string(),
410                }
411            }
412        })
413        .collect();
414    let has_version = field_names.contains(&"version".to_string());
415    let has_soft_delete = field_names.contains(&"deleted_at".to_string());
416
417    let update_impl = if has_version {
418        quote! {
419            fn update<'a, E>(
420                &'a mut self,
421                executor: E,
422            ) -> impl ::std::future::Future<
423                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
424            > + Send
425            where
426                E: premix_orm::IntoExecutor<'a, DB = DB>
427            {
428                async move {
429                let mut executor = executor.into_executor();
430                let table_name = Self::table_name();
431                let mut set_clause = String::new();
432                let mut i = 1usize;
433                #(
434                    if i > 1 {
435                        set_clause.push_str(", ");
436                    }
437                    set_clause.push_str(#field_names);
438                    set_clause.push_str(" = ");
439                    set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
440                    i += 1;
441                )*
442                let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
443                let ver_p = <DB as premix_orm::SqlDialect>::placeholder(2 + #field_idents_len);
444                let sql = format!(
445                    "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
446                    table_name, set_clause, id_p, ver_p
447                );
448
449                premix_orm::tracing::debug!(
450                    operation = "update",
451                    table = table_name,
452                    sql = %sql,
453                    "premix query"
454                );
455
456                let mut query = premix_orm::sqlx::query::<DB>(&sql)
457                    #( .bind(&self.#field_idents) )*
458                    .bind(&self.id)
459                    .bind(&self.version);
460
461                let result = executor.execute(query).await?;
462
463                if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
464                    let exists_p = <DB as premix_orm::SqlDialect>::placeholder(1);
465                    let exists_sql = format!("SELECT id FROM {} WHERE id = {}", table_name, exists_p);
466                    let exists_query = premix_orm::sqlx::query_as::<DB, (i32,)>(&exists_sql).bind(&self.id);
467                    let exists = executor.fetch_optional(exists_query).await?;
468
469                    if exists.is_none() {
470                        Ok(premix_orm::UpdateResult::NotFound)
471                    } else {
472                        Ok(premix_orm::UpdateResult::VersionConflict)
473                    }
474                } else {
475                    self.version += 1;
476                    Ok(premix_orm::UpdateResult::Success)
477                }
478                }
479            }
480        }
481    } else {
482        quote! {
483            fn update<'a, E>(
484                &'a mut self,
485                executor: E,
486            ) -> impl ::std::future::Future<
487                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
488            > + Send
489            where
490                E: premix_orm::IntoExecutor<'a, DB = DB>
491            {
492                async move {
493                let mut executor = executor.into_executor();
494                let table_name = Self::table_name();
495                let mut set_clause = String::new();
496                let mut i = 1usize;
497                #(
498                    if i > 1 {
499                        set_clause.push_str(", ");
500                    }
501                    set_clause.push_str(#field_names);
502                    set_clause.push_str(" = ");
503                    set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
504                    i += 1;
505                )*
506                let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
507                let sql = format!("UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
508
509                premix_orm::tracing::debug!(
510                    operation = "update",
511                    table = table_name,
512                    sql = %sql,
513                    "premix query"
514                );
515
516                let mut query = premix_orm::sqlx::query::<DB>(&sql)
517                    #( .bind(&self.#field_idents) )*
518                    .bind(&self.id);
519
520                let result = executor.execute(query).await?;
521
522                if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
523                    Ok(premix_orm::UpdateResult::NotFound)
524                } else {
525                    Ok(premix_orm::UpdateResult::Success)
526                }
527                }
528            }
529        }
530    };
531
532    let delete_impl = if has_soft_delete {
533        quote! {
534            fn delete<'a, E>(
535                &'a mut self,
536                executor: E,
537            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
538            + Send
539            where
540                E: premix_orm::IntoExecutor<'a, DB = DB>
541            {
542                async move {
543                let mut executor = executor.into_executor();
544                let table_name = Self::table_name();
545                let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
546                let sql = format!("UPDATE {} SET deleted_at = {} WHERE id = {}", table_name, <DB as premix_orm::SqlDialect>::current_timestamp_fn(), id_p);
547
548                premix_orm::tracing::debug!(
549                    operation = "delete",
550                    table = table_name,
551                    sql = %sql,
552                    "premix query"
553                );
554
555                let query = premix_orm::sqlx::query::<DB>(&sql).bind(&self.id);
556                executor.execute(query).await?;
557
558                self.deleted_at = Some("DELETED".to_string());
559                Ok(())
560                }
561            }
562            fn has_soft_delete() -> bool { true }
563        }
564    } else {
565        quote! {
566            fn delete<'a, E>(
567                &'a mut self,
568                executor: E,
569            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
570            + Send
571            where
572                E: premix_orm::IntoExecutor<'a, DB = DB>
573            {
574                async move {
575                let mut executor = executor.into_executor();
576                let table_name = Self::table_name();
577                let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
578                let sql = format!("DELETE FROM {} WHERE id = {}", table_name, id_p);
579
580                premix_orm::tracing::debug!(
581                    operation = "delete",
582                    table = table_name,
583                    sql = %sql,
584                    "premix query"
585                );
586
587                let query = premix_orm::sqlx::query::<DB>(&sql).bind(&self.id);
588                executor.execute(query).await?;
589
590                Ok(())
591                }
592            }
593            fn has_soft_delete() -> bool { false }
594        }
595    };
596
597    let mut related_model_bounds = Vec::new();
598    for field in all_fields {
599        for attr in &field.attrs {
600            if attr.path().is_ident("has_many")
601                && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
602            {
603                related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> });
604            } else if attr.path().is_ident("belongs_to")
605                && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
606            {
607                related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> + Clone });
608            }
609        }
610    }
611
612    let hooks_impl = if custom_hooks {
613        quote! {}
614    } else {
615        quote! {
616            impl premix_orm::ModelHooks for #struct_name {}
617        }
618    };
619
620    let validation_impl = if custom_validation {
621        quote! {}
622    } else {
623        quote! {
624            impl premix_orm::ModelValidation for #struct_name {}
625        }
626    };
627
628    // Generic Implementation
629    Ok(quote! {
630        impl<'r, R> premix_orm::sqlx::FromRow<'r, R> for #struct_name
631        where
632            R: premix_orm::sqlx::Row,
633            R::Database: premix_orm::sqlx::Database,
634            #(
635                #field_types: premix_orm::sqlx::Type<R::Database> + premix_orm::sqlx::Decode<'r, R::Database>,
636            )*
637            for<'c> &'c str: premix_orm::sqlx::ColumnIndex<R>,
638        {
639            fn from_row(row: &'r R) -> Result<Self, premix_orm::sqlx::Error> {
640                use premix_orm::sqlx::Row;
641                Ok(Self {
642                    #(
643                        #field_idents: row.try_get(#field_names)?,
644                    )*
645                    #(
646                        #ignored_field_idents: None,
647                    )*
648                })
649            }
650        }
651
652
653        impl<DB> premix_orm::Model<DB> for #struct_name
654        where
655            DB: premix_orm::SqlDialect,
656            for<'c> &'c str: premix_orm::sqlx::ColumnIndex<DB::Row>,
657            usize: premix_orm::sqlx::ColumnIndex<DB::Row>,
658            for<'q> <DB as premix_orm::sqlx::Database>::Arguments<'q>: premix_orm::sqlx::IntoArguments<'q, DB>,
659            for<'c> &'c mut <DB as premix_orm::sqlx::Database>::Connection: premix_orm::sqlx::Executor<'c, Database = DB>,
660            i32: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
661            i64: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
662            String: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
663            bool: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
664            Option<String>: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
665            #( #related_model_bounds, )*
666        {
667            fn table_name() -> &'static str {
668                #table_name
669            }
670
671            fn create_table_sql() -> String {
672                let mut cols = vec!["id ".to_string() + <DB as premix_orm::SqlDialect>::auto_increment_pk()];
673                #(
674                    if #field_names != "id" {
675                        let sql_type = #field_sql_type_exprs;
676                        cols.push(format!("{} {}", #field_names, sql_type));
677                    }
678                )*
679                format!("CREATE TABLE IF NOT EXISTS {} ({})", #table_name, cols.join(", "))
680            }
681
682            fn list_columns() -> Vec<String> {
683                vec![ #( #field_names.to_string() ),* ]
684            }
685
686            fn sensitive_fields() -> &'static [&'static str] {
687                &[ #( #sensitive_field_literals ),* ]
688            }
689
690            fn save<'a, E>(
691                &'a mut self,
692                executor: E,
693            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
694            + Send
695            where
696                E: premix_orm::IntoExecutor<'a, DB = DB>
697            {
698                async move {
699                let mut executor = executor.into_executor();
700                use premix_orm::ModelHooks;
701                self.before_save().await?;
702
703                // Filter out 'id' and 'version' for INSERT
704                const ALL_COLUMNS: [&str; #field_idents_len] = [#( #field_names ),*];
705                const NO_ID_COLUMNS: [&str; #field_names_no_id_len] = [#( #field_names_no_id ),*];
706                let columns: &[&str] = if self.id == 0 { &NO_ID_COLUMNS } else { &ALL_COLUMNS };
707                let column_list: &str = if self.id == 0 { #no_id_columns_joined } else { #all_columns_joined };
708                let placeholders = premix_orm::build_placeholders::<DB>(1, columns.len());
709
710                let supports_returning = <DB as premix_orm::SqlDialect>::supports_returning();
711                if supports_returning {
712                    let sql = format!(
713                        "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
714                        #table_name,
715                        column_list,
716                        placeholders
717                    );
718
719                    premix_orm::tracing::debug!(
720                        operation = "insert",
721                        table = #table_name,
722                        sql = %sql,
723                        "premix query"
724                    );
725
726                    let mut query = premix_orm::sqlx::query_as::<DB, (i32,)>(&sql);
727                    #(
728                        if #field_names != "id" {
729                            query = query.bind(&self.#field_idents);
730                        } else if self.id != 0 {
731                            query = query.bind(&self.id);
732                        }
733                    )*
734
735                    if let Some((id,)) = executor.fetch_optional(query).await? {
736                        self.id = id;
737                    }
738                } else {
739                    let sql = format!(
740                        "INSERT INTO {} ({}) VALUES ({})",
741                        #table_name,
742                        column_list,
743                        placeholders
744                    );
745
746                    premix_orm::tracing::debug!(
747                        operation = "insert",
748                        table = #table_name,
749                        sql = %sql,
750                        "premix query"
751                    );
752
753                    let mut query = premix_orm::sqlx::query::<DB>(&sql);
754                    #(
755                        if #field_names != "id" {
756                            query = query.bind(&self.#field_idents);
757                        } else if self.id != 0 {
758                            query = query.bind(&self.id);
759                        }
760                    )*
761
762                    let result = executor.execute(query).await?;
763                    let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
764                    if last_id > 0 {
765                        self.id = last_id as i32;
766                    }
767                }
768
769                self.after_save().await?;
770                Ok(())
771                }
772            }
773
774            #update_impl
775            #delete_impl
776
777            fn find_by_id<'a, E>(
778                executor: E,
779                id: i32,
780            ) -> impl ::std::future::Future<Output = Result<Option<Self>, premix_orm::sqlx::Error>>
781            + Send
782            where
783                E: premix_orm::IntoExecutor<'a, DB = DB>
784            {
785                async move {
786                let mut executor = executor.into_executor();
787                let p = <DB as premix_orm::SqlDialect>::placeholder(1);
788                let mut where_clause = format!("WHERE id = {}", p);
789                if Self::has_soft_delete() {
790                    where_clause.push_str(" AND deleted_at IS NULL");
791                }
792                let sql = format!("SELECT * FROM {} {} LIMIT 1", #table_name, where_clause);
793                premix_orm::tracing::debug!(
794                    operation = "select",
795                    table = #table_name,
796                    sql = %sql,
797                    "premix query"
798                );
799                let query = premix_orm::sqlx::query_as::<DB, Self>(&sql).bind(id);
800
801                executor.fetch_optional(query).await
802                }
803            }
804
805            fn eager_load<'a>(
806                models: &mut [Self],
807                relation: &str,
808                executor: premix_orm::Executor<'a, DB>,
809            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>> + Send
810            {
811                async move {
812                    let mut executor = executor;
813                    #eager_load_body
814                }
815            }
816        }
817
818        #hooks_impl
819        #validation_impl
820
821        impl premix_orm::ModelSchema for #struct_name {
822            fn schema() -> premix_orm::schema::SchemaTable {
823                let columns = vec![
824                    #(
825                        premix_orm::schema::SchemaColumn {
826                            name: #field_names.to_string(),
827                            sql_type: #field_sql_types.to_string(),
828                            nullable: #field_nullables,
829                            primary_key: #field_primary_keys,
830                        }
831                    ),*
832                ];
833                let indexes = vec![
834                    #(#index_tokens),*
835                ];
836                let foreign_keys = vec![
837                    #(#foreign_key_tokens),*
838                ];
839                premix_orm::schema::SchemaTable {
840                    name: #table_name.to_string(),
841                    columns,
842                    indexes,
843                    foreign_keys,
844                    create_sql: None,
845                }
846            }
847        }
848    })
849}
850
851fn has_premix_field_flag(field: &Field, flag: &str) -> bool {
852    for attr in &field.attrs {
853        if attr.path().is_ident("premix")
854            && let Ok(meta) = attr.parse_args::<syn::Ident>()
855            && meta == flag
856        {
857            return true;
858        }
859    }
860    false
861}
862
863fn is_ignored(field: &Field) -> bool {
864    has_premix_field_flag(field, "ignore")
865}
866
867fn is_sensitive(field: &Field) -> bool {
868    has_premix_field_flag(field, "sensitive")
869}
870
871struct IndexSpec {
872    name: String,
873    columns: Vec<String>,
874    unique: bool,
875}
876
877struct ForeignKeySpec {
878    column: String,
879    ref_table: String,
880    ref_column: String,
881}
882
883fn collect_schema_specs(
884    fields: &syn::punctuated::Punctuated<Field, Token![,]>,
885    table_name: &str,
886) -> syn::Result<(Vec<IndexSpec>, Vec<ForeignKeySpec>)> {
887    let mut indexes = Vec::new();
888    let mut foreign_keys = Vec::new();
889
890    for field in fields {
891        if is_ignored(field) {
892            continue;
893        }
894        let field_name = field
895            .ident
896            .as_ref()
897            .ok_or_else(|| syn::Error::new_spanned(field, "Field must have an ident"))?
898            .to_string();
899
900        for attr in &field.attrs {
901            if !attr.path().is_ident("premix") {
902                continue;
903            }
904
905            attr.parse_nested_meta(|meta| {
906                if meta.path.is_ident("index") || meta.path.is_ident("unique") {
907                    let unique = meta.path.is_ident("unique");
908                    let mut name = None;
909                    if meta.input.peek(syn::token::Paren) {
910                        meta.parse_nested_meta(|nested| {
911                            if nested.path.is_ident("name") {
912                                let lit: LitStr = nested.value()?.parse()?;
913                                name = Some(lit.value());
914                                Ok(())
915                            } else {
916                                Err(nested.error("unsupported index option"))
917                            }
918                        })?;
919                    }
920                    let index_name =
921                        name.unwrap_or_else(|| format!("idx_{}_{}", table_name, field_name));
922                    indexes.push(IndexSpec {
923                        name: index_name,
924                        columns: vec![field_name.clone()],
925                        unique,
926                    });
927                } else if meta.path.is_ident("foreign_key") {
928                    let mut ref_table = None;
929                    let mut ref_column = None;
930                    meta.parse_nested_meta(|nested| {
931                        if nested.path.is_ident("table") {
932                            let lit: LitStr = nested.value()?.parse()?;
933                            ref_table = Some(lit.value());
934                            Ok(())
935                        } else if nested.path.is_ident("column") {
936                            let lit: LitStr = nested.value()?.parse()?;
937                            ref_column = Some(lit.value());
938                            Ok(())
939                        } else {
940                            Err(nested.error("unsupported foreign_key option"))
941                        }
942                    })?;
943
944                    let ref_table = ref_table.ok_or_else(|| {
945                        syn::Error::new_spanned(attr, "foreign_key requires table = \"...\"")
946                    })?;
947                    let ref_column = ref_column.unwrap_or_else(|| "id".to_string());
948                    foreign_keys.push(ForeignKeySpec {
949                        column: field_name.clone(),
950                        ref_table,
951                        ref_column,
952                    });
953                }
954                Ok(())
955            })?;
956        }
957    }
958
959    Ok((indexes, foreign_keys))
960}
961
962fn is_option_type(ty: &syn::Type) -> bool {
963    if let syn::Type::Path(path) = ty {
964        if let Some(seg) = path.path.segments.last() {
965            return seg.ident == "Option";
966        }
967    }
968    false
969}
970
971fn has_premix_flag(attrs: &[Attribute], flag: &str) -> bool {
972    for attr in attrs {
973        if attr.path().is_ident("premix") {
974            let args = attr.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated);
975            if let Ok(args) = args {
976                if args.iter().any(|ident| ident == flag) {
977                    return true;
978                }
979            }
980        }
981    }
982    false
983}
984
985fn type_name_for_field(ty: &syn::Type) -> Option<String> {
986    if let syn::Type::Path(path) = ty {
987        let segment = path.path.segments.last()?;
988        let ident = segment.ident.to_string();
989        if ident == "Option" {
990            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
991                for arg in args.args.iter() {
992                    if let syn::GenericArgument::Type(inner) = arg {
993                        return type_name_for_field(inner);
994                    }
995                }
996            }
997            None
998        } else if ident == "Vec" {
999            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
1000                if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
1001                    if let Some(inner_ident) = type_name_for_field(inner) {
1002                        return Some(format!("Vec<{}>", inner_ident));
1003                    }
1004                }
1005            }
1006            Some("Vec".to_string())
1007        } else {
1008            Some(ident)
1009        }
1010    } else {
1011        None
1012    }
1013}
1014
1015fn sql_type_for_field(name: &str, ty: &syn::Type) -> &'static str {
1016    let type_name = type_name_for_field(ty);
1017    match type_name.as_deref() {
1018        Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => "INTEGER",
1019        Some("i64" | "u64") => "BIGINT",
1020        Some("f32" | "f64") => "REAL",
1021        Some("bool") => "BOOLEAN",
1022        Some("String" | "str") => "TEXT",
1023        Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => "TEXT",
1024        Some("Vec<u8>") => "BLOB",
1025        _ => {
1026            if name == "id" || name.ends_with("_id") {
1027                "INTEGER"
1028            } else {
1029                "TEXT"
1030            }
1031        }
1032    }
1033}
1034
1035fn sql_type_expr_for_field(name: &str, ty: &syn::Type) -> proc_macro2::TokenStream {
1036    let type_name = type_name_for_field(ty);
1037    match type_name.as_deref() {
1038        Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => {
1039            quote! { <DB as premix_orm::SqlDialect>::int_type() }
1040        }
1041        Some("i64" | "u64") => quote! { <DB as premix_orm::SqlDialect>::bigint_type() },
1042        Some("f32" | "f64") => quote! { <DB as premix_orm::SqlDialect>::float_type() },
1043        Some("bool") => quote! { <DB as premix_orm::SqlDialect>::bool_type() },
1044        Some("String" | "str") => quote! { <DB as premix_orm::SqlDialect>::text_type() },
1045        Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => {
1046            quote! { <DB as premix_orm::SqlDialect>::text_type() }
1047        }
1048        Some("Vec<u8>") => quote! { <DB as premix_orm::SqlDialect>::blob_type() },
1049        _ => {
1050            if name == "id" || name.ends_with("_id") {
1051                quote! { <DB as premix_orm::SqlDialect>::int_type() }
1052            } else {
1053                quote! { <DB as premix_orm::SqlDialect>::text_type() }
1054            }
1055        }
1056    }
1057}