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