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 let Some(marker) = reference.relation.as_ref() {
302                return Some(quote! {
303                    let relation = <#marker as #crate_path::Backref>::meta();
304                    refs.push(#crate_path::ReferenceMeta {
305                        logical_name: #logical_name,
306                        table_name: relation.table_name,
307                        table_schema: relation.table_schema,
308                        columns: relation.columns,
309                        join_type: relation.join_type,
310                    });
311                });
312            }
313            let join_type = reference_join_type(reference, &field.ty, crate_path);
314            let columns = reference_columns(reference, crate_path);
315
316            Some(quote! {
317                refs.push(#crate_path::ReferenceMeta {
318                    logical_name: #logical_name,
319                    table_name: <#ty as #crate_path::Record>::record_table_name(),
320                    table_schema: <#ty as #crate_path::Record>::record_table_schema(),
321                    columns: &[#(#columns),*],
322                    join_type: #join_type,
323                });
324            })
325        })
326        .collect()
327}
328
329fn reference_join_type(
330    reference: &crate::schemable::ReferenceSpec,
331    ty: &Type,
332    crate_path: &proc_macro2::TokenStream,
333) -> proc_macro2::TokenStream {
334    match reference.join.as_deref() {
335        Some("left") => quote! { #crate_path::JoinType::Left },
336        Some("inner") => quote! { #crate_path::JoinType::Inner },
337        _ if is_option(ty) => quote! { #crate_path::JoinType::Left },
338        _ => quote! { #crate_path::JoinType::Inner },
339    }
340}
341
342fn reference_columns(
343    reference: &crate::schemable::ReferenceSpec,
344    crate_path: &proc_macro2::TokenStream,
345) -> Vec<proc_macro2::TokenStream> {
346    if !reference.on.is_empty() {
347        return reference
348            .on
349            .iter()
350            .map(|column| {
351                let from = column.from.as_str();
352                let to = column.to.as_str();
353                quote! { #crate_path::JoinColumn { from: #from, to: #to } }
354            })
355            .collect();
356    }
357    let from = reference.from.as_deref().unwrap_or("");
358    let to = reference.to.as_deref().unwrap_or("id");
359    vec![quote! { #crate_path::JoinColumn { from: #from, to: #to } }]
360}
361
362/// Generate field initializers for unordered (name-based) struct construction.
363fn gen_field_initializers_unordered(
364    fields: &[FieldMeta],
365    crate_path: &proc_macro2::TokenStream,
366) -> Vec<proc_macro2::TokenStream> {
367    let mut inits = Vec::with_capacity(fields.len());
368
369    for field in fields {
370        let Some(ident) = &field.ident else {
371            continue;
372        };
373
374        let init = if is_skip(field) || !is_selectable(field) {
375            gen_default_init(ident)
376        } else if is_reference(field) {
377            // Reference fields cannot be scanned unordered - they need prefixed column names
378            gen_reference_unordered_error(ident, &field.ty)
379        } else if is_flatten(field) {
380            gen_flatten_init_unordered(ident, &field.ty, crate_path)
381        } else if is_json(field) {
382            gen_json_init_unordered(ident, field, crate_path)
383        } else {
384            gen_scalar_init_unordered(ident, field)
385        };
386
387        inits.push(init);
388    }
389
390    inits
391}
392
393/// Generate default initialization for non-selectable field.
394fn gen_default_init(ident: &syn::Ident) -> proc_macro2::TokenStream {
395    quote! {
396        #ident: ::core::default::Default::default(),
397    }
398}
399
400/// Generate compile error for reference field in unordered scan.
401fn gen_reference_unordered_error(ident: &syn::Ident, ty: &Type) -> proc_macro2::TokenStream {
402    let error_msg = format!(
403        "Cannot use record_scan_unordered with reference field '{}' of type '{}'. \
404        Reference fields require ordered scanning (record_scan_ordered) because they use \
405        prefixed column names. Use record_scan_ordered or scan_row instead.",
406        ident,
407        quote::quote!(#ty)
408    );
409    quote! {
410        #ident: {
411            let unsupported: Result<#ty, ::sqlx::Error> =
412                Err(::sqlx::Error::ColumnNotFound(#error_msg.to_string()));
413            unsupported?
414        },
415    }
416}
417
418/// Generate initialization for flattened field.
419fn gen_flatten_init(
420    ident: &syn::Ident,
421    ty: &Type,
422    crate_path: &proc_macro2::TokenStream,
423) -> proc_macro2::TokenStream {
424    quote! {
425        #ident: <#ty as #crate_path::Record>::record_scan_ordered(row, start_idx)?,
426    }
427}
428
429fn gen_optional_reference_init(
430    ident: &syn::Ident,
431    ty: &Type,
432    crate_path: &proc_macro2::TokenStream,
433) -> proc_macro2::TokenStream {
434    let inner_ty = option_inner_type(ty).unwrap_or(ty);
435    quote! {
436        #ident: {
437            let nested_cols = <#inner_ty as #crate_path::Record>::record_column_names();
438            let start = *start_idx;
439            let mut all_null = true;
440            for offset in 0..nested_cols.len() {
441                let raw = row.try_get_raw(start + offset)?;
442                if !raw.is_null() {
443                    all_null = false;
444                    break;
445                }
446            }
447            if all_null {
448                *start_idx += nested_cols.len();
449                None
450            } else {
451                Some(<#inner_ty as #crate_path::Record>::record_scan_ordered(row, start_idx)?)
452            }
453        },
454    }
455}
456
457/// Generate initialization for JSON-deserialized field.
458fn gen_json_init(
459    ident: &syn::Ident,
460    _crate_path: &proc_macro2::TokenStream,
461) -> proc_macro2::TokenStream {
462    quote! {
463        #ident: {
464            let json_val: ::serde_json::Value = row.try_get(*start_idx)?;
465            *start_idx += 1;
466            ::serde_json::from_value(json_val)
467                .map_err(|e| ::sqlx::Error::Decode(Box::new(e)))?
468        },
469    }
470}
471
472/// Generate initialization for scalar field.
473fn gen_scalar_init(ident: &syn::Ident) -> proc_macro2::TokenStream {
474    quote! {
475        #ident: {
476            let val = row.try_get(*start_idx)?;
477            *start_idx += 1;
478            val
479        },
480    }
481}
482
483/// Check if field should be skipped (column only).
484pub(super) fn is_skip(field: &FieldMeta) -> bool {
485    field.column.skip || field.column.prefetch.is_some()
486}
487
488/// Check if field should be flattened (column only).
489pub(super) fn is_flatten(field: &FieldMeta) -> bool {
490    field.column.flatten
491}
492
493/// Check if field is a reference (column only).
494pub(super) fn is_reference(field: &FieldMeta) -> bool {
495    field.column.reference.is_some() || field.column.backref.is_some()
496}
497
498fn is_bindable(field: &FieldMeta) -> bool {
499    !is_skip(field)
500        && !is_reference(field)
501        && !field.column.read_only
502        && !field.column.skip_bind
503        && field.column.insertable.unwrap_or(true)
504        && field.column.updatable.unwrap_or(true)
505}
506
507fn is_option(ty: &Type) -> bool {
508    option_inner_type(ty).is_some()
509}
510
511pub(super) fn option_inner_type(ty: &Type) -> Option<&Type> {
512    let Type::Path(path) = ty else {
513        return None;
514    };
515    if !is_canonical_option(path) {
516        return None;
517    }
518    let segment = path.path.segments.last()?;
519    let PathArguments::AngleBracketed(args) = &segment.arguments else {
520        return None;
521    };
522    let Some(GenericArgument::Type(inner)) = args.args.first() else {
523        return None;
524    };
525    if is_u8_type(inner) {
526        return None;
527    }
528    Some(inner)
529}
530
531fn is_u8_type(ty: &Type) -> bool {
532    let Type::Path(path) = ty else {
533        return false;
534    };
535    if path.qself.is_some() {
536        return false;
537    }
538    let mut segments = path
539        .path
540        .segments
541        .iter()
542        .map(|segment| segment.ident.to_string());
543    matches!(
544        (
545            segments.next(),
546            segments.next(),
547            segments.next(),
548            segments.next()
549        ),
550        (Some(first), None, None, None) if first == "u8"
551    )
552}
553
554pub(super) fn array_inner_type(ty: &Type) -> Option<&Type> {
555    let candidate = option_inner_type(ty).unwrap_or(ty);
556    let Type::Path(path) = candidate else {
557        return None;
558    };
559    if !is_canonical_vec(path) {
560        return None;
561    }
562    let segment = path.path.segments.last()?;
563    let PathArguments::AngleBracketed(args) = &segment.arguments else {
564        return None;
565    };
566    let Some(GenericArgument::Type(inner)) = args.args.first() else {
567        return None;
568    };
569    Some(inner)
570}
571
572fn is_canonical_option(path: &TypePath) -> bool {
573    if path.qself.is_some() {
574        return false;
575    }
576    let mut segments = path
577        .path
578        .segments
579        .iter()
580        .map(|segment| segment.ident.to_string());
581    match (
582        segments.next(),
583        segments.next(),
584        segments.next(),
585        segments.next(),
586    ) {
587        (Some(first), None, None, None) => first == "Option",
588        (Some(first), Some(second), Some(third), None) => {
589            (first == "std" || first == "core") && second == "option" && third == "Option"
590        }
591        _ => false,
592    }
593}
594
595fn is_canonical_vec(path: &TypePath) -> bool {
596    if path.qself.is_some() {
597        return false;
598    }
599    let mut segments = path
600        .path
601        .segments
602        .iter()
603        .map(|segment| segment.ident.to_string());
604    match (
605        segments.next(),
606        segments.next(),
607        segments.next(),
608        segments.next(),
609    ) {
610        (Some(first), None, None, None) => first == "Vec",
611        (Some(first), Some(second), Some(third), None) => {
612            (first == "std" || first == "alloc") && second == "vec" && third == "Vec"
613        }
614        _ => false,
615    }
616}
617
618/// Check if field should be JSON-serialized (column only).
619pub(super) fn is_json(field: &FieldMeta) -> bool {
620    field.column.json
621        || field
622            .column
623            .sql_type
624            .as_ref()
625            .map(|ty| matches!(ty.value().to_ascii_lowercase().as_str(), "json" | "jsonb"))
626            .unwrap_or(false)
627}
628
629/// Check if field is selectable (column attr only, None means true).
630pub(super) fn is_selectable(field: &FieldMeta) -> bool {
631    field.column.selectable.unwrap_or(true)
632}
633
634/// Generate the record_column_names implementation.
635fn gen_record_column_names(
636    fields: &[FieldMeta],
637    crate_path: &proc_macro2::TokenStream,
638) -> Vec<proc_macro2::TokenStream> {
639    let mut stmts = Vec::new();
640
641    for field in fields {
642        if is_skip(field) || !is_selectable(field) {
643            continue;
644        }
645
646        if is_reference(field) {
647            let ty = option_inner_type(&field.ty).unwrap_or(&field.ty);
648            let field_name = field
649                .column
650                .name
651                .as_ref()
652                .map(|lit| lit.value())
653                .or_else(|| field.ident.as_ref().map(|i| i.to_string()))
654                .unwrap_or_default();
655
656            stmts.push(quote! {
657                {
658                    let nested_cols = <#ty as #crate_path::Record>::record_column_names();
659                    for col in nested_cols {
660                        cols.push(format!("{}.{}", #field_name, col));
661                    }
662                }
663            });
664        } else if is_flatten(field) {
665            let ty = &field.ty;
666            stmts.push(quote! {
667                cols.extend(<#ty as #crate_path::Record>::record_column_names());
668            });
669        } else {
670            let col_name = field
671                .column
672                .name
673                .as_ref()
674                .map(|lit| lit.value())
675                .or_else(|| field.ident.as_ref().map(|i| i.to_string()))
676                .unwrap_or_default();
677            stmts.push(quote! {
678                cols.push(#col_name.to_string());
679            });
680        }
681    }
682
683    stmts
684}
685
686fn gen_record_bind_column_names(
687    fields: &[FieldMeta],
688    crate_path: &proc_macro2::TokenStream,
689) -> Vec<proc_macro2::TokenStream> {
690    fields
691        .iter()
692        .filter(|field| is_bindable(field))
693        .filter_map(|field| {
694            if is_flatten(field) {
695                let ty = &field.ty;
696                return Some(quote! {
697                    cols.extend(<#ty as #crate_path::Record>::record_bind_column_names());
698                });
699            }
700            let col_name = field
701                .column
702                .name
703                .as_ref()
704                .map(|lit| lit.value())
705                .or_else(|| field.ident.as_ref().map(|ident| ident.to_string()))?;
706            Some(quote! {
707                cols.push(#col_name.to_string());
708            })
709        })
710        .collect()
711}
712
713fn gen_bind_statements(
714    fields: &[FieldMeta],
715    crate_path: &proc_macro2::TokenStream,
716) -> Vec<proc_macro2::TokenStream> {
717    fields
718        .iter()
719        .filter(|field| is_bindable(field))
720        .filter_map(|field| {
721            let ident = field.ident.as_ref()?;
722            if is_flatten(field) {
723                let ty = &field.ty;
724                return Some(quote! {
725                    <#ty as #crate_path::Record>::record_bind_values(&self.#ident, args)?;
726                });
727            }
728            if is_json(field) {
729                return Some(quote! {
730                    {
731                        let value = ::serde_json::to_value(&self.#ident)
732                            .map_err(|err| ::sqlx::Error::Decode(Box::new(err)))?;
733                        ::sqlx::Arguments::add(args, value)
734                            .map_err(::sqlx::Error::Decode)?;
735                    }
736                });
737            }
738            Some(quote! {
739                {
740                    ::sqlx::Arguments::add(args, self.#ident.clone())
741                        .map_err(::sqlx::Error::Decode)?;
742                }
743            })
744        })
745        .collect()
746}
747
748fn gen_bind_selected_arms(
749    fields: &[FieldMeta],
750    crate_path: &proc_macro2::TokenStream,
751) -> Vec<proc_macro2::TokenStream> {
752    fields
753        .iter()
754        .filter(|field| is_bindable(field))
755        .filter_map(|field| {
756            let ident = field.ident.as_ref()?;
757            if is_flatten(field) {
758                let ty = &field.ty;
759                return Some(quote! {
760                    nested if <#ty as #crate_path::Record>::record_bind_column_names()
761                        .iter()
762                        .any(|name| name == nested) => {
763                        <#ty as #crate_path::Record>::record_bind_selected(
764                            &self.#ident,
765                            &[nested],
766                            args,
767                        )?;
768                    }
769                });
770            }
771            let col_name = field
772                .column
773                .name
774                .as_ref()
775                .map(|lit| lit.value())
776                .or_else(|| field.ident.as_ref().map(|ident| ident.to_string()))?;
777            if is_json(field) {
778                return Some(quote! {
779                    #col_name => {
780                        let value = ::serde_json::to_value(&self.#ident)
781                            .map_err(|err| ::sqlx::Error::Decode(Box::new(err)))?;
782                        ::sqlx::Arguments::add(args, value)
783                            .map_err(::sqlx::Error::Decode)?;
784                    }
785                });
786            }
787            Some(quote! {
788                #col_name => {
789                    ::sqlx::Arguments::add(args, self.#ident.clone())
790                        .map_err(::sqlx::Error::Decode)?;
791                }
792            })
793        })
794        .collect()
795}
796
797/// Generate initialization for flattened field (unordered).
798fn gen_flatten_init_unordered(
799    ident: &syn::Ident,
800    ty: &Type,
801    crate_path: &proc_macro2::TokenStream,
802) -> proc_macro2::TokenStream {
803    quote! {
804        #ident: <#ty as #crate_path::Record>::record_scan_unordered(row)?,
805    }
806}
807
808/// Generate initialization for JSON-deserialized field (unordered).
809fn gen_json_init_unordered(
810    ident: &syn::Ident,
811    field: &FieldMeta,
812    _crate_path: &proc_macro2::TokenStream,
813) -> proc_macro2::TokenStream {
814    let col_name = field
815        .column
816        .name
817        .as_ref()
818        .map(|lit| lit.value())
819        .unwrap_or_else(|| ident.to_string());
820
821    quote! {
822        #ident: {
823            let json_val: ::serde_json::Value = row.try_get(#col_name)?;
824            ::serde_json::from_value(json_val)
825                .map_err(|e| ::sqlx::Error::Decode(Box::new(e)))?
826        },
827    }
828}
829
830/// Generate initialization for scalar field (unordered).
831fn gen_scalar_init_unordered(ident: &syn::Ident, field: &FieldMeta) -> proc_macro2::TokenStream {
832    let col_name = field
833        .column
834        .name
835        .as_ref()
836        .map(|lit| lit.value())
837        .unwrap_or_else(|| ident.to_string());
838
839    quote! {
840        #ident: row.try_get(#col_name)?,
841    }
842}