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