Skip to main content

mool_macros_impl/
model.rs

1use quote::quote;
2use syn::{DeriveInput, GenericArgument, PathArguments, Type, TypePath};
3
4use crate::schemable::{FieldMeta, ParsedStruct};
5
6pub fn derive_model(
7    input: proc_macro2::TokenStream,
8    runtime_path: proc_macro2::TokenStream,
9) -> proc_macro2::TokenStream {
10    let input = match syn::parse2::<DeriveInput>(input) {
11        Ok(input) => input,
12        Err(err) => return err.to_compile_error(),
13    };
14    derive_model_impl(&input, runtime_path)
15}
16
17fn derive_model_impl(
18    input: &DeriveInput,
19    runtime_path: proc_macro2::TokenStream,
20) -> proc_macro2::TokenStream {
21    let parsed = match ParsedStruct::from_derive_input(input.clone()) {
22        Ok(parsed) => parsed,
23        Err(err) => return err.to_compile_error(),
24    };
25
26    let ident = &parsed.ident;
27    let mut generics = parsed.generics.clone();
28    let pk_fields = match primary_key_fields(&parsed) {
29        Ok(fields) => fields,
30        Err(err) => return err.to_compile_error(),
31    };
32    if pk_fields.is_empty() {
33        return syn::Error::new_spanned(
34            ident,
35            "Model requires a primary key field or a field named `id`",
36        )
37        .to_compile_error();
38    };
39
40    let pk_idents = pk_fields
41        .iter()
42        .filter_map(|field| field.ident.as_ref())
43        .collect::<Vec<_>>();
44    let pk_types = pk_fields.iter().map(|field| &field.ty).collect::<Vec<_>>();
45    let pk_columns = pk_fields
46        .iter()
47        .map(|field| column_name(field))
48        .collect::<Vec<_>>();
49    let pk_type = match pk_types.as_slice() {
50        [ty] => quote! { #ty },
51        _ => quote! { (#(#pk_types),*) },
52    };
53    let pk_value = match pk_idents.as_slice() {
54        [ident] => quote! { self.#ident.clone() },
55        _ => quote! { (#(self.#pk_idents.clone()),*) },
56    };
57
58    let record = crate::record::derive_record_impl(input, runtime_path.clone());
59    let crate_path = crate::runtime_path(input, runtime_path);
60    let wc = generics.where_clause.get_or_insert(syn::WhereClause {
61        where_token: <syn::Token![where]>::default(),
62        predicates: syn::punctuated::Punctuated::new(),
63    });
64    wc.predicates.push(syn::parse_quote! {
65        #pk_type: ::core::clone::Clone + ::core::hash::Hash + ::core::cmp::Eq
66    });
67    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
68    let into_table = gen_into_table(&parsed, &crate_path);
69    let sql_enum_schema = gen_sql_enum_schema(&parsed, &crate_path);
70
71    quote! {
72        #record
73        #into_table
74        #sql_enum_schema
75
76        impl #impl_generics #crate_path::Model for #ident #ty_generics #where_clause {
77            type PrimaryKey = #pk_type;
78
79            fn model_schema() -> #crate_path::ModelSchema<Self> {
80                #crate_path::ModelSchema::new(
81                    <Self as #crate_path::Record>::record_schema(),
82                    &[#(#pk_columns),*],
83                )
84            }
85
86            fn primary_key(&self) -> Self::PrimaryKey {
87                #pk_value
88            }
89        }
90    }
91}
92
93fn primary_key_fields(parsed: &ParsedStruct) -> syn::Result<Vec<&FieldMeta>> {
94    if let Some(primary_key) = &parsed.container.primary_key {
95        let mut out = Vec::with_capacity(primary_key.columns.len());
96        for column in primary_key.columns.iter() {
97            let value = column.as_str();
98            let Some(field) = parsed
99                .fields
100                .iter()
101                .find(|field| column_name(field) == value)
102            else {
103                return Err(syn::Error::new(
104                    proc_macro2::Span::call_site(),
105                    format!("table primary_key references unknown column '{value}'"),
106                ));
107            };
108            out.push(field);
109        }
110        return Ok(out);
111    }
112    let flagged = parsed
113        .fields
114        .iter()
115        .filter(|field| field.column.primary_key)
116        .collect::<Vec<_>>();
117    if !flagged.is_empty() {
118        return Ok(flagged);
119    }
120    Ok(parsed
121        .fields
122        .iter()
123        .find(|field| field.ident.as_ref().is_some_and(|ident| ident == "id"))
124        .into_iter()
125        .collect())
126}
127
128fn gen_into_table(
129    parsed: &ParsedStruct,
130    crate_path: &proc_macro2::TokenStream,
131) -> proc_macro2::TokenStream {
132    let ident = &parsed.ident;
133    let generics = parsed.generics.clone();
134    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
135    let table_name = parsed
136        .container
137        .name
138        .as_ref()
139        .or(parsed.container.table.as_ref())
140        .map(|lit| lit.value())
141        .unwrap_or_else(|| crate::schemable::to_snake_case(&ident.to_string()));
142    let schema_tokens = gen_schema_call(parsed.container.schema.as_ref().map(|lit| lit.value()));
143    let columns = parsed
144        .fields
145        .iter()
146        .filter(is_table_field)
147        .filter_map(|field| gen_column(field, &table_name, crate_path));
148    let indexes = parsed
149        .fields
150        .iter()
151        .filter(is_table_field)
152        .filter_map(gen_index);
153    let constraints = parsed
154        .fields
155        .iter()
156        .filter(is_table_field)
157        .filter_map(gen_unique_constraint);
158    let primary_key = gen_primary_key(parsed);
159    let foreign_keys = parsed.container.foreign_keys.iter().map(gen_foreign_key);
160
161    quote! {
162        impl #impl_generics #crate_path::IntoTable for #ident #ty_generics #where_clause {
163            fn into_table(
164                dialect: &#crate_path::Dialect,
165            ) -> #crate_path::Table {
166                let mut table = #crate_path::TableBuilder::new(#table_name);
167                #schema_tokens
168                #(#columns)*
169                #primary_key
170                #(#indexes)*
171                #(#constraints)*
172                #(#foreign_keys)*
173                table.build()
174            }
175        }
176    }
177}
178
179fn is_table_field(field: &&crate::schemable::FieldMeta) -> bool {
180    !field.column.skip && !field.column.flatten && field.column.prefetch.is_none()
181}
182
183fn gen_schema_call(schema: Option<String>) -> Option<proc_macro2::TokenStream> {
184    schema.map(|value| quote! { table = table.schema(#value); })
185}
186
187fn gen_column(
188    field: &crate::schemable::FieldMeta,
189    table_name: &str,
190    crate_path: &proc_macro2::TokenStream,
191) -> Option<proc_macro2::TokenStream> {
192    let name = column_name(field);
193    let ty = &field.ty;
194    let inferred_array_type = array_sql_type(ty);
195    let nullable_tokens = gen_nullable(
196        field,
197        field.column.serial || field.column.sql_type.is_some() || inferred_array_type.is_some(),
198    );
199    let pk = field
200        .column
201        .primary_key
202        .then(|| quote! { let c = c.primary_key(); });
203    let default = field.column.default.as_ref().map(|lit| {
204        let value = lit.value();
205        quote! { let c = c.default(#value); }
206    });
207    let check = field.column.check.as_ref().map(|lit| {
208        let value = lit.value();
209        quote! { let c = c.check(#value); }
210    });
211    let references = gen_reference(field);
212    let body = quote! {
213        #nullable_tokens
214        #pk
215        #default
216        #check
217        #references
218        c
219    };
220
221    if field.column.serial {
222        return Some(quote! {
223            table = table.column(#name, "bigserial", |c| {
224                #body
225            });
226        });
227    }
228    if field.column.sql_enum {
229        let enum_ty = option_inner_type(ty).unwrap_or(ty);
230        let check_name = format!("ck_{table_name}_{name}_sql_enum");
231        return Some(quote! {
232            table = table.column(
233                #name,
234                <#enum_ty as #crate_path::SqlEnum>::sql_column_type(*dialect),
235                |c| {
236                    #body
237                },
238            );
239            if let Some(check) = <#enum_ty as #crate_path::SqlEnum>::sql_check_expr(#name, *dialect) {
240                table = table.check(#check_name, check);
241            }
242        });
243    }
244    if let Some(sql_type) = field.column.sql_type.as_ref().map(|lit| lit.value()) {
245        return Some(quote! {
246            table = table.column(#name, #sql_type, |c| {
247                #body
248            });
249        });
250    }
251    if let Some(sql_type) = inferred_array_type {
252        return Some(quote! {
253            table = table.column(#name, #sql_type, |c| {
254                #body
255            });
256        });
257    }
258    Some(quote! {
259        table = table.column_from_type::<#ty>(dialect, #name, |c| {
260            #body
261        });
262    })
263}
264
265fn gen_nullable(field: &FieldMeta, explicit_type: bool) -> Option<proc_macro2::TokenStream> {
266    match field.column.nullable {
267        Some(true) => Some(quote! { let c = c.nullable(); }),
268        Some(false) => Some(quote! { let c = c.not_null(); }),
269        None if explicit_type && is_option_type(&field.ty) => {
270            Some(quote! { let c = c.nullable(); })
271        }
272        None if explicit_type => Some(quote! { let c = c.not_null(); }),
273        None => None,
274    }
275}
276
277fn gen_reference(field: &crate::schemable::FieldMeta) -> Option<proc_macro2::TokenStream> {
278    let references = field.column.references.as_ref()?.value();
279    let (table, column) = references.rsplit_once('.')?;
280    let name = field.column.references_name.as_ref().map(|lit| lit.value());
281    Some(match name {
282        Some(name) => quote! {
283            let c = c.references_named(#name, #table, #column);
284        },
285        None => quote! {
286            let c = c.references(#table, #column);
287        },
288    })
289}
290
291fn gen_index(field: &crate::schemable::FieldMeta) -> Option<proc_macro2::TokenStream> {
292    if !field.column.index && field.column.index_name.is_none() {
293        return None;
294    }
295    let name = column_name(field);
296    Some(
297        match field.column.index_name.as_ref().map(|lit| lit.value()) {
298            Some(index_name) => quote! { table = table.index(#index_name, &[#name]); },
299            None => quote! { table = table.index_columns(&[#name]); },
300        },
301    )
302}
303
304fn gen_unique_constraint(field: &crate::schemable::FieldMeta) -> Option<proc_macro2::TokenStream> {
305    if !field.column.unique && field.column.unique_name.is_none() {
306        return None;
307    }
308    let name = column_name(field);
309    Some(
310        match field.column.unique_name.as_ref().map(|lit| lit.value()) {
311            Some(unique_name) => quote! { table = table.unique(#unique_name, &[#name]); },
312            None => quote! { table = table.unique_columns(&[#name]); },
313        },
314    )
315}
316
317fn gen_primary_key(parsed: &ParsedStruct) -> Option<proc_macro2::TokenStream> {
318    let primary_key = parsed.container.primary_key.as_ref()?;
319    let columns = primary_key.columns.iter().collect::<Vec<_>>();
320    Some(match primary_key.name.as_ref() {
321        Some(name) => quote! { table = table.primary_key(#name, &[#(#columns),*]); },
322        None => quote! { table = table.primary_key_columns(&[#(#columns),*]); },
323    })
324}
325
326fn gen_foreign_key(foreign_key: &crate::schemable::ForeignKeySpec) -> proc_macro2::TokenStream {
327    let columns = foreign_key.columns.iter().collect::<Vec<_>>();
328    let target_table = &foreign_key.references.table;
329    let target_columns = foreign_key.references.columns.iter().collect::<Vec<_>>();
330    match foreign_key.name.as_ref() {
331        Some(name) => {
332            quote! { table = table.foreign_key_named_columns(#name, &[#(#columns),*], #target_table, &[#(#target_columns),*]); }
333        }
334        None => {
335            quote! { table = table.foreign_key_columns(&[#(#columns),*], #target_table, &[#(#target_columns),*]); }
336        }
337    }
338}
339
340fn gen_sql_enum_schema(
341    parsed: &ParsedStruct,
342    crate_path: &proc_macro2::TokenStream,
343) -> proc_macro2::TokenStream {
344    let ident = &parsed.ident;
345    let generics = parsed.generics.clone();
346    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
347    let enum_types = sql_enum_types(parsed);
348    let registrations = enum_types.iter().map(|ty| {
349        quote! {
350            #crate_path::enums::SqlEnumRegistration::new(
351                #crate_path::enums::__private::register_enum::<#ty>,
352            )
353        }
354    });
355    quote! {
356        impl #impl_generics #crate_path::enums::SqlEnumSchema for #ident #ty_generics #where_clause {
357            const SQL_ENUMS: &'static [#crate_path::enums::SqlEnumRegistration] = &[
358                #(#registrations),*
359            ];
360        }
361    }
362}
363
364fn sql_enum_types(parsed: &ParsedStruct) -> Vec<&Type> {
365    let mut out = Vec::new();
366    let mut seen = std::collections::HashSet::new();
367    for field in &parsed.fields {
368        if !field.column.sql_enum {
369            continue;
370        }
371        let ty = option_inner_type(&field.ty).unwrap_or(&field.ty);
372        let key = quote::quote!(#ty).to_string();
373        if seen.insert(key) {
374            out.push(ty);
375        }
376    }
377    out
378}
379
380fn column_name(field: &FieldMeta) -> String {
381    field
382        .column
383        .name
384        .as_ref()
385        .map(|lit| lit.value())
386        .or_else(|| field.ident.as_ref().map(|ident| ident.to_string()))
387        .unwrap_or_default()
388}
389
390fn is_option_type(ty: &syn::Type) -> bool {
391    option_inner_type(ty).is_some()
392}
393
394fn array_sql_type(ty: &Type) -> Option<&'static str> {
395    let inner = array_inner_type(ty)?;
396    let Type::Path(path) = inner else {
397        return None;
398    };
399    if path.qself.is_some() {
400        return None;
401    }
402    let normalized = path
403        .path
404        .segments
405        .iter()
406        .map(|segment| segment.ident.to_string())
407        .collect::<Vec<_>>()
408        .join("::");
409    match normalized.as_str() {
410        "String" | "std::string::String" | "alloc::string::String" => Some("text[]"),
411        "bool" => Some("boolean[]"),
412        "i16" => Some("smallint[]"),
413        "i32" => Some("integer[]"),
414        "i64" => Some("bigint[]"),
415        "f32" => Some("real[]"),
416        "f64" => Some("double precision[]"),
417        "uuid::Uuid" => Some("uuid[]"),
418        "chrono::NaiveDate" => Some("date[]"),
419        "chrono::NaiveDateTime" => Some("timestamp[]"),
420        "chrono::DateTime" => Some("timestamptz[]"),
421        _ => None,
422    }
423}
424
425fn array_inner_type(ty: &Type) -> Option<&Type> {
426    let candidate = option_inner_type(ty).unwrap_or(ty);
427    let Type::Path(path) = candidate else {
428        return None;
429    };
430    if !is_canonical_vec(path) {
431        return None;
432    }
433    let segment = path.path.segments.last()?;
434    let PathArguments::AngleBracketed(args) = &segment.arguments else {
435        return None;
436    };
437    let Some(GenericArgument::Type(inner)) = args.args.first() else {
438        return None;
439    };
440    if is_u8_type(inner) {
441        return None;
442    }
443    Some(inner)
444}
445
446fn option_inner_type(ty: &Type) -> Option<&Type> {
447    let Type::Path(path) = ty else {
448        return None;
449    };
450    if !is_canonical_option(path) {
451        return None;
452    }
453    let segment = path.path.segments.last()?;
454    let PathArguments::AngleBracketed(args) = &segment.arguments else {
455        return None;
456    };
457    let Some(GenericArgument::Type(inner)) = args.args.first() else {
458        return None;
459    };
460    Some(inner)
461}
462
463fn is_canonical_option(path: &TypePath) -> bool {
464    if path.qself.is_some() {
465        return false;
466    }
467    let mut segments = path
468        .path
469        .segments
470        .iter()
471        .map(|segment| segment.ident.to_string());
472    match (
473        segments.next(),
474        segments.next(),
475        segments.next(),
476        segments.next(),
477    ) {
478        (Some(first), None, None, None) => first == "Option",
479        (Some(first), Some(second), Some(third), None) => {
480            (first == "std" || first == "core") && second == "option" && third == "Option"
481        }
482        _ => false,
483    }
484}
485
486fn is_canonical_vec(path: &TypePath) -> bool {
487    if path.qself.is_some() {
488        return false;
489    }
490    let mut segments = path
491        .path
492        .segments
493        .iter()
494        .map(|segment| segment.ident.to_string());
495    match (
496        segments.next(),
497        segments.next(),
498        segments.next(),
499        segments.next(),
500    ) {
501        (Some(first), None, None, None) => first == "Vec",
502        (Some(first), Some(second), Some(third), None) => {
503            (first == "std" || first == "alloc") && second == "vec" && third == "Vec"
504        }
505        _ => false,
506    }
507}
508
509fn is_u8_type(ty: &Type) -> bool {
510    let Type::Path(path) = ty else {
511        return false;
512    };
513    if path.qself.is_some() {
514        return false;
515    }
516    let mut segments = path
517        .path
518        .segments
519        .iter()
520        .map(|segment| segment.ident.to_string());
521    matches!(
522        (
523            segments.next(),
524            segments.next(),
525            segments.next(),
526            segments.next()
527        ),
528        (Some(first), None, None, None) if first == "u8"
529    )
530}