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 let column_desc = <#ty as #crate_path::ColumnType>::column_desc(dialect);
260 table = table.column(#name, column_desc.sql_type, |c| {
261 let c = if column_desc.nullable {
262 c.nullable()
263 } else {
264 c.not_null()
265 };
266 #body
267 });
268 })
269}
270
271fn gen_nullable(field: &FieldMeta, explicit_type: bool) -> Option<proc_macro2::TokenStream> {
272 match field.column.nullable {
273 Some(true) => Some(quote! { let c = c.nullable(); }),
274 Some(false) => Some(quote! { let c = c.not_null(); }),
275 None if explicit_type && is_option_type(&field.ty) => {
276 Some(quote! { let c = c.nullable(); })
277 }
278 None if explicit_type => Some(quote! { let c = c.not_null(); }),
279 None => None,
280 }
281}
282
283fn gen_reference(field: &crate::schemable::FieldMeta) -> Option<proc_macro2::TokenStream> {
284 let reference = field.column.reference.as_ref()?;
285 let references = reference.target.as_ref()?;
286 let (table, column) = references.rsplit_once('.')?;
287 let name = reference.name.clone();
288 Some(match name {
289 Some(name) => quote! {
290 let c = c.references_named(#name, #table, #column);
291 },
292 None => quote! {
293 let c = c.references(#table, #column);
294 },
295 })
296}
297
298fn gen_index(field: &crate::schemable::FieldMeta) -> Option<proc_macro2::TokenStream> {
299 if !field.column.index && field.column.index_name.is_none() {
300 return None;
301 }
302 let name = column_name(field);
303 Some(
304 match field.column.index_name.as_ref().map(|lit| lit.value()) {
305 Some(index_name) => quote! { table = table.index(#index_name, &[#name]); },
306 None => quote! { table = table.index_columns(&[#name]); },
307 },
308 )
309}
310
311fn gen_unique_constraint(field: &crate::schemable::FieldMeta) -> Option<proc_macro2::TokenStream> {
312 if !field.column.unique && field.column.unique_name.is_none() {
313 return None;
314 }
315 let name = column_name(field);
316 Some(
317 match field.column.unique_name.as_ref().map(|lit| lit.value()) {
318 Some(unique_name) => quote! { table = table.unique(#unique_name, &[#name]); },
319 None => quote! { table = table.unique_columns(&[#name]); },
320 },
321 )
322}
323
324fn gen_primary_key(parsed: &ParsedStruct) -> Option<proc_macro2::TokenStream> {
325 let primary_key = parsed.container.primary_key.as_ref()?;
326 let columns = primary_key.columns.iter().collect::<Vec<_>>();
327 Some(match primary_key.name.as_ref() {
328 Some(name) => quote! { table = table.primary_key(#name, &[#(#columns),*]); },
329 None => quote! { table = table.primary_key_columns(&[#(#columns),*]); },
330 })
331}
332
333fn gen_foreign_key(foreign_key: &crate::schemable::ForeignKeySpec) -> proc_macro2::TokenStream {
334 let columns = foreign_key.columns.iter().collect::<Vec<_>>();
335 let target_table = &foreign_key.references.table;
336 let target_columns = foreign_key.references.columns.iter().collect::<Vec<_>>();
337 match foreign_key.name.as_ref() {
338 Some(name) => {
339 quote! { table = table.foreign_key_named_columns(#name, &[#(#columns),*], #target_table, &[#(#target_columns),*]); }
340 }
341 None => {
342 quote! { table = table.foreign_key_columns(&[#(#columns),*], #target_table, &[#(#target_columns),*]); }
343 }
344 }
345}
346
347fn gen_sql_enum_schema(
348 parsed: &ParsedStruct,
349 crate_path: &proc_macro2::TokenStream,
350) -> proc_macro2::TokenStream {
351 let ident = &parsed.ident;
352 let generics = parsed.generics.clone();
353 let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
354 let enum_types = sql_enum_types(parsed);
355 let registrations = enum_types.iter().map(|ty| {
356 quote! {
357 #crate_path::enums::SqlEnumRegistration::new(
358 #crate_path::enums::__private::register_enum::<#ty>,
359 )
360 }
361 });
362 quote! {
363 impl #impl_generics #crate_path::enums::SqlEnumSchema for #ident #ty_generics #where_clause {
364 const SQL_ENUMS: &'static [#crate_path::enums::SqlEnumRegistration] = &[
365 #(#registrations),*
366 ];
367 }
368 }
369}
370
371fn sql_enum_types(parsed: &ParsedStruct) -> Vec<&Type> {
372 let mut out = Vec::new();
373 let mut seen = std::collections::HashSet::new();
374 for field in &parsed.fields {
375 if !field.column.sql_enum {
376 continue;
377 }
378 let ty = option_inner_type(&field.ty).unwrap_or(&field.ty);
379 let key = quote::quote!(#ty).to_string();
380 if seen.insert(key) {
381 out.push(ty);
382 }
383 }
384 out
385}
386
387fn column_name(field: &FieldMeta) -> String {
388 field
389 .column
390 .name
391 .as_ref()
392 .map(|lit| lit.value())
393 .or_else(|| field.ident.as_ref().map(|ident| ident.to_string()))
394 .unwrap_or_default()
395}
396
397fn is_option_type(ty: &syn::Type) -> bool {
398 option_inner_type(ty).is_some()
399}
400
401fn array_sql_type(ty: &Type) -> Option<&'static str> {
402 let inner = array_inner_type(ty)?;
403 let Type::Path(path) = inner else {
404 return None;
405 };
406 if path.qself.is_some() {
407 return None;
408 }
409 let normalized = path
410 .path
411 .segments
412 .iter()
413 .map(|segment| segment.ident.to_string())
414 .collect::<Vec<_>>()
415 .join("::");
416 match normalized.as_str() {
417 "String" | "std::string::String" | "alloc::string::String" => Some("text[]"),
418 "bool" => Some("boolean[]"),
419 "i16" => Some("smallint[]"),
420 "i32" => Some("integer[]"),
421 "i64" => Some("bigint[]"),
422 "f32" => Some("real[]"),
423 "f64" => Some("double precision[]"),
424 "uuid::Uuid" => Some("uuid[]"),
425 "chrono::NaiveDate" => Some("date[]"),
426 "chrono::NaiveDateTime" => Some("timestamp[]"),
427 "chrono::DateTime" => Some("timestamptz[]"),
428 _ => None,
429 }
430}
431
432fn array_inner_type(ty: &Type) -> Option<&Type> {
433 let candidate = option_inner_type(ty).unwrap_or(ty);
434 let Type::Path(path) = candidate else {
435 return None;
436 };
437 if !is_canonical_vec(path) {
438 return None;
439 }
440 let segment = path.path.segments.last()?;
441 let PathArguments::AngleBracketed(args) = &segment.arguments else {
442 return None;
443 };
444 let Some(GenericArgument::Type(inner)) = args.args.first() else {
445 return None;
446 };
447 if is_u8_type(inner) {
448 return None;
449 }
450 Some(inner)
451}
452
453fn option_inner_type(ty: &Type) -> Option<&Type> {
454 let Type::Path(path) = ty else {
455 return None;
456 };
457 if !is_canonical_option(path) {
458 return None;
459 }
460 let segment = path.path.segments.last()?;
461 let PathArguments::AngleBracketed(args) = &segment.arguments else {
462 return None;
463 };
464 let Some(GenericArgument::Type(inner)) = args.args.first() else {
465 return None;
466 };
467 Some(inner)
468}
469
470fn is_canonical_option(path: &TypePath) -> bool {
471 if path.qself.is_some() {
472 return false;
473 }
474 let mut segments = path
475 .path
476 .segments
477 .iter()
478 .map(|segment| segment.ident.to_string());
479 match (
480 segments.next(),
481 segments.next(),
482 segments.next(),
483 segments.next(),
484 ) {
485 (Some(first), None, None, None) => first == "Option",
486 (Some(first), Some(second), Some(third), None) => {
487 (first == "std" || first == "core") && second == "option" && third == "Option"
488 }
489 _ => false,
490 }
491}
492
493fn is_canonical_vec(path: &TypePath) -> bool {
494 if path.qself.is_some() {
495 return false;
496 }
497 let mut segments = path
498 .path
499 .segments
500 .iter()
501 .map(|segment| segment.ident.to_string());
502 match (
503 segments.next(),
504 segments.next(),
505 segments.next(),
506 segments.next(),
507 ) {
508 (Some(first), None, None, None) => first == "Vec",
509 (Some(first), Some(second), Some(third), None) => {
510 (first == "std" || first == "alloc") && second == "vec" && third == "Vec"
511 }
512 _ => false,
513 }
514}
515
516fn is_u8_type(ty: &Type) -> bool {
517 let Type::Path(path) = ty else {
518 return false;
519 };
520 if path.qself.is_some() {
521 return false;
522 }
523 let mut segments = path
524 .path
525 .segments
526 .iter()
527 .map(|segment| segment.ident.to_string());
528 matches!(
529 (
530 segments.next(),
531 segments.next(),
532 segments.next(),
533 segments.next()
534 ),
535 (Some(first), None, None, None) if first == "u8"
536 )
537}