Skip to main content

mool_macros_impl/
record.rs

1use quote::quote;
2use std::collections::HashSet;
3use syn::{DeriveInput, GenericArgument, PathArguments, Type, TypePath};
4
5use crate::schemable::{FieldMeta, ParsedStruct, to_snake_case};
6use crate::typed_handles::gen_typed_handles;
7
8/// Derives the Record trait for deserializing database rows into structs.
9pub fn derive_record(
10    input: proc_macro2::TokenStream,
11    runtime_path: proc_macro2::TokenStream,
12) -> proc_macro2::TokenStream {
13    let input = match syn::parse2::<DeriveInput>(input) {
14        Ok(input) => input,
15        Err(err) => return err.to_compile_error(),
16    };
17    derive_record_impl(&input, runtime_path)
18}
19
20/// Internal implementation of Record derive macro.
21pub(crate) fn derive_record_impl(
22    input: &DeriveInput,
23    runtime_path: proc_macro2::TokenStream,
24) -> proc_macro2::TokenStream {
25    let parsed = match ParsedStruct::from_derive_input(input.clone()) {
26        Ok(p) => p,
27        Err(e) => return e.to_compile_error(),
28    };
29
30    let ident = &parsed.ident;
31    let mut generics = parsed.generics.clone();
32
33    let crate_path = crate::runtime_path(input, runtime_path);
34
35    gen_where_clause(&mut generics, &parsed.fields, &crate_path);
36
37    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
38    let table_name = gen_table_name(&parsed, &crate_path);
39    let table_schema = gen_table_schema(&parsed);
40    let scan_root = gen_scan_root(&parsed.fields);
41    let references = gen_references(&parsed.fields, &crate_path);
42    let field_inits = gen_field_initializers(&parsed.fields, &crate_path);
43    let field_inits_unordered = gen_field_initializers_unordered(&parsed.fields, &crate_path);
44    let column_names = gen_record_column_names(&parsed.fields, &crate_path);
45    let bind_names = gen_record_bind_column_names(&parsed.fields, &crate_path);
46    let bind_stmts = gen_bind_statements(&parsed.fields, &crate_path);
47    let bind_selected_arms = gen_bind_selected_arms(&parsed.fields, &crate_path);
48    let typed_handles = gen_typed_handles(&parsed, &crate_path);
49
50    quote! {
51        impl #impl_generics #crate_path::Record for #ident #ty_generics #where_clause {
52            fn record_schema() -> #crate_path::RecordSchema<Self> {
53                let references = {
54                    let mut refs = ::std::vec::Vec::new();
55                    #(#references)*
56                    refs
57                };
58                let columns = {
59                    let mut cols = ::std::vec::Vec::new();
60                    #(#column_names)*
61                    cols
62                };
63                let bind_columns = {
64                    let mut cols = ::std::vec::Vec::new();
65                    #(#bind_names)*
66                    cols
67                };
68                #crate_path::RecordSchema::new(#table_name)
69                    .schema(#table_schema)
70                    .root(#scan_root)
71                    .references(references)
72                    .columns(columns)
73                    .bind_columns(bind_columns)
74            }
75
76            fn record_bind_values(
77                &self,
78                args: &mut #crate_path::Arguments<'static>,
79            ) -> Result<(), ::sqlx::Error> {
80                #(#bind_stmts)*
81                Ok(())
82            }
83
84            fn record_bind_selected(
85                &self,
86                columns: &[&str],
87                args: &mut #crate_path::Arguments<'static>,
88            ) -> Result<(), ::sqlx::Error> {
89                for column in columns {
90                    match *column {
91                        #(#bind_selected_arms)*
92                        other => {
93                            return Err(::sqlx::Error::ColumnNotFound(other.to_string()));
94                        }
95                    }
96                }
97                Ok(())
98            }
99
100            fn record_scan_ordered(
101                row: &#crate_path::Row,
102                start_idx: &mut usize,
103            ) -> Result<Self, ::sqlx::Error> {
104                use ::sqlx::Row as _;
105                use ::sqlx::ValueRef as _;
106                Ok(Self {
107                    #(#field_inits)*
108                })
109            }
110
111            fn record_scan_unordered(
112                row: &#crate_path::Row,
113            ) -> Result<Self, ::sqlx::Error> {
114                use ::sqlx::Row as _;
115                Ok(Self {
116                    #(#field_inits_unordered)*
117                })
118            }
119        }
120
121        impl #impl_generics ::sqlx::FromRow<'_, #crate_path::Row> for #ident #ty_generics #where_clause {
122            fn from_row(row: &#crate_path::Row) -> Result<Self, ::sqlx::Error> {
123                <Self as #crate_path::Record>::record_scan(row)
124            }
125        }
126
127        #typed_handles
128    }
129}
130
131/// Generate where clause predicates for Record trait bounds.
132fn gen_where_clause(
133    generics: &mut syn::Generics,
134    fields: &[FieldMeta],
135    crate_path: &proc_macro2::TokenStream,
136) {
137    let mut seen = HashSet::new();
138    let wc = generics.where_clause.get_or_insert(syn::WhereClause {
139        where_token: <syn::Token![where]>::default(),
140        predicates: syn::punctuated::Punctuated::new(),
141    });
142
143    for field in fields {
144        if is_skip(field) {
145            continue;
146        }
147
148        let ty = &field.ty;
149        let ty_str = quote::quote!(#ty).to_string();
150
151        if (is_flatten(field) || is_reference(field)) && is_selectable(field) {
152            let bound_ty = option_inner_type(ty).unwrap_or(ty);
153            let bound_ty_str = quote::quote!(#bound_ty).to_string();
154            if seen.insert(bound_ty_str) {
155                wc.predicates.push(syn::parse_quote! {
156                    #bound_ty: #crate_path::Record
157                });
158            }
159        } else if is_json(field) && is_selectable(field) && seen.insert(ty_str.clone()) {
160            wc.predicates.push(syn::parse_quote! {
161                #ty: ::serde::de::DeserializeOwned
162            });
163        }
164
165        if !is_bindable(field) {
166            continue;
167        }
168
169        if is_flatten(field) {
170            if seen.insert(ty_str.clone()) {
171                wc.predicates.push(syn::parse_quote! {
172                    #ty: #crate_path::Record
173                });
174            }
175        } else if is_json(field) {
176            if seen.insert(format!("{ty_str}:serialize")) {
177                wc.predicates.push(syn::parse_quote! {
178                    #ty: ::serde::Serialize
179                });
180            }
181        } else {
182            wc.predicates.push(syn::parse_quote! {
183                #ty: ::core::clone::Clone
184                    + for<'q> ::sqlx::Encode<'q, #crate_path::Database>
185                    + ::sqlx::Type<#crate_path::Database>
186                    + ::core::marker::Send
187            });
188        }
189    }
190}
191
192/// Generate field initializers for struct construction.
193fn gen_field_initializers(
194    fields: &[FieldMeta],
195    crate_path: &proc_macro2::TokenStream,
196) -> Vec<proc_macro2::TokenStream> {
197    let mut inits = Vec::with_capacity(fields.len());
198
199    for field in fields {
200        let Some(ident) = &field.ident else {
201            continue;
202        };
203
204        let init = if is_skip(field) || !is_selectable(field) {
205            gen_default_init(ident)
206        } else if is_reference(field) && is_option(&field.ty) {
207            gen_optional_reference_init(ident, &field.ty, crate_path)
208        } else if is_flatten(field) || is_reference(field) {
209            gen_flatten_init(
210                ident,
211                option_inner_type(&field.ty).unwrap_or(&field.ty),
212                crate_path,
213            )
214        } else if is_json(field) {
215            gen_json_init(ident, crate_path)
216        } else {
217            gen_scalar_init(ident)
218        };
219
220        inits.push(init);
221    }
222
223    inits
224}
225
226fn gen_table_name(
227    parsed: &ParsedStruct,
228    crate_path: &proc_macro2::TokenStream,
229) -> proc_macro2::TokenStream {
230    if let Some(name) = parsed
231        .container
232        .name
233        .as_ref()
234        .or(parsed.container.table.as_ref())
235    {
236        let value = name.value();
237        return quote! { #value };
238    }
239
240    if let Some(field) = parsed
241        .fields
242        .iter()
243        .find(|field| is_flatten(field) || is_reference(field))
244    {
245        let ty = option_inner_type(&field.ty).unwrap_or(&field.ty);
246        return quote! { <#ty as #crate_path::Record>::record_table_name() };
247    }
248
249    let value = to_snake_case(&parsed.ident.to_string());
250    quote! { #value }
251}
252
253fn gen_table_schema(parsed: &ParsedStruct) -> proc_macro2::TokenStream {
254    if let Some(schema) = parsed.container.schema.as_ref() {
255        let value = schema.value();
256        quote! { Some(#value) }
257    } else {
258        quote! { None }
259    }
260}
261
262fn gen_scan_root(fields: &[FieldMeta]) -> proc_macro2::TokenStream {
263    if let Some(field) = fields
264        .iter()
265        .find(|field| is_flatten(field) || is_reference(field))
266        && let Some(ident) = &field.ident
267    {
268        let value = ident.to_string();
269        return quote! { Some(#value) };
270    }
271
272    quote! { None }
273}
274
275fn gen_references(
276    fields: &[FieldMeta],
277    crate_path: &proc_macro2::TokenStream,
278) -> Vec<proc_macro2::TokenStream> {
279    fields
280        .iter()
281        .filter(|field| is_reference(field))
282        .filter_map(|field| {
283            let ident = field.ident.as_ref()?;
284            let logical_name = ident.to_string();
285            let ty = option_inner_type(&field.ty).unwrap_or(&field.ty);
286            if let Some(backref) = field.column.backref.as_ref() {
287                let marker = &backref.path;
288                return Some(quote! {
289                    let backref = <#marker as #crate_path::Backref>::meta();
290                    refs.push(#crate_path::ReferenceMeta {
291                        logical_name: #logical_name,
292                        table_name: backref.table_name,
293                        table_schema: backref.table_schema,
294                        columns: backref.columns,
295                        join_type: backref.join_type,
296                    });
297                });
298            }
299
300            let reference = field.column.reference.as_ref()?;
301            if !reference.is_join_reference() {
302                return None;
303            }
304            if let Some(marker) = reference.relation.as_ref() {
305                return Some(quote! {
306                    let relation = <#marker as #crate_path::Backref>::meta();
307                    refs.push(#crate_path::ReferenceMeta {
308                        logical_name: #logical_name,
309                        table_name: relation.table_name,
310                        table_schema: relation.table_schema,
311                        columns: relation.columns,
312                        join_type: relation.join_type,
313                    });
314                });
315            }
316            let join_type = reference_join_type(reference, &field.ty, crate_path);
317            let columns = reference_columns(reference, crate_path);
318
319            Some(quote! {
320                refs.push(#crate_path::ReferenceMeta {
321                    logical_name: #logical_name,
322                    table_name: <#ty as #crate_path::Record>::record_table_name(),
323                    table_schema: <#ty as #crate_path::Record>::record_table_schema(),
324                    columns: &[#(#columns),*],
325                    join_type: #join_type,
326                });
327            })
328        })
329        .collect()
330}
331
332fn reference_join_type(
333    reference: &crate::schemable::ReferenceSpec,
334    ty: &Type,
335    crate_path: &proc_macro2::TokenStream,
336) -> proc_macro2::TokenStream {
337    match reference.join.as_deref() {
338        Some("left") => quote! { #crate_path::JoinType::Left },
339        Some("inner") => quote! { #crate_path::JoinType::Inner },
340        _ if is_option(ty) => quote! { #crate_path::JoinType::Left },
341        _ => quote! { #crate_path::JoinType::Inner },
342    }
343}
344
345fn reference_columns(
346    reference: &crate::schemable::ReferenceSpec,
347    crate_path: &proc_macro2::TokenStream,
348) -> Vec<proc_macro2::TokenStream> {
349    if !reference.on.is_empty() {
350        return reference
351            .on
352            .iter()
353            .map(|column| {
354                let from = column.from.as_str();
355                let to = column.to.as_str();
356                quote! { #crate_path::JoinColumn { from: #from, to: #to } }
357            })
358            .collect();
359    }
360    let from = reference.from.as_deref().unwrap_or("");
361    let to = reference.to.as_deref().unwrap_or("id");
362    vec![quote! { #crate_path::JoinColumn { from: #from, to: #to } }]
363}
364
365/// Generate field initializers for unordered (name-based) struct construction.
366fn gen_field_initializers_unordered(
367    fields: &[FieldMeta],
368    crate_path: &proc_macro2::TokenStream,
369) -> Vec<proc_macro2::TokenStream> {
370    let mut inits = Vec::with_capacity(fields.len());
371
372    for field in fields {
373        let Some(ident) = &field.ident else {
374            continue;
375        };
376
377        let init = if is_skip(field) || !is_selectable(field) {
378            gen_default_init(ident)
379        } else if is_reference(field) {
380            // Reference fields cannot be scanned unordered - they need prefixed column names
381            gen_reference_unordered_error(ident, &field.ty)
382        } else if is_flatten(field) {
383            gen_flatten_init_unordered(ident, &field.ty, crate_path)
384        } else if is_json(field) {
385            gen_json_init_unordered(ident, field, crate_path)
386        } else {
387            gen_scalar_init_unordered(ident, field)
388        };
389
390        inits.push(init);
391    }
392
393    inits
394}
395
396/// Generate default initialization for non-selectable field.
397fn gen_default_init(ident: &syn::Ident) -> proc_macro2::TokenStream {
398    quote! {
399        #ident: ::core::default::Default::default(),
400    }
401}
402
403/// Generate compile error for reference field in unordered scan.
404fn gen_reference_unordered_error(ident: &syn::Ident, ty: &Type) -> proc_macro2::TokenStream {
405    let error_msg = format!(
406        "Cannot use record_scan_unordered with reference field '{}' of type '{}'. \
407        Reference fields require ordered scanning (record_scan_ordered) because they use \
408        prefixed column names. Use record_scan_ordered or scan_row instead.",
409        ident,
410        quote::quote!(#ty)
411    );
412    quote! {
413        #ident: {
414            let unsupported: Result<#ty, ::sqlx::Error> =
415                Err(::sqlx::Error::ColumnNotFound(#error_msg.to_string()));
416            unsupported?
417        },
418    }
419}
420
421/// Generate initialization for flattened field.
422fn gen_flatten_init(
423    ident: &syn::Ident,
424    ty: &Type,
425    crate_path: &proc_macro2::TokenStream,
426) -> proc_macro2::TokenStream {
427    quote! {
428        #ident: <#ty as #crate_path::Record>::record_scan_ordered(row, start_idx)?,
429    }
430}
431
432fn gen_optional_reference_init(
433    ident: &syn::Ident,
434    ty: &Type,
435    crate_path: &proc_macro2::TokenStream,
436) -> proc_macro2::TokenStream {
437    let inner_ty = option_inner_type(ty).unwrap_or(ty);
438    quote! {
439        #ident: {
440            let nested_cols = <#inner_ty as #crate_path::Record>::record_column_names();
441            let start = *start_idx;
442            let mut all_null = true;
443            for offset in 0..nested_cols.len() {
444                let raw = row.try_get_raw(start + offset)?;
445                if !raw.is_null() {
446                    all_null = false;
447                    break;
448                }
449            }
450            if all_null {
451                *start_idx += nested_cols.len();
452                None
453            } else {
454                Some(<#inner_ty as #crate_path::Record>::record_scan_ordered(row, start_idx)?)
455            }
456        },
457    }
458}
459
460/// Generate initialization for JSON-deserialized field.
461fn gen_json_init(
462    ident: &syn::Ident,
463    _crate_path: &proc_macro2::TokenStream,
464) -> proc_macro2::TokenStream {
465    quote! {
466        #ident: {
467            let json_val: ::serde_json::Value = row.try_get(*start_idx)?;
468            *start_idx += 1;
469            ::serde_json::from_value(json_val)
470                .map_err(|e| ::sqlx::Error::Decode(Box::new(e)))?
471        },
472    }
473}
474
475/// Generate initialization for scalar field.
476fn gen_scalar_init(ident: &syn::Ident) -> proc_macro2::TokenStream {
477    quote! {
478        #ident: {
479            let val = row.try_get(*start_idx)?;
480            *start_idx += 1;
481            val
482        },
483    }
484}
485
486/// Check if field should be skipped (column only).
487pub(super) fn is_skip(field: &FieldMeta) -> bool {
488    field.column.skip || field.column.prefetch.is_some()
489}
490
491/// Check if field should be flattened (column only).
492pub(super) fn is_flatten(field: &FieldMeta) -> bool {
493    field.column.flatten
494}
495
496/// Check if field is a reference (column only).
497pub(super) fn is_reference(field: &FieldMeta) -> bool {
498    field
499        .column
500        .reference
501        .as_ref()
502        .is_some_and(|reference| reference.is_join_reference())
503        || field.column.backref.is_some()
504}
505
506fn is_bindable(field: &FieldMeta) -> bool {
507    !is_skip(field)
508        && !is_reference(field)
509        && !field.column.read_only
510        && !field.column.skip_bind
511        && field.column.insertable.unwrap_or(true)
512        && field.column.updatable.unwrap_or(true)
513}
514
515fn is_option(ty: &Type) -> bool {
516    option_inner_type(ty).is_some()
517}
518
519pub(super) fn option_inner_type(ty: &Type) -> Option<&Type> {
520    let Type::Path(path) = ty else {
521        return None;
522    };
523    if !is_canonical_option(path) {
524        return None;
525    }
526    let segment = path.path.segments.last()?;
527    let PathArguments::AngleBracketed(args) = &segment.arguments else {
528        return None;
529    };
530    let Some(GenericArgument::Type(inner)) = args.args.first() else {
531        return None;
532    };
533    if is_u8_type(inner) {
534        return None;
535    }
536    Some(inner)
537}
538
539fn is_u8_type(ty: &Type) -> bool {
540    let Type::Path(path) = ty else {
541        return false;
542    };
543    if path.qself.is_some() {
544        return false;
545    }
546    let mut segments = path
547        .path
548        .segments
549        .iter()
550        .map(|segment| segment.ident.to_string());
551    matches!(
552        (
553            segments.next(),
554            segments.next(),
555            segments.next(),
556            segments.next()
557        ),
558        (Some(first), None, None, None) if first == "u8"
559    )
560}
561
562pub(super) fn array_inner_type(ty: &Type) -> Option<&Type> {
563    let candidate = option_inner_type(ty).unwrap_or(ty);
564    let Type::Path(path) = candidate else {
565        return None;
566    };
567    if !is_canonical_vec(path) {
568        return None;
569    }
570    let segment = path.path.segments.last()?;
571    let PathArguments::AngleBracketed(args) = &segment.arguments else {
572        return None;
573    };
574    let Some(GenericArgument::Type(inner)) = args.args.first() else {
575        return None;
576    };
577    Some(inner)
578}
579
580fn is_canonical_option(path: &TypePath) -> bool {
581    if path.qself.is_some() {
582        return false;
583    }
584    let mut segments = path
585        .path
586        .segments
587        .iter()
588        .map(|segment| segment.ident.to_string());
589    match (
590        segments.next(),
591        segments.next(),
592        segments.next(),
593        segments.next(),
594    ) {
595        (Some(first), None, None, None) => first == "Option",
596        (Some(first), Some(second), Some(third), None) => {
597            (first == "std" || first == "core") && second == "option" && third == "Option"
598        }
599        _ => false,
600    }
601}
602
603fn is_canonical_vec(path: &TypePath) -> bool {
604    if path.qself.is_some() {
605        return false;
606    }
607    let mut segments = path
608        .path
609        .segments
610        .iter()
611        .map(|segment| segment.ident.to_string());
612    match (
613        segments.next(),
614        segments.next(),
615        segments.next(),
616        segments.next(),
617    ) {
618        (Some(first), None, None, None) => first == "Vec",
619        (Some(first), Some(second), Some(third), None) => {
620            (first == "std" || first == "alloc") && second == "vec" && third == "Vec"
621        }
622        _ => false,
623    }
624}
625
626/// Check if field should be JSON-serialized (column only).
627pub(super) fn is_json(field: &FieldMeta) -> bool {
628    field.column.json
629        || field
630            .column
631            .sql_type
632            .as_ref()
633            .map(|ty| matches!(ty.value().to_ascii_lowercase().as_str(), "json" | "jsonb"))
634            .unwrap_or(false)
635}
636
637/// Check if field is selectable (column attr only, None means true).
638pub(super) fn is_selectable(field: &FieldMeta) -> bool {
639    field.column.selectable.unwrap_or(true)
640}
641
642/// Generate the record_column_names implementation.
643fn gen_record_column_names(
644    fields: &[FieldMeta],
645    crate_path: &proc_macro2::TokenStream,
646) -> Vec<proc_macro2::TokenStream> {
647    let mut stmts = Vec::new();
648
649    for field in fields {
650        if is_skip(field) || !is_selectable(field) {
651            continue;
652        }
653
654        if is_reference(field) {
655            let ty = option_inner_type(&field.ty).unwrap_or(&field.ty);
656            let field_name = field
657                .column
658                .name
659                .as_ref()
660                .map(|lit| lit.value())
661                .or_else(|| field.ident.as_ref().map(|i| i.to_string()))
662                .unwrap_or_default();
663
664            stmts.push(quote! {
665                {
666                    let nested_cols = <#ty as #crate_path::Record>::record_column_names();
667                    for col in nested_cols {
668                        cols.push(format!("{}.{}", #field_name, col));
669                    }
670                }
671            });
672        } else if is_flatten(field) {
673            let ty = &field.ty;
674            stmts.push(quote! {
675                cols.extend(<#ty as #crate_path::Record>::record_column_names());
676            });
677        } else {
678            let col_name = field
679                .column
680                .name
681                .as_ref()
682                .map(|lit| lit.value())
683                .or_else(|| field.ident.as_ref().map(|i| i.to_string()))
684                .unwrap_or_default();
685            stmts.push(quote! {
686                cols.push(#col_name.to_string());
687            });
688        }
689    }
690
691    stmts
692}
693
694fn gen_record_bind_column_names(
695    fields: &[FieldMeta],
696    crate_path: &proc_macro2::TokenStream,
697) -> Vec<proc_macro2::TokenStream> {
698    fields
699        .iter()
700        .filter(|field| is_bindable(field))
701        .filter_map(|field| {
702            if is_flatten(field) {
703                let ty = &field.ty;
704                return Some(quote! {
705                    cols.extend(<#ty as #crate_path::Record>::record_bind_column_names());
706                });
707            }
708            let col_name = field
709                .column
710                .name
711                .as_ref()
712                .map(|lit| lit.value())
713                .or_else(|| field.ident.as_ref().map(|ident| ident.to_string()))?;
714            Some(quote! {
715                cols.push(#col_name.to_string());
716            })
717        })
718        .collect()
719}
720
721fn gen_bind_statements(
722    fields: &[FieldMeta],
723    crate_path: &proc_macro2::TokenStream,
724) -> Vec<proc_macro2::TokenStream> {
725    fields
726        .iter()
727        .filter(|field| is_bindable(field))
728        .filter_map(|field| {
729            let ident = field.ident.as_ref()?;
730            if is_flatten(field) {
731                let ty = &field.ty;
732                return Some(quote! {
733                    <#ty as #crate_path::Record>::record_bind_values(&self.#ident, args)?;
734                });
735            }
736            if is_json(field) {
737                return Some(quote! {
738                    {
739                        let value = ::serde_json::to_value(&self.#ident)
740                            .map_err(|err| ::sqlx::Error::Decode(Box::new(err)))?;
741                        ::sqlx::Arguments::add(args, value)
742                            .map_err(::sqlx::Error::Decode)?;
743                    }
744                });
745            }
746            Some(quote! {
747                {
748                    ::sqlx::Arguments::add(args, self.#ident.clone())
749                        .map_err(::sqlx::Error::Decode)?;
750                }
751            })
752        })
753        .collect()
754}
755
756fn gen_bind_selected_arms(
757    fields: &[FieldMeta],
758    crate_path: &proc_macro2::TokenStream,
759) -> Vec<proc_macro2::TokenStream> {
760    fields
761        .iter()
762        .filter(|field| is_bindable(field))
763        .filter_map(|field| {
764            let ident = field.ident.as_ref()?;
765            if is_flatten(field) {
766                let ty = &field.ty;
767                return Some(quote! {
768                    nested if <#ty as #crate_path::Record>::record_bind_column_names()
769                        .iter()
770                        .any(|name| name == nested) => {
771                        <#ty as #crate_path::Record>::record_bind_selected(
772                            &self.#ident,
773                            &[nested],
774                            args,
775                        )?;
776                    }
777                });
778            }
779            let col_name = field
780                .column
781                .name
782                .as_ref()
783                .map(|lit| lit.value())
784                .or_else(|| field.ident.as_ref().map(|ident| ident.to_string()))?;
785            if is_json(field) {
786                return Some(quote! {
787                    #col_name => {
788                        let value = ::serde_json::to_value(&self.#ident)
789                            .map_err(|err| ::sqlx::Error::Decode(Box::new(err)))?;
790                        ::sqlx::Arguments::add(args, value)
791                            .map_err(::sqlx::Error::Decode)?;
792                    }
793                });
794            }
795            Some(quote! {
796                #col_name => {
797                    ::sqlx::Arguments::add(args, self.#ident.clone())
798                        .map_err(::sqlx::Error::Decode)?;
799                }
800            })
801        })
802        .collect()
803}
804
805/// Generate initialization for flattened field (unordered).
806fn gen_flatten_init_unordered(
807    ident: &syn::Ident,
808    ty: &Type,
809    crate_path: &proc_macro2::TokenStream,
810) -> proc_macro2::TokenStream {
811    quote! {
812        #ident: <#ty as #crate_path::Record>::record_scan_unordered(row)?,
813    }
814}
815
816/// Generate initialization for JSON-deserialized field (unordered).
817fn gen_json_init_unordered(
818    ident: &syn::Ident,
819    field: &FieldMeta,
820    _crate_path: &proc_macro2::TokenStream,
821) -> proc_macro2::TokenStream {
822    let col_name = field
823        .column
824        .name
825        .as_ref()
826        .map(|lit| lit.value())
827        .unwrap_or_else(|| ident.to_string());
828
829    quote! {
830        #ident: {
831            let json_val: ::serde_json::Value = row.try_get(#col_name)?;
832            ::serde_json::from_value(json_val)
833                .map_err(|e| ::sqlx::Error::Decode(Box::new(e)))?
834        },
835    }
836}
837
838/// Generate initialization for scalar field (unordered).
839fn gen_scalar_init_unordered(ident: &syn::Ident, field: &FieldMeta) -> proc_macro2::TokenStream {
840    let col_name = field
841        .column
842        .name
843        .as_ref()
844        .map(|lit| lit.value())
845        .unwrap_or_else(|| ident.to_string());
846
847    quote! {
848        #ident: row.try_get(#col_name)?,
849    }
850}