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