Skip to main content

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;
9mod static_query;
10
11/// Compile-time query macro for true Zero-Overhead SQL generation.
12///
13/// This macro generates SQL at compile time, achieving 0% overhead compared to raw sqlx.
14///
15/// # Example
16///
17/// ```ignore
18/// use premix_orm::prelude::*;
19///
20/// // FIND (SELECT + LIMIT 1)
21/// let user = premix_query!(User, FIND, filter_eq("id", 1))
22///     .fetch_one(&pool).await?;
23///
24/// // INSERT
25/// let new_user = premix_query!(
26///     User, INSERT,
27///     set("name", "Bob"),
28///     set("age", 30)
29/// ).fetch_one(&pool).await?;
30///
31/// // UPDATE
32/// premix_query!(
33///     User, UPDATE,
34///     set("age", 31),
35///     filter_eq("name", "Bob")
36/// ).execute(&pool).await?;
37///
38/// // DELETE
39/// premix_query!(
40///     User, DELETE,
41///     filter_eq("id", 1)
42/// ).execute(&pool).await?;
43///
44/// // UPDATE + RETURNING *
45/// let updated = premix_query!(
46///     User, UPDATE,
47///     set("age", 32),
48///     filter_eq("name", "Bob"),
49///     returning_all()
50/// ).fetch_one(&pool).await?;
51/// ```
52///
53/// # Supported Operations
54///
55/// - `SELECT` - Generate SELECT query
56/// - `FIND` - Generate SELECT + LIMIT 1 query
57/// - `INSERT` - Generate INSERT query (with `set` assignments)
58/// - `UPDATE` - Generate UPDATE query (with `set` assignments and filters)
59/// - `DELETE` - Generate DELETE query (with filters)
60///
61/// # Supported Clauses
62///
63/// - `filter_eq/ne/gt/lt/gte/lte("col", val)` - WHERE clause conditions
64/// - `set("col", val)` - SET/VALUES clause for INSERT/UPDATE
65/// - `limit(N)` - LIMIT N
66/// - `offset(N)` - OFFSET N
67/// - `returning_all()` - Append `RETURNING *` for UPDATE/DELETE
68#[proc_macro]
69pub fn premix_query(input: TokenStream) -> TokenStream {
70    let input = parse_macro_input!(input as static_query::StaticQueryInput);
71    TokenStream::from(static_query::generate_static_query(input))
72}
73
74#[proc_macro_derive(Model, attributes(has_many, belongs_to, premix))]
75pub fn derive_model(input: TokenStream) -> TokenStream {
76    let input = parse_macro_input!(input as DeriveInput);
77    match derive_model_impl(&input) {
78        Ok(tokens) => TokenStream::from(tokens),
79        Err(err) => TokenStream::from(err.to_compile_error()),
80    }
81}
82
83fn derive_model_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
84    let impl_block = generate_generic_impl(input)?;
85    let rel_block = relations::impl_relations(input)?;
86    Ok(quote! {
87        #impl_block
88        #rel_block
89    })
90}
91
92fn generate_generic_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
93    let struct_name = &input.ident;
94    let table_name = struct_name.to_string().to_lowercase() + "s";
95    let custom_hooks = has_premix_flag(&input.attrs, "custom_hooks");
96    let custom_validation = has_premix_flag(&input.attrs, "custom_validation");
97
98    let all_fields = if let Data::Struct(data) = &input.data {
99        if let Fields::Named(fields) = &data.fields {
100            &fields.named
101        } else {
102            return Err(syn::Error::new_spanned(
103                &data.fields,
104                "Premix Model only supports structs with named fields",
105            ));
106        }
107    } else {
108        return Err(syn::Error::new_spanned(
109            input,
110            "Premix Model only supports structs",
111        ));
112    };
113
114    let mut db_fields = Vec::new();
115    let mut ignored_field_idents = Vec::new();
116
117    for field in all_fields {
118        if is_ignored(field) {
119            ignored_field_idents.push(field.ident.as_ref().unwrap());
120        } else {
121            db_fields.push(field);
122        }
123    }
124
125    let field_idents: Vec<_> = db_fields
126        .iter()
127        .map(|f| f.ident.as_ref().unwrap())
128        .collect();
129    let field_types: Vec<_> = db_fields.iter().map(|f| &f.ty).collect();
130    let _field_indices: Vec<_> = (0..db_fields.len()).collect();
131    let field_names: Vec<_> = field_idents.iter().map(|id| id.to_string()).collect();
132    let field_names_no_id: Vec<_> = field_names
133        .iter()
134        .filter(|name| *name != "id")
135        .cloned()
136        .collect();
137    let field_names_no_id_len = field_names_no_id.len();
138    // all_columns_joined and no_id_columns_joined removed as part of Zero-Overhead optimization (replaced by concat!)
139
140    // Prepare head/tail for concat! (to avoid trailing commas and handle separators)
141    let all_cols_head = field_names.first().cloned().unwrap_or_default();
142    let all_cols_tail: Vec<_> = field_names.iter().skip(1).cloned().collect();
143
144    let no_id_cols_head = field_names_no_id.first().cloned().unwrap_or_default();
145    let no_id_cols_tail: Vec<_> = field_names_no_id.iter().skip(1).cloned().collect();
146
147    let field_idents_len = field_idents.len();
148    let field_nullables: Vec<_> = db_fields.iter().map(|f| is_option_type(&f.ty)).collect();
149    let field_primary_keys: Vec<_> = field_names.iter().map(|n| n == "id").collect();
150    let field_sql_types: Vec<_> = db_fields
151        .iter()
152        .map(|field| {
153            let name = field.ident.as_ref().unwrap().to_string();
154            sql_type_for_field(&name, &field.ty).to_string()
155        })
156        .collect();
157    let field_sql_type_exprs: Vec<_> = db_fields
158        .iter()
159        .map(|field| {
160            let name = field.ident.as_ref().unwrap().to_string();
161            sql_type_expr_for_field(&name, &field.ty)
162        })
163        .collect();
164    let sensitive_field_literals: Vec<LitStr> = db_fields
165        .iter()
166        .filter(|f| is_sensitive(f))
167        .map(|f| {
168            LitStr::new(
169                &f.ident.as_ref().unwrap().to_string(),
170                f.ident.as_ref().unwrap().span(),
171            )
172        })
173        .collect();
174
175    let eager_load_body = relations::generate_eager_load_body(input)?;
176    let (index_specs, foreign_key_specs) = collect_schema_specs(all_fields, &table_name)?;
177    let index_tokens: Vec<_> = index_specs
178        .iter()
179        .map(|spec| {
180            let name = &spec.name;
181            let columns = &spec.columns;
182            let unique = spec.unique;
183            quote! {
184                premix_orm::schema::SchemaIndex {
185                    name: #name.to_string(),
186                    columns: vec![#(#columns.to_string()),*],
187                    unique: #unique,
188                }
189            }
190        })
191        .collect();
192    let foreign_key_tokens: Vec<_> = foreign_key_specs
193        .iter()
194        .map(|spec| {
195            let column = &spec.column;
196            let ref_table = &spec.ref_table;
197            let ref_column = &spec.ref_column;
198            quote! {
199                premix_orm::schema::SchemaForeignKey {
200                    column: #column.to_string(),
201                    ref_table: #ref_table.to_string(),
202                    ref_column: #ref_column.to_string(),
203                }
204            }
205        })
206        .collect();
207    let has_version = field_names.contains(&"version".to_string());
208    let has_soft_delete = field_names.contains(&"deleted_at".to_string());
209
210    let update_impl = if has_version {
211        quote! {
212            fn update<'a, E>(
213                &'a mut self,
214                executor: E,
215            ) -> impl ::std::future::Future<
216                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
217            > + Send
218            where
219                E: premix_orm::IntoExecutor<'a, DB = DB>
220            {
221                async move {
222                let mut executor = executor.into_executor();
223                let table_name = Self::table_name();
224                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
225                let sql = SQL.get_or_init(|| {
226                    let mut set_clause = String::with_capacity(#field_idents_len * 8);
227                    let mut i = 1usize;
228                    #(
229                        if i > 1 {
230                            set_clause.push_str(", ");
231                        }
232                        set_clause.push_str(#field_names);
233                        set_clause.push_str(" = ");
234                        set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
235                        i += 1;
236                    )*
237                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
238                    let ver_p = <DB as premix_orm::SqlDialect>::placeholder(2 + #field_idents_len);
239                    let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 64);
240                    use ::std::fmt::Write;
241                    let _ = write!(
242                        sql,
243                        "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
244                        table_name,
245                        set_clause,
246                        id_p,
247                        ver_p
248                    );
249                    sql
250                });
251
252                premix_orm::tracing::debug!(
253                    operation = "update",
254                    table = table_name,
255                    sql = %sql,
256                    "premix query"
257                );
258
259                let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
260                    #( .bind(&self.#field_idents) )*
261                    .bind(&self.id)
262                    .bind(&self.version);
263
264                let result = executor.execute(query).await?;
265
266                if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
267                    static EXISTS_SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
268                    let exists_sql = EXISTS_SQL.get_or_init(|| {
269                        let exists_p = <DB as premix_orm::SqlDialect>::placeholder(1);
270                        let mut exists_sql = String::with_capacity(table_name.len() + 32);
271                        use ::std::fmt::Write;
272                        let _ = write!(exists_sql, "SELECT id FROM {} WHERE id = {}", table_name, exists_p);
273                        exists_sql
274                    });
275                    let exists_query =
276                        premix_orm::sqlx::query_as::<DB, (i32,)>(exists_sql)
277                            .persistent(true)
278                            .bind(&self.id);
279                    let exists = executor.fetch_optional(exists_query).await?;
280
281                    if exists.is_none() {
282                        Ok(premix_orm::UpdateResult::NotFound)
283                    } else {
284                        Ok(premix_orm::UpdateResult::VersionConflict)
285                    }
286                } else {
287                    self.version += 1;
288                    Ok(premix_orm::UpdateResult::Success)
289                }
290                }
291            }
292
293            fn update_fast<'a, E>(
294                &'a mut self,
295                executor: E,
296            ) -> impl ::std::future::Future<
297                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
298            > + Send
299            where
300                E: premix_orm::IntoExecutor<'a, DB = DB>
301            {
302                async move {
303                let mut executor = executor.into_executor();
304                let table_name = Self::table_name();
305                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
306                let sql = SQL.get_or_init(|| {
307                    let mut set_clause = String::with_capacity(#field_idents_len * 8);
308                    let mut i = 1usize;
309                    #(
310                        if i > 1 {
311                            set_clause.push_str(", ");
312                        }
313                        set_clause.push_str(#field_names);
314                        set_clause.push_str(" = ");
315                        set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
316                        i += 1;
317                    )*
318                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
319                    let ver_p = <DB as premix_orm::SqlDialect>::placeholder(2 + #field_idents_len);
320                    let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 64);
321                    use ::std::fmt::Write;
322                    let _ = write!(
323                        sql,
324                        "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
325                        table_name,
326                        set_clause,
327                        id_p,
328                        ver_p
329                    );
330                    sql
331                });
332
333                let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
334                    #( .bind(&self.#field_idents) )*
335                    .bind(&self.id)
336                    .bind(&self.version);
337
338                let result = executor.execute(query).await?;
339
340                if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
341                    static EXISTS_SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
342                    let exists_sql = EXISTS_SQL.get_or_init(|| {
343                        let exists_p = <DB as premix_orm::SqlDialect>::placeholder(1);
344                        let mut exists_sql = String::with_capacity(table_name.len() + 32);
345                        use ::std::fmt::Write;
346                        let _ = write!(exists_sql, "SELECT id FROM {} WHERE id = {}", table_name, exists_p);
347                        exists_sql
348                    });
349                    let exists_query =
350                        premix_orm::sqlx::query_as::<DB, (i32,)>(exists_sql)
351                            .persistent(true)
352                            .bind(&self.id);
353                    let exists = executor.fetch_optional(exists_query).await?;
354
355                    if exists.is_none() {
356                        Ok(premix_orm::UpdateResult::NotFound)
357                    } else {
358                        Ok(premix_orm::UpdateResult::VersionConflict)
359                    }
360                } else {
361                    self.version += 1;
362                    Ok(premix_orm::UpdateResult::Success)
363                }
364                }
365            }
366            fn update_ultra<'a, E>(
367                &'a mut self,
368                executor: E,
369            ) -> impl ::std::future::Future<
370                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
371            > + Send
372            where
373                E: premix_orm::IntoExecutor<'a, DB = DB>
374            {
375                async move { self.update_fast(executor).await }
376            }
377        }
378    } else {
379        quote! {
380            fn update<'a, E>(
381                &'a mut self,
382                executor: E,
383            ) -> impl ::std::future::Future<
384                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
385            > + Send
386            where
387                E: premix_orm::IntoExecutor<'a, DB = DB>
388            {
389                async move {
390                let mut executor = executor.into_executor();
391                let table_name = Self::table_name();
392                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
393                let sql = SQL.get_or_init(|| {
394                    let mut set_clause = String::with_capacity(#field_idents_len * 8);
395                    let mut i = 1usize;
396                    #(
397                        if i > 1 {
398                            set_clause.push_str(", ");
399                        }
400                        set_clause.push_str(#field_names);
401                        set_clause.push_str(" = ");
402                        set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
403                        i += 1;
404                    )*
405                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
406                    let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 32);
407                    use ::std::fmt::Write;
408                    let _ = write!(sql, "UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
409                    sql
410                });
411
412                premix_orm::tracing::debug!(
413                    operation = "update",
414                    table = table_name,
415                    sql = %sql,
416                    "premix query"
417                );
418
419                let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
420                    #( .bind(&self.#field_idents) )*
421                    .bind(&self.id);
422
423                let result = executor.execute(query).await?;
424
425                if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
426                    Ok(premix_orm::UpdateResult::NotFound)
427                } else {
428                    Ok(premix_orm::UpdateResult::Success)
429                }
430                }
431            }
432
433            fn update_fast<'a, E>(
434                &'a mut self,
435                executor: E,
436            ) -> impl ::std::future::Future<
437                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
438            > + Send
439            where
440                E: premix_orm::IntoExecutor<'a, DB = DB>
441            {
442                async move {
443                let mut executor = executor.into_executor();
444                let table_name = Self::table_name();
445                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
446                let sql = SQL.get_or_init(|| {
447                    let mut set_clause = String::with_capacity(#field_idents_len * 8);
448                    let mut i = 1usize;
449                    #(
450                        if i > 1 {
451                            set_clause.push_str(", ");
452                        }
453                        set_clause.push_str(#field_names);
454                        set_clause.push_str(" = ");
455                        set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
456                        i += 1;
457                    )*
458                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
459                    let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 32);
460                    use ::std::fmt::Write;
461                    let _ = write!(sql, "UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
462                    sql
463                });
464
465                let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
466                    #( .bind(&self.#field_idents) )*
467                    .bind(&self.id);
468
469                let result = executor.execute(query).await?;
470
471                if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
472                    Ok(premix_orm::UpdateResult::NotFound)
473                } else {
474                    Ok(premix_orm::UpdateResult::Success)
475                }
476                }
477            }
478            fn update_ultra<'a, E>(
479                &'a mut self,
480                executor: E,
481            ) -> impl ::std::future::Future<
482                Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
483            > + Send
484            where
485                E: premix_orm::IntoExecutor<'a, DB = DB>
486            {
487                async move { self.update_fast(executor).await }
488            }
489        }
490    };
491
492    let delete_impl = if has_soft_delete {
493        quote! {
494            fn delete<'a, E>(
495                &'a mut self,
496                executor: E,
497            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
498            + Send
499            where
500                E: premix_orm::IntoExecutor<'a, DB = DB>
501            {
502                async move {
503                let mut executor = executor.into_executor();
504                let table_name = Self::table_name();
505                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
506                let sql = SQL.get_or_init(|| {
507                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
508                    let mut sql = String::with_capacity(table_name.len() + 64);
509                    use ::std::fmt::Write;
510                    let _ = write!(
511                        sql,
512                        "UPDATE {} SET deleted_at = {} WHERE id = {}",
513                        table_name,
514                        <DB as premix_orm::SqlDialect>::current_timestamp_fn(),
515                        id_p
516                    );
517                    sql
518                });
519
520                premix_orm::tracing::debug!(
521                    operation = "delete",
522                    table = table_name,
523                    sql = %sql,
524                    "premix query"
525                );
526
527                let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
528                executor.execute(query).await?;
529
530                self.deleted_at = Some("DELETED".to_string());
531                Ok(())
532                }
533            }
534            fn delete_fast<'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                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
546                let sql = SQL.get_or_init(|| {
547                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
548                    let mut sql = String::with_capacity(table_name.len() + 64);
549                    use ::std::fmt::Write;
550                    let _ = write!(
551                        sql,
552                        "UPDATE {} SET deleted_at = {} WHERE id = {}",
553                        table_name,
554                        <DB as premix_orm::SqlDialect>::current_timestamp_fn(),
555                        id_p
556                    );
557                    sql
558                });
559
560                let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
561                executor.execute(query).await?;
562
563                self.deleted_at = Some("DELETED".to_string());
564                Ok(())
565                }
566            }
567            fn delete_ultra<'a, E>(
568                &'a mut self,
569                executor: E,
570            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
571            + Send
572            where
573                E: premix_orm::IntoExecutor<'a, DB = DB>
574            {
575                async move { self.delete_fast(executor).await }
576            }
577            fn has_soft_delete() -> bool { true }
578        }
579    } else {
580        quote! {
581            fn delete<'a, E>(
582                &'a mut self,
583                executor: E,
584            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
585            + Send
586            where
587                E: premix_orm::IntoExecutor<'a, DB = DB>
588            {
589                async move {
590                let mut executor = executor.into_executor();
591                let table_name = Self::table_name();
592                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
593                let sql = SQL.get_or_init(|| {
594                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
595                    let mut sql = String::with_capacity(table_name.len() + 24);
596                    use ::std::fmt::Write;
597                    let _ = write!(sql, "DELETE FROM {} WHERE id = {}", table_name, id_p);
598                    sql
599                });
600
601                premix_orm::tracing::debug!(
602                    operation = "delete",
603                    table = table_name,
604                    sql = %sql,
605                    "premix query"
606                );
607
608                let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
609                executor.execute(query).await?;
610
611                Ok(())
612                }
613            }
614            fn delete_fast<'a, E>(
615                &'a mut self,
616                executor: E,
617            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
618            + Send
619            where
620                E: premix_orm::IntoExecutor<'a, DB = DB>
621            {
622                async move {
623                let mut executor = executor.into_executor();
624                let table_name = Self::table_name();
625                static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
626                let sql = SQL.get_or_init(|| {
627                    let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
628                    let mut sql = String::with_capacity(table_name.len() + 24);
629                    use ::std::fmt::Write;
630                    let _ = write!(sql, "DELETE FROM {} WHERE id = {}", table_name, id_p);
631                    sql
632                });
633
634                let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
635                executor.execute(query).await?;
636
637                Ok(())
638                }
639            }
640            fn delete_ultra<'a, E>(
641                &'a mut self,
642                executor: E,
643            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
644            + Send
645            where
646                E: premix_orm::IntoExecutor<'a, DB = DB>
647            {
648                async move { self.delete_fast(executor).await }
649            }
650            fn has_soft_delete() -> bool { false }
651        }
652    };
653
654    let mut related_model_bounds = Vec::new();
655    for field in all_fields {
656        for attr in &field.attrs {
657            if attr.path().is_ident("has_many")
658                && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
659            {
660                related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> });
661            } else if attr.path().is_ident("belongs_to")
662                && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
663            {
664                related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> + Clone });
665            }
666        }
667    }
668
669    let hooks_impl = if custom_hooks {
670        quote! {}
671    } else {
672        quote! {
673            impl premix_orm::ModelHooks for #struct_name {}
674        }
675    };
676
677    let validation_impl = if custom_validation {
678        quote! {}
679    } else {
680        quote! {
681            impl premix_orm::ModelValidation for #struct_name {}
682        }
683    };
684
685    let col_consts: Vec<_> = field_names
686        .iter()
687        .zip(field_idents.iter())
688        .map(|(name, ident)| {
689            let const_name = syn::Ident::new(&ident.to_string().to_uppercase(), ident.span());
690            quote! {
691                pub const #const_name: &str = #name;
692            }
693        })
694        .collect();
695
696    let columns_mod_ident = syn::Ident::new(
697        &format!("columns_{}", struct_name.to_string().to_lowercase()),
698        struct_name.span(),
699    );
700
701    // Generic Implementation
702    Ok(quote! {
703        // Generate column constants
704        #[allow(non_snake_case)]
705        pub mod #columns_mod_ident {
706             #( #col_consts )*
707        }
708
709        impl<'r, R> premix_orm::sqlx::FromRow<'r, R> for #struct_name
710        where
711            R: premix_orm::sqlx::Row,
712            R::Database: premix_orm::sqlx::Database,
713            #(
714                #field_types: premix_orm::sqlx::Type<R::Database> + premix_orm::sqlx::Decode<'r, R::Database>,
715            )*
716            for<'c> &'c str: premix_orm::sqlx::ColumnIndex<R>,
717        {
718            fn from_row(row: &'r R) -> Result<Self, premix_orm::sqlx::Error> {
719                use premix_orm::sqlx::Row;
720                Ok(Self {
721                    #(
722                        #field_idents: row.try_get(#field_names)?,
723                    )*
724                    #(
725                        #ignored_field_idents: None,
726                    )*
727                })
728            }
729        }
730
731
732        impl<DB> premix_orm::Model<DB> for #struct_name
733        where
734            DB: premix_orm::SqlDialect,
735            for<'c> &'c str: premix_orm::sqlx::ColumnIndex<DB::Row>,
736            usize: premix_orm::sqlx::ColumnIndex<DB::Row>,
737            for<'q> <DB as premix_orm::sqlx::Database>::Arguments<'q>: premix_orm::sqlx::IntoArguments<'q, DB>,
738            for<'c> &'c mut <DB as premix_orm::sqlx::Database>::Connection: premix_orm::sqlx::Executor<'c, Database = DB>,
739            i32: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
740            i64: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
741            String: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
742            bool: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
743            Option<String>: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
744            #( #related_model_bounds, )*
745        {
746            fn table_name() -> &'static str {
747                #table_name
748            }
749
750            fn create_table_sql() -> String {
751                let mut cols = vec!["id ".to_string() + <DB as premix_orm::SqlDialect>::auto_increment_pk()];
752                #(
753                    if #field_names != "id" {
754                        let sql_type = #field_sql_type_exprs;
755                        cols.push(format!("{} {}", #field_names, sql_type));
756                    }
757                )*
758                format!("CREATE TABLE IF NOT EXISTS {} ({})", #table_name, cols.join(", "))
759            }
760
761            fn list_columns() -> ::std::vec::Vec<::std::string::String> {
762                vec![ #( #field_names.to_string() ),* ]
763            }
764
765            fn sensitive_fields() -> &'static [&'static str] {
766                &[ #( #sensitive_field_literals ),* ]
767            }
768
769            fn from_row_fast(row: &<DB as premix_orm::sqlx::Database>::Row) -> Result<Self, premix_orm::sqlx::Error>
770            where
771                usize: premix_orm::sqlx::ColumnIndex<<DB as premix_orm::sqlx::Database>::Row>,
772                for<'c> &'c str: premix_orm::sqlx::ColumnIndex<<DB as premix_orm::sqlx::Database>::Row>,
773            {
774                use premix_orm::sqlx::Row;
775                let mut idx: usize = 0;
776                #(
777                    let #field_idents = row.try_get(idx)?;
778                    idx += 1;
779                )*
780                Ok(Self {
781                    #( #field_idents, )*
782                    #( #ignored_field_idents: None, )*
783                })
784            }
785
786            fn save<'a, E>(
787                &'a mut self,
788                executor: E,
789            ) -> impl ::std::future::Future<Output = ::std::result::Result<(), premix_orm::sqlx::Error>>
790            + Send
791            where
792                E: premix_orm::IntoExecutor<'a, DB = DB>
793            {
794                async move {
795                let mut executor = executor.into_executor();
796                use premix_orm::ModelHooks;
797                self.before_save().await?;
798
799                // CONSTANT column lists to avoid runtime joining/allocation
800                // We use head/tail pattern to insert ", " separator without trailing comma
801                const ALL_COLUMNS_LIST: &str = concat!(#all_cols_head, #( ", ", #all_cols_tail ),*);
802                const NO_ID_COLUMNS_LIST: &str = concat!(#no_id_cols_head, #( ", ", #no_id_cols_tail ),*);
803
804                let column_list: &str = if self.id == 0 { NO_ID_COLUMNS_LIST } else { ALL_COLUMNS_LIST };
805
806                // We still need to calculate placeholders at runtime because they depend on the count and DB dialect
807                let count = if self.id == 0 { #field_names_no_id_len } else { #field_idents_len };
808                let placeholders = premix_orm::cached_placeholders::<DB>(count);
809
810                let supports_returning = <DB as premix_orm::SqlDialect>::supports_returning();
811                if supports_returning {
812                    // Optimized format usage
813                    let sql = format!(
814                        "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
815                        #table_name,
816                        column_list,
817                        placeholders
818                    );
819
820                    premix_orm::tracing::debug!(
821                        operation = "insert",
822                        table = #table_name,
823                        sql = %sql,
824                        "premix query"
825                    );
826
827                    let mut query = premix_orm::sqlx::query_as::<DB, (i32,)>(&sql)
828                        .persistent(true);
829                    #(
830                        if #field_names != "id" {
831                            query = query.bind(&self.#field_idents);
832                        } else if self.id != 0 {
833                            query = query.bind(&self.id);
834                        }
835                    )*
836
837                    if let Some((id,)) = executor.fetch_optional(query).await? {
838                        self.id = id;
839                    }
840                } else {
841                    let sql = format!(
842                        "INSERT INTO {} ({}) VALUES ({})",
843                        #table_name,
844                        column_list,
845                        placeholders
846                    );
847
848                    premix_orm::tracing::debug!(
849                        operation = "insert",
850                        table = #table_name,
851                        sql = %sql,
852                        "premix query"
853                    );
854
855                    let mut query = premix_orm::sqlx::query::<DB>(&sql).persistent(true);
856                    #(
857                        if #field_names != "id" {
858                            query = query.bind(&self.#field_idents);
859                        } else if self.id != 0 {
860                            query = query.bind(&self.id);
861                        }
862                    )*
863
864                    let result = executor.execute(query).await?;
865                    let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
866                    if last_id > 0 {
867                        self.id = last_id as i32;
868                    }
869                }
870
871                self.after_save().await?;
872                Ok(())
873                }
874            }
875
876            fn save_fast<'a, E>(
877                &'a mut self,
878                executor: E,
879            ) -> impl ::std::future::Future<Output = ::std::result::Result<(), premix_orm::sqlx::Error>>
880            + Send
881            where
882                E: premix_orm::IntoExecutor<'a, DB = DB>
883            {
884                async move {
885                let mut executor = executor.into_executor();
886
887                // CONSTANT column lists to avoid runtime joining/allocation
888                // We use head/tail pattern to insert ", " separator without trailing comma
889                const ALL_COLUMNS_LIST: &str = concat!(#all_cols_head, #( ", ", #all_cols_tail ),*);
890                const NO_ID_COLUMNS_LIST: &str = concat!(#no_id_cols_head, #( ", ", #no_id_cols_tail ),*);
891
892                let column_list: &str = if self.id == 0 { NO_ID_COLUMNS_LIST } else { ALL_COLUMNS_LIST };
893
894                // We still need to calculate placeholders at runtime because they depend on the count and DB dialect
895                let count = if self.id == 0 { #field_names_no_id_len } else { #field_idents_len };
896                let placeholders = premix_orm::cached_placeholders::<DB>(count);
897
898                let supports_returning = <DB as premix_orm::SqlDialect>::supports_returning();
899                if supports_returning {
900                    let sql = format!(
901                        "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
902                        #table_name,
903                        column_list,
904                        placeholders
905                    );
906
907                    let mut query = premix_orm::sqlx::query_as::<DB, (i32,)>(&sql)
908                        .persistent(true);
909                    #(
910                        if #field_names != "id" {
911                            query = query.bind(&self.#field_idents);
912                        } else if self.id != 0 {
913                            query = query.bind(&self.id);
914                        }
915                    )*
916
917                    if let Some((id,)) = executor.fetch_optional(query).await? {
918                        self.id = id;
919                    }
920                } else {
921                    let sql = format!(
922                        "INSERT INTO {} ({}) VALUES ({})",
923                        #table_name,
924                        column_list,
925                        placeholders
926                    );
927
928                    let mut query = premix_orm::sqlx::query::<DB>(&sql).persistent(true);
929                    #(
930                        if #field_names != "id" {
931                            query = query.bind(&self.#field_idents);
932                        } else if self.id != 0 {
933                            query = query.bind(&self.id);
934                        }
935                    )*
936
937                    let result = executor.execute(query).await?;
938                    let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
939                    if last_id > 0 {
940                        self.id = last_id as i32;
941                    }
942                }
943
944                Ok(())
945                }
946            }
947            fn save_ultra<'a, E>(
948                &'a mut self,
949                executor: E,
950            ) -> impl ::std::future::Future<Output = ::std::result::Result<(), premix_orm::sqlx::Error>>
951            + Send
952            where
953                E: premix_orm::IntoExecutor<'a, DB = DB>
954            {
955                async move {
956                let mut executor = executor.into_executor();
957
958                // CONSTANT column lists to avoid runtime joining/allocation
959                // We use head/tail pattern to insert ", " separator without trailing comma
960                const ALL_COLUMNS_LIST: &str = concat!(#all_cols_head, #( ", ", #all_cols_tail ),*);
961                const NO_ID_COLUMNS_LIST: &str = concat!(#no_id_cols_head, #( ", ", #no_id_cols_tail ),*);
962
963                let column_list: &str = if self.id == 0 { NO_ID_COLUMNS_LIST } else { ALL_COLUMNS_LIST };
964
965                // We still need to calculate placeholders at runtime because they depend on the count and DB dialect
966                let count = if self.id == 0 { #field_names_no_id_len } else { #field_idents_len };
967                let placeholders = premix_orm::cached_placeholders::<DB>(count);
968
969                let sql = format!(
970                    "INSERT INTO {} ({}) VALUES ({})",
971                    #table_name,
972                    column_list,
973                    placeholders
974                );
975
976                let mut query = premix_orm::sqlx::query::<DB>(&sql).persistent(true);
977                #(
978                    if #field_names != "id" {
979                        query = query.bind(&self.#field_idents);
980                    } else if self.id != 0 {
981                        query = query.bind(&self.id);
982                    }
983                )*
984
985                let result = executor.execute(query).await?;
986                let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
987                if last_id > 0 {
988                    self.id = last_id as i32;
989                }
990
991                Ok(())
992                }
993            }
994
995            #update_impl
996            #delete_impl
997
998            fn find_by_id<'a, E>(
999                executor: E,
1000                id: i32,
1001            ) -> impl ::std::future::Future<Output = ::std::result::Result<::std::option::Option<Self>, premix_orm::sqlx::Error>>
1002            + Send
1003            where
1004                E: premix_orm::IntoExecutor<'a, DB = DB>
1005            {
1006                async move {
1007                let mut executor = executor.into_executor();
1008                let p = <DB as premix_orm::SqlDialect>::placeholder(1);
1009
1010                // Optimization: Pre-calculate the base SQL string
1011                let sql = if Self::has_soft_delete() {
1012                    format!("SELECT * FROM {} WHERE id = {} AND deleted_at IS NULL LIMIT 1", #table_name, p)
1013                } else {
1014                    format!("SELECT * FROM {} WHERE id = {} LIMIT 1", #table_name, p)
1015                };
1016
1017                premix_orm::tracing::debug!(
1018                    operation = "select",
1019                    table = #table_name,
1020                    sql = %sql,
1021                    "premix query"
1022                );
1023                let query = premix_orm::sqlx::query_as::<DB, Self>(&sql)
1024                    .persistent(true)
1025                    .bind(id);
1026                executor.fetch_optional(query).await
1027                }
1028            }
1029
1030            fn eager_load<'a>(
1031                models: &mut [Self],
1032                relation: &str,
1033                executor: premix_orm::Executor<'a, DB>,
1034            ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>> + Send
1035            {
1036                async move {
1037                    let mut executor = executor;
1038                    #eager_load_body
1039                }
1040            }
1041        }
1042
1043        #hooks_impl
1044        #validation_impl
1045
1046        impl premix_orm::ModelSchema for #struct_name {
1047            fn schema() -> premix_orm::schema::SchemaTable {
1048                let columns = vec![
1049                    #(
1050                        premix_orm::schema::SchemaColumn {
1051                            name: #field_names.to_string(),
1052                            sql_type: #field_sql_types.to_string(),
1053                            nullable: #field_nullables,
1054                            primary_key: #field_primary_keys,
1055                        }
1056                    ),*
1057                ];
1058                let indexes = vec![
1059                    #(#index_tokens),*
1060                ];
1061                let foreign_keys = vec![
1062                    #(#foreign_key_tokens),*
1063                ];
1064                premix_orm::schema::SchemaTable {
1065                    name: #table_name.to_string(),
1066                    columns,
1067                    indexes,
1068                    foreign_keys,
1069                    create_sql: None,
1070                }
1071            }
1072        }
1073    })
1074}
1075
1076fn has_premix_field_flag(field: &Field, flag: &str) -> bool {
1077    for attr in &field.attrs {
1078        if attr.path().is_ident("premix")
1079            && let Ok(meta) = attr.parse_args::<syn::Ident>()
1080            && meta == flag
1081        {
1082            return true;
1083        }
1084    }
1085    false
1086}
1087
1088fn is_ignored(field: &Field) -> bool {
1089    has_premix_field_flag(field, "ignore")
1090}
1091
1092fn is_sensitive(field: &Field) -> bool {
1093    has_premix_field_flag(field, "sensitive")
1094}
1095
1096struct IndexSpec {
1097    name: String,
1098    columns: Vec<String>,
1099    unique: bool,
1100}
1101
1102struct ForeignKeySpec {
1103    column: String,
1104    ref_table: String,
1105    ref_column: String,
1106}
1107
1108fn collect_schema_specs(
1109    fields: &syn::punctuated::Punctuated<Field, Token![,]>,
1110    table_name: &str,
1111) -> syn::Result<(Vec<IndexSpec>, Vec<ForeignKeySpec>)> {
1112    let mut indexes = Vec::new();
1113    let mut foreign_keys = Vec::new();
1114
1115    for field in fields {
1116        if is_ignored(field) {
1117            continue;
1118        }
1119        let field_name = field
1120            .ident
1121            .as_ref()
1122            .ok_or_else(|| syn::Error::new_spanned(field, "Field must have an ident"))?
1123            .to_string();
1124
1125        for attr in &field.attrs {
1126            if !attr.path().is_ident("premix") {
1127                continue;
1128            }
1129
1130            attr.parse_nested_meta(|meta| {
1131                if meta.path.is_ident("index") || meta.path.is_ident("unique") {
1132                    let unique = meta.path.is_ident("unique");
1133                    let mut name = None;
1134                    if meta.input.peek(syn::token::Paren) {
1135                        meta.parse_nested_meta(|nested| {
1136                            if nested.path.is_ident("name") {
1137                                let lit: LitStr = nested.value()?.parse()?;
1138                                name = Some(lit.value());
1139                                Ok(())
1140                            } else {
1141                                Err(nested.error("unsupported index option"))
1142                            }
1143                        })?;
1144                    }
1145                    let index_name =
1146                        name.unwrap_or_else(|| format!("idx_{}_{}", table_name, field_name));
1147                    indexes.push(IndexSpec {
1148                        name: index_name,
1149                        columns: vec![field_name.clone()],
1150                        unique,
1151                    });
1152                } else if meta.path.is_ident("foreign_key") {
1153                    let mut ref_table = None;
1154                    let mut ref_column = None;
1155                    meta.parse_nested_meta(|nested| {
1156                        if nested.path.is_ident("table") {
1157                            let lit: LitStr = nested.value()?.parse()?;
1158                            ref_table = Some(lit.value());
1159                            Ok(())
1160                        } else if nested.path.is_ident("column") {
1161                            let lit: LitStr = nested.value()?.parse()?;
1162                            ref_column = Some(lit.value());
1163                            Ok(())
1164                        } else {
1165                            Err(nested.error("unsupported foreign_key option"))
1166                        }
1167                    })?;
1168
1169                    let ref_table = ref_table.ok_or_else(|| {
1170                        syn::Error::new_spanned(attr, "foreign_key requires table = \"...\"")
1171                    })?;
1172                    let ref_column = ref_column.unwrap_or_else(|| "id".to_string());
1173                    foreign_keys.push(ForeignKeySpec {
1174                        column: field_name.clone(),
1175                        ref_table,
1176                        ref_column,
1177                    });
1178                }
1179                Ok(())
1180            })?;
1181        }
1182    }
1183
1184    Ok((indexes, foreign_keys))
1185}
1186
1187fn is_option_type(ty: &syn::Type) -> bool {
1188    if let syn::Type::Path(path) = ty {
1189        if let Some(seg) = path.path.segments.last() {
1190            return seg.ident == "Option";
1191        }
1192    }
1193    false
1194}
1195
1196fn has_premix_flag(attrs: &[Attribute], flag: &str) -> bool {
1197    for attr in attrs {
1198        if attr.path().is_ident("premix") {
1199            let args = attr.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated);
1200            if let Ok(args) = args {
1201                if args.iter().any(|ident| ident == flag) {
1202                    return true;
1203                }
1204            }
1205        }
1206    }
1207    false
1208}
1209
1210fn type_name_for_field(ty: &syn::Type) -> Option<String> {
1211    if let syn::Type::Path(path) = ty {
1212        let segment = path.path.segments.last()?;
1213        let ident = segment.ident.to_string();
1214        if ident == "Option" {
1215            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
1216                for arg in args.args.iter() {
1217                    if let syn::GenericArgument::Type(inner) = arg {
1218                        return type_name_for_field(inner);
1219                    }
1220                }
1221            }
1222            None
1223        } else if ident == "Vec" {
1224            if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
1225                if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
1226                    if let Some(inner_ident) = type_name_for_field(inner) {
1227                        return Some(format!("Vec<{}>", inner_ident));
1228                    }
1229                }
1230            }
1231            Some("Vec".to_string())
1232        } else {
1233            Some(ident)
1234        }
1235    } else {
1236        None
1237    }
1238}
1239
1240fn sql_type_for_field(name: &str, ty: &syn::Type) -> &'static str {
1241    let type_name = type_name_for_field(ty);
1242    match type_name.as_deref() {
1243        Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => "INTEGER",
1244        Some("i64" | "u64") => "BIGINT",
1245        Some("f32" | "f64") => "REAL",
1246        Some("bool") => "BOOLEAN",
1247        Some("String" | "str") => "TEXT",
1248        Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => "TEXT",
1249        Some("Vec<u8>") => "BLOB",
1250        _ => {
1251            if name == "id" || name.ends_with("_id") {
1252                "INTEGER"
1253            } else {
1254                "TEXT"
1255            }
1256        }
1257    }
1258}
1259
1260fn sql_type_expr_for_field(name: &str, ty: &syn::Type) -> proc_macro2::TokenStream {
1261    let type_name = type_name_for_field(ty);
1262    match type_name.as_deref() {
1263        Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => {
1264            quote! { <DB as premix_orm::SqlDialect>::int_type() }
1265        }
1266        Some("i64" | "u64") => quote! { <DB as premix_orm::SqlDialect>::bigint_type() },
1267        Some("f32" | "f64") => quote! { <DB as premix_orm::SqlDialect>::float_type() },
1268        Some("bool") => quote! { <DB as premix_orm::SqlDialect>::bool_type() },
1269        Some("String" | "str") => quote! { <DB as premix_orm::SqlDialect>::text_type() },
1270        Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => {
1271            quote! { <DB as premix_orm::SqlDialect>::text_type() }
1272        }
1273        Some("Vec<u8>") => quote! { <DB as premix_orm::SqlDialect>::blob_type() },
1274        _ => {
1275            if name == "id" || name.ends_with("_id") {
1276                quote! { <DB as premix_orm::SqlDialect>::int_type() }
1277            } else {
1278                quote! { <DB as premix_orm::SqlDialect>::text_type() }
1279            }
1280        }
1281    }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286    use syn::parse_quote;
1287
1288    use super::*;
1289
1290    #[test]
1291    fn generate_generic_impl_includes_table_and_columns() {
1292        let input: DeriveInput = parse_quote! {
1293            struct User {
1294                id: i32,
1295                name: String,
1296                version: i32,
1297                deleted_at: Option<String>,
1298            }
1299        };
1300        let tokens = generate_generic_impl(&input).unwrap().to_string();
1301        assert!(tokens.contains("CREATE TABLE IF NOT EXISTS"));
1302        assert!(tokens.contains("users"));
1303        assert!(tokens.contains("deleted_at"));
1304        assert!(tokens.contains("version"));
1305    }
1306
1307    #[test]
1308    fn generate_generic_impl_rejects_tuple_struct() {
1309        let input: DeriveInput = parse_quote! {
1310            struct User(i32, String);
1311        };
1312        let err = generate_generic_impl(&input).unwrap_err();
1313        assert!(err.to_string().contains("named fields"));
1314    }
1315
1316    #[test]
1317    fn generate_generic_impl_rejects_non_struct() {
1318        let input: DeriveInput = parse_quote! {
1319            enum User {
1320                A,
1321                B,
1322            }
1323        };
1324        let err = generate_generic_impl(&input).unwrap_err();
1325        assert!(err.to_string().contains("only supports structs"));
1326    }
1327
1328    #[test]
1329    fn generate_generic_impl_version_update_branch() {
1330        let input: DeriveInput = parse_quote! {
1331            struct User {
1332                id: i32,
1333                version: i32,
1334                name: String,
1335            }
1336        };
1337        let tokens = generate_generic_impl(&input).unwrap().to_string();
1338        assert!(tokens.contains("version = version + 1"));
1339    }
1340
1341    #[test]
1342    fn generate_generic_impl_no_version_branch() {
1343        let input: DeriveInput = parse_quote! {
1344            struct User {
1345                id: i32,
1346                name: String,
1347            }
1348        };
1349        let tokens = generate_generic_impl(&input).unwrap().to_string();
1350        assert!(!tokens.contains("version = version + 1"));
1351    }
1352
1353    #[test]
1354    fn generate_generic_impl_includes_default_hooks_and_validation() {
1355        let input: DeriveInput = parse_quote! {
1356            struct User {
1357                id: i32,
1358                name: String,
1359            }
1360        };
1361        let tokens = generate_generic_impl(&input).unwrap().to_string();
1362        assert!(tokens.contains("ModelHooks"));
1363        assert!(tokens.contains("ModelValidation"));
1364    }
1365
1366    #[test]
1367    fn generate_generic_impl_includes_schema_impl() {
1368        let input: DeriveInput = parse_quote! {
1369            struct User {
1370                id: i32,
1371                name: String,
1372            }
1373        };
1374        let tokens = generate_generic_impl(&input).unwrap().to_string();
1375        assert!(tokens.contains("ModelSchema"));
1376        assert!(tokens.contains("SchemaColumn"));
1377    }
1378
1379    #[test]
1380    fn generate_generic_impl_includes_index_and_foreign_key_metadata() {
1381        let input: DeriveInput = parse_quote! {
1382            struct User {
1383                id: i32,
1384                #[premix(index)]
1385                name: String,
1386                #[premix(unique(name = "users_email_uidx"))]
1387                email: String,
1388                #[premix(foreign_key(table = "accounts", column = "id"))]
1389                account_id: i32,
1390            }
1391        };
1392        let tokens = generate_generic_impl(&input).unwrap().to_string();
1393        assert!(tokens.contains("SchemaIndex"));
1394        assert!(tokens.contains("idx_users_name"));
1395        assert!(tokens.contains("users_email_uidx"));
1396        assert!(tokens.contains("SchemaForeignKey"));
1397        assert!(tokens.contains("accounts"));
1398        assert!(tokens.contains("account_id"));
1399    }
1400
1401    #[test]
1402    fn generate_generic_impl_includes_sensitive_fields() {
1403        let input: DeriveInput = parse_quote! {
1404            struct User {
1405                id: i32,
1406                #[premix(sensitive)]
1407                email: String,
1408            }
1409        };
1410        let tokens = generate_generic_impl(&input).unwrap().to_string();
1411        assert!(tokens.contains("sensitive_fields"));
1412        assert!(tokens.contains("\"email\""));
1413    }
1414
1415    #[test]
1416    fn generate_generic_impl_skips_custom_hooks_and_validation() {
1417        let input: DeriveInput = parse_quote! {
1418            #[premix(custom_hooks, custom_validation)]
1419            struct User {
1420                id: i32,
1421                name: String,
1422            }
1423        };
1424        let tokens = generate_generic_impl(&input).unwrap().to_string();
1425        assert!(!tokens.contains("impl premix_orm :: ModelHooks"));
1426        assert!(!tokens.contains("impl premix_orm :: ModelValidation"));
1427    }
1428
1429    #[test]
1430    fn is_ignored_detects_attribute() {
1431        let field: Field = parse_quote! {
1432            #[premix(ignore)]
1433            ignored: Option<String>
1434        };
1435        assert!(is_ignored(&field));
1436    }
1437
1438    #[test]
1439    fn is_ignored_false_for_other_attrs() {
1440        let field: Field = parse_quote! {
1441            #[serde(skip)]
1442            name: String
1443        };
1444        assert!(!is_ignored(&field));
1445    }
1446
1447    #[test]
1448    fn is_ignored_false_for_premix_other_arg() {
1449        let field: Field = parse_quote! {
1450            #[premix(skip)]
1451            name: String
1452        };
1453        assert!(!is_ignored(&field));
1454    }
1455
1456    #[test]
1457    fn is_sensitive_detects_attribute() {
1458        let field: Field = parse_quote! {
1459            #[premix(sensitive)]
1460            secret: String
1461        };
1462        assert!(is_sensitive(&field));
1463    }
1464
1465    #[test]
1466    fn is_sensitive_false_for_other_attrs() {
1467        let field: Field = parse_quote! {
1468            #[serde(skip)]
1469            secret: String
1470        };
1471        assert!(!is_sensitive(&field));
1472    }
1473
1474    #[test]
1475    fn is_ignored_false_when_premix_has_no_args() {
1476        let field: Field = parse_quote! {
1477            #[premix]
1478            name: String
1479        };
1480        assert!(!is_ignored(&field));
1481    }
1482
1483    #[test]
1484    fn derive_model_impl_emits_tokens() {
1485        let input: DeriveInput = parse_quote! {
1486            struct User {
1487                id: i32,
1488                name: String,
1489            }
1490        };
1491        let tokens = derive_model_impl(&input).unwrap().to_string();
1492        assert!(tokens.contains("impl"));
1493    }
1494
1495    #[test]
1496    fn derive_model_impl_propagates_error() {
1497        let input: DeriveInput = parse_quote! {
1498            enum User {
1499                A,
1500            }
1501        };
1502        let err = derive_model_impl(&input).unwrap_err();
1503        assert!(err.to_string().contains("only supports structs"));
1504    }
1505
1506    #[test]
1507    fn generate_generic_impl_includes_soft_delete_delete_impl() {
1508        let input: DeriveInput = parse_quote! {
1509            struct AuditLog {
1510                id: i32,
1511                deleted_at: Option<String>,
1512            }
1513        };
1514        let tokens = generate_generic_impl(&input).unwrap().to_string();
1515        assert!(tokens.contains("deleted_at ="));
1516        assert!(tokens.contains("has_soft_delete"));
1517    }
1518
1519    #[test]
1520    fn generate_generic_impl_ignores_marked_fields() {
1521        let input: DeriveInput = parse_quote! {
1522            struct User {
1523                id: i32,
1524                name: String,
1525                #[premix(ignore)]
1526                temp: Option<String>,
1527            }
1528        };
1529        let tokens = generate_generic_impl(&input).unwrap().to_string();
1530        assert!(tokens.contains("temp : None"));
1531        assert!(!tokens.contains("\"temp\""));
1532    }
1533
1534    #[test]
1535    fn generate_generic_impl_adds_relation_bounds() {
1536        let input: DeriveInput = parse_quote! {
1537            struct User {
1538                id: i32,
1539                #[has_many(Post)]
1540                posts: Vec<Post>,
1541            }
1542        };
1543        let tokens = generate_generic_impl(&input).unwrap().to_string();
1544        assert!(tokens.contains("Post : premix_orm :: Model < DB >"));
1545    }
1546
1547    #[test]
1548    fn generate_generic_impl_records_field_names() {
1549        let input: DeriveInput = parse_quote! {
1550            struct Account {
1551                id: i32,
1552                user_id: i32,
1553                is_active: bool,
1554            }
1555        };
1556        let tokens = generate_generic_impl(&input).unwrap().to_string();
1557        assert!(tokens.contains("\"user_id\""));
1558        assert!(tokens.contains("\"is_active\""));
1559    }
1560
1561    #[test]
1562    fn generate_generic_impl_creates_column_constants() {
1563        let input: DeriveInput = parse_quote! {
1564            struct User {
1565                id: i32,
1566                name: String,
1567            }
1568        };
1569        let tokens = generate_generic_impl(&input).unwrap().to_string();
1570        assert!(tokens.contains("pub mod columns_user"));
1571        assert!(tokens.contains("pub const ID : & str = \"id\""));
1572        assert!(tokens.contains("pub const NAME : & str = \"name\""));
1573    }
1574}