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            // 整型主键默认开启自增(SQLite: AUTOINCREMENT / MySQL: AUTO_INCREMENT /
603            // PostgreSQL: SERIAL),便于 `create` 时以 `id = 0` 触发自动回填。
604            // 字符串 / UUID 主键不做自增。
605            if col_type == "Integer" || col_type == "BigInteger" {
606                col_def = quote! { #col_def .auto_increment() };
607            }
608        }
609        if is_unique {
610            col_def = quote! { #col_def .unique() };
611        }
612        column_toks.push(col_def);
613    }
614
615    // Index definitions derived from index/uniqueIndex tags, grouped by index
616    // name so composite indexes share a single `IndexDefinition`.
617    let mut index_defs: Vec<proc_macro2::TokenStream> = Vec::new();
618    let mut groups: std::collections::HashMap<String, (bool, Vec<String>)> =
619        std::collections::HashMap::new();
620    for f in fields {
621        if f.kind == FieldKind::Skip {
622            continue;
623        }
624        let col = f.column_name();
625        for ix in &f.indexes {
626            let key = if ix.name.is_empty() {
627                col.clone()
628            } else {
629                ix.name.clone()
630            };
631            groups
632                .entry(key.clone())
633                .or_insert((ix.unique, Vec::new()))
634                .1
635                .push(col.clone());
636            if ix.unique {
637                groups.get_mut(&key).unwrap().0 = true;
638            }
639        }
640    }
641    let mut sorted_keys: Vec<&String> = groups.keys().collect();
642    sorted_keys.sort();
643    for key in sorted_keys {
644        let (unique, cols) = &groups[key];
645        let name = if key.starts_with("idx_") {
646            key.clone()
647        } else {
648            format!("idx_{}_{}", table_name, key)
649        };
650        let cols_lit: Vec<syn::LitStr> = cols
651            .iter()
652            .map(|c| syn::LitStr::new(c, Span::call_site()))
653            .collect();
654        if *unique {
655            index_defs.push(quote! {
656                IndexDefinition::new(#name, &[#(#cols_lit),*]).unique()
657            });
658        } else {
659            index_defs.push(quote! {
660                IndexDefinition::new(#name, &[#(#cols_lit),*])
661            });
662        }
663    }
664
665    quote! {
666        fn schema() -> Option<TableDefinition> {
667            Some(TableDefinition::new(#table_name)
668                #( .add_column(#column_toks) )*
669                #( .add_index(#index_defs) )*
670            )
671        }
672    }
673}
674
675/// Build the complete `impl Model for ...` block.
676fn build_model_impl(
677    input: &DeriveInput,
678    config: &ModelConfig,
679    fields: &[FieldInfo],
680) -> proc_macro2::TokenStream {
681    let ident = &input.ident;
682    let table_name = &config.table_name;
683
684    let pk = fields.iter().find(|f| f.kind == FieldKind::PrimaryKey);
685    let ts = fields.iter().find(|f| f.kind == FieldKind::Timestamps);
686    let ts_field_created = fields
687        .iter()
688        .find(|f| f.kind == FieldKind::TimestampField && f.ident == "created_at");
689    let ts_field_updated = fields
690        .iter()
691        .find(|f| f.kind == FieldKind::TimestampField && f.ident == "updated_at");
692    let ts_field_deleted = fields
693        .iter()
694        .find(|f| f.kind == FieldKind::TimestampField && f.ident == "deleted_at");
695    let persist: Vec<&FieldInfo> = fields
696        .iter()
697        .filter(|f| f.kind == FieldKind::Persist)
698        .collect();
699
700    // --- id / set_id ---
701    let (id_fn, set_id_fn) = match pk {
702        Some(pk) => {
703            let pk_ident = &pk.ident;
704            let pk_ty = inner_type(&pk.ty);
705            let pk_tag = type_tag(pk_ty).unwrap_or("");
706            let (id_expr, set_expr) = match pk_tag {
707                "string" => (
708                    quote! {
709                        if self.#pk_ident.is_empty() { None } else { Some(self.#pk_ident.clone()) }
710                    },
711                    quote! { self.#pk_ident = id; },
712                ),
713                "uuid" => (
714                    quote! {
715                        if self.#pk_ident.is_nil() { None } else { Some(self.#pk_ident.to_string()) }
716                    },
717                    quote! { self.#pk_ident = Uuid::parse_str(&id).unwrap_or_else(|_| Uuid::nil()); },
718                ),
719                "i64" => (
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                "i32" => (
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                "i16" => (
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                "i8" => (
738                    quote! {
739                        if self.#pk_ident > 0 { Some(self.#pk_ident.to_string()) } else { None }
740                    },
741                    quote! { self.#pk_ident = id.parse().unwrap_or(0); },
742                ),
743                // Fallback: ToString + From<String>.
744                _ => (
745                    quote! {
746                        let s = self.#pk_ident.to_string();
747                        if s.is_empty() { None } else { Some(s) }
748                    },
749                    quote! { self.#pk_ident = id.into(); },
750                ),
751            };
752            (
753                quote! { fn id(&self) -> Option<String> { #id_expr } },
754                quote! { fn set_id(&mut self, id: String) { #set_expr } },
755            )
756        }
757        None => (
758            quote! {
759                fn id(&self) -> Option<String> { None }
760            },
761            quote! {
762                fn set_id(&mut self, _id: String) {}
763            },
764        ),
765    };
766
767    // --- timestamps accessors ---
768    // Prefer a `Timestamps` struct field; otherwise fall back to standalone
769    // created_at/updated_at/deleted_at fields.
770    let ts_impl = if let Some(ts) = ts {
771        let t = &ts.ident;
772        quote! {
773            fn created_at(&self) -> Option<DateTime<Utc>> { self.#t.created_at }
774            fn updated_at(&self) -> Option<DateTime<Utc>> { self.#t.updated_at }
775            fn deleted_at(&self) -> Option<DateTime<Utc>> { self.#t.deleted_at }
776            fn set_created_at(&mut self, timestamp: DateTime<Utc>) { self.#t.created_at = Some(timestamp); }
777            fn set_updated_at(&mut self, timestamp: DateTime<Utc>) { self.#t.updated_at = Some(timestamp); }
778            fn set_deleted_at(&mut self, timestamp: Option<DateTime<Utc>>) { self.#t.deleted_at = timestamp; }
779        }
780    } else {
781        let created = ts_field_created.map(|f| &f.ident);
782        let updated = ts_field_updated.map(|f| &f.ident);
783        let deleted = ts_field_deleted.map(|f| &f.ident);
784        let (created_get, created_set) = match created {
785            Some(ci) => (
786                quote! { self.#ci },
787                quote! { self.#ci = Some(timestamp); },
788            ),
789            None => (quote! { None }, quote! {}),
790        };
791        let (updated_get, updated_set) = match updated {
792            Some(ui) => (
793                quote! { self.#ui },
794                quote! { self.#ui = Some(timestamp); },
795            ),
796            None => (quote! { None }, quote! {}),
797        };
798        let (deleted_get, deleted_set) = match deleted {
799            Some(di) => (
800                quote! { self.#di },
801                quote! { self.#di = timestamp; },
802            ),
803            None => (quote! { None }, quote! {}),
804        };
805        quote! {
806            fn created_at(&self) -> Option<DateTime<Utc>> { #created_get }
807            fn updated_at(&self) -> Option<DateTime<Utc>> { #updated_get }
808            fn deleted_at(&self) -> Option<DateTime<Utc>> { #deleted_get }
809            fn set_created_at(&mut self, timestamp: DateTime<Utc>) { #created_set }
810            fn set_updated_at(&mut self, timestamp: DateTime<Utc>) { #updated_set }
811            fn set_deleted_at(&mut self, timestamp: Option<DateTime<Utc>>) { #deleted_set }
812        }
813    };
814
815    // --- columns() ---
816    let column_entries = gen_columns_entries(&persist);
817
818    // --- from_row() ---
819    let mut literals: Vec<proc_macro2::TokenStream> = Vec::new();
820    for f in fields {
821        let fident = &f.ident;
822        match f.kind {
823            FieldKind::Skip => {
824                literals.push(quote! { #fident: Default::default() });
825            }
826            FieldKind::Timestamps => {
827                literals.push(quote! {
828                    #fident: {
829                        let mut ts = Timestamps::new();
830                        ts.created_at = row.get("created_at").and_then(|v| match v {
831                            SqlValue::DateTime(dt) => Some(*dt),
832                            _ => None,
833                        });
834                        ts.updated_at = row.get("updated_at").and_then(|v| match v {
835                            SqlValue::DateTime(dt) => Some(*dt),
836                            _ => None,
837                        });
838                        ts.deleted_at = row.get("deleted_at").and_then(|v| match v {
839                            SqlValue::DateTime(dt) => Some(*dt),
840                            _ => None,
841                        });
842                        ts
843                    }
844                });
845            }
846            FieldKind::PrimaryKey | FieldKind::Persist | FieldKind::TimestampField => {
847                if let Some(expr) = gen_from_row_expr(f) {
848                    literals.push(quote! { #fident: #expr });
849                } else {
850                    literals.push(quote! { #fident: Default::default() });
851                }
852            }
853        }
854    }
855
856    // --- schema() ---
857    let field_refs: Vec<&FieldInfo> = fields.iter().collect();
858    let schema_impl = gen_schema_impl(&config.table_name, &field_refs);
859
860    quote! {
861        #[automatically_derived]
862        impl Model for #ident {
863            fn table_name() -> &'static str {
864                #table_name
865            }
866
867            #id_fn
868            #set_id_fn
869            #ts_impl
870
871            #schema_impl
872
873            fn columns(&self) -> Vec<(&'static str, SqlValue)> {
874                vec![
875                    #(#column_entries,)*
876                ]
877            }
878
879            fn from_row(row: &Row) -> Option<Self> {
880                Some(Self {
881                    #(#literals,)*
882                })
883            }
884        }
885    }
886}
887
888/// Entry point for `#[derive(Model)]`.
889#[proc_macro_derive(Model, attributes(model))]
890pub fn derive_model(input: TokenStream) -> TokenStream {
891    let input = parse_macro_input!(input as DeriveInput);
892    match expand(&input) {
893        Ok(ts) => ts.into(),
894        Err(e) => e.to_compile_error().into(),
895    }
896}
897
898fn expand(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
899    let config = ModelConfig::parse(&input.attrs)?;
900    let fields = collect_fields(input, &config.primary_key)?;
901
902    let model_impl = build_model_impl(input, &config, &fields);
903
904    Ok(quote! {
905        const _: () = {
906            use torm::orm::model::Timestamps;
907            use torm::db::db_types::{Row, SqlValue};
908            use torm::chrono::{DateTime, Utc};
909            use torm::Uuid;
910            use torm::orm::migration::{
911                TableDefinition, ColumnDefinition, ColumnType, IndexDefinition,
912            };
913
914            #model_impl
915        };
916    })
917}