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