Skip to main content

torm_derive/
lib.rs

1//! Derive macros for the TORM library.
2//!
3//! The main macro is [`Model`](macro@Model), which generates the verbose
4//! boilerplate for implementing `torm::orm::model::Model` from a plain
5//! struct definition.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use torm::{Model, Timestamps};
11//!
12//! #[derive(Debug, Clone, Model)]
13//! #[model(table_name = "users", primary_key = "id")]
14//! pub struct User {
15//!     pub id: i64,
16//!     pub name: String,
17//!     pub created_at: Option<chrono::DateTime<chrono::Utc>>,
18//!     pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
19//!     pub timestamps: Timestamps,
20//! }
21//! ```
22//!
23//! # Field tags
24//!
25//! In addition to the container-level `#[model(table_name = "...", primary_key =
26//! "...")]` attributes, each field may carry GORM-style tags that are recorded
27//! in the generated [`Model::schema`](trait@crate::Model) method so that
28//! [`Database::auto_migrate`](https://docs.rs/torm/latest/torm/db/database/struct.Database.html)
29//! can create the table and its indexes automatically:
30//!
31//! - `#[model(primaryKey)]` — marks the field as the primary key.
32//! - `#[model(index)]` or `#[model(index = "idx_name")]` — a plain index.
33//!   Without a name it defaults to `idx_<table>_<column>`; fields sharing the
34//!   same explicit name form a composite index.
35//! - `#[model(uniqueIndex)]` or `#[model(uniqueIndex = "idx_name")]` — a unique
36//!   index (also implying a `UNIQUE` column constraint on a single column).
37
38extern crate proc_macro;
39
40use proc_macro::TokenStream;
41use proc_macro2::Span;
42use quote::quote;
43use syn::{
44    parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, Lit, Meta, Type,
45    punctuated::Punctuated, spanned::Spanned, token::Comma,
46};
47
48/// The role a struct field plays in the generated `Model` impl.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum FieldKind {
51    /// The primary-key field (`id` by default).
52    PrimaryKey,
53    /// A `Timestamps` struct field backing the timestamp accessors.
54    Timestamps,
55    /// A standalone `created_at`/`updated_at`/`deleted_at` timestamp field.
56    TimestampField,
57    /// A normal, persistable field (goes into `columns()` / `from_row()`).
58    Persist,
59    /// Excluded from persistence (`#[model(skip)]` or unsupported type).
60    Skip,
61}
62
63/// A single index declaration attached to a field, GORM-style.
64///
65/// Parsed from `#[model(index = "...")]`, `#[model(uniqueIndex = "...")]` or
66/// the bare `#[model(index)]` / `#[model(uniqueIndex)]` path forms.
67#[derive(Debug, Clone)]
68struct FieldIndex {
69    /// Index name (empty means derive a default `idx_<table>_<column>`).
70    name: String,
71    /// Unique index flag.
72    unique: bool,
73    /// Composite partners (other columns sharing the same index name).
74    partners: Vec<String>,
75}
76
77/// Decorated struct field.
78struct FieldInfo {
79    ident: syn::Ident,
80    column: Option<String>,
81    kind: FieldKind,
82    ty: Type,
83    /// Whether the type is `Option<...>`.
84    optional: bool,
85    /// GORM-style field index declarations.
86    indexes: Vec<FieldIndex>,
87}
88
89impl FieldInfo {
90    fn column_name(&self) -> String {
91        self.column
92            .clone()
93            .unwrap_or_else(|| self.ident.to_string())
94    }
95}
96
97/// Container-level `#[model(...)]` configuration.
98struct ModelConfig {
99    table_name: String,
100    primary_key: String,
101}
102
103impl ModelConfig {
104    fn parse(attrs: &[Attribute]) -> syn::Result<Self> {
105        let mut table_name: Option<String> = None;
106        let mut primary_key: Option<String> = None;
107
108        for attr in attrs {
109            if !attr.path().is_ident("model") {
110                continue;
111            }
112            let Meta::List(list) = &attr.meta else { continue };
113            let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
114            for item in nested {
115                match item {
116                    Meta::NameValue(nv) if nv.path.is_ident("table_name") => {
117                        table_name = Some(expr_to_string(&nv.value)?);
118                    }
119                    Meta::NameValue(nv) if nv.path.is_ident("primary_key") => {
120                        primary_key = Some(expr_to_string(&nv.value)?);
121                    }
122                    _ => {}
123                }
124            }
125        }
126
127        let table_name = table_name.ok_or_else(|| {
128            syn::Error::new(
129                Span::call_site(),
130                "#[model(table_name = \"...\")] is required on #[derive(Model)] structs",
131            )
132        })?;
133
134        Ok(Self {
135            table_name,
136            primary_key: primary_key.unwrap_or_else(|| "id".to_string()),
137        })
138    }
139}
140
141fn expr_to_string(expr: &Expr) -> syn::Result<String> {
142    match expr {
143        Expr::Lit(lit) => match &lit.lit {
144            Lit::Str(s) => Ok(s.value()),
145            _ => Err(syn::Error::new_spanned(lit, "expected a string literal")),
146        },
147        _ => Err(syn::Error::new_spanned(expr, "expected a string literal")),
148    }
149}
150
151/// Collect and decorate all fields of the input struct.
152fn collect_fields(input: &DeriveInput, pk_name: &str) -> syn::Result<Vec<FieldInfo>> {
153    let data = match &input.data {
154        Data::Struct(s) => s,
155        Data::Enum(_) | Data::Union(_) => {
156            return Err(syn::Error::new(
157                input.ident.span(),
158                "#[derive(Model)] is only supported on structs",
159            ))
160        }
161    };
162
163    let named = match &data.fields {
164        Fields::Named(named) => &named.named,
165        Fields::Unnamed(_) | Fields::Unit => {
166            return Err(syn::Error::new(
167                input.ident.span(),
168                "#[derive(Model)] requires named struct fields",
169            ))
170        }
171    };
172
173    let mut out = Vec::new();
174    for field in named {
175        let ident = field
176            .ident
177            .clone()
178            .ok_or_else(|| syn::Error::new(field.span(), "named field expected"))?;
179        let ty = field.ty.clone();
180        let optional = field_type_is_option(&ty);
181
182        let mut skip = false;
183        let mut column: Option<String> = None;
184        let mut indexes: Vec<FieldIndex> = Vec::new();
185        for attr in &field.attrs {
186            if !attr.path().is_ident("model") {
187                continue;
188            }
189            let Meta::List(list) = &attr.meta else { continue };
190            let nested = list.parse_args_with(Punctuated::<Meta, Comma>::parse_terminated)?;
191            for item in nested {
192                match item {
193                    Meta::Path(path) if path.is_ident("skip") => skip = true,
194                    Meta::Path(path) if path.is_ident("index") => {
195                        indexes.push(FieldIndex {
196                            name: String::new(),
197                            unique: false,
198                            partners: Vec::new(),
199                        });
200                    }
201                    Meta::Path(path) if path.is_ident("uniqueIndex") => {
202                        indexes.push(FieldIndex {
203                            name: String::new(),
204                            unique: true,
205                            partners: Vec::new(),
206                        });
207                    }
208                    Meta::NameValue(nv) if nv.path.is_ident("column") => {
209                        column = Some(expr_to_string(&nv.value)?);
210                    }
211                    Meta::NameValue(nv) if nv.path.is_ident("index") => {
212                        indexes.push(FieldIndex {
213                            name: expr_to_string(&nv.value)?,
214                            unique: false,
215                            partners: Vec::new(),
216                        });
217                    }
218                    Meta::NameValue(nv) if nv.path.is_ident("uniqueIndex") => {
219                        indexes.push(FieldIndex {
220                            name: expr_to_string(&nv.value)?,
221                            unique: true,
222                            partners: Vec::new(),
223                        });
224                    }
225                    _ => {}
226                }
227            }
228        }
229
230        let kind = if skip {
231            FieldKind::Skip
232        } else if field_type_is_timestamps(&ty) {
233            FieldKind::Timestamps
234        } else if ident.to_string() == pk_name {
235            FieldKind::PrimaryKey
236        } else if is_standalone_timestamp_field(&ident, &ty) {
237            FieldKind::TimestampField
238        } else if type_tag(&ty).is_some() {
239            FieldKind::Persist
240        } else {
241            // Unsupported / association types are silently excluded.
242            FieldKind::Skip
243        };
244
245        out.push(FieldInfo {
246            ident,
247            column,
248            kind,
249            ty,
250            optional,
251            indexes,
252        });
253    }
254
255    Ok(out)
256}
257
258fn field_type_is_timestamps(ty: &Type) -> bool {
259    let Type::Path(tp) = ty else { return false };
260    matches!(tp.path.segments.last(), Some(s) if s.ident == "Timestamps")
261}
262
263fn field_type_is_option(ty: &Type) -> bool {
264    let Type::Path(tp) = ty else { return false };
265    matches!(tp.path.segments.last(), Some(s) if s.ident == "Option")
266}
267
268/// True if this is a standalone timestamp field like
269/// `created_at: Option<DateTime<Utc>>`.
270fn is_standalone_timestamp_field(ident: &syn::Ident, ty: &Type) -> bool {
271    let name = ident.to_string();
272    let valid_name = matches!(name.as_str(), "created_at" | "updated_at" | "deleted_at");
273    valid_name && type_tag(ty) == Some("datetime")
274}
275
276/// The innermost type when a type is `Option<...>` (or itself).
277fn inner_type(ty: &Type) -> &Type {
278    if let Type::Path(tp) = ty {
279        if let Some(s) = tp.path.segments.last() {
280            if s.ident == "Option" {
281                if let syn::PathArguments::AngleBracketed(args) = &s.arguments {
282                    if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
283                        return inner;
284                    }
285                }
286            }
287        }
288    }
289    ty
290}
291
292/// A simple type tag for the persistable set.
293fn type_tag(ty: &Type) -> Option<&'static str> {
294    let ty = inner_type(ty);
295    let Type::Path(tp) = ty else { return None };
296    let last = tp.path.segments.last()?;
297    let name = last.ident.to_string();
298    let tag = match name.as_str() {
299        "String" => "string",
300        "bool" => "bool",
301        "i8" => "i8",
302        "i16" => "i16",
303        "i32" => "i32",
304        "i64" => "i64",
305        "f32" => "f32",
306        "f64" => "f64",
307        "DateTime" => "datetime",
308        "Uuid" => "uuid",
309        "Vec" => {
310            // Only `Vec<u8>` maps to bytes; other Vec<T> are treated as
311            // unsupported (association fields) and auto-skipped.
312            let is_u8 = matches!(
313                &last.arguments,
314                syn::PathArguments::AngleBracketed(args)
315                    if args.args.len() == 1
316                        && matches!(
317                            args.args.first(),
318                            Some(syn::GenericArgument::Type(Type::Path(tp)))
319                                if tp.path.is_ident("u8")
320                        )
321            );
322            if is_u8 {
323                "bytes"
324            } else {
325                return None;
326            }
327        }
328        _ => return None,
329    };
330    Some(tag)
331}
332
333/// Generate the `columns()` vec entries for the persist fields.
334fn gen_columns_entries(fields: &[&FieldInfo]) -> Vec<proc_macro2::TokenStream> {
335    fields
336        .iter()
337        .filter_map(|f| {
338            let col = f.column_name();
339            let fname = &f.ident;
340            let tag = type_tag(&f.ty)?;
341            let expr = match tag {
342                "string" => {
343                    if f.optional {
344                        quote! { match &self.#fname { Some(v) => SqlValue::String(v.clone()), None => SqlValue::Null } }
345                    } else {
346                        quote! { SqlValue::String(self.#fname.clone()) }
347                    }
348                }
349                "bool" => {
350                    if f.optional {
351                        quote! { match self.#fname { Some(v) => SqlValue::Bool(v), None => SqlValue::Null } }
352                    } else {
353                        quote! { SqlValue::Bool(self.#fname) }
354                    }
355                }
356                "i8" => {
357                    if f.optional {
358                        quote! { match self.#fname { Some(v) => SqlValue::I8(v), None => SqlValue::Null } }
359                    } else {
360                        quote! { SqlValue::I8(self.#fname) }
361                    }
362                }
363                "i16" => {
364                    if f.optional {
365                        quote! { match self.#fname { Some(v) => SqlValue::I16(v), None => SqlValue::Null } }
366                    } else {
367                        quote! { SqlValue::I16(self.#fname) }
368                    }
369                }
370                "i32" => {
371                    if f.optional {
372                        quote! { match self.#fname { Some(v) => SqlValue::I32(v), None => SqlValue::Null } }
373                    } else {
374                        quote! { SqlValue::I32(self.#fname) }
375                    }
376                }
377                "i64" => {
378                    if f.optional {
379                        quote! { match self.#fname { Some(v) => SqlValue::I64(v), None => SqlValue::Null } }
380                    } else {
381                        quote! { SqlValue::I64(self.#fname) }
382                    }
383                }
384                "f32" => {
385                    if f.optional {
386                        quote! { match self.#fname { Some(v) => SqlValue::F32(v), None => SqlValue::Null } }
387                    } else {
388                        quote! { SqlValue::F32(self.#fname) }
389                    }
390                }
391                "f64" => {
392                    if f.optional {
393                        quote! { match self.#fname { Some(v) => SqlValue::F64(v), None => SqlValue::Null } }
394                    } else {
395                        quote! { SqlValue::F64(self.#fname) }
396                    }
397                }
398                "datetime" => {
399                    if f.optional {
400                        quote! { match self.#fname { Some(v) => SqlValue::DateTime(v), None => SqlValue::Null } }
401                    } else {
402                        quote! { SqlValue::DateTime(self.#fname) }
403                    }
404                }
405                "uuid" => {
406                    if f.optional {
407                        quote! { match &self.#fname { Some(v) => SqlValue::String(v.to_string()), None => SqlValue::Null } }
408                    } else {
409                        quote! { SqlValue::String(self.#fname.to_string()) }
410                    }
411                }
412                "bytes" => {
413                    if f.optional {
414                        quote! { match &self.#fname { Some(v) => SqlValue::Bytes(v.clone()), None => SqlValue::Null } }
415                    } else {
416                        quote! { SqlValue::Bytes(self.#fname.clone()) }
417                    }
418                }
419                _ => return None,
420            };
421            Some(quote! { (#col, #expr) })
422        })
423        .collect()
424}
425
426/// Generate a `from_row` field initializer expression yielding the field value.
427fn gen_from_row_expr(f: &FieldInfo) -> Option<proc_macro2::TokenStream> {
428    let col = f.column_name();
429    let tag = type_tag(&f.ty)?;
430    let inner = match tag {
431        "string" => Some(quote! {
432            match row.get(#col)? {
433                SqlValue::String(s) => s.clone(),
434                SqlValue::Json(s) => s.clone(),
435                SqlValue::I64(i) => i.to_string(),
436                SqlValue::I32(i) => i.to_string(),
437                SqlValue::I16(i) => i.to_string(),
438                SqlValue::I8(i) => i.to_string(),
439                SqlValue::Bool(b) => b.to_string(),
440                _ => return None,
441            }
442        }),
443        "bool" => Some(quote! {
444            match row.get(#col)? {
445                SqlValue::Bool(v) => *v,
446                SqlValue::I32(1) => true,
447                SqlValue::I64(1) => true,
448                SqlValue::I32(0) => false,
449                SqlValue::I64(0) => false,
450                _ => return None,
451            }
452        }),
453        "i8" => Some(quote! {
454            match row.get(#col)? {
455                SqlValue::I8(v) => *v,
456                SqlValue::I16(v) => *v as i8,
457                SqlValue::I32(v) => *v as i8,
458                SqlValue::I64(v) => *v as i8,
459                _ => return None,
460            }
461        }),
462        "i16" => Some(quote! {
463            match row.get(#col)? {
464                SqlValue::I16(v) => *v,
465                SqlValue::I8(v) => *v as i16,
466                SqlValue::I32(v) => *v as i16,
467                SqlValue::I64(v) => *v as i16,
468                _ => return None,
469            }
470        }),
471        "i32" => Some(quote! {
472            match row.get(#col)? {
473                SqlValue::I32(v) => *v,
474                SqlValue::I8(v) => *v as i32,
475                SqlValue::I16(v) => *v as i32,
476                SqlValue::I64(v) => *v as i32,
477                _ => return None,
478            }
479        }),
480        "i64" => Some(quote! {
481            match row.get(#col)? {
482                SqlValue::I64(v) => *v,
483                SqlValue::I32(v) => *v as i64,
484                SqlValue::I16(v) => *v as i64,
485                SqlValue::I8(v) => *v as i64,
486                _ => return None,
487            }
488        }),
489        "f32" => Some(quote! {
490            match row.get(#col)? {
491                SqlValue::F32(v) => *v,
492                SqlValue::F64(v) => *v as f32,
493                _ => return None,
494            }
495        }),
496        "f64" => Some(quote! {
497            match row.get(#col)? {
498                SqlValue::F64(v) => *v,
499                SqlValue::F32(v) => *v as f64,
500                _ => return None,
501            }
502        }),
503        "datetime" => Some(quote! {
504            match row.get(#col)? {
505                SqlValue::DateTime(v) => *v,
506                // SQLite stores datetimes as TEXT; parse the common format.
507                SqlValue::String(s) => {
508                    chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
509                        .map(|dt| dt.and_utc())
510                        .ok()?
511                }
512                _ => return None,
513            }
514        }),
515        "uuid" => Some(quote! {
516            Uuid::parse_str(row.get(#col)?.as_str()?).ok()?
517        }),
518        "bytes" => Some(quote! {
519            match row.get(#col)? {
520                SqlValue::Bytes(v) => v.clone(),
521                _ => return None,
522            }
523        }),
524        _ => None,
525    }?;
526
527    if f.optional {
528        Some(quote! {
529            match row.get(#col) {
530                Some(SqlValue::Null) | None => None,
531                _ => Some(#inner),
532            }
533        })
534    } else {
535        Some(inner)
536    }
537}
538
539/// Map a `type_tag` to the migration `ColumnType` variant name.
540fn tag_to_column_type(tag: &str) -> Option<&'static str> {
541    match tag {
542        "i8" | "i16" | "i32" => Some("Integer"),
543        "i64" => Some("BigInteger"),
544        "string" => Some("String"),
545        "bool" => Some("Boolean"),
546        "f32" => Some("Float"),
547        "f64" => Some("Double"),
548        "datetime" => Some("DateTime"),
549        "uuid" => Some("Uuid"),
550        "bytes" => Some("Binary"),
551        _ => None,
552    }
553}
554
555/// Build the `schema()` implementation for a model, returning a
556/// `TableDefinition` describing its columns and GORM-style indexes.
557fn gen_schema_impl(
558    table_name: &str,
559    fields: &[&FieldInfo],
560) -> proc_macro2::TokenStream {
561    // Column definitions for all persistable / pk / timestamp fields.
562    let mut column_toks: Vec<proc_macro2::TokenStream> = Vec::new();
563    for f in fields {
564        if f.kind == FieldKind::Skip {
565            continue;
566        }
567        let col = f.column_name();
568        let optional = f.optional;
569
570        // Resolve column type from the (innermost) persistable tag.
571        let (col_type, is_pk) = match f.kind {
572            FieldKind::PrimaryKey => (
573                tag_to_column_type(type_tag(&f.ty).unwrap_or("string")).or(Some("Integer")),
574                true,
575            ),
576            FieldKind::Persist => (type_tag(&f.ty).and_then(tag_to_column_type), false),
577            FieldKind::TimestampField => (Some("DateTime"), false),
578            FieldKind::Timestamps => {
579                // Backed by a `Timestamps` struct: emit the three timestamp columns.
580                for ts_col in ["created_at", "updated_at", "deleted_at"] {
581                    column_toks.push(quote! {
582                        ColumnDefinition::new(#ts_col, ColumnType::DateTime).nullable(true)
583                    });
584                }
585                continue;
586            }
587            FieldKind::Skip => continue,
588        };
589        let Some(col_type) = col_type else {
590            continue;
591        };
592        let col_type_tok = syn::Ident::new(col_type, Span::call_site());
593
594        // A bare uniqueIndex on a single column implies a unique column too.
595        let is_unique = f.indexes.iter().any(|ix| ix.unique && ix.partners.is_empty());
596
597        let mut col_def = quote! {
598            ColumnDefinition::new(#col, ColumnType::#col_type_tok).nullable(#optional)
599        };
600        if is_pk {
601            col_def = quote! { #col_def .primary_key() };
602        }
603        if is_unique {
604            col_def = quote! { #col_def .unique() };
605        }
606        column_toks.push(col_def);
607    }
608
609    // Index definitions derived from index/uniqueIndex tags, grouped by index
610    // name so composite indexes share a single `IndexDefinition`.
611    let mut index_defs: Vec<proc_macro2::TokenStream> = Vec::new();
612    let mut groups: std::collections::HashMap<String, (bool, Vec<String>)> =
613        std::collections::HashMap::new();
614    for f in fields {
615        if f.kind == FieldKind::Skip {
616            continue;
617        }
618        let col = f.column_name();
619        for ix in &f.indexes {
620            let key = if ix.name.is_empty() {
621                col.clone()
622            } else {
623                ix.name.clone()
624            };
625            groups
626                .entry(key.clone())
627                .or_insert((ix.unique, Vec::new()))
628                .1
629                .push(col.clone());
630            if ix.unique {
631                groups.get_mut(&key).unwrap().0 = true;
632            }
633        }
634    }
635    let mut sorted_keys: Vec<&String> = groups.keys().collect();
636    sorted_keys.sort();
637    for key in sorted_keys {
638        let (unique, cols) = &groups[key];
639        let name = if key.starts_with("idx_") {
640            key.clone()
641        } else {
642            format!("idx_{}_{}", table_name, key)
643        };
644        let cols_lit: Vec<syn::LitStr> = cols
645            .iter()
646            .map(|c| syn::LitStr::new(c, Span::call_site()))
647            .collect();
648        if *unique {
649            index_defs.push(quote! {
650                IndexDefinition::new(#name, &[#(#cols_lit),*]).unique()
651            });
652        } else {
653            index_defs.push(quote! {
654                IndexDefinition::new(#name, &[#(#cols_lit),*])
655            });
656        }
657    }
658
659    quote! {
660        fn schema() -> Option<TableDefinition> {
661            Some(TableDefinition::new(#table_name)
662                #( .add_column(#column_toks) )*
663                #( .add_index(#index_defs) )*
664            )
665        }
666    }
667}
668
669/// Build the complete `impl Model for ...` block.
670fn build_model_impl(
671    input: &DeriveInput,
672    config: &ModelConfig,
673    fields: &[FieldInfo],
674) -> proc_macro2::TokenStream {
675    let ident = &input.ident;
676    let table_name = &config.table_name;
677
678    let pk = fields.iter().find(|f| f.kind == FieldKind::PrimaryKey);
679    let ts = fields.iter().find(|f| f.kind == FieldKind::Timestamps);
680    let ts_field_created = fields
681        .iter()
682        .find(|f| f.kind == FieldKind::TimestampField && f.ident == "created_at");
683    let ts_field_updated = fields
684        .iter()
685        .find(|f| f.kind == FieldKind::TimestampField && f.ident == "updated_at");
686    let ts_field_deleted = fields
687        .iter()
688        .find(|f| f.kind == FieldKind::TimestampField && f.ident == "deleted_at");
689    let persist: Vec<&FieldInfo> = fields
690        .iter()
691        .filter(|f| f.kind == FieldKind::Persist)
692        .collect();
693
694    // --- id / set_id ---
695    let (id_fn, set_id_fn) = match pk {
696        Some(pk) => {
697            let pk_ident = &pk.ident;
698            let pk_ty = inner_type(&pk.ty);
699            let pk_tag = type_tag(pk_ty).unwrap_or("");
700            let (id_expr, set_expr) = match pk_tag {
701                "string" => (
702                    quote! {
703                        if self.#pk_ident.is_empty() { None } else { Some(self.#pk_ident.clone()) }
704                    },
705                    quote! { self.#pk_ident = id; },
706                ),
707                "uuid" => (
708                    quote! {
709                        if self.#pk_ident.is_nil() { None } else { Some(self.#pk_ident.to_string()) }
710                    },
711                    quote! { self.#pk_ident = Uuid::parse_str(&id).unwrap_or_else(|_| Uuid::nil()); },
712                ),
713                "i64" => (
714                    quote! {
715                        if self.#pk_ident > 0 { Some(self.#pk_ident.to_string()) } else { None }
716                    },
717                    quote! { self.#pk_ident = id.parse().unwrap_or(0); },
718                ),
719                "i32" => (
720                    quote! {
721                        if self.#pk_ident > 0 { Some(self.#pk_ident.to_string()) } else { None }
722                    },
723                    quote! { self.#pk_ident = id.parse().unwrap_or(0); },
724                ),
725                "i16" => (
726                    quote! {
727                        if self.#pk_ident > 0 { Some(self.#pk_ident.to_string()) } else { None }
728                    },
729                    quote! { self.#pk_ident = id.parse().unwrap_or(0); },
730                ),
731                "i8" => (
732                    quote! {
733                        if self.#pk_ident > 0 { Some(self.#pk_ident.to_string()) } else { None }
734                    },
735                    quote! { self.#pk_ident = id.parse().unwrap_or(0); },
736                ),
737                // Fallback: ToString + From<String>.
738                _ => (
739                    quote! {
740                        let s = self.#pk_ident.to_string();
741                        if s.is_empty() { None } else { Some(s) }
742                    },
743                    quote! { self.#pk_ident = id.into(); },
744                ),
745            };
746            (
747                quote! { fn id(&self) -> Option<String> { #id_expr } },
748                quote! { fn set_id(&mut self, id: String) { #set_expr } },
749            )
750        }
751        None => (
752            quote! {
753                fn id(&self) -> Option<String> { None }
754            },
755            quote! {
756                fn set_id(&mut self, _id: String) {}
757            },
758        ),
759    };
760
761    // --- timestamps accessors ---
762    // Prefer a `Timestamps` struct field; otherwise fall back to standalone
763    // created_at/updated_at/deleted_at fields.
764    let ts_impl = if let Some(ts) = ts {
765        let t = &ts.ident;
766        quote! {
767            fn created_at(&self) -> Option<DateTime<Utc>> { self.#t.created_at }
768            fn updated_at(&self) -> Option<DateTime<Utc>> { self.#t.updated_at }
769            fn deleted_at(&self) -> Option<DateTime<Utc>> { self.#t.deleted_at }
770            fn set_created_at(&mut self, timestamp: DateTime<Utc>) { self.#t.created_at = Some(timestamp); }
771            fn set_updated_at(&mut self, timestamp: DateTime<Utc>) { self.#t.updated_at = Some(timestamp); }
772            fn set_deleted_at(&mut self, timestamp: Option<DateTime<Utc>>) { self.#t.deleted_at = timestamp; }
773        }
774    } else {
775        let created = ts_field_created.map(|f| &f.ident);
776        let updated = ts_field_updated.map(|f| &f.ident);
777        let deleted = ts_field_deleted.map(|f| &f.ident);
778        let (created_get, created_set) = match created {
779            Some(ci) => (
780                quote! { self.#ci },
781                quote! { self.#ci = Some(timestamp); },
782            ),
783            None => (quote! { None }, quote! {}),
784        };
785        let (updated_get, updated_set) = match updated {
786            Some(ui) => (
787                quote! { self.#ui },
788                quote! { self.#ui = Some(timestamp); },
789            ),
790            None => (quote! { None }, quote! {}),
791        };
792        let (deleted_get, deleted_set) = match deleted {
793            Some(di) => (
794                quote! { self.#di },
795                quote! { self.#di = timestamp; },
796            ),
797            None => (quote! { None }, quote! {}),
798        };
799        quote! {
800            fn created_at(&self) -> Option<DateTime<Utc>> { #created_get }
801            fn updated_at(&self) -> Option<DateTime<Utc>> { #updated_get }
802            fn deleted_at(&self) -> Option<DateTime<Utc>> { #deleted_get }
803            fn set_created_at(&mut self, timestamp: DateTime<Utc>) { #created_set }
804            fn set_updated_at(&mut self, timestamp: DateTime<Utc>) { #updated_set }
805            fn set_deleted_at(&mut self, timestamp: Option<DateTime<Utc>>) { #deleted_set }
806        }
807    };
808
809    // --- columns() ---
810    let column_entries = gen_columns_entries(&persist);
811
812    // --- from_row() ---
813    let mut literals: Vec<proc_macro2::TokenStream> = Vec::new();
814    for f in fields {
815        let fident = &f.ident;
816        match f.kind {
817            FieldKind::Skip => {
818                literals.push(quote! { #fident: Default::default() });
819            }
820            FieldKind::Timestamps => {
821                literals.push(quote! {
822                    #fident: {
823                        let mut ts = Timestamps::new();
824                        ts.created_at = row.get("created_at").and_then(|v| match v {
825                            SqlValue::DateTime(dt) => Some(*dt),
826                            _ => None,
827                        });
828                        ts.updated_at = row.get("updated_at").and_then(|v| match v {
829                            SqlValue::DateTime(dt) => Some(*dt),
830                            _ => None,
831                        });
832                        ts.deleted_at = row.get("deleted_at").and_then(|v| match v {
833                            SqlValue::DateTime(dt) => Some(*dt),
834                            _ => None,
835                        });
836                        ts
837                    }
838                });
839            }
840            FieldKind::PrimaryKey | FieldKind::Persist | FieldKind::TimestampField => {
841                if let Some(expr) = gen_from_row_expr(f) {
842                    literals.push(quote! { #fident: #expr });
843                } else {
844                    literals.push(quote! { #fident: Default::default() });
845                }
846            }
847        }
848    }
849
850    // --- schema() ---
851    let field_refs: Vec<&FieldInfo> = fields.iter().collect();
852    let schema_impl = gen_schema_impl(&config.table_name, &field_refs);
853
854    quote! {
855        #[automatically_derived]
856        impl Model for #ident {
857            fn table_name() -> &'static str {
858                #table_name
859            }
860
861            #id_fn
862            #set_id_fn
863            #ts_impl
864
865            #schema_impl
866
867            fn columns(&self) -> Vec<(&'static str, SqlValue)> {
868                vec![
869                    #(#column_entries,)*
870                ]
871            }
872
873            fn from_row(row: &Row) -> Option<Self> {
874                Some(Self {
875                    #(#literals,)*
876                })
877            }
878        }
879    }
880}
881
882/// Entry point for `#[derive(Model)]`.
883#[proc_macro_derive(Model, attributes(model))]
884pub fn derive_model(input: TokenStream) -> TokenStream {
885    let input = parse_macro_input!(input as DeriveInput);
886    match expand(&input) {
887        Ok(ts) => ts.into(),
888        Err(e) => e.to_compile_error().into(),
889    }
890}
891
892fn expand(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
893    let config = ModelConfig::parse(&input.attrs)?;
894    let fields = collect_fields(input, &config.primary_key)?;
895
896    let model_impl = build_model_impl(input, &config, &fields);
897
898    Ok(quote! {
899        const _: () = {
900            use torm::orm::model::Timestamps;
901            use torm::db::db_types::{Row, SqlValue};
902            use torm::chrono::{DateTime, Utc};
903            use torm::Uuid;
904            use torm::orm::migration::{
905                TableDefinition, ColumnDefinition, ColumnType, IndexDefinition,
906            };
907
908            #model_impl
909        };
910    })
911}