Skip to main content

rustango_macros/
lib.rs

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