Skip to main content

ignis_derive/
lib.rs

1//! Ignis-DB Procedural Macros
2//!
3//! This crate provides clean derive macros for the Ignis-DB ORM:
4//! - `#[derive(IgnisModel)]` - Automatic trait implementation
5//! - `#[derive(Insertable)]` - Safe insert operations
6//! - `#[derive(Updatable)]` - Safe update operations  
7//! - `#[derive(Deletable)]` - Safe delete operations
8//! - `#[derive(PgComposite)]` - PostgreSQL composite type support
9//!
10//! Usage:
11//! ```rust,ignore
12//! #[derive(IgnisModel, Insertable, Updatable, Deletable)]
13//! #[ignis(table = "users", pk = "id")]
14//! pub struct User {
15//!     pub id: i32,
16//!     pub name: String,
17//!     pub email: String,
18//!     pub address: Address, // Composite type
19//! }
20//!
21//! #[derive(PgComposite)]
22//! #[pg_composite(name = "address")]
23//! pub struct Address {
24//!     pub street: String,
25//!     pub city: String,
26//!     pub zipcode: Option<String>,
27//! }
28//! ```
29
30use proc_macro::TokenStream;
31use quote::quote;
32use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type};
33
34/// Derive macro for IgnisModel trait
35#[proc_macro_derive(IgnisModel, attributes(ignis))]
36pub fn derive_ignis_model(input: TokenStream) -> TokenStream {
37    let input = parse_macro_input!(input as DeriveInput);
38
39    match impl_ignis_model(&input) {
40        Ok(tokens) => tokens.into(),
41        Err(err) => {
42            let error = format!("Failed to derive IgnisModel: {}", err);
43            quote! {
44                compile_error!(#error);
45            }
46            .into()
47        }
48    }
49}
50
51/// Derive macro for Insertable trait
52#[proc_macro_derive(Insertable)]
53pub fn derive_insertable(input: TokenStream) -> TokenStream {
54    let input = parse_macro_input!(input as DeriveInput);
55
56    match impl_insertable(&input) {
57        Ok(tokens) => tokens.into(),
58        Err(err) => {
59            let error = format!("Failed to derive Insertable: {}", err);
60            quote! {
61                compile_error!(#error);
62            }
63            .into()
64        }
65    }
66}
67
68/// Derive macro for Updatable trait
69#[proc_macro_derive(Updatable)]
70pub fn derive_updatable(input: TokenStream) -> TokenStream {
71    let input = parse_macro_input!(input as DeriveInput);
72
73    match impl_updatable(&input) {
74        Ok(tokens) => tokens.into(),
75        Err(err) => {
76            let error = format!("Failed to derive Updatable: {}", err);
77            quote! {
78                compile_error!(#error);
79            }
80            .into()
81        }
82    }
83}
84
85/// Derive macro for Deletable trait
86#[proc_macro_derive(Deletable)]
87pub fn derive_deletable(input: TokenStream) -> TokenStream {
88    let input = parse_macro_input!(input as DeriveInput);
89
90    match impl_deletable(&input) {
91        Ok(tokens) => tokens.into(),
92        Err(err) => {
93            let error = format!("Failed to derive Deletable: {}", err);
94            quote! {
95                compile_error!(#error);
96            }
97            .into()
98        }
99    }
100}
101
102/// Derive macro for PostgreSQL composite types
103#[proc_macro_derive(PgComposite, attributes(pg_composite))]
104pub fn derive_pg_composite(input: TokenStream) -> TokenStream {
105    let input = parse_macro_input!(input as DeriveInput);
106
107    match impl_pg_composite(&input) {
108        Ok(tokens) => tokens.into(),
109        Err(err) => {
110            let error = format!("Failed to derive PgComposite: {}", err);
111            quote! {
112                compile_error!(#error);
113            }
114            .into()
115        }
116    }
117}
118
119/// Extract ignis attributes from struct
120fn extract_ignis_attrs(attrs: &[Attribute]) -> anyhow::Result<IgnisConfig> {
121    let mut table_name = None;
122    let mut primary_keys = Vec::new();
123    let mut schema = None;
124
125    for attr in attrs {
126        if attr.path().is_ident("ignis") {
127            // Parse the attribute content using syn's parse_nested_meta
128            attr.parse_nested_meta(|meta| {
129                let ident = meta
130                    .path
131                    .get_ident()
132                    .ok_or_else(|| meta.error("Expected identifier"))?;
133
134                match ident.to_string().as_str() {
135                    "table" => {
136                        let value = meta.value()?.parse::<LitStr>()?;
137                        table_name = Some(value.value());
138                        Ok(())
139                    }
140                    "pk" => {
141                        let value = meta.value()?.parse::<LitStr>()?;
142                        // Support comma-separated composite keys
143                        let keys: Vec<String> = value
144                            .value()
145                            .split(',')
146                            .map(|s| s.trim().to_string())
147                            .collect();
148                        primary_keys = keys;
149                        Ok(())
150                    }
151                    "schema" => {
152                        let value = meta.value()?.parse::<LitStr>()?;
153                        schema = Some(value.value());
154                        Ok(())
155                    }
156                    _ => Err(meta.error("Unknown ignis attribute")),
157                }
158            })?;
159        }
160    }
161
162    let table_name =
163        table_name.ok_or_else(|| anyhow::anyhow!("Missing #[ignis(table = \"...\")]"))?;
164    if primary_keys.is_empty() {
165        return Err(anyhow::anyhow!("Missing #[ignis(pk = \"...\")]"));
166    }
167
168    Ok(IgnisConfig {
169        table_name,
170        primary_keys,
171        schema,
172    })
173}
174
175/// Configuration extracted from ignis attributes
176#[derive(Debug)]
177struct IgnisConfig {
178    table_name: String,
179    primary_keys: Vec<String>,
180    schema: Option<String>,
181}
182
183/// Generate field bindings for INSERT operations
184fn generate_insert_fields(fields: &Fields) -> anyhow::Result<proc_macro2::TokenStream> {
185    let mut field_bindings = Vec::new();
186
187    for field in fields.iter() {
188        let field_name = field
189            .ident
190            .as_ref()
191            .ok_or_else(|| anyhow::anyhow!("Unnamed fields not supported"))?;
192
193        field_bindings.push(quote! {
194            let value = ::ignis_db::BindValue::from(self.#field_name.clone());
195            builder = builder.value(stringify!(#field_name), value.to_parameter_binding())
196                .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
197        });
198    }
199
200    Ok(quote! {
201        let mut builder = ::ignis_db::macros::parameter_binding::SafeInsertBuilder::new(Self::FULL_TABLE_NAME)
202            .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
203
204        #(#field_bindings)*
205
206        builder.execute(executor).await
207    })
208}
209
210/// Generate field bindings for UPDATE operations
211fn generate_update_fields(
212    fields: &Fields,
213    primary_keys: &[String],
214) -> anyhow::Result<proc_macro2::TokenStream> {
215    let mut set_bindings = Vec::new();
216    let primary_key_set: std::collections::HashSet<_> = primary_keys.iter().collect();
217
218    for field in fields.iter() {
219        let field_name = field
220            .ident
221            .as_ref()
222            .ok_or_else(|| anyhow::anyhow!("Unnamed fields not supported"))?;
223        let field_name_str = field_name.to_string();
224
225        // Skip primary key fields in SET clause
226        if !primary_key_set.contains(&field_name_str) {
227            set_bindings.push(quote! {
228                let value = ::ignis_db::BindValue::from(self.#field_name.clone());
229                builder = builder.set(stringify!(#field_name), value.to_parameter_binding())
230                    .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
231            });
232        }
233    }
234
235    Ok(quote! {
236        let mut builder = ::ignis_db::macros::parameter_binding::SafeUpdateBuilder::new(Self::FULL_TABLE_NAME)
237            .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
238
239        #(#set_bindings)*
240
241        // Add WHERE clause for composite primary key
242        let pk_values = self.get_composite_key_values()
243            .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
244
245        for (key, value) in pk_values {
246            builder = builder.r#where(key, value.to_parameter_binding())
247                .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
248        }
249
250        builder.execute(executor).await
251    })
252}
253
254/// Generate field bindings for DELETE operations
255fn generate_delete_fields() -> proc_macro2::TokenStream {
256    quote! {
257        let mut builder = ::ignis_db::macros::parameter_binding::SafeDeleteBuilder::new(Self::FULL_TABLE_NAME)
258            .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
259
260        // Add WHERE clause for composite primary key
261        let pk_values = self.get_composite_key_values()
262            .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
263
264        for (key, value) in pk_values {
265            builder = builder.r#where(key, value.to_parameter_binding())
266                .map_err(|e| sqlx::Error::Protocol(e.to_string().into()))?;
267        }
268
269        builder.execute(executor).await
270    }
271}
272
273/// Generate composite key extraction
274fn generate_composite_key_extraction(
275    primary_keys: &[String],
276    fields: &Fields,
277) -> anyhow::Result<proc_macro2::TokenStream> {
278    let mut key_extractions = Vec::new();
279
280    for pk_name in primary_keys {
281        let pk_ident = syn::Ident::new(pk_name, proc_macro2::Span::call_site());
282
283        // Find the primary key field to determine its type
284        let pk_field = fields
285            .iter()
286            .find(|field| field.ident.as_ref().map(|i| i.to_string()) == Some(pk_name.to_string()))
287            .ok_or_else(|| {
288                anyhow::anyhow!("Primary key field '{}' not found in struct", pk_name)
289            })?;
290
291        // Generate type-specific conversion based on the field type
292        let conversion = match &pk_field.ty {
293            Type::Path(type_path) => {
294                let type_name = type_path
295                    .path
296                    .segments
297                    .last()
298                    .ok_or_else(|| anyhow::anyhow!("Invalid type path"))?
299                    .ident
300                    .to_string();
301
302                match type_name.as_str() {
303                    "i32" => quote! { (#pk_name, ::ignis_db::BindValue::Int4(self.#pk_ident)) },
304                    "i64" => quote! { (#pk_name, ::ignis_db::BindValue::Int8(self.#pk_ident)) },
305                    "String" => {
306                        quote! { (#pk_name, ::ignis_db::BindValue::Text(self.#pk_ident.clone())) }
307                    }
308                    "bool" => quote! { (#pk_name, ::ignis_db::BindValue::Bool(self.#pk_ident)) },
309                    "Uuid" => quote! { (#pk_name, ::ignis_db::BindValue::Uuid(self.#pk_ident)) },
310                    "DateTime" => {
311                        quote! { (#pk_name, ::ignis_db::BindValue::Timestamptz(self.#pk_ident)) }
312                    }
313                    "Decimal" => {
314                        quote! { (#pk_name, ::ignis_db::BindValue::Numeric(self.#pk_ident)) }
315                    }
316                    "Value" => {
317                        quote! { (#pk_name, ::ignis_db::BindValue::Jsonb(self.#pk_ident.clone())) }
318                    }
319                    _ => {
320                        // Try to use From<T> for BindValue for custom types
321                        quote! { (#pk_name, ::ignis_db::BindValue::from(self.#pk_ident.clone())) }
322                    }
323                }
324            }
325            _ => {
326                return Err(anyhow::anyhow!(
327                    "Unsupported primary key type for field {}",
328                    pk_name
329                ))
330            }
331        };
332
333        key_extractions.push(conversion);
334    }
335
336    Ok(quote! {
337        Ok(vec![#(#key_extractions),*])
338    })
339}
340
341/// Implement IgnisModel trait
342fn impl_ignis_model(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
343    let name = &input.ident;
344    let config = extract_ignis_attrs(&input.attrs)?;
345
346    let fields = match &input.data {
347        Data::Struct(data) => &data.fields,
348        _ => {
349            return Err(anyhow::anyhow!(
350                "IgnisModel can only be derived for structs"
351            ))
352        }
353    };
354
355    let table_name = &config.table_name;
356    let primary_keys = &config.primary_keys;
357    let schema = config.schema.as_ref();
358
359    let full_table_name = if let Some(schema_name) = schema {
360        format!("{}.{}", schema_name, table_name)
361    } else {
362        table_name.clone()
363    };
364
365    // For compatibility, store all keys as comma-separated string in PK const
366    let pk_string = primary_keys.join(", ");
367
368    // Generate appropriate extraction method
369    let (pk_extraction, composite_extraction) = if primary_keys.len() == 1 {
370        // Single primary key
371        let pk_name = &primary_keys[0];
372        let pk_ident = syn::Ident::new(pk_name, proc_macro2::Span::call_site());
373
374        let pk_field = fields
375            .iter()
376            .find(|field| field.ident.as_ref().map(|i| i.to_string()) == Some(pk_name.to_string()))
377            .ok_or_else(|| {
378                anyhow::anyhow!("Primary key field '{}' not found in struct", pk_name)
379            })?;
380
381        let single_extraction = match &pk_field.ty {
382            Type::Path(type_path) => {
383                let type_name = type_path
384                    .path
385                    .segments
386                    .last()
387                    .ok_or_else(|| anyhow::anyhow!("Invalid type path"))?
388                    .ident
389                    .to_string();
390
391                match type_name.as_str() {
392                    "i32" => quote! { Ok(::ignis_db::BindValue::Int4(self.#pk_ident)) },
393                    "i64" => quote! { Ok(::ignis_db::BindValue::Int8(self.#pk_ident)) },
394                    "String" => quote! { Ok(::ignis_db::BindValue::Text(self.#pk_ident.clone())) },
395                    "bool" => quote! { Ok(::ignis_db::BindValue::Bool(self.#pk_ident)) },
396                    "Uuid" => quote! { Ok(::ignis_db::BindValue::Uuid(self.#pk_ident)) },
397                    "DateTime" => quote! { Ok(::ignis_db::BindValue::Timestamptz(self.#pk_ident)) },
398                    "Decimal" => quote! { Ok(::ignis_db::BindValue::Numeric(self.#pk_ident)) },
399                    "Value" => quote! { Ok(::ignis_db::BindValue::Jsonb(self.#pk_ident.clone())) },
400                    _ => quote! { Ok(::ignis_db::BindValue::from(self.#pk_ident.clone())) },
401                }
402            }
403            _ => return Err(anyhow::anyhow!("Unsupported primary key type")),
404        };
405
406        // For single key, composite returns same as single in a vec
407        let composite = quote! {
408            Ok(vec![(#pk_name, self.get_primary_key_value()?)])
409        };
410
411        (single_extraction, composite)
412    } else {
413        // Composite primary key
414        let composite_extraction = generate_composite_key_extraction(primary_keys, fields)?;
415
416        // For get_primary_key_value(), return error for composite keys
417        let single = quote! {
418            Err(anyhow::anyhow!("Cannot use get_primary_key_value() with composite key. Use get_composite_key_values() instead."))
419        };
420
421        (single, composite_extraction)
422    };
423
424    let expanded = if let Some(schema_name) = schema {
425        quote! {
426            impl ::ignis_db::IgnisModel for #name {
427                const TABLE: &'static str = #table_name;
428                const PK: &'static str = #pk_string;
429                const SCHEMA: Option<&'static str> = Some(#schema_name);
430                const FULL_TABLE_NAME: &'static str = #full_table_name;
431
432                fn get_primary_key_value(&self) -> anyhow::Result<::ignis_db::BindValue> {
433                    #pk_extraction
434                }
435
436                fn get_composite_key_values(&self) -> anyhow::Result<Vec<(&'static str, ::ignis_db::BindValue)>> {
437                    #composite_extraction
438                }
439            }
440        }
441    } else {
442        quote! {
443            impl ::ignis_db::IgnisModel for #name {
444                const TABLE: &'static str = #table_name;
445                const PK: &'static str = #pk_string;
446                const SCHEMA: Option<&'static str> = None;
447                const FULL_TABLE_NAME: &'static str = #full_table_name;
448
449                fn get_primary_key_value(&self) -> anyhow::Result<::ignis_db::BindValue> {
450                    #pk_extraction
451                }
452
453                fn get_composite_key_values(&self) -> anyhow::Result<Vec<(&'static str, ::ignis_db::BindValue)>> {
454                    #composite_extraction
455                }
456            }
457        }
458    };
459
460    Ok(expanded)
461}
462
463/// Implement Insertable trait
464fn impl_insertable(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
465    let name = &input.ident;
466
467    let fields = match &input.data {
468        Data::Struct(data) => &data.fields,
469        _ => {
470            return Err(anyhow::anyhow!(
471                "Insertable can only be derived for structs"
472            ))
473        }
474    };
475
476    let insert_impl = generate_insert_fields(fields)?;
477
478    let expanded = quote! {
479        impl ::ignis_db::Insertable for #name {
480            async fn insert<'e, E>(&self, executor: E) -> sqlx::Result<u64>
481            where
482                E: sqlx::Executor<'e, Database = sqlx::Postgres>,
483            {
484                #insert_impl
485            }
486        }
487    };
488
489    Ok(expanded)
490}
491
492/// Implement Updatable trait
493fn impl_updatable(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
494    let name = &input.ident;
495    let config = extract_ignis_attrs(&input.attrs)?;
496
497    let fields = match &input.data {
498        Data::Struct(data) => &data.fields,
499        _ => return Err(anyhow::anyhow!("Updatable can only be derived for structs")),
500    };
501
502    let update_impl = generate_update_fields(fields, &config.primary_keys)?;
503
504    let expanded = quote! {
505        impl ::ignis_db::Updatable for #name {
506            async fn update<'e, E>(&self, executor: E) -> sqlx::Result<u64>
507            where
508                E: sqlx::Executor<'e, Database = sqlx::Postgres>,
509            {
510                #update_impl
511            }
512        }
513    };
514
515    Ok(expanded)
516}
517
518/// Implement Deletable trait
519fn impl_deletable(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
520    let name = &input.ident;
521
522    let delete_impl = generate_delete_fields();
523
524    let expanded = quote! {
525        impl ::ignis_db::Deletable for #name {
526            async fn delete<'e, E>(&self, executor: E) -> sqlx::Result<u64>
527            where
528                E: sqlx::Executor<'e, Database = sqlx::Postgres>,
529            {
530                #delete_impl
531            }
532        }
533    };
534
535    Ok(expanded)
536}
537
538/// Implement PgComposite trait - PostgreSQL composite type support
539fn impl_pg_composite(input: &DeriveInput) -> anyhow::Result<proc_macro2::TokenStream> {
540    let struct_name = &input.ident;
541    let (impl_generics, type_generics, where_clause) = input.generics.split_for_impl();
542
543    // Parse pg_composite attributes
544    let composite_config = parse_pg_composite_attributes(&input.attrs)?;
545    let type_name = composite_config
546        .type_name
547        .unwrap_or_else(|| struct_name.to_string().to_lowercase());
548
549    // Analyze the struct fields
550    let fields_info = analyze_composite_fields(&input.data)?;
551
552    // Generate implementations
553    let sqlx_type_impl = generate_sqlx_type_impl(
554        struct_name,
555        &type_name,
556        &impl_generics,
557        &type_generics,
558        where_clause,
559    );
560    let sqlx_encode_impl = generate_sqlx_encode_impl(
561        struct_name,
562        &fields_info,
563        &impl_generics,
564        &type_generics,
565        where_clause,
566    );
567    let sqlx_decode_impl = generate_sqlx_decode_impl(
568        struct_name,
569        &fields_info,
570        &impl_generics,
571        &type_generics,
572        where_clause,
573    );
574    let ddl_functions = generate_ddl_functions(
575        struct_name,
576        &type_name,
577        &fields_info,
578        &impl_generics,
579        &type_generics,
580        where_clause,
581    );
582
583    Ok(quote! {
584        #sqlx_type_impl
585        #sqlx_encode_impl
586        #sqlx_decode_impl
587        #ddl_functions
588    })
589}
590
591/// Configuration for pg_composite attributes
592#[derive(Default)]
593struct PgCompositeConfig {
594    type_name: Option<String>,
595    schema: Option<String>,
596}
597
598/// Parse pg_composite attributes
599fn parse_pg_composite_attributes(attrs: &[Attribute]) -> anyhow::Result<PgCompositeConfig> {
600    let mut config = PgCompositeConfig::default();
601
602    for attr in attrs {
603        if attr.path().is_ident("pg_composite") {
604            attr.parse_nested_meta(|meta| {
605                if meta.path.is_ident("name") {
606                    let value = meta.value()?;
607                    let lit: syn::LitStr = value.parse()?;
608                    config.type_name = Some(lit.value());
609                    Ok(())
610                } else if meta.path.is_ident("schema") {
611                    let value = meta.value()?;
612                    let lit: syn::LitStr = value.parse()?;
613                    config.schema = Some(lit.value());
614                    Ok(())
615                } else {
616                    Err(meta.error("unsupported pg_composite attribute"))
617                }
618            })?;
619        }
620    }
621
622    Ok(config)
623}
624
625/// Information about composite type fields
626enum CompositeFieldsInfo {
627    Named(Vec<CompositeNamedField>),
628    Unnamed(Vec<CompositeUnnamedField>),
629    Unit,
630}
631
632struct CompositeNamedField {
633    name: Ident,
634    #[allow(dead_code)]
635    ty: Type, // Used for type validation in composite type system
636    sql_type: String,
637}
638
639struct CompositeUnnamedField {
640    #[allow(dead_code)]
641    ty: Type, // Used for type validation in composite type system
642    sql_type: String,
643    index: usize,
644}
645
646/// Analyze composite type fields
647fn analyze_composite_fields(data: &Data) -> anyhow::Result<CompositeFieldsInfo> {
648    match data {
649        Data::Struct(data_struct) => {
650            match &data_struct.fields {
651                Fields::Named(fields) => {
652                    let mut named_fields = Vec::new();
653                    for field in &fields.named {
654                        let field_name = field.ident.as_ref().unwrap().clone();
655                        let field_type = field.ty.clone();
656                        let sql_type = rust_type_to_pg_type(&field_type);
657
658                        // Validate type compatibility with PostgreSQL
659                        validate_type_compatibility(&field_type, &sql_type)?;
660
661                        named_fields.push(CompositeNamedField {
662                            name: field_name,
663                            ty: field_type,
664                            sql_type,
665                        });
666                    }
667                    Ok(CompositeFieldsInfo::Named(named_fields))
668                }
669                Fields::Unnamed(fields) => {
670                    let mut unnamed_fields = Vec::new();
671                    for (index, field) in fields.unnamed.iter().enumerate() {
672                        let field_type = field.ty.clone();
673                        let sql_type = rust_type_to_pg_type(&field_type);
674
675                        // Validate type compatibility with PostgreSQL
676                        validate_type_compatibility(&field_type, &sql_type)?;
677
678                        unnamed_fields.push(CompositeUnnamedField {
679                            ty: field_type,
680                            sql_type,
681                            index,
682                        });
683                    }
684                    Ok(CompositeFieldsInfo::Unnamed(unnamed_fields))
685                }
686                Fields::Unit => Ok(CompositeFieldsInfo::Unit),
687            }
688        }
689        _ => Err(anyhow::anyhow!(
690            "PgComposite can only be derived for structs"
691        )),
692    }
693}
694
695/// Validate that a Rust type is compatible with the corresponding PostgreSQL type
696fn validate_type_compatibility(rust_type: &Type, sql_type: &str) -> anyhow::Result<()> {
697    match rust_type {
698        Type::Path(type_path) => {
699            let path = &type_path.path;
700            if let Some(segment) = path.segments.last() {
701                let type_name = segment.ident.to_string();
702
703                // Validate known type mappings
704                let expected_sql_types: &[&str] = match type_name.as_str() {
705                    "i16" => &["SMALLINT"],
706                    "i32" => &["INTEGER", "INT", "INT4"],
707                    "i64" => &["BIGINT", "INT8"],
708                    "f32" => &["REAL", "FLOAT4"],
709                    "f64" => &["DOUBLE PRECISION", "FLOAT8"],
710                    "bool" => &["BOOLEAN", "BOOL"],
711                    "String" => &["TEXT", "VARCHAR"],
712                    "Uuid" => &["UUID"],
713                    "Decimal" => &["NUMERIC", "DECIMAL"],
714                    "Option" => {
715                        // For Option<T>, we need to validate T
716                        if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
717                            if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
718                            {
719                                return validate_type_compatibility(inner_type, sql_type);
720                            }
721                        }
722                        return Ok(()); // Allow Option types through
723                    }
724                    _ => {
725                        // Custom types should be checked but not fail validation
726                        // This allows for user-defined types and composite types
727                        return Ok(());
728                    }
729                };
730
731                // Check if the SQL type matches any of the expected types
732                if !expected_sql_types
733                    .iter()
734                    .any(|&expected| expected.eq_ignore_ascii_case(sql_type))
735                {
736                    return Err(anyhow::anyhow!(
737                        "Type compatibility warning: Rust type '{}' mapped to SQL type '{}', expected one of: {}",
738                        type_name,
739                        sql_type,
740                        expected_sql_types.join(", ")
741                    ));
742                }
743            }
744        }
745        _ => {
746            // For non-path types, we'll allow them but could add more validation here
747            return Ok(());
748        }
749    }
750
751    Ok(())
752}
753
754/// Check if a type is Option<T>
755#[allow(dead_code)]
756fn is_option_type(ty: &Type) -> bool {
757    match ty {
758        Type::Path(type_path) => {
759            let path = &type_path.path;
760            if let Some(segment) = path.segments.last() {
761                segment.ident == "Option"
762            } else {
763                false
764            }
765        }
766        _ => false,
767    }
768}
769
770/// Convert Rust types to PostgreSQL types
771fn rust_type_to_pg_type(ty: &Type) -> String {
772    match ty {
773        Type::Path(type_path) => {
774            let path = &type_path.path;
775            if let Some(segment) = path.segments.last() {
776                match segment.ident.to_string().as_str() {
777                    "i16" => "SMALLINT".to_string(),
778                    "i32" => "INTEGER".to_string(),
779                    "i64" => "BIGINT".to_string(),
780                    "f32" => "REAL".to_string(),
781                    "f64" => "DOUBLE PRECISION".to_string(),
782                    "bool" => "BOOLEAN".to_string(),
783                    "String" => "TEXT".to_string(),
784                    "Uuid" => "UUID".to_string(),
785                    "Decimal" => "NUMERIC".to_string(),
786                    "Option" => {
787                        // For Option<T>, extract T and make it nullable
788                        if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
789                            if let Some(syn::GenericArgument::Type(inner_type)) = args.args.first()
790                            {
791                                return rust_type_to_pg_type(inner_type);
792                            }
793                        }
794                        "TEXT".to_string()
795                    }
796                    _ => "TEXT".to_string(), // Default fallback
797                }
798            } else {
799                "TEXT".to_string()
800            }
801        }
802        _ => "TEXT".to_string(),
803    }
804}
805
806/// Generate sqlx::Type implementation
807fn generate_sqlx_type_impl(
808    struct_name: &Ident,
809    type_name: &str,
810    impl_generics: &syn::ImplGenerics,
811    type_generics: &syn::TypeGenerics,
812    where_clause: Option<&syn::WhereClause>,
813) -> proc_macro2::TokenStream {
814    quote! {
815        impl #impl_generics sqlx::Type<sqlx::Postgres> for #struct_name #type_generics #where_clause {
816            fn type_info() -> sqlx::postgres::PgTypeInfo {
817                sqlx::postgres::PgTypeInfo::with_name(#type_name)
818            }
819        }
820    }
821}
822
823/// Generate sqlx::Encode implementation
824fn generate_sqlx_encode_impl(
825    struct_name: &Ident,
826    fields_info: &CompositeFieldsInfo,
827    impl_generics: &syn::ImplGenerics,
828    type_generics: &syn::TypeGenerics,
829    where_clause: Option<&syn::WhereClause>,
830) -> proc_macro2::TokenStream {
831    let encode_body = match fields_info {
832        CompositeFieldsInfo::Named(fields) => {
833            let field_encodings: Vec<_> = fields
834                .iter()
835                .map(|field| {
836                    let field_name = &field.name;
837                    quote! { encoder.encode(&self.#field_name); }
838                })
839                .collect();
840
841            quote! {
842                let mut encoder = sqlx::postgres::types::PgRecordEncoder::new(buf);
843                #(#field_encodings)*
844                encoder.finish();
845            }
846        }
847        CompositeFieldsInfo::Unnamed(fields) => {
848            let field_encodings: Vec<_> = fields
849                .iter()
850                .map(|field| {
851                    let index = syn::Index::from(field.index);
852                    quote! { encoder.encode(&self.#index); }
853                })
854                .collect();
855
856            quote! {
857                let mut encoder = sqlx::postgres::types::PgRecordEncoder::new(buf);
858                #(#field_encodings)*
859                encoder.finish();
860            }
861        }
862        CompositeFieldsInfo::Unit => {
863            quote! {
864                // Unit struct - no fields to encode
865            }
866        }
867    };
868
869    quote! {
870        impl #impl_generics sqlx::Encode<'_, sqlx::Postgres> for #struct_name #type_generics #where_clause {
871            fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> sqlx::encode::IsNull {
872                #encode_body
873                sqlx::encode::IsNull::No
874            }
875        }
876    }
877}
878
879/// Generate sqlx::Decode implementation
880fn generate_sqlx_decode_impl(
881    struct_name: &Ident,
882    fields_info: &CompositeFieldsInfo,
883    impl_generics: &syn::ImplGenerics,
884    type_generics: &syn::TypeGenerics,
885    where_clause: Option<&syn::WhereClause>,
886) -> proc_macro2::TokenStream {
887    let decode_body = match fields_info {
888        CompositeFieldsInfo::Named(fields) => {
889            let field_decodings: Vec<_> = fields
890                .iter()
891                .map(|field| {
892                    let field_name = &field.name;
893                    quote! { #field_name: decoder.try_decode()?, }
894                })
895                .collect();
896
897            quote! {
898                let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
899                Ok(Self {
900                    #(#field_decodings)*
901                })
902            }
903        }
904        CompositeFieldsInfo::Unnamed(fields) => {
905            let field_decodings: Vec<_> = fields
906                .iter()
907                .map(|_| {
908                    quote! { decoder.try_decode()?, }
909                })
910                .collect();
911
912            quote! {
913                let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
914                Ok(Self(#(#field_decodings)*))
915            }
916        }
917        CompositeFieldsInfo::Unit => {
918            quote! {
919                Ok(Self)
920            }
921        }
922    };
923
924    quote! {
925        impl #impl_generics sqlx::Decode<'_, sqlx::Postgres> for #struct_name #type_generics #where_clause {
926            fn decode(value: sqlx::postgres::PgValueRef<'_>) -> Result<Self, sqlx::error::BoxDynError> {
927                #decode_body
928            }
929        }
930    }
931}
932
933/// Generate DDL helper functions
934fn generate_ddl_functions(
935    struct_name: &Ident,
936    type_name: &str,
937    fields_info: &CompositeFieldsInfo,
938    impl_generics: &syn::ImplGenerics,
939    type_generics: &syn::TypeGenerics,
940    where_clause: Option<&syn::WhereClause>,
941) -> proc_macro2::TokenStream {
942    let field_definitions = match fields_info {
943        CompositeFieldsInfo::Named(fields) => {
944            let definitions: Vec<_> = fields
945                .iter()
946                .map(|field| {
947                    let field_name = field.name.to_string();
948                    let sql_type = &field.sql_type;
949
950                    // PostgreSQL composite types do not support NOT NULL constraints on individual fields
951                    // The nullability is handled when the composite type is used as a column
952                    format!("    \"{}\" {}", field_name, sql_type)
953                })
954                .collect();
955            definitions.join(",\n")
956        }
957        CompositeFieldsInfo::Unnamed(fields) => {
958            let definitions: Vec<_> = fields
959                .iter()
960                .enumerate()
961                .map(|(index, field)| {
962                    let field_name = format!("field_{}", index);
963                    let sql_type = &field.sql_type;
964
965                    // PostgreSQL composite types do not support NOT NULL constraints on individual fields
966                    // The nullability is handled when the composite type is used as a column
967                    format!("    \"{}\" {}", field_name, sql_type)
968                })
969                .collect();
970            definitions.join(",\n")
971        }
972        CompositeFieldsInfo::Unit => String::new(),
973    };
974
975    quote! {
976        impl #impl_generics #struct_name #type_generics #where_clause {
977            /// Generate CREATE TYPE DDL for this composite type
978            pub fn generate_create_ddl() -> String {
979                format!(
980                    "CREATE TYPE \"{}\" AS (\n{}\n);",
981                    #type_name,
982                    #field_definitions
983                )
984            }
985
986            /// Generate DROP TYPE DDL for this composite type
987            pub fn generate_drop_ddl() -> String {
988                format!("DROP TYPE IF EXISTS \"{}\" CASCADE;", #type_name)
989            }
990
991            /// Get the PostgreSQL type name
992            pub fn pg_type_name() -> &'static str {
993                #type_name
994            }
995        }
996    }
997}