1use proc_macro::TokenStream;
31use quote::quote;
32use syn::{parse_macro_input, Attribute, Data, DeriveInput, Fields, Ident, LitStr, Type};
33
34#[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#[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#[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#[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#[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
119fn 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 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 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#[derive(Debug)]
177struct IgnisConfig {
178 table_name: String,
179 primary_keys: Vec<String>,
180 schema: Option<String>,
181}
182
183fn 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
210fn 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 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 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
254fn 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 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
273fn 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 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 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 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
341fn 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 let pk_string = primary_keys.join(", ");
367
368 let (pk_extraction, composite_extraction) = if primary_keys.len() == 1 {
370 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 let composite = quote! {
408 Ok(vec![(#pk_name, self.get_primary_key_value()?)])
409 };
410
411 (single_extraction, composite)
412 } else {
413 let composite_extraction = generate_composite_key_extraction(primary_keys, fields)?;
415
416 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
463fn 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
492fn 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
518fn 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
538fn 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 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 let fields_info = analyze_composite_fields(&input.data)?;
551
552 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#[derive(Default)]
593struct PgCompositeConfig {
594 type_name: Option<String>,
595 schema: Option<String>,
596}
597
598fn 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
625enum 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, sql_type: String,
637}
638
639struct CompositeUnnamedField {
640 #[allow(dead_code)]
641 ty: Type, sql_type: String,
643 index: usize,
644}
645
646fn 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(&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(&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
695fn 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 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 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(()); }
724 _ => {
725 return Ok(());
728 }
729 };
730
731 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 return Ok(());
748 }
749 }
750
751 Ok(())
752}
753
754#[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
770fn 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 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(), }
798 } else {
799 "TEXT".to_string()
800 }
801 }
802 _ => "TEXT".to_string(),
803 }
804}
805
806fn 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
823fn 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 }
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
879fn 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
933fn 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 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 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 pub fn generate_create_ddl() -> String {
979 format!(
980 "CREATE TYPE \"{}\" AS (\n{}\n);",
981 #type_name,
982 #field_definitions
983 )
984 }
985
986 pub fn generate_drop_ddl() -> String {
988 format!("DROP TYPE IF EXISTS \"{}\" CASCADE;", #type_name)
989 }
990
991 pub fn pg_type_name() -> &'static str {
993 #type_name
994 }
995 }
996 }
997}