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}