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