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