Skip to main content

sql_orm_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Span, TokenStream as TokenStream2};
3use quote::{format_ident, quote};
4use std::collections::BTreeMap;
5use syn::{
6    Data, DeriveInput, Error, Expr, ExprLit, Field, Fields, Ident, Lit, LitStr, Path, Result,
7    Token, Type, parse_macro_input, punctuated::Punctuated, spanned::Spanned,
8};
9
10#[proc_macro_derive(Entity, attributes(orm))]
11pub fn derive_entity(input: TokenStream) -> TokenStream {
12    match derive_entity_impl(parse_macro_input!(input as DeriveInput)) {
13        Ok(tokens) => tokens.into(),
14        Err(error) => error.to_compile_error().into(),
15    }
16}
17
18#[proc_macro_derive(DbContext, attributes(orm))]
19pub fn derive_db_context(input: TokenStream) -> TokenStream {
20    match derive_db_context_impl(parse_macro_input!(input as DeriveInput)) {
21        Ok(tokens) => tokens.into(),
22        Err(error) => error.to_compile_error().into(),
23    }
24}
25
26#[proc_macro_derive(Insertable, attributes(orm))]
27pub fn derive_insertable(input: TokenStream) -> TokenStream {
28    match derive_insertable_impl(parse_macro_input!(input as DeriveInput)) {
29        Ok(tokens) => tokens.into(),
30        Err(error) => error.to_compile_error().into(),
31    }
32}
33
34#[proc_macro_derive(Changeset, attributes(orm))]
35pub fn derive_changeset(input: TokenStream) -> TokenStream {
36    match derive_changeset_impl(parse_macro_input!(input as DeriveInput)) {
37        Ok(tokens) => tokens.into(),
38        Err(error) => error.to_compile_error().into(),
39    }
40}
41
42#[proc_macro_derive(FromRow, attributes(orm))]
43pub fn derive_from_row(input: TokenStream) -> TokenStream {
44    match derive_from_row_impl(parse_macro_input!(input as DeriveInput)) {
45        Ok(tokens) => tokens.into(),
46        Err(error) => error.to_compile_error().into(),
47    }
48}
49
50#[proc_macro_derive(AuditFields, attributes(orm))]
51pub fn derive_audit_fields(input: TokenStream) -> TokenStream {
52    match derive_policy_fields_impl(
53        parse_macro_input!(input as DeriveInput),
54        PolicyFieldsKind::Audit,
55    ) {
56        Ok(tokens) => tokens.into(),
57        Err(error) => error.to_compile_error().into(),
58    }
59}
60
61#[proc_macro_derive(SoftDeleteFields, attributes(orm))]
62pub fn derive_soft_delete_fields(input: TokenStream) -> TokenStream {
63    match derive_policy_fields_impl(
64        parse_macro_input!(input as DeriveInput),
65        PolicyFieldsKind::SoftDelete,
66    ) {
67        Ok(tokens) => tokens.into(),
68        Err(error) => error.to_compile_error().into(),
69    }
70}
71
72#[proc_macro_derive(TenantContext, attributes(orm))]
73pub fn derive_tenant_context(input: TokenStream) -> TokenStream {
74    match derive_tenant_context_impl(parse_macro_input!(input as DeriveInput)) {
75        Ok(tokens) => tokens.into(),
76        Err(error) => error.to_compile_error().into(),
77    }
78}
79
80#[derive(Clone, Copy)]
81enum PolicyFieldsKind {
82    Audit,
83    SoftDelete,
84}
85
86impl PolicyFieldsKind {
87    fn derive_name(self) -> &'static str {
88        match self {
89            Self::Audit => "AuditFields",
90            Self::SoftDelete => "SoftDeleteFields",
91        }
92    }
93
94    fn policy_name(self) -> &'static str {
95        match self {
96            Self::Audit => "audit",
97            Self::SoftDelete => "soft_delete",
98        }
99    }
100
101    fn default_insertable(self) -> bool {
102        match self {
103            Self::Audit => true,
104            Self::SoftDelete => false,
105        }
106    }
107
108    fn default_updatable(self) -> bool {
109        true
110    }
111}
112
113fn derive_policy_fields_impl(input: DeriveInput, kind: PolicyFieldsKind) -> Result<TokenStream2> {
114    let ident = input.ident;
115    let derive_name = kind.derive_name();
116    let policy_name = kind.policy_name();
117    let fields = match input.data {
118        Data::Struct(data) => match data.fields {
119            Fields::Named(fields) => fields.named,
120            _ => {
121                return Err(Error::new_spanned(
122                    ident,
123                    format!("{derive_name} solo soporta structs con campos nombrados"),
124                ));
125            }
126        },
127        _ => {
128            return Err(Error::new_spanned(
129                ident,
130                format!("{derive_name} solo soporta structs"),
131            ));
132        }
133    };
134
135    let mut columns = Vec::new();
136    let mut column_names = Vec::new();
137    let mut runtime_values = Vec::new();
138    let mut seen_column_names = std::collections::BTreeSet::new();
139
140    for field in fields.iter() {
141        let field_ident = field.ident.as_ref().ok_or_else(|| {
142            Error::new_spanned(field, format!("{derive_name} requiere campos nombrados"))
143        })?;
144        let config = parse_policy_field_config(field, kind)?;
145        let type_info = analyze_type(&field.ty)?;
146        let field_ty = &field.ty;
147        let rust_field = LitStr::new(&field_ident.to_string(), field_ident.span());
148        let column_name = config
149            .column
150            .unwrap_or_else(|| LitStr::new(&field_ident.to_string(), field_ident.span()));
151        validate_non_empty_lit_str(&column_name, "column no puede estar vacío")?;
152        if !seen_column_names.insert(column_name.value()) {
153            return Err(Error::new_spanned(
154                &column_name,
155                format!("{derive_name} no permite columnas duplicadas"),
156            ));
157        }
158        column_names.push(column_name.clone());
159        let renamed_from = option_lit_str(config.renamed_from);
160        let sql_type = match (config.sql_type, config.unsafe_sql_type) {
161            (Some(sql_type), None) => sql_type_from_string(&sql_type)?,
162            (None, Some(sql_type)) => unsafe_sql_type_from_string(&sql_type),
163            (Some(sql_type), Some(_)) => {
164                return Err(Error::new_spanned(
165                    sql_type,
166                    "sql_type y unsafe_sql_type no pueden usarse juntos",
167                ));
168            }
169            (None, None) => {
170                quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::SQL_SERVER_TYPE }
171            }
172        };
173        let nullable = config.nullable || type_info.nullable;
174        let default_sql = option_lit_str(config.default_sql);
175        let max_length = config.length.map_or_else(
176            || quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::DEFAULT_MAX_LENGTH },
177            |length| quote! { Some(#length) },
178        );
179        let precision = config.precision.map_or_else(
180            || quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::DEFAULT_PRECISION },
181            |precision| quote! { Some(#precision) },
182        );
183        let scale = config.scale.map_or_else(
184            || quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::DEFAULT_SCALE },
185            |scale| quote! { Some(#scale) },
186        );
187        let insertable = config.insertable.unwrap_or(kind.default_insertable());
188        let updatable = config.updatable.unwrap_or(kind.default_updatable());
189
190        if matches!(kind, PolicyFieldsKind::Audit | PolicyFieldsKind::SoftDelete) {
191            runtime_values.push(quote! {
192                ::sql_orm::core::ColumnValue::new(
193                    #column_name,
194                    <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
195                        self.#field_ident
196                    )
197                )
198            });
199        }
200
201        columns.push(quote! {
202            ::sql_orm::core::ColumnMetadata {
203                rust_field: #rust_field,
204                column_name: #column_name,
205                renamed_from: #renamed_from,
206                sql_type: #sql_type,
207                nullable: #nullable,
208                primary_key: false,
209                identity: None,
210                default_sql: #default_sql,
211                computed_sql: None,
212                rowversion: false,
213                insertable: #insertable,
214                updatable: #updatable,
215                max_length: #max_length,
216                precision: #precision,
217                scale: #scale,
218            }
219        });
220    }
221
222    let runtime_values_impl = match kind {
223        PolicyFieldsKind::Audit => quote! {
224            impl ::sql_orm::AuditValues for #ident {
225                fn audit_values(self) -> Vec<::sql_orm::core::ColumnValue> {
226                    vec![
227                        #(#runtime_values),*
228                    ]
229                }
230            }
231        },
232        PolicyFieldsKind::SoftDelete => quote! {
233            impl ::sql_orm::SoftDeleteValues for #ident {
234                fn soft_delete_values(self) -> Vec<::sql_orm::core::ColumnValue> {
235                    vec![
236                        #(#runtime_values),*
237                    ]
238                }
239            }
240        },
241    };
242
243    Ok(quote! {
244        impl ::sql_orm::core::EntityPolicy for #ident {
245            const POLICY_NAME: &'static str = #policy_name;
246            const COLUMN_NAMES: &'static [&'static str] = &[#(#column_names),*];
247
248            fn columns() -> &'static [::sql_orm::core::ColumnMetadata] {
249                const COLUMNS: &[::sql_orm::core::ColumnMetadata] = &[
250                    #(#columns),*
251                ];
252
253                COLUMNS
254            }
255        }
256
257        #runtime_values_impl
258    })
259}
260
261fn derive_tenant_context_impl(input: DeriveInput) -> Result<TokenStream2> {
262    let ident = input.ident;
263    let fields = match input.data {
264        Data::Struct(data) => match data.fields {
265            Fields::Named(fields) => fields.named,
266            _ => {
267                return Err(Error::new_spanned(
268                    ident,
269                    "TenantContext solo soporta structs con campos nombrados",
270                ));
271            }
272        },
273        _ => {
274            return Err(Error::new_spanned(
275                ident,
276                "TenantContext solo soporta structs",
277            ));
278        }
279    };
280
281    if fields.len() != 1 {
282        return Err(Error::new_spanned(
283            ident,
284            "TenantContext requiere exactamente un campo tenant",
285        ));
286    }
287
288    let field = fields
289        .first()
290        .expect("TenantContext must have exactly one field after validation");
291    let field_ident = field
292        .ident
293        .as_ref()
294        .ok_or_else(|| Error::new_spanned(field, "TenantContext requiere campos nombrados"))?;
295    let config = parse_tenant_context_field_config(field)?;
296    let type_info = analyze_type(&field.ty)?;
297
298    if type_info.nullable {
299        return Err(Error::new_spanned(
300            &field.ty,
301            "TenantContext no soporta Option<T>; la ausencia de tenant debe representarse sin configurar tenant activo en el contexto",
302        ));
303    }
304
305    let field_ty = &field.ty;
306    let rust_field = LitStr::new(&field_ident.to_string(), field_ident.span());
307    let column_name = config
308        .column
309        .unwrap_or_else(|| LitStr::new(&field_ident.to_string(), field_ident.span()));
310    validate_non_empty_lit_str(&column_name, "column no puede estar vacío")?;
311    let renamed_from = option_lit_str(config.renamed_from);
312    let sql_type = match (config.sql_type, config.unsafe_sql_type) {
313        (Some(sql_type), None) => sql_type_from_string(&sql_type)?,
314        (None, Some(sql_type)) => unsafe_sql_type_from_string(&sql_type),
315        (Some(sql_type), Some(_)) => {
316            return Err(Error::new_spanned(
317                sql_type,
318                "sql_type y unsafe_sql_type no pueden usarse juntos",
319            ));
320        }
321        (None, None) => {
322            quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::SQL_SERVER_TYPE }
323        }
324    };
325    let max_length = config.length.map_or_else(
326        || quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::DEFAULT_MAX_LENGTH },
327        |length| quote! { Some(#length) },
328    );
329    let precision = config.precision.map_or_else(
330        || quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::DEFAULT_PRECISION },
331        |precision| quote! { Some(#precision) },
332    );
333    let scale = config.scale.map_or_else(
334        || quote! { <#field_ty as ::sql_orm::core::SqlTypeMapping>::DEFAULT_SCALE },
335        |scale| quote! { Some(#scale) },
336    );
337
338    Ok(quote! {
339        impl ::sql_orm::core::EntityPolicy for #ident {
340            const POLICY_NAME: &'static str = "tenant";
341            const COLUMN_NAMES: &'static [&'static str] = &[#column_name];
342
343            fn columns() -> &'static [::sql_orm::core::ColumnMetadata] {
344                const COLUMNS: &[::sql_orm::core::ColumnMetadata] = &[
345                    ::sql_orm::core::ColumnMetadata {
346                        rust_field: #rust_field,
347                        column_name: #column_name,
348                        renamed_from: #renamed_from,
349                        sql_type: #sql_type,
350                        nullable: false,
351                        primary_key: false,
352                        identity: None,
353                        default_sql: None,
354                        computed_sql: None,
355                        rowversion: false,
356                        insertable: true,
357                        updatable: false,
358                        max_length: #max_length,
359                        precision: #precision,
360                        scale: #scale,
361                    }
362                ];
363
364                COLUMNS
365            }
366        }
367
368        impl ::sql_orm::TenantContext for #ident {
369            const COLUMN_NAME: &'static str = #column_name;
370
371            fn tenant_value(&self) -> ::sql_orm::core::SqlValue {
372                <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
373                    ::core::clone::Clone::clone(&self.#field_ident)
374                )
375            }
376        }
377    })
378}
379
380fn derive_from_row_impl(input: DeriveInput) -> Result<TokenStream2> {
381    let ident = input.ident;
382    let fields = extract_named_fields(&ident, input.data, "FromRow")?;
383    let generics = input.generics;
384    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
385
386    let field_initializers = fields
387        .iter()
388        .map(|field| {
389            let field_ident = field
390                .ident
391                .as_ref()
392                .ok_or_else(|| Error::new_spanned(field, "FromRow requiere campos nombrados"))?;
393            let config = parse_persistence_field_config(field, "FromRow")?;
394            let column_name = config
395                .column
396                .unwrap_or_else(|| LitStr::new(&field_ident.to_string(), field_ident.span()));
397            validate_non_empty_lit_str(&column_name, "column no puede estar vacío")?;
398
399            let field_ty = &field.ty;
400            let type_info = analyze_type(field_ty)?;
401            let value = if type_info.nullable {
402                quote! {
403                    row.try_get_typed::<#field_ty>(#column_name)?.flatten()
404                }
405            } else {
406                quote! {
407                    row.get_required_typed::<#field_ty>(#column_name)?
408                }
409            };
410
411            Ok(quote! {
412                #field_ident: #value
413            })
414        })
415        .collect::<Result<Vec<_>>>()?;
416
417    Ok(quote! {
418        impl #impl_generics ::sql_orm::core::FromRow for #ident #ty_generics #where_clause {
419            fn from_row<R: ::sql_orm::core::Row>(row: &R) -> Result<Self, ::sql_orm::core::OrmError> {
420                Ok(Self {
421                    #(#field_initializers),*
422                })
423            }
424        }
425    })
426}
427
428fn derive_entity_impl(input: DeriveInput) -> Result<TokenStream2> {
429    let ident = input.ident;
430    let EntityConfig {
431        table: entity_table,
432        schema: entity_schema,
433        renamed_from: entity_renamed_from,
434        indexes: entity_indexes,
435        audit: entity_audit,
436        soft_delete: entity_soft_delete,
437        tenant: entity_tenant,
438    } = parse_entity_config(&input.attrs)?;
439    let fields = match input.data {
440        Data::Struct(data) => match data.fields {
441            Fields::Named(fields) => fields.named,
442            _ => {
443                return Err(Error::new_spanned(
444                    ident,
445                    "Entity solo soporta structs con campos nombrados",
446                ));
447            }
448        },
449        _ => return Err(Error::new_spanned(ident, "Entity solo soporta structs")),
450    };
451
452    let schema = entity_schema.unwrap_or_else(|| LitStr::new("dbo", Span::call_site()));
453    let table =
454        entity_table.unwrap_or_else(|| LitStr::new(&default_table_name(&ident), ident.span()));
455    let renamed_from = option_lit_str(entity_renamed_from);
456    let rust_name = LitStr::new(&ident.to_string(), ident.span());
457
458    let mut columns = Vec::new();
459    let mut column_symbols = Vec::new();
460    let mut primary_key_columns = Vec::new();
461    let mut primary_key_value_expr = None;
462    let mut persist_mode_expr = None;
463    let mut insert_values = Vec::new();
464    let mut update_changes = Vec::new();
465    let mut entity_concurrency_token = None;
466    let mut sync_fields = Vec::new();
467    let mut from_row_fields = Vec::new();
468    let mut indexes = Vec::new();
469    let mut foreign_keys = Vec::new();
470    let mut foreign_key_accessors = Vec::new();
471    let mut field_foreign_keys = BTreeMap::<String, FieldForeignKeyInfo>::new();
472    let mut navigations = Vec::new();
473    let mut field_columns = BTreeMap::<String, LitStr>::new();
474    let mut entity_column_names = Vec::new();
475
476    let has_explicit_primary_key = has_explicit_primary_key(&fields)?;
477
478    for field in fields.iter() {
479        let field_ident = field
480            .ident
481            .as_ref()
482            .ok_or_else(|| Error::new_spanned(field, "Entity requiere campos nombrados"))?;
483        let config = parse_field_config(field)?;
484        let rust_field = LitStr::new(&field_ident.to_string(), field_ident.span());
485
486        if let Some(navigation) = config.navigation {
487            let wrapper = validate_navigation_field_type(&field.ty, &navigation)?;
488            let target = navigation.target.clone();
489            let target_rust_name = LitStr::new(
490                &path_last_ident(&target)
491                    .map(|ident| ident.to_string())
492                    .unwrap_or_else(|| quote! { #target }.to_string()),
493                target.span(),
494            );
495            let kind = navigation_kind_tokens(navigation.kind);
496            let foreign_key_field = navigation.foreign_key.clone();
497            let foreign_key_field_name = foreign_key_field.to_string();
498
499            from_row_fields.push(quote! {
500                #field_ident: ::core::default::Default::default()
501            });
502            sync_fields.push(quote! {
503                self.#field_ident = persisted.#field_ident;
504            });
505
506            navigations.push(PendingNavigation {
507                rust_field,
508                kind: navigation.kind,
509                kind_tokens: kind,
510                wrapper,
511                target,
512                target_rust_name,
513                foreign_key_field,
514                foreign_key_field_name,
515            });
516
517            continue;
518        }
519
520        let column_name = config
521            .column
522            .unwrap_or_else(|| LitStr::new(&field_ident.to_string(), field_ident.span()));
523        entity_column_names.push(column_name.clone());
524        field_columns.insert(field_ident.to_string(), column_name.clone());
525        let type_info = analyze_type(&field.ty)?;
526
527        let primary_key = config.primary_key
528            || (field_ident == &Ident::new("id", field_ident.span()) && !has_explicit_primary_key);
529        if primary_key {
530            primary_key_columns.push(column_name.clone());
531            let field_ty = &field.ty;
532            if primary_key_value_expr.is_none() {
533                primary_key_value_expr = Some(quote! {
534                    Ok(<#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
535                        ::core::clone::Clone::clone(&self.#field_ident)
536                    ))
537                });
538            }
539
540            if persist_mode_expr.is_none() {
541                let identity_strategy = if config.identity {
542                    match type_info.kind {
543                        TypeKind::I64 | TypeKind::I32 | TypeKind::I16 | TypeKind::U8 => {
544                            Some(quote! {
545                                if self.#field_ident == 0 {
546                                    Ok(::sql_orm::EntityPersistMode::Insert)
547                                } else {
548                                    Ok(::sql_orm::EntityPersistMode::Update(
549                                        <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
550                                            ::core::clone::Clone::clone(&self.#field_ident)
551                                        )
552                                    ))
553                                }
554                            })
555                        }
556                        _ => None,
557                    }
558                } else {
559                    None
560                };
561
562                persist_mode_expr = Some(identity_strategy.unwrap_or_else(|| {
563                    quote! {
564                        Ok(::sql_orm::EntityPersistMode::InsertOrUpdate(
565                            <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
566                                ::core::clone::Clone::clone(&self.#field_ident)
567                            )
568                        ))
569                    }
570                }));
571            }
572        }
573
574        let sql_type = match (config.sql_type, config.unsafe_sql_type) {
575            (Some(sql_type), None) => sql_type_from_string(&sql_type)?,
576            (None, Some(sql_type)) => unsafe_sql_type_from_string(&sql_type),
577            (Some(sql_type), Some(_)) => {
578                return Err(Error::new_spanned(
579                    sql_type,
580                    "sql_type y unsafe_sql_type no pueden usarse juntos",
581                ));
582            }
583            (None, None) => infer_sql_type(&type_info, config.rowversion, &field.ty)?,
584        };
585
586        if config.identity && !type_info.is_integer {
587            return Err(Error::new_spanned(
588                &field.ty,
589                "identity solo se soporta sobre tipos enteros",
590            ));
591        }
592
593        if config.rowversion && !type_info.is_vec_u8 {
594            return Err(Error::new_spanned(&field.ty, "rowversion requiere Vec<u8>"));
595        }
596
597        let nullable = config.nullable || type_info.nullable;
598        let identity = if config.identity {
599            let seed = config.identity_seed.unwrap_or(1);
600            let increment = config.identity_increment.unwrap_or(1);
601            quote! {
602                Some(::sql_orm::core::IdentityMetadata::new(#seed, #increment))
603            }
604        } else {
605            quote! { None }
606        };
607
608        let max_length = config
609            .length
610            .or_else(|| type_info.default_max_length.filter(|_| !config.rowversion));
611        let precision = config.precision.or(type_info.default_precision);
612        let scale = config.scale.or(type_info.default_scale);
613        let default_sql = option_lit_str(config.default_sql);
614        let renamed_from = option_lit_str(config.renamed_from);
615        let has_computed_sql = config.computed_sql.is_some();
616        let computed_sql = option_lit_str(config.computed_sql);
617        let max_length = option_number(max_length);
618        let precision = option_number(precision);
619        let scale = option_number(scale);
620        let rowversion = config.rowversion;
621        let insertable = !config.identity && !rowversion && !has_computed_sql;
622        let updatable = !primary_key && !rowversion && !has_computed_sql;
623
624        columns.push(quote! {
625            ::sql_orm::core::ColumnMetadata {
626                rust_field: #rust_field,
627                column_name: #column_name,
628                renamed_from: #renamed_from,
629                sql_type: #sql_type,
630                nullable: #nullable,
631                primary_key: #primary_key,
632                identity: #identity,
633                default_sql: #default_sql,
634                computed_sql: #computed_sql,
635                rowversion: #rowversion,
636                insertable: #insertable,
637                updatable: #updatable,
638                max_length: #max_length,
639                precision: #precision,
640                scale: #scale,
641            }
642        });
643
644        column_symbols.push(quote! {
645            pub const #field_ident: ::sql_orm::core::EntityColumn<#ident> =
646                ::sql_orm::core::EntityColumn::new(#rust_field, #column_name);
647        });
648
649        if insertable {
650            let field_ty = &field.ty;
651            insert_values.push(quote! {
652                values.push(::sql_orm::core::ColumnValue::new(
653                    #column_name,
654                    <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
655                        ::core::clone::Clone::clone(&self.#field_ident)
656                    ),
657                ));
658            });
659        }
660
661        if updatable {
662            let field_ty = &field.ty;
663            update_changes.push(quote! {
664                changes.push(::sql_orm::core::ColumnValue::new(
665                    #column_name,
666                    <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
667                        ::core::clone::Clone::clone(&self.#field_ident)
668                    ),
669                ));
670            });
671        }
672
673        if rowversion {
674            let field_ty = &field.ty;
675            entity_concurrency_token = Some(quote! {
676                Ok(Some(
677                    <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
678                        ::core::clone::Clone::clone(&self.#field_ident)
679                    )
680                ))
681            });
682        }
683
684        sync_fields.push(quote! {
685            self.#field_ident = persisted.#field_ident;
686        });
687
688        let field_ty = &field.ty;
689        let from_row_value = if type_info.nullable {
690            quote! {
691                row.try_get_typed::<#field_ty>(#column_name)?.flatten()
692            }
693        } else {
694            quote! {
695                row.get_required_typed::<#field_ty>(#column_name)?
696            }
697        };
698
699        from_row_fields.push(quote! {
700            #field_ident: #from_row_value
701        });
702
703        for index in config.indexes {
704            let index_name = index.name.unwrap_or_else(|| {
705                generated_index_name(
706                    if index.unique { "ux" } else { "ix" },
707                    table.value().as_str(),
708                    column_name.value().as_str(),
709                    field_ident.span(),
710                )
711            });
712            let unique = index.unique;
713
714            indexes.push(quote! {
715                ::sql_orm::core::IndexMetadata {
716                    name: #index_name,
717                    columns: {
718                        const COLUMNS: &[::sql_orm::core::IndexColumnMetadata] =
719                            &[::sql_orm::core::IndexColumnMetadata::asc(#column_name)];
720                        COLUMNS
721                    },
722                    unique: #unique,
723                }
724            });
725        }
726
727        if let Some(foreign_key) = config.foreign_key {
728            let foreign_key_has_explicit_name = foreign_key.name.is_some();
729            let foreign_key_name = foreign_key.name.clone().unwrap_or_else(|| {
730                generated_foreign_key_name(
731                    table.value().as_str(),
732                    column_name.value().as_str(),
733                    foreign_key.generated_referenced_table_name.as_str(),
734                    field_ident.span(),
735                )
736            });
737            let referenced_schema = foreign_key.referenced_schema_tokens();
738            let referenced_table = foreign_key.referenced_table_tokens();
739            let referenced_column = foreign_key.referenced_column_tokens();
740            let on_delete = config
741                .on_delete
742                .unwrap_or(ReferentialActionConfig::NoAction);
743
744            if on_delete == ReferentialActionConfig::SetNull && !nullable {
745                return Err(Error::new_spanned(
746                    &field.ty,
747                    "on_delete = \"set null\" requiere un campo nullable",
748                ));
749            }
750
751            let on_delete = referential_action_tokens(on_delete);
752
753            foreign_keys.push(quote! {
754                ::sql_orm::core::ForeignKeyMetadata::new(
755                    #foreign_key_name,
756                    {
757                        const COLUMNS: &[&'static str] = &[#column_name];
758                        COLUMNS
759                    },
760                    #referenced_schema,
761                    #referenced_table,
762                    {
763                        const REFERENCED_COLUMNS: &[&'static str] = &[#referenced_column];
764                        REFERENCED_COLUMNS
765                    },
766                    #on_delete,
767                    ::sql_orm::core::ReferentialAction::NoAction,
768                )
769            });
770
771            let foreign_key_accessor = format_ident!("__sql_orm_fk_{}", field_ident);
772            foreign_key_accessors.push(quote! {
773                #[doc(hidden)]
774                pub fn #foreign_key_accessor() -> &'static ::sql_orm::core::ForeignKeyMetadata {
775                    <Self as ::sql_orm::core::Entity>::metadata()
776                        .foreign_key(#foreign_key_name)
777                        .expect("generated foreign key accessor must reference existing metadata")
778                }
779            });
780
781            field_foreign_keys.insert(
782                field_ident.to_string(),
783                FieldForeignKeyInfo {
784                    name: foreign_key_name,
785                    local_column: column_name.clone(),
786                    referenced_column,
787                    field_span: field_ident.span(),
788                    has_explicit_name: foreign_key_has_explicit_name,
789                    structured_target: foreign_key.structured_target_key(),
790                },
791            );
792        }
793    }
794
795    validate_repeated_structured_foreign_keys(&field_foreign_keys)?;
796
797    let navigation_metadata = navigations
798        .iter()
799        .map(|navigation| {
800            let rust_field = &navigation.rust_field;
801            let kind_tokens = &navigation.kind_tokens;
802            let target = &navigation.target;
803            let target_rust_name = &navigation.target_rust_name;
804            let target_schema = quote! { #target::__SQL_ORM_ENTITY_SCHEMA };
805            let target_table = quote! { #target::__SQL_ORM_ENTITY_TABLE };
806
807            match navigation.kind {
808                NavigationKindConfig::BelongsTo => {
809                    let foreign_key = field_foreign_keys
810                        .get(&navigation.foreign_key_field_name)
811                        .ok_or_else(|| {
812                            Error::new(
813                                navigation.foreign_key_field.span(),
814                                "belongs_to requiere foreign_key = campo_con_foreign_key existente",
815                            )
816                        })?;
817                    let local_column = &foreign_key.local_column;
818                    let referenced_column = &foreign_key.referenced_column;
819                    let foreign_key_name = &foreign_key.name;
820                    let navigation_target = path_key(&navigation.target);
821
822                    let foreign_key_target = foreign_key.structured_target.as_ref().ok_or_else(|| {
823                        Error::new(
824                            navigation.foreign_key_field.span(),
825                            "belongs_to requiere que foreign_key apunte a una foreign key estructurada: #[orm(foreign_key(entity = Target, column = id))]",
826                        )
827                    })?;
828
829                    if foreign_key_target != &navigation_target {
830                        return Err(Error::new(
831                            navigation.foreign_key_field.span(),
832                            "belongs_to requiere que el target coincida con la entidad declarada en foreign_key(entity = ...)",
833                        ));
834                    }
835
836                    Ok(quote! {
837                        ::sql_orm::core::NavigationMetadata::new(
838                            #rust_field,
839                            #kind_tokens,
840                            #target_rust_name,
841                            #target_schema,
842                            #target_table,
843                            {
844                                const LOCAL_COLUMNS: &[&'static str] = &[#local_column];
845                                LOCAL_COLUMNS
846                            },
847                            {
848                                const TARGET_COLUMNS: &[&'static str] = &[#referenced_column];
849                                TARGET_COLUMNS
850                            },
851                            Some(#foreign_key_name),
852                        )
853                    })
854                }
855                NavigationKindConfig::HasOne | NavigationKindConfig::HasMany => {
856                    let foreign_key_field = &navigation.foreign_key_field;
857                    let foreign_key_accessor =
858                        format_ident!("__sql_orm_fk_{}", foreign_key_field);
859                    Ok(quote! {
860                        {
861                            let foreign_key = #target::#foreign_key_accessor();
862                            assert!(
863                                foreign_key.references_table(#schema, #table),
864                                "has_one/has_many requiere que foreign_key apunte a la entidad local",
865                            );
866
867                            ::sql_orm::core::NavigationMetadata::new(
868                            #rust_field,
869                            #kind_tokens,
870                            #target_rust_name,
871                            #target_schema,
872                            #target_table,
873                            {
874                                foreign_key.referenced_columns
875                            },
876                            {
877                                foreign_key.columns
878                            },
879                            Some(foreign_key.name),
880                            )
881                        }
882                    })
883                }
884            }
885        })
886        .collect::<Result<Vec<_>>>()?;
887
888    for index in entity_indexes {
889        let resolved_columns = index
890            .columns
891            .iter()
892            .map(|column| {
893                field_columns
894                    .get(&column.to_string())
895                    .cloned()
896                    .ok_or_else(|| {
897                        Error::new_spanned(
898                            column,
899                            "index compuesto referencia un campo inexistente en la entidad",
900                        )
901                    })
902            })
903            .collect::<Result<Vec<_>>>()?;
904        let generated_suffix = resolved_columns
905            .iter()
906            .map(LitStr::value)
907            .collect::<Vec<_>>()
908            .join("_");
909        let index_name = index.name.unwrap_or_else(|| {
910            generated_index_name(
911                if index.unique { "ux" } else { "ix" },
912                table.value().as_str(),
913                generated_suffix.as_str(),
914                index.columns[0].span(),
915            )
916        });
917        let unique = index.unique;
918
919        indexes.push(quote! {
920            ::sql_orm::core::IndexMetadata {
921                name: #index_name,
922                columns: {
923                    const COLUMNS: &[::sql_orm::core::IndexColumnMetadata] =
924                        &[#(::sql_orm::core::IndexColumnMetadata::asc(#resolved_columns)),*];
925                    COLUMNS
926                },
927                unique: #unique,
928            }
929        });
930    }
931
932    if primary_key_columns.is_empty() {
933        return Err(Error::new_spanned(
934            ident,
935            "Entity requiere al menos una primary key",
936        ));
937    }
938
939    let metadata_ident = Ident::new(
940        &format!("__SQL_ORM_ENTITY_METADATA_FOR_{}", ident),
941        Span::call_site(),
942    );
943    let primary_key_value_impl = if primary_key_columns.len() == 1 {
944        primary_key_value_expr.expect("single primary key must produce key extraction")
945    } else {
946        quote! {
947            Err(::sql_orm::core::OrmError::compile(
948                "ActiveRecord currently supports delete only for entities with a single primary key column",
949            ))
950        }
951    };
952    let persist_mode_impl = if primary_key_columns.len() == 1 {
953        persist_mode_expr.expect("single primary key must produce save strategy")
954    } else {
955        quote! {
956            Err(::sql_orm::core::OrmError::compile(
957                "ActiveRecord currently supports save only for entities with a single primary key column",
958            ))
959        }
960    };
961    let entity_concurrency_token_impl = entity_concurrency_token.unwrap_or_else(|| {
962        quote! {
963            Ok(None)
964        }
965    });
966    let audit_collision_checks = entity_audit
967        .as_ref()
968        .map(|audit| {
969            entity_column_names
970                .iter()
971                .map(|column_name| {
972                    quote! {
973                        const _: () = assert!(
974                            !::sql_orm::core::column_name_exists(
975                                <#audit as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES,
976                                #column_name,
977                            ),
978                            concat!(
979                                "audit policy column `",
980                                #column_name,
981                                "` collides with an entity column; rename the entity field with #[orm(column = \"...\")] or the AuditFields field with #[orm(column = \"...\")]",
982                            ),
983                        );
984                    }
985                })
986                .collect::<Vec<_>>()
987        })
988        .unwrap_or_default();
989    let soft_delete_collision_checks = entity_soft_delete
990        .as_ref()
991        .map(|soft_delete| {
992            entity_column_names
993                .iter()
994                .map(|column_name| {
995                    quote! {
996                        const _: () = assert!(
997                            !::sql_orm::core::column_name_exists(
998                                <#soft_delete as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES,
999                                #column_name,
1000                            ),
1001                            concat!(
1002                                "soft_delete policy column `",
1003                                #column_name,
1004                                "` collides with an entity column; rename the entity field with #[orm(column = \"...\")] or the soft delete policy field with #[orm(column = \"...\")]",
1005                            ),
1006                        );
1007                    }
1008                })
1009                .collect::<Vec<_>>()
1010        })
1011        .unwrap_or_default();
1012    let tenant_collision_checks = entity_tenant
1013        .as_ref()
1014        .map(|tenant| {
1015            entity_column_names
1016                .iter()
1017                .map(|column_name| {
1018                    quote! {
1019                        const _: () = assert!(
1020                            !::sql_orm::core::column_name_exists(
1021                                <#tenant as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES,
1022                                #column_name,
1023                            ),
1024                            concat!(
1025                                "tenant policy column `",
1026                                #column_name,
1027                                "` collides with an entity column; rename the entity field with #[orm(column = \"...\")] or the TenantContext field with #[orm(column = \"...\")]",
1028                            ),
1029                        );
1030                    }
1031                })
1032                .collect::<Vec<_>>()
1033        })
1034        .unwrap_or_default();
1035    let audit_soft_delete_collision_checks = match (&entity_audit, &entity_soft_delete) {
1036        (Some(audit), Some(soft_delete)) => {
1037            quote! {
1038                const _: () = {
1039                    let soft_delete_columns =
1040                        <#soft_delete as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES;
1041                    let audit_columns = <#audit as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES;
1042                    let mut index = 0;
1043                    while index < soft_delete_columns.len() {
1044                        assert!(
1045                            !::sql_orm::core::column_name_exists(
1046                                audit_columns,
1047                                soft_delete_columns[index],
1048                            ),
1049                            "soft_delete policy columns collide with audit policy columns; rename one of the generated columns explicitly",
1050                        );
1051                        index += 1;
1052                    }
1053                };
1054            }
1055        }
1056        _ => quote! {},
1057    };
1058    let tenant_policy_collision_checks = {
1059        let mut checks = Vec::new();
1060
1061        if let (Some(audit), Some(tenant)) = (&entity_audit, &entity_tenant) {
1062            checks.push(quote! {
1063                const _: () = {
1064                    let tenant_columns =
1065                        <#tenant as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES;
1066                    let audit_columns = <#audit as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES;
1067                    let mut index = 0;
1068                    while index < tenant_columns.len() {
1069                        assert!(
1070                            !::sql_orm::core::column_name_exists(
1071                                audit_columns,
1072                                tenant_columns[index],
1073                            ),
1074                            "tenant policy columns collide with audit policy columns; rename one of the generated columns explicitly",
1075                        );
1076                        index += 1;
1077                    }
1078                };
1079            });
1080        }
1081
1082        if let (Some(soft_delete), Some(tenant)) = (&entity_soft_delete, &entity_tenant) {
1083            checks.push(quote! {
1084                const _: () = {
1085                    let tenant_columns =
1086                        <#tenant as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES;
1087                    let soft_delete_columns =
1088                        <#soft_delete as ::sql_orm::core::EntityPolicy>::COLUMN_NAMES;
1089                    let mut index = 0;
1090                    while index < tenant_columns.len() {
1091                        assert!(
1092                            !::sql_orm::core::column_name_exists(
1093                                soft_delete_columns,
1094                                tenant_columns[index],
1095                            ),
1096                            "tenant policy columns collide with soft_delete policy columns; rename one of the generated columns explicitly",
1097                        );
1098                        index += 1;
1099                    }
1100                };
1101            });
1102        }
1103
1104        checks
1105    };
1106    let tenant_context_bound_check = entity_tenant.as_ref().map(|tenant| {
1107        quote! {
1108            const _: &'static str = <#tenant as ::sql_orm::TenantContext>::COLUMN_NAME;
1109        }
1110    });
1111    let primary_key_metadata = quote! {
1112        ::sql_orm::core::PrimaryKeyMetadata::new(
1113            None,
1114            {
1115                const COLUMNS: &[&'static str] = &[#(#primary_key_columns),*];
1116                COLUMNS
1117            },
1118        )
1119    };
1120    let indexes_metadata = quote! {
1121        {
1122            const INDEXES: &[::sql_orm::core::IndexMetadata] = &[#(#indexes),*];
1123            INDEXES
1124        }
1125    };
1126    let foreign_keys_metadata = quote! {
1127        {
1128            const FOREIGN_KEYS: &[::sql_orm::core::ForeignKeyMetadata] = &[#(#foreign_keys),*];
1129            FOREIGN_KEYS
1130        }
1131    };
1132    let navigations_metadata = quote! {
1133        {
1134            const NAVIGATIONS: &[::sql_orm::core::NavigationMetadata] =
1135                &[#(#navigation_metadata),*];
1136            NAVIGATIONS
1137        }
1138    };
1139
1140    let has_inverse_navigation_metadata = navigations.iter().any(|navigation| {
1141        matches!(
1142            navigation.kind,
1143            NavigationKindConfig::HasOne | NavigationKindConfig::HasMany
1144        )
1145    });
1146    let has_generated_policies =
1147        entity_audit.is_some() || entity_soft_delete.is_some() || entity_tenant.is_some();
1148    let audit_columns_extend = entity_audit.as_ref().map(|audit| {
1149        quote! {
1150            columns.extend_from_slice(
1151                <#audit as ::sql_orm::core::EntityPolicy>::columns()
1152            );
1153        }
1154    });
1155    let soft_delete_columns_extend = entity_soft_delete.as_ref().map(|soft_delete| {
1156        quote! {
1157            columns.extend_from_slice(
1158                <#soft_delete as ::sql_orm::core::EntityPolicy>::columns()
1159            );
1160        }
1161    });
1162    let tenant_columns_extend = entity_tenant.as_ref().map(|tenant| {
1163        quote! {
1164            columns.extend_from_slice(
1165                <#tenant as ::sql_orm::core::EntityPolicy>::columns()
1166            );
1167        }
1168    });
1169    let audit_contract_impl = entity_audit.as_ref().map_or_else(
1170        || {
1171            quote! {
1172                impl ::sql_orm::AuditEntity for #ident {
1173                    fn audit_policy() -> Option<::sql_orm::core::EntityPolicyMetadata> {
1174                        None
1175                    }
1176                }
1177            }
1178        },
1179        |audit| {
1180            quote! {
1181                impl ::sql_orm::AuditEntity for #ident {
1182                    fn audit_policy() -> Option<::sql_orm::core::EntityPolicyMetadata> {
1183                        Some(<#audit as ::sql_orm::core::EntityPolicy>::metadata())
1184                    }
1185                }
1186            }
1187        },
1188    );
1189    let soft_delete_contract_impl = entity_soft_delete.as_ref().map_or_else(
1190        || {
1191            quote! {
1192                impl ::sql_orm::SoftDeleteEntity for #ident {
1193                    fn soft_delete_policy() -> Option<::sql_orm::core::EntityPolicyMetadata> {
1194                        None
1195                    }
1196                }
1197            }
1198        },
1199        |soft_delete| {
1200            quote! {
1201                impl ::sql_orm::SoftDeleteEntity for #ident {
1202                    fn soft_delete_policy() -> Option<::sql_orm::core::EntityPolicyMetadata> {
1203                        Some(<#soft_delete as ::sql_orm::core::EntityPolicy>::metadata())
1204                    }
1205                }
1206            }
1207        },
1208    );
1209    let tenant_contract_impl = entity_tenant.as_ref().map_or_else(
1210        || {
1211            quote! {
1212                impl ::sql_orm::TenantScopedEntity for #ident {
1213                    fn tenant_policy() -> Option<::sql_orm::core::EntityPolicyMetadata> {
1214                        None
1215                    }
1216                }
1217            }
1218        },
1219        |tenant| {
1220            quote! {
1221                impl ::sql_orm::TenantScopedEntity for #ident {
1222                    fn tenant_policy() -> Option<::sql_orm::core::EntityPolicyMetadata> {
1223                        Some(<#tenant as ::sql_orm::core::EntityPolicy>::metadata())
1224                    }
1225                }
1226            }
1227        },
1228    );
1229    let include_navigation_impls = include_navigation_impls(&ident, &navigations)?;
1230    let include_collection_impls = include_collection_impls(&ident, &navigations)?;
1231
1232    let (metadata_static, metadata_expr) = if has_generated_policies
1233        || has_inverse_navigation_metadata
1234    {
1235        (
1236            quote! {
1237                static #metadata_ident: ::std::sync::OnceLock<::sql_orm::core::EntityMetadata> =
1238                    ::std::sync::OnceLock::new();
1239            },
1240            quote! {
1241                #metadata_ident.get_or_init(|| {
1242                    let mut columns = ::std::vec::Vec::new();
1243                    columns.extend_from_slice(&[#(#columns),*]);
1244                    #audit_columns_extend
1245                    #soft_delete_columns_extend
1246                    #tenant_columns_extend
1247                    let columns: &'static [::sql_orm::core::ColumnMetadata] =
1248                        ::std::boxed::Box::leak(columns.into_boxed_slice());
1249                    let navigations: &'static [::sql_orm::core::NavigationMetadata] =
1250                        ::std::boxed::Box::leak(::std::vec![#(#navigation_metadata),*].into_boxed_slice());
1251
1252                    ::sql_orm::core::EntityMetadata {
1253                        rust_name: #rust_name,
1254                        schema: #schema,
1255                        table: #table,
1256                        renamed_from: #renamed_from,
1257                        columns,
1258                        primary_key: #primary_key_metadata,
1259                        indexes: #indexes_metadata,
1260                        foreign_keys: #foreign_keys_metadata,
1261                        navigations,
1262                    }
1263                })
1264            },
1265        )
1266    } else {
1267        (
1268            quote! {
1269                static #metadata_ident: ::sql_orm::core::EntityMetadata =
1270                    ::sql_orm::core::EntityMetadata {
1271                        rust_name: #rust_name,
1272                        schema: #schema,
1273                        table: #table,
1274                        renamed_from: #renamed_from,
1275                        columns: &[#(#columns),*],
1276                        primary_key: #primary_key_metadata,
1277                        indexes: #indexes_metadata,
1278                        foreign_keys: #foreign_keys_metadata,
1279                        navigations: #navigations_metadata,
1280                    };
1281            },
1282            quote! {
1283                &#metadata_ident
1284            },
1285        )
1286    };
1287
1288    Ok(quote! {
1289        #(#audit_collision_checks)*
1290        #(#soft_delete_collision_checks)*
1291        #(#tenant_collision_checks)*
1292        #audit_soft_delete_collision_checks
1293        #(#tenant_policy_collision_checks)*
1294        #tenant_context_bound_check
1295
1296        #metadata_static
1297
1298        #[allow(non_upper_case_globals)]
1299        impl #ident {
1300            #[doc(hidden)]
1301            pub const __SQL_ORM_ENTITY_SCHEMA: &'static str = #schema;
1302
1303            #[doc(hidden)]
1304            pub const __SQL_ORM_ENTITY_TABLE: &'static str = #table;
1305
1306            #(#column_symbols)*
1307            #(#foreign_key_accessors)*
1308        }
1309
1310        impl ::sql_orm::core::Entity for #ident {
1311            fn metadata() -> &'static ::sql_orm::core::EntityMetadata {
1312                #metadata_expr
1313            }
1314        }
1315
1316        impl ::sql_orm::core::FromRow for #ident {
1317            fn from_row<R: ::sql_orm::core::Row>(row: &R) -> Result<Self, ::sql_orm::core::OrmError> {
1318                Ok(Self {
1319                    #(#from_row_fields),*
1320                })
1321            }
1322        }
1323
1324        impl ::sql_orm::EntityPrimaryKey for #ident {
1325            fn primary_key_value(&self) -> Result<::sql_orm::core::SqlValue, ::sql_orm::core::OrmError> {
1326                #primary_key_value_impl
1327            }
1328        }
1329
1330        impl ::sql_orm::EntityPersist for #ident {
1331            fn persist_mode(&self) -> Result<::sql_orm::EntityPersistMode, ::sql_orm::core::OrmError> {
1332                #persist_mode_impl
1333            }
1334
1335            fn insert_values(&self) -> ::std::vec::Vec<::sql_orm::core::ColumnValue> {
1336                let mut values = ::std::vec::Vec::new();
1337                #(#insert_values)*
1338                values
1339            }
1340
1341            fn update_changes(&self) -> ::std::vec::Vec<::sql_orm::core::ColumnValue> {
1342                let mut changes = ::std::vec::Vec::new();
1343                #(#update_changes)*
1344                changes
1345            }
1346
1347            fn concurrency_token(&self) -> Result<::core::option::Option<::sql_orm::core::SqlValue>, ::sql_orm::core::OrmError> {
1348                #entity_concurrency_token_impl
1349            }
1350
1351            fn sync_persisted(&mut self, persisted: Self) {
1352                #(#sync_fields)*
1353            }
1354        }
1355
1356        #audit_contract_impl
1357        #soft_delete_contract_impl
1358        #tenant_contract_impl
1359        #(#include_navigation_impls)*
1360        #(#include_collection_impls)*
1361    })
1362}
1363
1364fn derive_db_context_impl(input: DeriveInput) -> Result<TokenStream2> {
1365    let context_ident = input.ident.clone();
1366    let ident = input.ident;
1367    let fields = extract_named_fields(&ident, input.data, "DbContext")?;
1368    let shared_connection_field = fields
1369        .first()
1370        .and_then(|field| field.ident.as_ref())
1371        .ok_or_else(|| {
1372            Error::new_spanned(
1373                &ident,
1374                "DbContext requiere al menos un campo DbSet<Entidad>",
1375            )
1376        })?;
1377
1378    let mut seen_entities = std::collections::HashSet::new();
1379    for field in &fields {
1380        let entity_type = dbset_entity_type(&field.ty).ok_or_else(|| {
1381            Error::new_spanned(
1382                &field.ty,
1383                "DbContext requiere campos con tipo DbSet<Entidad>",
1384            )
1385        })?;
1386        let entity_key = quote! { #entity_type }.to_string();
1387        if !seen_entities.insert(entity_key) {
1388            return Err(Error::new_spanned(
1389                &field.ty,
1390                "DbContext no soporta múltiples DbSet para la misma entidad",
1391            ));
1392        }
1393    }
1394
1395    let initializers = fields
1396        .iter()
1397        .map(|field| {
1398            let field_ident = field
1399                .ident
1400                .as_ref()
1401                .ok_or_else(|| Error::new_spanned(field, "DbContext requiere campos nombrados"))?;
1402            let entity_type = dbset_entity_type(&field.ty).ok_or_else(|| {
1403                Error::new_spanned(
1404                    &field.ty,
1405                    "DbContext requiere campos con tipo DbSet<Entidad>",
1406                )
1407            })?;
1408
1409            Ok(quote! {
1410                #field_ident: ::sql_orm::DbSet::<#entity_type>::with_tracking_registry(
1411                    connection.clone(),
1412                    ::std::sync::Arc::clone(&tracking_registry)
1413                )
1414            })
1415        })
1416        .collect::<Result<Vec<_>>>()?;
1417
1418    let dbset_access_impls = fields
1419        .iter()
1420        .map(|field| {
1421            let field_ident = field
1422                .ident
1423                .as_ref()
1424                .ok_or_else(|| Error::new_spanned(field, "DbContext requiere campos nombrados"))?;
1425            let entity_type = dbset_entity_type(&field.ty).ok_or_else(|| {
1426                Error::new_spanned(
1427                    &field.ty,
1428                    "DbContext requiere campos con tipo DbSet<Entidad>",
1429                )
1430            })?;
1431
1432            Ok(quote! {
1433                impl ::sql_orm::DbContextEntitySet<#entity_type> for #context_ident {
1434                    fn db_set(&self) -> &::sql_orm::DbSet<#entity_type> {
1435                        &self.#field_ident
1436                    }
1437                }
1438            })
1439        })
1440        .collect::<Result<Vec<_>>>()?;
1441
1442    let save_plan_entity_metadata = fields
1443        .iter()
1444        .map(|field| {
1445            let entity_type = dbset_entity_type(&field.ty).ok_or_else(|| {
1446                Error::new_spanned(
1447                    &field.ty,
1448                    "DbContext requiere campos con tipo DbSet<Entidad>",
1449                )
1450            })?;
1451
1452            Ok(quote! {
1453                <#entity_type as ::sql_orm::core::Entity>::metadata()
1454            })
1455        })
1456        .collect::<Result<Vec<_>>>()?;
1457
1458    let save_added_steps = fields
1459        .iter()
1460        .enumerate()
1461        .map(|(field_index, field)| {
1462            let field_ident = field
1463                .ident
1464                .as_ref()
1465                .ok_or_else(|| Error::new_spanned(field, "DbContext requiere campos nombrados"))?;
1466            Ok(quote! {
1467                #field_index => {
1468                    saved += self.#field_ident.save_tracked_added().await?;
1469                }
1470            })
1471        })
1472        .collect::<Result<Vec<_>>>()?;
1473
1474    let save_modified_steps = fields
1475        .iter()
1476        .enumerate()
1477        .map(|(field_index, field)| {
1478            let field_ident = field
1479                .ident
1480                .as_ref()
1481                .ok_or_else(|| Error::new_spanned(field, "DbContext requiere campos nombrados"))?;
1482            Ok(quote! {
1483                #field_index => {
1484                    saved += self.#field_ident.save_tracked_modified().await?;
1485                }
1486            })
1487        })
1488        .collect::<Result<Vec<_>>>()?;
1489
1490    let save_deleted_steps = fields
1491        .iter()
1492        .enumerate()
1493        .map(|(field_index, field)| {
1494            let field_ident = field
1495                .ident
1496                .as_ref()
1497                .ok_or_else(|| Error::new_spanned(field, "DbContext requiere campos nombrados"))?;
1498            Ok(quote! {
1499                #field_index => {
1500                    saved += self.#field_ident.save_tracked_deleted().await?;
1501                }
1502            })
1503        })
1504        .collect::<Result<Vec<_>>>()?;
1505
1506    let save_changes_bounds = fields
1507        .iter()
1508        .map(|field| {
1509            let entity_type = dbset_entity_type(&field.ty).ok_or_else(|| {
1510                Error::new_spanned(
1511                    &field.ty,
1512                    "DbContext requiere campos con tipo DbSet<Entidad>",
1513                )
1514            })?;
1515
1516            Ok(quote! {
1517                #entity_type: ::core::clone::Clone
1518                    + ::sql_orm::EntityPersist
1519                    + ::sql_orm::EntityPrimaryKey
1520                    + ::sql_orm::SoftDeleteEntity
1521                    + ::sql_orm::TenantScopedEntity
1522                    + ::sql_orm::core::FromRow
1523                    + ::core::marker::Send
1524            })
1525        })
1526        .collect::<Result<Vec<_>>>()?;
1527
1528    let migration_entity_metadata_static = Ident::new(
1529        &format!("__{}_MIGRATION_ENTITY_METADATA", ident),
1530        Span::call_site(),
1531    );
1532    let migration_entity_metadata = fields
1533        .iter()
1534        .map(|field| {
1535            let entity_type = dbset_entity_type(&field.ty).ok_or_else(|| {
1536                Error::new_spanned(
1537                    &field.ty,
1538                    "DbContext requiere campos con tipo DbSet<Entidad>",
1539                )
1540            })?;
1541
1542            Ok(quote! {
1543                <#entity_type as ::sql_orm::core::Entity>::metadata()
1544            })
1545        })
1546        .collect::<Result<Vec<_>>>()?;
1547
1548    Ok(quote! {
1549        impl #ident {
1550            fn __from_shared_parts(
1551                connection: ::sql_orm::SharedConnection,
1552                tracking_registry: ::sql_orm::TrackingRegistryHandle,
1553            ) -> Self {
1554                Self {
1555                    #(#initializers),*
1556                }
1557            }
1558        }
1559
1560        impl ::sql_orm::DbContext for #ident {
1561            fn from_shared_connection(connection: ::sql_orm::SharedConnection) -> Self {
1562                let tracking_registry =
1563                    ::std::sync::Arc::new(::sql_orm::TrackingRegistry::default());
1564                Self::__from_shared_parts(connection, tracking_registry)
1565            }
1566
1567            fn shared_connection(&self) -> ::sql_orm::SharedConnection {
1568                self.#shared_connection_field.shared_connection()
1569            }
1570
1571            fn tracking_registry(&self) -> ::sql_orm::TrackingRegistryHandle {
1572                self.#shared_connection_field.tracking_registry()
1573            }
1574        }
1575
1576        impl #ident {
1577            pub fn from_shared_connection(connection: ::sql_orm::SharedConnection) -> Self {
1578                <Self as ::sql_orm::DbContext>::from_shared_connection(connection)
1579            }
1580
1581            pub fn with_audit_provider(
1582                &self,
1583                provider: ::std::sync::Arc<dyn ::sql_orm::AuditProvider>,
1584            ) -> Self {
1585                let tracking_registry =
1586                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1587                let connection =
1588                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1589                        .with_audit_provider(provider);
1590                Self::__from_shared_parts(connection, tracking_registry)
1591            }
1592
1593            pub fn with_audit_request_values(
1594                &self,
1595                request_values: ::sql_orm::AuditRequestValues,
1596            ) -> Self {
1597                let tracking_registry =
1598                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1599                let connection =
1600                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1601                        .with_audit_request_values(request_values);
1602                Self::__from_shared_parts(connection, tracking_registry)
1603            }
1604
1605            pub fn with_audit_values<V>(&self, values: V) -> Self
1606            where
1607                V: ::sql_orm::AuditValues,
1608            {
1609                let tracking_registry =
1610                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1611                let connection =
1612                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1613                        .with_audit_values(values);
1614                Self::__from_shared_parts(connection, tracking_registry)
1615            }
1616
1617            pub fn clear_audit_request_values(&self) -> Self {
1618                let tracking_registry =
1619                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1620                let connection =
1621                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1622                        .clear_audit_request_values();
1623                Self::__from_shared_parts(connection, tracking_registry)
1624            }
1625
1626            pub fn with_soft_delete_provider(
1627                &self,
1628                provider: ::std::sync::Arc<dyn ::sql_orm::SoftDeleteProvider>,
1629            ) -> Self {
1630                let tracking_registry =
1631                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1632                let connection =
1633                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1634                        .with_soft_delete_provider(provider);
1635                Self::__from_shared_parts(connection, tracking_registry)
1636            }
1637
1638            pub fn with_soft_delete_request_values(
1639                &self,
1640                request_values: ::sql_orm::SoftDeleteRequestValues,
1641            ) -> Self {
1642                let tracking_registry =
1643                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1644                let connection =
1645                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1646                        .with_soft_delete_request_values(request_values);
1647                Self::__from_shared_parts(connection, tracking_registry)
1648            }
1649
1650            pub fn with_soft_delete_values<V>(&self, values: V) -> Self
1651            where
1652                V: ::sql_orm::SoftDeleteValues,
1653            {
1654                let tracking_registry =
1655                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1656                let connection =
1657                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1658                        .with_soft_delete_values(values);
1659                Self::__from_shared_parts(connection, tracking_registry)
1660            }
1661
1662            pub fn clear_soft_delete_request_values(&self) -> Self {
1663                let tracking_registry =
1664                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1665                let connection =
1666                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1667                        .clear_soft_delete_request_values();
1668                Self::__from_shared_parts(connection, tracking_registry)
1669            }
1670
1671            pub fn with_tenant<T>(&self, tenant: T) -> Self
1672            where
1673                T: ::sql_orm::TenantContext,
1674            {
1675                let tracking_registry =
1676                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1677                let connection =
1678                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1679                        .with_tenant(tenant);
1680                Self::__from_shared_parts(connection, tracking_registry)
1681            }
1682
1683            pub fn clear_tenant(&self) -> Self {
1684                let tracking_registry =
1685                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1686                let connection =
1687                    <Self as ::sql_orm::DbContext>::shared_connection(self)
1688                        .clear_tenant();
1689                Self::__from_shared_parts(connection, tracking_registry)
1690            }
1691
1692            pub fn from_connection(
1693                connection: ::sql_orm::tiberius::MssqlConnection<
1694                    ::sql_orm::tiberius::TokioConnectionStream
1695                >,
1696            ) -> Self {
1697                <Self as ::sql_orm::DbContext>::from_shared_connection(
1698                    ::sql_orm::SharedConnection::from_connection(connection)
1699                )
1700            }
1701
1702            #[cfg(feature = "pool-bb8")]
1703            pub fn from_pool(pool: ::sql_orm::MssqlPool) -> Self {
1704                <Self as ::sql_orm::DbContext>::from_shared_connection(
1705                    ::sql_orm::SharedConnection::from_pool(pool)
1706                )
1707            }
1708
1709            pub async fn connect(connection_string: &str) -> Result<Self, ::sql_orm::core::OrmError> {
1710                let connection = ::sql_orm::tiberius::MssqlConnection::connect(connection_string)
1711                    .await?;
1712                Ok(Self::from_connection(connection))
1713            }
1714
1715            pub async fn connect_with_options(
1716                connection_string: &str,
1717                options: ::sql_orm::MssqlOperationalOptions,
1718            ) -> Result<Self, ::sql_orm::core::OrmError> {
1719                let config = ::sql_orm::MssqlConnectionConfig::from_connection_string_with_options(
1720                    connection_string,
1721                    options,
1722                )?;
1723                Self::connect_with_config(config).await
1724            }
1725
1726            pub async fn connect_with_config(
1727                config: ::sql_orm::MssqlConnectionConfig,
1728            ) -> Result<Self, ::sql_orm::core::OrmError> {
1729                let connection =
1730                    ::sql_orm::tiberius::MssqlConnection::connect_with_config(config).await?;
1731                Ok(Self::from_connection(connection))
1732            }
1733
1734            pub async fn transaction<F, Fut, T>(&self, operation: F) -> Result<T, ::sql_orm::core::OrmError>
1735            where
1736                F: FnOnce(Self) -> Fut + Send,
1737                Fut: ::core::future::Future<Output = Result<T, ::sql_orm::core::OrmError>> + Send,
1738                T: Send,
1739            {
1740                let shared_connection =
1741                    <Self as ::sql_orm::DbContext>::shared_connection(self);
1742                let tracking_registry =
1743                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1744
1745                shared_connection.run_transaction(|transaction_connection| async move {
1746                    let transaction_context =
1747                        Self::__from_shared_parts(transaction_connection, tracking_registry);
1748                    operation(transaction_context).await
1749                }).await
1750            }
1751
1752            pub async fn health_check(&self) -> Result<(), ::sql_orm::core::OrmError> {
1753                <Self as ::sql_orm::DbContext>::health_check(self).await
1754            }
1755
1756            /// Clears every experimental tracking entry currently registered
1757            /// on this context.
1758            ///
1759            /// This does not execute SQL and does not reset values already
1760            /// held by `Tracked<T>` wrappers. It only detaches the current
1761            /// unit-of-work entries so later `save_changes()` calls ignore
1762            /// them.
1763            pub fn clear_tracker(&self) {
1764                <Self as ::sql_orm::DbContext>::clear_tracker(self)
1765            }
1766
1767            async fn __sql_orm_save_changes_without_transaction(&self) -> Result<usize, ::sql_orm::core::OrmError>
1768            where
1769                #(#save_changes_bounds,)*
1770            {
1771                let mut saved = 0usize;
1772                let save_plan = ::sql_orm::save_changes_operation_plan(&[
1773                    #(#save_plan_entity_metadata),*
1774                ])?;
1775
1776                for entity_index in save_plan.added_order() {
1777                    match *entity_index {
1778                        #(#save_added_steps)*
1779                        _ => {}
1780                    }
1781                }
1782
1783                for entity_index in save_plan.modified_order() {
1784                    match *entity_index {
1785                        #(#save_modified_steps)*
1786                        _ => {}
1787                    }
1788                }
1789
1790                for entity_index in save_plan.deleted_order() {
1791                    match *entity_index {
1792                        #(#save_deleted_steps)*
1793                        _ => {}
1794                    }
1795                }
1796
1797                Ok(saved)
1798            }
1799
1800            /// Persists currently registered experimental tracking entries.
1801            ///
1802            /// This method processes live `Tracked<T>` wrappers registered by
1803            /// this context in three deterministic phases: `Added`,
1804            /// `Modified`, then `Deleted`. For simple foreign keys declared
1805            /// between entities in the context, inserts and updates run
1806            /// principals before dependents and deletes run the reverse order.
1807            ///
1808            /// The implementation reuses the normal `DbSet` insert, update
1809            /// and delete paths, so tenant, audit, soft-delete and rowversion
1810            /// behavior remains centralized in the public persistence layer.
1811            /// It opens an internal transaction when no transaction is active
1812            /// and reuses an outer `db.transaction(...)` when one is already
1813            /// active.
1814            ///
1815            /// Tracking remains experimental in this release slice: dropping a
1816            /// wrapper detaches it, relationship graph mutations are not
1817            /// persisted, and composite primary keys are rejected before SQL
1818            /// for pending tracked operations.
1819            pub async fn save_changes(&self) -> Result<usize, ::sql_orm::core::OrmError>
1820            where
1821                #(#save_changes_bounds,)*
1822            {
1823                let shared_connection =
1824                    <Self as ::sql_orm::DbContext>::shared_connection(self);
1825
1826                if shared_connection.is_transaction_active() {
1827                    self.__sql_orm_save_changes_without_transaction().await
1828                } else {
1829                    let tracking_registry =
1830                        <Self as ::sql_orm::DbContext>::tracking_registry(self);
1831                    shared_connection.run_transaction(|transaction_connection| async move {
1832                        let transaction_context =
1833                            Self::__from_shared_parts(transaction_connection, tracking_registry);
1834                        transaction_context.__sql_orm_save_changes_without_transaction().await
1835                    }).await
1836                }
1837            }
1838        }
1839
1840        impl ::sql_orm::MigrationModelSource for #ident {
1841            fn entity_metadata() -> &'static [&'static ::sql_orm::EntityMetadata] {
1842                static #migration_entity_metadata_static:
1843                    ::std::sync::OnceLock<
1844                        ::std::boxed::Box<[&'static ::sql_orm::EntityMetadata]>
1845                    > = ::std::sync::OnceLock::new();
1846
1847                #migration_entity_metadata_static
1848                    .get_or_init(|| {
1849                        ::std::boxed::Box::new([#(#migration_entity_metadata),*])
1850                    })
1851                    .as_ref()
1852            }
1853        }
1854
1855        #(#dbset_access_impls)*
1856    })
1857}
1858
1859fn derive_insertable_impl(input: DeriveInput) -> Result<TokenStream2> {
1860    let ident = input.ident;
1861    let model_config = parse_persistence_model_config(&input.attrs, "Insertable")?;
1862    let entity = model_config
1863        .entity
1864        .as_ref()
1865        .expect("validated persistence model must include entity");
1866    let fields = extract_named_fields(&ident, input.data, "Insertable")?;
1867
1868    let values = fields
1869        .iter()
1870        .map(|field| {
1871            let field_ident = field
1872                .ident
1873                .as_ref()
1874                .ok_or_else(|| Error::new_spanned(field, "Insertable requiere campos nombrados"))?;
1875            let field_config = parse_persistence_field_config(field, "Insertable")?;
1876            let field_ty = &field.ty;
1877            let column_name =
1878                persistence_column_name_expr(entity, field_ident, field_config.column.as_ref());
1879
1880            Ok(quote! {
1881                ::sql_orm::core::ColumnValue::new(
1882                    #column_name,
1883                    <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
1884                        ::core::clone::Clone::clone(&self.#field_ident)
1885                    ),
1886                )
1887            })
1888        })
1889        .collect::<Result<Vec<_>>>()?;
1890
1891    Ok(quote! {
1892        impl ::sql_orm::core::Insertable<#entity> for #ident {
1893            fn values(&self) -> ::std::vec::Vec<::sql_orm::core::ColumnValue> {
1894                ::std::vec![#(#values),*]
1895            }
1896        }
1897    })
1898}
1899
1900fn derive_changeset_impl(input: DeriveInput) -> Result<TokenStream2> {
1901    let ident = input.ident;
1902    let model_config = parse_persistence_model_config(&input.attrs, "Changeset")?;
1903    let entity = model_config
1904        .entity
1905        .as_ref()
1906        .expect("validated persistence model must include entity");
1907    let fields = extract_named_fields(&ident, input.data, "Changeset")?;
1908
1909    let changes = fields
1910        .iter()
1911        .map(|field| {
1912            let field_ident = field
1913                .ident
1914                .as_ref()
1915                .ok_or_else(|| Error::new_spanned(field, "Changeset requiere campos nombrados"))?;
1916            let field_config = parse_persistence_field_config(field, "Changeset")?;
1917            let inner_ty = option_inner_type(&field.ty).ok_or_else(|| {
1918                Error::new_spanned(
1919                    &field.ty,
1920                    "Changeset requiere Option<T> en cada campo para distinguir campos omitidos",
1921                )
1922            })?;
1923            let column_name =
1924                persistence_column_name_expr(entity, field_ident, field_config.column.as_ref());
1925
1926            Ok(quote! {
1927                let column_name = #column_name;
1928                let column = <#entity as ::sql_orm::core::Entity>::metadata()
1929                    .column(column_name)
1930                    .expect("generated Changeset field must reference existing entity metadata");
1931
1932                if let ::core::option::Option::Some(value) = &self.#field_ident {
1933                    if column.updatable {
1934                        changes.push(::sql_orm::core::ColumnValue::new(
1935                            column_name,
1936                            <#inner_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
1937                                ::core::clone::Clone::clone(value)
1938                            ),
1939                        ));
1940                    }
1941                }
1942            })
1943        })
1944        .collect::<Result<Vec<_>>>()?;
1945
1946    let concurrency_tokens = fields
1947        .iter()
1948        .map(|field| {
1949            let field_ident = field
1950                .ident
1951                .as_ref()
1952                .ok_or_else(|| Error::new_spanned(field, "Changeset requiere campos nombrados"))?;
1953            let field_config = parse_persistence_field_config(field, "Changeset")?;
1954            let inner_ty = option_inner_type(&field.ty).ok_or_else(|| {
1955                Error::new_spanned(
1956                    &field.ty,
1957                    "Changeset requiere Option<T> en cada campo para distinguir campos omitidos",
1958                )
1959            })?;
1960            let column_name =
1961                persistence_column_name_expr(entity, field_ident, field_config.column.as_ref());
1962
1963            Ok(quote! {
1964                let column_name = #column_name;
1965                let column = <#entity as ::sql_orm::core::Entity>::metadata()
1966                    .column(column_name)
1967                    .expect("generated Changeset field must reference existing entity metadata");
1968
1969                if column.rowversion {
1970                    if let ::core::option::Option::Some(value) = &self.#field_ident {
1971                        return Ok(Some(
1972                            <#inner_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
1973                                ::core::clone::Clone::clone(value)
1974                            )
1975                        ));
1976                    }
1977                }
1978            })
1979        })
1980        .collect::<Result<Vec<_>>>()?;
1981
1982    Ok(quote! {
1983        impl ::sql_orm::core::Changeset<#entity> for #ident {
1984            fn changes(&self) -> ::std::vec::Vec<::sql_orm::core::ColumnValue> {
1985                let mut changes = ::std::vec::Vec::new();
1986                #(#changes)*
1987                changes
1988            }
1989
1990            fn concurrency_token(&self) -> Result<::core::option::Option<::sql_orm::core::SqlValue>, ::sql_orm::core::OrmError> {
1991                #(#concurrency_tokens)*
1992                Ok(None)
1993            }
1994        }
1995    })
1996}
1997
1998fn extract_named_fields(
1999    ident: &Ident,
2000    data: Data,
2001    derive_name: &str,
2002) -> Result<syn::punctuated::Punctuated<Field, syn::token::Comma>> {
2003    match data {
2004        Data::Struct(data) => match data.fields {
2005            Fields::Named(fields) => Ok(fields.named),
2006            _ => Err(Error::new_spanned(
2007                ident,
2008                format!("{derive_name} solo soporta structs con campos nombrados"),
2009            )),
2010        },
2011        _ => Err(Error::new_spanned(
2012            ident,
2013            format!("{derive_name} solo soporta structs"),
2014        )),
2015    }
2016}
2017
2018fn has_explicit_primary_key(
2019    fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>,
2020) -> Result<bool> {
2021    for field in fields {
2022        if parse_field_config(field)?.primary_key {
2023            return Ok(true);
2024        }
2025    }
2026    Ok(false)
2027}
2028
2029fn parse_persistence_model_config(
2030    attrs: &[syn::Attribute],
2031    derive_name: &str,
2032) -> Result<PersistenceModelConfig> {
2033    let mut config = PersistenceModelConfig::default();
2034
2035    for attr in attrs {
2036        if !attr.path().is_ident("orm") {
2037            continue;
2038        }
2039
2040        attr.parse_nested_meta(|meta| {
2041            if meta.path.is_ident("entity") {
2042                config.entity = Some(meta.value()?.parse()?);
2043            } else {
2044                return Err(meta.error(format!(
2045                    "atributo orm no soportado a nivel de {derive_name}"
2046                )));
2047            }
2048
2049            Ok(())
2050        })?;
2051    }
2052
2053    let Some(entity) = config.entity else {
2054        return Err(Error::new(
2055            Span::call_site(),
2056            format!("{derive_name} requiere #[orm(entity = MiEntidad)]"),
2057        ));
2058    };
2059
2060    Ok(PersistenceModelConfig {
2061        entity: Some(entity),
2062    })
2063}
2064
2065fn parse_entity_config(attrs: &[syn::Attribute]) -> Result<EntityConfig> {
2066    let mut config = EntityConfig::default();
2067
2068    for attr in attrs {
2069        if !attr.path().is_ident("orm") {
2070            continue;
2071        }
2072
2073        attr.parse_nested_meta(|meta| {
2074            if meta.path.is_ident("table") {
2075                config.table = Some(parse_lit_str(meta.value()?.parse()?)?);
2076            } else if meta.path.is_ident("schema") {
2077                config.schema = Some(parse_lit_str(meta.value()?.parse()?)?);
2078            } else if meta.path.is_ident("renamed_from") {
2079                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2080            } else if meta.path.is_ident("audit") {
2081                if config.audit.is_some() {
2082                    return Err(meta.error(
2083                        "Entity solo soporta una policy audit; multiples policies que generen columnas solapadas deben rechazarse explicitamente",
2084                    ));
2085                }
2086                config.audit = Some(meta.value()?.parse()?);
2087            } else if meta.path.is_ident("soft_delete") {
2088                if config.soft_delete.is_some() {
2089                    return Err(meta.error(
2090                        "Entity solo soporta una policy soft_delete; multiples policies que generen columnas solapadas deben rechazarse explicitamente",
2091                    ));
2092                }
2093                config.soft_delete = Some(meta.value()?.parse()?);
2094            } else if meta.path.is_ident("tenant") {
2095                if config.tenant.is_some() {
2096                    return Err(meta.error(
2097                        "Entity solo soporta una policy tenant; multiples tenants deben rechazarse explicitamente",
2098                    ));
2099                }
2100                config.tenant = Some(meta.value()?.parse()?);
2101            } else if meta.path.is_ident("index") {
2102                config.indexes.push(parse_entity_index_config(meta)?);
2103            } else {
2104                return Err(meta.error("atributo orm no soportado a nivel de entidad"));
2105            }
2106
2107            Ok(())
2108        })?;
2109    }
2110
2111    Ok(config)
2112}
2113
2114fn parse_entity_index_config(meta: syn::meta::ParseNestedMeta<'_>) -> Result<EntityIndexConfig> {
2115    let mut index = EntityIndexConfig::default();
2116
2117    meta.parse_nested_meta(|nested| {
2118        if nested.path.is_ident("name") {
2119            index.name = Some(parse_lit_str(nested.value()?.parse()?)?);
2120        } else if nested.path.is_ident("unique") {
2121            index.unique = true;
2122        } else if nested.path.is_ident("columns") {
2123            let content;
2124            syn::parenthesized!(content in nested.input);
2125            let columns = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
2126            index.columns.extend(columns);
2127        } else {
2128            return Err(nested.error("index de entidad solo soporta name, unique y columns(...)"));
2129        }
2130
2131        Ok(())
2132    })?;
2133
2134    if index.columns.is_empty() {
2135        return Err(meta.error("index a nivel de entidad requiere columns(campo1, campo2, ...)"));
2136    }
2137
2138    Ok(index)
2139}
2140
2141fn parse_persistence_field_config(
2142    field: &Field,
2143    derive_name: &str,
2144) -> Result<PersistenceFieldConfig> {
2145    let mut config = PersistenceFieldConfig::default();
2146
2147    for attr in &field.attrs {
2148        if !attr.path().is_ident("orm") {
2149            continue;
2150        }
2151
2152        attr.parse_nested_meta(|meta| {
2153            if meta.path.is_ident("column") {
2154                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2155            } else {
2156                return Err(meta.error(format!(
2157                    "atributo orm no soportado en campos de {derive_name}"
2158                )));
2159            }
2160
2161            Ok(())
2162        })?;
2163    }
2164
2165    Ok(config)
2166}
2167
2168fn parse_field_config(field: &Field) -> Result<FieldConfig> {
2169    let mut config = FieldConfig::default();
2170
2171    for attr in &field.attrs {
2172        if !attr.path().is_ident("orm") {
2173            continue;
2174        }
2175
2176        attr.parse_nested_meta(|meta| {
2177            if meta.path.is_ident("column") {
2178                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2179            } else if meta.path.is_ident("primary_key") {
2180                config.primary_key = true;
2181            } else if meta.path.is_ident("identity") {
2182                config.identity = true;
2183                if !meta.input.is_empty() {
2184                    meta.parse_nested_meta(|nested| {
2185                        if nested.path.is_ident("seed") {
2186                            config.identity_seed = Some(parse_i64_expr(nested.value()?.parse()?)?);
2187                        } else if nested.path.is_ident("increment") {
2188                            config.identity_increment =
2189                                Some(parse_i64_expr(nested.value()?.parse()?)?);
2190                        } else {
2191                            return Err(nested.error("identity solo soporta seed e increment"));
2192                        }
2193
2194                        Ok(())
2195                    })?;
2196                }
2197            } else if meta.path.is_ident("length") {
2198                config.length = Some(parse_u32_expr(meta.value()?.parse()?)?);
2199            } else if meta.path.is_ident("nullable") {
2200                config.nullable = true;
2201            } else if meta.path.is_ident("default_sql") {
2202                return Err(meta.error(
2203                    "default_sql es un fragmento SQL unsafe; usa unsafe_default_sql = \"...\" para reconocer explícitamente el riesgo",
2204                ));
2205            } else if meta.path.is_ident("unsafe_default_sql") {
2206                config.default_sql = Some(parse_lit_str(meta.value()?.parse()?)?);
2207            } else if meta.path.is_ident("renamed_from") {
2208                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2209            } else if meta.path.is_ident("index") {
2210                let mut index = IndexConfig::default();
2211                meta.parse_nested_meta(|nested| {
2212                    if nested.path.is_ident("name") {
2213                        index.name = Some(parse_lit_str(nested.value()?.parse()?)?);
2214                    } else {
2215                        return Err(nested.error("index solo soporta name"));
2216                    }
2217
2218                    Ok(())
2219                })?;
2220                config.indexes.push(index);
2221            } else if meta.path.is_ident("unique") {
2222                config.indexes.push(IndexConfig {
2223                    unique: true,
2224                    ..IndexConfig::default()
2225                });
2226            } else if meta.path.is_ident("sql_type") {
2227                config.sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2228            } else if meta.path.is_ident("unsafe_sql_type") {
2229                config.unsafe_sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2230            } else if meta.path.is_ident("precision") {
2231                config.precision = Some(parse_u8_expr(meta.value()?.parse()?)?);
2232            } else if meta.path.is_ident("scale") {
2233                config.scale = Some(parse_u8_expr(meta.value()?.parse()?)?);
2234            } else if meta.path.is_ident("computed_sql") {
2235                return Err(meta.error(
2236                    "computed_sql es un fragmento SQL unsafe; usa unsafe_computed_sql = \"...\" para reconocer explícitamente el riesgo",
2237                ));
2238            } else if meta.path.is_ident("unsafe_computed_sql") {
2239                config.computed_sql = Some(parse_lit_str(meta.value()?.parse()?)?);
2240            } else if meta.path.is_ident("rowversion") {
2241                config.rowversion = true;
2242            } else if meta.path.is_ident("foreign_key") {
2243                config.foreign_key = Some(parse_foreign_key_config(meta)?);
2244            } else if meta.path.is_ident("belongs_to") {
2245                if config.navigation.is_some() {
2246                    return Err(meta.error(
2247                        "un campo solo puede declarar una navegación: belongs_to, has_one o has_many",
2248                    ));
2249                }
2250                config.navigation = Some(parse_navigation_config(
2251                    meta,
2252                    NavigationKindConfig::BelongsTo,
2253                )?);
2254            } else if meta.path.is_ident("has_one") {
2255                if config.navigation.is_some() {
2256                    return Err(meta.error(
2257                        "un campo solo puede declarar una navegación: belongs_to, has_one o has_many",
2258                    ));
2259                }
2260                config.navigation =
2261                    Some(parse_navigation_config(meta, NavigationKindConfig::HasOne)?);
2262            } else if meta.path.is_ident("has_many") {
2263                if config.navigation.is_some() {
2264                    return Err(meta.error(
2265                        "un campo solo puede declarar una navegación: belongs_to, has_one o has_many",
2266                    ));
2267                }
2268                config.navigation = Some(parse_navigation_config(
2269                    meta,
2270                    NavigationKindConfig::HasMany,
2271                )?);
2272            } else if meta.path.is_ident("many_to_many") {
2273                return Err(meta.error(
2274                    "many_to_many directo no está soportado todavía; modele la relación con una entidad intermedia explícita",
2275                ));
2276            } else if meta.path.is_ident("on_delete") {
2277                config.on_delete = Some(parse_referential_action_expr(meta.value()?.parse()?)?);
2278            } else {
2279                return Err(meta.error("atributo orm no soportado a nivel de campo"));
2280            }
2281
2282            Ok(())
2283        })?;
2284    }
2285
2286    if config.navigation.is_some()
2287        && (config.column.is_some()
2288            || config.primary_key
2289            || config.identity
2290            || config.nullable
2291            || config.length.is_some()
2292            || config.default_sql.is_some()
2293            || config.renamed_from.is_some()
2294            || config.computed_sql.is_some()
2295            || config.rowversion
2296            || config.sql_type.is_some()
2297            || config.precision.is_some()
2298            || config.scale.is_some()
2299            || !config.indexes.is_empty()
2300            || config.foreign_key.is_some()
2301            || config.on_delete.is_some())
2302    {
2303        return Err(Error::new_spanned(
2304            field,
2305            "los campos de navegación solo soportan belongs_to, has_one o has_many; no se generan columnas para ellos",
2306        ));
2307    }
2308
2309    if config.navigation.is_none() && is_navigation_wrapper_type(&field.ty) {
2310        return Err(Error::new_spanned(
2311            field,
2312            "los campos Navigation<T> y Collection<T> requieren #[orm(belongs_to(...))], #[orm(has_one(...))] o #[orm(has_many(...))]",
2313        ));
2314    }
2315
2316    Ok(config)
2317}
2318
2319fn parse_policy_field_config(field: &Field, kind: PolicyFieldsKind) -> Result<AuditFieldConfig> {
2320    let mut config = AuditFieldConfig::default();
2321    let derive_name = kind.derive_name();
2322
2323    for attr in &field.attrs {
2324        if !attr.path().is_ident("orm") {
2325            continue;
2326        }
2327
2328        attr.parse_nested_meta(|meta| {
2329            if meta.path.is_ident("column") {
2330                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2331            } else if meta.path.is_ident("length") {
2332                config.length = Some(parse_u32_expr(meta.value()?.parse()?)?);
2333            } else if meta.path.is_ident("nullable") {
2334                config.nullable = true;
2335            } else if meta.path.is_ident("default_sql") {
2336                return Err(meta.error(
2337                    "default_sql es un fragmento SQL unsafe; usa unsafe_default_sql = \"...\" para reconocer explícitamente el riesgo",
2338                ));
2339            } else if meta.path.is_ident("unsafe_default_sql") {
2340                config.default_sql = Some(parse_lit_str(meta.value()?.parse()?)?);
2341            } else if meta.path.is_ident("renamed_from") {
2342                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2343            } else if meta.path.is_ident("sql_type") {
2344                config.sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2345            } else if meta.path.is_ident("unsafe_sql_type") {
2346                config.unsafe_sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2347            } else if meta.path.is_ident("precision") {
2348                config.precision = Some(parse_u8_expr(meta.value()?.parse()?)?);
2349            } else if meta.path.is_ident("scale") {
2350                config.scale = Some(parse_u8_expr(meta.value()?.parse()?)?);
2351            } else if meta.path.is_ident("insertable") {
2352                config.insertable = Some(parse_bool_expr(meta.value()?.parse()?)?);
2353            } else if meta.path.is_ident("updatable") {
2354                config.updatable = Some(parse_bool_expr(meta.value()?.parse()?)?);
2355            } else if (matches!(kind, PolicyFieldsKind::Audit)
2356                && (meta.path.is_ident("created_at")
2357                    || meta.path.is_ident("created_by")
2358                    || meta.path.is_ident("updated_at")
2359                    || meta.path.is_ident("updated_by")))
2360                || (matches!(kind, PolicyFieldsKind::SoftDelete)
2361                    && (meta.path.is_ident("deleted_at")
2362                        || meta.path.is_ident("deleted_by")
2363                        || meta.path.is_ident("is_deleted")))
2364            {
2365            } else {
2366                return Err(meta.error(format!(
2367                    "atributo orm no soportado en campos de {derive_name}"
2368                )));
2369            }
2370
2371            Ok(())
2372        })?;
2373    }
2374
2375    Ok(config)
2376}
2377
2378fn parse_tenant_context_field_config(field: &Field) -> Result<TenantContextFieldConfig> {
2379    let mut config = TenantContextFieldConfig::default();
2380
2381    for attr in &field.attrs {
2382        if !attr.path().is_ident("orm") {
2383            continue;
2384        }
2385
2386        attr.parse_nested_meta(|meta| {
2387            if meta.path.is_ident("column") {
2388                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2389            } else if meta.path.is_ident("length") {
2390                config.length = Some(parse_u32_expr(meta.value()?.parse()?)?);
2391            } else if meta.path.is_ident("renamed_from") {
2392                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2393            } else if meta.path.is_ident("sql_type") {
2394                config.sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2395            } else if meta.path.is_ident("precision") {
2396                config.precision = Some(parse_u8_expr(meta.value()?.parse()?)?);
2397            } else if meta.path.is_ident("scale") {
2398                config.scale = Some(parse_u8_expr(meta.value()?.parse()?)?);
2399            } else {
2400                return Err(meta.error("atributo orm no soportado en campos de TenantContext"));
2401            }
2402
2403            Ok(())
2404        })?;
2405    }
2406
2407    Ok(config)
2408}
2409
2410fn parse_lit_str(expr: Expr) -> Result<LitStr> {
2411    match expr {
2412        Expr::Lit(ExprLit {
2413            lit: Lit::Str(value),
2414            ..
2415        }) => Ok(value),
2416        other => Err(Error::new_spanned(other, "se esperaba un string literal")),
2417    }
2418}
2419
2420fn validate_non_empty_lit_str(value: &LitStr, message: &str) -> Result<()> {
2421    if value.value().is_empty() {
2422        return Err(Error::new_spanned(value, message));
2423    }
2424
2425    Ok(())
2426}
2427
2428fn parse_bool_expr(expr: Expr) -> Result<bool> {
2429    match expr {
2430        Expr::Lit(ExprLit {
2431            lit: Lit::Bool(value),
2432            ..
2433        }) => Ok(value.value),
2434        other => Err(Error::new_spanned(other, "se esperaba un boolean literal")),
2435    }
2436}
2437
2438fn parse_foreign_key_config(meta: syn::meta::ParseNestedMeta<'_>) -> Result<ForeignKeyConfig> {
2439    if meta.input.peek(Token![=]) {
2440        return parse_foreign_key_string_config(meta.value()?.parse()?);
2441    }
2442
2443    let mut entity = None;
2444    let mut column = None;
2445    let mut name = None;
2446
2447    meta.parse_nested_meta(|nested| {
2448        if nested.path.is_ident("entity") {
2449            let path: Path = nested.value()?.parse()?;
2450            entity = Some(path);
2451        } else if nested.path.is_ident("column") {
2452            column = Some(nested.value()?.parse::<Ident>()?);
2453        } else if nested.path.is_ident("name") {
2454            name = Some(parse_lit_str(nested.value()?.parse()?)?);
2455        } else {
2456            return Err(nested.error("foreign_key solo soporta entity, column y name"));
2457        }
2458
2459        Ok(())
2460    })?;
2461
2462    let entity = entity.ok_or_else(|| meta.error("foreign_key requiere entity = MiEntidad"))?;
2463    let column = column.ok_or_else(|| meta.error("foreign_key requiere column = id"))?;
2464    let generated_table_name = default_table_name_from_path(&entity)?;
2465
2466    Ok(ForeignKeyConfig {
2467        name,
2468        generated_referenced_table_name: generated_table_name,
2469        target: ForeignKeyTarget::Structured { entity, column },
2470    })
2471}
2472
2473fn parse_navigation_config(
2474    meta: syn::meta::ParseNestedMeta<'_>,
2475    kind: NavigationKindConfig,
2476) -> Result<NavigationConfig> {
2477    let content;
2478    syn::parenthesized!(content in meta.input);
2479
2480    let target: Path = content.parse()?;
2481    let mut foreign_key = None;
2482
2483    while !content.is_empty() {
2484        content.parse::<Token![,]>()?;
2485        if content.is_empty() {
2486            break;
2487        }
2488
2489        let key: Ident = content.parse()?;
2490        content.parse::<Token![=]>()?;
2491
2492        if key == "foreign_key" {
2493            if foreign_key.is_some() {
2494                return Err(Error::new(
2495                    key.span(),
2496                    format!("{} no permite foreign_key duplicado", kind.attribute_name()),
2497                ));
2498            }
2499            foreign_key = Some(content.parse()?);
2500        } else {
2501            return Err(Error::new(
2502                key.span(),
2503                format!("{} solo soporta foreign_key", kind.attribute_name()),
2504            ));
2505        }
2506    }
2507
2508    let foreign_key = foreign_key.ok_or_else(|| {
2509        meta.error(format!(
2510            "{} requiere foreign_key = campo",
2511            kind.attribute_name()
2512        ))
2513    })?;
2514
2515    Ok(NavigationConfig {
2516        kind,
2517        target,
2518        foreign_key,
2519    })
2520}
2521
2522fn validate_repeated_structured_foreign_keys(
2523    foreign_keys: &BTreeMap<String, FieldForeignKeyInfo>,
2524) -> Result<()> {
2525    let mut targets = BTreeMap::<&str, Vec<&FieldForeignKeyInfo>>::new();
2526
2527    for foreign_key in foreign_keys.values() {
2528        let Some(target) = foreign_key.structured_target.as_deref() else {
2529            continue;
2530        };
2531        targets.entry(target).or_default().push(foreign_key);
2532    }
2533
2534    for foreign_keys_for_target in targets.values() {
2535        if foreign_keys_for_target.len() < 2 {
2536            continue;
2537        }
2538
2539        if let Some(unnamed_foreign_key) = foreign_keys_for_target
2540            .iter()
2541            .find(|foreign_key| !foreign_key.has_explicit_name)
2542        {
2543            return Err(Error::new(
2544                unnamed_foreign_key.field_span,
2545                "múltiples foreign keys estructuradas al mismo target requieren name explícito para desambiguar navegaciones",
2546            ));
2547        }
2548    }
2549
2550    Ok(())
2551}
2552
2553fn parse_foreign_key_string_config(expr: Expr) -> Result<ForeignKeyConfig> {
2554    let value = parse_lit_str(expr)?;
2555    let raw = value.value();
2556    let segments = raw.split('.').collect::<Vec<_>>();
2557
2558    let (referenced_schema, referenced_table, referenced_column) = match segments.as_slice() {
2559        [table, column] => (
2560            LitStr::new("dbo", value.span()),
2561            LitStr::new(table, value.span()),
2562            LitStr::new(column, value.span()),
2563        ),
2564        [schema, table, column] => (
2565            LitStr::new(schema, value.span()),
2566            LitStr::new(table, value.span()),
2567            LitStr::new(column, value.span()),
2568        ),
2569        _ => {
2570            return Err(Error::new_spanned(
2571                value,
2572                "foreign_key requiere el formato \"tabla.columna\" o \"schema.tabla.columna\", o la forma estructurada foreign_key(entity = Customer, column = id)",
2573            ));
2574        }
2575    };
2576
2577    if referenced_schema.value().is_empty()
2578        || referenced_table.value().is_empty()
2579        || referenced_column.value().is_empty()
2580    {
2581        return Err(Error::new_spanned(
2582            value,
2583            "foreign_key no permite segmentos vacíos",
2584        ));
2585    }
2586
2587    Ok(ForeignKeyConfig {
2588        name: None,
2589        generated_referenced_table_name: referenced_table.value(),
2590        target: ForeignKeyTarget::Legacy {
2591            referenced_schema,
2592            referenced_table,
2593            referenced_column,
2594        },
2595    })
2596}
2597
2598fn parse_referential_action_expr(expr: Expr) -> Result<ReferentialActionConfig> {
2599    let value = parse_lit_str(expr)?;
2600    match value.value().to_ascii_lowercase().as_str() {
2601        "no action" => Ok(ReferentialActionConfig::NoAction),
2602        "cascade" => Ok(ReferentialActionConfig::Cascade),
2603        "set null" => Ok(ReferentialActionConfig::SetNull),
2604        _ => Err(Error::new_spanned(
2605            value,
2606            "solo se soportan los valores \"no action\", \"cascade\" y \"set null\"",
2607        )),
2608    }
2609}
2610
2611fn parse_u32_expr(expr: Expr) -> Result<u32> {
2612    parse_int::<u32>(expr, "se esperaba un entero u32")
2613}
2614
2615fn parse_u8_expr(expr: Expr) -> Result<u8> {
2616    parse_int::<u8>(expr, "se esperaba un entero u8")
2617}
2618
2619fn parse_i64_expr(expr: Expr) -> Result<i64> {
2620    parse_int::<i64>(expr, "se esperaba un entero i64")
2621}
2622
2623fn parse_int<T>(expr: Expr, message: &str) -> Result<T>
2624where
2625    T: std::str::FromStr,
2626    <T as std::str::FromStr>::Err: std::fmt::Display,
2627{
2628    match expr {
2629        Expr::Lit(ExprLit {
2630            lit: Lit::Int(value),
2631            ..
2632        }) => value
2633            .base10_parse::<T>()
2634            .map_err(|_| Error::new_spanned(value, message)),
2635        other => Err(Error::new_spanned(other, message)),
2636    }
2637}
2638
2639fn option_lit_str(value: Option<LitStr>) -> TokenStream2 {
2640    match value {
2641        Some(value) => quote! { Some(#value) },
2642        None => quote! { None },
2643    }
2644}
2645
2646fn persistence_column_name_expr(
2647    entity: &Type,
2648    field_ident: &Ident,
2649    explicit_column: Option<&LitStr>,
2650) -> TokenStream2 {
2651    let field_name = LitStr::new(&field_ident.to_string(), field_ident.span());
2652
2653    match explicit_column {
2654        Some(column_name) => {
2655            let error = LitStr::new(
2656                &format!(
2657                    "la columna '{}' no existe en la metadata de la entidad de destino",
2658                    column_name.value()
2659                ),
2660                column_name.span(),
2661            );
2662
2663            quote! {{
2664                <#entity as ::sql_orm::core::Entity>::metadata()
2665                    .column(#column_name)
2666                    .expect(#error)
2667                    .column_name
2668            }}
2669        }
2670        None => {
2671            let error = LitStr::new(
2672                &format!(
2673                    "el campo '{}' no existe en la metadata de la entidad de destino",
2674                    field_ident
2675                ),
2676                field_ident.span(),
2677            );
2678
2679            quote! {{
2680                <#entity as ::sql_orm::core::Entity>::metadata()
2681                    .field(#field_name)
2682                    .expect(#error)
2683                    .column_name
2684            }}
2685        }
2686    }
2687}
2688
2689fn option_number<T>(value: Option<T>) -> TokenStream2
2690where
2691    T: quote::ToTokens,
2692{
2693    match value {
2694        Some(value) => quote! { Some(#value) },
2695        None => quote! { None },
2696    }
2697}
2698
2699fn referential_action_tokens(action: ReferentialActionConfig) -> TokenStream2 {
2700    match action {
2701        ReferentialActionConfig::NoAction => {
2702            quote! { ::sql_orm::core::ReferentialAction::NoAction }
2703        }
2704        ReferentialActionConfig::Cascade => {
2705            quote! { ::sql_orm::core::ReferentialAction::Cascade }
2706        }
2707        ReferentialActionConfig::SetNull => {
2708            quote! { ::sql_orm::core::ReferentialAction::SetNull }
2709        }
2710    }
2711}
2712
2713fn navigation_kind_tokens(kind: NavigationKindConfig) -> TokenStream2 {
2714    match kind {
2715        NavigationKindConfig::BelongsTo => quote! { ::sql_orm::core::NavigationKind::BelongsTo },
2716        NavigationKindConfig::HasOne => quote! { ::sql_orm::core::NavigationKind::HasOne },
2717        NavigationKindConfig::HasMany => quote! { ::sql_orm::core::NavigationKind::HasMany },
2718    }
2719}
2720
2721fn include_navigation_impls(
2722    entity_ident: &Ident,
2723    navigations: &[PendingNavigation],
2724) -> Result<Vec<TokenStream2>> {
2725    let mut grouped =
2726        BTreeMap::<String, (Path, Vec<(Ident, LitStr, NavigationWrapperConfig)>)>::new();
2727
2728    for navigation in navigations {
2729        if !matches!(
2730            navigation.kind,
2731            NavigationKindConfig::BelongsTo | NavigationKindConfig::HasOne
2732        ) {
2733            continue;
2734        }
2735
2736        let field_ident = Ident::new(&navigation.rust_field.value(), navigation.rust_field.span());
2737        let target = &navigation.target;
2738        let key = quote! { #target }.to_string();
2739        grouped
2740            .entry(key)
2741            .or_insert_with(|| (navigation.target.clone(), Vec::new()))
2742            .1
2743            .push((
2744                field_ident,
2745                navigation.rust_field.clone(),
2746                navigation.wrapper,
2747            ));
2748    }
2749
2750    grouped
2751        .into_values()
2752        .map(|(target, fields)| {
2753            let arms = fields.into_iter().map(|(field_ident, rust_field, wrapper)| {
2754                let assignment = match wrapper {
2755                    NavigationWrapperConfig::Eager => {
2756                        quote! { self.#field_ident = ::sql_orm::Navigation::from_option(value); }
2757                    }
2758                    NavigationWrapperConfig::Lazy => {
2759                        quote! { self.#field_ident = ::sql_orm::LazyNavigation::from_option(value); }
2760                    }
2761                };
2762
2763                quote! {
2764                    #rust_field => {
2765                        #assignment
2766                        Ok(())
2767                    }
2768                }
2769            });
2770
2771            Ok(quote! {
2772                impl ::sql_orm::IncludeNavigation<#target> for #entity_ident {
2773                    fn set_included_navigation(
2774                        &mut self,
2775                        navigation: &str,
2776                        value: ::core::option::Option<#target>,
2777                    ) -> ::core::result::Result<(), ::sql_orm::core::OrmError> {
2778                        match navigation {
2779                            #(#arms,)*
2780                            _ => Err(::sql_orm::core::OrmError::mapping(
2781                                ::std::format!(
2782                                    "entity `{}` does not support include navigation `{}` for `{}`",
2783                                    <Self as ::sql_orm::core::Entity>::metadata().rust_name,
2784                                    navigation,
2785                                    ::core::any::type_name::<#target>(),
2786                                )
2787                            )),
2788                        }
2789                    }
2790                }
2791            })
2792        })
2793        .collect()
2794}
2795
2796fn include_collection_impls(
2797    entity_ident: &Ident,
2798    navigations: &[PendingNavigation],
2799) -> Result<Vec<TokenStream2>> {
2800    let mut grouped =
2801        BTreeMap::<String, (Path, Vec<(Ident, LitStr, NavigationWrapperConfig)>)>::new();
2802
2803    for navigation in navigations {
2804        if !matches!(navigation.kind, NavigationKindConfig::HasMany) {
2805            continue;
2806        }
2807
2808        let field_ident = Ident::new(&navigation.rust_field.value(), navigation.rust_field.span());
2809        let target = &navigation.target;
2810        let key = quote! { #target }.to_string();
2811        grouped
2812            .entry(key)
2813            .or_insert_with(|| (navigation.target.clone(), Vec::new()))
2814            .1
2815            .push((
2816                field_ident,
2817                navigation.rust_field.clone(),
2818                navigation.wrapper,
2819            ));
2820    }
2821
2822    grouped
2823        .into_values()
2824        .map(|(target, fields)| {
2825            let arms = fields.into_iter().map(|(field_ident, rust_field, wrapper)| {
2826                let assignment = match wrapper {
2827                    NavigationWrapperConfig::Eager => {
2828                        quote! { self.#field_ident = ::sql_orm::Collection::from_vec(values); }
2829                    }
2830                    NavigationWrapperConfig::Lazy => {
2831                        quote! { self.#field_ident = ::sql_orm::LazyCollection::from_vec(values); }
2832                    }
2833                };
2834
2835                quote! {
2836                    #rust_field => {
2837                        #assignment
2838                        Ok(())
2839                    }
2840                }
2841            });
2842
2843            Ok(quote! {
2844                impl ::sql_orm::IncludeCollection<#target> for #entity_ident {
2845                    fn set_included_collection(
2846                        &mut self,
2847                        navigation: &str,
2848                        values: ::std::vec::Vec<#target>,
2849                    ) -> ::core::result::Result<(), ::sql_orm::core::OrmError> {
2850                        match navigation {
2851                            #(#arms,)*
2852                            _ => Err(::sql_orm::core::OrmError::mapping(
2853                                ::std::format!(
2854                                    "entity `{}` does not support include collection `{}` for `{}`",
2855                                    <Self as ::sql_orm::core::Entity>::metadata().rust_name,
2856                                    navigation,
2857                                    ::core::any::type_name::<#target>(),
2858                                )
2859                            )),
2860                        }
2861                    }
2862                }
2863            })
2864        })
2865        .collect()
2866}
2867
2868fn validate_navigation_field_type(
2869    ty: &Type,
2870    navigation: &NavigationConfig,
2871) -> Result<NavigationWrapperConfig> {
2872    let expected_wrappers = match navigation.kind {
2873        NavigationKindConfig::BelongsTo | NavigationKindConfig::HasOne => {
2874            ("Navigation", "LazyNavigation")
2875        }
2876        NavigationKindConfig::HasMany => ("Collection", "LazyCollection"),
2877    };
2878
2879    let (actual_target, wrapper) = navigation_wrapper_inner_last_ident(ty, expected_wrappers)
2880        .ok_or_else(|| {
2881            Error::new_spanned(
2882                ty,
2883                format!(
2884                    "{} requiere un campo {}<{}> o {}<{}>",
2885                    navigation.kind.attribute_name(),
2886                    expected_wrappers.0,
2887                    path_last_ident(&navigation.target)
2888                        .map(|ident| ident.to_string())
2889                        .unwrap_or_else(|| "Entidad".to_string()),
2890                    expected_wrappers.1,
2891                    path_last_ident(&navigation.target)
2892                        .map(|ident| ident.to_string())
2893                        .unwrap_or_else(|| "Entidad".to_string()),
2894                ),
2895            )
2896        })?;
2897
2898    let Some(expected_target) = path_last_ident(&navigation.target) else {
2899        return Err(Error::new_spanned(
2900            &navigation.target,
2901            format!(
2902                "{} requiere una ruta de entidad válida",
2903                navigation.kind.attribute_name()
2904            ),
2905        ));
2906    };
2907
2908    if actual_target != expected_target {
2909        return Err(Error::new_spanned(
2910            ty,
2911            format!(
2912                "{} apunta a {}, pero el campo usa {}",
2913                navigation.kind.attribute_name(),
2914                expected_target,
2915                actual_target,
2916            ),
2917        ));
2918    }
2919
2920    Ok(wrapper)
2921}
2922
2923fn navigation_wrapper_inner_last_ident<'a>(
2924    ty: &'a Type,
2925    wrappers: (&str, &str),
2926) -> Option<(&'a Ident, NavigationWrapperConfig)> {
2927    generic_wrapper_inner_last_ident(ty, wrappers.0)
2928        .map(|ident| (ident, NavigationWrapperConfig::Eager))
2929        .or_else(|| {
2930            generic_wrapper_inner_last_ident(ty, wrappers.1)
2931                .map(|ident| (ident, NavigationWrapperConfig::Lazy))
2932        })
2933}
2934
2935fn generic_wrapper_inner_last_ident<'a>(ty: &'a Type, wrapper: &str) -> Option<&'a Ident> {
2936    let Type::Path(type_path) = ty else {
2937        return None;
2938    };
2939
2940    let segment = type_path.path.segments.last()?;
2941    if segment.ident != wrapper {
2942        return None;
2943    }
2944
2945    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
2946        return None;
2947    };
2948
2949    let syn::GenericArgument::Type(Type::Path(inner)) = arguments.args.first()? else {
2950        return None;
2951    };
2952
2953    path_last_ident(&inner.path)
2954}
2955
2956fn is_navigation_wrapper_type(ty: &Type) -> bool {
2957    let Type::Path(type_path) = ty else {
2958        return false;
2959    };
2960
2961    type_path.path.segments.last().is_some_and(|segment| {
2962        segment.ident == "Navigation"
2963            || segment.ident == "Collection"
2964            || segment.ident == "LazyNavigation"
2965            || segment.ident == "LazyCollection"
2966    })
2967}
2968
2969fn path_last_ident(path: &Path) -> Option<&Ident> {
2970    path.segments.last().map(|segment| &segment.ident)
2971}
2972
2973fn path_key(path: &Path) -> String {
2974    quote! { #path }.to_string()
2975}
2976
2977fn infer_sql_type(type_info: &TypeInfo, rowversion: bool, ty: &Type) -> Result<TokenStream2> {
2978    if rowversion {
2979        return Ok(quote! { ::sql_orm::core::SqlServerType::RowVersion });
2980    }
2981
2982    let token = match type_info.kind {
2983        TypeKind::I64 => quote! { ::sql_orm::core::SqlServerType::BigInt },
2984        TypeKind::I32 => quote! { ::sql_orm::core::SqlServerType::Int },
2985        TypeKind::I16 => quote! { ::sql_orm::core::SqlServerType::SmallInt },
2986        TypeKind::U8 => quote! { ::sql_orm::core::SqlServerType::TinyInt },
2987        TypeKind::Bool => quote! { ::sql_orm::core::SqlServerType::Bit },
2988        TypeKind::String => quote! { ::sql_orm::core::SqlServerType::NVarChar },
2989        TypeKind::VecU8 => quote! { ::sql_orm::core::SqlServerType::VarBinary },
2990        TypeKind::Uuid => quote! { ::sql_orm::core::SqlServerType::UniqueIdentifier },
2991        TypeKind::NaiveDateTime => quote! { ::sql_orm::core::SqlServerType::DateTime2 },
2992        TypeKind::NaiveDate => quote! { ::sql_orm::core::SqlServerType::Date },
2993        TypeKind::NaiveTime => quote! { ::sql_orm::core::SqlServerType::Time },
2994        TypeKind::DateTimeFixedOffset => {
2995            quote! { ::sql_orm::core::SqlServerType::DateTimeOffset }
2996        }
2997        TypeKind::Decimal => quote! { ::sql_orm::core::SqlServerType::Decimal },
2998        TypeKind::Float => quote! { ::sql_orm::core::SqlServerType::Float },
2999        TypeKind::Unknown => {
3000            return Err(Error::new_spanned(
3001                ty,
3002                "tipo Rust no soportado todavía para derive(Entity)",
3003            ));
3004        }
3005    };
3006
3007    Ok(token)
3008}
3009
3010fn sql_type_from_string(value: &LitStr) -> Result<TokenStream2> {
3011    sql_type_from_string_with_custom(value, false)
3012}
3013
3014fn unsafe_sql_type_from_string(value: &LitStr) -> TokenStream2 {
3015    sql_type_from_string_with_custom(value, true)
3016        .expect("unsafe SQL type fragments always allow custom SQL Server types")
3017}
3018
3019fn sql_type_from_string_with_custom(value: &LitStr, allow_custom: bool) -> Result<TokenStream2> {
3020    let normalized = value.value().to_ascii_lowercase();
3021
3022    let token = if normalized.starts_with("bigint") {
3023        quote! { ::sql_orm::core::SqlServerType::BigInt }
3024    } else if normalized == "int" {
3025        quote! { ::sql_orm::core::SqlServerType::Int }
3026    } else if normalized.starts_with("smallint") {
3027        quote! { ::sql_orm::core::SqlServerType::SmallInt }
3028    } else if normalized.starts_with("tinyint") {
3029        quote! { ::sql_orm::core::SqlServerType::TinyInt }
3030    } else if normalized.starts_with("bit") {
3031        quote! { ::sql_orm::core::SqlServerType::Bit }
3032    } else if normalized.starts_with("uniqueidentifier") {
3033        quote! { ::sql_orm::core::SqlServerType::UniqueIdentifier }
3034    } else if normalized.starts_with("datetimeoffset") {
3035        quote! { ::sql_orm::core::SqlServerType::DateTimeOffset }
3036    } else if normalized.starts_with("datetime2") {
3037        quote! { ::sql_orm::core::SqlServerType::DateTime2 }
3038    } else if normalized.starts_with("date") {
3039        quote! { ::sql_orm::core::SqlServerType::Date }
3040    } else if normalized.starts_with("time") {
3041        quote! { ::sql_orm::core::SqlServerType::Time }
3042    } else if normalized.starts_with("decimal") {
3043        quote! { ::sql_orm::core::SqlServerType::Decimal }
3044    } else if normalized.starts_with("float") {
3045        quote! { ::sql_orm::core::SqlServerType::Float }
3046    } else if normalized.starts_with("money") {
3047        quote! { ::sql_orm::core::SqlServerType::Money }
3048    } else if normalized.starts_with("nvarchar") {
3049        quote! { ::sql_orm::core::SqlServerType::NVarChar }
3050    } else if normalized.starts_with("varbinary") {
3051        quote! { ::sql_orm::core::SqlServerType::VarBinary }
3052    } else if normalized.starts_with("rowversion") {
3053        quote! { ::sql_orm::core::SqlServerType::RowVersion }
3054    } else if allow_custom {
3055        quote! { ::sql_orm::core::SqlServerType::Custom(#value) }
3056    } else {
3057        return Err(Error::new_spanned(
3058            value,
3059            "sql_type no reconoce este tipo SQL Server; usa unsafe_sql_type = \"...\" para tipos SQL custom",
3060        ));
3061    };
3062
3063    Ok(token)
3064}
3065
3066fn analyze_type(ty: &Type) -> Result<TypeInfo> {
3067    let nullable = option_inner_type(ty).is_some();
3068    let effective = option_inner_type(ty).unwrap_or(ty);
3069    let kind = classify_type(effective)?;
3070
3071    Ok(TypeInfo {
3072        nullable,
3073        is_integer: matches!(
3074            kind,
3075            TypeKind::I64 | TypeKind::I32 | TypeKind::I16 | TypeKind::U8
3076        ),
3077        is_vec_u8: matches!(kind, TypeKind::VecU8),
3078        default_max_length: matches!(kind, TypeKind::String).then_some(255),
3079        default_precision: matches!(kind, TypeKind::Decimal).then_some(18),
3080        default_scale: matches!(kind, TypeKind::Decimal).then_some(2),
3081        kind,
3082    })
3083}
3084
3085fn classify_type(ty: &Type) -> Result<TypeKind> {
3086    match ty {
3087        Type::Path(type_path) => {
3088            let segment = type_path
3089                .path
3090                .segments
3091                .last()
3092                .ok_or_else(|| Error::new_spanned(type_path, "tipo inválido"))?;
3093
3094            let ident = segment.ident.to_string();
3095            let kind = match ident.as_str() {
3096                "i64" => TypeKind::I64,
3097                "i32" => TypeKind::I32,
3098                "i16" => TypeKind::I16,
3099                "u8" => TypeKind::U8,
3100                "bool" => TypeKind::Bool,
3101                "String" => TypeKind::String,
3102                "Uuid" => TypeKind::Uuid,
3103                "NaiveDateTime" => TypeKind::NaiveDateTime,
3104                "NaiveDate" => TypeKind::NaiveDate,
3105                "NaiveTime" => TypeKind::NaiveTime,
3106                "DateTime" if type_path_is_datetime_fixed_offset(&type_path.path) => {
3107                    TypeKind::DateTimeFixedOffset
3108                }
3109                "Decimal" => TypeKind::Decimal,
3110                "f32" | "f64" => TypeKind::Float,
3111                "Vec" if type_path_is_vec_u8(&type_path.path) => TypeKind::VecU8,
3112                _ => TypeKind::Unknown,
3113            };
3114
3115            Ok(kind)
3116        }
3117        _ => Ok(TypeKind::Unknown),
3118    }
3119}
3120
3121fn option_inner_type(ty: &Type) -> Option<&Type> {
3122    let Type::Path(type_path) = ty else {
3123        return None;
3124    };
3125
3126    let segment = type_path.path.segments.last()?;
3127    if segment.ident != "Option" {
3128        return None;
3129    }
3130
3131    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3132        return None;
3133    };
3134
3135    let syn::GenericArgument::Type(inner) = arguments.args.first()? else {
3136        return None;
3137    };
3138
3139    Some(inner)
3140}
3141
3142fn dbset_entity_type(ty: &Type) -> Option<&Type> {
3143    let Type::Path(type_path) = ty else {
3144        return None;
3145    };
3146
3147    let segment = type_path.path.segments.last()?;
3148    if segment.ident != "DbSet" {
3149        return None;
3150    }
3151
3152    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3153        return None;
3154    };
3155
3156    let syn::GenericArgument::Type(inner) = arguments.args.first()? else {
3157        return None;
3158    };
3159
3160    Some(inner)
3161}
3162
3163fn type_path_is_vec_u8(path: &Path) -> bool {
3164    let Some(segment) = path.segments.last() else {
3165        return false;
3166    };
3167
3168    if segment.ident != "Vec" {
3169        return false;
3170    }
3171
3172    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3173        return false;
3174    };
3175
3176    let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = arguments.args.first() else {
3177        return false;
3178    };
3179
3180    inner_path.path.is_ident("u8")
3181}
3182
3183fn type_path_is_datetime_fixed_offset(path: &Path) -> bool {
3184    let Some(segment) = path.segments.last() else {
3185        return false;
3186    };
3187
3188    if segment.ident != "DateTime" {
3189        return false;
3190    }
3191
3192    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3193        return false;
3194    };
3195
3196    let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = arguments.args.first() else {
3197        return false;
3198    };
3199
3200    inner_path
3201        .path
3202        .segments
3203        .last()
3204        .is_some_and(|segment| segment.ident == "FixedOffset")
3205}
3206
3207fn default_table_name(ident: &Ident) -> String {
3208    pluralize(&to_snake_case(&ident.to_string()))
3209}
3210
3211fn default_table_name_from_path(path: &Path) -> Result<String> {
3212    let ident = path
3213        .segments
3214        .last()
3215        .map(|segment| &segment.ident)
3216        .ok_or_else(|| {
3217            Error::new_spanned(path, "foreign_key requiere una ruta de entidad válida")
3218        })?;
3219
3220    Ok(default_table_name(ident))
3221}
3222
3223fn to_snake_case(value: &str) -> String {
3224    let mut output = String::with_capacity(value.len());
3225
3226    for (index, ch) in value.chars().enumerate() {
3227        if ch.is_uppercase() {
3228            if index > 0 {
3229                output.push('_');
3230            }
3231
3232            for lower in ch.to_lowercase() {
3233                output.push(lower);
3234            }
3235        } else {
3236            output.push(ch);
3237        }
3238    }
3239
3240    output
3241}
3242
3243fn pluralize(value: &str) -> String {
3244    if ends_with_consonant_y(value) {
3245        let stem = &value[..value.len() - 1];
3246        format!("{stem}ies")
3247    } else if value.ends_with('s')
3248        || value.ends_with('x')
3249        || value.ends_with('z')
3250        || value.ends_with("ch")
3251        || value.ends_with("sh")
3252    {
3253        format!("{value}es")
3254    } else {
3255        format!("{value}s")
3256    }
3257}
3258
3259fn ends_with_consonant_y(value: &str) -> bool {
3260    let mut chars = value.chars().rev();
3261    let Some(last) = chars.next() else {
3262        return false;
3263    };
3264    let Some(previous) = chars.next() else {
3265        return false;
3266    };
3267
3268    last == 'y' && !matches!(previous, 'a' | 'e' | 'i' | 'o' | 'u')
3269}
3270
3271fn generated_index_name(prefix: &str, table: &str, column: &str, span: Span) -> LitStr {
3272    LitStr::new(&format!("{prefix}_{table}_{column}"), span)
3273}
3274
3275fn generated_foreign_key_name(
3276    table: &str,
3277    column: &str,
3278    referenced_table: &str,
3279    span: Span,
3280) -> LitStr {
3281    LitStr::new(&format!("fk_{table}_{column}_{referenced_table}"), span)
3282}
3283
3284#[derive(Default)]
3285struct EntityConfig {
3286    table: Option<LitStr>,
3287    schema: Option<LitStr>,
3288    renamed_from: Option<LitStr>,
3289    indexes: Vec<EntityIndexConfig>,
3290    audit: Option<Path>,
3291    soft_delete: Option<Path>,
3292    tenant: Option<Path>,
3293}
3294
3295#[derive(Default)]
3296struct EntityIndexConfig {
3297    name: Option<LitStr>,
3298    unique: bool,
3299    columns: Vec<Ident>,
3300}
3301
3302#[derive(Default)]
3303struct PersistenceModelConfig {
3304    entity: Option<Type>,
3305}
3306
3307#[derive(Default)]
3308struct PersistenceFieldConfig {
3309    column: Option<LitStr>,
3310}
3311
3312#[derive(Default)]
3313struct AuditFieldConfig {
3314    column: Option<LitStr>,
3315    renamed_from: Option<LitStr>,
3316    nullable: bool,
3317    length: Option<u32>,
3318    default_sql: Option<LitStr>,
3319    sql_type: Option<LitStr>,
3320    unsafe_sql_type: Option<LitStr>,
3321    precision: Option<u8>,
3322    scale: Option<u8>,
3323    insertable: Option<bool>,
3324    updatable: Option<bool>,
3325}
3326
3327#[derive(Default)]
3328struct TenantContextFieldConfig {
3329    column: Option<LitStr>,
3330    renamed_from: Option<LitStr>,
3331    length: Option<u32>,
3332    sql_type: Option<LitStr>,
3333    unsafe_sql_type: Option<LitStr>,
3334    precision: Option<u8>,
3335    scale: Option<u8>,
3336}
3337
3338#[derive(Default)]
3339struct FieldConfig {
3340    column: Option<LitStr>,
3341    renamed_from: Option<LitStr>,
3342    primary_key: bool,
3343    identity: bool,
3344    identity_seed: Option<i64>,
3345    identity_increment: Option<i64>,
3346    nullable: bool,
3347    length: Option<u32>,
3348    default_sql: Option<LitStr>,
3349    computed_sql: Option<LitStr>,
3350    rowversion: bool,
3351    sql_type: Option<LitStr>,
3352    unsafe_sql_type: Option<LitStr>,
3353    precision: Option<u8>,
3354    scale: Option<u8>,
3355    indexes: Vec<IndexConfig>,
3356    foreign_key: Option<ForeignKeyConfig>,
3357    on_delete: Option<ReferentialActionConfig>,
3358    navigation: Option<NavigationConfig>,
3359}
3360
3361#[derive(Default)]
3362struct IndexConfig {
3363    name: Option<LitStr>,
3364    unique: bool,
3365}
3366
3367struct ForeignKeyConfig {
3368    name: Option<LitStr>,
3369    generated_referenced_table_name: String,
3370    target: ForeignKeyTarget,
3371}
3372
3373struct FieldForeignKeyInfo {
3374    name: LitStr,
3375    local_column: LitStr,
3376    referenced_column: TokenStream2,
3377    field_span: Span,
3378    has_explicit_name: bool,
3379    structured_target: Option<String>,
3380}
3381
3382struct PendingNavigation {
3383    rust_field: LitStr,
3384    kind: NavigationKindConfig,
3385    kind_tokens: TokenStream2,
3386    wrapper: NavigationWrapperConfig,
3387    target: Path,
3388    target_rust_name: LitStr,
3389    foreign_key_field: Ident,
3390    foreign_key_field_name: String,
3391}
3392
3393struct NavigationConfig {
3394    kind: NavigationKindConfig,
3395    target: Path,
3396    foreign_key: Ident,
3397}
3398
3399#[derive(Clone, Copy)]
3400enum NavigationKindConfig {
3401    BelongsTo,
3402    HasOne,
3403    HasMany,
3404}
3405
3406#[derive(Clone, Copy)]
3407enum NavigationWrapperConfig {
3408    Eager,
3409    Lazy,
3410}
3411
3412impl NavigationKindConfig {
3413    fn attribute_name(self) -> &'static str {
3414        match self {
3415            Self::BelongsTo => "belongs_to",
3416            Self::HasOne => "has_one",
3417            Self::HasMany => "has_many",
3418        }
3419    }
3420}
3421
3422impl ForeignKeyConfig {
3423    fn structured_target_key(&self) -> Option<String> {
3424        match &self.target {
3425            ForeignKeyTarget::Structured { entity, .. } => Some(path_key(entity)),
3426            ForeignKeyTarget::Legacy { .. } => None,
3427        }
3428    }
3429
3430    fn referenced_schema_tokens(&self) -> TokenStream2 {
3431        match &self.target {
3432            ForeignKeyTarget::Legacy {
3433                referenced_schema, ..
3434            } => quote! { #referenced_schema },
3435            ForeignKeyTarget::Structured { entity, .. } => {
3436                quote! { #entity::__SQL_ORM_ENTITY_SCHEMA }
3437            }
3438        }
3439    }
3440
3441    fn referenced_table_tokens(&self) -> TokenStream2 {
3442        match &self.target {
3443            ForeignKeyTarget::Legacy {
3444                referenced_table, ..
3445            } => quote! { #referenced_table },
3446            ForeignKeyTarget::Structured { entity, .. } => {
3447                quote! { #entity::__SQL_ORM_ENTITY_TABLE }
3448            }
3449        }
3450    }
3451
3452    fn referenced_column_tokens(&self) -> TokenStream2 {
3453        match &self.target {
3454            ForeignKeyTarget::Legacy {
3455                referenced_column, ..
3456            } => quote! { #referenced_column },
3457            ForeignKeyTarget::Structured { entity, column } => {
3458                quote! { #entity::#column.column_name() }
3459            }
3460        }
3461    }
3462}
3463
3464enum ForeignKeyTarget {
3465    Legacy {
3466        referenced_schema: LitStr,
3467        referenced_table: LitStr,
3468        referenced_column: LitStr,
3469    },
3470    Structured {
3471        entity: Path,
3472        column: Ident,
3473    },
3474}
3475
3476#[derive(Clone, Copy, PartialEq, Eq)]
3477enum ReferentialActionConfig {
3478    NoAction,
3479    Cascade,
3480    SetNull,
3481}
3482
3483struct TypeInfo {
3484    nullable: bool,
3485    is_integer: bool,
3486    is_vec_u8: bool,
3487    default_max_length: Option<u32>,
3488    default_precision: Option<u8>,
3489    default_scale: Option<u8>,
3490    kind: TypeKind,
3491}
3492
3493enum TypeKind {
3494    I64,
3495    I32,
3496    I16,
3497    U8,
3498    Bool,
3499    String,
3500    VecU8,
3501    Uuid,
3502    NaiveDateTime,
3503    NaiveDate,
3504    NaiveTime,
3505    DateTimeFixedOffset,
3506    Decimal,
3507    Float,
3508    Unknown,
3509}