Skip to main content

rustango_macros/
lib.rs

1//! Proc-macros for rustango.
2//!
3//! v0.1 ships `#[derive(Model)]`, which emits:
4//! * a `Model` impl carrying a static `ModelSchema`,
5//! * an `inventory::submit!` so the model is discoverable from the registry,
6//! * an inherent `objects()` returning a `QuerySet<Self>`,
7//! * a `sqlx::FromRow` impl so query results decode into the struct.
8
9use proc_macro::TokenStream;
10use proc_macro2::TokenStream as TokenStream2;
11use quote::quote;
12use syn::{
13    parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields, GenericArgument, LitStr,
14    PathArguments, Type, TypePath,
15};
16
17/// Derive a `Model` impl. See crate docs for the supported attributes.
18#[proc_macro_derive(Model, attributes(rustango))]
19pub fn derive_model(input: TokenStream) -> TokenStream {
20    let input = parse_macro_input!(input as DeriveInput);
21    expand(&input)
22        .unwrap_or_else(syn::Error::into_compile_error)
23        .into()
24}
25
26/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
27/// marker struct, wiring the full CRUD ViewSet in one annotation.
28///
29/// ```ignore
30/// #[derive(ViewSet)]
31/// #[viewset(
32///     model        = Post,
33///     fields       = "id, title, body, author_id",
34///     filter_fields = "author_id",
35///     search_fields = "title, body",
36///     ordering     = "-published_at",
37///     page_size    = 20,
38/// )]
39/// pub struct PostViewSet;
40///
41/// // Mount into your app:
42/// let app = Router::new()
43///     .merge(PostViewSet::router("/api/posts", pool.clone()));
44/// ```
45///
46/// Attributes:
47/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
48///   `SCHEMA` constant drives the endpoints.
49/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
50///   and accepted on create/update (default: all scalar fields).
51/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
52/// * `search_fields = "a, b"` — fields searched by `?search=...`.
53/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
54/// * `page_size = N` — default page size (default: 20, max: 1000).
55/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
56/// * `permissions(list = "...", retrieve = "...", create = "...",
57///   update = "...", destroy = "...")` — codenames required per action.
58#[proc_macro_derive(ViewSet, attributes(viewset))]
59pub fn derive_viewset(input: TokenStream) -> TokenStream {
60    let input = parse_macro_input!(input as DeriveInput);
61    expand_viewset(&input)
62        .unwrap_or_else(syn::Error::into_compile_error)
63        .into()
64}
65
66/// Derive `rustango::forms::FormStruct` (slice 8.4B). Generates a
67/// `parse(&HashMap<String, String>) -> Result<Self, FormError>` impl
68/// that walks every named field and:
69///
70/// * Parses the string value into the field's Rust type (`String`,
71///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
72///   nullable case).
73/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
74///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
75///   validators in declaration order, returning `FormError::Parse`
76///   on the first failure.
77///
78/// Example:
79///
80/// ```ignore
81/// #[derive(Form)]
82/// pub struct CreateItemForm {
83///     #[form(min_length = 1, max_length = 64)]
84///     pub name: String,
85///     #[form(min = 0, max = 150)]
86///     pub age: i32,
87///     pub active: bool,
88///     pub email: Option<String>,
89/// }
90///
91/// let parsed = CreateItemForm::parse(&form_map)?;
92/// ```
93#[proc_macro_derive(Form, attributes(form))]
94pub fn derive_form(input: TokenStream) -> TokenStream {
95    let input = parse_macro_input!(input as DeriveInput);
96    expand_form(&input)
97        .unwrap_or_else(syn::Error::into_compile_error)
98        .into()
99}
100
101/// Derive [`rustango::serializer::ModelSerializer`] for a struct.
102///
103/// # Container attribute (required)
104/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
105///
106/// # Field attributes
107/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
108/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
109/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
110/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
111///
112/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
113#[proc_macro_derive(Serializer, attributes(serializer))]
114pub fn derive_serializer(input: TokenStream) -> TokenStream {
115    let input = parse_macro_input!(input as DeriveInput);
116    expand_serializer(&input)
117        .unwrap_or_else(syn::Error::into_compile_error)
118        .into()
119}
120
121/// Bake every `*.json` migration file in a directory into the binary
122/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
123/// of `(name, json_content)` pairs, lex-sorted by file stem.
124///
125/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
126/// behaviour as `migrate(pool, dir)` but with no filesystem access.
127/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
128/// (i.e. the crate that invokes the macro). Default is
129/// `"./migrations"` if no argument is supplied.
130///
131/// ```ignore
132/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
133/// // or:
134/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
135///
136/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
137/// ```
138///
139/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
140/// file's `name` field must equal its file stem, every `prev`
141/// reference must point to another migration in the same directory,
142/// and the JSON must parse. A broken chain — orphan `prev`, missing
143/// predecessor, malformed file — fails at macro-expansion time with
144/// a clear `compile_error!`. *No other Django-shape Rust framework
145/// validates migration chains at compile time*: Cot's migrations are
146/// imperative Rust code (no static chain), Loco's are SeaORM
147/// up/down (same), Rwf's are raw SQL (no chain at all).
148///
149/// Each migration is included via `include_str!` so cargo's rebuild
150/// detection picks up file *content* changes. **Caveat:** cargo
151/// doesn't watch directory listings, so adding or removing a
152/// migration file inside the dir won't auto-trigger a rebuild — run
153/// `cargo clean` (or just bump any other source file) when you add
154/// new migrations during embedded development.
155#[proc_macro]
156pub fn embed_migrations(input: TokenStream) -> TokenStream {
157    expand_embed_migrations(input.into())
158        .unwrap_or_else(syn::Error::into_compile_error)
159        .into()
160}
161
162/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
163/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
164/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
165/// functions are zero-boilerplate:
166///
167/// ```ignore
168/// #[rustango::main]
169/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
170///     rustango::server::Builder::from_env().await?
171///         .migrate("migrations").await?
172///         .api(my_app::urls::api())
173///         .seed_with(my_app::seed::run).await?
174///         .serve("0.0.0.0:8080").await
175/// }
176/// ```
177///
178/// Optional `flavor = "current_thread"` passes through to
179/// `#[tokio::main]`; default is the multi-threaded runtime.
180///
181/// Pulls `tracing-subscriber` into the rustango crate behind the
182/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
183/// out get plain `#[tokio::main]` ergonomics without the dependency.
184#[proc_macro_attribute]
185pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
186    expand_main(args.into(), item.into())
187        .unwrap_or_else(syn::Error::into_compile_error)
188        .into()
189}
190
191fn expand_main(
192    args: TokenStream2,
193    item: TokenStream2,
194) -> syn::Result<TokenStream2> {
195    let mut input: syn::ItemFn = syn::parse2(item)?;
196    if input.sig.asyncness.is_none() {
197        return Err(syn::Error::new(
198            input.sig.ident.span(),
199            "`#[rustango::main]` must wrap an `async fn`",
200        ));
201    }
202
203    // Parse optional `flavor = "..."` etc. from the attribute args
204    // and pass them straight through to `#[tokio::main(...)]`.
205    let tokio_attr = if args.is_empty() {
206        quote! { #[::tokio::main] }
207    } else {
208        quote! { #[::tokio::main(#args)] }
209    };
210
211    // Re-block the body so the tracing init runs before user code.
212    let body = input.block.clone();
213    input.block = syn::parse2(quote! {{
214        {
215            use ::rustango::__private_runtime::tracing_subscriber::{self, EnvFilter};
216            // `try_init` so duplicate installers (e.g. tests already
217            // holding a subscriber) don't panic.
218            let _ = tracing_subscriber::fmt()
219                .with_env_filter(
220                    EnvFilter::try_from_default_env()
221                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
222                )
223                .try_init();
224        }
225        #body
226    }})?;
227
228    Ok(quote! {
229        #tokio_attr
230        #input
231    })
232}
233
234fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
235    // Default to "./migrations" if invoked without args.
236    let path_str = if input.is_empty() {
237        "./migrations".to_string()
238    } else {
239        let lit: LitStr = syn::parse2(input)?;
240        lit.value()
241    };
242
243    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
244        syn::Error::new(
245            proc_macro2::Span::call_site(),
246            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
247        )
248    })?;
249    let abs = std::path::Path::new(&manifest).join(&path_str);
250
251    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
252    if abs.is_dir() {
253        let read = std::fs::read_dir(&abs).map_err(|e| {
254            syn::Error::new(
255                proc_macro2::Span::call_site(),
256                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
257            )
258        })?;
259        for entry in read.flatten() {
260            let path = entry.path();
261            if !path.is_file() {
262                continue;
263            }
264            if path.extension().and_then(|s| s.to_str()) != Some("json") {
265                continue;
266            }
267            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
268                continue;
269            };
270            entries.push((stem.to_owned(), path));
271        }
272    }
273    entries.sort_by(|a, b| a.0.cmp(&b.0));
274
275    // Compile-time chain validation: read each migration's JSON,
276    // pull `name` and `prev` (file-stem-keyed for the chain check),
277    // and verify every `prev` points to another migration in the
278    // slice. Mismatches between the file stem and the embedded
279    // `name` field — and broken `prev` chains — fail at MACRO
280    // EXPANSION time so a misshapen migration set never compiles.
281    //
282    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
283    // migrations + a Rust proc-macro that reads them is the unique
284    // combo nothing else in the Django-shape Rust camp can match
285    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
286    // Rwf's are raw SQL — none have a static chain to validate).
287    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
288    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
289    for (stem, path) in &entries {
290        let raw = std::fs::read_to_string(path).map_err(|e| {
291            syn::Error::new(
292                proc_macro2::Span::call_site(),
293                format!(
294                    "embed_migrations!: cannot read {} for chain validation: {e}",
295                    path.display()
296                ),
297            )
298        })?;
299        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
300            syn::Error::new(
301                proc_macro2::Span::call_site(),
302                format!(
303                    "embed_migrations!: {} is not valid JSON: {e}",
304                    path.display()
305                ),
306            )
307        })?;
308        let name = json
309            .get("name")
310            .and_then(|v| v.as_str())
311            .ok_or_else(|| {
312                syn::Error::new(
313                    proc_macro2::Span::call_site(),
314                    format!(
315                        "embed_migrations!: {} is missing the `name` field",
316                        path.display()
317                    ),
318                )
319            })?
320            .to_owned();
321        if name != *stem {
322            return Err(syn::Error::new(
323                proc_macro2::Span::call_site(),
324                format!(
325                    "embed_migrations!: file stem `{stem}` does not match the migration's \
326                     `name` field `{name}` — rename the file or fix the JSON",
327                ),
328            ));
329        }
330        let prev = json
331            .get("prev")
332            .and_then(|v| v.as_str())
333            .map(str::to_owned);
334        chain_names.push(name.clone());
335        prev_refs.push((name, prev));
336    }
337
338    let name_set: std::collections::HashSet<&str> =
339        chain_names.iter().map(String::as_str).collect();
340    for (name, prev) in &prev_refs {
341        if let Some(p) = prev {
342            if !name_set.contains(p.as_str()) {
343                return Err(syn::Error::new(
344                    proc_macro2::Span::call_site(),
345                    format!(
346                        "embed_migrations!: broken migration chain — `{name}` declares \
347                         prev=`{p}` but no migration with that name exists in {}",
348                        abs.display()
349                    ),
350                ));
351            }
352        }
353    }
354
355    let pairs: Vec<TokenStream2> = entries
356        .iter()
357        .map(|(name, path)| {
358            let path_lit = path.display().to_string();
359            quote! { (#name, ::core::include_str!(#path_lit)) }
360        })
361        .collect();
362
363    Ok(quote! {
364        {
365            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
366            __RUSTANGO_EMBEDDED
367        }
368    })
369}
370
371fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
372    let struct_name = &input.ident;
373
374    let Data::Struct(data) = &input.data else {
375        return Err(syn::Error::new_spanned(
376            struct_name,
377            "Model can only be derived on structs",
378        ));
379    };
380    let Fields::Named(named) = &data.fields else {
381        return Err(syn::Error::new_spanned(
382            struct_name,
383            "Model requires a struct with named fields",
384        ));
385    };
386
387    let container = parse_container_attrs(input)?;
388    let table = container
389        .table
390        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
391    let model_name = struct_name.to_string();
392
393    let collected = collect_fields(named, &table)?;
394
395    // Validate that #[rustango(display = "…")] names a real field.
396    if let Some((ref display, span)) = container.display {
397        if !collected.field_names.iter().any(|n| n == display) {
398            return Err(syn::Error::new(
399                span,
400                format!("`display = \"{display}\"` does not match any field on this struct"),
401            ));
402        }
403    }
404    let display = container.display.map(|(name, _)| name);
405    let app_label = container.app.clone();
406
407    // Validate admin field-name lists against declared field names.
408    if let Some(admin) = &container.admin {
409        for (label, list) in [
410            ("list_display", &admin.list_display),
411            ("search_fields", &admin.search_fields),
412            ("readonly_fields", &admin.readonly_fields),
413            ("list_filter", &admin.list_filter),
414        ] {
415            if let Some((names, span)) = list {
416                for name in names {
417                    if !collected.field_names.iter().any(|n| n == name) {
418                        return Err(syn::Error::new(
419                            *span,
420                            format!(
421                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
422                            ),
423                        ));
424                    }
425                }
426            }
427        }
428        if let Some((pairs, span)) = &admin.ordering {
429            for (name, _) in pairs {
430                if !collected.field_names.iter().any(|n| n == name) {
431                    return Err(syn::Error::new(
432                        *span,
433                        format!(
434                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
435                        ),
436                    ));
437                }
438            }
439        }
440        if let Some((groups, span)) = &admin.fieldsets {
441            for (_, fields) in groups {
442                for name in fields {
443                    if !collected.field_names.iter().any(|n| n == name) {
444                        return Err(syn::Error::new(
445                            *span,
446                            format!(
447                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
448                            ),
449                        ));
450                    }
451                }
452            }
453        }
454    }
455    if let Some(audit) = &container.audit {
456        if let Some((names, span)) = &audit.track {
457            for name in names {
458                if !collected.field_names.iter().any(|n| n == name) {
459                    return Err(syn::Error::new(
460                        *span,
461                        format!(
462                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
463                        ),
464                    ));
465                }
466            }
467        }
468    }
469
470    // Build the audit_track list for ModelSchema: None when no audit attr,
471    // Some(empty) when audit present without track, Some(names) when explicit.
472    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
473        audit
474            .track
475            .as_ref()
476            .map(|(names, _)| names.clone())
477            .unwrap_or_default()
478    });
479
480    // Merge field-level indexes into the container's index list.
481    let mut all_indexes: Vec<IndexAttr> = container.indexes;
482    for field in &named.named {
483        let ident = field.ident.as_ref().expect("named");
484        let col = to_snake_case(&ident.to_string()); // column name fallback
485        // Re-parse field attrs to check for index flag
486        if let Ok(fa) = parse_field_attrs(field) {
487            if fa.index {
488                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
489                let auto_name = if fa.index_unique {
490                    format!("{table}_{col_name}_uq_idx")
491                } else {
492                    format!("{table}_{col_name}_idx")
493                };
494                all_indexes.push(IndexAttr {
495                    name: fa.index_name.or(Some(auto_name)),
496                    columns: vec![col_name],
497                    unique: fa.index_unique,
498                });
499            }
500        }
501    }
502
503    let model_impl = model_impl_tokens(
504        struct_name,
505        &model_name,
506        &table,
507        display.as_deref(),
508        app_label.as_deref(),
509        container.admin.as_ref(),
510        &collected.field_schemas,
511        collected.soft_delete_column.as_deref(),
512        container.permissions,
513        audit_track_names.as_deref(),
514        &container.m2m,
515        &all_indexes,
516        &container.checks,
517        &container.composite_fks,
518        &container.generic_fks,
519    );
520    let module_ident = column_module_ident(struct_name);
521    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
522    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
523        let track_set: Option<std::collections::HashSet<&str>> = audit
524            .track
525            .as_ref()
526            .map(|(names, _)| names.iter().map(String::as_str).collect());
527        collected
528            .column_entries
529            .iter()
530            .filter(|c| {
531                track_set
532                    .as_ref()
533                    .map_or(true, |s| s.contains(c.name.as_str()))
534            })
535            .collect()
536    });
537    let inherent_impl = inherent_impl_tokens(
538        struct_name,
539        &collected,
540        collected.primary_key.as_ref(),
541        &column_consts,
542        audited_fields.as_deref(),
543    );
544    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
545    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
546    let reverse_helpers = reverse_helper_tokens(struct_name, &collected.fk_relations);
547    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
548
549    Ok(quote! {
550        #model_impl
551        #inherent_impl
552        #from_row_impl
553        #column_module
554        #reverse_helpers
555        #m2m_accessors
556
557        ::rustango::core::inventory::submit! {
558            ::rustango::core::ModelEntry {
559                schema: <#struct_name as ::rustango::core::Model>::SCHEMA,
560                // `module_path!()` evaluates at the registration site,
561                // so a Model declared in `crate::blog::models` records
562                // `"<crate>::blog::models"` and `resolved_app_label()`
563                // can infer "blog" without an explicit attribute.
564                module_path: ::core::module_path!(),
565            }
566        }
567    })
568}
569
570/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
571/// matches `field_name` against the model's FK fields and, for a
572/// match, decodes the FK target via the parent's macro-generated
573/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
574/// `ForeignKey::Loaded` on `self`.
575///
576/// Always emitted (with empty arms for FK-less models, which
577/// return `Ok(false)` for any field name) so the `T: LoadRelated`
578/// trait bound on `fetch_on` is universally satisfied — users
579/// never have to think about implementing it.
580fn load_related_impl_tokens(
581    struct_name: &syn::Ident,
582    fk_relations: &[FkRelation],
583) -> TokenStream2 {
584    let arms = fk_relations.iter().map(|rel| {
585        let parent_ty = &rel.parent_type;
586        let fk_col = rel.fk_column.as_str();
587        // FK field's Rust ident matches its SQL column name in v0.8
588        // (no `column = "..."` rename ships on FK fields).
589        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
590        quote! {
591            #fk_col => {
592                let _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
593                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
594                    ::rustango::core::SqlValue::I64(v) => v,
595                    _ => 0i64,
596                };
597                self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
598                ::core::result::Result::Ok(true)
599            }
600        }
601    });
602    quote! {
603        impl ::rustango::sql::LoadRelated for #struct_name {
604            #[allow(unused_variables)]
605            fn __rustango_load_related(
606                &mut self,
607                row: &::rustango::sql::sqlx::postgres::PgRow,
608                field_name: &str,
609                alias: &str,
610            ) -> ::core::result::Result<bool, ::rustango::sql::sqlx::Error> {
611                match field_name {
612                    #( #arms )*
613                    _ => ::core::result::Result::Ok(false),
614                }
615            }
616        }
617    }
618}
619
620/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
621/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
622/// which expands to a `LoadRelatedMy` impl when rustango is built with
623/// the `mysql` feature, and to nothing otherwise. The decoded parent
624/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
625/// decoder, also batch8) so the dual emission is symmetric across
626/// backends.
627fn load_related_impl_my_tokens(
628    struct_name: &syn::Ident,
629    fk_relations: &[FkRelation],
630) -> TokenStream2 {
631    let arms = fk_relations.iter().map(|rel| {
632        let parent_ty = &rel.parent_type;
633        let fk_col = rel.fk_column.as_str();
634        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
635        // `self` IS hygiene-tracked through macro_rules — emitted from
636        // a different context than the `&mut self` parameter inside
637        // the macro_rules-expanded fn. Pass it through as `__self`
638        // and let the macro_rules rebind it to the receiver.
639        quote! {
640            #fk_col => {
641                let _parent: #parent_ty =
642                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
643                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
644                    ::rustango::core::SqlValue::I64(v) => v,
645                    _ => 0i64,
646                };
647                __self.#field_ident = ::rustango::sql::ForeignKey::loaded(_pk, _parent);
648                ::core::result::Result::Ok(true)
649            }
650        }
651    });
652    quote! {
653        ::rustango::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias| {
654            #( #arms )*
655        });
656    }
657}
658
659/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
660/// matches `field_name` against the model's FK fields and returns
661/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
662/// group children by parent PK.
663///
664/// Always emitted (with `_ => None` for FK-less models) so the
665/// trait bound on `fetch_with_prefetch` is universally satisfied.
666fn fk_pk_access_impl_tokens(
667    struct_name: &syn::Ident,
668    fk_relations: &[FkRelation],
669) -> TokenStream2 {
670    let arms = fk_relations.iter().map(|rel| {
671        let fk_col = rel.fk_column.as_str();
672        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
673        quote! {
674            #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
675        }
676    });
677    quote! {
678        impl ::rustango::sql::FkPkAccess for #struct_name {
679            #[allow(unused_variables)]
680            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
681                match field_name {
682                    #( #arms )*
683                    _ => ::core::option::Option::None,
684                }
685            }
686        }
687    }
688}
689
690/// For every `ForeignKey<Parent>` field on `Child`, emit
691/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
692/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
693/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
694/// — the canonical reverse-FK fetch. One round trip, no N+1.
695fn reverse_helper_tokens(
696    child_ident: &syn::Ident,
697    fk_relations: &[FkRelation],
698) -> TokenStream2 {
699    if fk_relations.is_empty() {
700        return TokenStream2::new();
701    }
702    // Snake-case the child struct name to derive the method suffix —
703    // `Post` → `post_set`, `BlogComment` → `blog_comment_set`. Avoids
704    // English-plural edge cases (Django's `<child>_set` convention).
705    let suffix = format!("{}_set", to_snake_case(&child_ident.to_string()));
706    let method_ident = syn::Ident::new(&suffix, child_ident.span());
707    let impls = fk_relations.iter().map(|rel| {
708        let parent_ty = &rel.parent_type;
709        let fk_col = rel.fk_column.as_str();
710        let doc = format!(
711            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
712             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
713             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
714             further `{child_ident}::objects()` filters via direct queryset use."
715        );
716        quote! {
717            impl #parent_ty {
718                #[doc = #doc]
719                ///
720                /// # Errors
721                /// Returns [`::rustango::sql::ExecError`] for SQL-writing
722                /// or driver failures.
723                pub async fn #method_ident<'_c, _E>(
724                    &self,
725                    _executor: _E,
726                ) -> ::core::result::Result<
727                    ::std::vec::Vec<#child_ident>,
728                    ::rustango::sql::ExecError,
729                >
730                where
731                    _E: ::rustango::sql::sqlx::Executor<
732                        '_c,
733                        Database = ::rustango::sql::sqlx::Postgres,
734                    >,
735                {
736                    let _pk: ::rustango::core::SqlValue = self.__rustango_pk_value();
737                    ::rustango::query::QuerySet::<#child_ident>::new()
738                        .filter(#fk_col, ::rustango::core::Op::Eq, _pk)
739                        .fetch_on(_executor)
740                        .await
741                }
742            }
743        }
744    });
745    quote! { #( #impls )* }
746}
747
748/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
749/// relation declared on the model.
750fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
751    if m2m_relations.is_empty() {
752        return TokenStream2::new();
753    }
754    let methods = m2m_relations.iter().map(|rel| {
755        let method_name = format!("{}_m2m", rel.name);
756        let method_ident = syn::Ident::new(&method_name, struct_name.span());
757        let through = rel.through.as_str();
758        let src_col = rel.src.as_str();
759        let dst_col = rel.dst.as_str();
760        quote! {
761            pub fn #method_ident(&self) -> ::rustango::sql::M2MManager {
762                ::rustango::sql::M2MManager {
763                    src_pk: self.__rustango_pk_value(),
764                    through: #through,
765                    src_col: #src_col,
766                    dst_col: #dst_col,
767                }
768            }
769        }
770    });
771    quote! {
772        impl #struct_name {
773            #( #methods )*
774        }
775    }
776}
777
778struct ColumnEntry {
779    /// The struct field ident, used both for the inherent const name on
780    /// the model and for the inner column type's name.
781    ident: syn::Ident,
782    /// The struct's field type, used as `Column::Value`.
783    value_ty: Type,
784    /// Rust-side field name (e.g. `"id"`).
785    name: String,
786    /// SQL-side column name (e.g. `"user_id"`).
787    column: String,
788    /// `::rustango::core::FieldType::I64` etc.
789    field_type_tokens: TokenStream2,
790}
791
792struct CollectedFields {
793    field_schemas: Vec<TokenStream2>,
794    from_row_inits: Vec<TokenStream2>,
795    /// Aliased counterparts of `from_row_inits` — read columns via
796    /// `format!("{prefix}__{col}")` aliases so a Model can be
797    /// decoded from a JOINed row's projected target columns.
798    from_aliased_row_inits: Vec<TokenStream2>,
799    /// Static column-name list — used by the simple insert path
800    /// (no `Auto<T>` fields). Aligned with `insert_values`.
801    insert_columns: Vec<TokenStream2>,
802    /// Static `Into<SqlValue>` expressions, one per field. Aligned
803    /// with `insert_columns`. Used by the simple insert path only.
804    insert_values: Vec<TokenStream2>,
805    /// Per-field push expressions for the dynamic (Auto-aware)
806    /// insert path. Each statement either unconditionally pushes
807    /// `(column, value)` or, for an `Auto<T>` field, conditionally
808    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
809    insert_pushes: Vec<TokenStream2>,
810    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
811    /// when `has_auto == false`.
812    returning_cols: Vec<TokenStream2>,
813    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
814    /// field. Run after `insert_returning` to populate the model.
815    auto_assigns: Vec<TokenStream2>,
816    /// `(ident, column_literal)` pairs for every Auto field. Used by
817    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
818    /// instead of `self`.
819    auto_field_idents: Vec<(syn::Ident, String)>,
820    /// Inner `T` of the first `Auto<T>` field, for the MySQL
821    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
822    first_auto_value_ty: Option<Type>,
823    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
824    /// by the all-Auto-Unset bulk path (Auto cols dropped from
825    /// `columns`).
826    bulk_pushes_no_auto: Vec<TokenStream2>,
827    /// Bulk-insert per-row pushes for **all fields including Auto**.
828    /// Used by the all-Auto-Set bulk path (Auto col included with the
829    /// caller-supplied value).
830    bulk_pushes_all: Vec<TokenStream2>,
831    /// Column-name literals for non-Auto fields only (paired with
832    /// `bulk_pushes_no_auto`).
833    bulk_columns_no_auto: Vec<TokenStream2>,
834    /// Column-name literals for every field including Auto (paired
835    /// with `bulk_pushes_all`).
836    bulk_columns_all: Vec<TokenStream2>,
837    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
838    /// + the loop that asserts every row matches. One pair per Auto
839    /// field. Empty when `has_auto == false`.
840    bulk_auto_uniformity: Vec<TokenStream2>,
841    /// Identifier of the first Auto field, used as the witness for
842    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
843    first_auto_ident: Option<syn::Ident>,
844    /// `true` if any field on the struct is `Auto<T>`.
845    has_auto: bool,
846    /// `true` when the primary-key field's Rust type is `Auto<T>`.
847    /// Gates `save()` codegen — only Auto PKs let us infer
848    /// insert-vs-update from the in-memory value.
849    pk_is_auto: bool,
850    /// `Assignment` constructors for every non-PK column. Drives the
851    /// UPDATE branch of `save()`.
852    update_assignments: Vec<TokenStream2>,
853    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
854    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
855    upsert_update_columns: Vec<TokenStream2>,
856    primary_key: Option<(syn::Ident, String)>,
857    column_entries: Vec<ColumnEntry>,
858    /// Rust-side field names, in declaration order. Used to validate
859    /// container attributes like `display = "…"`.
860    field_names: Vec<String>,
861    /// FK fields on this child model. Drives the reverse-relation
862    /// helper emit — for each FK, the macro adds an inherent
863    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
864    /// method on the parent type.
865    fk_relations: Vec<FkRelation>,
866    /// SQL column name of the `#[rustango(soft_delete)]` field, if
867    /// the model has one. Drives emission of the `soft_delete_on` /
868    /// `restore_on` inherent methods. At most one such column per
869    /// model is allowed; collect_fields rejects duplicates.
870    soft_delete_column: Option<String>,
871}
872
873#[derive(Clone)]
874struct FkRelation {
875    /// Inner type of `ForeignKey<T>` — the parent model. The reverse
876    /// helper is emitted as `impl <ParentType> { … }`.
877    parent_type: Type,
878    /// SQL column name on the child table for this FK (e.g. `"author"`).
879    /// Used in the generated `WHERE <fk_column> = $1` clause.
880    fk_column: String,
881}
882
883fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
884    let cap = named.named.len();
885    let mut out = CollectedFields {
886        field_schemas: Vec::with_capacity(cap),
887        from_row_inits: Vec::with_capacity(cap),
888        from_aliased_row_inits: Vec::with_capacity(cap),
889        insert_columns: Vec::with_capacity(cap),
890        insert_values: Vec::with_capacity(cap),
891        insert_pushes: Vec::with_capacity(cap),
892        returning_cols: Vec::new(),
893        auto_assigns: Vec::new(),
894        auto_field_idents: Vec::new(),
895        first_auto_value_ty: None,
896        bulk_pushes_no_auto: Vec::with_capacity(cap),
897        bulk_pushes_all: Vec::with_capacity(cap),
898        bulk_columns_no_auto: Vec::with_capacity(cap),
899        bulk_columns_all: Vec::with_capacity(cap),
900        bulk_auto_uniformity: Vec::new(),
901        first_auto_ident: None,
902        has_auto: false,
903        pk_is_auto: false,
904        update_assignments: Vec::with_capacity(cap),
905        upsert_update_columns: Vec::with_capacity(cap),
906        primary_key: None,
907        column_entries: Vec::with_capacity(cap),
908        field_names: Vec::with_capacity(cap),
909        fk_relations: Vec::new(),
910        soft_delete_column: None,
911    };
912
913    for field in &named.named {
914        let info = process_field(field, table)?;
915        out.field_names.push(info.ident.to_string());
916        out.field_schemas.push(info.schema);
917        out.from_row_inits.push(info.from_row_init);
918        out.from_aliased_row_inits.push(info.from_aliased_row_init);
919        if let Some(parent_ty) = info.fk_inner.clone() {
920            out.fk_relations.push(FkRelation {
921                parent_type: parent_ty,
922                fk_column: info.column.clone(),
923            });
924        }
925        if info.soft_delete {
926            if out.soft_delete_column.is_some() {
927                return Err(syn::Error::new_spanned(
928                    field,
929                    "only one field may be marked `#[rustango(soft_delete)]`",
930                ));
931            }
932            out.soft_delete_column = Some(info.column.clone());
933        }
934        let column = info.column.as_str();
935        let ident = info.ident;
936        out.insert_columns.push(quote!(#column));
937        out.insert_values.push(quote! {
938            ::core::convert::Into::<::rustango::core::SqlValue>::into(
939                ::core::clone::Clone::clone(&self.#ident)
940            )
941        });
942        if info.auto {
943            out.has_auto = true;
944            if out.first_auto_ident.is_none() {
945                out.first_auto_ident = Some(ident.clone());
946                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
947            }
948            out.returning_cols.push(quote!(#column));
949            out.auto_field_idents
950                .push((ident.clone(), info.column.clone()));
951            out.auto_assigns.push(quote! {
952                self.#ident = ::rustango::sql::try_get_returning(_returning_row, #column)?;
953            });
954            out.insert_pushes.push(quote! {
955                if let ::rustango::sql::Auto::Set(_v) = &self.#ident {
956                    _columns.push(#column);
957                    _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
958                        ::core::clone::Clone::clone(_v)
959                    ));
960                }
961            });
962            // Bulk: Auto fields appear only in the all-Set path,
963            // never in the Unset path (we drop them from `columns`).
964            out.bulk_columns_all.push(quote!(#column));
965            out.bulk_pushes_all.push(quote! {
966                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
967                    ::core::clone::Clone::clone(&_row.#ident)
968                ));
969            });
970            // Uniformity check: every row's Auto state must match the
971            // first row's. Mixed Set/Unset within one bulk_insert is
972            // rejected here so the column list stays consistent.
973            let ident_clone = ident.clone();
974            out.bulk_auto_uniformity.push(quote! {
975                for _r in rows.iter().skip(1) {
976                    if matches!(_r.#ident_clone, ::rustango::sql::Auto::Unset) != _first_unset {
977                        return ::core::result::Result::Err(
978                            ::rustango::sql::ExecError::Sql(
979                                ::rustango::sql::SqlError::BulkAutoMixed
980                            )
981                        );
982                    }
983                }
984            });
985        } else {
986            out.insert_pushes.push(quote! {
987                _columns.push(#column);
988                _values.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
989                    ::core::clone::Clone::clone(&self.#ident)
990                ));
991            });
992            // Bulk: non-Auto fields appear in BOTH paths.
993            out.bulk_columns_no_auto.push(quote!(#column));
994            out.bulk_columns_all.push(quote!(#column));
995            let push_expr = quote! {
996                _row_vals.push(::core::convert::Into::<::rustango::core::SqlValue>::into(
997                    ::core::clone::Clone::clone(&_row.#ident)
998                ));
999            };
1000            out.bulk_pushes_no_auto.push(push_expr.clone());
1001            out.bulk_pushes_all.push(push_expr);
1002        }
1003        if info.primary_key {
1004            if out.primary_key.is_some() {
1005                return Err(syn::Error::new_spanned(
1006                    field,
1007                    "only one field may be marked `#[rustango(primary_key)]`",
1008                ));
1009            }
1010            out.primary_key = Some((ident.clone(), info.column.clone()));
1011            if info.auto {
1012                out.pk_is_auto = true;
1013            }
1014        } else if info.auto_now_add {
1015            // Immutable post-insert: skip from UPDATE entirely.
1016        } else if info.auto_now {
1017            // `auto_now` columns: bind `chrono::Utc::now()` on every
1018            // UPDATE so the column is always overridden with the
1019            // wall-clock at write time, regardless of what value the
1020            // user left in the struct field.
1021            out.update_assignments.push(quote! {
1022                ::rustango::core::Assignment {
1023                    column: #column,
1024                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1025                        ::chrono::Utc::now()
1026                    ),
1027                }
1028            });
1029            out.upsert_update_columns.push(quote!(#column));
1030        } else {
1031            out.update_assignments.push(quote! {
1032                ::rustango::core::Assignment {
1033                    column: #column,
1034                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1035                        ::core::clone::Clone::clone(&self.#ident)
1036                    ),
1037                }
1038            });
1039            out.upsert_update_columns.push(quote!(#column));
1040        }
1041        out.column_entries.push(ColumnEntry {
1042            ident: ident.clone(),
1043            value_ty: info.value_ty.clone(),
1044            name: ident.to_string(),
1045            column: info.column.clone(),
1046            field_type_tokens: info.field_type_tokens,
1047        });
1048    }
1049    Ok(out)
1050}
1051
1052fn model_impl_tokens(
1053    struct_name: &syn::Ident,
1054    model_name: &str,
1055    table: &str,
1056    display: Option<&str>,
1057    app_label: Option<&str>,
1058    admin: Option<&AdminAttrs>,
1059    field_schemas: &[TokenStream2],
1060    soft_delete_column: Option<&str>,
1061    permissions: bool,
1062    audit_track: Option<&[String]>,
1063    m2m_relations: &[M2MAttr],
1064    indexes: &[IndexAttr],
1065    checks: &[CheckAttr],
1066    composite_fks: &[CompositeFkAttr],
1067    generic_fks: &[GenericFkAttr],
1068) -> TokenStream2 {
1069    let display_tokens = if let Some(name) = display {
1070        quote!(::core::option::Option::Some(#name))
1071    } else {
1072        quote!(::core::option::Option::None)
1073    };
1074    let app_label_tokens = if let Some(name) = app_label {
1075        quote!(::core::option::Option::Some(#name))
1076    } else {
1077        quote!(::core::option::Option::None)
1078    };
1079    let soft_delete_tokens = if let Some(col) = soft_delete_column {
1080        quote!(::core::option::Option::Some(#col))
1081    } else {
1082        quote!(::core::option::Option::None)
1083    };
1084    let audit_track_tokens = match audit_track {
1085        None => quote!(::core::option::Option::None),
1086        Some(names) => {
1087            let lits = names.iter().map(|n| n.as_str());
1088            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
1089        }
1090    };
1091    let admin_tokens = admin_config_tokens(admin);
1092    let indexes_tokens = indexes.iter().map(|idx| {
1093        let name = idx.name.as_deref().unwrap_or("unnamed_index");
1094        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
1095        let unique = idx.unique;
1096        quote! {
1097            ::rustango::core::IndexSchema {
1098                name: #name,
1099                columns: &[ #(#cols),* ],
1100                unique: #unique,
1101            }
1102        }
1103    });
1104    let checks_tokens = checks.iter().map(|c| {
1105        let name = c.name.as_str();
1106        let expr = c.expr.as_str();
1107        quote! {
1108            ::rustango::core::CheckConstraint {
1109                name: #name,
1110                expr: #expr,
1111            }
1112        }
1113    });
1114    let composite_fk_tokens = composite_fks.iter().map(|rel| {
1115        let name = rel.name.as_str();
1116        let to = rel.to.as_str();
1117        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
1118        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
1119        quote! {
1120            ::rustango::core::CompositeFkRelation {
1121                name: #name,
1122                to: #to,
1123                from: &[ #(#from_cols),* ],
1124                on: &[ #(#on_cols),* ],
1125            }
1126        }
1127    });
1128    let generic_fk_tokens = generic_fks.iter().map(|rel| {
1129        let name = rel.name.as_str();
1130        let ct_col = rel.ct_column.as_str();
1131        let pk_col = rel.pk_column.as_str();
1132        quote! {
1133            ::rustango::core::GenericRelation {
1134                name: #name,
1135                ct_column: #ct_col,
1136                pk_column: #pk_col,
1137            }
1138        }
1139    });
1140    let m2m_tokens = m2m_relations.iter().map(|rel| {
1141        let name = rel.name.as_str();
1142        let to = rel.to.as_str();
1143        let through = rel.through.as_str();
1144        let src = rel.src.as_str();
1145        let dst = rel.dst.as_str();
1146        quote! {
1147            ::rustango::core::M2MRelation {
1148                name: #name,
1149                to: #to,
1150                through: #through,
1151                src_col: #src,
1152                dst_col: #dst,
1153            }
1154        }
1155    });
1156    quote! {
1157        impl ::rustango::core::Model for #struct_name {
1158            const SCHEMA: &'static ::rustango::core::ModelSchema = &::rustango::core::ModelSchema {
1159                name: #model_name,
1160                table: #table,
1161                fields: &[ #(#field_schemas),* ],
1162                display: #display_tokens,
1163                app_label: #app_label_tokens,
1164                admin: #admin_tokens,
1165                soft_delete_column: #soft_delete_tokens,
1166                permissions: #permissions,
1167                audit_track: #audit_track_tokens,
1168                m2m: &[ #(#m2m_tokens),* ],
1169                indexes: &[ #(#indexes_tokens),* ],
1170                check_constraints: &[ #(#checks_tokens),* ],
1171                composite_relations: &[ #(#composite_fk_tokens),* ],
1172                generic_relations: &[ #(#generic_fk_tokens),* ],
1173            };
1174        }
1175    }
1176}
1177
1178/// Emit the `admin: Option<&'static AdminConfig>` field for the model
1179/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
1180/// otherwise a static reference to a populated `AdminConfig`.
1181fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
1182    let Some(admin) = admin else {
1183        return quote!(::core::option::Option::None);
1184    };
1185
1186    let list_display = admin
1187        .list_display
1188        .as_ref()
1189        .map(|(v, _)| v.as_slice())
1190        .unwrap_or(&[]);
1191    let list_display_lits = list_display.iter().map(|s| s.as_str());
1192
1193    let search_fields = admin
1194        .search_fields
1195        .as_ref()
1196        .map(|(v, _)| v.as_slice())
1197        .unwrap_or(&[]);
1198    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
1199
1200    let readonly_fields = admin
1201        .readonly_fields
1202        .as_ref()
1203        .map(|(v, _)| v.as_slice())
1204        .unwrap_or(&[]);
1205    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
1206
1207    let list_filter = admin
1208        .list_filter
1209        .as_ref()
1210        .map(|(v, _)| v.as_slice())
1211        .unwrap_or(&[]);
1212    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
1213
1214    let actions = admin
1215        .actions
1216        .as_ref()
1217        .map(|(v, _)| v.as_slice())
1218        .unwrap_or(&[]);
1219    let actions_lits = actions.iter().map(|s| s.as_str());
1220
1221    let fieldsets = admin
1222        .fieldsets
1223        .as_ref()
1224        .map(|(v, _)| v.as_slice())
1225        .unwrap_or(&[]);
1226    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
1227        let title = title.as_str();
1228        let field_lits = fields.iter().map(|s| s.as_str());
1229        quote!(::rustango::core::Fieldset {
1230            title: #title,
1231            fields: &[ #( #field_lits ),* ],
1232        })
1233    });
1234
1235    let list_per_page = admin.list_per_page.unwrap_or(0);
1236
1237    let ordering_pairs = admin
1238        .ordering
1239        .as_ref()
1240        .map(|(v, _)| v.as_slice())
1241        .unwrap_or(&[]);
1242    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
1243        let name = name.as_str();
1244        let desc = *desc;
1245        quote!((#name, #desc))
1246    });
1247
1248    quote! {
1249        ::core::option::Option::Some(&::rustango::core::AdminConfig {
1250            list_display: &[ #( #list_display_lits ),* ],
1251            search_fields: &[ #( #search_fields_lits ),* ],
1252            list_per_page: #list_per_page,
1253            ordering: &[ #( #ordering_tokens ),* ],
1254            readonly_fields: &[ #( #readonly_fields_lits ),* ],
1255            list_filter: &[ #( #list_filter_lits ),* ],
1256            actions: &[ #( #actions_lits ),* ],
1257            fieldsets: &[ #( #fieldset_tokens ),* ],
1258        })
1259    }
1260}
1261
1262fn inherent_impl_tokens(
1263    struct_name: &syn::Ident,
1264    fields: &CollectedFields,
1265    primary_key: Option<&(syn::Ident, String)>,
1266    column_consts: &TokenStream2,
1267    audited_fields: Option<&[&ColumnEntry]>,
1268) -> TokenStream2 {
1269    // Audit-emit fragments threaded into write paths. Non-empty only
1270    // when the model carries `#[rustango(audit(...))]`. They reborrow
1271    // `_executor` (a `&mut PgConnection` for audited models — the
1272    // macro switches the signature below) so the data write and the
1273    // audit INSERT both run on the same caller-supplied connection.
1274    let executor_passes_to_data_write = if audited_fields.is_some() {
1275        quote!(&mut *_executor)
1276    } else {
1277        quote!(_executor)
1278    };
1279    let executor_param = if audited_fields.is_some() {
1280        quote!(_executor: &mut ::rustango::sql::sqlx::PgConnection)
1281    } else {
1282        quote!(_executor: _E)
1283    };
1284    let executor_generics = if audited_fields.is_some() {
1285        quote!()
1286    } else {
1287        quote!(<'_c, _E>)
1288    };
1289    let executor_where = if audited_fields.is_some() {
1290        quote!()
1291    } else {
1292        quote! {
1293            where
1294                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
1295        }
1296    };
1297    // For audited models the `_on` methods take `&mut PgConnection`, so
1298    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
1299    // must acquire a connection first. Non-audited models keep the
1300    // direct delegation since `&PgPool` IS an Executor.
1301    let pool_to_save_on = if audited_fields.is_some() {
1302        quote! {
1303            let mut _conn = pool.acquire().await?;
1304            self.save_on(&mut *_conn).await
1305        }
1306    } else {
1307        quote!(self.save_on(pool).await)
1308    };
1309    let pool_to_insert_on = if audited_fields.is_some() {
1310        quote! {
1311            let mut _conn = pool.acquire().await?;
1312            self.insert_on(&mut *_conn).await
1313        }
1314    } else {
1315        quote!(self.insert_on(pool).await)
1316    };
1317    let pool_to_delete_on = if audited_fields.is_some() {
1318        quote! {
1319            let mut _conn = pool.acquire().await?;
1320            self.delete_on(&mut *_conn).await
1321        }
1322    } else {
1323        quote!(self.delete_on(pool).await)
1324    };
1325    let pool_to_bulk_insert_on = if audited_fields.is_some() {
1326        quote! {
1327            let mut _conn = pool.acquire().await?;
1328            Self::bulk_insert_on(rows, &mut *_conn).await
1329        }
1330    } else {
1331        quote!(Self::bulk_insert_on(rows, pool).await)
1332    };
1333    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
1334    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
1335    // directly, but `upsert_on` for audited models takes
1336    // `&mut PgConnection` (the audit emit needs a real connection).
1337    // Add the missing acquire shim to keep audited Auto-PK upsert
1338    // compiling.
1339    let pool_to_upsert_on = if audited_fields.is_some() {
1340        quote! {
1341            let mut _conn = pool.acquire().await?;
1342            self.upsert_on(&mut *_conn).await
1343        }
1344    } else {
1345        quote!(self.upsert_on(pool).await)
1346    };
1347
1348    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
1349    // (audit-on-connection over &Pool needs a bi-dialect transaction
1350    // helper, deferred). Two body shapes:
1351    // - has_auto: build InsertQuery skipping Auto::Unset columns,
1352    //   request Auto cols in `returning`, dispatch via
1353    //   `insert_returning_pool`, then on the returned `PgRow` /
1354    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
1355    //   row OR drop the single i64 into the first Auto field on MySQL
1356    //   (multi-Auto models on MySQL error at runtime since
1357    //   `LAST_INSERT_ID()` only reports one)
1358    // - non-Auto: build InsertQuery with explicit columns/values and
1359    //   call `insert_pool` (no returning needed)
1360    // pool_insert_method body for the audited Auto-PK case is moved
1361    // to after audit_pair_tokens / audit_pk_to_string (they live
1362    // ~150 lines below). This block keeps the non-audited and
1363    // non-Auto branches in place — the audited Auto-PK arm is
1364    // computed below and merged via the dispatch helper variable.
1365    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
1366        // Audited models with explicit (non-Auto) PKs go through
1367        // the non-Auto insert path below — the audit emit is one
1368        // round-trip after the INSERT inside the same tx via
1369        // audit::save_one_with_audit_pool? No, INSERT semantics
1370        // differ. For non-Auto PK + audited, route through a
1371        // dedicated insert + audit emit on the same tx, but defer
1372        // the macro emission to the audit-bundle-aware block below
1373        // — this `quote!()` placeholder gets overwritten there.
1374        quote!()
1375    } else if audited_fields.is_some() && fields.has_auto {
1376        // Audited Auto-PK insert_pool — assembled after the audit
1377        // bundles. Placeholder; real emission below.
1378        quote!()
1379    } else if fields.has_auto {
1380        let pushes = &fields.insert_pushes;
1381        let returning_cols = &fields.returning_cols;
1382        quote! {
1383            /// Insert this row against either backend, populating any
1384            /// `Auto<T>` PK from the auto-assigned value.
1385            ///
1386            /// # Errors
1387            /// As [`Self::insert`].
1388            pub async fn insert_pool(
1389                &mut self,
1390                pool: &::rustango::sql::Pool,
1391            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1392                let mut _columns: ::std::vec::Vec<&'static str> =
1393                    ::std::vec::Vec::new();
1394                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1395                    ::std::vec::Vec::new();
1396                #( #pushes )*
1397                let _query = ::rustango::core::InsertQuery {
1398                    model: <Self as ::rustango::core::Model>::SCHEMA,
1399                    columns: _columns,
1400                    values: _values,
1401                    returning: ::std::vec![ #( #returning_cols ),* ],
1402                    on_conflict: ::core::option::Option::None,
1403                };
1404                let _result = ::rustango::sql::insert_returning_pool(
1405                    pool, &_query,
1406                ).await?;
1407                ::rustango::sql::apply_auto_pk_pool(_result, self)
1408            }
1409        }
1410    } else {
1411        let insert_columns = &fields.insert_columns;
1412        let insert_values = &fields.insert_values;
1413        quote! {
1414            /// Insert this row into its table against either backend.
1415            /// Equivalent to [`Self::insert`] but takes
1416            /// [`::rustango::sql::Pool`].
1417            ///
1418            /// # Errors
1419            /// As [`Self::insert`].
1420            pub async fn insert_pool(
1421                &self,
1422                pool: &::rustango::sql::Pool,
1423            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1424                let _query = ::rustango::core::InsertQuery {
1425                    model: <Self as ::rustango::core::Model>::SCHEMA,
1426                    columns: ::std::vec![ #( #insert_columns ),* ],
1427                    values: ::std::vec![ #( #insert_values ),* ],
1428                    returning: ::std::vec::Vec::new(),
1429                    on_conflict: ::core::option::Option::None,
1430                };
1431                ::rustango::sql::insert_pool(pool, &_query).await
1432            }
1433        }
1434    };
1435
1436    // pool_save_method moved to after audit_pair_tokens /
1437    // audit_pk_to_string (they live ~70 lines below) — needed for
1438    // the audited branch which builds an UpdateQuery + PendingEntry
1439    // and dispatches via audit::save_one_with_audit_pool.
1440
1441    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
1442    // are computed (they live ~80 lines below).
1443
1444    // Build the (column, JSON value) pair list used by every
1445    // snapshot-style audit emission. Reused across delete_on,
1446    // soft_delete_on, restore_on, and (later) bulk paths. Empty
1447    // when the model isn't audited.
1448    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
1449        .map(|tracked| {
1450            tracked
1451                .iter()
1452                .map(|c| {
1453                    let column_lit = c.column.as_str();
1454                    let ident = &c.ident;
1455                    quote! {
1456                        (
1457                            #column_lit,
1458                            ::serde_json::to_value(&self.#ident)
1459                                .unwrap_or(::serde_json::Value::Null),
1460                        )
1461                    }
1462                })
1463                .collect()
1464        })
1465        .unwrap_or_default();
1466    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
1467        if fields.pk_is_auto {
1468            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
1469        } else {
1470            quote!(::std::format!("{}", &self.#pk_ident))
1471        }
1472    } else {
1473        quote!(::std::string::String::new())
1474    };
1475    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
1476        if audited_fields.is_some() {
1477            let pairs = audit_pair_tokens.iter();
1478            let pk_str = audit_pk_to_string.clone();
1479            quote! {
1480                let _audit_entry = ::rustango::audit::PendingEntry {
1481                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1482                    entity_pk: #pk_str,
1483                    operation: #op_path,
1484                    source: ::rustango::audit::current_source(),
1485                    changes: ::rustango::audit::snapshot_changes(&[
1486                        #( #pairs ),*
1487                    ]),
1488                };
1489                ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
1490            }
1491        } else {
1492            quote!()
1493        }
1494    };
1495    let audit_insert_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Create));
1496    let audit_delete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Delete));
1497    let audit_softdelete_emit = make_op_emit(quote!(::rustango::audit::AuditOp::SoftDelete));
1498    let audit_restore_emit = make_op_emit(quote!(::rustango::audit::AuditOp::Restore));
1499
1500    // `save_pool(&Pool)` — emitted for every model with a PK.
1501    // Audited Auto-PK models are deferred (the Auto::Unset →
1502    // insert_pool path needs the audited-insert flow from a future
1503    // batch). Three body shapes:
1504    // - non-audited, plain PK: build UpdateQuery + dispatch through
1505    //   sql::update_pool
1506    // - non-audited, Auto-PK: same, but Auto::Unset routes to
1507    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
1508    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
1509    //   through audit::save_one_with_audit_pool (per-backend tx wraps
1510    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
1511    //   write field values) — diff-style audit (with pre-UPDATE
1512    //   SELECT for `before` values) needs per-tracked-column codegen
1513    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
1514    //   `save` keeps the diff for now.
1515    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
1516        let pk_column_lit = pk_col.as_str();
1517        let assignments = &fields.update_assignments;
1518        if audited_fields.is_some() {
1519            if fields.pk_is_auto {
1520                // Auto-PK + audited: defer. The Auto::Unset insert
1521                // path needs a transactional INSERT + LAST_INSERT_ID
1522                // + audit emit flow — that's a follow-up batch.
1523                quote!()
1524            } else {
1525                let pairs = audit_pair_tokens.iter();
1526                let pk_str = audit_pk_to_string.clone();
1527                quote! {
1528                    /// Save (UPDATE) this row against either backend
1529                    /// with audit emission inside the same transaction.
1530                    /// Bi-dialect counterpart of [`Self::save`] for
1531                    /// audited models with non-`Auto<T>` PKs.
1532                    ///
1533                    /// Captures **post-write** field state (snapshot
1534                    /// audit). The legacy &PgPool [`Self::save`]
1535                    /// captures BEFORE+AFTER for true diff audit;
1536                    /// porting that to the &Pool path needs runtime
1537                    /// per-tracked-column decoding and is deferred.
1538                    ///
1539                    /// # Errors
1540                    /// As [`Self::save`].
1541                    pub async fn save_pool(
1542                        &mut self,
1543                        pool: &::rustango::sql::Pool,
1544                    ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1545                        let _query = ::rustango::core::UpdateQuery {
1546                            model: <Self as ::rustango::core::Model>::SCHEMA,
1547                            set: ::std::vec![ #( #assignments ),* ],
1548                            where_clause: ::rustango::core::WhereExpr::Predicate(
1549                                ::rustango::core::Filter {
1550                                    column: #pk_column_lit,
1551                                    op: ::rustango::core::Op::Eq,
1552                                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1553                                        ::core::clone::Clone::clone(&self.#pk_ident)
1554                                    ),
1555                                }
1556                            ),
1557                        };
1558                        let _audit_entry = ::rustango::audit::PendingEntry {
1559                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1560                            entity_pk: #pk_str,
1561                            operation: ::rustango::audit::AuditOp::Update,
1562                            source: ::rustango::audit::current_source(),
1563                            changes: ::rustango::audit::snapshot_changes(&[
1564                                #( #pairs ),*
1565                            ]),
1566                        };
1567                        let _ = ::rustango::audit::save_one_with_audit_pool(
1568                            pool, &_query, &_audit_entry,
1569                        ).await?;
1570                        ::core::result::Result::Ok(())
1571                    }
1572                }
1573            }
1574        } else {
1575            let dispatch_unset = if fields.pk_is_auto {
1576                quote! {
1577                    if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1578                        return self.insert_pool(pool).await;
1579                    }
1580                }
1581            } else {
1582                quote!()
1583            };
1584            quote! {
1585                /// Save this row to its table against either backend.
1586                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
1587                /// `UPDATE` keyed on the PK.
1588                ///
1589                /// # Errors
1590                /// As [`Self::save`].
1591                pub async fn save_pool(
1592                    &mut self,
1593                    pool: &::rustango::sql::Pool,
1594                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1595                    #dispatch_unset
1596                    let _query = ::rustango::core::UpdateQuery {
1597                        model: <Self as ::rustango::core::Model>::SCHEMA,
1598                        set: ::std::vec![ #( #assignments ),* ],
1599                        where_clause: ::rustango::core::WhereExpr::Predicate(
1600                            ::rustango::core::Filter {
1601                                column: #pk_column_lit,
1602                                op: ::rustango::core::Op::Eq,
1603                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1604                                    ::core::clone::Clone::clone(&self.#pk_ident)
1605                                ),
1606                            }
1607                        ),
1608                    };
1609                    let _ = ::rustango::sql::update_pool(pool, &_query).await?;
1610                    ::core::result::Result::Ok(())
1611                }
1612            }
1613        }
1614    } else {
1615        quote!()
1616    };
1617
1618    // Audited `insert_pool` (overrides the placeholder set higher up
1619    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
1620    // audited models get insert_pool routing through
1621    // audit::insert_one_with_audit_pool (per-backend tx wraps INSERT
1622    // + auto-PK readback + audit emit). Snapshot-style audit (the
1623    // PendingEntry's `changes` carries post-write field values).
1624    let pool_insert_method = if audited_fields.is_some() {
1625        if let Some(_) = primary_key {
1626            let pushes = if fields.has_auto {
1627                fields.insert_pushes.clone()
1628            } else {
1629                // For non-Auto-PK models, the macro normally builds
1630                // {columns, values} from fields.insert_columns +
1631                // fields.insert_values rather than insert_pushes.
1632                // Map those into the pushes shape.
1633                fields
1634                    .insert_columns
1635                    .iter()
1636                    .zip(&fields.insert_values)
1637                    .map(|(col, val)| {
1638                        quote! {
1639                            _columns.push(#col);
1640                            _values.push(#val);
1641                        }
1642                    })
1643                    .collect()
1644            };
1645            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
1646                fields.returning_cols.clone()
1647            } else {
1648                // Non-Auto-PK: still need RETURNING something for the
1649                // audit helper's contract (it errors on empty
1650                // returning). Return the PK column so the audit row
1651                // can carry the assigned PK back. Some non-Auto PKs
1652                // are server-side-default (e.g. UUIDv4 default), so
1653                // RETURNING is genuinely useful.
1654                primary_key
1655                    .map(|(_, col)| {
1656                        let lit = col.as_str();
1657                        vec![quote!(#lit)]
1658                    })
1659                    .unwrap_or_default()
1660            };
1661            let pairs = audit_pair_tokens.iter();
1662            let pk_str = audit_pk_to_string.clone();
1663            quote! {
1664                /// Insert this row against either backend with audit
1665                /// emission inside the same transaction. Bi-dialect
1666                /// counterpart of [`Self::insert`] for audited models.
1667                ///
1668                /// Snapshot-style audit (post-write field values).
1669                ///
1670                /// # Errors
1671                /// As [`Self::insert`].
1672                pub async fn insert_pool(
1673                    &mut self,
1674                    pool: &::rustango::sql::Pool,
1675                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1676                    let mut _columns: ::std::vec::Vec<&'static str> =
1677                        ::std::vec::Vec::new();
1678                    let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
1679                        ::std::vec::Vec::new();
1680                    #( #pushes )*
1681                    let _query = ::rustango::core::InsertQuery {
1682                        model: <Self as ::rustango::core::Model>::SCHEMA,
1683                        columns: _columns,
1684                        values: _values,
1685                        returning: ::std::vec![ #( #returning_cols ),* ],
1686                        on_conflict: ::core::option::Option::None,
1687                    };
1688                    let _audit_entry = ::rustango::audit::PendingEntry {
1689                        entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1690                        entity_pk: #pk_str,
1691                        operation: ::rustango::audit::AuditOp::Create,
1692                        source: ::rustango::audit::current_source(),
1693                        changes: ::rustango::audit::snapshot_changes(&[
1694                            #( #pairs ),*
1695                        ]),
1696                    };
1697                    let _result = ::rustango::audit::insert_one_with_audit_pool(
1698                        pool, &_query, &_audit_entry,
1699                    ).await?;
1700                    ::rustango::sql::apply_auto_pk_pool(_result, self)
1701                }
1702            }
1703        } else {
1704            quote!()
1705        }
1706    } else {
1707        // Keep the non-audited pool_insert_method we built earlier.
1708        pool_insert_method
1709    };
1710
1711    // Update audited save_pool: now that insert_pool is wired for
1712    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
1713    // insert_pool. Non-audited save_pool already does this.
1714    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
1715    // Replaces the snapshot-only emission with a per-backend transaction
1716    // body that:
1717    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
1718    //     column), capturing BEFORE values
1719    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
1720    //  3. builds AFTER pairs from &self
1721    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
1722    //     AuditOp::Update + diff_changes(...) on the same tx connection
1723    //  5. commits
1724    //
1725    // Per-backend arms inline the SQL string + placeholder shape, then
1726    // share the `audit_before_pair_tokens` decoder block (Row::try_get
1727    // is polymorphic over Row type — the same tokens work against
1728    // PgRow and MySqlRow as long as the field's Rust type implements
1729    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
1730    // primitives + chrono/uuid/serde_json::Value all do).
1731    let pool_save_method = if let Some(tracked) = audited_fields {
1732        if let Some((pk_ident, pk_col)) = primary_key {
1733            let pk_column_lit = pk_col.as_str();
1734            // Two iterators — quote!'s `#(#var)*` consumes the
1735            // iterator, and we need to splice the same after-pairs
1736            // sequence into both per-backend arms.
1737            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
1738            let pk_str = audit_pk_to_string.clone();
1739            // Per-tracked-column BEFORE-pair token list. Each entry
1740            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
1741            // The Row alias resolves to PgRow / MySqlRow per call site,
1742            // so the same template generates both the PG and MySQL bodies.
1743            let mk_before_pairs = |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
1744                tracked
1745                    .iter()
1746                    .map(|c| {
1747                        let column_lit = c.column.as_str();
1748                        let value_ty = &c.value_ty;
1749                        quote! {
1750                            (
1751                                #column_lit,
1752                                match #getter::<#value_ty>(
1753                                    _audit_before_row, #column_lit,
1754                                ) {
1755                                    ::core::result::Result::Ok(v) => {
1756                                        ::serde_json::to_value(&v)
1757                                            .unwrap_or(::serde_json::Value::Null)
1758                                    }
1759                                    ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1760                                },
1761                            )
1762                        }
1763                    })
1764                    .collect()
1765            };
1766            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
1767                mk_before_pairs(quote!(::rustango::sql::try_get_returning));
1768            let before_pairs_my: Vec<proc_macro2::TokenStream> =
1769                mk_before_pairs(quote!(::rustango::sql::try_get_returning_my));
1770            let pg_select_cols: String = tracked
1771                .iter()
1772                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1773                .collect::<Vec<_>>()
1774                .join(", ");
1775            let my_select_cols: String = tracked
1776                .iter()
1777                .map(|c| format!("`{}`", c.column.replace('`', "``")))
1778                .collect::<Vec<_>>()
1779                .join(", ");
1780            let pk_value_for_bind = if fields.pk_is_auto {
1781                quote!(self.#pk_ident.get().copied().unwrap_or_default())
1782            } else {
1783                quote!(::core::clone::Clone::clone(&self.#pk_ident))
1784            };
1785            let assignments = &fields.update_assignments;
1786            let unset_dispatch = if fields.has_auto {
1787                quote! {
1788                    if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
1789                        return self.insert_pool(pool).await;
1790                    }
1791                }
1792            } else {
1793                quote!()
1794            };
1795            quote! {
1796                /// Save this row against either backend with audit
1797                /// emission (diff-style: BEFORE+AFTER) inside the
1798                /// same transaction. Auto::Unset PK routes to
1799                /// insert_pool. Bi-dialect counterpart of
1800                /// [`Self::save`] for audited models.
1801                ///
1802                /// The audit row's `changes` JSON contains one
1803                /// `{ "field": { "before": …, "after": … } }` entry
1804                /// per tracked column whose value actually changed
1805                /// — same shape as the existing &PgPool save() emits.
1806                ///
1807                /// # Errors
1808                /// As [`Self::save`].
1809                pub async fn save_pool(
1810                    &mut self,
1811                    pool: &::rustango::sql::Pool,
1812                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
1813                    #unset_dispatch
1814                    let _query = ::rustango::core::UpdateQuery {
1815                        model: <Self as ::rustango::core::Model>::SCHEMA,
1816                        set: ::std::vec![ #( #assignments ),* ],
1817                        where_clause: ::rustango::core::WhereExpr::Predicate(
1818                            ::rustango::core::Filter {
1819                                column: #pk_column_lit,
1820                                op: ::rustango::core::Op::Eq,
1821                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1822                                    ::core::clone::Clone::clone(&self.#pk_ident)
1823                                ),
1824                            }
1825                        ),
1826                    };
1827                    let _after_pairs: ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
1828                        ::std::vec![ #( #after_pairs_pg ),* ];
1829                    ::rustango::audit::save_one_with_diff_pool(
1830                        pool,
1831                        &_query,
1832                        #pk_column_lit,
1833                        ::core::convert::Into::<::rustango::core::SqlValue>::into(
1834                            #pk_value_for_bind,
1835                        ),
1836                        <Self as ::rustango::core::Model>::SCHEMA.table,
1837                        #pk_str,
1838                        _after_pairs,
1839                        #pg_select_cols,
1840                        #my_select_cols,
1841                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
1842                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
1843                    ).await
1844                }
1845            }
1846        } else {
1847            quote!()
1848        }
1849    } else {
1850        pool_save_method
1851    };
1852
1853    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
1854    // body shapes:
1855    // - non-audited: simple dispatch through `sql::delete_pool`
1856    // - audited: routes through `audit::delete_one_with_audit_pool`,
1857    //   which opens a per-backend transaction wrapping DELETE +
1858    //   audit emit so the data write and audit row commit atomically.
1859    let pool_delete_method = {
1860        let pk_column_lit = primary_key
1861            .map(|(_, col)| col.as_str())
1862            .unwrap_or("id");
1863        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
1864        if let Some(pk_ident) = pk_ident_for_pool {
1865            if audited_fields.is_some() {
1866                let pairs = audit_pair_tokens.iter();
1867                let pk_str = audit_pk_to_string.clone();
1868                quote! {
1869                    /// Delete this row against either backend with audit
1870                    /// emission inside the same transaction. Bi-dialect
1871                    /// counterpart of [`Self::delete`] for audited models.
1872                    ///
1873                    /// # Errors
1874                    /// As [`Self::delete`].
1875                    pub async fn delete_pool(
1876                        &self,
1877                        pool: &::rustango::sql::Pool,
1878                    ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1879                        let _query = ::rustango::core::DeleteQuery {
1880                            model: <Self as ::rustango::core::Model>::SCHEMA,
1881                            where_clause: ::rustango::core::WhereExpr::Predicate(
1882                                ::rustango::core::Filter {
1883                                    column: #pk_column_lit,
1884                                    op: ::rustango::core::Op::Eq,
1885                                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1886                                        ::core::clone::Clone::clone(&self.#pk_ident)
1887                                    ),
1888                                }
1889                            ),
1890                        };
1891                        let _audit_entry = ::rustango::audit::PendingEntry {
1892                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
1893                            entity_pk: #pk_str,
1894                            operation: ::rustango::audit::AuditOp::Delete,
1895                            source: ::rustango::audit::current_source(),
1896                            changes: ::rustango::audit::snapshot_changes(&[
1897                                #( #pairs ),*
1898                            ]),
1899                        };
1900                        ::rustango::audit::delete_one_with_audit_pool(
1901                            pool, &_query, &_audit_entry,
1902                        ).await
1903                    }
1904                }
1905            } else {
1906                quote! {
1907                    /// Delete the row identified by this instance's primary key
1908                    /// against either backend. Equivalent to [`Self::delete`] but
1909                    /// takes [`::rustango::sql::Pool`] and dispatches per backend.
1910                    ///
1911                    /// # Errors
1912                    /// As [`Self::delete`].
1913                    pub async fn delete_pool(
1914                        &self,
1915                        pool: &::rustango::sql::Pool,
1916                    ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
1917                        let _query = ::rustango::core::DeleteQuery {
1918                            model: <Self as ::rustango::core::Model>::SCHEMA,
1919                            where_clause: ::rustango::core::WhereExpr::Predicate(
1920                                ::rustango::core::Filter {
1921                                    column: #pk_column_lit,
1922                                    op: ::rustango::core::Op::Eq,
1923                                    value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
1924                                        ::core::clone::Clone::clone(&self.#pk_ident)
1925                                    ),
1926                                }
1927                            ),
1928                        };
1929                        ::rustango::sql::delete_pool(pool, &_query).await
1930                    }
1931                }
1932            }
1933        } else {
1934            quote!()
1935        }
1936    };
1937
1938    // Update emission captures both BEFORE and AFTER state — runs an
1939    // extra SELECT against `_executor` BEFORE the UPDATE, captures
1940    // each tracked field's prior value, then after the UPDATE diffs
1941    // against the in-memory `&self`. `diff_changes` drops unchanged
1942    // columns so the JSON only contains the actual delta.
1943    //
1944    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
1945    // and binds `_audit_before_pairs`; `audit_update_post` runs
1946    // after the UPDATE and emits the PendingEntry.
1947    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) =
1948        if let Some(tracked) = audited_fields {
1949            if tracked.is_empty() {
1950                (quote!(), quote!())
1951            } else {
1952                let select_cols: String = tracked
1953                    .iter()
1954                    .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
1955                    .collect::<Vec<_>>()
1956                    .join(", ");
1957                let pk_column_for_select = primary_key
1958                    .map(|(_, col)| col.clone())
1959                    .unwrap_or_default();
1960                let select_cols_lit = select_cols;
1961                let pk_column_lit_for_select = pk_column_for_select;
1962                let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
1963                    if fields.pk_is_auto {
1964                        quote!(self.#pk_ident.get().copied().unwrap_or_default())
1965                    } else {
1966                        quote!(::core::clone::Clone::clone(&self.#pk_ident))
1967                    }
1968                } else {
1969                    quote!(0_i64)
1970                };
1971                let before_pairs = tracked.iter().map(|c| {
1972                    let column_lit = c.column.as_str();
1973                    let value_ty = &c.value_ty;
1974                    quote! {
1975                        (
1976                            #column_lit,
1977                            match ::rustango::sql::sqlx::Row::try_get::<#value_ty, _>(
1978                                &_audit_before_row, #column_lit,
1979                            ) {
1980                                ::core::result::Result::Ok(v) => {
1981                                    ::serde_json::to_value(&v)
1982                                        .unwrap_or(::serde_json::Value::Null)
1983                                }
1984                                ::core::result::Result::Err(_) => ::serde_json::Value::Null,
1985                            },
1986                        )
1987                    }
1988                });
1989                let after_pairs = tracked.iter().map(|c| {
1990                    let column_lit = c.column.as_str();
1991                    let ident = &c.ident;
1992                    quote! {
1993                        (
1994                            #column_lit,
1995                            ::serde_json::to_value(&self.#ident)
1996                                .unwrap_or(::serde_json::Value::Null),
1997                        )
1998                    }
1999                });
2000                let pk_str = audit_pk_to_string.clone();
2001                let pre = quote! {
2002                    let _audit_select_sql = ::std::format!(
2003                        r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
2004                        #select_cols_lit,
2005                        <Self as ::rustango::core::Model>::SCHEMA.table,
2006                        #pk_column_lit_for_select,
2007                    );
2008                    let _audit_before_pairs:
2009                        ::std::option::Option<::std::vec::Vec<(&'static str, ::serde_json::Value)>> =
2010                        match ::rustango::sql::sqlx::query(&_audit_select_sql)
2011                            .bind(#pk_value_for_bind)
2012                            .fetch_optional(&mut *_executor)
2013                            .await
2014                        {
2015                            ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
2016                                ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
2017                            }
2018                            _ => ::core::option::Option::None,
2019                        };
2020                };
2021                let post = quote! {
2022                    if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
2023                        let _audit_after:
2024                            ::std::vec::Vec<(&'static str, ::serde_json::Value)> =
2025                            ::std::vec![ #( #after_pairs ),* ];
2026                        let _audit_entry = ::rustango::audit::PendingEntry {
2027                            entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2028                            entity_pk: #pk_str,
2029                            operation: ::rustango::audit::AuditOp::Update,
2030                            source: ::rustango::audit::current_source(),
2031                            changes: ::rustango::audit::diff_changes(
2032                                &_audit_before,
2033                                &_audit_after,
2034                            ),
2035                        };
2036                        ::rustango::audit::emit_one(&mut *_executor, &_audit_entry).await?;
2037                    }
2038                };
2039                (pre, post)
2040            }
2041        } else {
2042            (quote!(), quote!())
2043        };
2044
2045    // Bulk-insert audit: capture every row's tracked fields after the
2046    // RETURNING populates each PK, then push one batched INSERT INTO
2047    // audit_log via `emit_many`. One round-trip regardless of N rows.
2048    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
2049        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
2050            if fields.pk_is_auto {
2051                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
2052            } else {
2053                quote!(::std::format!("{}", &_row.#pk_ident))
2054            }
2055        } else {
2056            quote!(::std::string::String::new())
2057        };
2058        let row_pairs = audited_fields
2059            .unwrap_or(&[])
2060            .iter()
2061            .map(|c| {
2062                let column_lit = c.column.as_str();
2063                let ident = &c.ident;
2064                quote! {
2065                    (
2066                        #column_lit,
2067                        ::serde_json::to_value(&_row.#ident)
2068                            .unwrap_or(::serde_json::Value::Null),
2069                    )
2070                }
2071            });
2072        quote! {
2073            let _audit_source = ::rustango::audit::current_source();
2074            let mut _audit_entries:
2075                ::std::vec::Vec<::rustango::audit::PendingEntry> =
2076                    ::std::vec::Vec::with_capacity(rows.len());
2077            for _row in rows.iter() {
2078                _audit_entries.push(::rustango::audit::PendingEntry {
2079                    entity_table: <Self as ::rustango::core::Model>::SCHEMA.table,
2080                    entity_pk: #row_pk_str,
2081                    operation: ::rustango::audit::AuditOp::Create,
2082                    source: _audit_source.clone(),
2083                    changes: ::rustango::audit::snapshot_changes(&[
2084                        #( #row_pairs ),*
2085                    ]),
2086                });
2087            }
2088            ::rustango::audit::emit_many(&mut *_executor, &_audit_entries).await?;
2089        }
2090    } else {
2091        quote!()
2092    };
2093
2094    let save_method = if fields.pk_is_auto {
2095        let (pk_ident, pk_column) = primary_key
2096            .expect("pk_is_auto implies primary_key is Some");
2097        let pk_column_lit = pk_column.as_str();
2098        let assignments = &fields.update_assignments;
2099        let upsert_cols = &fields.upsert_update_columns;
2100        let upsert_pushes = &fields.insert_pushes;
2101        let upsert_returning = &fields.returning_cols;
2102        let upsert_auto_assigns = &fields.auto_assigns;
2103        let conflict_clause = if fields.upsert_update_columns.is_empty() {
2104            quote!(::rustango::core::ConflictClause::DoNothing)
2105        } else {
2106            quote!(::rustango::core::ConflictClause::DoUpdate {
2107                target: ::std::vec![#pk_column_lit],
2108                update_columns: ::std::vec![ #( #upsert_cols ),* ],
2109            })
2110        };
2111        Some(quote! {
2112            /// Insert this row if its `Auto<T>` primary key is
2113            /// `Unset`, otherwise update the existing row matching the
2114            /// PK. Mirrors Django's `save()` — caller doesn't need to
2115            /// pick `insert` vs the bulk-update path manually.
2116            ///
2117            /// On the insert branch, populates the PK from `RETURNING`
2118            /// (same behavior as `insert`). On the update branch,
2119            /// writes every non-PK column back; if no row matches the
2120            /// PK, returns `Ok(())` silently.
2121            ///
2122            /// Only generated when the primary key is declared as
2123            /// `Auto<T>`. Models with a manually-managed PK must use
2124            /// `insert` or the QuerySet update builder.
2125            ///
2126            /// # Errors
2127            /// Returns [`::rustango::sql::ExecError`] for SQL-writing
2128            /// or driver failures.
2129            pub async fn save(
2130                &mut self,
2131                pool: &::rustango::sql::sqlx::PgPool,
2132            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2133                #pool_to_save_on
2134            }
2135
2136            /// Like [`Self::save`] but accepts any sqlx executor —
2137            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
2138            /// escape hatch for tenant-scoped writes: schema-mode
2139            /// tenants share the registry pool but rely on a per-
2140            /// checkout `SET search_path`, so passing `&PgPool` would
2141            /// silently hit the wrong schema. Acquire a connection
2142            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
2143            ///
2144            /// # Errors
2145            /// As [`Self::save`].
2146            pub async fn save_on #executor_generics (
2147                &mut self,
2148                #executor_param,
2149            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2150            #executor_where
2151            {
2152                if matches!(self.#pk_ident, ::rustango::sql::Auto::Unset) {
2153                    return self.insert_on(#executor_passes_to_data_write).await;
2154                }
2155                #audit_update_pre
2156                let _query = ::rustango::core::UpdateQuery {
2157                    model: <Self as ::rustango::core::Model>::SCHEMA,
2158                    set: ::std::vec![ #( #assignments ),* ],
2159                    where_clause: ::rustango::core::WhereExpr::Predicate(
2160                        ::rustango::core::Filter {
2161                            column: #pk_column_lit,
2162                            op: ::rustango::core::Op::Eq,
2163                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2164                                ::core::clone::Clone::clone(&self.#pk_ident)
2165                            ),
2166                        }
2167                    ),
2168                };
2169                let _ = ::rustango::sql::update_on(
2170                    #executor_passes_to_data_write,
2171                    &_query,
2172                ).await?;
2173                #audit_update_post
2174                ::core::result::Result::Ok(())
2175            }
2176
2177            /// Per-call override for the audit source. Runs
2178            /// [`Self::save_on`] inside an [`::rustango::audit::with_source`]
2179            /// scope so the resulting audit entry records `source`
2180            /// instead of the task-local default. Useful for seed
2181            /// scripts and one-off CLI tools that don't sit inside an
2182            /// admin handler. The override applies only to this call;
2183            /// no global state changes.
2184            ///
2185            /// # Errors
2186            /// As [`Self::save_on`].
2187            pub async fn save_on_with #executor_generics (
2188                &mut self,
2189                #executor_param,
2190                source: ::rustango::audit::AuditSource,
2191            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2192            #executor_where
2193            {
2194                ::rustango::audit::with_source(source, self.save_on(_executor)).await
2195            }
2196
2197            /// Insert this row or update it in-place if the primary key already
2198            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
2199            ///
2200            /// With `Auto::Unset` PK the server assigns a new key and no conflict
2201            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
2202            /// inserted if absent or all non-PK columns are overwritten if present.
2203            ///
2204            /// # Errors
2205            /// As [`Self::insert_on`].
2206            pub async fn upsert(
2207                &mut self,
2208                pool: &::rustango::sql::sqlx::PgPool,
2209            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2210                #pool_to_upsert_on
2211            }
2212
2213            /// Like [`Self::upsert`] but accepts any sqlx executor.
2214            /// See [`Self::save_on`] for tenancy-scoped rationale.
2215            ///
2216            /// # Errors
2217            /// As [`Self::upsert`].
2218            pub async fn upsert_on #executor_generics (
2219                &mut self,
2220                #executor_param,
2221            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2222            #executor_where
2223            {
2224                let mut _columns: ::std::vec::Vec<&'static str> =
2225                    ::std::vec::Vec::new();
2226                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2227                    ::std::vec::Vec::new();
2228                #( #upsert_pushes )*
2229                let query = ::rustango::core::InsertQuery {
2230                    model: <Self as ::rustango::core::Model>::SCHEMA,
2231                    columns: _columns,
2232                    values: _values,
2233                    returning: ::std::vec![ #( #upsert_returning ),* ],
2234                    on_conflict: ::core::option::Option::Some(#conflict_clause),
2235                };
2236                let _returning_row_v = ::rustango::sql::insert_returning_on(
2237                    #executor_passes_to_data_write,
2238                    &query,
2239                ).await?;
2240                let _returning_row = &_returning_row_v;
2241                #( #upsert_auto_assigns )*
2242                ::core::result::Result::Ok(())
2243            }
2244        })
2245    } else {
2246        None
2247    };
2248
2249    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
2250        let pk_column_lit = pk_column.as_str();
2251        // Optional `soft_delete_on` / `restore_on` companions when the
2252        // model has a `#[rustango(soft_delete)]` column. They land
2253        // alongside the regular `delete_on` so callers have both
2254        // options — a hard delete (audit-tracked as a real DELETE) and
2255        // a logical delete (audit-tracked as an UPDATE setting the
2256        // deleted_at column to NOW()).
2257        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
2258            let col_lit = col;
2259            quote! {
2260                /// Soft-delete this row by setting its
2261                /// `#[rustango(soft_delete)]` column to `NOW()`.
2262                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
2263                /// the row stays in the table; query helpers can
2264                /// filter it out by checking the column for `IS NOT
2265                /// NULL`.
2266                ///
2267                /// # Errors
2268                /// As [`Self::delete`].
2269                pub async fn soft_delete_on #executor_generics (
2270                    &self,
2271                    #executor_param,
2272                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2273                #executor_where
2274                {
2275                    let _query = ::rustango::core::UpdateQuery {
2276                        model: <Self as ::rustango::core::Model>::SCHEMA,
2277                        set: ::std::vec![
2278                            ::rustango::core::Assignment {
2279                                column: #col_lit,
2280                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2281                                    ::chrono::Utc::now()
2282                                ),
2283                            },
2284                        ],
2285                        where_clause: ::rustango::core::WhereExpr::Predicate(
2286                            ::rustango::core::Filter {
2287                                column: #pk_column_lit,
2288                                op: ::rustango::core::Op::Eq,
2289                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2290                                    ::core::clone::Clone::clone(&self.#pk_ident)
2291                                ),
2292                            }
2293                        ),
2294                    };
2295                    let _affected = ::rustango::sql::update_on(
2296                        #executor_passes_to_data_write,
2297                        &_query,
2298                    ).await?;
2299                    #audit_softdelete_emit
2300                    ::core::result::Result::Ok(_affected)
2301                }
2302
2303                /// Inverse of [`Self::soft_delete_on`] — clears the
2304                /// soft-delete column back to NULL so the row is
2305                /// considered live again.
2306                ///
2307                /// # Errors
2308                /// As [`Self::delete`].
2309                pub async fn restore_on #executor_generics (
2310                    &self,
2311                    #executor_param,
2312                ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2313                #executor_where
2314                {
2315                    let _query = ::rustango::core::UpdateQuery {
2316                        model: <Self as ::rustango::core::Model>::SCHEMA,
2317                        set: ::std::vec![
2318                            ::rustango::core::Assignment {
2319                                column: #col_lit,
2320                                value: ::rustango::core::SqlValue::Null,
2321                            },
2322                        ],
2323                        where_clause: ::rustango::core::WhereExpr::Predicate(
2324                            ::rustango::core::Filter {
2325                                column: #pk_column_lit,
2326                                op: ::rustango::core::Op::Eq,
2327                                value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2328                                    ::core::clone::Clone::clone(&self.#pk_ident)
2329                                ),
2330                            }
2331                        ),
2332                    };
2333                    let _affected = ::rustango::sql::update_on(
2334                        #executor_passes_to_data_write,
2335                        &_query,
2336                    ).await?;
2337                    #audit_restore_emit
2338                    ::core::result::Result::Ok(_affected)
2339                }
2340            }
2341        } else {
2342            quote!()
2343        };
2344        quote! {
2345            /// Delete the row identified by this instance's primary key.
2346            ///
2347            /// Returns the number of rows affected (0 or 1).
2348            ///
2349            /// # Errors
2350            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
2351            /// driver failures.
2352            pub async fn delete(
2353                &self,
2354                pool: &::rustango::sql::sqlx::PgPool,
2355            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError> {
2356                #pool_to_delete_on
2357            }
2358
2359            /// Like [`Self::delete`] but accepts any sqlx executor —
2360            /// for tenant-scoped deletes against an explicitly-acquired
2361            /// connection. See [`Self::save_on`] for the rationale.
2362            ///
2363            /// # Errors
2364            /// As [`Self::delete`].
2365            pub async fn delete_on #executor_generics (
2366                &self,
2367                #executor_param,
2368            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2369            #executor_where
2370            {
2371                let query = ::rustango::core::DeleteQuery {
2372                    model: <Self as ::rustango::core::Model>::SCHEMA,
2373                    where_clause: ::rustango::core::WhereExpr::Predicate(
2374                        ::rustango::core::Filter {
2375                            column: #pk_column_lit,
2376                            op: ::rustango::core::Op::Eq,
2377                            value: ::core::convert::Into::<::rustango::core::SqlValue>::into(
2378                                ::core::clone::Clone::clone(&self.#pk_ident)
2379                            ),
2380                        }
2381                    ),
2382                };
2383                let _affected = ::rustango::sql::delete_on(
2384                    #executor_passes_to_data_write,
2385                    &query,
2386                ).await?;
2387                #audit_delete_emit
2388                ::core::result::Result::Ok(_affected)
2389            }
2390
2391            /// Per-call audit-source override for [`Self::delete_on`].
2392            /// See [`Self::save_on_with`] for shape rationale.
2393            ///
2394            /// # Errors
2395            /// As [`Self::delete_on`].
2396            pub async fn delete_on_with #executor_generics (
2397                &self,
2398                #executor_param,
2399                source: ::rustango::audit::AuditSource,
2400            ) -> ::core::result::Result<u64, ::rustango::sql::ExecError>
2401            #executor_where
2402            {
2403                ::rustango::audit::with_source(source, self.delete_on(_executor)).await
2404            }
2405            #pool_delete_method
2406            #pool_insert_method
2407            #pool_save_method
2408            #soft_delete_methods
2409        }
2410    });
2411
2412    let insert_method = if fields.has_auto {
2413        let pushes = &fields.insert_pushes;
2414        let returning_cols = &fields.returning_cols;
2415        let auto_assigns = &fields.auto_assigns;
2416        quote! {
2417            /// Insert this row into its table. Skips columns whose
2418            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
2419            /// sequence fills them in, then reads each `Auto` column
2420            /// back via `RETURNING` and stores it on `self`.
2421            ///
2422            /// # Errors
2423            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
2424            /// driver failures.
2425            pub async fn insert(
2426                &mut self,
2427                pool: &::rustango::sql::sqlx::PgPool,
2428            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2429                #pool_to_insert_on
2430            }
2431
2432            /// Like [`Self::insert`] but accepts any sqlx executor.
2433            /// See [`Self::save_on`] for tenancy-scoped rationale.
2434            ///
2435            /// # Errors
2436            /// As [`Self::insert`].
2437            pub async fn insert_on #executor_generics (
2438                &mut self,
2439                #executor_param,
2440            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2441            #executor_where
2442            {
2443                let mut _columns: ::std::vec::Vec<&'static str> =
2444                    ::std::vec::Vec::new();
2445                let mut _values: ::std::vec::Vec<::rustango::core::SqlValue> =
2446                    ::std::vec::Vec::new();
2447                #( #pushes )*
2448                let query = ::rustango::core::InsertQuery {
2449                    model: <Self as ::rustango::core::Model>::SCHEMA,
2450                    columns: _columns,
2451                    values: _values,
2452                    returning: ::std::vec![ #( #returning_cols ),* ],
2453                    on_conflict: ::core::option::Option::None,
2454                };
2455                let _returning_row_v = ::rustango::sql::insert_returning_on(
2456                    #executor_passes_to_data_write,
2457                    &query,
2458                ).await?;
2459                let _returning_row = &_returning_row_v;
2460                #( #auto_assigns )*
2461                #audit_insert_emit
2462                ::core::result::Result::Ok(())
2463            }
2464
2465            /// Per-call audit-source override for [`Self::insert_on`].
2466            /// See [`Self::save_on_with`] for shape rationale.
2467            ///
2468            /// # Errors
2469            /// As [`Self::insert_on`].
2470            pub async fn insert_on_with #executor_generics (
2471                &mut self,
2472                #executor_param,
2473                source: ::rustango::audit::AuditSource,
2474            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2475            #executor_where
2476            {
2477                ::rustango::audit::with_source(source, self.insert_on(_executor)).await
2478            }
2479        }
2480    } else {
2481        let insert_columns = &fields.insert_columns;
2482        let insert_values = &fields.insert_values;
2483        quote! {
2484            /// Insert this row into its table.
2485            ///
2486            /// # Errors
2487            /// Returns [`::rustango::sql::ExecError`] for SQL-writing or
2488            /// driver failures.
2489            pub async fn insert(
2490                &self,
2491                pool: &::rustango::sql::sqlx::PgPool,
2492            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2493                self.insert_on(pool).await
2494            }
2495
2496            /// Like [`Self::insert`] but accepts any sqlx executor.
2497            /// See [`Self::save_on`] for tenancy-scoped rationale.
2498            ///
2499            /// # Errors
2500            /// As [`Self::insert`].
2501            pub async fn insert_on<'_c, _E>(
2502                &self,
2503                _executor: _E,
2504            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2505            where
2506                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2507            {
2508                let query = ::rustango::core::InsertQuery {
2509                    model: <Self as ::rustango::core::Model>::SCHEMA,
2510                    columns: ::std::vec![ #( #insert_columns ),* ],
2511                    values: ::std::vec![ #( #insert_values ),* ],
2512                    returning: ::std::vec::Vec::new(),
2513                    on_conflict: ::core::option::Option::None,
2514                };
2515                ::rustango::sql::insert_on(_executor, &query).await
2516            }
2517        }
2518    };
2519
2520    let bulk_insert_method = if fields.has_auto {
2521        let cols_no_auto = &fields.bulk_columns_no_auto;
2522        let cols_all = &fields.bulk_columns_all;
2523        let pushes_no_auto = &fields.bulk_pushes_no_auto;
2524        let pushes_all = &fields.bulk_pushes_all;
2525        let returning_cols = &fields.returning_cols;
2526        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
2527        let uniformity = &fields.bulk_auto_uniformity;
2528        let first_auto_ident = fields
2529            .first_auto_ident
2530            .as_ref()
2531            .expect("has_auto implies first_auto_ident is Some");
2532        quote! {
2533            /// Bulk-insert `rows` in a single round-trip. Every row's
2534            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
2535            /// (sequence fills them in) or uniformly `Auto::Set(_)`
2536            /// (caller-supplied values). Mixed Set/Unset is rejected
2537            /// — call `insert` per row for that case.
2538            ///
2539            /// Empty slice is a no-op. Each row's `Auto` fields are
2540            /// populated from the `RETURNING` clause in input order
2541            /// before this returns.
2542            ///
2543            /// # Errors
2544            /// Returns [`::rustango::sql::ExecError`] for validation,
2545            /// SQL-writing, mixed-Auto rejection, or driver failures.
2546            pub async fn bulk_insert(
2547                rows: &mut [Self],
2548                pool: &::rustango::sql::sqlx::PgPool,
2549            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2550                #pool_to_bulk_insert_on
2551            }
2552
2553            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
2554            /// See [`Self::save_on`] for tenancy-scoped rationale.
2555            ///
2556            /// # Errors
2557            /// As [`Self::bulk_insert`].
2558            pub async fn bulk_insert_on #executor_generics (
2559                rows: &mut [Self],
2560                #executor_param,
2561            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2562            #executor_where
2563            {
2564                if rows.is_empty() {
2565                    return ::core::result::Result::Ok(());
2566                }
2567                let _first_unset = matches!(
2568                    rows[0].#first_auto_ident,
2569                    ::rustango::sql::Auto::Unset
2570                );
2571                #( #uniformity )*
2572
2573                let mut _all_rows: ::std::vec::Vec<
2574                    ::std::vec::Vec<::rustango::core::SqlValue>,
2575                > = ::std::vec::Vec::with_capacity(rows.len());
2576                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
2577                    for _row in rows.iter() {
2578                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2579                            ::std::vec::Vec::new();
2580                        #( #pushes_no_auto )*
2581                        _all_rows.push(_row_vals);
2582                    }
2583                    ::std::vec![ #( #cols_no_auto ),* ]
2584                } else {
2585                    for _row in rows.iter() {
2586                        let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2587                            ::std::vec::Vec::new();
2588                        #( #pushes_all )*
2589                        _all_rows.push(_row_vals);
2590                    }
2591                    ::std::vec![ #( #cols_all ),* ]
2592                };
2593
2594                let _query = ::rustango::core::BulkInsertQuery {
2595                    model: <Self as ::rustango::core::Model>::SCHEMA,
2596                    columns: _columns,
2597                    rows: _all_rows,
2598                    returning: ::std::vec![ #( #returning_cols ),* ],
2599                    on_conflict: ::core::option::Option::None,
2600                };
2601                let _returned = ::rustango::sql::bulk_insert_on(
2602                    #executor_passes_to_data_write,
2603                    &_query,
2604                ).await?;
2605                if _returned.len() != rows.len() {
2606                    return ::core::result::Result::Err(
2607                        ::rustango::sql::ExecError::Sql(
2608                            ::rustango::sql::SqlError::BulkInsertReturningMismatch {
2609                                expected: rows.len(),
2610                                actual: _returned.len(),
2611                            }
2612                        )
2613                    );
2614                }
2615                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
2616                    #auto_assigns_for_row
2617                }
2618                #audit_bulk_insert_emit
2619                ::core::result::Result::Ok(())
2620            }
2621        }
2622    } else {
2623        let cols_all = &fields.bulk_columns_all;
2624        let pushes_all = &fields.bulk_pushes_all;
2625        quote! {
2626            /// Bulk-insert `rows` in a single round-trip. Every row's
2627            /// fields are written verbatim — there are no `Auto<T>`
2628            /// fields on this model.
2629            ///
2630            /// Empty slice is a no-op.
2631            ///
2632            /// # Errors
2633            /// Returns [`::rustango::sql::ExecError`] for validation,
2634            /// SQL-writing, or driver failures.
2635            pub async fn bulk_insert(
2636                rows: &[Self],
2637                pool: &::rustango::sql::sqlx::PgPool,
2638            ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2639                Self::bulk_insert_on(rows, pool).await
2640            }
2641
2642            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
2643            /// See [`Self::save_on`] for tenancy-scoped rationale.
2644            ///
2645            /// # Errors
2646            /// As [`Self::bulk_insert`].
2647            pub async fn bulk_insert_on<'_c, _E>(
2648                rows: &[Self],
2649                _executor: _E,
2650            ) -> ::core::result::Result<(), ::rustango::sql::ExecError>
2651            where
2652                _E: ::rustango::sql::sqlx::Executor<'_c, Database = ::rustango::sql::sqlx::Postgres>,
2653            {
2654                if rows.is_empty() {
2655                    return ::core::result::Result::Ok(());
2656                }
2657                let mut _all_rows: ::std::vec::Vec<
2658                    ::std::vec::Vec<::rustango::core::SqlValue>,
2659                > = ::std::vec::Vec::with_capacity(rows.len());
2660                for _row in rows.iter() {
2661                    let mut _row_vals: ::std::vec::Vec<::rustango::core::SqlValue> =
2662                        ::std::vec::Vec::new();
2663                    #( #pushes_all )*
2664                    _all_rows.push(_row_vals);
2665                }
2666                let _query = ::rustango::core::BulkInsertQuery {
2667                    model: <Self as ::rustango::core::Model>::SCHEMA,
2668                    columns: ::std::vec![ #( #cols_all ),* ],
2669                    rows: _all_rows,
2670                    returning: ::std::vec::Vec::new(),
2671                    on_conflict: ::core::option::Option::None,
2672                };
2673                let _ = ::rustango::sql::bulk_insert_on(_executor, &_query).await?;
2674                ::core::result::Result::Ok(())
2675            }
2676        }
2677    };
2678
2679    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
2680        quote! {
2681            /// Hidden runtime accessor for the primary-key value as a
2682            /// [`SqlValue`]. Used by reverse-relation helpers
2683            /// (`<parent>::<child>_set`) emitted from sibling models'
2684            /// FK fields. Not part of the public API.
2685            #[doc(hidden)]
2686            pub fn __rustango_pk_value(&self) -> ::rustango::core::SqlValue {
2687                ::core::convert::Into::<::rustango::core::SqlValue>::into(
2688                    ::core::clone::Clone::clone(&self.#pk_ident)
2689                )
2690            }
2691        }
2692    });
2693
2694    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
2695        quote! {
2696            impl ::rustango::sql::HasPkValue for #struct_name {
2697                fn __rustango_pk_value_impl(&self) -> ::rustango::core::SqlValue {
2698                    ::core::convert::Into::<::rustango::core::SqlValue>::into(
2699                        ::core::clone::Clone::clone(&self.#pk_ident)
2700                    )
2701                }
2702            }
2703        }
2704    });
2705
2706    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
2707
2708    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk_pool`
2709    // dispatch to the right per-backend body without the macro emitting
2710    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
2711    // so audited models with non-Auto PKs (which still go through
2712    // `insert_one_with_audit_pool` → `apply_auto_pk_pool`) link.
2713    let assign_auto_pk_pool_impl = {
2714        let auto_assigns = &fields.auto_assigns;
2715        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
2716            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
2717            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
2718            // Auto<Uuid>/etc. fail to link against MySQL (intended —
2719            // those models can't use AUTO_INCREMENT). The trait is only
2720            // touched on the MySQL arm at runtime, so PG-only consumers
2721            // never see the bound failure.
2722            //
2723            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
2724            // Auto<i64> PK + auto_now_add timestamp) errored hard at
2725            // runtime with "multi-column RETURNING". MySQL has no
2726            // multi-column RETURNING semantic and a follow-up SELECT
2727            // would need cross-trait plumbing. Pragmatic shape: succeed
2728            // with the FIRST Auto field populated from LAST_INSERT_ID();
2729            // any other Auto fields stay `Auto::Unset`. Callers that
2730            // need the DB-defaulted timestamp / UUID can re-fetch the
2731            // row by PK after `save_pool`. Fixes the cookbook chapter
2732            // 12 dialect divergence.
2733            let value_ty = fields
2734                .first_auto_value_ty
2735                .as_ref()
2736                .expect("first_auto_value_ty set whenever first_auto_ident is");
2737            quote! {
2738                let _converted = <#value_ty as ::rustango::sql::MysqlAutoIdSet>
2739                    ::rustango_from_mysql_auto_id(_id)?;
2740                self.#first = ::rustango::sql::Auto::Set(_converted);
2741                ::core::result::Result::Ok(())
2742            }
2743        } else {
2744            quote! {
2745                let _ = _id;
2746                ::core::result::Result::Ok(())
2747            }
2748        };
2749        quote! {
2750            impl ::rustango::sql::AssignAutoPkPool for #struct_name {
2751                fn __rustango_assign_from_pg_row(
2752                    &mut self,
2753                    _returning_row: &::rustango::sql::PgReturningRow,
2754                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2755                    #( #auto_assigns )*
2756                    ::core::result::Result::Ok(())
2757                }
2758                fn __rustango_assign_from_mysql_id(
2759                    &mut self,
2760                    _id: i64,
2761                ) -> ::core::result::Result<(), ::rustango::sql::ExecError> {
2762                    #mysql_body
2763                }
2764            }
2765        }
2766    };
2767
2768    let from_aliased_row_inits = &fields.from_aliased_row_inits;
2769    let aliased_row_helper = quote! {
2770        /// Decode a row's aliased target columns (produced by
2771        /// `select_related`'s LEFT JOIN) into a fresh instance of
2772        /// this model. Reads each column via
2773        /// `format!("{prefix}__{col}")`, matching the alias the
2774        /// SELECT writer emitted. Slice 9.0d.
2775        #[doc(hidden)]
2776        pub fn __rustango_from_aliased_row(
2777            row: &::rustango::sql::sqlx::postgres::PgRow,
2778            prefix: &str,
2779        ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2780            ::core::result::Result::Ok(Self {
2781                #( #from_aliased_row_inits ),*
2782            })
2783        }
2784    };
2785    // v0.23.0-batch8 — MySQL counterpart, gated through the
2786    // cfg-aware macro_rules so PG-only builds expand to nothing.
2787    let aliased_row_helper_my = quote! {
2788        ::rustango::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
2789            #( #from_aliased_row_inits ),*
2790        });
2791    };
2792
2793    let load_related_impl =
2794        load_related_impl_tokens(struct_name, &fields.fk_relations);
2795    let load_related_impl_my =
2796        load_related_impl_my_tokens(struct_name, &fields.fk_relations);
2797
2798    quote! {
2799        impl #struct_name {
2800            /// Start a new `QuerySet` over this model.
2801            #[must_use]
2802            pub fn objects() -> ::rustango::query::QuerySet<#struct_name> {
2803                ::rustango::query::QuerySet::new()
2804            }
2805
2806            #insert_method
2807
2808            #bulk_insert_method
2809
2810            #save_method
2811
2812            #pk_methods
2813
2814            #pk_value_helper
2815
2816            #aliased_row_helper
2817
2818            #column_consts
2819        }
2820
2821        #aliased_row_helper_my
2822
2823        #load_related_impl
2824
2825        #load_related_impl_my
2826
2827        #has_pk_value_impl
2828
2829        #fk_pk_access_impl
2830
2831        #assign_auto_pk_pool_impl
2832    }
2833}
2834
2835/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
2836/// `auto_assigns` but reading from `_returning_row` and writing to
2837/// `_row_mut` instead of `self`.
2838fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
2839    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
2840        let col_lit = column.as_str();
2841        quote! {
2842            _row_mut.#ident = ::rustango::sql::sqlx::Row::try_get(
2843                _returning_row,
2844                #col_lit,
2845            )?;
2846        }
2847    });
2848    quote! { #( #lines )* }
2849}
2850
2851/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
2852fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
2853    let lines = entries.iter().map(|e| {
2854        let ident = &e.ident;
2855        let col_ty = column_type_ident(ident);
2856        quote! {
2857            #[allow(non_upper_case_globals)]
2858            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
2859        }
2860    });
2861    quote! { #(#lines)* }
2862}
2863
2864/// Emit a hidden per-model module carrying one zero-sized type per field,
2865/// each with a `Column` impl pointing back at the model.
2866fn column_module_tokens(
2867    module_ident: &syn::Ident,
2868    struct_name: &syn::Ident,
2869    entries: &[ColumnEntry],
2870) -> TokenStream2 {
2871    let items = entries.iter().map(|e| {
2872        let col_ty = column_type_ident(&e.ident);
2873        let value_ty = &e.value_ty;
2874        let name = &e.name;
2875        let column = &e.column;
2876        let field_type_tokens = &e.field_type_tokens;
2877        quote! {
2878            #[derive(::core::clone::Clone, ::core::marker::Copy)]
2879            pub struct #col_ty;
2880
2881            impl ::rustango::core::Column for #col_ty {
2882                type Model = super::#struct_name;
2883                type Value = #value_ty;
2884                const NAME: &'static str = #name;
2885                const COLUMN: &'static str = #column;
2886                const FIELD_TYPE: ::rustango::core::FieldType = #field_type_tokens;
2887            }
2888        }
2889    });
2890    quote! {
2891        #[doc(hidden)]
2892        #[allow(non_camel_case_types, non_snake_case)]
2893        pub mod #module_ident {
2894            // Re-import the parent scope so field types referencing
2895            // sibling models (e.g. `ForeignKey<Author>`) resolve
2896            // inside this submodule. Without this we'd hit
2897            // `proc_macro_derive_resolution_fallback` warnings.
2898            #[allow(unused_imports)]
2899            use super::*;
2900            #(#items)*
2901        }
2902    }
2903}
2904
2905fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
2906    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
2907}
2908
2909fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
2910    syn::Ident::new(
2911        &format!("__rustango_cols_{struct_name}"),
2912        struct_name.span(),
2913    )
2914}
2915
2916fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
2917    // The Postgres impl is always emitted — every rustango build pulls in
2918    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
2919    // routed through `::rustango::__impl_my_from_row!`, a cfg-gated
2920    // macro_rules whose body collapses to nothing when rustango is built
2921    // without the `mysql` feature. No user-facing feature shim required.
2922    //
2923    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
2924    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
2925    // back into a comma-separated list inside square brackets. Since each
2926    // entry is already in `field: expr` shape, the existing tokens slot in.
2927    quote! {
2928        impl<'r> ::rustango::sql::sqlx::FromRow<'r, ::rustango::sql::sqlx::postgres::PgRow>
2929            for #struct_name
2930        {
2931            fn from_row(
2932                row: &'r ::rustango::sql::sqlx::postgres::PgRow,
2933            ) -> ::core::result::Result<Self, ::rustango::sql::sqlx::Error> {
2934                ::core::result::Result::Ok(Self {
2935                    #( #from_row_inits ),*
2936                })
2937            }
2938        }
2939
2940        ::rustango::__impl_my_from_row!(#struct_name, |row| {
2941            #( #from_row_inits ),*
2942        });
2943    }
2944}
2945
2946struct ContainerAttrs {
2947    table: Option<String>,
2948    display: Option<(String, proc_macro2::Span)>,
2949    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
2950    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
2951    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
2952    /// at runtime — this attribute is the override for cases where
2953    /// the inference is wrong (e.g. a model that conceptually belongs
2954    /// to one app but is physically in another module).
2955    app: Option<String>,
2956    /// Django ModelAdmin-shape per-model knobs from
2957    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
2958    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
2959    /// admin code falls back to `AdminConfig::DEFAULT`.
2960    admin: Option<AdminAttrs>,
2961    /// Per-model audit configuration from `#[rustango(audit(...))]`.
2962    /// `None` when the model isn't audited — write paths emit no
2963    /// audit entries. When present, single-row writes capture
2964    /// before/after for the listed fields and bulk writes batch
2965    /// snapshots into one INSERT into `rustango_audit_log`.
2966    audit: Option<AuditAttrs>,
2967    /// `true` when `#[rustango(permissions)]` is present. Signals that
2968    /// `auto_create_permissions` should seed the four CRUD codenames for
2969    /// this model.
2970    permissions: bool,
2971    /// Many-to-many relations declared via
2972    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
2973    ///                 src = "post_id", dst = "tag_id"))]`.
2974    m2m: Vec<M2MAttr>,
2975    /// Composite indexes declared via
2976    /// `#[rustango(index("col1, col2"))]` or
2977    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
2978    /// Single-column indexes from `#[rustango(index)]` on fields are
2979    /// accumulated here during field collection.
2980    indexes: Vec<IndexAttr>,
2981    /// Table-level CHECK constraints declared via
2982    /// `#[rustango(check(name = "…", expr = "…"))]`.
2983    checks: Vec<CheckAttr>,
2984    /// Composite (multi-column) FKs declared via
2985    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
2986    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
2987    composite_fks: Vec<CompositeFkAttr>,
2988    /// Generic ("any model") FKs declared via
2989    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
2990    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
2991    generic_fks: Vec<GenericFkAttr>,
2992}
2993
2994/// Parsed form of one index declaration (field-level or container-level).
2995struct IndexAttr {
2996    /// Index name; auto-derived when `None` at parse time.
2997    name: Option<String>,
2998    /// Column names in the index.
2999    columns: Vec<String>,
3000    /// `true` for `CREATE UNIQUE INDEX`.
3001    unique: bool,
3002}
3003
3004/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
3005struct CheckAttr {
3006    name: String,
3007    expr: String,
3008}
3009
3010/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
3011/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
3012/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
3013/// the v0.15.0 ContentType plan — multi-column foreign keys live on
3014/// the model, not the field.
3015struct CompositeFkAttr {
3016    /// Logical relation name (free-form Rust identifier).
3017    name: String,
3018    /// SQL table name of the target.
3019    to: String,
3020    /// Source-side column names, in declaration order.
3021    from: Vec<String>,
3022    /// Target-side column names, same length / order as `from`.
3023    on: Vec<String>,
3024}
3025
3026/// Parsed form of one `#[rustango(generic_fk(name = "target",
3027/// ct_column = "content_type_id", pk_column = "object_pk"))]`
3028/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
3029/// generic ("any model") FKs live on the model, not the field.
3030struct GenericFkAttr {
3031    /// Logical relation name (free-form Rust identifier).
3032    name: String,
3033    /// Source-side column carrying the `content_type_id` value.
3034    ct_column: String,
3035    /// Source-side column carrying the target row's primary key.
3036    pk_column: String,
3037}
3038
3039/// Parsed form of one `#[rustango(m2m(...))]` declaration.
3040struct M2MAttr {
3041    /// Accessor suffix: `tags` → generates `tags_m2m()`.
3042    name: String,
3043    /// Target table (e.g. `"app_tags"`).
3044    to: String,
3045    /// Junction table (e.g. `"post_tags"`).
3046    through: String,
3047    /// Source FK column in the junction table (e.g. `"post_id"`).
3048    src: String,
3049    /// Destination FK column in the junction table (e.g. `"tag_id"`).
3050    dst: String,
3051}
3052
3053/// Parsed shape of `#[rustango(audit(track = "name, body", source =
3054/// "user"))]`. `track` is a comma-separated list of field names whose
3055/// before/after values land in the JSONB `changes` column. `source`
3056/// is informational only — it pins a default source when the model
3057/// is written outside any `audit::with_source(...)` scope (rare).
3058#[derive(Default)]
3059struct AuditAttrs {
3060    /// Field names to capture in the `changes` JSONB. Validated
3061    /// against declared scalar fields at compile time. Empty means
3062    /// "track every scalar field" — Django's audit-everything default.
3063    track: Option<(Vec<String>, proc_macro2::Span)>,
3064}
3065
3066/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
3067/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
3068/// comma-separated strings; we validate each ident against the model's
3069/// declared fields at compile time.
3070#[derive(Default)]
3071struct AdminAttrs {
3072    list_display: Option<(Vec<String>, proc_macro2::Span)>,
3073    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
3074    list_per_page: Option<usize>,
3075    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
3076    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
3077    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
3078    /// Bulk action names. No field-validation against model fields —
3079    /// these are action handlers, not column references.
3080    actions: Option<(Vec<String>, proc_macro2::Span)>,
3081    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
3082    /// sections, comma-separated fields per section, optional
3083    /// `Title:` prefix. Empty title omits the `<legend>`.
3084    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
3085}
3086
3087fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
3088    let mut out = ContainerAttrs {
3089        table: None,
3090        display: None,
3091        app: None,
3092        admin: None,
3093        audit: None,
3094        permissions: false,
3095        m2m: Vec::new(),
3096        indexes: Vec::new(),
3097        checks: Vec::new(),
3098        composite_fks: Vec::new(),
3099        generic_fks: Vec::new(),
3100    };
3101    for attr in &input.attrs {
3102        if !attr.path().is_ident("rustango") {
3103            continue;
3104        }
3105        attr.parse_nested_meta(|meta| {
3106            if meta.path.is_ident("table") {
3107                let s: LitStr = meta.value()?.parse()?;
3108                out.table = Some(s.value());
3109                return Ok(());
3110            }
3111            if meta.path.is_ident("display") {
3112                let s: LitStr = meta.value()?.parse()?;
3113                out.display = Some((s.value(), s.span()));
3114                return Ok(());
3115            }
3116            if meta.path.is_ident("app") {
3117                let s: LitStr = meta.value()?.parse()?;
3118                out.app = Some(s.value());
3119                return Ok(());
3120            }
3121            if meta.path.is_ident("admin") {
3122                let mut admin = AdminAttrs::default();
3123                meta.parse_nested_meta(|inner| {
3124                    if inner.path.is_ident("list_display") {
3125                        let s: LitStr = inner.value()?.parse()?;
3126                        admin.list_display =
3127                            Some((split_field_list(&s.value()), s.span()));
3128                        return Ok(());
3129                    }
3130                    if inner.path.is_ident("search_fields") {
3131                        let s: LitStr = inner.value()?.parse()?;
3132                        admin.search_fields =
3133                            Some((split_field_list(&s.value()), s.span()));
3134                        return Ok(());
3135                    }
3136                    if inner.path.is_ident("readonly_fields") {
3137                        let s: LitStr = inner.value()?.parse()?;
3138                        admin.readonly_fields =
3139                            Some((split_field_list(&s.value()), s.span()));
3140                        return Ok(());
3141                    }
3142                    if inner.path.is_ident("list_per_page") {
3143                        let lit: syn::LitInt = inner.value()?.parse()?;
3144                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
3145                        return Ok(());
3146                    }
3147                    if inner.path.is_ident("ordering") {
3148                        let s: LitStr = inner.value()?.parse()?;
3149                        admin.ordering = Some((
3150                            parse_ordering_list(&s.value()),
3151                            s.span(),
3152                        ));
3153                        return Ok(());
3154                    }
3155                    if inner.path.is_ident("list_filter") {
3156                        let s: LitStr = inner.value()?.parse()?;
3157                        admin.list_filter =
3158                            Some((split_field_list(&s.value()), s.span()));
3159                        return Ok(());
3160                    }
3161                    if inner.path.is_ident("actions") {
3162                        let s: LitStr = inner.value()?.parse()?;
3163                        admin.actions =
3164                            Some((split_field_list(&s.value()), s.span()));
3165                        return Ok(());
3166                    }
3167                    if inner.path.is_ident("fieldsets") {
3168                        let s: LitStr = inner.value()?.parse()?;
3169                        admin.fieldsets =
3170                            Some((parse_fieldset_list(&s.value()), s.span()));
3171                        return Ok(());
3172                    }
3173                    Err(inner.error(
3174                        "unknown admin attribute (supported: \
3175                         `list_display`, `search_fields`, `readonly_fields`, \
3176                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
3177                         `fieldsets`)",
3178                    ))
3179                })?;
3180                out.admin = Some(admin);
3181                return Ok(());
3182            }
3183            if meta.path.is_ident("audit") {
3184                let mut audit = AuditAttrs::default();
3185                meta.parse_nested_meta(|inner| {
3186                    if inner.path.is_ident("track") {
3187                        let s: LitStr = inner.value()?.parse()?;
3188                        audit.track =
3189                            Some((split_field_list(&s.value()), s.span()));
3190                        return Ok(());
3191                    }
3192                    Err(inner.error(
3193                        "unknown audit attribute (supported: `track`)",
3194                    ))
3195                })?;
3196                out.audit = Some(audit);
3197                return Ok(());
3198            }
3199            if meta.path.is_ident("permissions") {
3200                out.permissions = true;
3201                return Ok(());
3202            }
3203            if meta.path.is_ident("unique_together") {
3204                // Django-shape composite UNIQUE index. Two syntaxes:
3205                //
3206                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
3207                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
3208                //
3209                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
3210                // (col1, col2)`, where <name> defaults to
3211                // `<table>_<col1>_<col2>_uq` when not supplied.
3212                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
3213                out.indexes.push(IndexAttr { name, columns, unique: true });
3214                return Ok(());
3215            }
3216            if meta.path.is_ident("index_together") {
3217                // Django-shape composite (non-unique) index. Two syntaxes
3218                // mirroring `unique_together`.
3219                //
3220                //   #[rustango(index_together = "created_at, status")]
3221                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
3222                let (columns, name) = parse_together_attr(&meta, "index_together")?;
3223                out.indexes.push(IndexAttr { name, columns, unique: false });
3224                return Ok(());
3225            }
3226            if meta.path.is_ident("index") {
3227                // Container-level composite index — legacy entry that
3228                // was advertised with a trailing `, unique, name = ...`
3229                // flag block which doesn't actually compose under
3230                // `parse_nested_meta`. Prefer `unique_together` /
3231                // `index_together` (above) for new code. The bare
3232                // `index = "..."` form is kept for back-compat: it
3233                // emits a non-unique composite index.
3234                let cols_lit: LitStr = meta.value()?.parse()?;
3235                let columns = split_field_list(&cols_lit.value());
3236                out.indexes.push(IndexAttr { name: None, columns, unique: false });
3237                return Ok(());
3238            }
3239            if meta.path.is_ident("check") {
3240                // #[rustango(check(name = "…", expr = "…"))]
3241                let mut name: Option<String> = None;
3242                let mut expr: Option<String> = None;
3243                meta.parse_nested_meta(|inner| {
3244                    if inner.path.is_ident("name") {
3245                        let s: LitStr = inner.value()?.parse()?;
3246                        name = Some(s.value());
3247                        return Ok(());
3248                    }
3249                    if inner.path.is_ident("expr") {
3250                        let s: LitStr = inner.value()?.parse()?;
3251                        expr = Some(s.value());
3252                        return Ok(());
3253                    }
3254                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
3255                })?;
3256                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
3257                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
3258                out.checks.push(CheckAttr { name, expr });
3259                return Ok(());
3260            }
3261            if meta.path.is_ident("generic_fk") {
3262                let mut gfk = GenericFkAttr {
3263                    name: String::new(),
3264                    ct_column: String::new(),
3265                    pk_column: String::new(),
3266                };
3267                meta.parse_nested_meta(|inner| {
3268                    if inner.path.is_ident("name") {
3269                        let s: LitStr = inner.value()?.parse()?;
3270                        gfk.name = s.value();
3271                        return Ok(());
3272                    }
3273                    if inner.path.is_ident("ct_column") {
3274                        let s: LitStr = inner.value()?.parse()?;
3275                        gfk.ct_column = s.value();
3276                        return Ok(());
3277                    }
3278                    if inner.path.is_ident("pk_column") {
3279                        let s: LitStr = inner.value()?.parse()?;
3280                        gfk.pk_column = s.value();
3281                        return Ok(());
3282                    }
3283                    Err(inner.error(
3284                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
3285                    ))
3286                })?;
3287                if gfk.name.is_empty() {
3288                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
3289                }
3290                if gfk.ct_column.is_empty() {
3291                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
3292                }
3293                if gfk.pk_column.is_empty() {
3294                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
3295                }
3296                out.generic_fks.push(gfk);
3297                return Ok(());
3298            }
3299            if meta.path.is_ident("fk_composite") {
3300                let mut fk = CompositeFkAttr {
3301                    name: String::new(),
3302                    to: String::new(),
3303                    from: Vec::new(),
3304                    on: Vec::new(),
3305                };
3306                meta.parse_nested_meta(|inner| {
3307                    if inner.path.is_ident("name") {
3308                        let s: LitStr = inner.value()?.parse()?;
3309                        fk.name = s.value();
3310                        return Ok(());
3311                    }
3312                    if inner.path.is_ident("to") {
3313                        let s: LitStr = inner.value()?.parse()?;
3314                        fk.to = s.value();
3315                        return Ok(());
3316                    }
3317                    // `on = ("col1", "col2", ...)` — parse a parenthesised
3318                    // comma-list of string literals.
3319                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
3320                        let value = inner.value()?;
3321                        let content;
3322                        syn::parenthesized!(content in value);
3323                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
3324                            content.parse_terminated(
3325                                |p| p.parse::<syn::LitStr>(),
3326                                syn::Token![,],
3327                            )?;
3328                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
3329                        if inner.path.is_ident("on") {
3330                            fk.on = cols;
3331                        } else {
3332                            fk.from = cols;
3333                        }
3334                        return Ok(());
3335                    }
3336                    Err(inner.error(
3337                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
3338                    ))
3339                })?;
3340                if fk.name.is_empty() {
3341                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
3342                }
3343                if fk.to.is_empty() {
3344                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
3345                }
3346                if fk.from.is_empty() || fk.on.is_empty() {
3347                    return Err(meta.error(
3348                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
3349                    ));
3350                }
3351                if fk.from.len() != fk.on.len() {
3352                    return Err(meta.error(format!(
3353                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
3354                        fk.from.len(),
3355                        fk.on.len(),
3356                    )));
3357                }
3358                out.composite_fks.push(fk);
3359                return Ok(());
3360            }
3361            if meta.path.is_ident("m2m") {
3362                let mut m2m = M2MAttr {
3363                    name: String::new(),
3364                    to: String::new(),
3365                    through: String::new(),
3366                    src: String::new(),
3367                    dst: String::new(),
3368                };
3369                meta.parse_nested_meta(|inner| {
3370                    if inner.path.is_ident("name") {
3371                        let s: LitStr = inner.value()?.parse()?;
3372                        m2m.name = s.value();
3373                        return Ok(());
3374                    }
3375                    if inner.path.is_ident("to") {
3376                        let s: LitStr = inner.value()?.parse()?;
3377                        m2m.to = s.value();
3378                        return Ok(());
3379                    }
3380                    if inner.path.is_ident("through") {
3381                        let s: LitStr = inner.value()?.parse()?;
3382                        m2m.through = s.value();
3383                        return Ok(());
3384                    }
3385                    if inner.path.is_ident("src") {
3386                        let s: LitStr = inner.value()?.parse()?;
3387                        m2m.src = s.value();
3388                        return Ok(());
3389                    }
3390                    if inner.path.is_ident("dst") {
3391                        let s: LitStr = inner.value()?.parse()?;
3392                        m2m.dst = s.value();
3393                        return Ok(());
3394                    }
3395                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`)"))
3396                })?;
3397                if m2m.name.is_empty() {
3398                    return Err(meta.error("m2m requires `name = \"...\"`"));
3399                }
3400                if m2m.to.is_empty() {
3401                    return Err(meta.error("m2m requires `to = \"...\"`"));
3402                }
3403                if m2m.through.is_empty() {
3404                    return Err(meta.error("m2m requires `through = \"...\"`"));
3405                }
3406                if m2m.src.is_empty() {
3407                    return Err(meta.error("m2m requires `src = \"...\"`"));
3408                }
3409                if m2m.dst.is_empty() {
3410                    return Err(meta.error("m2m requires `dst = \"...\"`"));
3411                }
3412                out.m2m.push(m2m);
3413                return Ok(());
3414            }
3415            Err(meta.error("unknown rustango container attribute"))
3416        })?;
3417    }
3418    Ok(out)
3419}
3420
3421/// Split a comma-separated field-name list (e.g. `"name, office"`) into
3422/// owned field names, trimming whitespace and skipping empty entries.
3423/// Field-name validation against the model is done by the caller.
3424fn split_field_list(raw: &str) -> Vec<String> {
3425    raw.split(',')
3426        .map(str::trim)
3427        .filter(|s| !s.is_empty())
3428        .map(str::to_owned)
3429        .collect()
3430}
3431
3432/// Shared parser for `unique_together` and `index_together` container
3433/// attrs. Accepts both shapes:
3434///
3435///   * `attr = "col1, col2"`              — auto-derived index name.
3436///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
3437///
3438/// Returns `(columns, name)`.
3439fn parse_together_attr(
3440    meta: &syn::meta::ParseNestedMeta<'_>,
3441    attr: &str,
3442) -> syn::Result<(Vec<String>, Option<String>)> {
3443    // Disambiguate by whether the next token is `=` (key-value) or
3444    // `(` (parenthesized).
3445    if meta.input.peek(syn::Token![=]) {
3446        let cols_lit: LitStr = meta.value()?.parse()?;
3447        let columns = split_field_list(&cols_lit.value());
3448        check_together_columns(meta, attr, &columns)?;
3449        return Ok((columns, None));
3450    }
3451    let mut columns: Option<Vec<String>> = None;
3452    let mut name: Option<String> = None;
3453    meta.parse_nested_meta(|inner| {
3454        if inner.path.is_ident("columns") {
3455            let s: LitStr = inner.value()?.parse()?;
3456            columns = Some(split_field_list(&s.value()));
3457            return Ok(());
3458        }
3459        if inner.path.is_ident("name") {
3460            let s: LitStr = inner.value()?.parse()?;
3461            name = Some(s.value());
3462            return Ok(());
3463        }
3464        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
3465    })?;
3466    let columns = columns.ok_or_else(|| meta.error(format!(
3467        "{attr}(...) requires a `columns = \"col1, col2\"` argument",
3468    )))?;
3469    check_together_columns(meta, attr, &columns)?;
3470    Ok((columns, name))
3471}
3472
3473fn check_together_columns(
3474    meta: &syn::meta::ParseNestedMeta<'_>,
3475    attr: &str,
3476    columns: &[String],
3477) -> syn::Result<()> {
3478    if columns.len() < 2 {
3479        let single = if attr == "unique_together" {
3480            "#[rustango(unique)] on the field"
3481        } else {
3482            "#[rustango(index)] on the field"
3483        };
3484        return Err(meta.error(format!(
3485            "{attr} expects two or more columns; for a single-column equivalent use {single}",
3486        )));
3487    }
3488    Ok(())
3489}
3490
3491/// Parse the fieldsets DSL: pipe-separated sections, optional
3492/// `"Title:"` prefix on each, comma-separated field names after.
3493/// Examples:
3494/// * `"name, office"` → one untitled section with two fields
3495/// * `"Identity: name, office | Metadata: created_at"` → two titled
3496///   sections
3497///
3498/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
3499fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
3500    raw.split('|')
3501        .map(str::trim)
3502        .filter(|s| !s.is_empty())
3503        .map(|section| {
3504            // Split off an optional `Title:` prefix (first colon).
3505            let (title, rest) = match section.split_once(':') {
3506                Some((title, rest)) if !title.contains(',') => {
3507                    (title.trim().to_owned(), rest)
3508                }
3509                _ => (String::new(), section),
3510            };
3511            let fields = split_field_list(rest);
3512            (title, fields)
3513        })
3514        .collect()
3515}
3516
3517/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
3518/// Returns `(field_name, desc)` pairs in the same order as the input.
3519fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
3520    raw.split(',')
3521        .map(str::trim)
3522        .filter(|s| !s.is_empty())
3523        .map(|spec| {
3524            spec.strip_prefix('-')
3525                .map_or((spec.to_owned(), false), |rest| (rest.trim().to_owned(), true))
3526        })
3527        .collect()
3528}
3529
3530struct FieldAttrs {
3531    column: Option<String>,
3532    primary_key: bool,
3533    fk: Option<String>,
3534    o2o: Option<String>,
3535    on: Option<String>,
3536    max_length: Option<u32>,
3537    min: Option<i64>,
3538    max: Option<i64>,
3539    default: Option<String>,
3540    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
3541    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
3542    /// "gen_random_uuid()"`. The Rust field type must be
3543    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
3544    /// INSERTs so the DB DEFAULT fires.
3545    auto_uuid: bool,
3546    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
3547    /// Server-set on insert, immutable from app code afterwards.
3548    /// Implies `auto + default = "now()"`. Field type must be
3549    /// `DateTime<Utc>`.
3550    auto_now_add: bool,
3551    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
3552    /// every insert AND every update. Implies `auto + default =
3553    /// "now()"`; the macro additionally rewrites `update_on` /
3554    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
3555    /// field value.
3556    auto_now: bool,
3557    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
3558    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
3559    /// `soft_delete_on(executor)` and `restore_on(executor)`
3560    /// methods on the model.
3561    soft_delete: bool,
3562    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
3563    /// the column in the generated DDL.
3564    unique: bool,
3565    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
3566    /// generates a `CREATE INDEX` for this column. `unique` here means
3567    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
3568    index: bool,
3569    index_unique: bool,
3570    index_name: Option<String>,
3571}
3572
3573fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
3574    let mut out = FieldAttrs {
3575        column: None,
3576        primary_key: false,
3577        fk: None,
3578        o2o: None,
3579        on: None,
3580        max_length: None,
3581        min: None,
3582        max: None,
3583        default: None,
3584        auto_uuid: false,
3585        auto_now_add: false,
3586        auto_now: false,
3587        soft_delete: false,
3588        unique: false,
3589        index: false,
3590        index_unique: false,
3591        index_name: None,
3592    };
3593    for attr in &field.attrs {
3594        if !attr.path().is_ident("rustango") {
3595            continue;
3596        }
3597        attr.parse_nested_meta(|meta| {
3598            if meta.path.is_ident("column") {
3599                let s: LitStr = meta.value()?.parse()?;
3600                out.column = Some(s.value());
3601                return Ok(());
3602            }
3603            if meta.path.is_ident("primary_key") {
3604                out.primary_key = true;
3605                return Ok(());
3606            }
3607            if meta.path.is_ident("fk") {
3608                let s: LitStr = meta.value()?.parse()?;
3609                out.fk = Some(s.value());
3610                return Ok(());
3611            }
3612            if meta.path.is_ident("o2o") {
3613                let s: LitStr = meta.value()?.parse()?;
3614                out.o2o = Some(s.value());
3615                return Ok(());
3616            }
3617            if meta.path.is_ident("on") {
3618                let s: LitStr = meta.value()?.parse()?;
3619                out.on = Some(s.value());
3620                return Ok(());
3621            }
3622            if meta.path.is_ident("max_length") {
3623                let lit: syn::LitInt = meta.value()?.parse()?;
3624                out.max_length = Some(lit.base10_parse::<u32>()?);
3625                return Ok(());
3626            }
3627            if meta.path.is_ident("min") {
3628                out.min = Some(parse_signed_i64(&meta)?);
3629                return Ok(());
3630            }
3631            if meta.path.is_ident("max") {
3632                out.max = Some(parse_signed_i64(&meta)?);
3633                return Ok(());
3634            }
3635            if meta.path.is_ident("default") {
3636                let s: LitStr = meta.value()?.parse()?;
3637                out.default = Some(s.value());
3638                return Ok(());
3639            }
3640            if meta.path.is_ident("auto_uuid") {
3641                out.auto_uuid = true;
3642                // Implied: PK + auto + DEFAULT gen_random_uuid().
3643                // Each is also explicitly settable; the explicit
3644                // value wins if conflicting.
3645                out.primary_key = true;
3646                if out.default.is_none() {
3647                    out.default = Some("gen_random_uuid()".into());
3648                }
3649                return Ok(());
3650            }
3651            if meta.path.is_ident("auto_now_add") {
3652                out.auto_now_add = true;
3653                if out.default.is_none() {
3654                    out.default = Some("now()".into());
3655                }
3656                return Ok(());
3657            }
3658            if meta.path.is_ident("auto_now") {
3659                out.auto_now = true;
3660                if out.default.is_none() {
3661                    out.default = Some("now()".into());
3662                }
3663                return Ok(());
3664            }
3665            if meta.path.is_ident("soft_delete") {
3666                out.soft_delete = true;
3667                return Ok(());
3668            }
3669            if meta.path.is_ident("unique") {
3670                out.unique = true;
3671                return Ok(());
3672            }
3673            if meta.path.is_ident("index") {
3674                out.index = true;
3675                // Optional sub-attrs: #[rustango(index(unique, name = "…"))]
3676                if meta.input.peek(syn::token::Paren) {
3677                    meta.parse_nested_meta(|inner| {
3678                        if inner.path.is_ident("unique") {
3679                            out.index_unique = true;
3680                            return Ok(());
3681                        }
3682                        if inner.path.is_ident("name") {
3683                            let s: LitStr = inner.value()?.parse()?;
3684                            out.index_name = Some(s.value());
3685                            return Ok(());
3686                        }
3687                        Err(inner.error("unknown index sub-attribute (supported: `unique`, `name`)"))
3688                    })?;
3689                }
3690                return Ok(());
3691            }
3692            Err(meta.error("unknown rustango field attribute"))
3693        })?;
3694    }
3695    Ok(out)
3696}
3697
3698/// Parse a signed integer literal, accepting optional leading `-`.
3699fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
3700    let expr: syn::Expr = meta.value()?.parse()?;
3701    match expr {
3702        syn::Expr::Lit(syn::ExprLit {
3703            lit: syn::Lit::Int(lit),
3704            ..
3705        }) => lit.base10_parse::<i64>(),
3706        syn::Expr::Unary(syn::ExprUnary {
3707            op: syn::UnOp::Neg(_),
3708            expr,
3709            ..
3710        }) => {
3711            if let syn::Expr::Lit(syn::ExprLit {
3712                lit: syn::Lit::Int(lit),
3713                ..
3714            }) = *expr
3715            {
3716                let v: i64 = lit.base10_parse()?;
3717                Ok(-v)
3718            } else {
3719                Err(syn::Error::new_spanned(expr, "expected integer literal"))
3720            }
3721        }
3722        other => Err(syn::Error::new_spanned(
3723            other,
3724            "expected integer literal (signed)",
3725        )),
3726    }
3727}
3728
3729struct FieldInfo<'a> {
3730    ident: &'a syn::Ident,
3731    column: String,
3732    primary_key: bool,
3733    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
3734    /// skip this column when `Auto::Unset` and emit it under
3735    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
3736    auto: bool,
3737    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
3738    /// the `Column::Value` associated type for typed-column tokens.
3739    value_ty: &'a Type,
3740    /// `FieldType` variant tokens (`::rustango::core::FieldType::I64`).
3741    field_type_tokens: TokenStream2,
3742    schema: TokenStream2,
3743    from_row_init: TokenStream2,
3744    /// Variant of [`Self::from_row_init`] that reads the column via
3745    /// `format!("{prefix}__{col}")` so a model can be decoded out of
3746    /// the aliased columns of a JOINed row. Drives slice 9.0d's
3747    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
3748    /// helper that `select_related` calls when stitching loaded FKs.
3749    from_aliased_row_init: TokenStream2,
3750    /// Inner type from a `ForeignKey<T>` field, if any. The reverse-
3751    /// relation helper emit (`Author::<child>_set`) needs to know `T`
3752    /// to point the generated method at the right child model.
3753    fk_inner: Option<Type>,
3754    /// `true` when this column was marked `#[rustango(auto_now)]` —
3755    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
3756    /// column instead of the user-supplied value, so `updated_at`
3757    /// always reflects the latest write without the caller having
3758    /// to remember to set it.
3759    auto_now: bool,
3760    /// `true` when this column was marked `#[rustango(auto_now_add)]`
3761    /// — the column is server-set on INSERT (DB DEFAULT) and
3762    /// **immutable** afterwards. `update_on` / `save_on` skip the
3763    /// column entirely so a stale `created_at` value in memory never
3764    /// rewrites the persisted timestamp.
3765    auto_now_add: bool,
3766    /// `true` when this column was marked `#[rustango(soft_delete)]`.
3767    /// Triggers emission of `soft_delete_on(executor)` and
3768    /// `restore_on(executor)` on the model's inherent impl. There is
3769    /// at most one such column per model — emission asserts this.
3770    soft_delete: bool,
3771}
3772
3773fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
3774    let attrs = parse_field_attrs(field)?;
3775    let ident = field
3776        .ident
3777        .as_ref()
3778        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
3779    let name = ident.to_string();
3780    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
3781    let primary_key = attrs.primary_key;
3782    let DetectedType {
3783        kind,
3784        nullable,
3785        auto: detected_auto,
3786        fk_inner,
3787    } = detect_type(&field.ty)?;
3788    check_bound_compatibility(field, &attrs, kind)?;
3789    let auto = detected_auto;
3790    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
3791    // INSERT path: the user must wrap the field in `Auto<T>`, which
3792    // marks the column as DB-default-supplied. The mixin attrs then
3793    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
3794    // for `auto_now`, force the value on UPDATE too.
3795    if attrs.auto_uuid {
3796        if kind != DetectedKind::Uuid {
3797            return Err(syn::Error::new_spanned(
3798                field,
3799                "`#[rustango(auto_uuid)]` requires the field type to be \
3800                 `Auto<uuid::Uuid>`",
3801            ));
3802        }
3803        if !detected_auto {
3804            return Err(syn::Error::new_spanned(
3805                field,
3806                "`#[rustango(auto_uuid)]` requires the field type to be \
3807                 wrapped in `Auto<...>` so the macro skips the column on \
3808                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
3809            ));
3810        }
3811    }
3812    if attrs.auto_now_add || attrs.auto_now {
3813        if kind != DetectedKind::DateTime {
3814            return Err(syn::Error::new_spanned(
3815                field,
3816                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
3817                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
3818            ));
3819        }
3820        if !detected_auto {
3821            return Err(syn::Error::new_spanned(
3822                field,
3823                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
3824                 the field type to be wrapped in `Auto<...>` so the macro skips \
3825                 the column on INSERT and the DB DEFAULT (`now()`) fires",
3826            ));
3827        }
3828    }
3829    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
3830        return Err(syn::Error::new_spanned(
3831            field,
3832            "`#[rustango(soft_delete)]` requires the field type to be \
3833             `Option<chrono::DateTime<chrono::Utc>>`",
3834        ));
3835    }
3836    let is_mixin_auto = attrs.auto_uuid || attrs.auto_now_add || attrs.auto_now;
3837    if detected_auto && !primary_key && !is_mixin_auto {
3838        return Err(syn::Error::new_spanned(
3839            field,
3840            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
3841             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
3842             `auto_now`",
3843        ));
3844    }
3845    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
3846        return Err(syn::Error::new_spanned(
3847            field,
3848            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
3849             SERIAL / BIGSERIAL already supplies a default sequence.",
3850        ));
3851    }
3852    if fk_inner.is_some() && primary_key {
3853        return Err(syn::Error::new_spanned(
3854            field,
3855            "`ForeignKey<T>` is not allowed on a primary-key field — \
3856             a row's PK is its own identity, not a reference to a parent.",
3857        ));
3858    }
3859    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
3860    let column_lit = column.as_str();
3861    let field_type_tokens = kind.variant_tokens();
3862    let max_length = optional_u32(attrs.max_length);
3863    let min = optional_i64(attrs.min);
3864    let max = optional_i64(attrs.max);
3865    let default = optional_str(attrs.default.as_deref());
3866
3867    let unique = attrs.unique;
3868    let schema = quote! {
3869        ::rustango::core::FieldSchema {
3870            name: #name,
3871            column: #column_lit,
3872            ty: #field_type_tokens,
3873            nullable: #nullable,
3874            primary_key: #primary_key,
3875            relation: #relation,
3876            max_length: #max_length,
3877            min: #min,
3878            max: #max,
3879            default: #default,
3880            auto: #auto,
3881            unique: #unique,
3882        }
3883    };
3884
3885    let from_row_init = quote! {
3886        #ident: ::rustango::sql::sqlx::Row::try_get(row, #column_lit)?
3887    };
3888    let from_aliased_row_init = quote! {
3889        #ident: ::rustango::sql::sqlx::Row::try_get(
3890            row,
3891            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
3892        )?
3893    };
3894
3895    Ok(FieldInfo {
3896        ident,
3897        column,
3898        primary_key,
3899        auto,
3900        value_ty: &field.ty,
3901        field_type_tokens,
3902        schema,
3903        from_row_init,
3904        from_aliased_row_init,
3905        fk_inner: fk_inner.cloned(),
3906        auto_now: attrs.auto_now,
3907        auto_now_add: attrs.auto_now_add,
3908        soft_delete: attrs.soft_delete,
3909    })
3910}
3911
3912fn check_bound_compatibility(
3913    field: &syn::Field,
3914    attrs: &FieldAttrs,
3915    kind: DetectedKind,
3916) -> syn::Result<()> {
3917    if attrs.max_length.is_some() && kind != DetectedKind::String {
3918        return Err(syn::Error::new_spanned(
3919            field,
3920            "`max_length` is only valid on `String` fields (or `Option<String>`)",
3921        ));
3922    }
3923    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
3924        return Err(syn::Error::new_spanned(
3925            field,
3926            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
3927        ));
3928    }
3929    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
3930        if min > max {
3931            return Err(syn::Error::new_spanned(
3932                field,
3933                format!("`min` ({min}) is greater than `max` ({max})"),
3934            ));
3935        }
3936    }
3937    Ok(())
3938}
3939
3940fn optional_u32(value: Option<u32>) -> TokenStream2 {
3941    if let Some(v) = value {
3942        quote!(::core::option::Option::Some(#v))
3943    } else {
3944        quote!(::core::option::Option::None)
3945    }
3946}
3947
3948fn optional_i64(value: Option<i64>) -> TokenStream2 {
3949    if let Some(v) = value {
3950        quote!(::core::option::Option::Some(#v))
3951    } else {
3952        quote!(::core::option::Option::None)
3953    }
3954}
3955
3956fn optional_str(value: Option<&str>) -> TokenStream2 {
3957    if let Some(v) = value {
3958        quote!(::core::option::Option::Some(#v))
3959    } else {
3960        quote!(::core::option::Option::None)
3961    }
3962}
3963
3964fn relation_tokens(
3965    field: &syn::Field,
3966    attrs: &FieldAttrs,
3967    fk_inner: Option<&syn::Type>,
3968    table: &str,
3969) -> syn::Result<TokenStream2> {
3970    if let Some(inner) = fk_inner {
3971        if attrs.fk.is_some() || attrs.o2o.is_some() {
3972            return Err(syn::Error::new_spanned(
3973                field,
3974                "`ForeignKey<T>` already declares the FK target via the type parameter — \
3975                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
3976            ));
3977        }
3978        let on = attrs.on.as_deref().unwrap_or("id");
3979        return Ok(quote! {
3980            ::core::option::Option::Some(::rustango::core::Relation::Fk {
3981                to: <#inner as ::rustango::core::Model>::SCHEMA.table,
3982                on: #on,
3983            })
3984        });
3985    }
3986    match (&attrs.fk, &attrs.o2o) {
3987        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
3988            field,
3989            "`fk` and `o2o` are mutually exclusive",
3990        )),
3991        (Some(to), None) => {
3992            let on = attrs.on.as_deref().unwrap_or("id");
3993            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
3994            // the model's own table. Threaded as a literal string at
3995            // macro-expansion time to sidestep the const-eval cycle
3996            // that `Self::SCHEMA.table` would create when referenced
3997            // inside Self::SCHEMA's own initializer.
3998            let resolved = if to == "self" { table } else { to };
3999            Ok(quote! {
4000                ::core::option::Option::Some(::rustango::core::Relation::Fk { to: #resolved, on: #on })
4001            })
4002        }
4003        (None, Some(to)) => {
4004            let on = attrs.on.as_deref().unwrap_or("id");
4005            let resolved = if to == "self" { table } else { to };
4006            Ok(quote! {
4007                ::core::option::Option::Some(::rustango::core::Relation::O2O { to: #resolved, on: #on })
4008            })
4009        }
4010        (None, None) => {
4011            if attrs.on.is_some() {
4012                return Err(syn::Error::new_spanned(
4013                    field,
4014                    "`on` requires `fk` or `o2o`",
4015                ));
4016            }
4017            Ok(quote!(::core::option::Option::None))
4018        }
4019    }
4020}
4021
4022/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
4023/// about kinds without depending on `rustango-core` (which would require a
4024/// proc-macro/normal split it doesn't have today).
4025#[derive(Clone, Copy, PartialEq, Eq)]
4026enum DetectedKind {
4027    I32,
4028    I64,
4029    F32,
4030    F64,
4031    Bool,
4032    String,
4033    DateTime,
4034    Date,
4035    Uuid,
4036    Json,
4037}
4038
4039impl DetectedKind {
4040    fn variant_tokens(self) -> TokenStream2 {
4041        match self {
4042            Self::I32 => quote!(::rustango::core::FieldType::I32),
4043            Self::I64 => quote!(::rustango::core::FieldType::I64),
4044            Self::F32 => quote!(::rustango::core::FieldType::F32),
4045            Self::F64 => quote!(::rustango::core::FieldType::F64),
4046            Self::Bool => quote!(::rustango::core::FieldType::Bool),
4047            Self::String => quote!(::rustango::core::FieldType::String),
4048            Self::DateTime => quote!(::rustango::core::FieldType::DateTime),
4049            Self::Date => quote!(::rustango::core::FieldType::Date),
4050            Self::Uuid => quote!(::rustango::core::FieldType::Uuid),
4051            Self::Json => quote!(::rustango::core::FieldType::Json),
4052        }
4053    }
4054
4055    fn is_integer(self) -> bool {
4056        matches!(self, Self::I32 | Self::I64)
4057    }
4058}
4059
4060/// Result of walking a field's Rust type. `kind` is the underlying
4061/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
4062/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
4063/// `Some(<T>)` when the field was `ForeignKey<T>` (or
4064/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
4065#[derive(Clone, Copy)]
4066struct DetectedType<'a> {
4067    kind: DetectedKind,
4068    nullable: bool,
4069    auto: bool,
4070    fk_inner: Option<&'a syn::Type>,
4071}
4072
4073/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
4074/// non-`Auto` types — the caller should already have routed Auto-only
4075/// codegen through this helper, so a `None` indicates a macro-internal
4076/// invariant break.
4077fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
4078    let Type::Path(TypePath { path, qself: None }) = ty else {
4079        return None;
4080    };
4081    let last = path.segments.last()?;
4082    if last.ident != "Auto" {
4083        return None;
4084    }
4085    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
4086        return None;
4087    };
4088    args.args.iter().find_map(|a| match a {
4089        syn::GenericArgument::Type(t) => Some(t),
4090        _ => None,
4091    })
4092}
4093
4094fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
4095    let Type::Path(TypePath { path, qself: None }) = ty else {
4096        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
4097    };
4098    let last = path
4099        .segments
4100        .last()
4101        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
4102
4103    if last.ident == "Option" {
4104        let inner = generic_inner(ty, &last.arguments, "Option")?;
4105        let inner_det = detect_type(inner)?;
4106        if inner_det.nullable {
4107            return Err(syn::Error::new_spanned(
4108                ty,
4109                "nested Option is not supported",
4110            ));
4111        }
4112        if inner_det.auto {
4113            return Err(syn::Error::new_spanned(
4114                ty,
4115                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4116            ));
4117        }
4118        return Ok(DetectedType {
4119            nullable: true,
4120            ..inner_det
4121        });
4122    }
4123
4124    if last.ident == "Auto" {
4125        let inner = generic_inner(ty, &last.arguments, "Auto")?;
4126        let inner_det = detect_type(inner)?;
4127        if inner_det.auto {
4128            return Err(syn::Error::new_spanned(
4129                ty,
4130                "nested Auto is not supported",
4131            ));
4132        }
4133        if inner_det.nullable {
4134            return Err(syn::Error::new_spanned(
4135                ty,
4136                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
4137            ));
4138        }
4139        if inner_det.fk_inner.is_some() {
4140            return Err(syn::Error::new_spanned(
4141                ty,
4142                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
4143            ));
4144        }
4145        if !matches!(
4146            inner_det.kind,
4147            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
4148        ) {
4149            return Err(syn::Error::new_spanned(
4150                ty,
4151                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
4152                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
4153                 (DEFAULT now())",
4154            ));
4155        }
4156        return Ok(DetectedType {
4157            auto: true,
4158            ..inner_det
4159        });
4160    }
4161
4162    if last.ident == "ForeignKey" {
4163        let inner = generic_inner(ty, &last.arguments, "ForeignKey")?;
4164        // `ForeignKey<T>` is stored as BIGINT — same column shape as
4165        // the v0.1 `i64` + `#[rustango(fk = …)]` form. The macro does
4166        // not recurse into `T` because `T` is a Model struct, not a
4167        // primitive — its identity is opaque to schema detection.
4168        return Ok(DetectedType {
4169            kind: DetectedKind::I64,
4170            nullable: false,
4171            auto: false,
4172            fk_inner: Some(inner),
4173        });
4174    }
4175
4176    let kind = match last.ident.to_string().as_str() {
4177        "i32" => DetectedKind::I32,
4178        "i64" => DetectedKind::I64,
4179        "f32" => DetectedKind::F32,
4180        "f64" => DetectedKind::F64,
4181        "bool" => DetectedKind::Bool,
4182        "String" => DetectedKind::String,
4183        "DateTime" => DetectedKind::DateTime,
4184        "NaiveDate" => DetectedKind::Date,
4185        "Uuid" => DetectedKind::Uuid,
4186        "Value" => DetectedKind::Json,
4187        other => {
4188            return Err(syn::Error::new_spanned(
4189                ty,
4190                format!("unsupported field type `{other}`; v0.1 supports i32/i64/f32/f64/bool/String/DateTime/NaiveDate/Uuid/serde_json::Value, optionally wrapped in Option or Auto (Auto only on integers)"),
4191            ));
4192        }
4193    };
4194    Ok(DetectedType {
4195        kind,
4196        nullable: false,
4197        auto: false,
4198        fk_inner: None,
4199    })
4200}
4201
4202fn generic_inner<'a>(
4203    ty: &'a Type,
4204    arguments: &'a PathArguments,
4205    wrapper: &str,
4206) -> syn::Result<&'a Type> {
4207    let PathArguments::AngleBracketed(args) = arguments else {
4208        return Err(syn::Error::new_spanned(
4209            ty,
4210            format!("{wrapper} requires a generic argument"),
4211        ));
4212    };
4213    args.args
4214        .iter()
4215        .find_map(|a| match a {
4216            GenericArgument::Type(t) => Some(t),
4217            _ => None,
4218        })
4219        .ok_or_else(|| {
4220            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
4221        })
4222}
4223
4224fn to_snake_case(s: &str) -> String {
4225    let mut out = String::with_capacity(s.len() + 4);
4226    for (i, ch) in s.chars().enumerate() {
4227        if ch.is_ascii_uppercase() {
4228            if i > 0 {
4229                out.push('_');
4230            }
4231            out.push(ch.to_ascii_lowercase());
4232        } else {
4233            out.push(ch);
4234        }
4235    }
4236    out
4237}
4238
4239// ============================================================
4240//  #[derive(Form)]  —  slice 8.4B
4241// ============================================================
4242
4243/// Per-field `#[form(...)]` attributes recognised by the derive.
4244#[derive(Default)]
4245struct FormFieldAttrs {
4246    min: Option<i64>,
4247    max: Option<i64>,
4248    min_length: Option<u32>,
4249    max_length: Option<u32>,
4250}
4251
4252/// Detected shape of a form field's Rust type.
4253#[derive(Clone, Copy)]
4254enum FormFieldKind {
4255    String,
4256    I32,
4257    I64,
4258    F32,
4259    F64,
4260    Bool,
4261}
4262
4263impl FormFieldKind {
4264    fn parse_method(self) -> &'static str {
4265        match self {
4266            Self::I32 => "i32",
4267            Self::I64 => "i64",
4268            Self::F32 => "f32",
4269            Self::F64 => "f64",
4270            // String + Bool don't go through `str::parse`; the codegen
4271            // handles them inline.
4272            Self::String | Self::Bool => "",
4273        }
4274    }
4275}
4276
4277fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
4278    let struct_name = &input.ident;
4279
4280    let Data::Struct(data) = &input.data else {
4281        return Err(syn::Error::new_spanned(
4282            struct_name,
4283            "Form can only be derived on structs",
4284        ));
4285    };
4286    let Fields::Named(named) = &data.fields else {
4287        return Err(syn::Error::new_spanned(
4288            struct_name,
4289            "Form requires a struct with named fields",
4290        ));
4291    };
4292
4293    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
4294    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
4295
4296    for field in &named.named {
4297        let ident = field
4298            .ident
4299            .as_ref()
4300            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
4301        let attrs = parse_form_field_attrs(field)?;
4302        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
4303
4304        let name_lit = ident.to_string();
4305        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
4306        field_blocks.push(parse_block);
4307        field_idents.push(ident);
4308    }
4309
4310    Ok(quote! {
4311        impl ::rustango::forms::Form for #struct_name {
4312            fn parse(
4313                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
4314            ) -> ::core::result::Result<Self, ::rustango::forms::FormErrors> {
4315                let mut __errors = ::rustango::forms::FormErrors::default();
4316                #( #field_blocks )*
4317                if !__errors.is_empty() {
4318                    return ::core::result::Result::Err(__errors);
4319                }
4320                ::core::result::Result::Ok(Self {
4321                    #( #field_idents ),*
4322                })
4323            }
4324        }
4325    })
4326}
4327
4328fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
4329    let mut out = FormFieldAttrs::default();
4330    for attr in &field.attrs {
4331        if !attr.path().is_ident("form") {
4332            continue;
4333        }
4334        attr.parse_nested_meta(|meta| {
4335            if meta.path.is_ident("min") {
4336                let lit: syn::LitInt = meta.value()?.parse()?;
4337                out.min = Some(lit.base10_parse::<i64>()?);
4338                return Ok(());
4339            }
4340            if meta.path.is_ident("max") {
4341                let lit: syn::LitInt = meta.value()?.parse()?;
4342                out.max = Some(lit.base10_parse::<i64>()?);
4343                return Ok(());
4344            }
4345            if meta.path.is_ident("min_length") {
4346                let lit: syn::LitInt = meta.value()?.parse()?;
4347                out.min_length = Some(lit.base10_parse::<u32>()?);
4348                return Ok(());
4349            }
4350            if meta.path.is_ident("max_length") {
4351                let lit: syn::LitInt = meta.value()?.parse()?;
4352                out.max_length = Some(lit.base10_parse::<u32>()?);
4353                return Ok(());
4354            }
4355            Err(meta.error(
4356                "unknown form attribute (supported: `min`, `max`, `min_length`, `max_length`)",
4357            ))
4358        })?;
4359    }
4360    Ok(out)
4361}
4362
4363fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
4364    let Type::Path(TypePath { path, qself: None }) = ty else {
4365        return Err(syn::Error::new(
4366            span,
4367            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
4368        ));
4369    };
4370    let last = path
4371        .segments
4372        .last()
4373        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
4374
4375    if last.ident == "Option" {
4376        let inner = generic_inner(ty, &last.arguments, "Option")?;
4377        let (kind, nested) = detect_form_field(inner, span)?;
4378        if nested {
4379            return Err(syn::Error::new(
4380                span,
4381                "nested Option in Form fields is not supported",
4382            ));
4383        }
4384        return Ok((kind, true));
4385    }
4386
4387    let kind = match last.ident.to_string().as_str() {
4388        "String" => FormFieldKind::String,
4389        "i32" => FormFieldKind::I32,
4390        "i64" => FormFieldKind::I64,
4391        "f32" => FormFieldKind::F32,
4392        "f64" => FormFieldKind::F64,
4393        "bool" => FormFieldKind::Bool,
4394        other => {
4395            return Err(syn::Error::new(
4396                span,
4397                format!(
4398                    "Form field type `{other}` is not supported in v0.8 — use String / \
4399                     i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
4400                ),
4401            ));
4402        }
4403    };
4404    Ok((kind, false))
4405}
4406
4407#[allow(clippy::too_many_lines)]
4408fn render_form_field_parse(
4409    ident: &syn::Ident,
4410    name_lit: &str,
4411    kind: FormFieldKind,
4412    nullable: bool,
4413    attrs: &FormFieldAttrs,
4414) -> TokenStream2 {
4415    // Pull the raw &str from the payload. Uses variable name `data` to
4416    // match the new `Form::parse(data: &HashMap<…>)` signature.
4417    let lookup = quote! {
4418        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
4419    };
4420
4421    let parsed_value = match kind {
4422        FormFieldKind::Bool => quote! {
4423            let __v: bool = match __raw {
4424                ::core::option::Option::None => false,
4425                ::core::option::Option::Some(__s) => !matches!(
4426                    __s.to_ascii_lowercase().as_str(),
4427                    "" | "false" | "0" | "off" | "no"
4428                ),
4429            };
4430        },
4431        FormFieldKind::String => {
4432            if nullable {
4433                quote! {
4434                    let __v: ::core::option::Option<::std::string::String> = match __raw {
4435                        ::core::option::Option::None => ::core::option::Option::None,
4436                        ::core::option::Option::Some(__s) if __s.is_empty() => {
4437                            ::core::option::Option::None
4438                        }
4439                        ::core::option::Option::Some(__s) => {
4440                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
4441                        }
4442                    };
4443                }
4444            } else {
4445                quote! {
4446                    let __v: ::std::string::String = match __raw {
4447                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
4448                            ::core::clone::Clone::clone(__s)
4449                        }
4450                        _ => {
4451                            __errors.add(#name_lit, "This field is required.");
4452                            ::std::string::String::new()
4453                        }
4454                    };
4455                }
4456            }
4457        }
4458        FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64 => {
4459            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
4460            let ty_lit = kind.parse_method();
4461            let default_val = match kind {
4462                FormFieldKind::I32 => quote! { 0i32 },
4463                FormFieldKind::I64 => quote! { 0i64 },
4464                FormFieldKind::F32 => quote! { 0f32 },
4465                FormFieldKind::F64 => quote! { 0f64 },
4466                _ => quote! { Default::default() },
4467            };
4468            if nullable {
4469                quote! {
4470                    let __v: ::core::option::Option<#parse_ty> = match __raw {
4471                        ::core::option::Option::None => ::core::option::Option::None,
4472                        ::core::option::Option::Some(__s) if __s.is_empty() => {
4473                            ::core::option::Option::None
4474                        }
4475                        ::core::option::Option::Some(__s) => {
4476                            match __s.parse::<#parse_ty>() {
4477                                ::core::result::Result::Ok(__n) => {
4478                                    ::core::option::Option::Some(__n)
4479                                }
4480                                ::core::result::Result::Err(__e) => {
4481                                    __errors.add(
4482                                        #name_lit,
4483                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4484                                    );
4485                                    ::core::option::Option::None
4486                                }
4487                            }
4488                        }
4489                    };
4490                }
4491            } else {
4492                quote! {
4493                    let __v: #parse_ty = match __raw {
4494                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
4495                            match __s.parse::<#parse_ty>() {
4496                                ::core::result::Result::Ok(__n) => __n,
4497                                ::core::result::Result::Err(__e) => {
4498                                    __errors.add(
4499                                        #name_lit,
4500                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
4501                                    );
4502                                    #default_val
4503                                }
4504                            }
4505                        }
4506                        _ => {
4507                            __errors.add(#name_lit, "This field is required.");
4508                            #default_val
4509                        }
4510                    };
4511                }
4512            }
4513        }
4514    };
4515
4516    let validators = render_form_validators(name_lit, kind, nullable, attrs);
4517
4518    quote! {
4519        let #ident = {
4520            #lookup
4521            #parsed_value
4522            #validators
4523            __v
4524        };
4525    }
4526}
4527
4528fn render_form_validators(
4529    name_lit: &str,
4530    kind: FormFieldKind,
4531    nullable: bool,
4532    attrs: &FormFieldAttrs,
4533) -> TokenStream2 {
4534    let mut checks: Vec<TokenStream2> = Vec::new();
4535
4536    let val_ref = if nullable {
4537        quote! { __v.as_ref() }
4538    } else {
4539        quote! { ::core::option::Option::Some(&__v) }
4540    };
4541
4542    let is_string = matches!(kind, FormFieldKind::String);
4543    let is_numeric = matches!(
4544        kind,
4545        FormFieldKind::I32 | FormFieldKind::I64 | FormFieldKind::F32 | FormFieldKind::F64
4546    );
4547
4548    if is_string {
4549        if let Some(min_len) = attrs.min_length {
4550            let min_len_usize = min_len as usize;
4551            checks.push(quote! {
4552                if let ::core::option::Option::Some(__s) = #val_ref {
4553                    if __s.len() < #min_len_usize {
4554                        __errors.add(
4555                            #name_lit,
4556                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
4557                        );
4558                    }
4559                }
4560            });
4561        }
4562        if let Some(max_len) = attrs.max_length {
4563            let max_len_usize = max_len as usize;
4564            checks.push(quote! {
4565                if let ::core::option::Option::Some(__s) = #val_ref {
4566                    if __s.len() > #max_len_usize {
4567                        __errors.add(
4568                            #name_lit,
4569                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
4570                        );
4571                    }
4572                }
4573            });
4574        }
4575    }
4576
4577    if is_numeric {
4578        if let Some(min) = attrs.min {
4579            checks.push(quote! {
4580                if let ::core::option::Option::Some(__n) = #val_ref {
4581                    if (*__n as f64) < (#min as f64) {
4582                        __errors.add(
4583                            #name_lit,
4584                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
4585                        );
4586                    }
4587                }
4588            });
4589        }
4590        if let Some(max) = attrs.max {
4591            checks.push(quote! {
4592                if let ::core::option::Option::Some(__n) = #val_ref {
4593                    if (*__n as f64) > (#max as f64) {
4594                        __errors.add(
4595                            #name_lit,
4596                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
4597                        );
4598                    }
4599                }
4600            });
4601        }
4602    }
4603
4604    quote! { #( #checks )* }
4605}
4606
4607// ============================================================
4608//  #[derive(ViewSet)]
4609// ============================================================
4610
4611struct ViewSetAttrs {
4612    model: syn::Path,
4613    fields: Option<Vec<String>>,
4614    filter_fields: Vec<String>,
4615    search_fields: Vec<String>,
4616    /// (field_name, desc)
4617    ordering: Vec<(String, bool)>,
4618    page_size: Option<usize>,
4619    read_only: bool,
4620    perms: ViewSetPermsAttrs,
4621}
4622
4623#[derive(Default)]
4624struct ViewSetPermsAttrs {
4625    list: Vec<String>,
4626    retrieve: Vec<String>,
4627    create: Vec<String>,
4628    update: Vec<String>,
4629    destroy: Vec<String>,
4630}
4631
4632fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
4633    let struct_name = &input.ident;
4634
4635    // Must be a unit struct or an empty named struct.
4636    match &input.data {
4637        Data::Struct(s) => match &s.fields {
4638            Fields::Unit | Fields::Named(_) => {}
4639            Fields::Unnamed(_) => {
4640                return Err(syn::Error::new_spanned(
4641                    struct_name,
4642                    "ViewSet can only be derived on a unit struct or an empty named struct",
4643                ));
4644            }
4645        },
4646        _ => {
4647            return Err(syn::Error::new_spanned(
4648                struct_name,
4649                "ViewSet can only be derived on a struct",
4650            ));
4651        }
4652    }
4653
4654    let attrs = parse_viewset_attrs(input)?;
4655    let model_path = &attrs.model;
4656
4657    // `.fields(&[...])` call — None means skip (use all scalar fields).
4658    let fields_call = if let Some(ref fields) = attrs.fields {
4659        let lits = fields.iter().map(|f| f.as_str());
4660        quote!(.fields(&[ #(#lits),* ]))
4661    } else {
4662        quote!()
4663    };
4664
4665    let filter_fields_call = if attrs.filter_fields.is_empty() {
4666        quote!()
4667    } else {
4668        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
4669        quote!(.filter_fields(&[ #(#lits),* ]))
4670    };
4671
4672    let search_fields_call = if attrs.search_fields.is_empty() {
4673        quote!()
4674    } else {
4675        let lits = attrs.search_fields.iter().map(|f| f.as_str());
4676        quote!(.search_fields(&[ #(#lits),* ]))
4677    };
4678
4679    let ordering_call = if attrs.ordering.is_empty() {
4680        quote!()
4681    } else {
4682        let pairs = attrs.ordering.iter().map(|(f, desc)| {
4683            let f = f.as_str();
4684            quote!((#f, #desc))
4685        });
4686        quote!(.ordering(&[ #(#pairs),* ]))
4687    };
4688
4689    let page_size_call = if let Some(n) = attrs.page_size {
4690        quote!(.page_size(#n))
4691    } else {
4692        quote!()
4693    };
4694
4695    let read_only_call = if attrs.read_only {
4696        quote!(.read_only())
4697    } else {
4698        quote!()
4699    };
4700
4701    let perms = &attrs.perms;
4702    let perms_call = if perms.list.is_empty()
4703        && perms.retrieve.is_empty()
4704        && perms.create.is_empty()
4705        && perms.update.is_empty()
4706        && perms.destroy.is_empty()
4707    {
4708        quote!()
4709    } else {
4710        let list_lits = perms.list.iter().map(|s| s.as_str());
4711        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
4712        let create_lits = perms.create.iter().map(|s| s.as_str());
4713        let update_lits = perms.update.iter().map(|s| s.as_str());
4714        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
4715        quote! {
4716            .permissions(::rustango::viewset::ViewSetPerms {
4717                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
4718                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
4719                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
4720                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
4721                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
4722            })
4723        }
4724    };
4725
4726    Ok(quote! {
4727        impl #struct_name {
4728            /// Build an `axum::Router` with the six standard REST endpoints
4729            /// for this ViewSet, mounted at `prefix`.
4730            pub fn router(prefix: &str, pool: ::rustango::sql::sqlx::PgPool) -> ::axum::Router {
4731                ::rustango::viewset::ViewSet::for_model(#model_path::SCHEMA)
4732                    #fields_call
4733                    #filter_fields_call
4734                    #search_fields_call
4735                    #ordering_call
4736                    #page_size_call
4737                    #perms_call
4738                    #read_only_call
4739                    .router(prefix, pool)
4740            }
4741        }
4742    })
4743}
4744
4745fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
4746    let mut model: Option<syn::Path> = None;
4747    let mut fields: Option<Vec<String>> = None;
4748    let mut filter_fields: Vec<String> = Vec::new();
4749    let mut search_fields: Vec<String> = Vec::new();
4750    let mut ordering: Vec<(String, bool)> = Vec::new();
4751    let mut page_size: Option<usize> = None;
4752    let mut read_only = false;
4753    let mut perms = ViewSetPermsAttrs::default();
4754
4755    for attr in &input.attrs {
4756        if !attr.path().is_ident("viewset") {
4757            continue;
4758        }
4759        attr.parse_nested_meta(|meta| {
4760            if meta.path.is_ident("model") {
4761                let path: syn::Path = meta.value()?.parse()?;
4762                model = Some(path);
4763                return Ok(());
4764            }
4765            if meta.path.is_ident("fields") {
4766                let s: LitStr = meta.value()?.parse()?;
4767                fields = Some(split_field_list(&s.value()));
4768                return Ok(());
4769            }
4770            if meta.path.is_ident("filter_fields") {
4771                let s: LitStr = meta.value()?.parse()?;
4772                filter_fields = split_field_list(&s.value());
4773                return Ok(());
4774            }
4775            if meta.path.is_ident("search_fields") {
4776                let s: LitStr = meta.value()?.parse()?;
4777                search_fields = split_field_list(&s.value());
4778                return Ok(());
4779            }
4780            if meta.path.is_ident("ordering") {
4781                let s: LitStr = meta.value()?.parse()?;
4782                ordering = parse_ordering_list(&s.value());
4783                return Ok(());
4784            }
4785            if meta.path.is_ident("page_size") {
4786                let lit: syn::LitInt = meta.value()?.parse()?;
4787                page_size = Some(lit.base10_parse::<usize>()?);
4788                return Ok(());
4789            }
4790            if meta.path.is_ident("read_only") {
4791                read_only = true;
4792                return Ok(());
4793            }
4794            if meta.path.is_ident("permissions") {
4795                meta.parse_nested_meta(|inner| {
4796                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
4797                        let s: LitStr = inner.value()?.parse()?;
4798                        Ok(split_field_list(&s.value()))
4799                    };
4800                    if inner.path.is_ident("list") {
4801                        perms.list = parse_codenames(&inner)?;
4802                    } else if inner.path.is_ident("retrieve") {
4803                        perms.retrieve = parse_codenames(&inner)?;
4804                    } else if inner.path.is_ident("create") {
4805                        perms.create = parse_codenames(&inner)?;
4806                    } else if inner.path.is_ident("update") {
4807                        perms.update = parse_codenames(&inner)?;
4808                    } else if inner.path.is_ident("destroy") {
4809                        perms.destroy = parse_codenames(&inner)?;
4810                    } else {
4811                        return Err(inner.error(
4812                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
4813                        ));
4814                    }
4815                    Ok(())
4816                })?;
4817                return Ok(());
4818            }
4819            Err(meta.error(
4820                "unknown viewset attribute (supported: model, fields, filter_fields, \
4821                 search_fields, ordering, page_size, read_only, permissions(...))",
4822            ))
4823        })?;
4824    }
4825
4826    let model = model.ok_or_else(|| {
4827        syn::Error::new_spanned(
4828            &input.ident,
4829            "`#[viewset(model = SomeModel)]` is required",
4830        )
4831    })?;
4832
4833    Ok(ViewSetAttrs {
4834        model,
4835        fields,
4836        filter_fields,
4837        search_fields,
4838        ordering,
4839        page_size,
4840        read_only,
4841        perms,
4842    })
4843}
4844
4845// ============================================================ #[derive(Serializer)]
4846
4847struct SerializerContainerAttrs {
4848    model: syn::Path,
4849}
4850
4851#[derive(Default)]
4852struct SerializerFieldAttrs {
4853    read_only: bool,
4854    write_only: bool,
4855    source: Option<String>,
4856    skip: bool,
4857    /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
4858    /// analog. The macro emits `from_model` initializer that calls
4859    /// `Self::fn_name(&model)` and stores the return value.
4860    method: Option<String>,
4861    /// `#[serializer(validate = "fn_name")]` — per-field validator
4862    /// callable run by `Self::validate(&self)`. Must return
4863    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
4864    /// the field name.
4865    validate: Option<String>,
4866    /// `#[serializer(nested)]` on a field whose type is another
4867    /// `Serializer` — the macro emits `from_model` initializer that
4868    /// reads the parent via `model.<source>.value()` then calls the
4869    /// child serializer's `from_model(parent)`. When the FK is
4870    /// unloaded the field falls back to `Default::default()` (does
4871    /// NOT panic) so a missing prefetch in prod degrades gracefully.
4872    /// Source field on the model defaults to the field name; override
4873    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
4874    /// panic-on-unloaded behavior for tests.
4875    nested: bool,
4876    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
4877    /// strict behavior: panic when the FK isn't loaded. Useful in
4878    /// test code where forgetting select_related must trip a hard
4879    /// failure rather than render a blank nested object.
4880    nested_strict: bool,
4881    /// `#[serializer(many = TagSerializer)]` — declare the field as
4882    /// a list of nested serializers. Field type must be `Vec<S>`
4883    /// where `S` is the inner serializer. The macro initializes the
4884    /// field to `Vec::new()` in `from_model` and emits a typed
4885    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
4886    /// maps each model row through `S::from_model`. Auto-load isn't
4887    /// possible (the M2M / one-to-many accessor is async); callers
4888    /// fetch the children + call the setter post-from_model.
4889    many: Option<syn::Type>,
4890}
4891
4892fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
4893    let mut model: Option<syn::Path> = None;
4894    for attr in &input.attrs {
4895        if !attr.path().is_ident("serializer") {
4896            continue;
4897        }
4898        attr.parse_nested_meta(|meta| {
4899            if meta.path.is_ident("model") {
4900                let _eq: syn::Token![=] = meta.input.parse()?;
4901                model = Some(meta.input.parse()?);
4902                return Ok(());
4903            }
4904            Err(meta.error("unknown serializer container attribute (supported: `model`)"))
4905        })?;
4906    }
4907    let model = model.ok_or_else(|| {
4908        syn::Error::new_spanned(
4909            &input.ident,
4910            "`#[serializer(model = SomeModel)]` is required",
4911        )
4912    })?;
4913    Ok(SerializerContainerAttrs { model })
4914}
4915
4916fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
4917    let mut out = SerializerFieldAttrs::default();
4918    for attr in &field.attrs {
4919        if !attr.path().is_ident("serializer") {
4920            continue;
4921        }
4922        attr.parse_nested_meta(|meta| {
4923            if meta.path.is_ident("read_only") {
4924                out.read_only = true;
4925                return Ok(());
4926            }
4927            if meta.path.is_ident("write_only") {
4928                out.write_only = true;
4929                return Ok(());
4930            }
4931            if meta.path.is_ident("skip") {
4932                out.skip = true;
4933                return Ok(());
4934            }
4935            if meta.path.is_ident("source") {
4936                let s: LitStr = meta.value()?.parse()?;
4937                out.source = Some(s.value());
4938                return Ok(());
4939            }
4940            if meta.path.is_ident("method") {
4941                let s: LitStr = meta.value()?.parse()?;
4942                out.method = Some(s.value());
4943                return Ok(());
4944            }
4945            if meta.path.is_ident("validate") {
4946                let s: LitStr = meta.value()?.parse()?;
4947                out.validate = Some(s.value());
4948                return Ok(());
4949            }
4950            if meta.path.is_ident("many") {
4951                let _eq: syn::Token![=] = meta.input.parse()?;
4952                out.many = Some(meta.input.parse()?);
4953                return Ok(());
4954            }
4955            if meta.path.is_ident("nested") {
4956                out.nested = true;
4957                // Optional strict flag inside parentheses:
4958                //   #[serializer(nested(strict))]
4959                if meta.input.peek(syn::token::Paren) {
4960                    meta.parse_nested_meta(|inner| {
4961                        if inner.path.is_ident("strict") {
4962                            out.nested_strict = true;
4963                            return Ok(());
4964                        }
4965                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
4966                    })?;
4967                }
4968                return Ok(());
4969            }
4970            Err(meta.error(
4971                "unknown serializer field attribute (supported: \
4972                 `read_only`, `write_only`, `source`, `skip`, `method`, `validate`, `nested`)",
4973            ))
4974        })?;
4975    }
4976    // Validate: read_only + write_only is nonsensical
4977    if out.read_only && out.write_only {
4978        return Err(syn::Error::new_spanned(
4979            field,
4980            "a field cannot be both `read_only` and `write_only`",
4981        ));
4982    }
4983    if out.method.is_some() && out.source.is_some() {
4984        return Err(syn::Error::new_spanned(
4985            field,
4986            "`method` and `source` are mutually exclusive — `method` computes \
4987             the value from a method, `source` reads it from a different model field",
4988        ));
4989    }
4990    Ok(out)
4991}
4992
4993fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
4994    let struct_name = &input.ident;
4995    let struct_name_lit = struct_name.to_string();
4996
4997    let Data::Struct(data) = &input.data else {
4998        return Err(syn::Error::new_spanned(
4999            struct_name,
5000            "Serializer can only be derived on structs",
5001        ));
5002    };
5003    let Fields::Named(named) = &data.fields else {
5004        return Err(syn::Error::new_spanned(
5005            struct_name,
5006            "Serializer requires a struct with named fields",
5007        ));
5008    };
5009
5010    let container = parse_serializer_container_attrs(input)?;
5011    let model_path = &container.model;
5012
5013    // Classify each field. `ty` is only consumed by the
5014    // `#[cfg(feature = "openapi")]` block below, but we always
5015    // capture it to keep the field-info build a single pass.
5016    #[allow(dead_code)]
5017    struct FieldInfo {
5018        ident: syn::Ident,
5019        ty: syn::Type,
5020        attrs: SerializerFieldAttrs,
5021    }
5022    let mut fields_info: Vec<FieldInfo> = Vec::new();
5023    for field in &named.named {
5024        let ident = field.ident.clone().expect("named field has ident");
5025        let attrs = parse_serializer_field_attrs(field)?;
5026        fields_info.push(FieldInfo {
5027            ident,
5028            ty: field.ty.clone(),
5029            attrs,
5030        });
5031    }
5032
5033    // Generate from_model body: struct literal with each field assigned.
5034    let from_model_fields = fields_info.iter().map(|fi| {
5035        let ident = &fi.ident;
5036        let ty = &fi.ty;
5037        if let Some(_inner) = &fi.attrs.many {
5038            // Many — collection field. Initialize empty; caller
5039            // populates via the macro-emitted set_<field> helper
5040            // after fetching the M2M children.
5041            quote! { #ident: ::std::vec::Vec::new() }
5042        } else if let Some(method) = &fi.attrs.method {
5043            // SerializerMethodField: call Self::<method>(&model) to
5044            // compute the value. Method signature must be
5045            // `fn <method>(model: &T) -> <field type>`.
5046            let method_ident = syn::Ident::new(method, ident.span());
5047            quote! { #ident: Self::#method_ident(model) }
5048        } else if fi.attrs.nested {
5049            // Nested serializer. Source defaults to the field name on
5050            // this struct; override via `source = "..."`. The source
5051            // field on the model is expected to be a `ForeignKey<T>`
5052            // whose `.value()` returns `Option<&T>` after lazy-load.
5053            //
5054            // Behavior matrix (tweakable per-field):
5055            //   * FK loaded   → nested object materializes via
5056            //                   ChildSerializer::from_model(parent).
5057            //   * FK unloaded → fall back to ChildSerializer::default()
5058            //                   (so prod doesn't crash on a missing
5059            //                   prefetch — just renders a blank nested
5060            //                   object). Add `#[serializer(nested,
5061            //                   strict)]` to keep the v0.18.1
5062            //                   panic-on-unloaded behavior for tests
5063            //                   that want hard guardrails.
5064            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
5065            let src_ident = syn::Ident::new(&src_name, ident.span());
5066            if fi.attrs.nested_strict {
5067                let panic_msg = format!(
5068                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
5069                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
5070                );
5071                quote! {
5072                    #ident: <#ty as ::rustango::serializer::ModelSerializer>::from_model(
5073                        model.#src_ident.value().expect(#panic_msg),
5074                    )
5075                }
5076            } else {
5077                quote! {
5078                    #ident: match model.#src_ident.value() {
5079                        ::core::option::Option::Some(__loaded) =>
5080                            <#ty as ::rustango::serializer::ModelSerializer>::from_model(__loaded),
5081                        ::core::option::Option::None =>
5082                            ::core::default::Default::default(),
5083                    }
5084                }
5085            }
5086        } else if fi.attrs.write_only || fi.attrs.skip {
5087            // Not read from model — use default
5088            quote! { #ident: ::core::default::Default::default() }
5089        } else if let Some(src) = &fi.attrs.source {
5090            let src_ident = syn::Ident::new(src, ident.span());
5091            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
5092        } else {
5093            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
5094        }
5095    });
5096
5097    // Per-field validators (DRF-shape `validators=[...]`). Emit a
5098    // `validate(&self)` method that runs each user-defined validator
5099    // and aggregates errors into `FormErrors`.
5100    let validator_calls: Vec<_> = fields_info.iter().filter_map(|fi| {
5101        let ident = &fi.ident;
5102        let name_lit = ident.to_string();
5103        let method = fi.attrs.validate.as_ref()?;
5104        let method_ident = syn::Ident::new(method, ident.span());
5105        Some(quote! {
5106            if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
5107                __errors.add(#name_lit.to_owned(), __e);
5108            }
5109        })
5110    }).collect();
5111    let validate_method = if validator_calls.is_empty() {
5112        quote! {}
5113    } else {
5114        quote! {
5115            impl #struct_name {
5116                /// Run every `#[serializer(validate = "...")]` per-field
5117                /// validator. Aggregates errors into `FormErrors` keyed
5118                /// by the field name. Returns `Ok(())` when all pass.
5119                pub fn validate(&self) -> ::core::result::Result<(), ::rustango::forms::FormErrors> {
5120                    let mut __errors = ::rustango::forms::FormErrors::default();
5121                    #( #validator_calls )*
5122                    if __errors.is_empty() {
5123                        ::core::result::Result::Ok(())
5124                    } else {
5125                        ::core::result::Result::Err(__errors)
5126                    }
5127                }
5128            }
5129        }
5130    };
5131
5132    // For every `#[serializer(many = S)]` field, emit a
5133    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
5134    // helper that maps the parents through `S::from_model`.
5135    let many_setters: Vec<_> = fields_info.iter().filter_map(|fi| {
5136        let many_ty = fi.attrs.many.as_ref()?;
5137        let ident = &fi.ident;
5138        let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
5139        Some(quote! {
5140            /// Populate this `many` field by mapping each parent model
5141            /// through the inner serializer's `from_model`. Call after
5142            /// fetching the M2M / one-to-many children since
5143            /// `from_model` itself can't await an SQL query.
5144            pub fn #setter(
5145                &mut self,
5146                models: &[<#many_ty as ::rustango::serializer::ModelSerializer>::Model],
5147            ) -> &mut Self {
5148                self.#ident = models.iter()
5149                    .map(<#many_ty as ::rustango::serializer::ModelSerializer>::from_model)
5150                    .collect();
5151                self
5152            }
5153        })
5154    }).collect();
5155    let many_setters_impl = if many_setters.is_empty() {
5156        quote! {}
5157    } else {
5158        quote! {
5159            impl #struct_name {
5160                #( #many_setters )*
5161            }
5162        }
5163    };
5164
5165    // Generate custom Serialize: skip write_only fields
5166    let output_fields: Vec<_> = fields_info
5167        .iter()
5168        .filter(|fi| !fi.attrs.write_only)
5169        .collect();
5170    let output_field_count = output_fields.len();
5171    let serialize_fields = output_fields.iter().map(|fi| {
5172        let ident = &fi.ident;
5173        let name_lit = ident.to_string();
5174        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
5175    });
5176
5177    // writable_fields: normal + write_only (not read_only, not skip)
5178    let writable_lits: Vec<_> = fields_info
5179        .iter()
5180        .filter(|fi| !fi.attrs.read_only && !fi.attrs.skip)
5181        .map(|fi| fi.ident.to_string())
5182        .collect();
5183
5184    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
5185    // Only includes fields shown in JSON output (skips write_only). For each
5186    // `Option<T>` field, omit from `required` and add `.nullable()`.
5187    let openapi_impl = {
5188        #[cfg(feature = "openapi")]
5189        {
5190            let property_calls = output_fields.iter().map(|fi| {
5191                let ident = &fi.ident;
5192                let name_lit = ident.to_string();
5193                let ty = &fi.ty;
5194                let nullable_call = if is_option(ty) {
5195                    quote! { .nullable() }
5196                } else {
5197                    quote! {}
5198                };
5199                quote! {
5200                    .property(
5201                        #name_lit,
5202                        <#ty as ::rustango::openapi::OpenApiSchema>::openapi_schema()
5203                            #nullable_call,
5204                    )
5205                }
5206            });
5207            let required_lits: Vec<_> = output_fields
5208                .iter()
5209                .filter(|fi| !is_option(&fi.ty))
5210                .map(|fi| fi.ident.to_string())
5211                .collect();
5212            quote! {
5213                impl ::rustango::openapi::OpenApiSchema for #struct_name {
5214                    fn openapi_schema() -> ::rustango::openapi::Schema {
5215                        ::rustango::openapi::Schema::object()
5216                            #( #property_calls )*
5217                            .required([ #( #required_lits ),* ])
5218                    }
5219                }
5220            }
5221        }
5222        #[cfg(not(feature = "openapi"))]
5223        {
5224            quote! {}
5225        }
5226    };
5227
5228    Ok(quote! {
5229        impl ::rustango::serializer::ModelSerializer for #struct_name {
5230            type Model = #model_path;
5231
5232            fn from_model(model: &Self::Model) -> Self {
5233                Self {
5234                    #( #from_model_fields ),*
5235                }
5236            }
5237
5238            fn writable_fields() -> &'static [&'static str] {
5239                &[ #( #writable_lits ),* ]
5240            }
5241        }
5242
5243        impl ::serde::Serialize for #struct_name {
5244            fn serialize<S>(&self, serializer: S)
5245                -> ::core::result::Result<S::Ok, S::Error>
5246            where
5247                S: ::serde::Serializer,
5248            {
5249                use ::serde::ser::SerializeStruct;
5250                let mut __state = serializer.serialize_struct(
5251                    #struct_name_lit,
5252                    #output_field_count,
5253                )?;
5254                #( #serialize_fields )*
5255                __state.end()
5256            }
5257        }
5258
5259        #openapi_impl
5260
5261        #validate_method
5262
5263        #many_setters_impl
5264    })
5265}
5266
5267/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
5268/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
5269/// when the feature is off.
5270#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
5271fn is_option(ty: &syn::Type) -> bool {
5272    if let syn::Type::Path(p) = ty {
5273        if let Some(last) = p.path.segments.last() {
5274            return last.ident == "Option";
5275        }
5276    }
5277    false
5278}