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                let shared_connection =
1716                    <Self as ::sql_orm::DbContext>::shared_connection(self);
1717                let transaction_connection = shared_connection.clone();
1718                let tracking_registry =
1719                    <Self as ::sql_orm::DbContext>::tracking_registry(self);
1720
1721                shared_connection.run_transaction(|| async move {
1722                    let transaction_context =
1723                        Self::__from_shared_parts(transaction_connection, tracking_registry);
1724                    operation(transaction_context).await
1725                }).await
1726            }
1727
1728            pub async fn health_check(&self) -> Result<(), ::sql_orm::core::OrmError> {
1729                <Self as ::sql_orm::DbContext>::health_check(self).await
1730            }
1731
1732            /// Clears every experimental tracking entry currently registered
1733            /// on this context.
1734            ///
1735            /// This does not execute SQL and does not reset values already
1736            /// held by `Tracked<T>` wrappers. It only detaches the current
1737            /// unit-of-work entries so later `save_changes()` calls ignore
1738            /// them.
1739            pub fn clear_tracker(&self) {
1740                <Self as ::sql_orm::DbContext>::clear_tracker(self)
1741            }
1742
1743            async fn __sql_orm_save_changes_without_transaction(&self) -> Result<usize, ::sql_orm::core::OrmError>
1744            where
1745                #(#save_changes_bounds,)*
1746            {
1747                let mut saved = 0usize;
1748                let save_plan = ::sql_orm::save_changes_operation_plan(&[
1749                    #(#save_plan_entity_metadata),*
1750                ])?;
1751
1752                for entity_index in save_plan.added_order() {
1753                    match *entity_index {
1754                        #(#save_added_steps)*
1755                        _ => {}
1756                    }
1757                }
1758
1759                for entity_index in save_plan.modified_order() {
1760                    match *entity_index {
1761                        #(#save_modified_steps)*
1762                        _ => {}
1763                    }
1764                }
1765
1766                for entity_index in save_plan.deleted_order() {
1767                    match *entity_index {
1768                        #(#save_deleted_steps)*
1769                        _ => {}
1770                    }
1771                }
1772
1773                Ok(saved)
1774            }
1775
1776            /// Persists currently registered experimental tracking entries.
1777            ///
1778            /// This method processes live `Tracked<T>` wrappers registered by
1779            /// this context in three deterministic phases: `Added`,
1780            /// `Modified`, then `Deleted`. For simple foreign keys declared
1781            /// between entities in the context, inserts and updates run
1782            /// principals before dependents and deletes run the reverse order.
1783            ///
1784            /// The implementation reuses the normal `DbSet` insert, update
1785            /// and delete paths, so tenant, audit, soft-delete and rowversion
1786            /// behavior remains centralized in the public persistence layer.
1787            /// It opens an internal transaction when no transaction is active
1788            /// and reuses an outer `db.transaction(...)` when one is already
1789            /// active.
1790            ///
1791            /// Tracking remains experimental in this release slice: dropping a
1792            /// wrapper detaches it, relationship graph mutations are not
1793            /// persisted, and composite primary keys are rejected before SQL
1794            /// for pending tracked operations.
1795            pub async fn save_changes(&self) -> Result<usize, ::sql_orm::core::OrmError>
1796            where
1797                #(#save_changes_bounds,)*
1798            {
1799                let shared_connection =
1800                    <Self as ::sql_orm::DbContext>::shared_connection(self);
1801
1802                if shared_connection.is_transaction_active() {
1803                    self.__sql_orm_save_changes_without_transaction().await
1804                } else {
1805                    shared_connection.run_transaction(|| async {
1806                        self.__sql_orm_save_changes_without_transaction().await
1807                    }).await
1808                }
1809            }
1810        }
1811
1812        impl ::sql_orm::MigrationModelSource for #ident {
1813            fn entity_metadata() -> &'static [&'static ::sql_orm::EntityMetadata] {
1814                static #migration_entity_metadata_static:
1815                    ::std::sync::OnceLock<
1816                        ::std::boxed::Box<[&'static ::sql_orm::EntityMetadata]>
1817                    > = ::std::sync::OnceLock::new();
1818
1819                #migration_entity_metadata_static
1820                    .get_or_init(|| {
1821                        ::std::boxed::Box::new([#(#migration_entity_metadata),*])
1822                    })
1823                    .as_ref()
1824            }
1825        }
1826
1827        #(#dbset_access_impls)*
1828    })
1829}
1830
1831fn derive_insertable_impl(input: DeriveInput) -> Result<TokenStream2> {
1832    let ident = input.ident;
1833    let model_config = parse_persistence_model_config(&input.attrs, "Insertable")?;
1834    let entity = model_config
1835        .entity
1836        .as_ref()
1837        .expect("validated persistence model must include entity");
1838    let fields = extract_named_fields(&ident, input.data, "Insertable")?;
1839
1840    let values = fields
1841        .iter()
1842        .map(|field| {
1843            let field_ident = field
1844                .ident
1845                .as_ref()
1846                .ok_or_else(|| Error::new_spanned(field, "Insertable requiere campos nombrados"))?;
1847            let field_config = parse_persistence_field_config(field, "Insertable")?;
1848            let field_ty = &field.ty;
1849            let column_name =
1850                persistence_column_name_expr(entity, field_ident, field_config.column.as_ref());
1851
1852            Ok(quote! {
1853                ::sql_orm::core::ColumnValue::new(
1854                    #column_name,
1855                    <#field_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
1856                        ::core::clone::Clone::clone(&self.#field_ident)
1857                    ),
1858                )
1859            })
1860        })
1861        .collect::<Result<Vec<_>>>()?;
1862
1863    Ok(quote! {
1864        impl ::sql_orm::core::Insertable<#entity> for #ident {
1865            fn values(&self) -> ::std::vec::Vec<::sql_orm::core::ColumnValue> {
1866                ::std::vec![#(#values),*]
1867            }
1868        }
1869    })
1870}
1871
1872fn derive_changeset_impl(input: DeriveInput) -> Result<TokenStream2> {
1873    let ident = input.ident;
1874    let model_config = parse_persistence_model_config(&input.attrs, "Changeset")?;
1875    let entity = model_config
1876        .entity
1877        .as_ref()
1878        .expect("validated persistence model must include entity");
1879    let fields = extract_named_fields(&ident, input.data, "Changeset")?;
1880
1881    let changes = fields
1882        .iter()
1883        .map(|field| {
1884            let field_ident = field
1885                .ident
1886                .as_ref()
1887                .ok_or_else(|| Error::new_spanned(field, "Changeset requiere campos nombrados"))?;
1888            let field_config = parse_persistence_field_config(field, "Changeset")?;
1889            let inner_ty = option_inner_type(&field.ty).ok_or_else(|| {
1890                Error::new_spanned(
1891                    &field.ty,
1892                    "Changeset requiere Option<T> en cada campo para distinguir campos omitidos",
1893                )
1894            })?;
1895            let column_name =
1896                persistence_column_name_expr(entity, field_ident, field_config.column.as_ref());
1897
1898            Ok(quote! {
1899                let column_name = #column_name;
1900                let column = <#entity as ::sql_orm::core::Entity>::metadata()
1901                    .column(column_name)
1902                    .expect("generated Changeset field must reference existing entity metadata");
1903
1904                if let ::core::option::Option::Some(value) = &self.#field_ident {
1905                    if column.updatable {
1906                        changes.push(::sql_orm::core::ColumnValue::new(
1907                            column_name,
1908                            <#inner_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
1909                                ::core::clone::Clone::clone(value)
1910                            ),
1911                        ));
1912                    }
1913                }
1914            })
1915        })
1916        .collect::<Result<Vec<_>>>()?;
1917
1918    let concurrency_tokens = fields
1919        .iter()
1920        .map(|field| {
1921            let field_ident = field
1922                .ident
1923                .as_ref()
1924                .ok_or_else(|| Error::new_spanned(field, "Changeset requiere campos nombrados"))?;
1925            let field_config = parse_persistence_field_config(field, "Changeset")?;
1926            let inner_ty = option_inner_type(&field.ty).ok_or_else(|| {
1927                Error::new_spanned(
1928                    &field.ty,
1929                    "Changeset requiere Option<T> en cada campo para distinguir campos omitidos",
1930                )
1931            })?;
1932            let column_name =
1933                persistence_column_name_expr(entity, field_ident, field_config.column.as_ref());
1934
1935            Ok(quote! {
1936                let column_name = #column_name;
1937                let column = <#entity as ::sql_orm::core::Entity>::metadata()
1938                    .column(column_name)
1939                    .expect("generated Changeset field must reference existing entity metadata");
1940
1941                if column.rowversion {
1942                    if let ::core::option::Option::Some(value) = &self.#field_ident {
1943                        return Ok(Some(
1944                            <#inner_ty as ::sql_orm::core::SqlTypeMapping>::to_sql_value(
1945                                ::core::clone::Clone::clone(value)
1946                            )
1947                        ));
1948                    }
1949                }
1950            })
1951        })
1952        .collect::<Result<Vec<_>>>()?;
1953
1954    Ok(quote! {
1955        impl ::sql_orm::core::Changeset<#entity> for #ident {
1956            fn changes(&self) -> ::std::vec::Vec<::sql_orm::core::ColumnValue> {
1957                let mut changes = ::std::vec::Vec::new();
1958                #(#changes)*
1959                changes
1960            }
1961
1962            fn concurrency_token(&self) -> Result<::core::option::Option<::sql_orm::core::SqlValue>, ::sql_orm::core::OrmError> {
1963                #(#concurrency_tokens)*
1964                Ok(None)
1965            }
1966        }
1967    })
1968}
1969
1970fn extract_named_fields(
1971    ident: &Ident,
1972    data: Data,
1973    derive_name: &str,
1974) -> Result<syn::punctuated::Punctuated<Field, syn::token::Comma>> {
1975    match data {
1976        Data::Struct(data) => match data.fields {
1977            Fields::Named(fields) => Ok(fields.named),
1978            _ => Err(Error::new_spanned(
1979                ident,
1980                format!("{derive_name} solo soporta structs con campos nombrados"),
1981            )),
1982        },
1983        _ => Err(Error::new_spanned(
1984            ident,
1985            format!("{derive_name} solo soporta structs"),
1986        )),
1987    }
1988}
1989
1990fn has_explicit_primary_key(
1991    fields: &syn::punctuated::Punctuated<Field, syn::token::Comma>,
1992) -> Result<bool> {
1993    for field in fields {
1994        if parse_field_config(field)?.primary_key {
1995            return Ok(true);
1996        }
1997    }
1998    Ok(false)
1999}
2000
2001fn parse_persistence_model_config(
2002    attrs: &[syn::Attribute],
2003    derive_name: &str,
2004) -> Result<PersistenceModelConfig> {
2005    let mut config = PersistenceModelConfig::default();
2006
2007    for attr in attrs {
2008        if !attr.path().is_ident("orm") {
2009            continue;
2010        }
2011
2012        attr.parse_nested_meta(|meta| {
2013            if meta.path.is_ident("entity") {
2014                config.entity = Some(meta.value()?.parse()?);
2015            } else {
2016                return Err(meta.error(format!(
2017                    "atributo orm no soportado a nivel de {derive_name}"
2018                )));
2019            }
2020
2021            Ok(())
2022        })?;
2023    }
2024
2025    let Some(entity) = config.entity else {
2026        return Err(Error::new(
2027            Span::call_site(),
2028            format!("{derive_name} requiere #[orm(entity = MiEntidad)]"),
2029        ));
2030    };
2031
2032    Ok(PersistenceModelConfig {
2033        entity: Some(entity),
2034    })
2035}
2036
2037fn parse_entity_config(attrs: &[syn::Attribute]) -> Result<EntityConfig> {
2038    let mut config = EntityConfig::default();
2039
2040    for attr in attrs {
2041        if !attr.path().is_ident("orm") {
2042            continue;
2043        }
2044
2045        attr.parse_nested_meta(|meta| {
2046            if meta.path.is_ident("table") {
2047                config.table = Some(parse_lit_str(meta.value()?.parse()?)?);
2048            } else if meta.path.is_ident("schema") {
2049                config.schema = Some(parse_lit_str(meta.value()?.parse()?)?);
2050            } else if meta.path.is_ident("renamed_from") {
2051                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2052            } else if meta.path.is_ident("audit") {
2053                if config.audit.is_some() {
2054                    return Err(meta.error(
2055                        "Entity solo soporta una policy audit; multiples policies que generen columnas solapadas deben rechazarse explicitamente",
2056                    ));
2057                }
2058                config.audit = Some(meta.value()?.parse()?);
2059            } else if meta.path.is_ident("soft_delete") {
2060                if config.soft_delete.is_some() {
2061                    return Err(meta.error(
2062                        "Entity solo soporta una policy soft_delete; multiples policies que generen columnas solapadas deben rechazarse explicitamente",
2063                    ));
2064                }
2065                config.soft_delete = Some(meta.value()?.parse()?);
2066            } else if meta.path.is_ident("tenant") {
2067                if config.tenant.is_some() {
2068                    return Err(meta.error(
2069                        "Entity solo soporta una policy tenant; multiples tenants deben rechazarse explicitamente",
2070                    ));
2071                }
2072                config.tenant = Some(meta.value()?.parse()?);
2073            } else if meta.path.is_ident("index") {
2074                config.indexes.push(parse_entity_index_config(meta)?);
2075            } else {
2076                return Err(meta.error("atributo orm no soportado a nivel de entidad"));
2077            }
2078
2079            Ok(())
2080        })?;
2081    }
2082
2083    Ok(config)
2084}
2085
2086fn parse_entity_index_config(meta: syn::meta::ParseNestedMeta<'_>) -> Result<EntityIndexConfig> {
2087    let mut index = EntityIndexConfig::default();
2088
2089    meta.parse_nested_meta(|nested| {
2090        if nested.path.is_ident("name") {
2091            index.name = Some(parse_lit_str(nested.value()?.parse()?)?);
2092        } else if nested.path.is_ident("unique") {
2093            index.unique = true;
2094        } else if nested.path.is_ident("columns") {
2095            let content;
2096            syn::parenthesized!(content in nested.input);
2097            let columns = Punctuated::<Ident, Token![,]>::parse_terminated(&content)?;
2098            index.columns.extend(columns);
2099        } else {
2100            return Err(nested.error("index de entidad solo soporta name, unique y columns(...)"));
2101        }
2102
2103        Ok(())
2104    })?;
2105
2106    if index.columns.is_empty() {
2107        return Err(meta.error("index a nivel de entidad requiere columns(campo1, campo2, ...)"));
2108    }
2109
2110    Ok(index)
2111}
2112
2113fn parse_persistence_field_config(
2114    field: &Field,
2115    derive_name: &str,
2116) -> Result<PersistenceFieldConfig> {
2117    let mut config = PersistenceFieldConfig::default();
2118
2119    for attr in &field.attrs {
2120        if !attr.path().is_ident("orm") {
2121            continue;
2122        }
2123
2124        attr.parse_nested_meta(|meta| {
2125            if meta.path.is_ident("column") {
2126                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2127            } else {
2128                return Err(meta.error(format!(
2129                    "atributo orm no soportado en campos de {derive_name}"
2130                )));
2131            }
2132
2133            Ok(())
2134        })?;
2135    }
2136
2137    Ok(config)
2138}
2139
2140fn parse_field_config(field: &Field) -> Result<FieldConfig> {
2141    let mut config = FieldConfig::default();
2142
2143    for attr in &field.attrs {
2144        if !attr.path().is_ident("orm") {
2145            continue;
2146        }
2147
2148        attr.parse_nested_meta(|meta| {
2149            if meta.path.is_ident("column") {
2150                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2151            } else if meta.path.is_ident("primary_key") {
2152                config.primary_key = true;
2153            } else if meta.path.is_ident("identity") {
2154                config.identity = true;
2155                if !meta.input.is_empty() {
2156                    meta.parse_nested_meta(|nested| {
2157                        if nested.path.is_ident("seed") {
2158                            config.identity_seed = Some(parse_i64_expr(nested.value()?.parse()?)?);
2159                        } else if nested.path.is_ident("increment") {
2160                            config.identity_increment =
2161                                Some(parse_i64_expr(nested.value()?.parse()?)?);
2162                        } else {
2163                            return Err(nested.error("identity solo soporta seed e increment"));
2164                        }
2165
2166                        Ok(())
2167                    })?;
2168                }
2169            } else if meta.path.is_ident("length") {
2170                config.length = Some(parse_u32_expr(meta.value()?.parse()?)?);
2171            } else if meta.path.is_ident("nullable") {
2172                config.nullable = true;
2173            } else if meta.path.is_ident("default_sql") {
2174                config.default_sql = Some(parse_lit_str(meta.value()?.parse()?)?);
2175            } else if meta.path.is_ident("renamed_from") {
2176                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2177            } else if meta.path.is_ident("index") {
2178                let mut index = IndexConfig::default();
2179                meta.parse_nested_meta(|nested| {
2180                    if nested.path.is_ident("name") {
2181                        index.name = Some(parse_lit_str(nested.value()?.parse()?)?);
2182                    } else {
2183                        return Err(nested.error("index solo soporta name"));
2184                    }
2185
2186                    Ok(())
2187                })?;
2188                config.indexes.push(index);
2189            } else if meta.path.is_ident("unique") {
2190                config.indexes.push(IndexConfig {
2191                    unique: true,
2192                    ..IndexConfig::default()
2193                });
2194            } else if meta.path.is_ident("sql_type") {
2195                config.sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2196            } else if meta.path.is_ident("precision") {
2197                config.precision = Some(parse_u8_expr(meta.value()?.parse()?)?);
2198            } else if meta.path.is_ident("scale") {
2199                config.scale = Some(parse_u8_expr(meta.value()?.parse()?)?);
2200            } else if meta.path.is_ident("computed_sql") {
2201                config.computed_sql = Some(parse_lit_str(meta.value()?.parse()?)?);
2202            } else if meta.path.is_ident("rowversion") {
2203                config.rowversion = true;
2204            } else if meta.path.is_ident("foreign_key") {
2205                config.foreign_key = Some(parse_foreign_key_config(meta)?);
2206            } else if meta.path.is_ident("belongs_to") {
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 = Some(parse_navigation_config(
2213                    meta,
2214                    NavigationKindConfig::BelongsTo,
2215                )?);
2216            } else if meta.path.is_ident("has_one") {
2217                if config.navigation.is_some() {
2218                    return Err(meta.error(
2219                        "un campo solo puede declarar una navegación: belongs_to, has_one o has_many",
2220                    ));
2221                }
2222                config.navigation =
2223                    Some(parse_navigation_config(meta, NavigationKindConfig::HasOne)?);
2224            } else if meta.path.is_ident("has_many") {
2225                if config.navigation.is_some() {
2226                    return Err(meta.error(
2227                        "un campo solo puede declarar una navegación: belongs_to, has_one o has_many",
2228                    ));
2229                }
2230                config.navigation = Some(parse_navigation_config(
2231                    meta,
2232                    NavigationKindConfig::HasMany,
2233                )?);
2234            } else if meta.path.is_ident("many_to_many") {
2235                return Err(meta.error(
2236                    "many_to_many directo no está soportado todavía; modele la relación con una entidad intermedia explícita",
2237                ));
2238            } else if meta.path.is_ident("on_delete") {
2239                config.on_delete = Some(parse_referential_action_expr(meta.value()?.parse()?)?);
2240            } else {
2241                return Err(meta.error("atributo orm no soportado a nivel de campo"));
2242            }
2243
2244            Ok(())
2245        })?;
2246    }
2247
2248    if config.navigation.is_some()
2249        && (config.column.is_some()
2250            || config.primary_key
2251            || config.identity
2252            || config.nullable
2253            || config.length.is_some()
2254            || config.default_sql.is_some()
2255            || config.renamed_from.is_some()
2256            || config.computed_sql.is_some()
2257            || config.rowversion
2258            || config.sql_type.is_some()
2259            || config.precision.is_some()
2260            || config.scale.is_some()
2261            || !config.indexes.is_empty()
2262            || config.foreign_key.is_some()
2263            || config.on_delete.is_some())
2264    {
2265        return Err(Error::new_spanned(
2266            field,
2267            "los campos de navegación solo soportan belongs_to, has_one o has_many; no se generan columnas para ellos",
2268        ));
2269    }
2270
2271    if config.navigation.is_none() && is_navigation_wrapper_type(&field.ty) {
2272        return Err(Error::new_spanned(
2273            field,
2274            "los campos Navigation<T> y Collection<T> requieren #[orm(belongs_to(...))], #[orm(has_one(...))] o #[orm(has_many(...))]",
2275        ));
2276    }
2277
2278    Ok(config)
2279}
2280
2281fn parse_policy_field_config(field: &Field, kind: PolicyFieldsKind) -> Result<AuditFieldConfig> {
2282    let mut config = AuditFieldConfig::default();
2283    let derive_name = kind.derive_name();
2284
2285    for attr in &field.attrs {
2286        if !attr.path().is_ident("orm") {
2287            continue;
2288        }
2289
2290        attr.parse_nested_meta(|meta| {
2291            if meta.path.is_ident("column") {
2292                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2293            } else if meta.path.is_ident("length") {
2294                config.length = Some(parse_u32_expr(meta.value()?.parse()?)?);
2295            } else if meta.path.is_ident("nullable") {
2296                config.nullable = true;
2297            } else if meta.path.is_ident("default_sql") {
2298                config.default_sql = Some(parse_lit_str(meta.value()?.parse()?)?);
2299            } else if meta.path.is_ident("renamed_from") {
2300                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2301            } else if meta.path.is_ident("sql_type") {
2302                config.sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2303            } else if meta.path.is_ident("precision") {
2304                config.precision = Some(parse_u8_expr(meta.value()?.parse()?)?);
2305            } else if meta.path.is_ident("scale") {
2306                config.scale = Some(parse_u8_expr(meta.value()?.parse()?)?);
2307            } else if meta.path.is_ident("insertable") {
2308                config.insertable = Some(parse_bool_expr(meta.value()?.parse()?)?);
2309            } else if meta.path.is_ident("updatable") {
2310                config.updatable = Some(parse_bool_expr(meta.value()?.parse()?)?);
2311            } else if (matches!(kind, PolicyFieldsKind::Audit)
2312                && (meta.path.is_ident("created_at")
2313                    || meta.path.is_ident("created_by")
2314                    || meta.path.is_ident("updated_at")
2315                    || meta.path.is_ident("updated_by")))
2316                || (matches!(kind, PolicyFieldsKind::SoftDelete)
2317                    && (meta.path.is_ident("deleted_at")
2318                        || meta.path.is_ident("deleted_by")
2319                        || meta.path.is_ident("is_deleted")))
2320            {
2321            } else {
2322                return Err(meta.error(format!(
2323                    "atributo orm no soportado en campos de {derive_name}"
2324                )));
2325            }
2326
2327            Ok(())
2328        })?;
2329    }
2330
2331    Ok(config)
2332}
2333
2334fn parse_tenant_context_field_config(field: &Field) -> Result<TenantContextFieldConfig> {
2335    let mut config = TenantContextFieldConfig::default();
2336
2337    for attr in &field.attrs {
2338        if !attr.path().is_ident("orm") {
2339            continue;
2340        }
2341
2342        attr.parse_nested_meta(|meta| {
2343            if meta.path.is_ident("column") {
2344                config.column = Some(parse_lit_str(meta.value()?.parse()?)?);
2345            } else if meta.path.is_ident("length") {
2346                config.length = Some(parse_u32_expr(meta.value()?.parse()?)?);
2347            } else if meta.path.is_ident("renamed_from") {
2348                config.renamed_from = Some(parse_lit_str(meta.value()?.parse()?)?);
2349            } else if meta.path.is_ident("sql_type") {
2350                config.sql_type = Some(parse_lit_str(meta.value()?.parse()?)?);
2351            } else if meta.path.is_ident("precision") {
2352                config.precision = Some(parse_u8_expr(meta.value()?.parse()?)?);
2353            } else if meta.path.is_ident("scale") {
2354                config.scale = Some(parse_u8_expr(meta.value()?.parse()?)?);
2355            } else {
2356                return Err(meta.error("atributo orm no soportado en campos de TenantContext"));
2357            }
2358
2359            Ok(())
2360        })?;
2361    }
2362
2363    Ok(config)
2364}
2365
2366fn parse_lit_str(expr: Expr) -> Result<LitStr> {
2367    match expr {
2368        Expr::Lit(ExprLit {
2369            lit: Lit::Str(value),
2370            ..
2371        }) => Ok(value),
2372        other => Err(Error::new_spanned(other, "se esperaba un string literal")),
2373    }
2374}
2375
2376fn validate_non_empty_lit_str(value: &LitStr, message: &str) -> Result<()> {
2377    if value.value().is_empty() {
2378        return Err(Error::new_spanned(value, message));
2379    }
2380
2381    Ok(())
2382}
2383
2384fn parse_bool_expr(expr: Expr) -> Result<bool> {
2385    match expr {
2386        Expr::Lit(ExprLit {
2387            lit: Lit::Bool(value),
2388            ..
2389        }) => Ok(value.value),
2390        other => Err(Error::new_spanned(other, "se esperaba un boolean literal")),
2391    }
2392}
2393
2394fn parse_foreign_key_config(meta: syn::meta::ParseNestedMeta<'_>) -> Result<ForeignKeyConfig> {
2395    if meta.input.peek(Token![=]) {
2396        return parse_foreign_key_string_config(meta.value()?.parse()?);
2397    }
2398
2399    let mut entity = None;
2400    let mut column = None;
2401    let mut name = None;
2402
2403    meta.parse_nested_meta(|nested| {
2404        if nested.path.is_ident("entity") {
2405            let path: Path = nested.value()?.parse()?;
2406            entity = Some(path);
2407        } else if nested.path.is_ident("column") {
2408            column = Some(nested.value()?.parse::<Ident>()?);
2409        } else if nested.path.is_ident("name") {
2410            name = Some(parse_lit_str(nested.value()?.parse()?)?);
2411        } else {
2412            return Err(nested.error("foreign_key solo soporta entity, column y name"));
2413        }
2414
2415        Ok(())
2416    })?;
2417
2418    let entity = entity.ok_or_else(|| meta.error("foreign_key requiere entity = MiEntidad"))?;
2419    let column = column.ok_or_else(|| meta.error("foreign_key requiere column = id"))?;
2420    let generated_table_name = default_table_name_from_path(&entity)?;
2421
2422    Ok(ForeignKeyConfig {
2423        name,
2424        generated_referenced_table_name: generated_table_name,
2425        target: ForeignKeyTarget::Structured { entity, column },
2426    })
2427}
2428
2429fn parse_navigation_config(
2430    meta: syn::meta::ParseNestedMeta<'_>,
2431    kind: NavigationKindConfig,
2432) -> Result<NavigationConfig> {
2433    let content;
2434    syn::parenthesized!(content in meta.input);
2435
2436    let target: Path = content.parse()?;
2437    let mut foreign_key = None;
2438
2439    while !content.is_empty() {
2440        content.parse::<Token![,]>()?;
2441        if content.is_empty() {
2442            break;
2443        }
2444
2445        let key: Ident = content.parse()?;
2446        content.parse::<Token![=]>()?;
2447
2448        if key == "foreign_key" {
2449            if foreign_key.is_some() {
2450                return Err(Error::new(
2451                    key.span(),
2452                    format!("{} no permite foreign_key duplicado", kind.attribute_name()),
2453                ));
2454            }
2455            foreign_key = Some(content.parse()?);
2456        } else {
2457            return Err(Error::new(
2458                key.span(),
2459                format!("{} solo soporta foreign_key", kind.attribute_name()),
2460            ));
2461        }
2462    }
2463
2464    let foreign_key = foreign_key.ok_or_else(|| {
2465        meta.error(format!(
2466            "{} requiere foreign_key = campo",
2467            kind.attribute_name()
2468        ))
2469    })?;
2470
2471    Ok(NavigationConfig {
2472        kind,
2473        target,
2474        foreign_key,
2475    })
2476}
2477
2478fn validate_repeated_structured_foreign_keys(
2479    foreign_keys: &BTreeMap<String, FieldForeignKeyInfo>,
2480) -> Result<()> {
2481    let mut targets = BTreeMap::<&str, Vec<&FieldForeignKeyInfo>>::new();
2482
2483    for foreign_key in foreign_keys.values() {
2484        let Some(target) = foreign_key.structured_target.as_deref() else {
2485            continue;
2486        };
2487        targets.entry(target).or_default().push(foreign_key);
2488    }
2489
2490    for foreign_keys_for_target in targets.values() {
2491        if foreign_keys_for_target.len() < 2 {
2492            continue;
2493        }
2494
2495        if let Some(unnamed_foreign_key) = foreign_keys_for_target
2496            .iter()
2497            .find(|foreign_key| !foreign_key.has_explicit_name)
2498        {
2499            return Err(Error::new(
2500                unnamed_foreign_key.field_span,
2501                "múltiples foreign keys estructuradas al mismo target requieren name explícito para desambiguar navegaciones",
2502            ));
2503        }
2504    }
2505
2506    Ok(())
2507}
2508
2509fn parse_foreign_key_string_config(expr: Expr) -> Result<ForeignKeyConfig> {
2510    let value = parse_lit_str(expr)?;
2511    let raw = value.value();
2512    let segments = raw.split('.').collect::<Vec<_>>();
2513
2514    let (referenced_schema, referenced_table, referenced_column) = match segments.as_slice() {
2515        [table, column] => (
2516            LitStr::new("dbo", value.span()),
2517            LitStr::new(table, value.span()),
2518            LitStr::new(column, value.span()),
2519        ),
2520        [schema, table, column] => (
2521            LitStr::new(schema, value.span()),
2522            LitStr::new(table, value.span()),
2523            LitStr::new(column, value.span()),
2524        ),
2525        _ => {
2526            return Err(Error::new_spanned(
2527                value,
2528                "foreign_key requiere el formato \"tabla.columna\" o \"schema.tabla.columna\", o la forma estructurada foreign_key(entity = Customer, column = id)",
2529            ));
2530        }
2531    };
2532
2533    if referenced_schema.value().is_empty()
2534        || referenced_table.value().is_empty()
2535        || referenced_column.value().is_empty()
2536    {
2537        return Err(Error::new_spanned(
2538            value,
2539            "foreign_key no permite segmentos vacíos",
2540        ));
2541    }
2542
2543    Ok(ForeignKeyConfig {
2544        name: None,
2545        generated_referenced_table_name: referenced_table.value(),
2546        target: ForeignKeyTarget::Legacy {
2547            referenced_schema,
2548            referenced_table,
2549            referenced_column,
2550        },
2551    })
2552}
2553
2554fn parse_referential_action_expr(expr: Expr) -> Result<ReferentialActionConfig> {
2555    let value = parse_lit_str(expr)?;
2556    match value.value().to_ascii_lowercase().as_str() {
2557        "no action" => Ok(ReferentialActionConfig::NoAction),
2558        "cascade" => Ok(ReferentialActionConfig::Cascade),
2559        "set null" => Ok(ReferentialActionConfig::SetNull),
2560        _ => Err(Error::new_spanned(
2561            value,
2562            "solo se soportan los valores \"no action\", \"cascade\" y \"set null\"",
2563        )),
2564    }
2565}
2566
2567fn parse_u32_expr(expr: Expr) -> Result<u32> {
2568    parse_int::<u32>(expr, "se esperaba un entero u32")
2569}
2570
2571fn parse_u8_expr(expr: Expr) -> Result<u8> {
2572    parse_int::<u8>(expr, "se esperaba un entero u8")
2573}
2574
2575fn parse_i64_expr(expr: Expr) -> Result<i64> {
2576    parse_int::<i64>(expr, "se esperaba un entero i64")
2577}
2578
2579fn parse_int<T>(expr: Expr, message: &str) -> Result<T>
2580where
2581    T: std::str::FromStr,
2582    <T as std::str::FromStr>::Err: std::fmt::Display,
2583{
2584    match expr {
2585        Expr::Lit(ExprLit {
2586            lit: Lit::Int(value),
2587            ..
2588        }) => value
2589            .base10_parse::<T>()
2590            .map_err(|_| Error::new_spanned(value, message)),
2591        other => Err(Error::new_spanned(other, message)),
2592    }
2593}
2594
2595fn option_lit_str(value: Option<LitStr>) -> TokenStream2 {
2596    match value {
2597        Some(value) => quote! { Some(#value) },
2598        None => quote! { None },
2599    }
2600}
2601
2602fn persistence_column_name_expr(
2603    entity: &Type,
2604    field_ident: &Ident,
2605    explicit_column: Option<&LitStr>,
2606) -> TokenStream2 {
2607    let field_name = LitStr::new(&field_ident.to_string(), field_ident.span());
2608
2609    match explicit_column {
2610        Some(column_name) => {
2611            let error = LitStr::new(
2612                &format!(
2613                    "la columna '{}' no existe en la metadata de la entidad de destino",
2614                    column_name.value()
2615                ),
2616                column_name.span(),
2617            );
2618
2619            quote! {{
2620                <#entity as ::sql_orm::core::Entity>::metadata()
2621                    .column(#column_name)
2622                    .expect(#error)
2623                    .column_name
2624            }}
2625        }
2626        None => {
2627            let error = LitStr::new(
2628                &format!(
2629                    "el campo '{}' no existe en la metadata de la entidad de destino",
2630                    field_ident
2631                ),
2632                field_ident.span(),
2633            );
2634
2635            quote! {{
2636                <#entity as ::sql_orm::core::Entity>::metadata()
2637                    .field(#field_name)
2638                    .expect(#error)
2639                    .column_name
2640            }}
2641        }
2642    }
2643}
2644
2645fn option_number<T>(value: Option<T>) -> TokenStream2
2646where
2647    T: quote::ToTokens,
2648{
2649    match value {
2650        Some(value) => quote! { Some(#value) },
2651        None => quote! { None },
2652    }
2653}
2654
2655fn referential_action_tokens(action: ReferentialActionConfig) -> TokenStream2 {
2656    match action {
2657        ReferentialActionConfig::NoAction => {
2658            quote! { ::sql_orm::core::ReferentialAction::NoAction }
2659        }
2660        ReferentialActionConfig::Cascade => {
2661            quote! { ::sql_orm::core::ReferentialAction::Cascade }
2662        }
2663        ReferentialActionConfig::SetNull => {
2664            quote! { ::sql_orm::core::ReferentialAction::SetNull }
2665        }
2666    }
2667}
2668
2669fn navigation_kind_tokens(kind: NavigationKindConfig) -> TokenStream2 {
2670    match kind {
2671        NavigationKindConfig::BelongsTo => quote! { ::sql_orm::core::NavigationKind::BelongsTo },
2672        NavigationKindConfig::HasOne => quote! { ::sql_orm::core::NavigationKind::HasOne },
2673        NavigationKindConfig::HasMany => quote! { ::sql_orm::core::NavigationKind::HasMany },
2674    }
2675}
2676
2677fn include_navigation_impls(
2678    entity_ident: &Ident,
2679    navigations: &[PendingNavigation],
2680) -> Result<Vec<TokenStream2>> {
2681    let mut grouped =
2682        BTreeMap::<String, (Path, Vec<(Ident, LitStr, NavigationWrapperConfig)>)>::new();
2683
2684    for navigation in navigations {
2685        if !matches!(
2686            navigation.kind,
2687            NavigationKindConfig::BelongsTo | NavigationKindConfig::HasOne
2688        ) {
2689            continue;
2690        }
2691
2692        let field_ident = Ident::new(&navigation.rust_field.value(), navigation.rust_field.span());
2693        let target = &navigation.target;
2694        let key = quote! { #target }.to_string();
2695        grouped
2696            .entry(key)
2697            .or_insert_with(|| (navigation.target.clone(), Vec::new()))
2698            .1
2699            .push((
2700                field_ident,
2701                navigation.rust_field.clone(),
2702                navigation.wrapper,
2703            ));
2704    }
2705
2706    grouped
2707        .into_values()
2708        .map(|(target, fields)| {
2709            let arms = fields.into_iter().map(|(field_ident, rust_field, wrapper)| {
2710                let assignment = match wrapper {
2711                    NavigationWrapperConfig::Eager => {
2712                        quote! { self.#field_ident = ::sql_orm::Navigation::from_option(value); }
2713                    }
2714                    NavigationWrapperConfig::Lazy => {
2715                        quote! { self.#field_ident = ::sql_orm::LazyNavigation::from_option(value); }
2716                    }
2717                };
2718
2719                quote! {
2720                    #rust_field => {
2721                        #assignment
2722                        Ok(())
2723                    }
2724                }
2725            });
2726
2727            Ok(quote! {
2728                impl ::sql_orm::IncludeNavigation<#target> for #entity_ident {
2729                    fn set_included_navigation(
2730                        &mut self,
2731                        navigation: &str,
2732                        value: ::core::option::Option<#target>,
2733                    ) -> ::core::result::Result<(), ::sql_orm::core::OrmError> {
2734                        match navigation {
2735                            #(#arms,)*
2736                            _ => Err(::sql_orm::core::OrmError::new(
2737                                ::std::format!(
2738                                    "entity `{}` does not support include navigation `{}` for `{}`",
2739                                    <Self as ::sql_orm::core::Entity>::metadata().rust_name,
2740                                    navigation,
2741                                    ::core::any::type_name::<#target>(),
2742                                )
2743                            )),
2744                        }
2745                    }
2746                }
2747            })
2748        })
2749        .collect()
2750}
2751
2752fn include_collection_impls(
2753    entity_ident: &Ident,
2754    navigations: &[PendingNavigation],
2755) -> Result<Vec<TokenStream2>> {
2756    let mut grouped =
2757        BTreeMap::<String, (Path, Vec<(Ident, LitStr, NavigationWrapperConfig)>)>::new();
2758
2759    for navigation in navigations {
2760        if !matches!(navigation.kind, NavigationKindConfig::HasMany) {
2761            continue;
2762        }
2763
2764        let field_ident = Ident::new(&navigation.rust_field.value(), navigation.rust_field.span());
2765        let target = &navigation.target;
2766        let key = quote! { #target }.to_string();
2767        grouped
2768            .entry(key)
2769            .or_insert_with(|| (navigation.target.clone(), Vec::new()))
2770            .1
2771            .push((
2772                field_ident,
2773                navigation.rust_field.clone(),
2774                navigation.wrapper,
2775            ));
2776    }
2777
2778    grouped
2779        .into_values()
2780        .map(|(target, fields)| {
2781            let arms = fields.into_iter().map(|(field_ident, rust_field, wrapper)| {
2782                let assignment = match wrapper {
2783                    NavigationWrapperConfig::Eager => {
2784                        quote! { self.#field_ident = ::sql_orm::Collection::from_vec(values); }
2785                    }
2786                    NavigationWrapperConfig::Lazy => {
2787                        quote! { self.#field_ident = ::sql_orm::LazyCollection::from_vec(values); }
2788                    }
2789                };
2790
2791                quote! {
2792                    #rust_field => {
2793                        #assignment
2794                        Ok(())
2795                    }
2796                }
2797            });
2798
2799            Ok(quote! {
2800                impl ::sql_orm::IncludeCollection<#target> for #entity_ident {
2801                    fn set_included_collection(
2802                        &mut self,
2803                        navigation: &str,
2804                        values: ::std::vec::Vec<#target>,
2805                    ) -> ::core::result::Result<(), ::sql_orm::core::OrmError> {
2806                        match navigation {
2807                            #(#arms,)*
2808                            _ => Err(::sql_orm::core::OrmError::new(
2809                                ::std::format!(
2810                                    "entity `{}` does not support include collection `{}` for `{}`",
2811                                    <Self as ::sql_orm::core::Entity>::metadata().rust_name,
2812                                    navigation,
2813                                    ::core::any::type_name::<#target>(),
2814                                )
2815                            )),
2816                        }
2817                    }
2818                }
2819            })
2820        })
2821        .collect()
2822}
2823
2824fn validate_navigation_field_type(
2825    ty: &Type,
2826    navigation: &NavigationConfig,
2827) -> Result<NavigationWrapperConfig> {
2828    let expected_wrappers = match navigation.kind {
2829        NavigationKindConfig::BelongsTo | NavigationKindConfig::HasOne => {
2830            ("Navigation", "LazyNavigation")
2831        }
2832        NavigationKindConfig::HasMany => ("Collection", "LazyCollection"),
2833    };
2834
2835    let (actual_target, wrapper) = navigation_wrapper_inner_last_ident(ty, expected_wrappers)
2836        .ok_or_else(|| {
2837            Error::new_spanned(
2838                ty,
2839                format!(
2840                    "{} requiere un campo {}<{}> o {}<{}>",
2841                    navigation.kind.attribute_name(),
2842                    expected_wrappers.0,
2843                    path_last_ident(&navigation.target)
2844                        .map(|ident| ident.to_string())
2845                        .unwrap_or_else(|| "Entidad".to_string()),
2846                    expected_wrappers.1,
2847                    path_last_ident(&navigation.target)
2848                        .map(|ident| ident.to_string())
2849                        .unwrap_or_else(|| "Entidad".to_string()),
2850                ),
2851            )
2852        })?;
2853
2854    let Some(expected_target) = path_last_ident(&navigation.target) else {
2855        return Err(Error::new_spanned(
2856            &navigation.target,
2857            format!(
2858                "{} requiere una ruta de entidad válida",
2859                navigation.kind.attribute_name()
2860            ),
2861        ));
2862    };
2863
2864    if actual_target != expected_target {
2865        return Err(Error::new_spanned(
2866            ty,
2867            format!(
2868                "{} apunta a {}, pero el campo usa {}",
2869                navigation.kind.attribute_name(),
2870                expected_target,
2871                actual_target,
2872            ),
2873        ));
2874    }
2875
2876    Ok(wrapper)
2877}
2878
2879fn navigation_wrapper_inner_last_ident<'a>(
2880    ty: &'a Type,
2881    wrappers: (&str, &str),
2882) -> Option<(&'a Ident, NavigationWrapperConfig)> {
2883    generic_wrapper_inner_last_ident(ty, wrappers.0)
2884        .map(|ident| (ident, NavigationWrapperConfig::Eager))
2885        .or_else(|| {
2886            generic_wrapper_inner_last_ident(ty, wrappers.1)
2887                .map(|ident| (ident, NavigationWrapperConfig::Lazy))
2888        })
2889}
2890
2891fn generic_wrapper_inner_last_ident<'a>(ty: &'a Type, wrapper: &str) -> Option<&'a Ident> {
2892    let Type::Path(type_path) = ty else {
2893        return None;
2894    };
2895
2896    let segment = type_path.path.segments.last()?;
2897    if segment.ident != wrapper {
2898        return None;
2899    }
2900
2901    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
2902        return None;
2903    };
2904
2905    let syn::GenericArgument::Type(Type::Path(inner)) = arguments.args.first()? else {
2906        return None;
2907    };
2908
2909    path_last_ident(&inner.path)
2910}
2911
2912fn is_navigation_wrapper_type(ty: &Type) -> bool {
2913    let Type::Path(type_path) = ty else {
2914        return false;
2915    };
2916
2917    type_path.path.segments.last().is_some_and(|segment| {
2918        segment.ident == "Navigation"
2919            || segment.ident == "Collection"
2920            || segment.ident == "LazyNavigation"
2921            || segment.ident == "LazyCollection"
2922    })
2923}
2924
2925fn path_last_ident(path: &Path) -> Option<&Ident> {
2926    path.segments.last().map(|segment| &segment.ident)
2927}
2928
2929fn path_key(path: &Path) -> String {
2930    quote! { #path }.to_string()
2931}
2932
2933fn infer_sql_type(type_info: &TypeInfo, rowversion: bool, ty: &Type) -> Result<TokenStream2> {
2934    if rowversion {
2935        return Ok(quote! { ::sql_orm::core::SqlServerType::RowVersion });
2936    }
2937
2938    let token = match type_info.kind {
2939        TypeKind::I64 => quote! { ::sql_orm::core::SqlServerType::BigInt },
2940        TypeKind::I32 => quote! { ::sql_orm::core::SqlServerType::Int },
2941        TypeKind::I16 => quote! { ::sql_orm::core::SqlServerType::SmallInt },
2942        TypeKind::U8 => quote! { ::sql_orm::core::SqlServerType::TinyInt },
2943        TypeKind::Bool => quote! { ::sql_orm::core::SqlServerType::Bit },
2944        TypeKind::String => quote! { ::sql_orm::core::SqlServerType::NVarChar },
2945        TypeKind::VecU8 => quote! { ::sql_orm::core::SqlServerType::VarBinary },
2946        TypeKind::Uuid => quote! { ::sql_orm::core::SqlServerType::UniqueIdentifier },
2947        TypeKind::NaiveDateTime => quote! { ::sql_orm::core::SqlServerType::DateTime2 },
2948        TypeKind::NaiveDate => quote! { ::sql_orm::core::SqlServerType::Date },
2949        TypeKind::Decimal => quote! { ::sql_orm::core::SqlServerType::Decimal },
2950        TypeKind::Float => quote! { ::sql_orm::core::SqlServerType::Float },
2951        TypeKind::Unknown => {
2952            return Err(Error::new_spanned(
2953                ty,
2954                "tipo Rust no soportado todavía para derive(Entity)",
2955            ));
2956        }
2957    };
2958
2959    Ok(token)
2960}
2961
2962fn sql_type_from_string(value: &LitStr) -> TokenStream2 {
2963    let normalized = value.value().to_ascii_lowercase();
2964
2965    if normalized.starts_with("bigint") {
2966        quote! { ::sql_orm::core::SqlServerType::BigInt }
2967    } else if normalized == "int" {
2968        quote! { ::sql_orm::core::SqlServerType::Int }
2969    } else if normalized.starts_with("smallint") {
2970        quote! { ::sql_orm::core::SqlServerType::SmallInt }
2971    } else if normalized.starts_with("tinyint") {
2972        quote! { ::sql_orm::core::SqlServerType::TinyInt }
2973    } else if normalized.starts_with("bit") {
2974        quote! { ::sql_orm::core::SqlServerType::Bit }
2975    } else if normalized.starts_with("uniqueidentifier") {
2976        quote! { ::sql_orm::core::SqlServerType::UniqueIdentifier }
2977    } else if normalized.starts_with("date") && !normalized.starts_with("datetime2") {
2978        quote! { ::sql_orm::core::SqlServerType::Date }
2979    } else if normalized.starts_with("datetime2") {
2980        quote! { ::sql_orm::core::SqlServerType::DateTime2 }
2981    } else if normalized.starts_with("decimal") {
2982        quote! { ::sql_orm::core::SqlServerType::Decimal }
2983    } else if normalized.starts_with("float") {
2984        quote! { ::sql_orm::core::SqlServerType::Float }
2985    } else if normalized.starts_with("money") {
2986        quote! { ::sql_orm::core::SqlServerType::Money }
2987    } else if normalized.starts_with("nvarchar") {
2988        quote! { ::sql_orm::core::SqlServerType::NVarChar }
2989    } else if normalized.starts_with("varbinary") {
2990        quote! { ::sql_orm::core::SqlServerType::VarBinary }
2991    } else if normalized.starts_with("rowversion") {
2992        quote! { ::sql_orm::core::SqlServerType::RowVersion }
2993    } else {
2994        quote! { ::sql_orm::core::SqlServerType::Custom(#value) }
2995    }
2996}
2997
2998fn analyze_type(ty: &Type) -> Result<TypeInfo> {
2999    let nullable = option_inner_type(ty).is_some();
3000    let effective = option_inner_type(ty).unwrap_or(ty);
3001    let kind = classify_type(effective)?;
3002
3003    Ok(TypeInfo {
3004        nullable,
3005        is_integer: matches!(
3006            kind,
3007            TypeKind::I64 | TypeKind::I32 | TypeKind::I16 | TypeKind::U8
3008        ),
3009        is_vec_u8: matches!(kind, TypeKind::VecU8),
3010        default_max_length: matches!(kind, TypeKind::String).then_some(255),
3011        default_precision: matches!(kind, TypeKind::Decimal).then_some(18),
3012        default_scale: matches!(kind, TypeKind::Decimal).then_some(2),
3013        kind,
3014    })
3015}
3016
3017fn classify_type(ty: &Type) -> Result<TypeKind> {
3018    match ty {
3019        Type::Path(type_path) => {
3020            let segment = type_path
3021                .path
3022                .segments
3023                .last()
3024                .ok_or_else(|| Error::new_spanned(type_path, "tipo inválido"))?;
3025
3026            let ident = segment.ident.to_string();
3027            let kind = match ident.as_str() {
3028                "i64" => TypeKind::I64,
3029                "i32" => TypeKind::I32,
3030                "i16" => TypeKind::I16,
3031                "u8" => TypeKind::U8,
3032                "bool" => TypeKind::Bool,
3033                "String" => TypeKind::String,
3034                "Uuid" => TypeKind::Uuid,
3035                "NaiveDateTime" => TypeKind::NaiveDateTime,
3036                "NaiveDate" => TypeKind::NaiveDate,
3037                "Decimal" => TypeKind::Decimal,
3038                "f32" | "f64" => TypeKind::Float,
3039                "Vec" if type_path_is_vec_u8(&type_path.path) => TypeKind::VecU8,
3040                _ => TypeKind::Unknown,
3041            };
3042
3043            Ok(kind)
3044        }
3045        _ => Ok(TypeKind::Unknown),
3046    }
3047}
3048
3049fn option_inner_type(ty: &Type) -> Option<&Type> {
3050    let Type::Path(type_path) = ty else {
3051        return None;
3052    };
3053
3054    let segment = type_path.path.segments.last()?;
3055    if segment.ident != "Option" {
3056        return None;
3057    }
3058
3059    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3060        return None;
3061    };
3062
3063    let syn::GenericArgument::Type(inner) = arguments.args.first()? else {
3064        return None;
3065    };
3066
3067    Some(inner)
3068}
3069
3070fn dbset_entity_type(ty: &Type) -> Option<&Type> {
3071    let Type::Path(type_path) = ty else {
3072        return None;
3073    };
3074
3075    let segment = type_path.path.segments.last()?;
3076    if segment.ident != "DbSet" {
3077        return None;
3078    }
3079
3080    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3081        return None;
3082    };
3083
3084    let syn::GenericArgument::Type(inner) = arguments.args.first()? else {
3085        return None;
3086    };
3087
3088    Some(inner)
3089}
3090
3091fn type_path_is_vec_u8(path: &Path) -> bool {
3092    let Some(segment) = path.segments.last() else {
3093        return false;
3094    };
3095
3096    if segment.ident != "Vec" {
3097        return false;
3098    }
3099
3100    let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments else {
3101        return false;
3102    };
3103
3104    let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = arguments.args.first() else {
3105        return false;
3106    };
3107
3108    inner_path.path.is_ident("u8")
3109}
3110
3111fn default_table_name(ident: &Ident) -> String {
3112    pluralize(&to_snake_case(&ident.to_string()))
3113}
3114
3115fn default_table_name_from_path(path: &Path) -> Result<String> {
3116    let ident = path
3117        .segments
3118        .last()
3119        .map(|segment| &segment.ident)
3120        .ok_or_else(|| {
3121            Error::new_spanned(path, "foreign_key requiere una ruta de entidad válida")
3122        })?;
3123
3124    Ok(default_table_name(ident))
3125}
3126
3127fn to_snake_case(value: &str) -> String {
3128    let mut output = String::with_capacity(value.len());
3129
3130    for (index, ch) in value.chars().enumerate() {
3131        if ch.is_uppercase() {
3132            if index > 0 {
3133                output.push('_');
3134            }
3135
3136            for lower in ch.to_lowercase() {
3137                output.push(lower);
3138            }
3139        } else {
3140            output.push(ch);
3141        }
3142    }
3143
3144    output
3145}
3146
3147fn pluralize(value: &str) -> String {
3148    if ends_with_consonant_y(value) {
3149        let stem = &value[..value.len() - 1];
3150        format!("{stem}ies")
3151    } else if value.ends_with('s')
3152        || value.ends_with('x')
3153        || value.ends_with('z')
3154        || value.ends_with("ch")
3155        || value.ends_with("sh")
3156    {
3157        format!("{value}es")
3158    } else {
3159        format!("{value}s")
3160    }
3161}
3162
3163fn ends_with_consonant_y(value: &str) -> bool {
3164    let mut chars = value.chars().rev();
3165    let Some(last) = chars.next() else {
3166        return false;
3167    };
3168    let Some(previous) = chars.next() else {
3169        return false;
3170    };
3171
3172    last == 'y' && !matches!(previous, 'a' | 'e' | 'i' | 'o' | 'u')
3173}
3174
3175fn generated_index_name(prefix: &str, table: &str, column: &str, span: Span) -> LitStr {
3176    LitStr::new(&format!("{prefix}_{table}_{column}"), span)
3177}
3178
3179fn generated_foreign_key_name(
3180    table: &str,
3181    column: &str,
3182    referenced_table: &str,
3183    span: Span,
3184) -> LitStr {
3185    LitStr::new(&format!("fk_{table}_{column}_{referenced_table}"), span)
3186}
3187
3188#[derive(Default)]
3189struct EntityConfig {
3190    table: Option<LitStr>,
3191    schema: Option<LitStr>,
3192    renamed_from: Option<LitStr>,
3193    indexes: Vec<EntityIndexConfig>,
3194    audit: Option<Path>,
3195    soft_delete: Option<Path>,
3196    tenant: Option<Path>,
3197}
3198
3199#[derive(Default)]
3200struct EntityIndexConfig {
3201    name: Option<LitStr>,
3202    unique: bool,
3203    columns: Vec<Ident>,
3204}
3205
3206#[derive(Default)]
3207struct PersistenceModelConfig {
3208    entity: Option<Type>,
3209}
3210
3211#[derive(Default)]
3212struct PersistenceFieldConfig {
3213    column: Option<LitStr>,
3214}
3215
3216#[derive(Default)]
3217struct AuditFieldConfig {
3218    column: Option<LitStr>,
3219    renamed_from: Option<LitStr>,
3220    nullable: bool,
3221    length: Option<u32>,
3222    default_sql: Option<LitStr>,
3223    sql_type: Option<LitStr>,
3224    precision: Option<u8>,
3225    scale: Option<u8>,
3226    insertable: Option<bool>,
3227    updatable: Option<bool>,
3228}
3229
3230#[derive(Default)]
3231struct TenantContextFieldConfig {
3232    column: Option<LitStr>,
3233    renamed_from: Option<LitStr>,
3234    length: Option<u32>,
3235    sql_type: Option<LitStr>,
3236    precision: Option<u8>,
3237    scale: Option<u8>,
3238}
3239
3240#[derive(Default)]
3241struct FieldConfig {
3242    column: Option<LitStr>,
3243    renamed_from: Option<LitStr>,
3244    primary_key: bool,
3245    identity: bool,
3246    identity_seed: Option<i64>,
3247    identity_increment: Option<i64>,
3248    nullable: bool,
3249    length: Option<u32>,
3250    default_sql: Option<LitStr>,
3251    computed_sql: Option<LitStr>,
3252    rowversion: bool,
3253    sql_type: Option<LitStr>,
3254    precision: Option<u8>,
3255    scale: Option<u8>,
3256    indexes: Vec<IndexConfig>,
3257    foreign_key: Option<ForeignKeyConfig>,
3258    on_delete: Option<ReferentialActionConfig>,
3259    navigation: Option<NavigationConfig>,
3260}
3261
3262#[derive(Default)]
3263struct IndexConfig {
3264    name: Option<LitStr>,
3265    unique: bool,
3266}
3267
3268struct ForeignKeyConfig {
3269    name: Option<LitStr>,
3270    generated_referenced_table_name: String,
3271    target: ForeignKeyTarget,
3272}
3273
3274struct FieldForeignKeyInfo {
3275    name: LitStr,
3276    local_column: LitStr,
3277    referenced_column: TokenStream2,
3278    field_span: Span,
3279    has_explicit_name: bool,
3280    structured_target: Option<String>,
3281}
3282
3283struct PendingNavigation {
3284    rust_field: LitStr,
3285    kind: NavigationKindConfig,
3286    kind_tokens: TokenStream2,
3287    wrapper: NavigationWrapperConfig,
3288    target: Path,
3289    target_rust_name: LitStr,
3290    foreign_key_field: Ident,
3291    foreign_key_field_name: String,
3292}
3293
3294struct NavigationConfig {
3295    kind: NavigationKindConfig,
3296    target: Path,
3297    foreign_key: Ident,
3298}
3299
3300#[derive(Clone, Copy)]
3301enum NavigationKindConfig {
3302    BelongsTo,
3303    HasOne,
3304    HasMany,
3305}
3306
3307#[derive(Clone, Copy)]
3308enum NavigationWrapperConfig {
3309    Eager,
3310    Lazy,
3311}
3312
3313impl NavigationKindConfig {
3314    fn attribute_name(self) -> &'static str {
3315        match self {
3316            Self::BelongsTo => "belongs_to",
3317            Self::HasOne => "has_one",
3318            Self::HasMany => "has_many",
3319        }
3320    }
3321}
3322
3323impl ForeignKeyConfig {
3324    fn structured_target_key(&self) -> Option<String> {
3325        match &self.target {
3326            ForeignKeyTarget::Structured { entity, .. } => Some(path_key(entity)),
3327            ForeignKeyTarget::Legacy { .. } => None,
3328        }
3329    }
3330
3331    fn referenced_schema_tokens(&self) -> TokenStream2 {
3332        match &self.target {
3333            ForeignKeyTarget::Legacy {
3334                referenced_schema, ..
3335            } => quote! { #referenced_schema },
3336            ForeignKeyTarget::Structured { entity, .. } => {
3337                quote! { #entity::__SQL_ORM_ENTITY_SCHEMA }
3338            }
3339        }
3340    }
3341
3342    fn referenced_table_tokens(&self) -> TokenStream2 {
3343        match &self.target {
3344            ForeignKeyTarget::Legacy {
3345                referenced_table, ..
3346            } => quote! { #referenced_table },
3347            ForeignKeyTarget::Structured { entity, .. } => {
3348                quote! { #entity::__SQL_ORM_ENTITY_TABLE }
3349            }
3350        }
3351    }
3352
3353    fn referenced_column_tokens(&self) -> TokenStream2 {
3354        match &self.target {
3355            ForeignKeyTarget::Legacy {
3356                referenced_column, ..
3357            } => quote! { #referenced_column },
3358            ForeignKeyTarget::Structured { entity, column } => {
3359                quote! { #entity::#column.column_name() }
3360            }
3361        }
3362    }
3363}
3364
3365enum ForeignKeyTarget {
3366    Legacy {
3367        referenced_schema: LitStr,
3368        referenced_table: LitStr,
3369        referenced_column: LitStr,
3370    },
3371    Structured {
3372        entity: Path,
3373        column: Ident,
3374    },
3375}
3376
3377#[derive(Clone, Copy, PartialEq, Eq)]
3378enum ReferentialActionConfig {
3379    NoAction,
3380    Cascade,
3381    SetNull,
3382}
3383
3384struct TypeInfo {
3385    nullable: bool,
3386    is_integer: bool,
3387    is_vec_u8: bool,
3388    default_max_length: Option<u32>,
3389    default_precision: Option<u8>,
3390    default_scale: Option<u8>,
3391    kind: TypeKind,
3392}
3393
3394enum TypeKind {
3395    I64,
3396    I32,
3397    I16,
3398    U8,
3399    Bool,
3400    String,
3401    VecU8,
3402    Uuid,
3403    NaiveDateTime,
3404    NaiveDate,
3405    Decimal,
3406    Float,
3407    Unknown,
3408}