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/// Resolve the consumer's local name for the `rustango` crate so the
18/// macro-emitted code keeps compiling when a downstream `Cargo.toml`
19/// renames the dep (`[dependencies] orm = { package = "rustango", … }`)
20/// or when the standalone `rustango-orm` crate ships in a future
21/// slice of the [orm-extract epic](https://github.com/ujeenet/rustango/issues/149).
22///
23/// Returns one of:
24/// - `quote!(::rustango)` — the consumer IS the `rustango` crate
25///   itself. We emit the absolute path `::rustango` (NOT `crate`)
26///   because examples and integration tests inside the rustango
27///   package compile as separate binaries — their own `crate::`
28///   namespace is the example/test file, not rustango's lib root.
29///   The absolute `::rustango` resolves correctly in both contexts
30///   (rustango lib code AND rustango/examples/*.rs).
31/// - `quote!(::ident)` — the consumer renamed the dep; emit the
32///   user's chosen ident.
33/// - `quote!(::rustango)` — fallback when `proc-macro-crate` can't
34///   read the manifest (rare; preserves today's behavior).
35///
36/// Issue [#142](https://github.com/ujeenet/rustango/issues/142).
37fn rustango_root() -> TokenStream2 {
38    use proc_macro_crate::{crate_name, FoundCrate};
39    match crate_name("rustango") {
40        Ok(FoundCrate::Itself) => quote!(::rustango),
41        Ok(FoundCrate::Name(name)) => {
42            let ident = proc_macro2::Ident::new(&name, proc_macro2::Span::call_site());
43            quote!(::#ident)
44        }
45        Err(_) => quote!(::rustango),
46    }
47}
48
49/// Derive a `Model` impl. See crate docs for the supported attributes.
50#[proc_macro_derive(Model, attributes(rustango))]
51pub fn derive_model(input: TokenStream) -> TokenStream {
52    let input = parse_macro_input!(input as DeriveInput);
53    expand(&input)
54        .unwrap_or_else(syn::Error::into_compile_error)
55        .into()
56}
57
58/// Derive a `router(prefix, pool) -> axum::Router` associated method on a
59/// marker struct, wiring the full CRUD ViewSet in one annotation.
60///
61/// ```ignore
62/// #[derive(ViewSet)]
63/// #[viewset(
64///     model        = Post,
65///     fields       = "id, title, body, author_id",
66///     filter_fields = "author_id",
67///     search_fields = "title, body",
68///     ordering     = "-published_at",
69///     page_size    = 20,
70/// )]
71/// pub struct PostViewSet;
72///
73/// // Mount into your app:
74/// let app = Router::new()
75///     .merge(PostViewSet::router("/api/posts", pool.clone()));
76/// ```
77///
78/// Attributes:
79/// * `model = TypeName` — *required*. The `#[derive(Model)]` struct whose
80///   `SCHEMA` constant drives the endpoints.
81/// * `fields = "a, b, c"` — scalar fields included in list/retrieve JSON
82///   and accepted on create/update (default: all scalar fields).
83/// * `filter_fields = "a, b"` — fields filterable via `?a=v` query params.
84/// * `search_fields = "a, b"` — fields searched by `?search=...`.
85/// * `ordering = "a, -b"` — default list ordering; prefix `-` for DESC.
86/// * `page_size = N` — default page size (default: 20, max: 1000).
87/// * `read_only` — flag; wires only `list` + `retrieve` (no mutations).
88/// * `serializer = SomeSerializer` — render list / retrieve / create
89///   responses through a `#[derive(Serializer)]` type instead of the
90///   default field-level projection (`read_only` / `source` / `method`
91///   / `write_only` overrides apply). Tri-dialect. Requires the
92///   `serializer` feature.
93/// * `permissions(list = "...", retrieve = "...", create = "...",
94///   update = "...", destroy = "...")` — codenames required per action.
95#[proc_macro_derive(ViewSet, attributes(viewset))]
96pub fn derive_viewset(input: TokenStream) -> TokenStream {
97    let input = parse_macro_input!(input as DeriveInput);
98    expand_viewset(&input)
99        .unwrap_or_else(syn::Error::into_compile_error)
100        .into()
101}
102
103/// Derive `rustango::forms::Form` (slice 8.4B). Generates a
104/// `parse(&HashMap<String, String>) -> Result<Self, FormErrors>` impl
105/// that walks every named field and:
106///
107/// * Parses the string value into the field's Rust type (`String`,
108///   `i32`, `i64`, `f32`, `f64`, `bool`, plus `Option<T>` for the
109///   nullable case).
110/// * Applies any `#[form(min = ..)]` / `#[form(max = ..)]` /
111///   `#[form(min_length = ..)]` / `#[form(max_length = ..)]`
112///   validators in declaration order, returning `FormError::Parse`
113///   on the first failure.
114///
115/// Example:
116///
117/// ```ignore
118/// #[derive(Form)]
119/// pub struct CreateItemForm {
120///     #[form(min_length = 1, max_length = 64)]
121///     pub name: String,
122///     #[form(min = 0, max = 150)]
123///     pub age: i32,
124///     pub active: bool,
125///     pub email: Option<String>,
126/// }
127///
128/// let parsed = CreateItemForm::parse(&form_map)?;
129/// ```
130#[proc_macro_derive(Form, attributes(form))]
131pub fn derive_form(input: TokenStream) -> TokenStream {
132    let input = parse_macro_input!(input as DeriveInput);
133    expand_form(&input)
134        .unwrap_or_else(syn::Error::into_compile_error)
135        .into()
136}
137
138/// Derive `rustango::serializer::ModelSerializer` for a struct.
139/// (intra-doc link disabled — the macro crate doesn't depend on
140/// `rustango` itself, so rustdoc can't resolve the path.)
141///
142/// # Container attribute (required)
143/// `#[serializer(model = TypeName)]` — the [`Model`] type this serializer maps from.
144///
145/// # Field attributes
146/// - `#[serializer(read_only)]` — mapped from model; included in JSON output; excluded from `writable_fields()`
147/// - `#[serializer(write_only)]` — `Default::default()` in `from_model`; excluded from JSON output; included in `writable_fields()`
148/// - `#[serializer(source = "field_name")]` — reads from `model.field_name` instead of `model.<field_ident>`
149/// - `#[serializer(skip)]` — `Default::default()` in `from_model`; included in JSON output; excluded from `writable_fields()` (user sets manually)
150/// - `#[serializer(method = "fn_name")]` — DRF `SerializerMethodField`: calls `Self::fn_name(&model)` for the field value; excluded from `writable_fields()`
151/// - `#[serializer(nested)]` / `nested(strict)` — auto-resolves nested serializer from a loaded `ForeignKey`; excluded from `writable_fields()`
152/// - `#[serializer(many = ChildSerializer)]` — collection of nested serializers; populated via macro-emitted `set_<field>(&[Child::Model])`; excluded from `writable_fields()`
153/// - `#[serializer(slug = "name")]` — DRF `SlugRelatedField`: clones `model.<source>.value()?.name`; excluded from `writable_fields()` (v0.44)
154/// - `#[serializer(validate = "fn_name")]` — per-field validator surfaced by `Self::validate(&self)`
155/// - `#[serializer(max_length = N)]` / `min_length` / `min` / `max` — declarative bounds checked on
156///   write; auto-inherit from the model's `FieldSchema` (`max_length`/`min`/`max`/`choices`) when
157///   no attr is given, override it when given (`min_length` is serializer-only)
158///
159/// The macro also emits a custom `impl serde::Serialize` — do **not** also `#[derive(Serialize)]`.
160#[proc_macro_derive(Serializer, attributes(serializer))]
161pub fn derive_serializer(input: TokenStream) -> TokenStream {
162    let input = parse_macro_input!(input as DeriveInput);
163    expand_serializer(&input)
164        .unwrap_or_else(syn::Error::into_compile_error)
165        .into()
166}
167
168/// Bake every `*.json` migration file in a directory into the binary
169/// at compile time. Returns a `&'static [(&'static str, &'static str)]`
170/// of `(name, json_content)` pairs, lex-sorted by file stem.
171///
172/// Pair with `rustango::migrate::migrate_embedded` at runtime — same
173/// behaviour as `migrate(pool, dir)` but with no filesystem access.
174/// The path is interpreted relative to the user's `CARGO_MANIFEST_DIR`
175/// (i.e. the crate that invokes the macro). Default is
176/// `"./migrations"` if no argument is supplied.
177///
178/// ```ignore
179/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!();
180/// // or:
181/// const EMBEDDED: &[(&str, &str)] = rustango::embed_migrations!("./migrations");
182///
183/// rustango::migrate::migrate_embedded(&pool, EMBEDDED).await?;
184/// ```
185///
186/// **Compile-time guarantees** (rustango v0.4+, slice 5): every JSON
187/// file's `name` field must equal its file stem, every `prev`
188/// reference must point to another migration in the same directory,
189/// and the JSON must parse. A broken chain — orphan `prev`, missing
190/// predecessor, malformed file — fails at macro-expansion time with
191/// a clear `compile_error!`. *No other Django-shape Rust framework
192/// validates migration chains at compile time*: Cot's migrations are
193/// imperative Rust code (no static chain), Loco's are SeaORM
194/// up/down (same), Rwf's are raw SQL (no chain at all).
195///
196/// Each migration is included via `include_str!` so cargo's rebuild
197/// detection picks up file *content* changes. **Caveat:** cargo
198/// doesn't watch directory listings, so adding or removing a
199/// migration file inside the dir won't auto-trigger a rebuild — run
200/// `cargo clean` (or just bump any other source file) when you add
201/// new migrations during embedded development.
202#[proc_macro]
203pub fn embed_migrations(input: TokenStream) -> TokenStream {
204    expand_embed_migrations(input.into())
205        .unwrap_or_else(syn::Error::into_compile_error)
206        .into()
207}
208
209/// `Q!()` — Django-shape filter syntax compile-time-resolved against
210/// typed columns. Issue #269 / T1.7.
211///
212/// Each invocation lowers to the equivalent typed-column method call:
213///
214/// ```ignore
215/// // These expand identically:
216/// Q!(User.email__icontains = "alice")
217/// User::email.ilike("%alice%")
218/// ```
219///
220/// Field-name typos fail the build (the macro emits `User::no_such_field`
221/// which doesn't exist) — the headline ergonomic win of this slice over
222/// Django's stringly-typed `__lookup` filters.
223///
224/// # Supported lookup suffixes
225///
226/// * bare `=` / `__exact` → `.eq(value)`
227/// * `__iexact` → `.ilike(value)` (case-insensitive equality, no wildcards)
228/// * `__ne` → `.ne(value)`
229/// * `__gt` / `__gte` / `__lt` / `__lte` → corresponding comparison
230/// * `__contains` / `__icontains` → `.like("%v%")` / `.ilike("%v%")`
231/// * `__startswith` / `__istartswith` → `.like("v%")` / `.ilike("v%")`
232/// * `__endswith` / `__iendswith` → `.like("%v")` / `.ilike("%v")`
233/// * `__in` → `.is_in(iterable)`
234/// * `__not_in` → `.not_in(iterable)`
235/// * `__isnull = true` → `.is_null()`; `__isnull = false` → `.is_not_null()`
236/// * `__between` accepts a tuple literal `(lo, hi)` → `.between(lo, hi)`
237/// * `__regex` / `__iregex` → `.regex(pattern)` / `.iregex(pattern)`
238///
239/// Unknown suffixes fail the build with a `compile_error!` pointing at
240/// the lookup token.
241///
242/// # Combine
243///
244/// Each `Q!()` returns a `TypedFilter<Model>` — chain via the existing
245/// `.and()` / `.or()` / `.not()` methods:
246///
247/// ```ignore
248/// User::objects()
249///     .where_(
250///         Q!(User.active = true)
251///             .and(Q!(User.email__icontains = "alice"))
252///     )
253///     .fetch(&pool).await?;
254/// ```
255///
256/// All emitted code routes through existing per-dialect writers — no new
257/// SQL emission machinery. Tri-dialect support is inherent.
258#[allow(non_snake_case)]
259#[proc_macro]
260pub fn Q(input: TokenStream) -> TokenStream {
261    expand_q(input.into())
262        .unwrap_or_else(syn::Error::into_compile_error)
263        .into()
264}
265
266/// `#[rustango::main]` — the Django-shape runserver entrypoint. Wraps
267/// `#[tokio::main]` and a default `tracing_subscriber` initialisation
268/// (env-filter, falling back to `info,sqlx=warn`) so user `main`
269/// functions are zero-boilerplate:
270///
271/// ```ignore
272/// #[rustango::main]
273/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
274///     rustango::server::Builder::from_env().await?
275///         .migrate("migrations").await?
276///         .api(my_app::urls::api())
277///         .seed_with(my_app::seed::run).await?
278///         .serve("0.0.0.0:8080").await
279/// }
280/// ```
281///
282/// Optional `flavor = "current_thread"` passes through to
283/// `#[tokio::main]`; default is the multi-threaded runtime.
284///
285/// Pulls `tracing-subscriber` into the rustango crate behind the
286/// `runtime` sub-feature (implied by `tenancy`), so apps that opt
287/// out get plain `#[tokio::main]` ergonomics without the dependency.
288#[proc_macro_attribute]
289pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
290    expand_main(args.into(), item.into())
291        .unwrap_or_else(syn::Error::into_compile_error)
292        .into()
293}
294
295fn expand_main(args: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
296    let mut input: syn::ItemFn = syn::parse2(item)?;
297    if input.sig.asyncness.is_none() {
298        return Err(syn::Error::new(
299            input.sig.ident.span(),
300            "`#[rustango::main]` must wrap an `async fn`",
301        ));
302    }
303
304    // v0.31.1 (#4): hand-roll the tokio runtime instead of delegating
305    // to `#[tokio::main]`. Tokio's proc-macro internally emits
306    // `::tokio::*` paths that resolve against the user crate's deps,
307    // so calling it through the rustango re-export still requires the
308    // user to add tokio to their own Cargo.toml. Building the
309    // runtime ourselves keeps the dep transitive through the
310    // `runtime` feature on rustango.
311    //
312    // Parse optional `flavor = "current_thread"` / `flavor =
313    // "multi_thread"` from the attribute args. Unknown args are
314    // tolerated (forward-compat with tokio's own arg surface).
315    let root = rustango_root();
316    let flavor = parse_flavor(&args);
317    let builder_call = match flavor {
318        Flavor::CurrentThread => quote! {
319            #root::__private_runtime::tokio::runtime::Builder::new_current_thread()
320        },
321        Flavor::MultiThread => quote! {
322            #root::__private_runtime::tokio::runtime::Builder::new_multi_thread()
323        },
324    };
325
326    // Detach the user body and rewrite `main` as a sync fn that
327    // builds the runtime and blocks on the async body.
328    let user_body = input.block.clone();
329    input.sig.asyncness = None;
330    input.block = syn::parse2(quote! {{
331        {
332            use #root::__private_runtime::tracing_subscriber::{self, EnvFilter};
333            // `try_init` so duplicate installers (e.g. tests already
334            // holding a subscriber) don't panic.
335            let _ = tracing_subscriber::fmt()
336                .with_env_filter(
337                    EnvFilter::try_from_default_env()
338                        .unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")),
339                )
340                .try_init();
341        }
342        let __rt = #builder_call
343            .enable_all()
344            .build()
345            .expect("failed to build tokio runtime");
346        __rt.block_on(async move #user_body)
347    }})?;
348
349    Ok(quote! {
350        #input
351    })
352}
353
354enum Flavor {
355    MultiThread,
356    CurrentThread,
357}
358
359fn parse_flavor(args: &TokenStream2) -> Flavor {
360    // Cheap parser: look for the literal token sequence
361    // `flavor = "current_thread"`. Everything else (including
362    // bare `multi_thread` or no args) defaults to multi-thread.
363    let s = args.to_string();
364    if s.contains("current_thread") {
365        Flavor::CurrentThread
366    } else {
367        Flavor::MultiThread
368    }
369}
370
371/// Parse form for `Q!()` — `<TypePath>.<Ident> = <Expr>`.
372struct QInput {
373    base_path: syn::Path,
374    field: syn::Ident,
375    value: syn::Expr,
376}
377
378impl syn::parse::Parse for QInput {
379    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
380        let base_path: syn::Path = input.parse()?;
381        input.parse::<syn::Token![.]>()?;
382        let field: syn::Ident = input.parse()?;
383        input.parse::<syn::Token![=]>()?;
384        let value: syn::Expr = input.parse()?;
385        Ok(QInput {
386            base_path,
387            field,
388            value,
389        })
390    }
391}
392
393fn expand_q(input: TokenStream2) -> syn::Result<TokenStream2> {
394    let q: QInput = syn::parse2(input)?;
395    let root = rustango_root();
396    let field_str = q.field.to_string();
397    let field_span = q.field.span();
398    let (base, suffix) = match field_str.find("__") {
399        Some(idx) => (&field_str[..idx], &field_str[idx + 2..]),
400        None => (field_str.as_str(), ""),
401    };
402    if base.is_empty() {
403        return Err(syn::Error::new(
404            field_span,
405            "Q!(): field name is empty before `__` suffix",
406        ));
407    }
408    let base_ident = syn::Ident::new(base, field_span);
409    let value = &q.value;
410    let path = &q.base_path;
411
412    // Most suffixes map directly to a Column method with the value
413    // forwarded unchanged. Some need value-shape massaging (wildcards
414    // for LIKE-family, tuple destructure for BETWEEN, literal-bool for
415    // ISNULL). Unknown suffixes fail the build.
416    let expanded = match suffix {
417        "" | "exact" => quote! {
418            #root::core::Column::eq(#path::#base_ident, #value)
419        },
420        "ne" => quote! {
421            #root::core::Column::ne(#path::#base_ident, #value)
422        },
423        "gt" => quote! {
424            #root::core::Column::gt(#path::#base_ident, #value)
425        },
426        "gte" => quote! {
427            #root::core::Column::gte(#path::#base_ident, #value)
428        },
429        "lt" => quote! {
430            #root::core::Column::lt(#path::#base_ident, #value)
431        },
432        "lte" => quote! {
433            #root::core::Column::lte(#path::#base_ident, #value)
434        },
435        "iexact" => quote! {
436            // Django emulates `__iexact` as case-insensitive equality.
437            // The non-wildcard `ILIKE value` is semantically identical
438            // for plain strings; LIKE-metachars `%` `_` in the rhs would
439            // accidentally match more — document the caveat.
440            #root::core::Column::ilike(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
441        },
442        "contains" => quote! {
443            #root::core::Column::like(
444                #path::#base_ident,
445                ::std::format!("%{}%", #value),
446            )
447        },
448        "icontains" => quote! {
449            #root::core::Column::ilike(
450                #path::#base_ident,
451                ::std::format!("%{}%", #value),
452            )
453        },
454        "startswith" => quote! {
455            #root::core::Column::like(
456                #path::#base_ident,
457                ::std::format!("{}%", #value),
458            )
459        },
460        "istartswith" => quote! {
461            #root::core::Column::ilike(
462                #path::#base_ident,
463                ::std::format!("{}%", #value),
464            )
465        },
466        "endswith" => quote! {
467            #root::core::Column::like(
468                #path::#base_ident,
469                ::std::format!("%{}", #value),
470            )
471        },
472        "iendswith" => quote! {
473            #root::core::Column::ilike(
474                #path::#base_ident,
475                ::std::format!("%{}", #value),
476            )
477        },
478        "in" => quote! {
479            #root::core::Column::is_in(#path::#base_ident, #value)
480        },
481        "not_in" => quote! {
482            #root::core::Column::not_in(#path::#base_ident, #value)
483        },
484        "isnull" => {
485            // Must be a bool literal at macro time so we can route to
486            // is_null vs is_not_null without a runtime branch.
487            let b = match value {
488                syn::Expr::Lit(syn::ExprLit {
489                    lit: syn::Lit::Bool(b),
490                    ..
491                }) => b.value(),
492                _ => {
493                    return Err(syn::Error::new_spanned(
494                        value,
495                        "Q!(): `__isnull` requires a `true` or `false` literal",
496                    ));
497                }
498            };
499            if b {
500                quote! { #root::core::Column::is_null(#path::#base_ident) }
501            } else {
502                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
503            }
504        }
505        "between" => {
506            // Accept a tuple literal `(lo, hi)`.
507            let tuple = match value {
508                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
509                _ => {
510                    return Err(syn::Error::new_spanned(
511                        value,
512                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
513                    ));
514                }
515            };
516            let lo = &tuple.elems[0];
517            let hi = &tuple.elems[1];
518            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
519        }
520        "regex" => quote! {
521            #root::core::Column::regex(#path::#base_ident, #value)
522        },
523        "iregex" => quote! {
524            #root::core::Column::iregex(#path::#base_ident, #value)
525        },
526        _ => {
527            return Err(syn::Error::new(
528                field_span,
529                format!(
530                    "Q!(): unknown lookup suffix `__{}`. Supported: __exact / __iexact / __ne / __gt / __gte / __lt / __lte / __contains / __icontains / __startswith / __istartswith / __endswith / __iendswith / __in / __not_in / __isnull / __between / __regex / __iregex",
531                    suffix
532                ),
533            ));
534        }
535    };
536    Ok(expanded)
537}
538
539fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
540    // Default to "./migrations" if invoked without args.
541    let path_str = if input.is_empty() {
542        "./migrations".to_string()
543    } else {
544        let lit: LitStr = syn::parse2(input)?;
545        lit.value()
546    };
547
548    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
549        syn::Error::new(
550            proc_macro2::Span::call_site(),
551            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
552        )
553    })?;
554    let abs = std::path::Path::new(&manifest).join(&path_str);
555
556    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
557    if abs.is_dir() {
558        let read = std::fs::read_dir(&abs).map_err(|e| {
559            syn::Error::new(
560                proc_macro2::Span::call_site(),
561                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
562            )
563        })?;
564        for entry in read.flatten() {
565            let path = entry.path();
566            if !path.is_file() {
567                continue;
568            }
569            if path.extension().and_then(|s| s.to_str()) != Some("json") {
570                continue;
571            }
572            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
573                continue;
574            };
575            entries.push((stem.to_owned(), path));
576        }
577    }
578    entries.sort_by(|a, b| a.0.cmp(&b.0));
579
580    // Compile-time chain validation: read each migration's JSON,
581    // pull `name` and `prev` (file-stem-keyed for the chain check),
582    // and verify every `prev` points to another migration in the
583    // slice. Mismatches between the file stem and the embedded
584    // `name` field — and broken `prev` chains — fail at MACRO
585    // EXPANSION time so a misshapen migration set never compiles.
586    //
587    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
588    // migrations + a Rust proc-macro that reads them is the unique
589    // combo nothing else in the Django-shape Rust camp can match
590    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
591    // Rwf's are raw SQL — none have a static chain to validate).
592    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
593    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
594    for (stem, path) in &entries {
595        let raw = std::fs::read_to_string(path).map_err(|e| {
596            syn::Error::new(
597                proc_macro2::Span::call_site(),
598                format!(
599                    "embed_migrations!: cannot read {} for chain validation: {e}",
600                    path.display()
601                ),
602            )
603        })?;
604        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
605            syn::Error::new(
606                proc_macro2::Span::call_site(),
607                format!(
608                    "embed_migrations!: {} is not valid JSON: {e}",
609                    path.display()
610                ),
611            )
612        })?;
613        let name = json
614            .get("name")
615            .and_then(|v| v.as_str())
616            .ok_or_else(|| {
617                syn::Error::new(
618                    proc_macro2::Span::call_site(),
619                    format!(
620                        "embed_migrations!: {} is missing the `name` field",
621                        path.display()
622                    ),
623                )
624            })?
625            .to_owned();
626        if name != *stem {
627            return Err(syn::Error::new(
628                proc_macro2::Span::call_site(),
629                format!(
630                    "embed_migrations!: file stem `{stem}` does not match the migration's \
631                     `name` field `{name}` — rename the file or fix the JSON",
632                ),
633            ));
634        }
635        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
636        chain_names.push(name.clone());
637        prev_refs.push((name, prev));
638    }
639
640    let name_set: std::collections::HashSet<&str> =
641        chain_names.iter().map(String::as_str).collect();
642    for (name, prev) in &prev_refs {
643        if let Some(p) = prev {
644            if !name_set.contains(p.as_str()) {
645                return Err(syn::Error::new(
646                    proc_macro2::Span::call_site(),
647                    format!(
648                        "embed_migrations!: broken migration chain — `{name}` declares \
649                         prev=`{p}` but no migration with that name exists in {}",
650                        abs.display()
651                    ),
652                ));
653            }
654        }
655    }
656
657    let pairs: Vec<TokenStream2> = entries
658        .iter()
659        .map(|(name, path)| {
660            let path_lit = path.display().to_string();
661            quote! { (#name, ::core::include_str!(#path_lit)) }
662        })
663        .collect();
664
665    Ok(quote! {
666        {
667            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
668            __RUSTANGO_EMBEDDED
669        }
670    })
671}
672
673fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
674    let root = rustango_root();
675    let struct_name = &input.ident;
676
677    let Data::Struct(data) = &input.data else {
678        return Err(syn::Error::new_spanned(
679            struct_name,
680            "Model can only be derived on structs",
681        ));
682    };
683    let Fields::Named(named) = &data.fields else {
684        return Err(syn::Error::new_spanned(
685            struct_name,
686            "Model requires a struct with named fields",
687        ));
688    };
689
690    let container = parse_container_attrs(input)?;
691    let table = container
692        .table
693        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
694    let model_name = struct_name.to_string();
695
696    let collected = collect_fields(named, &table)?;
697
698    // Validate that #[rustango(display = "…")] names a real field.
699    if let Some((ref display, span)) = container.display {
700        if !collected.field_names.iter().any(|n| n == display) {
701            return Err(syn::Error::new(
702                span,
703                format!("`display = \"{display}\"` does not match any field on this struct"),
704            ));
705        }
706    }
707    let display = container.display.map(|(name, _)| name);
708    let app_label = container.app.clone();
709
710    // Validate admin field-name lists against declared field names.
711    // Note: `list_display` is intentionally NOT validated here. As of
712    // v0.32 it may also reference inventory-registered computed
713    // fields (via `register_admin_computed!`) whose existence the
714    // macro can't see at compile time — they're submitted from any
715    // crate that depends on rustango. The runtime list-view resolves
716    // unknown names against the inventory + silently drops the
717    // truly-bogus ones, which is the cheaper trade-off versus
718    // forcing a per-Model attr to opt out.
719    if let Some(admin) = &container.admin {
720        for (label, list) in [
721            ("search_fields", &admin.search_fields),
722            ("readonly_fields", &admin.readonly_fields),
723            ("list_filter", &admin.list_filter),
724        ] {
725            if let Some((names, span)) = list {
726                for name in names {
727                    if !collected.field_names.iter().any(|n| n == name) {
728                        return Err(syn::Error::new(
729                            *span,
730                            format!(
731                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
732                            ),
733                        ));
734                    }
735                }
736            }
737        }
738        if let Some((pairs, span)) = &admin.ordering {
739            for (name, _) in pairs {
740                if !collected.field_names.iter().any(|n| n == name) {
741                    return Err(syn::Error::new(
742                        *span,
743                        format!(
744                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
745                        ),
746                    ));
747                }
748            }
749        }
750        if let Some((groups, span)) = &admin.fieldsets {
751            for (_, fields) in groups {
752                for name in fields {
753                    if !collected.field_names.iter().any(|n| n == name) {
754                        return Err(syn::Error::new(
755                            *span,
756                            format!(
757                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
758                            ),
759                        ));
760                    }
761                }
762            }
763        }
764    }
765    if let Some(audit) = &container.audit {
766        if let Some((names, span)) = &audit.track {
767            for name in names {
768                if !collected.field_names.iter().any(|n| n == name) {
769                    return Err(syn::Error::new(
770                        *span,
771                        format!(
772                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
773                        ),
774                    ));
775                }
776            }
777        }
778    }
779
780    // Issue #291 / T2.5 — validate each `default_order` column name
781    // against the model's collected fields. Typos fail at macro-expand
782    // time, not at the database.
783    for (col, _desc, span) in &container.default_order {
784        if !collected.field_names.iter().any(|n| n == col) {
785            return Err(syn::Error::new(
786                *span,
787                format!(
788                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
789                ),
790            ));
791        }
792    }
793
794    // Build the audit_track list for ModelSchema: None when no audit attr,
795    // Some(empty) when audit present without track, Some(names) when explicit.
796    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
797        audit
798            .track
799            .as_ref()
800            .map(|(names, _)| names.clone())
801            .unwrap_or_default()
802    });
803
804    // Merge field-level indexes into the container's index list.
805    let mut all_indexes: Vec<IndexAttr> = container.indexes;
806    for field in &named.named {
807        let ident = field.ident.as_ref().expect("named");
808        let col = to_snake_case(&ident.to_string()); // column name fallback
809                                                     // Re-parse field attrs to check for index flag
810        if let Ok(fa) = parse_field_attrs(field) {
811            if fa.index {
812                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
813                let auto_name = if fa.index_unique {
814                    format!("{table}_{col_name}_uq_idx")
815                } else {
816                    format!("{table}_{col_name}_idx")
817                };
818                all_indexes.push(IndexAttr {
819                    name: fa.index_name.or(Some(auto_name)),
820                    columns: vec![col_name],
821                    unique: fa.index_unique,
822                    method: fa.index_method,
823                    where_clause: None,
824                    include: Vec::new(),
825                });
826            }
827        }
828    }
829
830    let model_impl = model_impl_tokens(
831        struct_name,
832        &model_name,
833        &table,
834        display.as_deref(),
835        app_label.as_deref(),
836        container.admin.as_ref(),
837        &container.default_order,
838        &collected.field_schemas,
839        collected.soft_delete_column.as_deref(),
840        container.permissions,
841        audit_track_names.as_deref(),
842        &container.m2m,
843        &all_indexes,
844        &container.checks,
845        &container.excludes,
846        &container.composite_fks,
847        &container.generic_fks,
848        container.scope.as_deref(),
849        container.is_view,
850        container.verbose_name.as_deref(),
851        container.verbose_name_plural.as_deref(),
852        container.managed,
853        container.base_manager_name.as_deref(),
854        container.order_with_respect_to.as_deref(),
855        container.proxy,
856        &container.required_db_features,
857        container.required_db_vendor.as_deref(),
858        container.default_related_name.as_deref(),
859        container.db_table_comment.as_deref(),
860        container
861            .get_latest_by
862            .as_ref()
863            .map(|(c, d)| (c.as_str(), *d)),
864        &container.extra_permissions,
865        &container.default_permissions,
866        &container.global_scopes,
867        &container.reverse_has_relations,
868        &container.generic_has_relations,
869    );
870    let module_ident = column_module_ident(struct_name);
871    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
872    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
873        let track_set: Option<std::collections::HashSet<&str>> = audit
874            .track
875            .as_ref()
876            .map(|(names, _)| names.iter().map(String::as_str).collect());
877        collected
878            .column_entries
879            .iter()
880            .filter(|c| {
881                track_set
882                    .as_ref()
883                    .map_or(true, |s| s.contains(c.name.as_str()))
884            })
885            .collect()
886    });
887    let inherent_impl = inherent_impl_tokens(
888        struct_name,
889        &collected,
890        collected.primary_key.as_ref(),
891        &column_consts,
892        audited_fields.as_deref(),
893        &all_indexes,
894        &container.manager_fns,
895    );
896    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
897    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
898    let reverse_helpers = reverse_helper_tokens(
899        struct_name,
900        &collected.fk_relations,
901        container.default_related_name.as_deref(),
902    );
903    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
904    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
905    // Issue #817 — `#[rustango(through(...))]` accessors.
906    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
907    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
908    let reverse_has_accessors =
909        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
910    let generic_fk_accessors = generic_fk_accessor_tokens(
911        struct_name,
912        &container.generic_fks,
913        &collected.column_entries,
914    );
915
916    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
917    // emits an empty extension trait so users can add methods via
918    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
919    // trait declaration. See `crates/rustango/src/manager.rs` for the
920    // pattern this replaces.
921    let manager_trait = container.manager_ext.as_ref().map(|name| {
922        let model_name_str = struct_name.to_string();
923        let doc = format!(
924            "Custom-Manager extension trait for [`{model_name_str}`]. \
925             Generated by `#[rustango(manager(ext = ...))]`. Add methods \
926             via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
927        );
928        quote! {
929            #[doc = #doc]
930            pub trait #name: ::core::marker::Sized {}
931        }
932    });
933
934    Ok(quote! {
935        #model_impl
936        #inherent_impl
937        #from_row_impl
938        #column_module
939        #reverse_helpers
940        #m2m_accessors
941        #generic_m2m_accessors
942        #through_accessors
943        #reverse_has_accessors
944        #generic_fk_accessors
945        #manager_trait
946
947        #root::core::inventory::submit! {
948            #root::core::ModelEntry {
949                schema: <#struct_name as #root::core::Model>::SCHEMA,
950                // `module_path!()` evaluates at the registration site,
951                // so a Model declared in `crate::blog::models` records
952                // `"<crate>::blog::models"` and `resolved_app_label()`
953                // can infer "blog" without an explicit attribute.
954                module_path: ::core::module_path!(),
955            }
956        }
957    })
958}
959
960/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
961/// matches `field_name` against the model's FK fields and, for a
962/// match, decodes the FK target via the parent's macro-generated
963/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
964/// `ForeignKey::Loaded` on `self`.
965///
966/// Always emitted (with empty arms for FK-less models, which
967/// return `Ok(false)` for any field name) so the `T: LoadRelated`
968/// trait bound on `fetch_on` is universally satisfied — users
969/// never have to think about implementing it.
970fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
971    let root = rustango_root();
972    let arms = fk_relations.iter().map(|rel| {
973        let parent_ty = &rel.parent_type;
974        let fk_col = rel.fk_column.as_str();
975        // FK field's Rust ident matches its SQL column name in v0.8
976        // (no `column = "..."` rename ships on FK fields).
977        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
978        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
979        let assign = if rel.nullable {
980            quote! {
981                self.#field_ident = ::core::option::Option::Some(
982                    #root::sql::ForeignKey::loaded(_pk, _parent),
983                );
984            }
985        } else {
986            quote! {
987                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
988            }
989        };
990        quote! {
991            #fk_col => {
992                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
993                // Audit #451 — multi-hop `select_related("a__b__c")`:
994                // stitch the deeper relation onto this parent first,
995                // decoding it at the accumulated `__next_alias`. The
996                // parent type also impls `LoadRelated`, so this recurses
997                // the FK chain to arbitrary depth.
998                if let ::core::option::Option::Some(__r) = __rest {
999                    let _ = #root::sql::LoadRelated::__rustango_load_related(
1000                        &mut _parent, row, __r, &__next_alias,
1001                    )?;
1002                }
1003                // Loud-in-debug, default-in-release: a divergence
1004                // between the FK field's declared `K` (drives the
1005                // expected `SqlValue::<Variant>`) and the parent's
1006                // `__rustango_pk_value` output is a macro-internal
1007                // invariant break — surfacing the panic in dev
1008                // catches it before users hit silent PK=0 corruption.
1009                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1010                    #root::core::SqlValue::#variant_ident(v) => v,
1011                    _other => {
1012                        ::core::debug_assert!(
1013                            false,
1014                            "rustango macro bug: load_related on FK `{}` expected \
1015                             SqlValue::{} from parent's __rustango_pk_value but got \
1016                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1017                            #fk_col,
1018                            ::core::stringify!(#variant_ident),
1019                            _other,
1020                        );
1021                        #default_expr
1022                    }
1023                };
1024                #assign
1025                ::core::result::Result::Ok(true)
1026            }
1027        }
1028    });
1029    quote! {
1030        #[cfg(feature = "postgres")]
1031        impl #root::sql::LoadRelated for #struct_name {
1032            #[allow(unused_variables)]
1033            fn __rustango_load_related(
1034                &mut self,
1035                row: &#root::sql::sqlx::postgres::PgRow,
1036                field_name: &str,
1037                alias: &str,
1038            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1039                // Audit #451 — split the multi-hop path: `base` is the FK
1040                // on THIS model, `__rest` (if any) is the remaining chain
1041                // to stitch onto the loaded parent. `__next_alias` is the
1042                // accumulated join alias the parent's columns live under
1043                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1044                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1045                    match field_name.split_once("__") {
1046                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1047                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1048                    };
1049                let __next_alias: ::std::string::String = match __rest {
1050                    ::core::option::Option::Some(__r) => {
1051                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1052                        ::std::format!("{}__{}", alias, __rb)
1053                    }
1054                    ::core::option::Option::None => ::std::string::String::new(),
1055                };
1056                match __base {
1057                    #( #arms )*
1058                    _ => ::core::result::Result::Ok(false),
1059                }
1060            }
1061        }
1062    }
1063}
1064
1065/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1066/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1067/// which expands to a `LoadRelatedMy` impl when rustango is built with
1068/// the `mysql` feature, and to nothing otherwise. The decoded parent
1069/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1070/// decoder, also batch8) so the dual emission is symmetric across
1071/// backends.
1072fn load_related_impl_my_tokens(
1073    struct_name: &syn::Ident,
1074    fk_relations: &[FkRelation],
1075) -> TokenStream2 {
1076    let root = rustango_root();
1077    let arms = fk_relations.iter().map(|rel| {
1078        let parent_ty = &rel.parent_type;
1079        let fk_col = rel.fk_column.as_str();
1080        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1081        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1082        let assign = if rel.nullable {
1083            quote! {
1084                __self.#field_ident = ::core::option::Option::Some(
1085                    #root::sql::ForeignKey::loaded(_pk, _parent),
1086                );
1087            }
1088        } else {
1089            quote! {
1090                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1091            }
1092        };
1093        // `self` IS hygiene-tracked through macro_rules — emitted from
1094        // a different context than the `&mut self` parameter inside
1095        // the macro_rules-expanded fn. Pass it through as `__self`
1096        // and let the macro_rules rebind it to the receiver.
1097        quote! {
1098            #fk_col => {
1099                let mut _parent: #parent_ty =
1100                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1101                // Audit #451 — multi-hop: stitch the deeper relation onto
1102                // the parent at the accumulated alias (see PG twin).
1103                if let ::core::option::Option::Some(__r) = __rest {
1104                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1105                        &mut _parent, row, __r, &__next_alias,
1106                    )?;
1107                }
1108                // See note in `load_related_impl_tokens` (PG twin) —
1109                // the same loud-in-debug invariant guard.
1110                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1111                    #root::core::SqlValue::#variant_ident(v) => v,
1112                    _other => {
1113                        ::core::debug_assert!(
1114                            false,
1115                            "rustango macro bug: load_related on FK `{}` expected \
1116                             SqlValue::{} from parent's __rustango_pk_value but got \
1117                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1118                            #fk_col,
1119                            ::core::stringify!(#variant_ident),
1120                            _other,
1121                        );
1122                        #default_expr
1123                    }
1124                };
1125                #assign
1126                ::core::result::Result::Ok(true)
1127            }
1128        }
1129    });
1130    quote! {
1131        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1132            #( #arms )*
1133        });
1134    }
1135}
1136
1137/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1138/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1139/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1140fn load_related_impl_sqlite_tokens(
1141    struct_name: &syn::Ident,
1142    fk_relations: &[FkRelation],
1143) -> TokenStream2 {
1144    let root = rustango_root();
1145    let arms = fk_relations.iter().map(|rel| {
1146        let parent_ty = &rel.parent_type;
1147        let fk_col = rel.fk_column.as_str();
1148        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1149        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1150        let assign = if rel.nullable {
1151            quote! {
1152                __self.#field_ident = ::core::option::Option::Some(
1153                    #root::sql::ForeignKey::loaded(_pk, _parent),
1154                );
1155            }
1156        } else {
1157            quote! {
1158                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1159            }
1160        };
1161        quote! {
1162            #fk_col => {
1163                let mut _parent: #parent_ty =
1164                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1165                // Audit #451 — multi-hop: stitch the deeper relation onto
1166                // the parent at the accumulated alias (see PG twin).
1167                if let ::core::option::Option::Some(__r) = __rest {
1168                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1169                        &mut _parent, row, __r, &__next_alias,
1170                    )?;
1171                }
1172                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1173                    #root::core::SqlValue::#variant_ident(v) => v,
1174                    _other => {
1175                        ::core::debug_assert!(
1176                            false,
1177                            "rustango macro bug: load_related on FK `{}` expected \
1178                             SqlValue::{} from parent's __rustango_pk_value but got \
1179                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1180                            #fk_col,
1181                            ::core::stringify!(#variant_ident),
1182                            _other,
1183                        );
1184                        #default_expr
1185                    }
1186                };
1187                #assign
1188                ::core::result::Result::Ok(true)
1189            }
1190        }
1191    });
1192    quote! {
1193        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1194            #( #arms )*
1195        });
1196    }
1197}
1198
1199/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1200/// matches `field_name` against the model's FK fields and returns
1201/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1202/// group children by parent PK.
1203///
1204/// Always emitted (with `_ => None` for FK-less models) so the
1205/// trait bound on `fetch_with_prefetch` is universally satisfied.
1206fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1207    let root = rustango_root();
1208    let arms = fk_relations.iter().map(|rel| {
1209        let fk_col = rel.fk_column.as_str();
1210        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1211        if rel.pk_kind == DetectedKind::I64 {
1212            // i64 FK — return the stored PK so prefetch_related can
1213            // group children by it. Nullable variant unwraps via
1214            // `as_ref().map(...)`: an unset (NULL) FK column yields
1215            // `None` and that child sits out of the grouping (correct
1216            // semantics — it has no parent to attach to).
1217            if rel.nullable {
1218                quote! {
1219                    #fk_col => self.#field_ident
1220                        .as_ref()
1221                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1222                }
1223            } else {
1224                quote! {
1225                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1226                }
1227            }
1228        } else {
1229            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1230            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1231            // i64-keyed grouping path — the trait signature is
1232            // `Option<i64>` and a non-i64 PK can't lower into it.
1233            // The FK still works for everything else (CRUD, lazy
1234            // load via `.get()`, select_related JOINs); only the
1235            // bulk prefetch grouper needs the integer key.
1236            quote! {
1237                #fk_col => ::core::option::Option::None,
1238            }
1239        }
1240    });
1241    // PK-type-agnostic version: every FK arm emits an
1242    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1243    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1244    // opt OUT of the legacy i64 method (it returns None) but opt IN
1245    // here.
1246    let value_arms = fk_relations.iter().map(|rel| {
1247        let fk_col = rel.fk_column.as_str();
1248        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1249        if rel.nullable {
1250            quote! {
1251                #fk_col => self.#field_ident
1252                    .as_ref()
1253                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1254                        #root::sql::ForeignKey::pk(fk)
1255                    )),
1256            }
1257        } else {
1258            quote! {
1259                #fk_col => ::core::option::Option::Some(
1260                    ::core::convert::Into::<#root::core::SqlValue>::into(
1261                        self.#field_ident.pk()
1262                    )
1263                ),
1264            }
1265        }
1266    });
1267    quote! {
1268        impl #root::sql::FkPkAccess for #struct_name {
1269            #[allow(unused_variables)]
1270            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1271                match field_name {
1272                    #( #arms )*
1273                    _ => ::core::option::Option::None,
1274                }
1275            }
1276            #[allow(unused_variables)]
1277            fn __rustango_fk_pk_value(
1278                &self,
1279                field_name: &str,
1280            ) -> ::core::option::Option<#root::core::SqlValue> {
1281                match field_name {
1282                    #( #value_arms )*
1283                    _ => ::core::option::Option::None,
1284                }
1285            }
1286        }
1287    }
1288}
1289
1290/// For every `ForeignKey<Parent>` field on `Child`, emit
1291/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1292/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1293/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1294/// — the canonical reverse-FK fetch. One round trip, no N+1.
1295///
1296/// **PG-only emission**: the accessor is bounded on
1297/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1298/// both of which are gated behind the `postgres` cargo feature. The
1299/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1300/// model derive itself compiles on tri-dialect / sqlite-only
1301/// downstream builds — the accessor just isn't materialised. A tri-
1302/// dialect `_set_pool` variant is a separate follow-up.
1303fn reverse_helper_tokens(
1304    child_ident: &syn::Ident,
1305    fk_relations: &[FkRelation],
1306    default_related_name: Option<&str>,
1307) -> TokenStream2 {
1308    let root = rustango_root();
1309    if fk_relations.is_empty() {
1310        return TokenStream2::new();
1311    }
1312    // Method-name resolution per FK (issue #816 + follow-up):
1313    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1314    //      itself — wins over everything else. Django's
1315    //      `ForeignKey(related_name="...")`.
1316    //   2. Container-level `default_related_name = "..."` on the
1317    //      child — Django's `class Meta: default_related_name`.
1318    //      Applies to every FK on this model that didn't override.
1319    //   3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1320    //      convention. `Post` → `post_set`, `BlogComment` →
1321    //      `blog_comment_set`. Avoids English-plural edge cases.
1322    //
1323    // The PG-on-executor variant keeps the resolved name; the
1324    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1325    // framework's convention for the `&Pool` flavor of every helper).
1326    let default_pg_suffix = default_related_name
1327        .map(str::to_owned)
1328        .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1329    let impls = fk_relations.iter().map(|rel| {
1330        let pg_suffix = rel
1331            .related_name
1332            .clone()
1333            .unwrap_or_else(|| default_pg_suffix.clone());
1334        let pool_suffix = format!("{}_pool", pg_suffix);
1335        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1336        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1337        let parent_ty = &rel.parent_type;
1338        let fk_col = rel.fk_column.as_str();
1339        let doc = format!(
1340            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1341             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1342             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1343             further `{child_ident}::objects()` filters via direct queryset use."
1344        );
1345        let pool_doc = format!(
1346            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1347             [`#root::sql::Pool`] and dispatches per backend so the \
1348             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1349             Use this from framework code that holds a `&Pool` (admin, \
1350             tenancy resolver, viewset handlers); reach for the executor- \
1351             bound variant when you already have a typed `sqlx::Executor`."
1352        );
1353        quote! {
1354            #[cfg(feature = "postgres")]
1355            impl #parent_ty {
1356                #[doc = #doc]
1357                ///
1358                /// # Errors
1359                /// Returns [`#root::sql::ExecError`] for SQL-writing
1360                /// or driver failures.
1361                pub async fn #pg_method_ident<'_c, _E>(
1362                    &self,
1363                    _executor: _E,
1364                ) -> ::core::result::Result<
1365                    ::std::vec::Vec<#child_ident>,
1366                    #root::sql::ExecError,
1367                >
1368                where
1369                    _E: #root::sql::sqlx::Executor<
1370                        '_c,
1371                        Database = #root::sql::sqlx::Postgres,
1372                    >,
1373                {
1374                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1375                    #root::query::QuerySet::<#child_ident>::new()
1376                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1377                        .fetch_on(_executor)
1378                        .await
1379                }
1380            }
1381
1382            impl #parent_ty {
1383                #[doc = #pool_doc]
1384                ///
1385                /// # Errors
1386                /// Returns [`#root::sql::ExecError`] for SQL-writing
1387                /// or driver failures.
1388                pub async fn #pool_method_ident(
1389                    &self,
1390                    pool: &#root::sql::Pool,
1391                ) -> ::core::result::Result<
1392                    ::std::vec::Vec<#child_ident>,
1393                    #root::sql::ExecError,
1394                > {
1395                    use #root::sql::FetcherPool as _;
1396                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1397                    #root::query::QuerySet::<#child_ident>::new()
1398                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1399                        .fetch(pool)
1400                        .await
1401                }
1402            }
1403        }
1404    });
1405    quote! { #( #impls )* }
1406}
1407
1408/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1409/// relation declared on the model.
1410/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1411/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1412///
1413/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1414/// `comment.content_object = post` ergonomics on top of the existing
1415/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1416///
1417/// `column_entries` is passed so we can resolve each `ct_column` /
1418/// `pk_column` SQL name back to its Rust field ident — the macro
1419/// only sees the column-side strings in the attribute, but the
1420/// emitted accessor needs to read the actual struct field.
1421fn generic_fk_accessor_tokens(
1422    struct_name: &syn::Ident,
1423    generic_fks: &[GenericFkAttr],
1424    column_entries: &[ColumnEntry],
1425) -> TokenStream2 {
1426    let root = rustango_root();
1427    if generic_fks.is_empty() {
1428        return TokenStream2::new();
1429    }
1430    let methods = generic_fks.iter().filter_map(|gfk| {
1431        // Resolve `ct_column` + `pk_column` to the struct's Rust
1432        // field idents. A typo (column name doesn't match any field)
1433        // emits no method for that registration — the user will see
1434        // the compiler reject the SCHEMA literal anyway, so there's
1435        // a clear error path without us double-reporting.
1436        let ct_ident = column_entries
1437            .iter()
1438            .find(|c| c.column == gfk.ct_column)
1439            .map(|c| c.ident.clone())?;
1440        let pk_ident = column_entries
1441            .iter()
1442            .find(|c| c.column == gfk.pk_column)
1443            .map(|c| c.ident.clone())?;
1444
1445        let accessor_ident =
1446            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1447        let setter_ident =
1448            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1449        let name_literal = gfk.name.as_str();
1450
1451        Some(quote! {
1452            #[doc = concat!(
1453                "Resolve the polymorphic `",
1454                #name_literal,
1455                "` relation. Reads `self.",
1456                stringify!(#ct_ident),
1457                "` + `self.",
1458                stringify!(#pk_ident),
1459                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1460                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1461                #name_literal,
1462                "\", ...))]`."
1463            )]
1464            pub async fn #accessor_ident(
1465                &self,
1466                pool: &#root::sql::Pool,
1467            ) -> ::core::result::Result<
1468                ::core::option::Option<#root::__serde_json::Value>,
1469                #root::sql::ExecError,
1470            > {
1471                let gfk = #root::contenttypes::GenericForeignKey::new(
1472                    self.#ct_ident as i64,
1473                    self.#pk_ident as i64,
1474                );
1475                gfk.get_object(pool).await
1476            }
1477
1478            #[doc = concat!(
1479                "Set the polymorphic `",
1480                #name_literal,
1481                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1482                stringify!(#ct_ident),
1483                "` and `self.",
1484                stringify!(#pk_ident),
1485                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1486                #name_literal,
1487                "\", ...))]`."
1488            )]
1489            pub async fn #setter_ident<T: #root::core::Model>(
1490                &mut self,
1491                pool: &#root::sql::Pool,
1492                target_pk: i64,
1493            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1494                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1495                    pool,
1496                    target_pk,
1497                ).await?;
1498                self.#ct_ident = gfk.content_type_id as _;
1499                self.#pk_ident = gfk.object_pk as _;
1500                ::core::result::Result::Ok(())
1501            }
1502        })
1503    });
1504    quote! {
1505        impl #struct_name {
1506            #( #methods )*
1507        }
1508    }
1509}
1510
1511fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1512    let root = rustango_root();
1513    if m2m_relations.is_empty() {
1514        return TokenStream2::new();
1515    }
1516    let methods = m2m_relations.iter().map(|rel| {
1517        let method_name = format!("{}_m2m", rel.name);
1518        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1519        let through = rel.through.as_str();
1520        let src_col = rel.src.as_str();
1521        let dst_col = rel.dst.as_str();
1522        quote! {
1523            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1524                #root::sql::M2MManager {
1525                    src_pk: self.__rustango_pk_value(),
1526                    through: #through,
1527                    src_col: #src_col,
1528                    dst_col: #dst_col,
1529                }
1530            }
1531        }
1532    });
1533    quote! {
1534        impl #struct_name {
1535            #( #methods )*
1536        }
1537    }
1538}
1539
1540/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1541/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1542fn generic_m2m_accessor_tokens(
1543    struct_name: &syn::Ident,
1544    relations: &[GenericM2MAttr],
1545) -> TokenStream2 {
1546    let root = rustango_root();
1547    if relations.is_empty() {
1548        return TokenStream2::new();
1549    }
1550    let methods = relations.iter().map(|rel| {
1551        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1552        let through = rel.through.as_str();
1553        let pk_col = rel.pk_column.as_str();
1554        let ct_col = rel.ct_column.as_str();
1555        let related_col = rel.related_column.as_str();
1556        quote! {
1557            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1558                #root::sql::GenericM2MManager {
1559                    src_pk: self.__rustango_pk_value(),
1560                    src_schema: <Self as #root::core::Model>::SCHEMA,
1561                    through: #through,
1562                    pk_col: #pk_col,
1563                    ct_col: #ct_col,
1564                    dst_col: #related_col,
1565                }
1566            }
1567        }
1568    });
1569    quote! {
1570        impl #struct_name {
1571            #( #methods )*
1572        }
1573    }
1574}
1575
1576/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1577/// associated functions for each `#[rustango(reverse_has(...))]`
1578/// attribute. Issue #830.
1579///
1580/// The two emitted functions return ready-to-use `WhereExpr` nodes
1581/// that downstream callers drop into
1582/// `QuerySet::<Self>::where_raw(...)`:
1583///
1584/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1585///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1586///   `whereHas` parity (without the closure-style sub-predicate
1587///   refinement — that's a follow-up; users can layer additional
1588///   predicates by constructing the SelectQuery themselves and
1589///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1590/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1591///   `whereDoesntHave` parity.
1592///
1593/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1594/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1595/// machinery threads the outer-table reference through automatically
1596/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1597fn reverse_has_accessor_tokens(
1598    struct_name: &syn::Ident,
1599    reverse_has_relations: &[ReverseHasAttr],
1600) -> TokenStream2 {
1601    let root = rustango_root();
1602    if reverse_has_relations.is_empty() {
1603        return TokenStream2::new();
1604    }
1605    let methods = reverse_has_relations.iter().map(|rel| {
1606        let exists_name = format!("{}_exists_expr", rel.name);
1607        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1608        let count_name = format!("{}_count", rel.name);
1609        let fetch_name = format!("{}_fetch", rel.name);
1610        let first_name = format!("{}_first", rel.name);
1611        let pluck_name = format!("{}_pluck", rel.name);
1612        let accessor_name = rel.name.as_str();
1613        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1614        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1615        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1616        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1617        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1618        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1619        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1620        let child = &rel.child;
1621        let child_fk_column = rel.child_fk_column.as_str();
1622        let self_pk_column = rel.self_pk_column.as_str();
1623        let exists_doc = format!(
1624            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1625             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1626             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1627             filter to {struct_name}s with at least one matching child.",
1628        );
1629        let not_exists_doc = format!(
1630            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1631             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1632             <outer>.{self_pk_column})`. Drop into \
1633             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1634             {struct_name}s with **no** matching child.",
1635        );
1636        let count_doc = format!(
1637            "Eloquent `$model->{name}->count()` analog — returns \
1638             the number of `{child}` rows whose `{child_fk_column}` \
1639             matches this `{struct_name}` instance's primary key. \
1640             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1641             {child_fk_column} = <self.pk>`.",
1642            name = rel.name,
1643        );
1644        let accessor_doc = format!(
1645            "Eloquent `$model->{name}` accessor — returns a \
1646             `QuerySet<{child}>` filtered to rows whose \
1647             `{child_fk_column}` matches this `{struct_name}` \
1648             instance's primary key. **Chainable**: compose `.filter()` \
1649             / `.order_by()` / `.limit()` etc. on top, then call \
1650             `.fetch(&pool)` (the QuerySet trait method) when \
1651             done. For the simple \"fetch all\" hot path with no \
1652             further composition, prefer the bare-name \
1653             `{name}_fetch(&pool)` companion.",
1654            name = rel.name,
1655        );
1656        let fetch_doc = format!(
1657            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1658             over `{name}(&self).fetch(&pool)`. Use this when \
1659             you don't need further `.filter()` / `.order_by()` \
1660             composition; falls back to the chainable accessor when \
1661             you do. Avoids the `_pool` suffix on the most common \
1662             call-site shape.",
1663            name = rel.name,
1664        );
1665        quote! {
1666            #[doc = #accessor_doc]
1667            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1668                #root::query::QuerySet::<#child>::new()
1669                    .filter(#child_fk_column, self.__rustango_pk_value())
1670            }
1671
1672            #[doc = #fetch_doc]
1673            pub async fn #fetch_ident(
1674                &self,
1675                pool: &#root::sql::Pool,
1676            ) -> ::core::result::Result<
1677                ::std::vec::Vec<#child>,
1678                #root::sql::ExecError,
1679            > {
1680                use #root::sql::FetcherPool as _;
1681                self.#accessor_ident().fetch(pool).await
1682            }
1683
1684            /// Eloquent `$model->relation->first()` / `hasOne`
1685            /// semantics — bare-name shortcut over
1686            /// `self.<name>().first(&pool)`. Returns `None` when no
1687            /// child rows match. Useful when the relation is
1688            /// nominally many-to-one in shape but at most one row is
1689            /// expected (latest comment, primary tag, etc.).
1690            pub async fn #first_ident(
1691                &self,
1692                pool: &#root::sql::Pool,
1693            ) -> ::core::result::Result<
1694                ::core::option::Option<#child>,
1695                #root::sql::ExecError,
1696            > {
1697                self.#accessor_ident().first(pool).await
1698            }
1699
1700            /// Eloquent `$model->relation->pluck($col)` — project a
1701            /// single column from the child rows into a `Vec<U>`.
1702            /// Skips the typed `Child` decode, which is cheaper when
1703            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1704            pub async fn #pluck_ident<U>(
1705                &self,
1706                col: &'static str,
1707                pool: &#root::sql::Pool,
1708            ) -> ::core::result::Result<
1709                ::std::vec::Vec<U>,
1710                #root::sql::ExecError,
1711            >
1712            where
1713                U: #root::sql::MaybePgScalar
1714                    + #root::sql::MaybeMyScalar
1715                    + #root::sql::MaybeSqliteScalar
1716                    + ::core::marker::Send
1717                    + ::core::marker::Unpin,
1718            {
1719                self.#accessor_ident()
1720                    .values_list_flat(col)
1721                    .fetch::<U>(pool)
1722                    .await
1723            }
1724
1725            #[doc = #exists_doc]
1726            pub fn #exists_ident() -> #root::core::WhereExpr {
1727                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1728                let child_schema =
1729                    <#child as #root::core::Model>::SCHEMA;
1730                let inner = SelectQuery {
1731                    where_clause: WhereExpr::ExprCompare {
1732                        lhs: Expr::Column(#child_fk_column),
1733                        op: Op::Eq,
1734                        rhs: Expr::OuterRef(#self_pk_column),
1735                    },
1736                    ..SelectQuery::new(child_schema)
1737                };
1738                WhereExpr::Exists(::std::boxed::Box::new(inner))
1739            }
1740
1741            #[doc = #not_exists_doc]
1742            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1743                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1744                let child_schema =
1745                    <#child as #root::core::Model>::SCHEMA;
1746                let inner = SelectQuery {
1747                    where_clause: WhereExpr::ExprCompare {
1748                        lhs: Expr::Column(#child_fk_column),
1749                        op: Op::Eq,
1750                        rhs: Expr::OuterRef(#self_pk_column),
1751                    },
1752                    ..SelectQuery::new(child_schema)
1753                };
1754                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1755            }
1756
1757            #[doc = #count_doc]
1758            pub async fn #count_ident(
1759                &self,
1760                pool: &#root::sql::Pool,
1761            ) -> ::core::result::Result<
1762                i64,
1763                #root::sql::ExecError,
1764            > {
1765                use #root::sql::CounterPool as _;
1766                #root::query::QuerySet::<#child>::new()
1767                    .filter(#child_fk_column, self.__rustango_pk_value())
1768                    .count(pool)
1769                    .await
1770            }
1771        }
1772    });
1773    quote! {
1774        impl #struct_name {
1775            #( #methods )*
1776        }
1777    }
1778}
1779
1780/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1781/// `#[rustango(through(...))]` attribute. Issue #817.
1782///
1783/// Each accessor builds a correlated subquery via
1784/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1785/// intermediate table, filters on the FK column pointing at this
1786/// model, and projects the intermediate PK. The outer queryset
1787/// filters the far model by its FK-to-intermediate column being in
1788/// that set.
1789///
1790/// The returned `QuerySet<Far>` is **chainable** — the subquery
1791/// lives inside a `where_raw` clause so the user's later
1792/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1793/// disturb it.
1794///
1795/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1796/// SQLite — no LATERAL or backend-specific syntax involved.
1797fn through_accessor_tokens(
1798    struct_name: &syn::Ident,
1799    through_relations: &[ThroughAttr],
1800) -> TokenStream2 {
1801    let root = rustango_root();
1802    if through_relations.is_empty() {
1803        return TokenStream2::new();
1804    }
1805    let methods = through_relations.iter().map(|rel| {
1806        let method_name = format!("{}_through", rel.name);
1807        let count_name = format!("{}_through_count", rel.name);
1808        let fetch_name = format!("{}_through_fetch", rel.name);
1809        let first_name = format!("{}_through_first", rel.name);
1810        let pluck_name = format!("{}_through_pluck", rel.name);
1811        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1812        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1813        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1814        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1815        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1816        let far = &rel.far;
1817        let intermediate = &rel.intermediate;
1818        let far_fk_column = rel.far_fk_column.as_str();
1819        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1820        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1821        let doc = format!(
1822            "Eloquent `hasManyThrough` accessor — returns a \
1823             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1824             instance through the intermediate `{intermediate}` table. \
1825             Generated SQL shape: \
1826             `… WHERE {far_fk_column} IN (SELECT \
1827             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1828             {intermediate_fk_column} = self.pk)`. Chainable like any \
1829             other QuerySet — compose `.filter()` / `.order_by()` / \
1830             `.limit()` etc. on top.",
1831        );
1832        let count_doc = format!(
1833            "Eloquent `$model->{name}->count()` analog for the \
1834             through-relation — returns the number of `{far}` rows \
1835             reachable through `{intermediate}`. Equivalent to \
1836             `self.{name}_through().count(pool)` but spelled \
1837             as a bare instance method for parity with the \
1838             `reverse_has` `<name>_count` shape.",
1839            name = rel.name,
1840        );
1841        quote! {
1842            #[doc = #doc]
1843            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1844                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1845                let intermediate_schema =
1846                    <#intermediate as #root::core::Model>::SCHEMA;
1847                let sub = SelectQuery {
1848                    where_clause: WhereExpr::Predicate(Filter {
1849                        column: #intermediate_fk_column,
1850                        op: Op::Eq,
1851                        value: self.__rustango_pk_value(),
1852                    }),
1853                    projection: ::core::option::Option::Some(
1854                        ::std::vec![#intermediate_pk_column],
1855                    ),
1856                    ..SelectQuery::new(intermediate_schema)
1857                };
1858                #root::query::QuerySet::<#far>::new().where_raw(
1859                    WhereExpr::InSubquery {
1860                        column: #far_fk_column,
1861                        negated: false,
1862                        subquery: ::std::boxed::Box::new(sub),
1863                    },
1864                )
1865            }
1866
1867            #[doc = #count_doc]
1868            pub async fn #count_ident(
1869                &self,
1870                pool: &#root::sql::Pool,
1871            ) -> ::core::result::Result<
1872                i64,
1873                #root::sql::ExecError,
1874            > {
1875                use #root::sql::CounterPool as _;
1876                self.#method_ident().count(pool).await
1877            }
1878
1879            /// Eloquent `$model->relation->get()` for the
1880            /// through-relation — bare-name hot-path over
1881            /// `self.<name>_through().fetch(&pool)`. Use this
1882            /// when you don't need further `.filter()` /
1883            /// `.order_by()` composition; falls back to the
1884            /// chainable accessor when you do. Avoids the `_pool`
1885            /// suffix on the most common call-site shape.
1886            pub async fn #fetch_ident(
1887                &self,
1888                pool: &#root::sql::Pool,
1889            ) -> ::core::result::Result<
1890                ::std::vec::Vec<#far>,
1891                #root::sql::ExecError,
1892            > {
1893                use #root::sql::FetcherPool as _;
1894                self.#method_ident().fetch(pool).await
1895            }
1896
1897            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1898            /// over `self.<name>_through().first(&pool)`. Returns
1899            /// `None` when no far rows are reachable through the
1900            /// intermediate. Useful when at most one row is
1901            /// expected (latest comment by country, primary tag,
1902            /// etc.).
1903            pub async fn #first_ident(
1904                &self,
1905                pool: &#root::sql::Pool,
1906            ) -> ::core::result::Result<
1907                ::core::option::Option<#far>,
1908                #root::sql::ExecError,
1909            > {
1910                self.#method_ident().first(pool).await
1911            }
1912
1913            /// Pluck a single column from the far rows into `Vec<U>`
1914            /// — cheaper than the typed `<Far>` decode when only one
1915            /// scalar column is needed.
1916            pub async fn #pluck_ident<U>(
1917                &self,
1918                col: &'static str,
1919                pool: &#root::sql::Pool,
1920            ) -> ::core::result::Result<
1921                ::std::vec::Vec<U>,
1922                #root::sql::ExecError,
1923            >
1924            where
1925                U: #root::sql::MaybePgScalar
1926                    + #root::sql::MaybeMyScalar
1927                    + #root::sql::MaybeSqliteScalar
1928                    + ::core::marker::Send
1929                    + ::core::marker::Unpin,
1930            {
1931                self.#method_ident()
1932                    .values_list_flat(col)
1933                    .fetch::<U>(pool)
1934                    .await
1935            }
1936        }
1937    });
1938    quote! {
1939        impl #struct_name {
1940            #( #methods )*
1941        }
1942    }
1943}
1944
1945struct ColumnEntry {
1946    /// The struct field ident, used both for the inherent const name on
1947    /// the model and for the inner column type's name.
1948    ident: syn::Ident,
1949    /// The struct's field type, used as `Column::Value`.
1950    value_ty: Type,
1951    /// Rust-side field name (e.g. `"id"`).
1952    name: String,
1953    /// SQL-side column name (e.g. `"user_id"`).
1954    column: String,
1955    /// `#root::core::FieldType::I64` etc.
1956    field_type_tokens: TokenStream2,
1957}
1958
1959struct CollectedFields {
1960    field_schemas: Vec<TokenStream2>,
1961    from_row_inits: Vec<TokenStream2>,
1962    /// Aliased counterparts of `from_row_inits` — read columns via
1963    /// `format!("{prefix}__{col}")` aliases so a Model can be
1964    /// decoded from a JOINed row's projected target columns.
1965    from_aliased_row_inits: Vec<TokenStream2>,
1966    /// Static column-name list — used by the simple insert path
1967    /// (no `Auto<T>` fields). Aligned with `insert_values`.
1968    insert_columns: Vec<TokenStream2>,
1969    /// Static `Into<SqlValue>` expressions, one per field. Aligned
1970    /// with `insert_columns`. Used by the simple insert path only.
1971    insert_values: Vec<TokenStream2>,
1972    /// Per-field push expressions for the dynamic (Auto-aware)
1973    /// insert path. Each statement either unconditionally pushes
1974    /// `(column, value)` or, for an `Auto<T>` field, conditionally
1975    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1976    insert_pushes: Vec<TokenStream2>,
1977    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1978    /// when `has_auto == false`.
1979    returning_cols: Vec<TokenStream2>,
1980    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1981    /// field. Run after `insert_returning` to populate the model.
1982    auto_assigns: Vec<TokenStream2>,
1983    /// `(ident, column_literal)` pairs for every Auto field. Used by
1984    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1985    /// instead of `self`.
1986    auto_field_idents: Vec<(syn::Ident, String)>,
1987    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1988    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1989    /// back into the struct after insert (MySQL has no RETURNING → the
1990    /// field stays at its placeholder, deferred, matching Django 6.0).
1991    generated_field_idents: Vec<(syn::Ident, String)>,
1992    /// Inner `T` of the first `Auto<T>` field, for the MySQL
1993    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1994    first_auto_value_ty: Option<Type>,
1995    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1996    /// by the all-Auto-Unset bulk path (Auto cols dropped from
1997    /// `columns`).
1998    bulk_pushes_no_auto: Vec<TokenStream2>,
1999    /// Bulk-insert per-row pushes for **all fields including Auto**.
2000    /// Used by the all-Auto-Set bulk path (Auto col included with the
2001    /// caller-supplied value).
2002    bulk_pushes_all: Vec<TokenStream2>,
2003    /// Column-name literals for non-Auto fields only (paired with
2004    /// `bulk_pushes_no_auto`).
2005    bulk_columns_no_auto: Vec<TokenStream2>,
2006    /// Column-name literals for every field including Auto (paired
2007    /// with `bulk_pushes_all`).
2008    bulk_columns_all: Vec<TokenStream2>,
2009    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
2010    /// + the loop that asserts every row matches. One pair per Auto
2011    /// field. Empty when `has_auto == false`.
2012    bulk_auto_uniformity: Vec<TokenStream2>,
2013    /// Identifier of the first Auto field, used as the witness for
2014    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
2015    first_auto_ident: Option<syn::Ident>,
2016    /// `true` if any field on the struct is `Auto<T>`.
2017    has_auto: bool,
2018    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2019    /// Gates `save()` codegen — only Auto PKs let us infer
2020    /// insert-vs-update from the in-memory value.
2021    pk_is_auto: bool,
2022    /// `Assignment` constructors for every non-PK column. Drives the
2023    /// UPDATE branch of `save()`.
2024    update_assignments: Vec<TokenStream2>,
2025    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2026    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2027    upsert_update_columns: Vec<TokenStream2>,
2028    primary_key: Option<(syn::Ident, String)>,
2029    column_entries: Vec<ColumnEntry>,
2030    /// Rust-side field names, in declaration order. Used to validate
2031    /// container attributes like `display = "…"`.
2032    field_names: Vec<String>,
2033    /// FK fields on this child model. Drives the reverse-relation
2034    /// helper emit — for each FK, the macro adds an inherent
2035    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2036    /// method on the parent type.
2037    fk_relations: Vec<FkRelation>,
2038    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2039    /// the model has one. Drives emission of the `soft_delete_on` /
2040    /// `restore_on` inherent methods. At most one such column per
2041    /// model is allowed; collect_fields rejects duplicates.
2042    soft_delete_column: Option<String>,
2043    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2044    /// companion to `soft_delete_column` for emitting predicates
2045    /// that need to read the field off `&self` (e.g. `trashed()`).
2046    soft_delete_field_ident: Option<syn::Ident>,
2047}
2048
2049#[derive(Clone)]
2050struct FkRelation {
2051    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2052    /// helper is emitted as `impl <ParentType> { … }`.
2053    parent_type: Type,
2054    /// SQL column name on the child table for this FK (e.g. `"author"`).
2055    /// Used in the generated `WHERE <fk_column> = $1` clause.
2056    fk_column: String,
2057    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2058    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2059    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2060    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2061    pk_kind: DetectedKind,
2062    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2063    /// FK column). Drives the `Some(...)` wrapping in load_related
2064    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2065    /// the codegen matches the field's declared shape.
2066    nullable: bool,
2067    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2068    /// override. When set, the reverse helper picks this name instead
2069    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2070    /// #816 (issue's "Related" note re: per-FK override).
2071    related_name: Option<String>,
2072}
2073
2074fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2075    let root = rustango_root();
2076    let cap = named.named.len();
2077    let mut out = CollectedFields {
2078        field_schemas: Vec::with_capacity(cap),
2079        from_row_inits: Vec::with_capacity(cap),
2080        from_aliased_row_inits: Vec::with_capacity(cap),
2081        insert_columns: Vec::with_capacity(cap),
2082        insert_values: Vec::with_capacity(cap),
2083        insert_pushes: Vec::with_capacity(cap),
2084        returning_cols: Vec::new(),
2085        auto_assigns: Vec::new(),
2086        auto_field_idents: Vec::new(),
2087        generated_field_idents: Vec::new(),
2088        first_auto_value_ty: None,
2089        bulk_pushes_no_auto: Vec::with_capacity(cap),
2090        bulk_pushes_all: Vec::with_capacity(cap),
2091        bulk_columns_no_auto: Vec::with_capacity(cap),
2092        bulk_columns_all: Vec::with_capacity(cap),
2093        bulk_auto_uniformity: Vec::new(),
2094        first_auto_ident: None,
2095        has_auto: false,
2096        pk_is_auto: false,
2097        update_assignments: Vec::with_capacity(cap),
2098        upsert_update_columns: Vec::with_capacity(cap),
2099        primary_key: None,
2100        column_entries: Vec::with_capacity(cap),
2101        field_names: Vec::with_capacity(cap),
2102        fk_relations: Vec::new(),
2103        soft_delete_column: None,
2104        soft_delete_field_ident: None,
2105    };
2106
2107    for field in &named.named {
2108        let info = process_field(field, table)?;
2109        out.field_names.push(info.ident.to_string());
2110        out.field_schemas.push(info.schema);
2111        out.from_row_inits.push(info.from_row_init);
2112        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2113        if let Some(parent_ty) = info.fk_inner.clone() {
2114            out.fk_relations.push(FkRelation {
2115                parent_type: parent_ty,
2116                fk_column: info.column.clone(),
2117                pk_kind: info.fk_pk_kind,
2118                nullable: info.nullable,
2119                related_name: info.related_name.clone(),
2120            });
2121        }
2122        if info.soft_delete {
2123            if out.soft_delete_column.is_some() {
2124                return Err(syn::Error::new_spanned(
2125                    field,
2126                    "only one field may be marked `#[rustango(soft_delete)]`",
2127                ));
2128            }
2129            out.soft_delete_column = Some(info.column.clone());
2130            out.soft_delete_field_ident = Some(info.ident.clone());
2131        }
2132        let column = info.column.as_str();
2133        let ident = info.ident;
2134        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2135        // skip every write path — Postgres recomputes the value
2136        // from EXPR. Push only the column-entry record (so typed
2137        // column constants still exist for filtering / projection)
2138        // and the schema literal (already pushed above) and move
2139        // on. No insert_columns/values, no insert_pushes, no
2140        // bulk_*, no update_assignments, no upsert_update_columns,
2141        // no returning_cols.
2142        if info.generated_as.is_some() {
2143            out.column_entries.push(ColumnEntry {
2144                ident: ident.clone(),
2145                value_ty: info.value_ty.clone(),
2146                name: ident.to_string(),
2147                column: info.column.clone(),
2148                field_type_tokens: info.field_type_tokens,
2149            });
2150            // #1028 — refresh the DB-computed value after insert on
2151            // RETURNING-capable backends. The column joins `returning_cols`
2152            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2153            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2154            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2155            // it keeps the placeholder (deferred refresh, matching Django).
2156            out.returning_cols.push(quote!(#column));
2157            out.generated_field_idents
2158                .push((ident.clone(), info.column.clone()));
2159            continue;
2160        }
2161        out.insert_columns.push(quote!(#column));
2162        out.insert_values.push(quote! {
2163            ::core::convert::Into::<#root::core::SqlValue>::into(
2164                ::core::clone::Clone::clone(&self.#ident)
2165            )
2166        });
2167        if info.auto {
2168            out.has_auto = true;
2169            if out.first_auto_ident.is_none() {
2170                out.first_auto_ident = Some(ident.clone());
2171                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2172            }
2173            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2174            // before binding, so the value is already in
2175            // `self.#ident` after the insert_push — RETURNING is
2176            // redundant. Skip adding this column to returning_cols /
2177            // auto_assigns / auto_field_idents to avoid (a) an
2178            // unnecessary RETURNING column on every dialect, and (b)
2179            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2180            // integer PK.
2181            if !info.default_uuid_v7 {
2182                out.returning_cols.push(quote!(#column));
2183                out.auto_field_idents
2184                    .push((ident.clone(), info.column.clone()));
2185                out.auto_assigns.push(quote! {
2186                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2187                });
2188            }
2189            if info.default_uuid_v7 {
2190                // Rust-side UUIDv7 generation (issue #823, Eloquent
2191                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2192                // and bind; Auto::Set → bind the user's value. The
2193                // column is ALWAYS present in the INSERT statement —
2194                // no RETURNING / no DB DEFAULT needed.
2195                out.insert_pushes.push(quote! {
2196                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2197                        self.#ident = #root::sql::Auto::Set(
2198                            #root::__uuid::Uuid::now_v7(),
2199                        );
2200                    }
2201                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2202                        _columns.push(#column);
2203                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2204                            ::core::clone::Clone::clone(_v)
2205                        ));
2206                    }
2207                });
2208            } else {
2209                out.insert_pushes.push(quote! {
2210                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2211                        _columns.push(#column);
2212                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2213                            ::core::clone::Clone::clone(_v)
2214                        ));
2215                    }
2216                });
2217            }
2218            // Bulk: Auto fields appear only in the all-Set path,
2219            // never in the Unset path (we drop them from `columns`).
2220            out.bulk_columns_all.push(quote!(#column));
2221            out.bulk_pushes_all.push(quote! {
2222                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2223                    ::core::clone::Clone::clone(&_row.#ident)
2224                ));
2225            });
2226            // Uniformity check: every row's Auto state must match the
2227            // first row's. Mixed Set/Unset within one bulk_insert is
2228            // rejected here so the column list stays consistent.
2229            let ident_clone = ident.clone();
2230            out.bulk_auto_uniformity.push(quote! {
2231                for _r in rows.iter().skip(1) {
2232                    if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2233                        return ::core::result::Result::Err(
2234                            #root::sql::ExecError::Sql(
2235                                #root::sql::SqlError::BulkAutoMixed
2236                            )
2237                        );
2238                    }
2239                }
2240            });
2241        } else {
2242            out.insert_pushes.push(quote! {
2243                _columns.push(#column);
2244                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2245                    ::core::clone::Clone::clone(&self.#ident)
2246                ));
2247            });
2248            // Bulk: non-Auto fields appear in BOTH paths.
2249            out.bulk_columns_no_auto.push(quote!(#column));
2250            out.bulk_columns_all.push(quote!(#column));
2251            let push_expr = quote! {
2252                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2253                    ::core::clone::Clone::clone(&_row.#ident)
2254                ));
2255            };
2256            out.bulk_pushes_no_auto.push(push_expr.clone());
2257            out.bulk_pushes_all.push(push_expr);
2258        }
2259        if info.primary_key {
2260            if out.primary_key.is_some() {
2261                return Err(syn::Error::new_spanned(
2262                    field,
2263                    "only one field may be marked `#[rustango(primary_key)]`",
2264                ));
2265            }
2266            out.primary_key = Some((ident.clone(), info.column.clone()));
2267            if info.auto {
2268                out.pk_is_auto = true;
2269            }
2270        } else if info.auto_now_add {
2271            // Immutable post-insert: skip from UPDATE entirely.
2272        } else if info.auto_now {
2273            // `auto_now` columns: bind `chrono::Utc::now()` on every
2274            // UPDATE so the column is always overridden with the
2275            // wall-clock at write time, regardless of what value the
2276            // user left in the struct field.
2277            out.update_assignments.push(quote! {
2278                #root::core::Assignment {
2279                    column: #column,
2280                    value: ::core::convert::Into::<#root::core::Expr>::into(
2281                        ::core::convert::Into::<#root::core::SqlValue>::into(
2282                            #root::__chrono::Utc::now()
2283                        )
2284                    ),
2285                }
2286            });
2287            out.upsert_update_columns.push(quote!(#column));
2288        } else {
2289            out.update_assignments.push(quote! {
2290                #root::core::Assignment {
2291                    column: #column,
2292                    value: ::core::convert::Into::<#root::core::Expr>::into(
2293                        ::core::convert::Into::<#root::core::SqlValue>::into(
2294                            ::core::clone::Clone::clone(&self.#ident)
2295                        )
2296                    ),
2297                }
2298            });
2299            out.upsert_update_columns.push(quote!(#column));
2300        }
2301        out.column_entries.push(ColumnEntry {
2302            ident: ident.clone(),
2303            value_ty: info.value_ty.clone(),
2304            name: ident.to_string(),
2305            column: info.column.clone(),
2306            field_type_tokens: info.field_type_tokens,
2307        });
2308    }
2309    Ok(out)
2310}
2311
2312fn model_impl_tokens(
2313    struct_name: &syn::Ident,
2314    model_name: &str,
2315    table: &str,
2316    display: Option<&str>,
2317    app_label: Option<&str>,
2318    admin: Option<&AdminAttrs>,
2319    default_order: &[(String, bool, proc_macro2::Span)],
2320    field_schemas: &[TokenStream2],
2321    soft_delete_column: Option<&str>,
2322    permissions: bool,
2323    audit_track: Option<&[String]>,
2324    m2m_relations: &[M2MAttr],
2325    indexes: &[IndexAttr],
2326    checks: &[CheckAttr],
2327    excludes: &[ExcludeAttr],
2328    composite_fks: &[CompositeFkAttr],
2329    generic_fks: &[GenericFkAttr],
2330    scope: Option<&str>,
2331    is_view: bool,
2332    verbose_name: Option<&str>,
2333    verbose_name_plural: Option<&str>,
2334    managed: bool,
2335    base_manager_name: Option<&str>,
2336    order_with_respect_to: Option<&str>,
2337    proxy: bool,
2338    required_db_features: &[String],
2339    required_db_vendor: Option<&str>,
2340    default_related_name: Option<&str>,
2341    db_table_comment: Option<&str>,
2342    get_latest_by: Option<(&str, bool)>,
2343    extra_permissions: &[(String, String)],
2344    default_permissions: &[String],
2345    global_scopes: &[GlobalScopeAttr],
2346    reverse_has_relations: &[ReverseHasAttr],
2347    generic_has_relations: &[GenericHasAttr],
2348) -> TokenStream2 {
2349    let root = rustango_root();
2350    let display_tokens = if let Some(name) = display {
2351        quote!(::core::option::Option::Some(#name))
2352    } else {
2353        quote!(::core::option::Option::None)
2354    };
2355    let app_label_tokens = if let Some(name) = app_label {
2356        quote!(::core::option::Option::Some(#name))
2357    } else {
2358        quote!(::core::option::Option::None)
2359    };
2360    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2361        quote!(::core::option::Option::Some(#col))
2362    } else {
2363        quote!(::core::option::Option::None)
2364    };
2365    let audit_track_tokens = match audit_track {
2366        None => quote!(::core::option::Option::None),
2367        Some(names) => {
2368            let lits = names.iter().map(|n| n.as_str());
2369            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2370        }
2371    };
2372    let admin_tokens = admin_config_tokens(admin);
2373    // Default `tenant` so single-tenant projects (no `scope` attr
2374    // anywhere) keep the v0.24.x behavior. Container-attr parser
2375    // already validated the value is "registry" or "tenant".
2376    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2377        Some("registry") => quote!(#root::core::ModelScope::Registry),
2378        _ => quote!(#root::core::ModelScope::Tenant),
2379    };
2380    let verbose_name_tokens = optional_str(verbose_name);
2381    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2382    let base_manager_name_tokens = optional_str(base_manager_name);
2383    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2384    let required_db_features_lits: Vec<&str> =
2385        required_db_features.iter().map(String::as_str).collect();
2386    let required_db_vendor_tokens = optional_str(required_db_vendor);
2387    let default_related_name_tokens = optional_str(default_related_name);
2388    let db_table_comment_tokens = optional_str(db_table_comment);
2389    let get_latest_by_tokens = match get_latest_by {
2390        Some((col, desc)) => {
2391            quote!(::core::option::Option::Some((#col, #desc)))
2392        }
2393        None => quote!(::core::option::Option::None),
2394    };
2395    let extra_permission_tokens: Vec<_> = extra_permissions
2396        .iter()
2397        .map(|(c, l)| quote!((#c, #l)))
2398        .collect();
2399    let default_permission_tokens: Vec<_> = default_permissions
2400        .iter()
2401        .map(|action| quote!(#action))
2402        .collect();
2403    let indexes_tokens = indexes.iter().map(|idx| {
2404        // When no explicit `name = "..."` was given, derive a stable,
2405        // collision-free name from the table + columns (Django-shape
2406        // `<table>_<col>_<col>_idx`) instead of a shared literal that
2407        // would clash the moment a model declares two unnamed indexes.
2408        // Capped at Postgres's 63-char identifier limit.
2409        let derived_name = idx.name.clone().unwrap_or_else(|| {
2410            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2411            if n.len() > 63 {
2412                n.truncate(63);
2413            }
2414            n
2415        });
2416        let name = derived_name.as_str();
2417        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2418        let unique = idx.unique;
2419        // Map the parsed method string onto the IndexMethod enum
2420        // variant — kept at the codegen layer so the IR doesn't
2421        // carry the string form.
2422        let method_variant = match idx.method.as_str() {
2423            "gin" => quote!(#root::core::IndexMethod::Gin),
2424            "gist" => quote!(#root::core::IndexMethod::Gist),
2425            "brin" => quote!(#root::core::IndexMethod::Brin),
2426            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2427            "hash" => quote!(#root::core::IndexMethod::Hash),
2428            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2429            _ => quote!(#root::core::IndexMethod::BTree),
2430        };
2431        let where_clause = match &idx.where_clause {
2432            Some(s) => quote!(::core::option::Option::Some(#s)),
2433            None => quote!(::core::option::Option::None),
2434        };
2435        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2436        quote! {
2437            #root::core::IndexSchema {
2438                name: #name,
2439                columns: &[ #(#cols),* ],
2440                unique: #unique,
2441                method: #method_variant,
2442                where_clause: #where_clause,
2443                include: &[ #(#include_lits),* ],
2444            }
2445        }
2446    });
2447    let checks_tokens = checks.iter().map(|c| {
2448        let name = c.name.as_str();
2449        let expr = c.expr.as_str();
2450        quote! {
2451            #root::core::CheckConstraint {
2452                name: #name,
2453                expr: #expr,
2454            }
2455        }
2456    });
2457    let excludes_tokens = excludes.iter().map(|e| {
2458        let name = e.name.as_str();
2459        let using = e.using.as_str();
2460        let element_tokens = e.elements.iter().map(|(col, op)| {
2461            let col_s = col.as_str();
2462            let op_s = op.as_str();
2463            quote!((#col_s, #op_s))
2464        });
2465        let where_tokens = match e.where_clause.as_deref() {
2466            Some(w) => quote!(::core::option::Option::Some(#w)),
2467            None => quote!(::core::option::Option::None),
2468        };
2469        quote! {
2470            #root::core::ExclusionConstraint {
2471                name: #name,
2472                using: #using,
2473                elements: &[ #(#element_tokens),* ],
2474                where_clause: #where_tokens,
2475            }
2476        }
2477    });
2478    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2479        let name = rel.name.as_str();
2480        let to = rel.to.as_str();
2481        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2482        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2483        quote! {
2484            #root::core::CompositeFkRelation {
2485                name: #name,
2486                to: #to,
2487                from: &[ #(#from_cols),* ],
2488                on: &[ #(#on_cols),* ],
2489            }
2490        }
2491    });
2492    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2493        let name = rel.name.as_str();
2494        let ct_col = rel.ct_column.as_str();
2495        let pk_col = rel.pk_column.as_str();
2496        quote! {
2497            #root::core::GenericRelation {
2498                name: #name,
2499                ct_column: #ct_col,
2500                pk_column: #pk_col,
2501            }
2502        }
2503    });
2504    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2505    // no `#[rustango(default_order = "...")]` attribute was supplied.
2506    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2507        let col_lit = col.as_str();
2508        quote! { (#col_lit, #desc) }
2509    });
2510
2511    // Issue #820 — `global_scopes` slice literal. Empty when no
2512    // `#[rustango(global_scope(...))]` attribute was supplied. The
2513    // `apply` path is re-emitted verbatim so it resolves in the
2514    // consumer's scope; the name is stored as a string literal.
2515    let global_scope_tokens = global_scopes.iter().map(|s| {
2516        let name = s.name.as_str();
2517        let apply = &s.apply;
2518        quote! {
2519            #root::core::GlobalScope {
2520                name: #name,
2521                apply: #apply,
2522            }
2523        }
2524    });
2525
2526    let m2m_tokens = m2m_relations.iter().map(|rel| {
2527        let name = rel.name.as_str();
2528        let to = rel.to.as_str();
2529        let through = rel.through.as_str();
2530        let src = rel.src.as_str();
2531        let dst = rel.dst.as_str();
2532        let auto_create = rel.auto_create;
2533        quote! {
2534            #root::core::M2MRelation {
2535                name: #name,
2536                to: #to,
2537                through: #through,
2538                src_col: #src,
2539                dst_col: #dst,
2540                auto_create: #auto_create,
2541            }
2542        }
2543    });
2544    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2545    // when the model declares `#[rustango(reverse_has(...))]`. Each
2546    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2547    // a const expression. Models without reverse_has fall through to
2548    // the trait's empty default — no override emitted.
2549    let reverse_relations_override = if reverse_has_relations.is_empty() {
2550        quote!()
2551    } else {
2552        let entries = reverse_has_relations.iter().map(|rel| {
2553            let name = rel.name.as_str();
2554            let child = &rel.child;
2555            let child_fk_column = rel.child_fk_column.as_str();
2556            let self_pk_column = rel.self_pk_column.as_str();
2557            quote! {
2558                #root::core::ReverseRelation {
2559                    name: #name,
2560                    child_schema: <#child as #root::core::Model>::SCHEMA,
2561                    child_fk_column: #child_fk_column,
2562                    self_pk_column: #self_pk_column,
2563                }
2564            }
2565        });
2566        quote! {
2567            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2568                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2569                RELS
2570            }
2571        }
2572    };
2573    // Issue #830 — `Model::generic_reverse_relations()` override for
2574    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2575    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2576        quote!()
2577    } else {
2578        let entries = generic_has_relations.iter().map(|rel| {
2579            let name = rel.name.as_str();
2580            let child = &rel.child;
2581            let ct_column = rel.ct_column.as_str();
2582            let pk_column = rel.pk_column.as_str();
2583            let self_pk_column = rel.self_pk_column.as_str();
2584            quote! {
2585                #root::core::GenericReverseRelation {
2586                    name: #name,
2587                    child_schema: <#child as #root::core::Model>::SCHEMA,
2588                    ct_column: #ct_column,
2589                    pk_column: #pk_column,
2590                    self_pk_column: #self_pk_column,
2591                }
2592            }
2593        });
2594        quote! {
2595            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2596                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2597                RELS
2598            }
2599        }
2600    };
2601    quote! {
2602        impl #root::core::Model for #struct_name {
2603            const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2604                name: #model_name,
2605                table: #table,
2606                fields: &[ #(#field_schemas),* ],
2607                display: #display_tokens,
2608                app_label: #app_label_tokens,
2609                admin: #admin_tokens,
2610                soft_delete_column: #soft_delete_tokens,
2611                permissions: #permissions,
2612                audit_track: #audit_track_tokens,
2613                m2m: &[ #(#m2m_tokens),* ],
2614                indexes: &[ #(#indexes_tokens),* ],
2615                check_constraints: &[ #(#checks_tokens),* ],
2616                exclusion_constraints: &[ #(#excludes_tokens),* ],
2617                composite_relations: &[ #(#composite_fk_tokens),* ],
2618                generic_relations: &[ #(#generic_fk_tokens),* ],
2619                scope: #scope_tokens,
2620                default_order: &[ #(#default_order_tokens),* ],
2621                is_view: #is_view,
2622                verbose_name: #verbose_name_tokens,
2623                verbose_name_plural: #verbose_name_plural_tokens,
2624                managed: #managed,
2625                base_manager_name: #base_manager_name_tokens,
2626                order_with_respect_to: #order_with_respect_to_tokens,
2627                proxy: #proxy,
2628                required_db_features: &[ #(#required_db_features_lits),* ],
2629                required_db_vendor: #required_db_vendor_tokens,
2630                default_related_name: #default_related_name_tokens,
2631                db_table_comment: #db_table_comment_tokens,
2632                get_latest_by: #get_latest_by_tokens,
2633                extra_permissions: &[ #(#extra_permission_tokens),* ],
2634                default_permissions: &[ #(#default_permission_tokens),* ],
2635                global_scopes: &[ #(#global_scope_tokens),* ],
2636            };
2637
2638            #reverse_relations_override
2639            #generic_reverse_relations_override
2640        }
2641    }
2642}
2643
2644/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2645/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2646/// otherwise a static reference to a populated `AdminConfig`.
2647fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2648    let root = rustango_root();
2649    let Some(admin) = admin else {
2650        return quote!(::core::option::Option::None);
2651    };
2652
2653    let list_display = admin
2654        .list_display
2655        .as_ref()
2656        .map(|(v, _)| v.as_slice())
2657        .unwrap_or(&[]);
2658    let list_display_lits = list_display.iter().map(|s| s.as_str());
2659
2660    let search_fields = admin
2661        .search_fields
2662        .as_ref()
2663        .map(|(v, _)| v.as_slice())
2664        .unwrap_or(&[]);
2665    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2666
2667    let readonly_fields = admin
2668        .readonly_fields
2669        .as_ref()
2670        .map(|(v, _)| v.as_slice())
2671        .unwrap_or(&[]);
2672    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2673
2674    let list_filter = admin
2675        .list_filter
2676        .as_ref()
2677        .map(|(v, _)| v.as_slice())
2678        .unwrap_or(&[]);
2679    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2680
2681    let actions = admin
2682        .actions
2683        .as_ref()
2684        .map(|(v, _)| v.as_slice())
2685        .unwrap_or(&[]);
2686    let actions_lits = actions.iter().map(|s| s.as_str());
2687
2688    let fieldsets = admin
2689        .fieldsets
2690        .as_ref()
2691        .map(|(v, _)| v.as_slice())
2692        .unwrap_or(&[]);
2693    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2694        let title = title.as_str();
2695        let field_lits = fields.iter().map(|s| s.as_str());
2696        quote!(#root::core::Fieldset {
2697            title: #title,
2698            fields: &[ #( #field_lits ),* ],
2699        })
2700    });
2701
2702    let list_display_links = admin
2703        .list_display_links
2704        .as_ref()
2705        .map(|(v, _)| v.as_slice())
2706        .unwrap_or(&[]);
2707    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2708
2709    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2710    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2711    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2712    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2713
2714    let prepopulated = admin
2715        .prepopulated_fields
2716        .as_ref()
2717        .map(|(v, _)| v.as_slice())
2718        .unwrap_or(&[]);
2719    let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2720        let target = target.as_str();
2721        let source_lits = sources.iter().map(|s| s.as_str());
2722        quote!(#root::core::PrepopulatedField {
2723            target: #target,
2724            sources: &[ #( #source_lits ),* ],
2725        })
2726    });
2727
2728    let raw_id_fields = admin
2729        .raw_id_fields
2730        .as_ref()
2731        .map(|(v, _)| v.as_slice())
2732        .unwrap_or(&[]);
2733    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2734
2735    let autocomplete_fields = admin
2736        .autocomplete_fields
2737        .as_ref()
2738        .map(|(v, _)| v.as_slice())
2739        .unwrap_or(&[]);
2740    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2741
2742    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2743    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2744        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2745        Some("none") => quote!(#root::core::ListSelectRelated::None),
2746        Some(raw) => {
2747            let names: Vec<&str> = raw
2748                .split(',')
2749                .map(str::trim)
2750                .filter(|s| !s.is_empty())
2751                .collect();
2752            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2753        }
2754    };
2755
2756    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2757    // a Vec<(String, String)>. Empty / unset → no overrides.
2758    let formfield_pairs: Vec<(&str, &str)> = admin
2759        .formfield_overrides
2760        .as_ref()
2761        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2762        .unwrap_or_default();
2763    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2764        let field = *field;
2765        let widget = *widget;
2766        quote!((#field, #widget))
2767    });
2768
2769    let list_per_page = admin.list_per_page.unwrap_or(0);
2770
2771    let ordering_pairs = admin
2772        .ordering
2773        .as_ref()
2774        .map(|(v, _)| v.as_slice())
2775        .unwrap_or(&[]);
2776    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2777        let name = name.as_str();
2778        let desc = *desc;
2779        quote!((#name, #desc))
2780    });
2781
2782    quote! {
2783        ::core::option::Option::Some(&#root::core::AdminConfig {
2784            list_display: &[ #( #list_display_lits ),* ],
2785            search_fields: &[ #( #search_fields_lits ),* ],
2786            list_per_page: #list_per_page,
2787            ordering: &[ #( #ordering_tokens ),* ],
2788            readonly_fields: &[ #( #readonly_fields_lits ),* ],
2789            list_filter: &[ #( #list_filter_lits ),* ],
2790            actions: &[ #( #actions_lits ),* ],
2791            fieldsets: &[ #( #fieldset_tokens ),* ],
2792            list_display_links: &[ #( #list_display_links_lits ),* ],
2793            search_help_text: #search_help_text,
2794            actions_on_top: #actions_on_top,
2795            actions_on_bottom: #actions_on_bottom,
2796            date_hierarchy: #date_hierarchy,
2797            prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2798            raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2799            autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2800            list_select_related: #list_select_related_tokens,
2801            formfield_overrides: &[ #( #formfield_tokens ),* ],
2802        })
2803    }
2804}
2805
2806fn inherent_impl_tokens(
2807    struct_name: &syn::Ident,
2808    fields: &CollectedFields,
2809    primary_key: Option<&(syn::Ident, String)>,
2810    column_consts: &TokenStream2,
2811    audited_fields: Option<&[&ColumnEntry]>,
2812    indexes: &[IndexAttr],
2813    manager_fns: &[syn::Ident],
2814) -> TokenStream2 {
2815    let root = rustango_root();
2816    // Audit-emit fragments threaded into write paths. Non-empty only
2817    // when the model carries `#[rustango(audit(...))]`. They reborrow
2818    // `_executor` (a `&mut PgConnection` for audited models — the
2819    // macro switches the signature below) so the data write and the
2820    // audit INSERT both run on the same caller-supplied connection.
2821    let executor_passes_to_data_write = if audited_fields.is_some() {
2822        quote!(&mut *_executor)
2823    } else {
2824        quote!(_executor)
2825    };
2826    let executor_param = if audited_fields.is_some() {
2827        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2828    } else {
2829        quote!(_executor: _E)
2830    };
2831    let executor_generics = if audited_fields.is_some() {
2832        quote!()
2833    } else {
2834        quote!(<'_c, _E>)
2835    };
2836    let executor_where = if audited_fields.is_some() {
2837        quote!()
2838    } else {
2839        quote! {
2840            where
2841                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2842        }
2843    };
2844    // For audited models the `_on` methods take `&mut PgConnection`, so
2845    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2846    // must acquire a connection first. Non-audited models keep the
2847    // direct delegation since `&PgPool` IS an Executor.
2848    let pool_to_save_on = if audited_fields.is_some() {
2849        quote! {
2850            let mut _conn = pool.acquire().await?;
2851            self.save_on(&mut *_conn).await
2852        }
2853    } else {
2854        quote!(self.save_on(pool).await)
2855    };
2856    let pool_to_insert_on = if audited_fields.is_some() {
2857        quote! {
2858            let mut _conn = pool.acquire().await?;
2859            self.insert_on(&mut *_conn).await
2860        }
2861    } else {
2862        quote!(self.insert_on(pool).await)
2863    };
2864    let pool_to_delete_on = if audited_fields.is_some() {
2865        quote! {
2866            let mut _conn = pool.acquire().await?;
2867            self.delete_on(&mut *_conn).await
2868        }
2869    } else {
2870        quote!(self.delete_on(pool).await)
2871    };
2872    let pool_to_bulk_insert_on = if audited_fields.is_some() {
2873        quote! {
2874            let mut _conn = pool.acquire().await?;
2875            Self::bulk_insert_on(rows, &mut *_conn).await
2876        }
2877    } else {
2878        quote!(Self::bulk_insert_on(rows, pool).await)
2879    };
2880    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2881    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2882    // directly, but `upsert_on` for audited models takes
2883    // `&mut PgConnection` (the audit emit needs a real connection).
2884    // Add the missing acquire shim to keep audited Auto-PK upsert
2885    // compiling.
2886    let pool_to_upsert_on = if audited_fields.is_some() {
2887        quote! {
2888            let mut _conn = pool.acquire().await?;
2889            self.upsert_on(&mut *_conn).await
2890        }
2891    } else {
2892        quote!(self.upsert_on(pool).await)
2893    };
2894
2895    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2896    // (audit-on-connection over &Pool needs a bi-dialect transaction
2897    // helper, deferred). Two body shapes:
2898    // - has_auto: build InsertQuery skipping Auto::Unset columns,
2899    //   request Auto cols in `returning`, dispatch via
2900    //   `insert_returning_pool`, then on the returned `PgRow` /
2901    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
2902    //   row OR drop the single i64 into the first Auto field on MySQL
2903    //   (multi-Auto models on MySQL error at runtime since
2904    //   `LAST_INSERT_ID()` only reports one)
2905    // - non-Auto: build InsertQuery with explicit columns/values and
2906    //   call `insert_pool` (no returning needed)
2907    // pool_insert_method body for the audited Auto-PK case is moved
2908    // to after audit_pair_tokens / audit_pk_to_string (they live
2909    // ~150 lines below). This block keeps the non-audited and
2910    // non-Auto branches in place — the audited Auto-PK arm is
2911    // computed below and merged via the dispatch helper variable.
2912    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2913        // Audited models with explicit (non-Auto) PKs go through
2914        // the non-Auto insert path below — the audit emit is one
2915        // round-trip after the INSERT inside the same tx via
2916        // audit::save_one_with_audit? No, INSERT semantics
2917        // differ. For non-Auto PK + audited, route through a
2918        // dedicated insert + audit emit on the same tx, but defer
2919        // the macro emission to the audit-bundle-aware block below
2920        // — this `quote!()` placeholder gets overwritten there.
2921        quote!()
2922    } else if audited_fields.is_some() && fields.has_auto {
2923        // Audited Auto-PK insert_pool — assembled after the audit
2924        // bundles. Placeholder; real emission below.
2925        quote!()
2926    } else if fields.has_auto {
2927        let pushes = &fields.insert_pushes;
2928        let returning_cols = &fields.returning_cols;
2929        // When every `Auto<T>` field is filled Rust-side
2930        // (`default_uuid_v7`, issue #823), there is no column to read
2931        // back from the database — `returning_cols` is empty. Route
2932        // through plain `insert_pool` instead of
2933        // `insert_returning_pool` to skip the redundant RETURNING /
2934        // LAST_INSERT_ID round-trip.
2935        if fields.returning_cols.is_empty() {
2936            quote! {
2937                /// Insert this row against either backend. Every
2938                /// `Auto<T>` PK on this model is filled Rust-side
2939                /// (e.g. `default_uuid_v7`) before binding, so no
2940                /// RETURNING round-trip is needed.
2941                ///
2942                /// # Errors
2943                /// As [`Self::insert`].
2944                pub async fn insert_pool(
2945                    &mut self,
2946                    pool: &#root::sql::Pool,
2947                ) -> ::core::result::Result<(), #root::sql::ExecError> {
2948                    let mut _columns: ::std::vec::Vec<&'static str> =
2949                        ::std::vec::Vec::new();
2950                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2951                        ::std::vec::Vec::new();
2952                    #( #pushes )*
2953                    let _query = #root::core::InsertQuery {
2954                        model: <Self as #root::core::Model>::SCHEMA,
2955                        columns: _columns,
2956                        values: _values,
2957                        returning: ::std::vec::Vec::new(),
2958                        on_conflict: ::core::option::Option::None,
2959                    };
2960                    #root::sql::insert_pool(pool, &_query).await
2961                }
2962
2963                /// Eloquent `Model::insertOrIgnore()` — same shape
2964                /// as the auto-PK branch above. Returns `Ok(true)`
2965                /// when inserted, `Ok(false)` when a conflict caused
2966                /// the INSERT to silently skip.
2967                ///
2968                /// # Errors
2969                /// As [`Self::insert`].
2970                pub async fn insert_or_ignore(
2971                    &mut self,
2972                    pool: &#root::sql::Pool,
2973                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2974                    let mut _columns: ::std::vec::Vec<&'static str> =
2975                        ::std::vec::Vec::new();
2976                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2977                        ::std::vec::Vec::new();
2978                    #( #pushes )*
2979                    let _query = #root::core::InsertQuery {
2980                        model: <Self as #root::core::Model>::SCHEMA,
2981                        columns: _columns,
2982                        values: _values,
2983                        returning: ::std::vec::Vec::new(),
2984                        on_conflict: ::core::option::Option::Some(
2985                            #root::core::ConflictClause::DoNothing,
2986                        ),
2987                    };
2988                    let dialect = pool.dialect();
2989                    let stmt = dialect.compile_insert(&_query)?;
2990                    let rows = #root::sql::raw_execute_pool(
2991                        pool, &stmt.sql, stmt.params,
2992                    ).await?;
2993                    ::core::result::Result::Ok(rows > 0)
2994                }
2995            }
2996        } else {
2997            quote! {
2998                /// Insert this row against either backend, populating any
2999                /// `Auto<T>` PK from the auto-assigned value.
3000                ///
3001                /// # Errors
3002                /// As [`Self::insert`].
3003                pub async fn insert_pool(
3004                    &mut self,
3005                    pool: &#root::sql::Pool,
3006                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3007                    let mut _columns: ::std::vec::Vec<&'static str> =
3008                        ::std::vec::Vec::new();
3009                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3010                        ::std::vec::Vec::new();
3011                    #( #pushes )*
3012                    let _query = #root::core::InsertQuery {
3013                        model: <Self as #root::core::Model>::SCHEMA,
3014                        columns: _columns,
3015                        values: _values,
3016                        returning: ::std::vec![ #( #returning_cols ),* ],
3017                        on_conflict: ::core::option::Option::None,
3018                    };
3019                    let _result = #root::sql::insert_returning_pool(
3020                        pool, &_query,
3021                    ).await?;
3022                    #root::sql::apply_auto_pk(_result, self)
3023                }
3024
3025                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3026                /// row or silently skip on unique-constraint
3027                /// violation. Returns `Ok(true)` when a row was
3028                /// inserted, `Ok(false)` when a conflict caused the
3029                /// INSERT to silently skip.
3030                ///
3031                /// **Caveat on auto-PK models**: when the row is
3032                /// skipped (conflict), this instance's `Auto<T>`
3033                /// fields stay `Unset` — no PK is back-populated
3034                /// because the server didn't auto-assign one. For
3035                /// "insert then read back the PK or the existing
3036                /// row's PK", use the `upsert` family or
3037                /// `get_or_create`.
3038                ///
3039                /// # Errors
3040                /// As [`Self::insert`].
3041                pub async fn insert_or_ignore(
3042                    &mut self,
3043                    pool: &#root::sql::Pool,
3044                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3045                    let mut _columns: ::std::vec::Vec<&'static str> =
3046                        ::std::vec::Vec::new();
3047                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3048                        ::std::vec::Vec::new();
3049                    #( #pushes )*
3050                    let _query = #root::core::InsertQuery {
3051                        model: <Self as #root::core::Model>::SCHEMA,
3052                        columns: _columns,
3053                        values: _values,
3054                        returning: ::std::vec::Vec::new(),
3055                        on_conflict: ::core::option::Option::Some(
3056                            #root::core::ConflictClause::DoNothing,
3057                        ),
3058                    };
3059                    let dialect = pool.dialect();
3060                    let stmt = dialect.compile_insert(&_query)?;
3061                    let rows = #root::sql::raw_execute_pool(
3062                        pool, &stmt.sql, stmt.params,
3063                    ).await?;
3064                    ::core::result::Result::Ok(rows > 0)
3065                }
3066            }
3067        }
3068    } else {
3069        let insert_columns = &fields.insert_columns;
3070        let insert_values = &fields.insert_values;
3071        quote! {
3072            /// Insert this row into its table against either backend.
3073            /// Equivalent to [`Self::insert`] but takes
3074            /// [`#root::sql::Pool`].
3075            ///
3076            /// # Errors
3077            /// As [`Self::insert`].
3078            pub async fn insert_pool(
3079                &self,
3080                pool: &#root::sql::Pool,
3081            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3082                let _query = #root::core::InsertQuery {
3083                    model: <Self as #root::core::Model>::SCHEMA,
3084                    columns: ::std::vec![ #( #insert_columns ),* ],
3085                    values: ::std::vec![ #( #insert_values ),* ],
3086                    returning: ::std::vec::Vec::new(),
3087                    on_conflict: ::core::option::Option::None,
3088                };
3089                #root::sql::insert_pool(pool, &_query).await
3090            }
3091
3092            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3093            /// or silently skip on unique-constraint violation. Maps
3094            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3095            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3096            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3097            /// `INSERT IGNORE INTO …`.
3098            ///
3099            /// Returns `Ok(true)` when a row was inserted,
3100            /// `Ok(false)` when a conflict caused the INSERT to
3101            /// silently skip.
3102            ///
3103            /// Use for "create-if-absent" patterns where you don't
3104            /// need the row back. For "find-or-create with the row
3105            /// returned", use the queryset-level
3106            /// `crate::sql::get_or_create` free function.
3107            ///
3108            /// # Errors
3109            /// As [`Self::insert`], plus any dialect-specific
3110            /// translation error from the ConflictClause writer.
3111            pub async fn insert_or_ignore(
3112                &self,
3113                pool: &#root::sql::Pool,
3114            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3115                let _query = #root::core::InsertQuery {
3116                    model: <Self as #root::core::Model>::SCHEMA,
3117                    columns: ::std::vec![ #( #insert_columns ),* ],
3118                    values: ::std::vec![ #( #insert_values ),* ],
3119                    returning: ::std::vec::Vec::new(),
3120                    on_conflict: ::core::option::Option::Some(
3121                        #root::core::ConflictClause::DoNothing,
3122                    ),
3123                };
3124                let dialect = pool.dialect();
3125                let stmt = dialect.compile_insert(&_query)?;
3126                let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3127                ::core::result::Result::Ok(rows > 0)
3128            }
3129        }
3130    };
3131
3132    // pool_save_method moved to after audit_pair_tokens /
3133    // audit_pk_to_string (they live ~70 lines below) — needed for
3134    // the audited branch which builds an UpdateQuery + PendingEntry
3135    // and dispatches via audit::save_one_with_audit.
3136
3137    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3138    // are computed (they live ~80 lines below).
3139
3140    // Build the (column, JSON value) pair list used by every
3141    // snapshot-style audit emission. Reused across delete_on,
3142    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3143    // when the model isn't audited.
3144    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3145        .map(|tracked| {
3146            tracked
3147                .iter()
3148                .map(|c| {
3149                    let column_lit = c.column.as_str();
3150                    let ident = &c.ident;
3151                    quote! {
3152                        (
3153                            #column_lit,
3154                            #root::__serde_json::to_value(&self.#ident)
3155                                .unwrap_or(#root::__serde_json::Value::Null),
3156                        )
3157                    }
3158                })
3159                .collect()
3160        })
3161        .unwrap_or_default();
3162    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3163        if fields.pk_is_auto {
3164            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3165        } else {
3166            quote!(::std::format!("{}", &self.#pk_ident))
3167        }
3168    } else {
3169        quote!(::std::string::String::new())
3170    };
3171    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3172        if audited_fields.is_some() {
3173            let pairs = audit_pair_tokens.iter();
3174            let pk_str = audit_pk_to_string.clone();
3175            quote! {
3176                let _audit_entry = #root::audit::PendingEntry {
3177                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3178                    entity_pk: #pk_str,
3179                    operation: #op_path,
3180                    source: #root::audit::current_source(),
3181                    changes: #root::audit::snapshot_changes(&[
3182                        #( #pairs ),*
3183                    ]),
3184                };
3185                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3186            }
3187        } else {
3188            quote!()
3189        }
3190    };
3191    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3192    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3193    let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3194    let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3195
3196    // `save_pool(&Pool)` — emitted for every model with a PK.
3197    // Audited Auto-PK models are deferred (the Auto::Unset →
3198    // insert_pool path needs the audited-insert flow from a future
3199    // batch). Three body shapes:
3200    // - non-audited, plain PK: build UpdateQuery + dispatch through
3201    //   sql::update_pool
3202    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3203    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3204    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3205    //   through audit::save_one_with_audit (per-backend tx wraps
3206    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3207    //   write field values) — diff-style audit (with pre-UPDATE
3208    //   SELECT for `before` values) needs per-tracked-column codegen
3209    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3210    //   `save` keeps the diff for now.
3211    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3212        let pk_column_lit = pk_col.as_str();
3213        let assignments = &fields.update_assignments;
3214        if audited_fields.is_some() {
3215            if fields.pk_is_auto {
3216                // Auto-PK + audited: defer. The Auto::Unset insert
3217                // path needs a transactional INSERT + LAST_INSERT_ID
3218                // + audit emit flow — that's a follow-up batch.
3219                quote!()
3220            } else {
3221                let pairs = audit_pair_tokens.iter();
3222                let pairs2 = audit_pair_tokens.iter();
3223                let pk_str = audit_pk_to_string.clone();
3224                let pk_str2 = audit_pk_to_string.clone();
3225                quote! {
3226                    /// Save (UPDATE) this row against either backend
3227                    /// with audit emission inside the same transaction.
3228                    /// Bi-dialect counterpart of [`Self::save`] for
3229                    /// audited models with non-`Auto<T>` PKs.
3230                    ///
3231                    /// Captures **post-write** field state (snapshot
3232                    /// audit). The legacy &PgPool [`Self::save`]
3233                    /// captures BEFORE+AFTER for true diff audit;
3234                    /// porting that to the &Pool path needs runtime
3235                    /// per-tracked-column decoding and is deferred.
3236                    ///
3237                    /// # Errors
3238                    /// As [`Self::save`].
3239                    pub async fn save_pool(
3240                        &mut self,
3241                        pool: &#root::sql::Pool,
3242                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3243                        let _query = #root::core::UpdateQuery {
3244                            model: <Self as #root::core::Model>::SCHEMA,
3245                            set: ::std::vec![ #( #assignments ),* ],
3246                            where_clause: #root::core::WhereExpr::Predicate(
3247                                #root::core::Filter {
3248                                    column: #pk_column_lit,
3249                                    op: #root::core::Op::Eq,
3250                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3251                                        ::core::clone::Clone::clone(&self.#pk_ident)
3252                                    ),
3253                                }
3254                            ),
3255                        };
3256                        let _audit_entry = #root::audit::PendingEntry {
3257                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3258                            entity_pk: #pk_str,
3259                            operation: #root::audit::AuditOp::Update,
3260                            source: #root::audit::current_source(),
3261                            changes: #root::audit::snapshot_changes(&[
3262                                #( #pairs ),*
3263                            ]),
3264                        };
3265                        let _affected = #root::audit::save_one_with_audit(
3266                            pool, &_query, &_audit_entry,
3267                        ).await?;
3268                        ::core::result::Result::Ok(_affected)
3269                    }
3270
3271                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3272                    /// (Django `Model.save(update_fields=[...])`).
3273                    /// Audit emission shrinks to the same column set so
3274                    /// the audit log reflects exactly what was written.
3275                    ///
3276                    /// # Errors
3277                    /// As [`Self::save_pool`], plus
3278                    /// [`#root::core::QueryError::UnknownField`] wrapped
3279                    /// in `ExecError::Query` for unknown field names.
3280                    pub async fn save_partial(
3281                        &mut self,
3282                        fields: &[&str],
3283                        pool: &#root::sql::Pool,
3284                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3285                        if fields.is_empty() {
3286                            #root::__tracing::warn!(
3287                                target: "rustango::save_partial",
3288                                model = <Self as #root::core::Model>::SCHEMA.name,
3289                                "save_partial called with empty field list — no-op"
3290                            );
3291                            return ::core::result::Result::Ok(0);
3292                        }
3293                        let _schema = <Self as #root::core::Model>::SCHEMA;
3294                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3295                            ::std::collections::HashSet::with_capacity(fields.len());
3296                        for f in fields {
3297                            match _schema.field(f) {
3298                                ::core::option::Option::Some(fs) => {
3299                                    _wanted_cols.insert(fs.column);
3300                                }
3301                                ::core::option::Option::None => {
3302                                    return ::core::result::Result::Err(
3303                                        #root::sql::ExecError::Query(
3304                                            #root::core::QueryError::UnknownField {
3305                                                model: _schema.name,
3306                                                field: (*f).to_owned(),
3307                                            }
3308                                        )
3309                                    );
3310                                }
3311                            }
3312                        }
3313                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3314                            ::std::vec![ #( #assignments ),* ];
3315                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3316                            .into_iter()
3317                            .filter(|a| _wanted_cols.contains(a.column))
3318                            .collect();
3319                        if _filtered.is_empty() {
3320                            #root::__tracing::warn!(
3321                                target: "rustango::save_partial",
3322                                model = _schema.name,
3323                                "save_partial: every named field maps to a non-assignable column — no-op"
3324                            );
3325                            return ::core::result::Result::Ok(0);
3326                        }
3327                        let _query = #root::core::UpdateQuery {
3328                            model: _schema,
3329                            set: _filtered,
3330                            where_clause: #root::core::WhereExpr::Predicate(
3331                                #root::core::Filter {
3332                                    column: #pk_column_lit,
3333                                    op: #root::core::Op::Eq,
3334                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3335                                        ::core::clone::Clone::clone(&self.#pk_ident)
3336                                    ),
3337                                }
3338                            ),
3339                        };
3340                        // Narrow the audit snapshot to the same column set.
3341                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3342                            ::std::vec![ #( #pairs2 ),* ];
3343                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3344                            _all_pairs
3345                                .into_iter()
3346                                .filter(|(col, _)| _wanted_cols.contains(col))
3347                                .collect();
3348                        let _audit_entry = #root::audit::PendingEntry {
3349                            entity_table: _schema.table,
3350                            entity_pk: #pk_str2,
3351                            operation: #root::audit::AuditOp::Update,
3352                            source: #root::audit::current_source(),
3353                            changes: #root::audit::snapshot_changes(&_narrowed),
3354                        };
3355                        let _affected = #root::audit::save_one_with_audit(
3356                            pool, &_query, &_audit_entry,
3357                        ).await?;
3358                        ::core::result::Result::Ok(_affected)
3359                    }
3360
3361                    /// Typed-column counterpart of [`Self::save_partial`] —
3362                    /// issue #67. `fields` is a tuple of [`Column`]
3363                    /// constants whose `Model` matches `Self`; typos and
3364                    /// model mismatches surface at *compile time*
3365                    /// (`Author::name` inside a `Post::save_partial_typed`
3366                    /// call is a type error, no runtime check).
3367                    ///
3368                    /// ```ignore
3369                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3370                    /// ```
3371                    ///
3372                    /// Lowers to [`Self::save_partial`] under the hood;
3373                    /// audit narrowing + every other semantic is identical.
3374                    ///
3375                    /// [`Column`]: #root::core::Column
3376                    ///
3377                    /// # Errors
3378                    /// As [`Self::save_partial`].
3379                    pub async fn save_partial_typed<
3380                        L: #root::core::TypedFieldList<Self>,
3381                    >(
3382                        &mut self,
3383                        fields: L,
3384                        pool: &#root::sql::Pool,
3385                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3386                        let _names = fields.rust_field_names();
3387                        let _refs: ::std::vec::Vec<&str> =
3388                            _names.iter().copied().collect();
3389                        self.save_partial(&_refs, pool).await
3390                    }
3391                }
3392            }
3393        } else {
3394            let dispatch_unset = if fields.pk_is_auto {
3395                quote! {
3396                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3397                        return self.insert_pool(pool).await.map(|()| 1u64);
3398                    }
3399                }
3400            } else {
3401                quote!()
3402            };
3403            quote! {
3404                /// Save this row to its table against either backend.
3405                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3406                /// `UPDATE` keyed on the PK.
3407                ///
3408                /// # Errors
3409                /// As [`Self::save`].
3410                pub async fn save_pool(
3411                    &mut self,
3412                    pool: &#root::sql::Pool,
3413                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3414                    #dispatch_unset
3415                    let _query = #root::core::UpdateQuery {
3416                        model: <Self as #root::core::Model>::SCHEMA,
3417                        set: ::std::vec![ #( #assignments ),* ],
3418                        where_clause: #root::core::WhereExpr::Predicate(
3419                            #root::core::Filter {
3420                                column: #pk_column_lit,
3421                                op: #root::core::Op::Eq,
3422                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3423                                    ::core::clone::Clone::clone(&self.#pk_ident)
3424                                ),
3425                            }
3426                        ),
3427                    };
3428                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3429                    ::core::result::Result::Ok(_affected)
3430                }
3431
3432                /// Save (UPDATE) only the listed Rust-side fields,
3433                /// leaving every other column untouched. Issue #66 —
3434                /// Django's `Model.save(update_fields=[...])` shape.
3435                ///
3436                /// `fields` are Rust-side struct field names; the macro
3437                /// resolves each to its SQL column. Unknown field
3438                /// names return [`#root::core::QueryError::UnknownField`]
3439                /// wrapped in `ExecError::Query`. An empty list is a
3440                /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3441                /// matching Django's "nothing to do" semantic.
3442                ///
3443                /// Use this when:
3444                /// * you only mutated a couple of fields on a wide row
3445                ///   (avoid re-writing every column on every save), or
3446                /// * two writers diverged after their initial read and
3447                ///   you want to preserve the other writer's changes to
3448                ///   columns you didn't touch.
3449                ///
3450                /// Auto-PK models with an unset PK return
3451                /// [`#root::core::QueryError::UnknownField`] with
3452                /// field name `<pk>` — `save_partial` is an
3453                /// UPDATE-only path. Call [`Self::insert_pool`]
3454                /// (or [`Self::save_pool`] which dispatches based on
3455                /// PK state) for the INSERT case.
3456                ///
3457                /// # Errors
3458                /// As [`Self::save_pool`], plus `UnknownField` for
3459                /// unknown / empty / Auto-Unset cases.
3460                pub async fn save_partial(
3461                    &mut self,
3462                    fields: &[&str],
3463                    pool: &#root::sql::Pool,
3464                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3465                    if fields.is_empty() {
3466                        #root::__tracing::warn!(
3467                            target: "rustango::save_partial",
3468                            model = <Self as #root::core::Model>::SCHEMA.name,
3469                            "save_partial called with empty field list — no-op"
3470                        );
3471                        return ::core::result::Result::Ok(0);
3472                    }
3473                    let _schema = <Self as #root::core::Model>::SCHEMA;
3474                    // Validate field names against the schema.
3475                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3476                        ::std::collections::HashSet::with_capacity(fields.len());
3477                    for f in fields {
3478                        match _schema.field(f) {
3479                            ::core::option::Option::Some(fs) => {
3480                                _wanted_cols.insert(fs.column);
3481                            }
3482                            ::core::option::Option::None => {
3483                                return ::core::result::Result::Err(
3484                                    #root::sql::ExecError::Query(
3485                                        #root::core::QueryError::UnknownField {
3486                                            model: _schema.name,
3487                                            field: (*f).to_owned(),
3488                                        }
3489                                    )
3490                                );
3491                            }
3492                        }
3493                    }
3494                    // Build the full assignment vec, then keep only the
3495                    // assignments whose column is in `_wanted_cols`.
3496                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3497                        ::std::vec![ #( #assignments ),* ];
3498                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3499                        .into_iter()
3500                        .filter(|a| _wanted_cols.contains(a.column))
3501                        .collect();
3502                    if _filtered.is_empty() {
3503                        // All field names valid, but they all map to
3504                        // non-assignable slots (PK column, computed/
3505                        // virtual fields, relations without an
3506                        // assignment). Same no-op semantic as Django.
3507                        #root::__tracing::warn!(
3508                            target: "rustango::save_partial",
3509                            model = _schema.name,
3510                            "save_partial: every named field maps to a non-assignable column — no-op"
3511                        );
3512                        return ::core::result::Result::Ok(0);
3513                    }
3514                    let _query = #root::core::UpdateQuery {
3515                        model: _schema,
3516                        set: _filtered,
3517                        where_clause: #root::core::WhereExpr::Predicate(
3518                            #root::core::Filter {
3519                                column: #pk_column_lit,
3520                                op: #root::core::Op::Eq,
3521                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3522                                    ::core::clone::Clone::clone(&self.#pk_ident)
3523                                ),
3524                            }
3525                        ),
3526                    };
3527                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3528                    ::core::result::Result::Ok(_affected)
3529                }
3530
3531                /// Typed-column counterpart of [`Self::save_partial`] —
3532                /// issue #67. `fields` is a tuple of [`Column`]
3533                /// constants whose `Model` matches `Self`; typos and
3534                /// model mismatches surface at *compile time*
3535                /// (`Author::name` inside a `Post::save_partial_typed`
3536                /// call is a type error, no runtime check).
3537                ///
3538                /// ```ignore
3539                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3540                /// ```
3541                ///
3542                /// Lowers to [`Self::save_partial`] under the hood — the
3543                /// tuple is reduced to a `&[&str]` slice of Rust-side
3544                /// field names and forwarded.
3545                ///
3546                /// [`Column`]: #root::core::Column
3547                ///
3548                /// # Errors
3549                /// As [`Self::save_partial`].
3550                pub async fn save_partial_typed<
3551                    L: #root::core::TypedFieldList<Self>,
3552                >(
3553                    &mut self,
3554                    fields: L,
3555                    pool: &#root::sql::Pool,
3556                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3557                    let _names = fields.rust_field_names();
3558                    let _refs: ::std::vec::Vec<&str> =
3559                        _names.iter().copied().collect();
3560                    self.save_partial(&_refs, pool).await
3561                }
3562            }
3563        }
3564    } else {
3565        quote!()
3566    };
3567
3568    // Audited `insert_pool` (overrides the placeholder set higher up
3569    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3570    // audited models get insert_pool routing through
3571    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3572    // + auto-PK readback + audit emit). Snapshot-style audit (the
3573    // PendingEntry's `changes` carries post-write field values).
3574    let pool_insert_method = if audited_fields.is_some() {
3575        if let Some(_) = primary_key {
3576            let pushes = if fields.has_auto {
3577                fields.insert_pushes.clone()
3578            } else {
3579                // For non-Auto-PK models, the macro normally builds
3580                // {columns, values} from fields.insert_columns +
3581                // fields.insert_values rather than insert_pushes.
3582                // Map those into the pushes shape.
3583                fields
3584                    .insert_columns
3585                    .iter()
3586                    .zip(&fields.insert_values)
3587                    .map(|(col, val)| {
3588                        quote! {
3589                            _columns.push(#col);
3590                            _values.push(#val);
3591                        }
3592                    })
3593                    .collect()
3594            };
3595            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3596                fields.returning_cols.clone()
3597            } else {
3598                // Non-Auto-PK: still need RETURNING something for the
3599                // audit helper's contract (it errors on empty
3600                // returning). Return the PK column so the audit row
3601                // can carry the assigned PK back. Some non-Auto PKs
3602                // are server-side-default (e.g. UUIDv4 default), so
3603                // RETURNING is genuinely useful.
3604                primary_key
3605                    .map(|(_, col)| {
3606                        let lit = col.as_str();
3607                        vec![quote!(#lit)]
3608                    })
3609                    .unwrap_or_default()
3610            };
3611            let pairs = audit_pair_tokens.iter();
3612            let pk_str = audit_pk_to_string.clone();
3613            quote! {
3614                /// Insert this row against either backend with audit
3615                /// emission inside the same transaction. Bi-dialect
3616                /// counterpart of [`Self::insert`] for audited models.
3617                ///
3618                /// Snapshot-style audit (post-write field values).
3619                ///
3620                /// # Errors
3621                /// As [`Self::insert`].
3622                pub async fn insert_pool(
3623                    &mut self,
3624                    pool: &#root::sql::Pool,
3625                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3626                    let mut _columns: ::std::vec::Vec<&'static str> =
3627                        ::std::vec::Vec::new();
3628                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3629                        ::std::vec::Vec::new();
3630                    #( #pushes )*
3631                    let _query = #root::core::InsertQuery {
3632                        model: <Self as #root::core::Model>::SCHEMA,
3633                        columns: _columns,
3634                        values: _values,
3635                        returning: ::std::vec![ #( #returning_cols ),* ],
3636                        on_conflict: ::core::option::Option::None,
3637                    };
3638                    let _audit_entry = #root::audit::PendingEntry {
3639                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
3640                        entity_pk: #pk_str,
3641                        operation: #root::audit::AuditOp::Create,
3642                        source: #root::audit::current_source(),
3643                        changes: #root::audit::snapshot_changes(&[
3644                            #( #pairs ),*
3645                        ]),
3646                    };
3647                    let _result = #root::audit::insert_one_with_audit(
3648                        pool, &_query, &_audit_entry,
3649                    ).await?;
3650                    #root::sql::apply_auto_pk(_result, self)
3651                }
3652            }
3653        } else {
3654            quote!()
3655        }
3656    } else {
3657        // Keep the non-audited pool_insert_method we built earlier.
3658        pool_insert_method
3659    };
3660
3661    // Update audited save_pool: now that insert_pool is wired for
3662    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3663    // insert_pool. Non-audited save_pool already does this.
3664    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3665    // Replaces the snapshot-only emission with a per-backend transaction
3666    // body that:
3667    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3668    //     column), capturing BEFORE values
3669    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3670    //  3. builds AFTER pairs from &self
3671    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3672    //     AuditOp::Update + diff_changes(...) on the same tx connection
3673    //  5. commits
3674    //
3675    // Per-backend arms inline the SQL string + placeholder shape, then
3676    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3677    // is polymorphic over Row type — the same tokens work against
3678    // PgRow and MySqlRow as long as the field's Rust type implements
3679    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3680    // primitives + chrono/uuid/serde_json::Value all do).
3681    let pool_save_method = if let Some(tracked) = audited_fields {
3682        if let Some((pk_ident, pk_col)) = primary_key {
3683            let pk_column_lit = pk_col.as_str();
3684            // Two iterators — quote!'s `#(#var)*` consumes the
3685            // iterator, and we need to splice the same after-pairs
3686            // sequence into both per-backend arms.
3687            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3688            let pk_str = audit_pk_to_string.clone();
3689            // Per-tracked-column BEFORE-pair token list. Each entry
3690            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3691            // The Row alias resolves to PgRow / MySqlRow per call site,
3692            // so the same template generates both the PG and MySQL bodies.
3693            let mk_before_pairs =
3694                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3695                    tracked
3696                        .iter()
3697                        .map(|c| {
3698                            let column_lit = c.column.as_str();
3699                            let value_ty = &c.value_ty;
3700                            quote! {
3701                                (
3702                                    #column_lit,
3703                                    match #getter::<#value_ty>(
3704                                        _audit_before_row, #column_lit,
3705                                    ) {
3706                                        ::core::result::Result::Ok(v) => {
3707                                            #root::__serde_json::to_value(&v)
3708                                                .unwrap_or(#root::__serde_json::Value::Null)
3709                                        }
3710                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3711                                    },
3712                                )
3713                            }
3714                        })
3715                        .collect()
3716                };
3717            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3718                mk_before_pairs(quote!(#root::sql::try_get_returning));
3719            let before_pairs_my: Vec<proc_macro2::TokenStream> =
3720                mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3721            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3722                mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3723            let pg_select_cols: String = tracked
3724                .iter()
3725                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3726                .collect::<Vec<_>>()
3727                .join(", ");
3728            let my_select_cols: String = tracked
3729                .iter()
3730                .map(|c| format!("`{}`", c.column.replace('`', "``")))
3731                .collect::<Vec<_>>()
3732                .join(", ");
3733            // SQLite uses double-quote identifier quoting (same as
3734            // Postgres in default config), so the column-list shape
3735            // matches PG.
3736            let sqlite_select_cols: String = pg_select_cols.clone();
3737            let pk_value_for_bind = if fields.pk_is_auto {
3738                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3739            } else {
3740                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3741            };
3742            let assignments = &fields.update_assignments;
3743            let unset_dispatch = if fields.has_auto {
3744                quote! {
3745                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3746                        return self.insert_pool(pool).await.map(|()| 1u64);
3747                    }
3748                }
3749            } else {
3750                quote!()
3751            };
3752            quote! {
3753                /// Save this row against either backend with audit
3754                /// emission (diff-style: BEFORE+AFTER) inside the
3755                /// same transaction. Auto::Unset PK routes to
3756                /// insert_pool. Bi-dialect counterpart of
3757                /// [`Self::save`] for audited models.
3758                ///
3759                /// The audit row's `changes` JSON contains one
3760                /// `{ "field": { "before": …, "after": … } }` entry
3761                /// per tracked column whose value actually changed
3762                /// — same shape as the existing &PgPool save() emits.
3763                ///
3764                /// # Errors
3765                /// As [`Self::save`].
3766                pub async fn save_pool(
3767                    &mut self,
3768                    pool: &#root::sql::Pool,
3769                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3770                    #unset_dispatch
3771                    let _query = #root::core::UpdateQuery {
3772                        model: <Self as #root::core::Model>::SCHEMA,
3773                        set: ::std::vec![ #( #assignments ),* ],
3774                        where_clause: #root::core::WhereExpr::Predicate(
3775                            #root::core::Filter {
3776                                column: #pk_column_lit,
3777                                op: #root::core::Op::Eq,
3778                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3779                                    ::core::clone::Clone::clone(&self.#pk_ident)
3780                                ),
3781                            }
3782                        ),
3783                    };
3784                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3785                        ::std::vec![ #( #after_pairs_pg ),* ];
3786                    #root::audit::save_one_with_diff(
3787                        pool,
3788                        &_query,
3789                        #pk_column_lit,
3790                        ::core::convert::Into::<#root::core::SqlValue>::into(
3791                            #pk_value_for_bind,
3792                        ),
3793                        <Self as #root::core::Model>::SCHEMA.table,
3794                        #pk_str,
3795                        _after_pairs,
3796                        #pg_select_cols,
3797                        #my_select_cols,
3798                        #sqlite_select_cols,
3799                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3800                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3801                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3802                    ).await
3803                }
3804            }
3805        } else {
3806            quote!()
3807        }
3808    } else {
3809        pool_save_method
3810    };
3811
3812    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3813    // body shapes:
3814    // - non-audited: simple dispatch through `sql::delete_pool`
3815    // - audited: routes through `audit::delete_one_with_audit`,
3816    //   which opens a per-backend transaction wrapping DELETE +
3817    //   audit emit so the data write and audit row commit atomically.
3818    let pool_delete_method = {
3819        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3820        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3821        if let Some(pk_ident) = pk_ident_for_pool {
3822            if audited_fields.is_some() {
3823                let pairs = audit_pair_tokens.iter();
3824                let pk_str = audit_pk_to_string.clone();
3825                quote! {
3826                    /// Delete this row against either backend with audit
3827                    /// emission inside the same transaction. Bi-dialect
3828                    /// counterpart of [`Self::delete`] for audited models.
3829                    ///
3830                    /// # Errors
3831                    /// As [`Self::delete`].
3832                    pub async fn delete_pool(
3833                        &self,
3834                        pool: &#root::sql::Pool,
3835                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3836                        let _query = #root::core::DeleteQuery {
3837                            model: <Self as #root::core::Model>::SCHEMA,
3838                            where_clause: #root::core::WhereExpr::Predicate(
3839                                #root::core::Filter {
3840                                    column: #pk_column_lit,
3841                                    op: #root::core::Op::Eq,
3842                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3843                                        ::core::clone::Clone::clone(&self.#pk_ident)
3844                                    ),
3845                                }
3846                            ),
3847                        };
3848                        let _audit_entry = #root::audit::PendingEntry {
3849                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3850                            entity_pk: #pk_str,
3851                            operation: #root::audit::AuditOp::Delete,
3852                            source: #root::audit::current_source(),
3853                            changes: #root::audit::snapshot_changes(&[
3854                                #( #pairs ),*
3855                            ]),
3856                        };
3857                        #root::audit::delete_one_with_audit(
3858                            pool, &_query, &_audit_entry,
3859                        ).await
3860                    }
3861                }
3862            } else {
3863                quote! {
3864                    /// Delete the row identified by this instance's primary key
3865                    /// against either backend. Equivalent to [`Self::delete`] but
3866                    /// takes [`#root::sql::Pool`] and dispatches per backend.
3867                    ///
3868                    /// # Errors
3869                    /// As [`Self::delete`].
3870                    pub async fn delete_pool(
3871                        &self,
3872                        pool: &#root::sql::Pool,
3873                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3874                        let _query = #root::core::DeleteQuery {
3875                            model: <Self as #root::core::Model>::SCHEMA,
3876                            where_clause: #root::core::WhereExpr::Predicate(
3877                                #root::core::Filter {
3878                                    column: #pk_column_lit,
3879                                    op: #root::core::Op::Eq,
3880                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3881                                        ::core::clone::Clone::clone(&self.#pk_ident)
3882                                    ),
3883                                }
3884                            ),
3885                        };
3886                        #root::sql::delete_pool(pool, &_query).await
3887                    }
3888                }
3889            }
3890        } else {
3891            quote!()
3892        }
3893    };
3894
3895    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3896    // matching this instance's PK and overwrite the in-memory state
3897    // with the freshly-fetched columns. Django's `refresh_from_db`.
3898    // Issue #825. Only emitted when the model declares a PK; non-PK
3899    // models can't address a specific row.
3900    //
3901    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3902    // every field from `self`; resets the PK to `Auto::Unset` when
3903    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3904    // a fresh autoincrement value. Non-Auto PKs preserve the source
3905    // PK — the caller must overwrite before insert. Pure-Rust, no
3906    // I/O, no dialect surface.
3907    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3908        let other_field_clones: Vec<TokenStream2> = fields
3909            .column_entries
3910            .iter()
3911            .filter(|c| &c.ident != pk_ident)
3912            .map(|c| {
3913                let ident = &c.ident;
3914                quote! {
3915                    #ident: ::core::clone::Clone::clone(&self.#ident)
3916                }
3917            })
3918            .collect();
3919        let pk_clone_token = if fields.pk_is_auto {
3920            quote! { #pk_ident: #root::sql::Auto::Unset }
3921        } else {
3922            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3923        };
3924        let replicate_doc = if fields.pk_is_auto {
3925            quote! {
3926                /// Eloquent-style `replicate()` — return a clone of this
3927                /// row with the primary key reset to [`Auto::Unset`] so
3928                /// the copy is ready for `insert_pool` / `save_pool` to
3929                /// allocate a fresh autoincrement value. Every other
3930                /// field is `Clone`d verbatim. Issue #825.
3931                ///
3932                /// `auto_now_add` / `auto_now` timestamp fields are
3933                /// **not** reset (Eloquent's `replicate` doesn't reset
3934                /// them either) — pass them through the normal insert
3935                /// path if you want fresh values, or assign them
3936                /// explicitly after the call.
3937            }
3938        } else {
3939            quote! {
3940                /// Eloquent-style `replicate()` — clone this row
3941                /// verbatim. Because the primary key is **not** an
3942                /// `Auto<T>`, the clone keeps the source PK; the
3943                /// caller must overwrite `copy.<pk>` before inserting
3944                /// to avoid a unique-key violation. Issue #825.
3945            }
3946        };
3947        // 2026-06-07 — field-name / shortcut collision guard.
3948        //
3949        // The macro emits both `pub const <field>: <field>_col = ...`
3950        // (per-field typed-column const, used by the typed-builder
3951        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3952        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3953        // model has a field named e.g. `count`, both items would land
3954        // in the same inherent impl with the same name, and the
3955        // compiler rejects the derive with "duplicate definitions".
3956        //
3957        // Drop the conflicting shortcut for that model. Callers can
3958        // still reach the same behavior via
3959        // `QuerySet::<Model>::default().count(&pool)`.
3960        let column_names: ::std::collections::HashSet<String> = fields
3961            .column_entries
3962            .iter()
3963            .map(|c| c.ident.to_string())
3964            .collect();
3965        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3966            if column_names.contains(name) {
3967                quote! {}
3968            } else {
3969                tokens
3970            }
3971        };
3972        let count_method = emit_if_no_field_collision(
3973            "count",
3974            quote! {
3975                /// Count rows of this model — `SELECT COUNT(*) FROM
3976                /// <table>`. Eloquent `Model::count()` parity.
3977                ///
3978                /// Skipped on models that already declare a field named
3979                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3980                /// in that case.
3981                ///
3982                /// # Errors
3983                /// As [`CounterPool::count`].
3984                ///
3985                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3986                pub async fn count(
3987                    pool: &#root::sql::Pool,
3988                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3989                    use #root::sql::CounterPool as _;
3990                    #root::query::QuerySet::<Self>::default()
3991                        .count(pool)
3992                        .await
3993                }
3994            },
3995        );
3996        let value_method = emit_if_no_field_collision(
3997            "value",
3998            quote! {
3999                /// Pluck a single scalar from the first row.
4000                /// Eloquent `Model::query()->value($col)` parity.
4001                ///
4002                /// Skipped on models that already declare a field named
4003                /// `value`. Drop into
4004                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
4005                ///
4006                /// # Errors
4007                /// As `ValuesFlatQuerySet::first`.
4008                pub async fn value<U>(
4009                    col: &str,
4010                    pool: &#root::sql::Pool,
4011                ) -> ::core::result::Result<
4012                    ::core::option::Option<U>,
4013                    #root::sql::ExecError,
4014                >
4015                where
4016                    U: #root::sql::MaybePgScalar
4017                        + #root::sql::MaybeMyScalar
4018                        + #root::sql::MaybeSqliteScalar
4019                        + ::core::marker::Send
4020                        + ::core::marker::Unpin,
4021                {
4022                    let _col_static: &'static str = Self::__resolve_col(col)?;
4023                    #root::query::QuerySet::<Self>::default()
4024                        .values_list_flat(_col_static)
4025                        .first::<U>(pool)
4026                        .await
4027                }
4028            },
4029        );
4030        let sum_method = emit_if_no_field_collision(
4031            "sum",
4032            quote! {
4033                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4034                /// Skipped on models that already declare a field named
4035                /// `sum`.
4036                ///
4037                /// # Errors
4038                /// As [`#root::sql::fetch_aggregate_pool`].
4039                pub async fn sum<U>(
4040                    col: &str,
4041                    pool: &#root::sql::Pool,
4042                ) -> ::core::result::Result<
4043                    ::core::option::Option<U>,
4044                    #root::sql::ExecError,
4045                >
4046                where
4047                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4048                        + #root::sql::MaybeMyFromRow
4049                        + #root::sql::MaybeSqliteFromRow
4050                        + ::core::marker::Send
4051                        + ::core::marker::Unpin,
4052                {
4053                    Self::__aggregate_one_pool::<U>(
4054                        col,
4055                        |c| #root::core::AggregateExpr::Sum(c),
4056                        pool,
4057                    )
4058                    .await
4059                }
4060            },
4061        );
4062        let avg_method = emit_if_no_field_collision(
4063            "avg",
4064            quote! {
4065                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4066                /// models that already declare a field named `avg`.
4067                ///
4068                /// # Errors
4069                /// As [`#root::sql::fetch_aggregate_pool`].
4070                pub async fn avg<U>(
4071                    col: &str,
4072                    pool: &#root::sql::Pool,
4073                ) -> ::core::result::Result<
4074                    ::core::option::Option<U>,
4075                    #root::sql::ExecError,
4076                >
4077                where
4078                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4079                        + #root::sql::MaybeMyFromRow
4080                        + #root::sql::MaybeSqliteFromRow
4081                        + ::core::marker::Send
4082                        + ::core::marker::Unpin,
4083                {
4084                    Self::__aggregate_one_pool::<U>(
4085                        col,
4086                        |c| #root::core::AggregateExpr::Avg(c),
4087                        pool,
4088                    )
4089                    .await
4090                }
4091            },
4092        );
4093        let min_method = emit_if_no_field_collision(
4094            "min",
4095            quote! {
4096                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4097                /// models that already declare a field named `min`.
4098                ///
4099                /// # Errors
4100                /// As [`#root::sql::fetch_aggregate_pool`].
4101                pub async fn min<U>(
4102                    col: &str,
4103                    pool: &#root::sql::Pool,
4104                ) -> ::core::result::Result<
4105                    ::core::option::Option<U>,
4106                    #root::sql::ExecError,
4107                >
4108                where
4109                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4110                        + #root::sql::MaybeMyFromRow
4111                        + #root::sql::MaybeSqliteFromRow
4112                        + ::core::marker::Send
4113                        + ::core::marker::Unpin,
4114                {
4115                    Self::__aggregate_one_pool::<U>(
4116                        col,
4117                        |c| #root::core::AggregateExpr::Min(c),
4118                        pool,
4119                    )
4120                    .await
4121                }
4122            },
4123        );
4124        let max_method = emit_if_no_field_collision(
4125            "max",
4126            quote! {
4127                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4128                /// models that already declare a field named `max`.
4129                ///
4130                /// # Errors
4131                /// As [`#root::sql::fetch_aggregate_pool`].
4132                pub async fn max<U>(
4133                    col: &str,
4134                    pool: &#root::sql::Pool,
4135                ) -> ::core::result::Result<
4136                    ::core::option::Option<U>,
4137                    #root::sql::ExecError,
4138                >
4139                where
4140                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4141                        + #root::sql::MaybeMyFromRow
4142                        + #root::sql::MaybeSqliteFromRow
4143                        + ::core::marker::Send
4144                        + ::core::marker::Unpin,
4145                {
4146                    Self::__aggregate_one_pool::<U>(
4147                        col,
4148                        |c| #root::core::AggregateExpr::Max(c),
4149                        pool,
4150                    )
4151                    .await
4152                }
4153            },
4154        );
4155        let first_method = emit_if_no_field_collision(
4156            "first",
4157            quote! {
4158                /// First row of this model. Eloquent `Model::first()`.
4159                /// Skipped on models that already declare a field named
4160                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4161                ///
4162                /// # Errors
4163                /// As `QuerySet::first`.
4164                pub async fn first(
4165                    pool: &#root::sql::Pool,
4166                ) -> ::core::result::Result<
4167                    ::core::option::Option<Self>,
4168                    #root::sql::ExecError,
4169                > {
4170                    #root::query::QuerySet::<Self>::default()
4171                        .first(pool)
4172                        .await
4173                }
4174            },
4175        );
4176        let last_method = emit_if_no_field_collision(
4177            "last",
4178            quote! {
4179                /// Last row of this model by primary-key DESC.
4180                /// Eloquent `Model::query()->latest('id')->first()`
4181                /// parity — fetches the highest-PK row without
4182                /// requiring the caller to spell the PK column.
4183                /// Returns `None` on an empty table.
4184                ///
4185                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4186                /// Skipped on models that already declare a field
4187                /// named `last`.
4188                ///
4189                /// # Errors
4190                /// As `QuerySet::last`.
4191                pub async fn last(
4192                    pool: &#root::sql::Pool,
4193                ) -> ::core::result::Result<
4194                    ::core::option::Option<Self>,
4195                    #root::sql::ExecError,
4196                > {
4197                    #root::query::QuerySet::<Self>::default()
4198                        .last(pool)
4199                        .await
4200                }
4201            },
4202        );
4203        quote! {
4204            /// Re-SELECT this row by its primary key and overwrite
4205            /// every in-memory field with the freshly-fetched value.
4206            /// Django's [`Model.refresh_from_db`]. Issue #825.
4207            ///
4208            /// Use this when the row may have been modified by another
4209            /// process / connection / job since you read it — e.g. after
4210            /// a queued task callback, or to re-sync stale UI state
4211            /// before re-saving.
4212            ///
4213            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4214            /// when the primary key no longer matches any row (e.g.
4215            /// the row was deleted concurrently).
4216            ///
4217            /// # Errors
4218            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4219            /// the PK no longer exists.
4220            ///
4221            /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4222            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4223            pub async fn refresh_from_db(
4224                &mut self,
4225                pool: &#root::sql::Pool,
4226            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4227                use #root::sql::FetcherPool as _;
4228                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4229                    ::core::clone::Clone::clone(&self.#pk_ident),
4230                );
4231                let mut _rows: ::std::vec::Vec<Self> =
4232                    #root::query::QuerySet::<Self>::default()
4233                        .filter(::core::stringify!(#pk_ident), _pk_val)
4234                        .limit(1)
4235                        .fetch(pool)
4236                        .await?;
4237                match _rows.into_iter().next() {
4238                    ::core::option::Option::Some(_fresh) => {
4239                        *self = _fresh;
4240                        ::core::result::Result::Ok(())
4241                    }
4242                    ::core::option::Option::None => ::core::result::Result::Err(
4243                        #root::sql::ExecError::Driver(
4244                            #root::sql::sqlx::Error::RowNotFound,
4245                        ),
4246                    ),
4247                }
4248            }
4249
4250            /// Atomically increment the integer column `col` by
4251            /// `by` for this row. Equivalent to
4252            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4253            /// Eloquent `Model::increment($col, $by)` / Django
4254            /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4255            /// parity.
4256            ///
4257            /// **Doesn't mutate `self`** — the in-memory copy is now
4258            /// stale; call [`Self::refresh_from_db_pool`] /
4259            /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4260            /// affected count (0 when the PK doesn't match any row,
4261            /// 1 on success).
4262            ///
4263            /// `col` is the Rust field name as a string; unknown
4264            /// fields surface as `UnknownField` at runtime. Negative
4265            /// `by` values atomically decrement (see also
4266            /// [`Self::decrement_pool`]).
4267            ///
4268            /// # Errors
4269            /// As [`UpdaterPool::execute_pool`].
4270            ///
4271            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4272            pub async fn increment(
4273                &self,
4274                col: &str,
4275                by: i64,
4276                pool: &#root::sql::Pool,
4277            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4278                Self::__increment_one(self, col, by, pool).await
4279            }
4280
4281            /// Sibling of [`Self::increment`] — atomically
4282            /// decrement this row's `col` by `by`. Eloquent
4283            /// `$model->decrement($col, $by)` parity. Equivalent to
4284            /// `self.increment(col, -by, &pool)`; the separate name
4285            /// keeps call sites readable.
4286            ///
4287            /// # Errors
4288            /// As [`Self::increment`].
4289            pub async fn decrement(
4290                &self,
4291                col: &str,
4292                by: i64,
4293                pool: &#root::sql::Pool,
4294            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4295                Self::__increment_one(self, col, -by, pool).await
4296            }
4297
4298            /// Bulk-increment: add `by` to `col` on every row of the
4299            /// table. Eloquent `Model::query()->increment($col, $by)`
4300            /// parity. Use for counters, score adjustments, view
4301            /// rollups.
4302            ///
4303            /// # Errors
4304            /// As [`UpdaterPool::execute_pool`].
4305            ///
4306            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4307            pub async fn increment_each(
4308                col: &str,
4309                by: i64,
4310                pool: &#root::sql::Pool,
4311            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4312                Self::__increment_all(col, by, pool).await
4313            }
4314
4315            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4316            ///
4317            /// # Errors
4318            /// As [`Self::increment_each`].
4319            pub async fn decrement_each(
4320                col: &str,
4321                by: i64,
4322                pool: &#root::sql::Pool,
4323            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4324                Self::__increment_all(col, -by, pool).await
4325            }
4326
4327            /// Internal: forward to
4328            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4329            /// One-line wrapper kept as a per-Model method so the
4330            /// macro's emitted `increment` / `decrement` instance
4331            /// calls don't have to thread `Self` through manually.
4332            #[doc(hidden)]
4333            pub async fn __increment_one(
4334                this: &Self,
4335                col: &str,
4336                by: i64,
4337                pool: &#root::sql::Pool,
4338            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4339                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4340                    ::core::clone::Clone::clone(&this.#pk_ident),
4341                );
4342                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4343                    ::core::stringify!(#pk_ident),
4344                    _pk_val,
4345                    col,
4346                    by,
4347                    pool,
4348                )
4349                .await
4350            }
4351
4352            /// Internal: forward to
4353            /// [`#root::sql::model_shortcuts::increment_all_pool`].
4354            #[doc(hidden)]
4355            pub async fn __increment_all(
4356                col: &str,
4357                by: i64,
4358                pool: &#root::sql::Pool,
4359            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4360                #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4361            }
4362
4363            /// Internal: forward to
4364            /// [`#root::sql::model_shortcuts::resolve_col`].
4365            #[doc(hidden)]
4366            pub fn __resolve_col(
4367                col: &str,
4368            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4369                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4370            }
4371
4372            /// Internal: forward to
4373            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4374            #[doc(hidden)]
4375            #[must_use]
4376            pub fn __add_signed_expr(
4377                col_static: &'static str,
4378                signed_by: i64,
4379            ) -> #root::core::Expr {
4380                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4381            }
4382
4383            /// Re-SELECT this row by its primary key and return a
4384            /// **new** instance with the freshly-fetched fields.
4385            /// Eloquent `Model::fresh()` parity — non-mutating
4386            /// counterpart of [`Self::refresh_from_db_pool`].
4387            ///
4388            /// Returns `Ok(None)` when the row was deleted
4389            /// concurrently — vs [`Self::refresh_from_db_pool`]
4390            /// which surfaces that as `RowNotFound` because
4391            /// in-place mutation has nothing to write to.
4392            ///
4393            /// Useful when you want to compare the in-memory
4394            /// instance against the persisted state (audit-style
4395            /// diffs, conflict detection) without mutating the
4396            /// reference you already hold.
4397            ///
4398            /// # Errors
4399            /// As [`FetcherPool::fetch`].
4400            ///
4401            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4402            pub async fn fresh(
4403                &self,
4404                pool: &#root::sql::Pool,
4405            ) -> ::core::result::Result<
4406                ::core::option::Option<Self>,
4407                #root::sql::ExecError,
4408            > {
4409                use #root::sql::FetcherPool as _;
4410                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4411                    ::core::clone::Clone::clone(&self.#pk_ident),
4412                );
4413                let _rows: ::std::vec::Vec<Self> =
4414                    #root::query::QuerySet::<Self>::default()
4415                        .filter(::core::stringify!(#pk_ident), _pk_val)
4416                        .limit(1)
4417                        .fetch(pool)
4418                        .await?;
4419                ::core::result::Result::Ok(_rows.into_iter().next())
4420            }
4421
4422            #replicate_doc
4423            #[must_use]
4424            pub fn replicate(&self) -> Self {
4425                Self {
4426                    #pk_clone_token,
4427                    #( #other_field_clones, )*
4428                }
4429            }
4430
4431            #first_method
4432
4433            #last_method
4434
4435            /// Throwing counterpart of [`Self::first_pool`] —
4436            /// errors with `RowNotFound` when the table is empty.
4437            /// Eloquent `Model::firstOrFail()` parity.
4438            ///
4439            /// # Errors
4440            /// As [`Self::first_pool`]; additionally
4441            /// [`sqlx::Error::RowNotFound`] on empty tables.
4442            ///
4443            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4444            pub async fn first_or_fail(
4445                pool: &#root::sql::Pool,
4446            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4447                // Route through the queryset rather than `Self::first`,
4448                // which is suppressed on models with a field named
4449                // `first` (field/shortcut collision guard).
4450                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4451                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4452                    ::core::option::Option::None => ::core::result::Result::Err(
4453                        #root::sql::ExecError::Driver(
4454                            #root::sql::sqlx::Error::RowNotFound,
4455                        ),
4456                    ),
4457                }
4458            }
4459
4460            /// Single-column projection — `SELECT <col> FROM
4461            /// <table>`. Returns `Vec<U>` where each element is the
4462            /// decoded value of the column. Eloquent
4463            /// `Model::pluck($column)` / Django
4464            /// `Model.objects.values_list('col', flat=True)` parity.
4465            ///
4466            /// Thin wrapper over `QuerySet::<Self>::default()
4467            /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4468            /// be decodable from the column's SQL type on every
4469            /// dialect the binary targets (common picks: `i64` /
4470            /// `i32` / `String` / `bool` / `f64`).
4471            ///
4472            /// # Errors
4473            /// As `ValuesFlatQuerySet::fetch`.
4474            pub async fn pluck<U>(
4475                col: &'static str,
4476                pool: &#root::sql::Pool,
4477            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4478            where
4479                U: #root::sql::MaybePgScalar
4480                    + #root::sql::MaybeMyScalar
4481                    + #root::sql::MaybeSqliteScalar
4482                    + ::core::marker::Send
4483                    + ::core::marker::Unpin,
4484            {
4485                #root::query::QuerySet::<Self>::default()
4486                    .values_list_flat(col)
4487                    .fetch::<U>(pool)
4488                    .await
4489            }
4490
4491            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4492            /// stream every row of this model in batches of `n`,
4493            /// invoking the callback once per batch. Stable PK-ASC
4494            /// ordering so the LIMIT/OFFSET pagination is
4495            /// deterministic across drivers.
4496            ///
4497            /// The callback is async — it can do further DB work
4498            /// (writes, related-row lookups, queue dispatch) per
4499            /// batch. Return `Err(...)` from the callback to abort
4500            /// the iteration early; the error bubbles up.
4501            ///
4502            /// **When to use**: bulk processing flows that can't
4503            /// fit the whole table in memory — sending newsletters,
4504            /// running data migrations, computing summary
4505            /// statistics. For small / known-bounded tables, plain
4506            /// `Self::all(&pool)` is simpler.
4507            ///
4508            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4509            /// large tables. For multi-million-row scans prefer
4510            /// keyset-by-PK (the standard "WHERE id > last_seen"
4511            /// shape) over `chunk(...)`.
4512            ///
4513            /// Skipped on models without a primary key — chunking
4514            /// needs a stable order to avoid skipping / repeating
4515            /// rows across batches.
4516            pub async fn chunk<F, Fut>(
4517                n: i64,
4518                pool: &#root::sql::Pool,
4519                mut cb: F,
4520            ) -> ::core::result::Result<(), #root::sql::ExecError>
4521            where
4522                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4523                Fut: ::core::future::Future<
4524                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4525                >,
4526            {
4527                use #root::sql::FetcherPool as _;
4528                let pk_col = match Self::primary_key_column() {
4529                    ::core::option::Option::Some(c) => c,
4530                    ::core::option::Option::None => {
4531                        return ::core::result::Result::Ok(());
4532                    }
4533                };
4534                let mut offset: i64 = 0;
4535                loop {
4536                    let rows: ::std::vec::Vec<Self> =
4537                        #root::query::QuerySet::<Self>::default()
4538                            .order_by(&[(pk_col, false)])
4539                            .limit(n)
4540                            .offset(offset)
4541                            .fetch(pool)
4542                            .await?;
4543                    if rows.is_empty() {
4544                        return ::core::result::Result::Ok(());
4545                    }
4546                    let len = rows.len() as i64;
4547                    cb(rows).await?;
4548                    if len < n {
4549                        return ::core::result::Result::Ok(());
4550                    }
4551                    offset += n;
4552                }
4553            }
4554
4555            /// Eloquent `Model::chunkById($n, fn (...))` — same
4556            /// per-batch callback shape as [`Self::chunk`], but uses
4557            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4558            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4559            /// — the right choice for multi-million-row sweeps.
4560            ///
4561            /// Requires the primary key to be a signed integer type
4562            /// (`i64` / `i32`); the keyset comparison rides on
4563            /// `__rustango_pk_value()` lowering through
4564            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4565            /// non-integer PKs (UUID, String) — those should use
4566            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4567            /// keyset loop.
4568            ///
4569            /// Callback errors abort iteration; the error bubbles up
4570            /// unchanged. Empty table → callback invoked zero times.
4571            pub async fn chunk_by_id<F, Fut>(
4572                n: i64,
4573                pool: &#root::sql::Pool,
4574                mut cb: F,
4575            ) -> ::core::result::Result<(), #root::sql::ExecError>
4576            where
4577                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4578                Fut: ::core::future::Future<
4579                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4580                >,
4581            {
4582                use #root::sql::FetcherPool as _;
4583                let pk_col = match Self::primary_key_column() {
4584                    ::core::option::Option::Some(c) => c,
4585                    ::core::option::Option::None => {
4586                        return ::core::result::Result::Ok(());
4587                    }
4588                };
4589                // Track the largest PK seen so the next batch picks
4590                // up from there. `i64::MIN` as the sentinel — the
4591                // very first iteration's `> MIN` matches every row,
4592                // so the loop entry is uniform with subsequent
4593                // iterations.
4594                let mut last_seen: i64 = i64::MIN;
4595                loop {
4596                    let key = ::std::format!("{}__gt", pk_col);
4597                    let rows: ::std::vec::Vec<Self> =
4598                        #root::query::QuerySet::<Self>::default()
4599                            .filter(key.as_str(), last_seen)
4600                            .order_by(&[(pk_col, false)])
4601                            .limit(n)
4602                            .fetch(pool)
4603                            .await?;
4604                    if rows.is_empty() {
4605                        return ::core::result::Result::Ok(());
4606                    }
4607                    let len = rows.len() as i64;
4608                    // Capture the last row's PK BEFORE moving rows
4609                    // into the callback.
4610                    let max_pk = match rows
4611                        .last()
4612                        .map(|r| r.__rustango_pk_value())
4613                    {
4614                        ::core::option::Option::Some(
4615                            #root::core::SqlValue::I64(v),
4616                        ) => v,
4617                        ::core::option::Option::Some(
4618                            #root::core::SqlValue::I32(v),
4619                        ) => i64::from(v),
4620                        _ => return ::core::result::Result::Ok(()),
4621                    };
4622                    cb(rows).await?;
4623                    if len < n {
4624                        return ::core::result::Result::Ok(());
4625                    }
4626                    last_seen = max_pk;
4627                }
4628            }
4629
4630            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4631            /// per-row callback companion to [`Self::chunk`].
4632            /// Streams every row in keyset-paginated batches of
4633            /// `batch` size, calling `cb` once per row.
4634            ///
4635            /// Inherits the keyset-paginated scan of
4636            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4637            /// non-integer PKs are silently a no-op).
4638            ///
4639            /// ```ignore
4640            /// Post::each(500, &pool, |p| async move {
4641            ///     reindex(p).await?;
4642            ///     Ok(())
4643            /// }).await?;
4644            /// ```
4645            ///
4646            /// Return `Err(...)` from the callback to abort
4647            /// iteration; the error bubbles up unchanged.
4648            pub async fn each<F, Fut>(
4649                batch: i64,
4650                pool: &#root::sql::Pool,
4651                mut cb: F,
4652            ) -> ::core::result::Result<(), #root::sql::ExecError>
4653            where
4654                F: ::core::ops::FnMut(Self) -> Fut,
4655                Fut: ::core::future::Future<
4656                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4657                >,
4658            {
4659                use #root::sql::FetcherPool as _;
4660                let pk_col = match Self::primary_key_column() {
4661                    ::core::option::Option::Some(c) => c,
4662                    ::core::option::Option::None => {
4663                        return ::core::result::Result::Ok(());
4664                    }
4665                };
4666                let mut last_seen: i64 = i64::MIN;
4667                loop {
4668                    let key = ::std::format!("{}__gt", pk_col);
4669                    let rows: ::std::vec::Vec<Self> =
4670                        #root::query::QuerySet::<Self>::default()
4671                            .filter(key.as_str(), last_seen)
4672                            .order_by(&[(pk_col, false)])
4673                            .limit(batch)
4674                            .fetch(pool)
4675                            .await?;
4676                    if rows.is_empty() {
4677                        return ::core::result::Result::Ok(());
4678                    }
4679                    let len = rows.len() as i64;
4680                    let max_pk = match rows
4681                        .last()
4682                        .map(|r| r.__rustango_pk_value())
4683                    {
4684                        ::core::option::Option::Some(
4685                            #root::core::SqlValue::I64(v),
4686                        ) => v,
4687                        ::core::option::Option::Some(
4688                            #root::core::SqlValue::I32(v),
4689                        ) => i64::from(v),
4690                        _ => return ::core::result::Result::Ok(()),
4691                    };
4692                    for row in rows {
4693                        cb(row).await?;
4694                    }
4695                    if len < batch {
4696                        return ::core::result::Result::Ok(());
4697                    }
4698                    last_seen = max_pk;
4699                }
4700            }
4701
4702            /// Delete every row of this model — `TRUNCATE TABLE
4703            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4704            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4705            /// support `TRUNCATE` inside foreign-key constraints
4706            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4707            /// / Django `Model.objects.all().delete()` parity.
4708            ///
4709            /// **Use only in tests / fixture-reset flows.** Production
4710            /// writes through this would silently bypass the
4711            /// `pre_delete` / `post_delete` signals (no per-row hooks
4712            /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4713            /// row's audit-log entry.
4714            ///
4715            /// # Errors
4716            /// As [`raw_execute_pool`].
4717            ///
4718            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4719            pub async fn truncate(
4720                pool: &#root::sql::Pool,
4721            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4722                let _table = <Self as #root::core::Model>::SCHEMA.table;
4723                let _dialect = pool.dialect();
4724                let _quoted = _dialect.quote_ident(_table);
4725                let _sql = if _dialect.name() == "postgres" {
4726                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4727                } else {
4728                    ::std::format!("DELETE FROM {}", _quoted)
4729                };
4730                #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4731            }
4732
4733            /// Bulk-delete every row whose primary key is in
4734            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4735            /// Returns the affected row count.
4736            ///
4737            /// Eloquent `Model::destroy([1, 2, 3])` / Django
4738            /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4739            /// Empty `pks` is a no-op (returns 0).
4740            ///
4741            /// Accepts any iterable whose elements are
4742            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4743            /// `[i64; N]`, etc.
4744            ///
4745            /// # Errors
4746            /// As `delete_pool`.
4747            pub async fn destroy<V>(
4748                pks: impl ::core::iter::IntoIterator<Item = V>,
4749                pool: &#root::sql::Pool,
4750            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4751            where
4752                V: ::core::convert::Into<#root::core::SqlValue>,
4753            {
4754                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4755                    pks.into_iter().map(::core::convert::Into::into).collect();
4756                if _values.is_empty() {
4757                    return ::core::result::Result::Ok(0);
4758                }
4759                let _query = #root::core::DeleteQuery {
4760                    model: <Self as #root::core::Model>::SCHEMA,
4761                    where_clause: #root::core::WhereExpr::Predicate(
4762                        #root::core::Filter {
4763                            column: <Self as #root::core::Model>::SCHEMA
4764                                .primary_key()
4765                                .ok_or_else(|| {
4766                                    #root::sql::ExecError::Sql(
4767                                        #root::sql::SqlError::MissingPrimaryKey,
4768                                    )
4769                                })?
4770                                .column,
4771                            op: #root::core::Op::In,
4772                            value: #root::core::SqlValue::List(_values),
4773                        },
4774                    ),
4775                };
4776                #root::sql::delete_pool(pool, &_query).await
4777            }
4778
4779            /// Fetch every row where `<col> = <val>`. Eloquent
4780            /// `Model::where($col, $val)->get()` / Django
4781            /// `Model.objects.filter(col=val).all()` parity.
4782            ///
4783            /// Thin wrapper over `QuerySet::<Self>::default()
4784            /// .filter(col, val).fetch(pool)`. For one row,
4785            /// use [`Self::first_where_pool`]; for a chain that
4786            /// needs further `.filter()` / `.order_by()` /
4787            /// `.limit()`, drop down to `Self::query().filter(...)`
4788            /// directly.
4789            ///
4790            /// `val` accepts any value `Into<SqlValue>` so plain
4791            /// strings, ints, UUIDs, etc. all work.
4792            ///
4793            /// # Errors
4794            /// As [`FetcherPool::fetch`].
4795            ///
4796            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4797            pub async fn where_(
4798                col: &str,
4799                val: impl ::core::convert::Into<#root::core::SqlValue>,
4800                pool: &#root::sql::Pool,
4801            ) -> ::core::result::Result<
4802                ::std::vec::Vec<Self>,
4803                #root::sql::ExecError,
4804            > {
4805                use #root::sql::FetcherPool as _;
4806                #root::query::QuerySet::<Self>::default()
4807                    .filter(col, val)
4808                    .fetch(pool)
4809                    .await
4810            }
4811
4812            /// Fetch every row where `<col> IN (vals)`. Eloquent
4813            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4814            /// `vals` returns no rows (matches SQL's empty-IN
4815            /// semantics).
4816            ///
4817            /// `vals` accepts any iterable whose items are
4818            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4819            /// etc.
4820            ///
4821            /// # Errors
4822            /// As [`FetcherPool::fetch`].
4823            ///
4824            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4825            pub async fn where_in<V>(
4826                col: &str,
4827                vals: impl ::core::iter::IntoIterator<Item = V>,
4828                pool: &#root::sql::Pool,
4829            ) -> ::core::result::Result<
4830                ::std::vec::Vec<Self>,
4831                #root::sql::ExecError,
4832            >
4833            where
4834                V: ::core::convert::Into<#root::core::SqlValue>,
4835            {
4836                use #root::sql::FetcherPool as _;
4837                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4838                    vals.into_iter().map(::core::convert::Into::into).collect();
4839                if _values.is_empty() {
4840                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4841                }
4842                let _key = ::std::format!("{}__in", col);
4843                #root::query::QuerySet::<Self>::default()
4844                    .filter(&_key, #root::core::SqlValue::List(_values))
4845                    .fetch(pool)
4846                    .await
4847            }
4848
4849            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4850            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4851            /// `vals` returns every row (matches SQL's empty-NOT-IN
4852            /// semantics — vacuously true for every row).
4853            ///
4854            /// # Errors
4855            /// As [`FetcherPool::fetch`].
4856            ///
4857            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4858            pub async fn where_not_in<V>(
4859                col: &str,
4860                vals: impl ::core::iter::IntoIterator<Item = V>,
4861                pool: &#root::sql::Pool,
4862            ) -> ::core::result::Result<
4863                ::std::vec::Vec<Self>,
4864                #root::sql::ExecError,
4865            >
4866            where
4867                V: ::core::convert::Into<#root::core::SqlValue>,
4868            {
4869                use #root::sql::FetcherPool as _;
4870                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4871                    vals.into_iter().map(::core::convert::Into::into).collect();
4872                if _values.is_empty() {
4873                    return #root::query::QuerySet::<Self>::default()
4874                        .fetch(pool)
4875                        .await;
4876                }
4877                let _key = ::std::format!("{}__not_in", col);
4878                #root::query::QuerySet::<Self>::default()
4879                    .filter(&_key, #root::core::SqlValue::List(_values))
4880                    .fetch(pool)
4881                    .await
4882            }
4883
4884            /// Fetch every row where `<col> IS NULL`. Eloquent
4885            /// `Model::whereNull($col)->get()` parity.
4886            ///
4887            /// # Errors
4888            /// As [`FetcherPool::fetch`].
4889            ///
4890            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4891            pub async fn where_null(
4892                col: &str,
4893                pool: &#root::sql::Pool,
4894            ) -> ::core::result::Result<
4895                ::std::vec::Vec<Self>,
4896                #root::sql::ExecError,
4897            > {
4898                use #root::sql::FetcherPool as _;
4899                let _key = ::std::format!("{}__isnull", col);
4900                #root::query::QuerySet::<Self>::default()
4901                    .filter(&_key, #root::core::SqlValue::Bool(true))
4902                    .fetch(pool)
4903                    .await
4904            }
4905
4906            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4907            /// `Model::whereNotNull($col)->get()` parity.
4908            ///
4909            /// # Errors
4910            /// As [`FetcherPool::fetch`].
4911            ///
4912            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4913            pub async fn where_not_null(
4914                col: &str,
4915                pool: &#root::sql::Pool,
4916            ) -> ::core::result::Result<
4917                ::std::vec::Vec<Self>,
4918                #root::sql::ExecError,
4919            > {
4920                use #root::sql::FetcherPool as _;
4921                let _key = ::std::format!("{}__isnull", col);
4922                #root::query::QuerySet::<Self>::default()
4923                    .filter(&_key, #root::core::SqlValue::Bool(false))
4924                    .fetch(pool)
4925                    .await
4926            }
4927
4928            /// Fetch up to `n` rows in random order. Eloquent
4929            /// `Model::inRandomOrder()->limit($n)->get()` /
4930            /// `Model::query()->inRandomOrder()->get()->take($n)`
4931            /// parity. **Performance caveat**: random ordering
4932            /// forces a full table scan + per-row random key sort;
4933            /// the optimizer cannot use an index. Prefer a
4934            /// `pk >= rand_offset LIMIT N` walk for huge tables.
4935            ///
4936            /// # Errors
4937            /// As [`FetcherPool::fetch`].
4938            ///
4939            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4940            pub async fn random_n(
4941                n: i64,
4942                pool: &#root::sql::Pool,
4943            ) -> ::core::result::Result<
4944                ::std::vec::Vec<Self>,
4945                #root::sql::ExecError,
4946            > {
4947                use #root::sql::FetcherPool as _;
4948                #root::query::QuerySet::<Self>::default()
4949                    .order_random()
4950                    .limit(n)
4951                    .fetch(pool)
4952                    .await
4953            }
4954
4955            /// Fetch one row in random order. Eloquent
4956            /// `Model::inRandomOrder()->first()` parity. Same
4957            /// performance caveat as [`Self::random_n_pool`].
4958            ///
4959            /// # Errors
4960            /// As [`FetcherPool::fetch`].
4961            ///
4962            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4963            pub async fn random(
4964                pool: &#root::sql::Pool,
4965            ) -> ::core::result::Result<
4966                ::core::option::Option<Self>,
4967                #root::sql::ExecError,
4968            > {
4969                ::core::result::Result::Ok(
4970                    Self::random_n(1, pool).await?.into_iter().next(),
4971                )
4972            }
4973
4974            /// Fetch every row ordered ASC by `field`. Eloquent
4975            /// `Model::oldest($field)->get()` parity — the multi-row
4976            /// counterpart of [`Self::earliest_pool`].
4977            ///
4978            /// # Errors
4979            /// As [`FetcherPool::fetch`].
4980            ///
4981            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4982            pub async fn oldest(
4983                field: &str,
4984                pool: &#root::sql::Pool,
4985            ) -> ::core::result::Result<
4986                ::std::vec::Vec<Self>,
4987                #root::sql::ExecError,
4988            > {
4989                use #root::sql::FetcherPool as _;
4990                #root::query::QuerySet::<Self>::default()
4991                    .order_by(&[(field, false)])
4992                    .fetch(pool)
4993                    .await
4994            }
4995
4996            /// Fetch every row ordered DESC by `field`. Eloquent
4997            /// `Model::latest($field)->get()` parity — the multi-row
4998            /// counterpart of [`Self::latest_pool`].
4999            ///
5000            /// # Errors
5001            /// As [`FetcherPool::fetch`].
5002            ///
5003            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5004            pub async fn newest(
5005                field: &str,
5006                pool: &#root::sql::Pool,
5007            ) -> ::core::result::Result<
5008                ::std::vec::Vec<Self>,
5009                #root::sql::ExecError,
5010            > {
5011                use #root::sql::FetcherPool as _;
5012                #root::query::QuerySet::<Self>::default()
5013                    .order_by(&[(field, true)])
5014                    .fetch(pool)
5015                    .await
5016            }
5017
5018            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5019            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5020            /// Routes through the existing `__year` lookup suffix
5021            /// (issue #829).
5022            ///
5023            /// # Errors
5024            /// As [`FetcherPool::fetch`].
5025            ///
5026            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5027            pub async fn where_year(
5028                col: &str,
5029                year: i64,
5030                pool: &#root::sql::Pool,
5031            ) -> ::core::result::Result<
5032                ::std::vec::Vec<Self>,
5033                #root::sql::ExecError,
5034            > {
5035                use #root::sql::FetcherPool as _;
5036                let _key = ::std::format!("{}__year", col);
5037                #root::query::QuerySet::<Self>::default()
5038                    .filter(&_key, #root::core::SqlValue::I64(year))
5039                    .fetch(pool)
5040                    .await
5041            }
5042
5043            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5044            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5045            /// `month` is 1–12.
5046            ///
5047            /// # Errors
5048            /// As [`FetcherPool::fetch`].
5049            ///
5050            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5051            pub async fn where_month(
5052                col: &str,
5053                month: i64,
5054                pool: &#root::sql::Pool,
5055            ) -> ::core::result::Result<
5056                ::std::vec::Vec<Self>,
5057                #root::sql::ExecError,
5058            > {
5059                use #root::sql::FetcherPool as _;
5060                let _key = ::std::format!("{}__month", col);
5061                #root::query::QuerySet::<Self>::default()
5062                    .filter(&_key, #root::core::SqlValue::I64(month))
5063                    .fetch(pool)
5064                    .await
5065            }
5066
5067            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5068            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5069            /// `day` is 1–31.
5070            ///
5071            /// # Errors
5072            /// As [`FetcherPool::fetch`].
5073            ///
5074            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5075            pub async fn where_day(
5076                col: &str,
5077                day: i64,
5078                pool: &#root::sql::Pool,
5079            ) -> ::core::result::Result<
5080                ::std::vec::Vec<Self>,
5081                #root::sql::ExecError,
5082            > {
5083                use #root::sql::FetcherPool as _;
5084                let _key = ::std::format!("{}__day", col);
5085                #root::query::QuerySet::<Self>::default()
5086                    .filter(&_key, #root::core::SqlValue::I64(day))
5087                    .fetch(pool)
5088                    .await
5089            }
5090
5091            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5092            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5093            /// `hour` is 0–23.
5094            ///
5095            /// # Errors
5096            /// As [`FetcherPool::fetch`].
5097            ///
5098            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5099            pub async fn where_hour(
5100                col: &str,
5101                hour: i64,
5102                pool: &#root::sql::Pool,
5103            ) -> ::core::result::Result<
5104                ::std::vec::Vec<Self>,
5105                #root::sql::ExecError,
5106            > {
5107                use #root::sql::FetcherPool as _;
5108                let _key = ::std::format!("{}__hour", col);
5109                #root::query::QuerySet::<Self>::default()
5110                    .filter(&_key, #root::core::SqlValue::I64(hour))
5111                    .fetch(pool)
5112                    .await
5113            }
5114
5115            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5116            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5117            /// `minute` is 0–59.
5118            ///
5119            /// # Errors
5120            /// As [`FetcherPool::fetch`].
5121            ///
5122            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5123            pub async fn where_minute(
5124                col: &str,
5125                minute: i64,
5126                pool: &#root::sql::Pool,
5127            ) -> ::core::result::Result<
5128                ::std::vec::Vec<Self>,
5129                #root::sql::ExecError,
5130            > {
5131                use #root::sql::FetcherPool as _;
5132                let _key = ::std::format!("{}__minute", col);
5133                #root::query::QuerySet::<Self>::default()
5134                    .filter(&_key, #root::core::SqlValue::I64(minute))
5135                    .fetch(pool)
5136                    .await
5137            }
5138
5139            /// Fetch every row where `<col> LIKE <pattern>` —
5140            /// caller-supplied pattern (must include `%` / `_`
5141            /// wildcards manually). Eloquent `Model::whereLike`
5142            /// parity.
5143            ///
5144            /// # Errors
5145            /// As [`FetcherPool::fetch`].
5146            ///
5147            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5148            pub async fn where_like(
5149                col: &str,
5150                pattern: impl ::core::convert::Into<::std::string::String>,
5151                pool: &#root::sql::Pool,
5152            ) -> ::core::result::Result<
5153                ::std::vec::Vec<Self>,
5154                #root::sql::ExecError,
5155            > {
5156                use #root::sql::FetcherPool as _;
5157                let _key = ::std::format!("{}__like", col);
5158                #root::query::QuerySet::<Self>::default()
5159                    .filter(
5160                        &_key,
5161                        #root::core::SqlValue::String(pattern.into()),
5162                    )
5163                    .fetch(pool)
5164                    .await
5165            }
5166
5167            /// Fetch every row where `<col> ILIKE <pattern>` —
5168            /// case-insensitive LIKE (PG native, MySQL/SQLite
5169            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5170            ///
5171            /// # Errors
5172            /// As [`FetcherPool::fetch`].
5173            ///
5174            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5175            pub async fn where_ilike(
5176                col: &str,
5177                pattern: impl ::core::convert::Into<::std::string::String>,
5178                pool: &#root::sql::Pool,
5179            ) -> ::core::result::Result<
5180                ::std::vec::Vec<Self>,
5181                #root::sql::ExecError,
5182            > {
5183                use #root::sql::FetcherPool as _;
5184                let _key = ::std::format!("{}__ilike", col);
5185                #root::query::QuerySet::<Self>::default()
5186                    .filter(
5187                        &_key,
5188                        #root::core::SqlValue::String(pattern.into()),
5189                    )
5190                    .fetch(pool)
5191                    .await
5192            }
5193
5194            /// Fetch every row where `<col>` starts with `prefix`
5195            /// (auto-appends `%`). Django `__startswith` / Eloquent
5196            /// `whereLike("col", "$prefix%")` parity.
5197            ///
5198            /// # Errors
5199            /// As [`FetcherPool::fetch`].
5200            ///
5201            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5202            pub async fn where_starts_with(
5203                col: &str,
5204                prefix: impl ::core::convert::Into<::std::string::String>,
5205                pool: &#root::sql::Pool,
5206            ) -> ::core::result::Result<
5207                ::std::vec::Vec<Self>,
5208                #root::sql::ExecError,
5209            > {
5210                use #root::sql::FetcherPool as _;
5211                let _key = ::std::format!("{}__startswith", col);
5212                #root::query::QuerySet::<Self>::default()
5213                    .filter(
5214                        &_key,
5215                        #root::core::SqlValue::String(prefix.into()),
5216                    )
5217                    .fetch(pool)
5218                    .await
5219            }
5220
5221            /// Fetch every row where `<col>` ends with `suffix`
5222            /// (auto-prepends `%`). Django `__endswith` / Eloquent
5223            /// `whereLike("col", "%$suffix")` parity.
5224            ///
5225            /// # Errors
5226            /// As [`FetcherPool::fetch`].
5227            ///
5228            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5229            pub async fn where_ends_with(
5230                col: &str,
5231                suffix: impl ::core::convert::Into<::std::string::String>,
5232                pool: &#root::sql::Pool,
5233            ) -> ::core::result::Result<
5234                ::std::vec::Vec<Self>,
5235                #root::sql::ExecError,
5236            > {
5237                use #root::sql::FetcherPool as _;
5238                let _key = ::std::format!("{}__endswith", col);
5239                #root::query::QuerySet::<Self>::default()
5240                    .filter(
5241                        &_key,
5242                        #root::core::SqlValue::String(suffix.into()),
5243                    )
5244                    .fetch(pool)
5245                    .await
5246            }
5247
5248            /// Fetch every row where `<col>` contains `substr`
5249            /// (auto-wraps with `%`). Django `__contains` /
5250            /// Eloquent `whereLike("col", "%$substr%")` parity.
5251            ///
5252            /// # Errors
5253            /// As [`FetcherPool::fetch`].
5254            ///
5255            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5256            pub async fn where_contains(
5257                col: &str,
5258                substr: impl ::core::convert::Into<::std::string::String>,
5259                pool: &#root::sql::Pool,
5260            ) -> ::core::result::Result<
5261                ::std::vec::Vec<Self>,
5262                #root::sql::ExecError,
5263            > {
5264                use #root::sql::FetcherPool as _;
5265                let _key = ::std::format!("{}__contains", col);
5266                #root::query::QuerySet::<Self>::default()
5267                    .filter(
5268                        &_key,
5269                        #root::core::SqlValue::String(substr.into()),
5270                    )
5271                    .fetch(pool)
5272                    .await
5273            }
5274
5275            /// Fetch every row where `<col> > val`. Eloquent
5276            /// `Model::where($col, ">", $val)->get()` parity.
5277            ///
5278            /// # Errors
5279            /// As [`FetcherPool::fetch`].
5280            ///
5281            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5282            pub async fn where_gt(
5283                col: &str,
5284                val: impl ::core::convert::Into<#root::core::SqlValue>,
5285                pool: &#root::sql::Pool,
5286            ) -> ::core::result::Result<
5287                ::std::vec::Vec<Self>,
5288                #root::sql::ExecError,
5289            > {
5290                use #root::sql::FetcherPool as _;
5291                let _key = ::std::format!("{}__gt", col);
5292                #root::query::QuerySet::<Self>::default()
5293                    .filter(&_key, val)
5294                    .fetch(pool)
5295                    .await
5296            }
5297
5298            /// Fetch every row where `<col> >= val`. Eloquent
5299            /// `Model::where($col, ">=", $val)->get()` parity.
5300            ///
5301            /// # Errors
5302            /// As [`FetcherPool::fetch`].
5303            ///
5304            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5305            pub async fn where_gte(
5306                col: &str,
5307                val: impl ::core::convert::Into<#root::core::SqlValue>,
5308                pool: &#root::sql::Pool,
5309            ) -> ::core::result::Result<
5310                ::std::vec::Vec<Self>,
5311                #root::sql::ExecError,
5312            > {
5313                use #root::sql::FetcherPool as _;
5314                let _key = ::std::format!("{}__gte", col);
5315                #root::query::QuerySet::<Self>::default()
5316                    .filter(&_key, val)
5317                    .fetch(pool)
5318                    .await
5319            }
5320
5321            /// Fetch every row where `<col> < val`. Eloquent
5322            /// `Model::where($col, "<", $val)->get()` parity.
5323            ///
5324            /// # Errors
5325            /// As [`FetcherPool::fetch`].
5326            ///
5327            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5328            pub async fn where_lt(
5329                col: &str,
5330                val: impl ::core::convert::Into<#root::core::SqlValue>,
5331                pool: &#root::sql::Pool,
5332            ) -> ::core::result::Result<
5333                ::std::vec::Vec<Self>,
5334                #root::sql::ExecError,
5335            > {
5336                use #root::sql::FetcherPool as _;
5337                let _key = ::std::format!("{}__lt", col);
5338                #root::query::QuerySet::<Self>::default()
5339                    .filter(&_key, val)
5340                    .fetch(pool)
5341                    .await
5342            }
5343
5344            /// Fetch every row where `<col> <= val`. Eloquent
5345            /// `Model::where($col, "<=", $val)->get()` parity.
5346            ///
5347            /// # Errors
5348            /// As [`FetcherPool::fetch`].
5349            ///
5350            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5351            pub async fn where_lte(
5352                col: &str,
5353                val: impl ::core::convert::Into<#root::core::SqlValue>,
5354                pool: &#root::sql::Pool,
5355            ) -> ::core::result::Result<
5356                ::std::vec::Vec<Self>,
5357                #root::sql::ExecError,
5358            > {
5359                use #root::sql::FetcherPool as _;
5360                let _key = ::std::format!("{}__lte", col);
5361                #root::query::QuerySet::<Self>::default()
5362                    .filter(&_key, val)
5363                    .fetch(pool)
5364                    .await
5365            }
5366
5367            /// Fetch every row where `<col> <> val`. Eloquent
5368            /// `Model::where($col, "!=", $val)->get()` parity.
5369            ///
5370            /// # Errors
5371            /// As [`FetcherPool::fetch`].
5372            ///
5373            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5374            pub async fn where_ne(
5375                col: &str,
5376                val: impl ::core::convert::Into<#root::core::SqlValue>,
5377                pool: &#root::sql::Pool,
5378            ) -> ::core::result::Result<
5379                ::std::vec::Vec<Self>,
5380                #root::sql::ExecError,
5381            > {
5382                use #root::sql::FetcherPool as _;
5383                let _key = ::std::format!("{}__ne", col);
5384                #root::query::QuerySet::<Self>::default()
5385                    .filter(&_key, val)
5386                    .fetch(pool)
5387                    .await
5388            }
5389
5390            /// Fetch every row matching `<col> = val` for ANY of the
5391            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5392            /// parity. Empty `cols` returns no rows.
5393            ///
5394            /// Resolves each `&str` column to its SCHEMA-registered
5395            /// `&'static str` once and builds a single OR-composed
5396            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5397            ///
5398            /// # Errors
5399            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5400            /// when any column is not declared on the model.
5401            ///
5402            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5403            pub async fn where_any(
5404                cols: &[&str],
5405                val: impl ::core::convert::Into<#root::core::SqlValue>,
5406                pool: &#root::sql::Pool,
5407            ) -> ::core::result::Result<
5408                ::std::vec::Vec<Self>,
5409                #root::sql::ExecError,
5410            > {
5411                Self::__where_multi(cols, val, false, pool).await
5412            }
5413
5414            /// Fetch every row matching `<col> = val` for ALL listed
5415            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5416            /// Empty `cols` returns every row (vacuous AND).
5417            ///
5418            /// # Errors
5419            /// As [`Self::where_any`].
5420            pub async fn where_all(
5421                cols: &[&str],
5422                val: impl ::core::convert::Into<#root::core::SqlValue>,
5423                pool: &#root::sql::Pool,
5424            ) -> ::core::result::Result<
5425                ::std::vec::Vec<Self>,
5426                #root::sql::ExecError,
5427            > {
5428                Self::__where_multi(cols, val, true, pool).await
5429            }
5430
5431            /// Internal: build a Q expression composing `cols` via
5432            /// AND (`all`) or OR (`!all`), then fetch. Backs
5433            /// `where_any` / `where_all`.
5434            #[doc(hidden)]
5435            pub async fn __where_multi(
5436                cols: &[&str],
5437                val: impl ::core::convert::Into<#root::core::SqlValue>,
5438                all: bool,
5439                pool: &#root::sql::Pool,
5440            ) -> ::core::result::Result<
5441                ::std::vec::Vec<Self>,
5442                #root::sql::ExecError,
5443            > {
5444                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5445                    .await
5446            }
5447
5448            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5449            /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5450            /// is NOT guaranteed without an explicit `order_by` —
5451            /// drop into `Self::query()` for that.
5452            ///
5453            /// # Errors
5454            /// As [`FetcherPool::fetch`].
5455            ///
5456            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5457            pub async fn take(
5458                n: i64,
5459                pool: &#root::sql::Pool,
5460            ) -> ::core::result::Result<
5461                ::std::vec::Vec<Self>,
5462                #root::sql::ExecError,
5463            > {
5464                use #root::sql::FetcherPool as _;
5465                #root::query::QuerySet::<Self>::default()
5466                    .limit(n)
5467                    .fetch(pool)
5468                    .await
5469            }
5470
5471            /// Fetch the page-th window of `per_page` rows
5472            /// (1-indexed). Eloquent
5473            /// `Model::query()->forPage($page, $perPage)->get()`
5474            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5475            /// LIMIT per_page`; for large offsets this is O(N) —
5476            /// prefer keyset pagination via PK on hot paths.
5477            ///
5478            /// # Errors
5479            /// As [`FetcherPool::fetch`].
5480            ///
5481            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5482            pub async fn for_page(
5483                page: i64,
5484                per_page: i64,
5485                pool: &#root::sql::Pool,
5486            ) -> ::core::result::Result<
5487                ::std::vec::Vec<Self>,
5488                #root::sql::ExecError,
5489            > {
5490                use #root::sql::FetcherPool as _;
5491                let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5492                #root::query::QuerySet::<Self>::default()
5493                    .limit(per_page)
5494                    .offset(_offset)
5495                    .fetch(pool)
5496                    .await
5497            }
5498
5499            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5500            /// one page of rows AND the total row count in one
5501            /// call. Returns `(rows, total)`. Two queries: a
5502            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5503            /// for the total.
5504            ///
5505            /// Useful for paginated UIs that need both the visible
5506            /// rows AND a "Page X of Y" / total-count widget. For
5507            /// large tables prefer keyset pagination + cached count;
5508            /// every call to `paginate` re-counts the full table.
5509            ///
5510            /// Same 1-indexed `page` convention as [`Self::for_page`].
5511            ///
5512            /// # Errors
5513            /// As [`Self::for_page`] and [`Self::count`].
5514            pub async fn paginate(
5515                page: i64,
5516                per_page: i64,
5517                pool: &#root::sql::Pool,
5518            ) -> ::core::result::Result<
5519                (::std::vec::Vec<Self>, i64),
5520                #root::sql::ExecError,
5521            > {
5522                // Route through the queryset rather than `Self::count`:
5523                // the `count` inherent method is suppressed on models
5524                // that declare a field named `count` (field/shortcut
5525                // collision guard), and `paginate` must still compile
5526                // for those models.
5527                let total = {
5528                    use #root::sql::CounterPool as _;
5529                    #root::query::QuerySet::<Self>::default()
5530                        .count(pool)
5531                        .await?
5532                };
5533                let rows = Self::for_page(page, per_page, pool).await?;
5534                ::core::result::Result::Ok((rows, total))
5535            }
5536
5537            /// Bulk-update — set `set_col = set_val` on every row
5538            /// matching `where_col = where_val`. Returns affected row
5539            /// count. Eloquent
5540            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5541            /// parity.
5542            ///
5543            /// For multi-column updates drop into the queryset
5544            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5545            ///
5546            /// # Errors
5547            /// As [`UpdaterPool::execute_pool`].
5548            ///
5549            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5550            pub async fn update_where(
5551                where_col: &str,
5552                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5553                set_col: &str,
5554                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5555                pool: &#root::sql::Pool,
5556            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5557                use #root::sql::UpdaterPool as _;
5558                #root::query::QuerySet::<Self>::default()
5559                    .filter(where_col, where_val)
5560                    .update()
5561                    .set(set_col, set_val)
5562                    .execute_pool(pool)
5563                    .await
5564            }
5565
5566            /// Bulk-delete — remove every row matching
5567            /// `where_col = where_val`. Returns affected row count.
5568            /// Eloquent
5569            /// `Model::where($where_col, $where_val)->delete()` parity.
5570            ///
5571            /// For more complex filters drop into the queryset
5572            /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5573            ///
5574            /// # Errors
5575            /// As [`#root::sql::delete_pool`].
5576            pub async fn delete_where(
5577                where_col: &str,
5578                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5579                pool: &#root::sql::Pool,
5580            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5581                let _query = #root::core::DeleteQuery {
5582                    model: <Self as #root::core::Model>::SCHEMA,
5583                    where_clause: #root::core::WhereExpr::Predicate(
5584                        #root::core::Filter {
5585                            column: <Self as #root::core::Model>::SCHEMA
5586                                .field(where_col)
5587                                .ok_or_else(|| {
5588                                    #root::sql::ExecError::Query(
5589                                        #root::core::QueryError::UnknownField {
5590                                            model: <Self as #root::core::Model>::SCHEMA.name,
5591                                            field: ::std::string::ToString::to_string(where_col),
5592                                        },
5593                                    )
5594                                })?
5595                                .column,
5596                            op: #root::core::Op::Eq,
5597                            value: ::core::convert::Into::into(where_val),
5598                        },
5599                    ),
5600                };
5601                #root::sql::delete_pool(pool, &_query).await
5602            }
5603
5604            /// Bulk-update — set `set_col = set_val` on EVERY row of
5605            /// this model's table. **No WHERE clause** — use with
5606            /// care. Eloquent `Model::query()->update([$col => $val])`
5607            /// parity.
5608            ///
5609            /// Use for backfills, one-shot reset flows, etc. The
5610            /// returned count is rows affected.
5611            ///
5612            /// # Errors
5613            /// As [`UpdaterPool::execute_pool`].
5614            ///
5615            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5616            pub async fn update_all(
5617                set_col: &str,
5618                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5619                pool: &#root::sql::Pool,
5620            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5621                use #root::sql::UpdaterPool as _;
5622                #root::query::QuerySet::<Self>::default()
5623                    .update()
5624                    .set(set_col, set_val)
5625                    .execute_pool(pool)
5626                    .await
5627            }
5628
5629            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5630            /// Eloquent `Model::whereNotLike` parity. Pattern is
5631            /// passed verbatim — caller controls `%` / `_`.
5632            ///
5633            /// # Errors
5634            /// As [`FetcherPool::fetch`].
5635            ///
5636            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5637            pub async fn where_not_like(
5638                col: &str,
5639                pattern: impl ::core::convert::Into<::std::string::String>,
5640                pool: &#root::sql::Pool,
5641            ) -> ::core::result::Result<
5642                ::std::vec::Vec<Self>,
5643                #root::sql::ExecError,
5644            > {
5645                use #root::sql::FetcherPool as _;
5646                let _key = ::std::format!("{}__not_like", col);
5647                #root::query::QuerySet::<Self>::default()
5648                    .filter(
5649                        &_key,
5650                        #root::core::SqlValue::String(pattern.into()),
5651                    )
5652                    .fetch(pool)
5653                    .await
5654            }
5655
5656            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5657            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5658            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5659            ///
5660            /// # Errors
5661            /// As [`FetcherPool::fetch`].
5662            ///
5663            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5664            pub async fn where_not_ilike(
5665                col: &str,
5666                pattern: impl ::core::convert::Into<::std::string::String>,
5667                pool: &#root::sql::Pool,
5668            ) -> ::core::result::Result<
5669                ::std::vec::Vec<Self>,
5670                #root::sql::ExecError,
5671            > {
5672                use #root::sql::FetcherPool as _;
5673                let _key = ::std::format!("{}__not_ilike", col);
5674                #root::query::QuerySet::<Self>::default()
5675                    .filter(
5676                        &_key,
5677                        #root::core::SqlValue::String(pattern.into()),
5678                    )
5679                    .fetch(pool)
5680                    .await
5681            }
5682
5683            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5684            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5685            /// parity.
5686            ///
5687            /// # Errors
5688            /// As [`FetcherPool::fetch`].
5689            ///
5690            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5691            pub async fn where_not_between(
5692                col: &str,
5693                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5694                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5695                pool: &#root::sql::Pool,
5696            ) -> ::core::result::Result<
5697                ::std::vec::Vec<Self>,
5698                #root::sql::ExecError,
5699            > {
5700                use #root::sql::FetcherPool as _;
5701                let _key = ::std::format!("{}__not_between", col);
5702                let _vals = #root::core::SqlValue::List(::std::vec![
5703                    ::core::convert::Into::into(lo),
5704                    ::core::convert::Into::into(hi),
5705                ]);
5706                #root::query::QuerySet::<Self>::default()
5707                    .filter(&_key, _vals)
5708                    .fetch(pool)
5709                    .await
5710            }
5711
5712            /// Returns the SQL table name for this model. Eloquent
5713            /// `$model->getTable()` parity.
5714            #[must_use]
5715            pub fn table_name() -> &'static str {
5716                <Self as #root::core::Model>::SCHEMA.table
5717            }
5718
5719            /// Returns the SQL column name of this model's primary
5720            /// key, or `None` when the model has no
5721            /// `#[rustango(primary_key)]`. Eloquent
5722            /// `$model->getKeyName()` parity.
5723            #[must_use]
5724            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5725                <Self as #root::core::Model>::SCHEMA
5726                    .primary_key()
5727                    .map(|f| f.column)
5728            }
5729
5730            /// Fetch every row where `<col> BETWEEN lo AND hi`
5731            /// (inclusive on both ends — same as SQL). Eloquent
5732            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5733            ///
5734            /// # Errors
5735            /// As [`FetcherPool::fetch`].
5736            ///
5737            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5738            pub async fn where_between(
5739                col: &str,
5740                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5741                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5742                pool: &#root::sql::Pool,
5743            ) -> ::core::result::Result<
5744                ::std::vec::Vec<Self>,
5745                #root::sql::ExecError,
5746            > {
5747                use #root::sql::FetcherPool as _;
5748                let _key = ::std::format!("{}__between", col);
5749                let _vals = #root::core::SqlValue::List(::std::vec![
5750                    ::core::convert::Into::into(lo),
5751                    ::core::convert::Into::into(hi),
5752                ]);
5753                #root::query::QuerySet::<Self>::default()
5754                    .filter(&_key, _vals)
5755                    .fetch(pool)
5756                    .await
5757            }
5758
5759            /// Fetch the first row where `<col> = <val>`. Returns
5760            /// `Ok(None)` when no row matches. Eloquent
5761            /// `Model::firstWhere($col, $val)` / Django
5762            /// `Model.objects.filter(col=val).first()` parity.
5763            ///
5764            /// Thin wrapper over `QuerySet::<Self>::default()
5765            /// .filter(col, val).first(pool)`. Use this when you
5766            /// want one row identified by a non-PK column (e.g.
5767            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5768            ///
5769            /// `val` accepts any value `Into<SqlValue>` so plain
5770            /// strings, ints, UUIDs, etc. all work.
5771            ///
5772            /// # Errors
5773            /// As `QuerySet::first`.
5774            pub async fn first_where(
5775                col: &str,
5776                val: impl ::core::convert::Into<#root::core::SqlValue>,
5777                pool: &#root::sql::Pool,
5778            ) -> ::core::result::Result<
5779                ::core::option::Option<Self>,
5780                #root::sql::ExecError,
5781            > {
5782                #root::query::QuerySet::<Self>::default()
5783                    .filter(col, val)
5784                    .first(pool)
5785                    .await
5786            }
5787
5788            #value_method
5789
5790            /// Fetch the row with the largest `field` value —
5791            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5792            /// `Ok(None)` for an empty table. Eloquent
5793            /// `Model::latest($field)->first()` / Django
5794            /// `Model.objects.latest(field)` (non-throwing) parity.
5795            /// Thin wrapper over `QuerySet::<Self>::default()
5796            /// .latest(field, pool)`.
5797            ///
5798            /// **Field name** is the Rust field ident as a string
5799            /// (not the SQL column). Unknown fields surface as
5800            /// `ExecError::Query(QueryError::UnknownField)` at
5801            /// compile time.
5802            ///
5803            /// # Errors
5804            /// As `QuerySet::latest`.
5805            pub async fn latest(
5806                field: &str,
5807                pool: &#root::sql::Pool,
5808            ) -> ::core::result::Result<
5809                ::core::option::Option<Self>,
5810                #root::sql::ExecError,
5811            > {
5812                #root::query::QuerySet::<Self>::default()
5813                    .latest(field, pool)
5814                    .await
5815            }
5816
5817            /// Sibling of [`Self::latest_pool`] — fetches the row
5818            /// with the smallest `field` value (`ORDER BY <field>
5819            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5820            /// ->first()` / Django `Model.objects.earliest(field)`
5821            /// parity.
5822            ///
5823            /// # Errors
5824            /// As [`Self::latest_pool`].
5825            pub async fn earliest(
5826                field: &str,
5827                pool: &#root::sql::Pool,
5828            ) -> ::core::result::Result<
5829                ::core::option::Option<Self>,
5830                #root::sql::ExecError,
5831            > {
5832                #root::query::QuerySet::<Self>::default()
5833                    .earliest(field, pool)
5834                    .await
5835            }
5836
5837            #count_method
5838
5839            /// `true` when the table contains at least one row.
5840            /// Eloquent `Model::query()->exists()` / Django
5841            /// `Model.objects.exists()` parity. Thin wrapper over
5842            /// `QuerySet::<Self>::default().exists(pool)`.
5843            ///
5844            /// # Errors
5845            /// As [`ExistsPool::exists`].
5846            ///
5847            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5848            pub async fn exists(
5849                pool: &#root::sql::Pool,
5850            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5851                use #root::sql::ExistsPool as _;
5852                #root::query::QuerySet::<Self>::default()
5853                    .exists(pool)
5854                    .await
5855            }
5856
5857            /// Inverse of [`Self::exists`] — returns `true` when
5858            /// the table has zero rows. Eloquent
5859            /// `Model::doesntExist()` parity.
5860            ///
5861            /// # Errors
5862            /// As [`Self::exists`].
5863            pub async fn doesnt_exist(
5864                pool: &#root::sql::Pool,
5865            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5866                Self::exists(pool).await.map(|e| !e)
5867            }
5868
5869            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5870            /// `true` when a row with primary key `pk` exists in
5871            /// the table. Sugar over
5872            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5873            ///
5874            /// Differs from [`Self::exists`] (which checks "any row
5875            /// in the table") by checking a specific PK existence.
5876            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5877            /// because the row is never materialized — the SQL is
5878            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5879            ///
5880            /// # Errors
5881            /// As [`#root::sql::ExistsPool::contains_pk`].
5882            pub async fn contains_pk(
5883                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5884                pool: &#root::sql::Pool,
5885            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5886                use #root::sql::ExistsPool as _;
5887                #root::query::QuerySet::<Self>::default()
5888                    .contains_pk(pool, pk)
5889                    .await
5890            }
5891
5892            #sum_method
5893            #avg_method
5894            #min_method
5895            #max_method
5896
5897            /// Internal: forward to
5898            /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5899            /// Backs `sum` / `avg` / `min` / `max`.
5900            #[doc(hidden)]
5901            pub async fn __aggregate_one_pool<U>(
5902                col: &str,
5903                build: fn(&'static str) -> #root::core::AggregateExpr,
5904                pool: &#root::sql::Pool,
5905            ) -> ::core::result::Result<
5906                ::core::option::Option<U>,
5907                #root::sql::ExecError,
5908            >
5909            where
5910                (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5911                    + #root::sql::MaybeMyFromRow
5912                    + #root::sql::MaybeSqliteFromRow
5913                    + ::core::marker::Send
5914                    + ::core::marker::Unpin,
5915            {
5916                #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5917                    .await
5918            }
5919
5920            /// Fetch every row of this model from `pool`. Eloquent
5921            /// `Model::all()` parity — a thin wrapper over
5922            /// `QuerySet::<Self>::default().fetch(pool)`.
5923            ///
5924            /// **Use with care on large tables** — there's no
5925            /// pagination or limit; the entire table is materialized
5926            /// into memory. For anything beyond fixture / lookup
5927            /// tables, page through `QuerySet::<Self>::default()
5928            /// .order_by(...).limit(N).offset(M).fetch(pool)`
5929            /// or stream via `.iterator(chunk_size)`.
5930            ///
5931            /// # Errors
5932            /// As [`FetcherPool::fetch`].
5933            ///
5934            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5935            pub async fn all(
5936                pool: &#root::sql::Pool,
5937            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5938            {
5939                use #root::sql::FetcherPool as _;
5940                #root::query::QuerySet::<Self>::default()
5941                    .fetch(pool)
5942                    .await
5943            }
5944
5945            /// Look up every row whose primary key is in `pks`.
5946            /// Returns the matching rows in **inventory** order — NOT
5947            /// the order of `pks`. Empty `pks` returns an empty
5948            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5949            /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5950            /// parity.
5951            ///
5952            /// Thin wrapper over `QuerySet::<Self>::default()
5953            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5954            /// Caller-supplied PKs that don't match a row are
5955            /// silently skipped (the returned vec is shorter than
5956            /// the input list). For an order-preserving / "fail
5957            /// when any missing" variant, build the queryset
5958            /// explicitly with `in_bulk` instead.
5959            ///
5960            /// Accepts any iterable whose elements are
5961            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5962            /// `Vec<Uuid>`, etc.
5963            ///
5964            /// # Errors
5965            /// As [`FetcherPool::fetch`].
5966            ///
5967            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5968            pub async fn find_many<V>(
5969                pks: impl ::core::iter::IntoIterator<Item = V>,
5970                pool: &#root::sql::Pool,
5971            ) -> ::core::result::Result<
5972                ::std::vec::Vec<Self>,
5973                #root::sql::ExecError,
5974            >
5975            where
5976                V: ::core::convert::Into<#root::core::SqlValue>,
5977            {
5978                use #root::sql::FetcherPool as _;
5979                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5980                    pks.into_iter().map(::core::convert::Into::into).collect();
5981                if _values.is_empty() {
5982                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5983                }
5984                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5985                #root::query::QuerySet::<Self>::default()
5986                    .filter(&_key, #root::core::SqlValue::List(_values))
5987                    .fetch(pool)
5988                    .await
5989            }
5990
5991            /// Look up the row whose primary key equals `pk`. Returns
5992            /// `Ok(None)` when no row matches; this is the
5993            /// non-throwing counterpart of Django's `.get(pk=…)`
5994            /// (which raises `DoesNotExist`). Eloquent `Model::find`
5995            /// shape — accepts any value `Into<SqlValue>`.
5996            ///
5997            /// One-liner shortcut for the common
5998            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5999            /// .limit(1).fetch(pool).await?.into_iter().next()`
6000            /// dance.
6001            ///
6002            /// # Errors
6003            /// As [`FetcherPool::fetch`].
6004            ///
6005            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
6006            pub async fn find(
6007                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6008                pool: &#root::sql::Pool,
6009            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
6010            {
6011                use #root::sql::FetcherPool as _;
6012                let _pk_val: #root::core::SqlValue = pk.into();
6013                let mut _rows: ::std::vec::Vec<Self> =
6014                    #root::query::QuerySet::<Self>::default()
6015                        .filter(::core::stringify!(#pk_ident), _pk_val)
6016                        .limit(1)
6017                        .fetch(pool)
6018                        .await?;
6019                ::core::result::Result::Ok(_rows.into_iter().next())
6020            }
6021
6022            /// Look up the row whose primary key equals `pk`. Errors
6023            /// when no row matches — the throwing counterpart of
6024            /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6025            /// Django `Model.objects.get(pk=…)` (which raises
6026            /// `DoesNotExist`) parity.
6027            ///
6028            /// Translates the miss into
6029            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6030            /// so callers can `?`-bubble straight through the typical
6031            /// `ExecError` error chain.
6032            ///
6033            /// # Errors
6034            /// As [`Self::find_pool`]; additionally
6035            /// [`sqlx::Error::RowNotFound`] when no row matches.
6036            ///
6037            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6038            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6039            pub async fn find_or_fail(
6040                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6041                pool: &#root::sql::Pool,
6042            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6043                match Self::find(pk, pool).await? {
6044                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6045                    ::core::option::Option::None => ::core::result::Result::Err(
6046                        #root::sql::ExecError::Driver(
6047                            #root::sql::sqlx::Error::RowNotFound,
6048                        ),
6049                    ),
6050                }
6051            }
6052
6053            /// Look up multiple rows by primary key, **failing** if
6054            /// any requested PK is missing. Eloquent
6055            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6056            /// inventory order (NOT request order — same as
6057            /// [`Self::find_many`]).
6058            ///
6059            /// Empty `pks` returns an empty `Vec` (no rows requested
6060            /// → nothing to fail on).
6061            ///
6062            /// Differs from [`Self::find_many`] in that this method
6063            /// requires every PK to resolve. If even one is missing,
6064            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6065            /// in `ExecError::Driver`). Rows are deduped at the SQL
6066            /// layer, so passing the same PK twice in `pks` counts as
6067            /// one expected row.
6068            ///
6069            /// # Errors
6070            /// As [`Self::find_many`], plus `RowNotFound` when the
6071            /// returned row count is less than the count of distinct
6072            /// requested PKs.
6073            pub async fn find_many_or_fail<V>(
6074                pks: impl ::core::iter::IntoIterator<Item = V>,
6075                pool: &#root::sql::Pool,
6076            ) -> ::core::result::Result<
6077                ::std::vec::Vec<Self>,
6078                #root::sql::ExecError,
6079            >
6080            where
6081                V: ::core::convert::Into<#root::core::SqlValue>,
6082            {
6083                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6084                    pks.into_iter().map(::core::convert::Into::into).collect();
6085                if _values.is_empty() {
6086                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6087                }
6088                // Dedup the requested PK list before counting so
6089                // duplicate-PK requests don't false-fail (the SQL
6090                // `IN (...)` clause naturally dedups too).
6091                let mut _seen: ::std::collections::HashSet<
6092                    ::std::string::String,
6093                > = ::std::collections::HashSet::new();
6094                for v in &_values {
6095                    _seen.insert(v.to_display_string());
6096                }
6097                let _expected = _seen.len();
6098                let _rows = Self::find_many(_values, pool).await?;
6099                if _rows.len() < _expected {
6100                    return ::core::result::Result::Err(
6101                        #root::sql::ExecError::Driver(
6102                            #root::sql::sqlx::Error::RowNotFound,
6103                        ),
6104                    );
6105                }
6106                ::core::result::Result::Ok(_rows)
6107            }
6108
6109            /// Find by primary key or run `fallback` to produce a
6110            /// default row to return. Eloquent
6111            /// `Model::findOr($pk, fn() => …)` parity.
6112            ///
6113            /// Unlike [`Self::find_or_fail_pool`] (which raises on
6114            /// miss), this is the "give me something sensible"
6115            /// branch: typical use is "fetch the user's row, else
6116            /// fall back to an anonymous/guest stub".
6117            ///
6118            /// `fallback` runs only when no row matches — the DB
6119            /// round-trip happens unconditionally.
6120            ///
6121            /// # Errors
6122            /// As [`Self::find_pool`].
6123            pub async fn find_or<F>(
6124                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6125                pool: &#root::sql::Pool,
6126                fallback: F,
6127            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6128            where
6129                F: ::core::ops::FnOnce() -> Self,
6130            {
6131                ::core::result::Result::Ok(
6132                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6133                )
6134            }
6135
6136            /// Same as [`Self::find_or`] but also returns a `bool`
6137            /// indicating whether the row was found in the DB
6138            /// (`true`) or freshly built from `fallback` (`false`).
6139            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6140            /// in PHP the returned model also exposes `->exists`;
6141            /// here that's surfaced as the second tuple element.
6142            ///
6143            /// Useful for edit-or-create form handlers where the
6144            /// caller needs to know whether to PATCH or POST when
6145            /// the user submits the form.
6146            ///
6147            /// # Errors
6148            /// As [`Self::find`].
6149            pub async fn find_or_new<F>(
6150                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6151                pool: &#root::sql::Pool,
6152                fallback: F,
6153            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6154            where
6155                F: ::core::ops::FnOnce() -> Self,
6156            {
6157                match Self::find(pk, pool).await? {
6158                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6159                    ::core::option::Option::None => {
6160                        ::core::result::Result::Ok((fallback(), false))
6161                    }
6162                }
6163            }
6164
6165            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6166            /// [`Self::find_or_new`] but **persists** the new row
6167            /// when the PK isn't found. Returns `(row, exists: bool)`
6168            /// — `exists=true` when the PK was found,
6169            /// `false` when a fresh row was inserted.
6170            ///
6171            /// ```ignore
6172            /// let (post, found) = Post::find_or_insert(
6173            ///     pk,
6174            ///     &pool,
6175            ///     || Post { id: Auto::default(), title: "new".into() },
6176            /// ).await?;
6177            /// // `found` true → returned existing row;
6178            /// // `found` false → fallback was inserted, `post.id` populated.
6179            /// ```
6180            ///
6181            /// **Caveat**: two concurrent calls that both miss the
6182            /// find can both INSERT, violating uniqueness. Wrap in a
6183            /// transaction or rely on a UNIQUE constraint + handle
6184            /// the conflict error if you need race-free semantics.
6185            ///
6186            /// # Errors
6187            /// As [`Self::find`] and [`Self::save_pool`].
6188            pub async fn find_or_insert<F>(
6189                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6190                pool: &#root::sql::Pool,
6191                fallback: F,
6192            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6193            where
6194                F: ::core::ops::FnOnce() -> Self,
6195            {
6196                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6197                    return ::core::result::Result::Ok((_row, true));
6198                }
6199                let mut _new = fallback();
6200                _new.save_pool(pool).await?;
6201                ::core::result::Result::Ok((_new, false))
6202            }
6203
6204            /// Fetch the first row of the table, or run `fallback`
6205            /// when the table is empty. Eloquent
6206            /// `Model::firstOr(fn() => …)` parity.
6207            ///
6208            /// # Errors
6209            /// As [`Self::first_pool`].
6210            pub async fn first_or<F>(
6211                pool: &#root::sql::Pool,
6212                fallback: F,
6213            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6214            where
6215                F: ::core::ops::FnOnce() -> Self,
6216            {
6217                // Route through the queryset rather than `Self::first`,
6218                // which is suppressed on models with a field named
6219                // `first` (field/shortcut collision guard).
6220                ::core::result::Result::Ok(
6221                    #root::query::QuerySet::<Self>::default()
6222                        .first(pool)
6223                        .await?
6224                        .unwrap_or_else(fallback),
6225                )
6226            }
6227
6228            /// Fetch exactly one row matching `<col> = <val>`. Errors
6229            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6230            /// or more than one matches
6231            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6232            /// Eloquent `Model::sole($col, $val)` parity.
6233            ///
6234            /// # Errors
6235            /// As [`Self::where_pool`] plus the explicit
6236            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6237            pub async fn sole(
6238                col: &str,
6239                val: impl ::core::convert::Into<#root::core::SqlValue>,
6240                pool: &#root::sql::Pool,
6241            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6242                let mut _rows = Self::where_(col, val, pool).await?;
6243                match _rows.len() {
6244                    0 => ::core::result::Result::Err(
6245                        #root::sql::ExecError::Driver(
6246                            #root::sql::sqlx::Error::RowNotFound,
6247                        ),
6248                    ),
6249                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6250                    n => ::core::result::Result::Err(
6251                        #root::sql::ExecError::MultipleRowsReturned {
6252                            op: "sole",
6253                            table: <Self as #root::core::Model>::SCHEMA.name,
6254                            count: n,
6255                        },
6256                    ),
6257                }
6258            }
6259        }
6260    } else {
6261        quote!()
6262    };
6263
6264    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6265    // the non-audited `_pool` methods but execute against an open
6266    // `PoolTx` so the writes participate in the caller's transaction.
6267    // Auditing inside TX is deferred; these always use the plain
6268    // executor primitives regardless of whether the model is audited.
6269    let tx_insert_method = if fields.has_auto {
6270        let pushes = &fields.insert_pushes;
6271        let returning_cols = &fields.returning_cols;
6272        quote! {
6273            /// Insert this row inside an open transaction, populating
6274            /// any `Auto<T>` PK from the auto-assigned value. Works
6275            /// against any backend that `tx` wraps.
6276            ///
6277            /// # Errors
6278            /// As [`Self::insert_pool`].
6279            pub async fn insert_tx(
6280                &mut self,
6281                tx: &mut #root::sql::PoolTx<'_>,
6282            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6283                let mut _columns: ::std::vec::Vec<&'static str> =
6284                    ::std::vec::Vec::new();
6285                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6286                    ::std::vec::Vec::new();
6287                #( #pushes )*
6288                let _query = #root::core::InsertQuery {
6289                    model: <Self as #root::core::Model>::SCHEMA,
6290                    columns: _columns,
6291                    values: _values,
6292                    returning: ::std::vec![ #( #returning_cols ),* ],
6293                    on_conflict: ::core::option::Option::None,
6294                };
6295                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6296                #root::sql::apply_auto_pk(_result, self)
6297            }
6298        }
6299    } else {
6300        let insert_columns = &fields.insert_columns;
6301        let insert_values = &fields.insert_values;
6302        quote! {
6303            /// Insert this row inside an open transaction.
6304            ///
6305            /// # Errors
6306            /// As [`Self::insert_pool`].
6307            pub async fn insert_tx(
6308                &self,
6309                tx: &mut #root::sql::PoolTx<'_>,
6310            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6311                let _query = #root::core::InsertQuery {
6312                    model: <Self as #root::core::Model>::SCHEMA,
6313                    columns: ::std::vec![ #( #insert_columns ),* ],
6314                    values: ::std::vec![ #( #insert_values ),* ],
6315                    returning: ::std::vec::Vec::new(),
6316                    on_conflict: ::core::option::Option::None,
6317                };
6318                #root::sql::insert_tx(tx, &_query).await
6319            }
6320        }
6321    };
6322
6323    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6324        let pk_column_lit = pk_col.as_str();
6325        let assignments = &fields.update_assignments;
6326        let dispatch_unset = if fields.pk_is_auto {
6327            quote! {
6328                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6329                    return self.insert_tx(tx).await.map(|()| 1u64);
6330                }
6331            }
6332        } else {
6333            quote!()
6334        };
6335        quote! {
6336            /// Save this row inside an open transaction. `INSERT` when
6337            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6338            /// PK. Works against any backend that `tx` wraps.
6339            ///
6340            /// # Errors
6341            /// As [`Self::save_pool`].
6342            pub async fn save_tx(
6343                &mut self,
6344                tx: &mut #root::sql::PoolTx<'_>,
6345            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6346                #dispatch_unset
6347                let _query = #root::core::UpdateQuery {
6348                    model: <Self as #root::core::Model>::SCHEMA,
6349                    set: ::std::vec![ #( #assignments ),* ],
6350                    where_clause: #root::core::WhereExpr::Predicate(
6351                        #root::core::Filter {
6352                            column: #pk_column_lit,
6353                            op: #root::core::Op::Eq,
6354                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6355                                ::core::clone::Clone::clone(&self.#pk_ident)
6356                            ),
6357                        }
6358                    ),
6359                };
6360                let _affected = #root::sql::update_tx(tx, &_query).await?;
6361                ::core::result::Result::Ok(_affected)
6362            }
6363        }
6364    } else {
6365        quote!()
6366    };
6367
6368    let tx_delete_method = {
6369        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6370        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6371        if let Some(pk_ident) = pk_ident_for_tx {
6372            quote! {
6373                /// Delete the row identified by this instance's PK
6374                /// inside an open transaction. Works against any backend
6375                /// that `tx` wraps.
6376                ///
6377                /// # Errors
6378                /// As [`Self::delete_pool`].
6379                pub async fn delete_tx(
6380                    &self,
6381                    tx: &mut #root::sql::PoolTx<'_>,
6382                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6383                    let _query = #root::core::DeleteQuery {
6384                        model: <Self as #root::core::Model>::SCHEMA,
6385                        where_clause: #root::core::WhereExpr::Predicate(
6386                            #root::core::Filter {
6387                                column: #pk_column_lit,
6388                                op: #root::core::Op::Eq,
6389                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6390                                    ::core::clone::Clone::clone(&self.#pk_ident)
6391                                ),
6392                            }
6393                        ),
6394                    };
6395                    #root::sql::delete_tx(tx, &_query).await
6396                }
6397            }
6398        } else {
6399            quote!()
6400        }
6401    };
6402
6403    // Update emission captures both BEFORE and AFTER state — runs an
6404    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6405    // each tracked field's prior value, then after the UPDATE diffs
6406    // against the in-memory `&self`. `diff_changes` drops unchanged
6407    // columns so the JSON only contains the actual delta.
6408    //
6409    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6410    // and binds `_audit_before_pairs`; `audit_update_post` runs
6411    // after the UPDATE and emits the PendingEntry.
6412    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6413        audited_fields
6414    {
6415        if tracked.is_empty() {
6416            (quote!(), quote!())
6417        } else {
6418            let select_cols: String = tracked
6419                .iter()
6420                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6421                .collect::<Vec<_>>()
6422                .join(", ");
6423            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6424            let select_cols_lit = select_cols;
6425            let pk_column_lit_for_select = pk_column_for_select;
6426            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6427                if fields.pk_is_auto {
6428                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6429                } else {
6430                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6431                }
6432            } else {
6433                quote!(0_i64)
6434            };
6435            let before_pairs = tracked.iter().map(|c| {
6436                let column_lit = c.column.as_str();
6437                let value_ty = &c.value_ty;
6438                quote! {
6439                    (
6440                        #column_lit,
6441                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6442                            &_audit_before_row, #column_lit,
6443                        ) {
6444                            ::core::result::Result::Ok(v) => {
6445                                #root::__serde_json::to_value(&v)
6446                                    .unwrap_or(#root::__serde_json::Value::Null)
6447                            }
6448                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6449                        },
6450                    )
6451                }
6452            });
6453            let after_pairs = tracked.iter().map(|c| {
6454                let column_lit = c.column.as_str();
6455                let ident = &c.ident;
6456                quote! {
6457                    (
6458                        #column_lit,
6459                        #root::__serde_json::to_value(&self.#ident)
6460                            .unwrap_or(#root::__serde_json::Value::Null),
6461                    )
6462                }
6463            });
6464            let pk_str = audit_pk_to_string.clone();
6465            let pre = quote! {
6466                let _audit_select_sql = ::std::format!(
6467                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6468                    #select_cols_lit,
6469                    <Self as #root::core::Model>::SCHEMA.table,
6470                    #pk_column_lit_for_select,
6471                );
6472                let _audit_before_pairs:
6473                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6474                    match #root::sql::sqlx::query(&_audit_select_sql)
6475                        .bind(#pk_value_for_bind)
6476                        .fetch_optional(&mut *_executor)
6477                        .await
6478                    {
6479                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6480                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6481                        }
6482                        _ => ::core::option::Option::None,
6483                    };
6484            };
6485            let post = quote! {
6486                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6487                    let _audit_after:
6488                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6489                        ::std::vec![ #( #after_pairs ),* ];
6490                    let _audit_entry = #root::audit::PendingEntry {
6491                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
6492                        entity_pk: #pk_str,
6493                        operation: #root::audit::AuditOp::Update,
6494                        source: #root::audit::current_source(),
6495                        changes: #root::audit::diff_changes(
6496                            &_audit_before,
6497                            &_audit_after,
6498                        ),
6499                    };
6500                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6501                }
6502            };
6503            (pre, post)
6504        }
6505    } else {
6506        (quote!(), quote!())
6507    };
6508
6509    // Bulk-insert audit: capture every row's tracked fields after the
6510    // RETURNING populates each PK, then push one batched INSERT INTO
6511    // audit_log via `emit_many`. One round-trip regardless of N rows.
6512    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6513        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6514            if fields.pk_is_auto {
6515                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6516            } else {
6517                quote!(::std::format!("{}", &_row.#pk_ident))
6518            }
6519        } else {
6520            quote!(::std::string::String::new())
6521        };
6522        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6523            let column_lit = c.column.as_str();
6524            let ident = &c.ident;
6525            quote! {
6526                (
6527                    #column_lit,
6528                    #root::__serde_json::to_value(&_row.#ident)
6529                        .unwrap_or(#root::__serde_json::Value::Null),
6530                )
6531            }
6532        });
6533        quote! {
6534            let _audit_source = #root::audit::current_source();
6535            let mut _audit_entries:
6536                ::std::vec::Vec<#root::audit::PendingEntry> =
6537                    ::std::vec::Vec::with_capacity(rows.len());
6538            for _row in rows.iter() {
6539                _audit_entries.push(#root::audit::PendingEntry {
6540                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
6541                    entity_pk: #row_pk_str,
6542                    operation: #root::audit::AuditOp::Create,
6543                    source: _audit_source.clone(),
6544                    changes: #root::audit::snapshot_changes(&[
6545                        #( #row_pairs ),*
6546                    ]),
6547                });
6548            }
6549            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6550        }
6551    } else {
6552        quote!()
6553    };
6554
6555    let save_method = if fields.pk_is_auto {
6556        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6557        let pk_column_lit = pk_column.as_str();
6558        let assignments = &fields.update_assignments;
6559        let upsert_cols = &fields.upsert_update_columns;
6560        let upsert_pushes = &fields.insert_pushes;
6561        let upsert_returning = &fields.returning_cols;
6562        let upsert_auto_assigns = &fields.auto_assigns;
6563        // Conflict target: prefer the first declared `unique_together`
6564        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6565        // `BIGSERIAL` and never collide on insert, so a PK-only target
6566        // would silently turn `upsert()` into "always-insert" for
6567        // surrogate-PK models with composite UNIQUE constraints — see
6568        // `RolePermission` / `UserRole` / `UserPermission` in the
6569        // tenancy permission engine. When no `unique_together` is
6570        // declared we keep the PK target (the original behaviour).
6571        let upsert_target_columns: Vec<String> = indexes
6572            .iter()
6573            .find(|i| i.unique && !i.columns.is_empty())
6574            .map(|i| i.columns.clone())
6575            .unwrap_or_else(|| vec![pk_column.clone()]);
6576        let upsert_target_lits = upsert_target_columns
6577            .iter()
6578            .map(String::as_str)
6579            .collect::<Vec<_>>();
6580        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6581            quote!(#root::core::ConflictClause::DoNothing)
6582        } else {
6583            quote!(#root::core::ConflictClause::DoUpdate {
6584                target: ::std::vec![ #( #upsert_target_lits ),* ],
6585                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6586            })
6587        };
6588        Some(quote! {
6589            /// Insert this row if its `Auto<T>` primary key is
6590            /// `Unset`, otherwise update the existing row matching the
6591            /// PK. Mirrors Django's `save()` — caller doesn't need to
6592            /// pick `insert` vs the bulk-update path manually.
6593            ///
6594            /// On the insert branch, populates the PK from `RETURNING`
6595            /// (same behavior as `insert`). On the update branch,
6596            /// writes every non-PK column back; if no row matches the
6597            /// PK, returns `Ok(())` silently.
6598            ///
6599            /// Only generated when the primary key is declared as
6600            /// `Auto<T>`. Models with a manually-managed PK must use
6601            /// `insert` or the QuerySet update builder.
6602            ///
6603            /// # Errors
6604            /// Returns [`#root::sql::ExecError`] for SQL-writing
6605            /// or driver failures.
6606            #[cfg(feature = "postgres")]
6607            pub async fn save(
6608                &mut self,
6609                pool: &#root::sql::sqlx::PgPool,
6610            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6611                #pool_to_save_on
6612            }
6613
6614            /// Like [`Self::save`] but accepts any sqlx executor —
6615            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6616            /// escape hatch for tenant-scoped writes: schema-mode
6617            /// tenants share the registry pool but rely on a per-
6618            /// checkout `SET search_path`, so passing `&PgPool` would
6619            /// silently hit the wrong schema. Acquire a connection
6620            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6621            ///
6622            /// # Errors
6623            /// As [`Self::save`].
6624            #[cfg(feature = "postgres")]
6625            pub async fn save_on #executor_generics (
6626                &mut self,
6627                #executor_param,
6628            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6629            #executor_where
6630            {
6631                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6632                // the rows-affected count (0 when the PK no longer exists,
6633                // the Django 6.0 `Model.NotUpdated` signal).
6634                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6635                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6636                }
6637                #audit_update_pre
6638                let _query = #root::core::UpdateQuery {
6639                    model: <Self as #root::core::Model>::SCHEMA,
6640                    set: ::std::vec![ #( #assignments ),* ],
6641                    where_clause: #root::core::WhereExpr::Predicate(
6642                        #root::core::Filter {
6643                            column: #pk_column_lit,
6644                            op: #root::core::Op::Eq,
6645                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6646                                ::core::clone::Clone::clone(&self.#pk_ident)
6647                            ),
6648                        }
6649                    ),
6650                };
6651                let _affected = #root::sql::__macro_internals::update_on(
6652                    #executor_passes_to_data_write,
6653                    &_query,
6654                ).await?;
6655                #audit_update_post
6656                ::core::result::Result::Ok(_affected)
6657            }
6658
6659            /// Per-call override for the audit source. Runs
6660            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6661            /// scope so the resulting audit entry records `source`
6662            /// instead of the task-local default. Useful for seed
6663            /// scripts and one-off CLI tools that don't sit inside an
6664            /// admin handler. The override applies only to this call;
6665            /// no global state changes.
6666            ///
6667            /// # Errors
6668            /// As [`Self::save_on`].
6669            #[cfg(feature = "postgres")]
6670            pub async fn save_on_with #executor_generics (
6671                &mut self,
6672                #executor_param,
6673                source: #root::audit::AuditSource,
6674            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6675            #executor_where
6676            {
6677                #root::audit::with_source(source, self.save_on(_executor)).await
6678            }
6679
6680            /// Insert this row or update it in-place if the primary key already
6681            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6682            ///
6683            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6684            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6685            /// inserted if absent or all non-PK columns are overwritten if present.
6686            ///
6687            /// # Errors
6688            /// As [`Self::insert_on`].
6689            #[cfg(feature = "postgres")]
6690            pub async fn upsert(
6691                &mut self,
6692                pool: &#root::sql::sqlx::PgPool,
6693            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6694                #pool_to_upsert_on
6695            }
6696
6697            /// Like [`Self::upsert`] but accepts any sqlx executor.
6698            /// See [`Self::save_on`] for tenancy-scoped rationale.
6699            ///
6700            /// # Errors
6701            /// As [`Self::upsert`].
6702            #[cfg(feature = "postgres")]
6703            pub async fn upsert_on #executor_generics (
6704                &mut self,
6705                #executor_param,
6706            ) -> ::core::result::Result<(), #root::sql::ExecError>
6707            #executor_where
6708            {
6709                let mut _columns: ::std::vec::Vec<&'static str> =
6710                    ::std::vec::Vec::new();
6711                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6712                    ::std::vec::Vec::new();
6713                #( #upsert_pushes )*
6714                let query = #root::core::InsertQuery {
6715                    model: <Self as #root::core::Model>::SCHEMA,
6716                    columns: _columns,
6717                    values: _values,
6718                    returning: ::std::vec![ #( #upsert_returning ),* ],
6719                    on_conflict: ::core::option::Option::Some(#conflict_clause),
6720                };
6721                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6722                    #executor_passes_to_data_write,
6723                    &query,
6724                ).await?;
6725                let _returning_row = &_returning_row_v;
6726                #( #upsert_auto_assigns )*
6727                ::core::result::Result::Ok(())
6728            }
6729        })
6730    } else {
6731        None
6732    };
6733
6734    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6735        let pk_column_lit = pk_column.as_str();
6736        // Optional `soft_delete_on` / `restore_on` companions when the
6737        // model has a `#[rustango(soft_delete)]` column. They land
6738        // alongside the regular `delete_on` so callers have both
6739        // options — a hard delete (audit-tracked as a real DELETE) and
6740        // a logical delete (audit-tracked as an UPDATE setting the
6741        // deleted_at column to NOW()).
6742        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6743            let col_lit = col;
6744            let sd_field_ident = fields
6745                .soft_delete_field_ident
6746                .clone()
6747                .expect("soft_delete_column without ident");
6748            quote! {
6749                /// Soft-delete this row by setting its
6750                /// `#[rustango(soft_delete)]` column to `NOW()`.
6751                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6752                /// the row stays in the table; query helpers can
6753                /// filter it out by checking the column for `IS NOT
6754                /// NULL`.
6755                ///
6756                /// # Errors
6757                /// As [`Self::delete`].
6758                #[cfg(feature = "postgres")]
6759                pub async fn soft_delete_on #executor_generics (
6760                    &self,
6761                    #executor_param,
6762                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6763                #executor_where
6764                {
6765                    let _query = #root::core::UpdateQuery {
6766                        model: <Self as #root::core::Model>::SCHEMA,
6767                        set: ::std::vec![
6768                            #root::core::Assignment {
6769                                column: #col_lit,
6770                                value: ::core::convert::Into::<#root::core::Expr>::into(
6771                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6772                                        #root::__chrono::Utc::now()
6773                                    )
6774                                ),
6775                            },
6776                        ],
6777                        where_clause: #root::core::WhereExpr::Predicate(
6778                            #root::core::Filter {
6779                                column: #pk_column_lit,
6780                                op: #root::core::Op::Eq,
6781                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6782                                    ::core::clone::Clone::clone(&self.#pk_ident)
6783                                ),
6784                            }
6785                        ),
6786                    };
6787                    let _affected = #root::sql::__macro_internals::update_on(
6788                        #executor_passes_to_data_write,
6789                        &_query,
6790                    ).await?;
6791                    #audit_softdelete_emit
6792                    ::core::result::Result::Ok(_affected)
6793                }
6794
6795                /// Inverse of [`Self::soft_delete_on`] — clears the
6796                /// soft-delete column back to NULL so the row is
6797                /// considered live again.
6798                ///
6799                /// # Errors
6800                /// As [`Self::delete`].
6801                #[cfg(feature = "postgres")]
6802                pub async fn restore_on #executor_generics (
6803                    &self,
6804                    #executor_param,
6805                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6806                #executor_where
6807                {
6808                    let _query = #root::core::UpdateQuery {
6809                        model: <Self as #root::core::Model>::SCHEMA,
6810                        set: ::std::vec![
6811                            #root::core::Assignment {
6812                                column: #col_lit,
6813                                value: ::core::convert::Into::<#root::core::Expr>::into(
6814                                    #root::core::SqlValue::Null
6815                                ),
6816                            },
6817                        ],
6818                        where_clause: #root::core::WhereExpr::Predicate(
6819                            #root::core::Filter {
6820                                column: #pk_column_lit,
6821                                op: #root::core::Op::Eq,
6822                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6823                                    ::core::clone::Clone::clone(&self.#pk_ident)
6824                                ),
6825                            }
6826                        ),
6827                    };
6828                    let _affected = #root::sql::__macro_internals::update_on(
6829                        #executor_passes_to_data_write,
6830                        &_query,
6831                    ).await?;
6832                    #audit_restore_emit
6833                    ::core::result::Result::Ok(_affected)
6834                }
6835
6836                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6837                /// — takes [`#root::sql::Pool`] and dispatches per
6838                /// backend. Eloquent `Model::delete()` semantics on
6839                /// soft-delete-enabled models (closes #821 partial).
6840                ///
6841                /// Sets the `#[rustango(soft_delete)]` column to
6842                /// `NOW()` on every backend. Query helpers
6843                /// (`QuerySet::active()` / `only_trashed()`,
6844                /// `soft_delete::active_filter` /
6845                /// `compose_with_active`) filter trashed rows out by
6846                /// reading `IS NULL` on the same column.
6847                ///
6848                /// # Errors
6849                /// As [`#root::sql::update_pool`].
6850                pub async fn soft_delete(
6851                    &self,
6852                    pool: &#root::sql::Pool,
6853                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6854                    let _query = #root::core::UpdateQuery {
6855                        model: <Self as #root::core::Model>::SCHEMA,
6856                        set: ::std::vec![
6857                            #root::core::Assignment {
6858                                column: #col_lit,
6859                                value: ::core::convert::Into::<#root::core::Expr>::into(
6860                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6861                                        #root::__chrono::Utc::now()
6862                                    )
6863                                ),
6864                            },
6865                        ],
6866                        where_clause: #root::core::WhereExpr::Predicate(
6867                            #root::core::Filter {
6868                                column: #pk_column_lit,
6869                                op: #root::core::Op::Eq,
6870                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6871                                    ::core::clone::Clone::clone(&self.#pk_ident)
6872                                ),
6873                            }
6874                        ),
6875                    };
6876                    #root::sql::update_pool(pool, &_query).await
6877                }
6878
6879                /// Tri-dialect counterpart of [`Self::restore_on`].
6880                /// Clears the `#[rustango(soft_delete)]` column back
6881                /// to `NULL`, marking the row live again. Eloquent
6882                /// `Model::restore()` parity.
6883                ///
6884                /// # Errors
6885                /// As [`#root::sql::update_pool`].
6886                pub async fn restore(
6887                    &self,
6888                    pool: &#root::sql::Pool,
6889                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6890                    let _query = #root::core::UpdateQuery {
6891                        model: <Self as #root::core::Model>::SCHEMA,
6892                        set: ::std::vec![
6893                            #root::core::Assignment {
6894                                column: #col_lit,
6895                                value: ::core::convert::Into::<#root::core::Expr>::into(
6896                                    #root::core::SqlValue::Null
6897                                ),
6898                            },
6899                        ],
6900                        where_clause: #root::core::WhereExpr::Predicate(
6901                            #root::core::Filter {
6902                                column: #pk_column_lit,
6903                                op: #root::core::Op::Eq,
6904                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6905                                    ::core::clone::Clone::clone(&self.#pk_ident)
6906                                ),
6907                            }
6908                        ),
6909                    };
6910                    #root::sql::update_pool(pool, &_query).await
6911                }
6912
6913                /// Hard-delete this row, ignoring the soft-delete
6914                /// column. Eloquent `Model::forceDelete()` parity —
6915                /// the escape hatch when you need to actually purge
6916                /// data (GDPR, fixture cleanup, etc.).
6917                ///
6918                /// Equivalent to [`Self::delete_pool`] (the framework's
6919                /// non-soft delete) — exposed under the Eloquent name
6920                /// for muscle-memory + so soft-delete-enabled models
6921                /// have all three operations (soft / restore / force)
6922                /// in one place.
6923                ///
6924                /// # Errors
6925                /// As [`Self::delete_pool`].
6926                pub async fn force_delete(
6927                    &self,
6928                    pool: &#root::sql::Pool,
6929                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6930                    Self::delete_pool(self, pool).await
6931                }
6932
6933                /// Returns `true` when this row is soft-deleted (its
6934                /// `#[rustango(soft_delete)]` column is currently
6935                /// set — Eloquent `$model->trashed()` parity).
6936                ///
6937                /// Pure in-memory predicate over `&self`; does not
6938                /// hit the database. Useful in admin/template code
6939                /// like `{% if post.trashed() %}…{% endif %}` and in
6940                /// guard clauses on restore/force-delete flows.
6941                pub fn trashed(&self) -> bool {
6942                    ::core::option::Option::is_some(&self.#sd_field_ident)
6943                }
6944
6945                /// Fetch every row whose `#[rustango(soft_delete)]`
6946                /// column is `NULL` (a.k.a. the live, non-trashed
6947                /// rows). Eloquent's default `Model::all()` behavior on
6948                /// a soft-delete model (Eloquent auto-scopes trashed
6949                /// rows out; rustango doesn't have auto-scoping yet —
6950                /// see issue #820 — so this is the explicit shortcut).
6951                ///
6952                /// One-liner over `QuerySet::<Self>::default()
6953                /// .active().fetch(pool)`. Closes #821 partial.
6954                ///
6955                /// # Errors
6956                /// As [`#root::sql::FetcherPool::fetch`].
6957                pub async fn active(
6958                    pool: &#root::sql::Pool,
6959                ) -> ::core::result::Result<
6960                    ::std::vec::Vec<Self>,
6961                    #root::sql::ExecError,
6962                > {
6963                    use #root::sql::FetcherPool as _;
6964                    #root::query::QuerySet::<Self>::default()
6965                        .active()
6966                        .fetch(pool)
6967                        .await
6968                }
6969
6970                /// Fetch ONLY soft-deleted rows. Eloquent
6971                /// `Model::onlyTrashed()->get()` parity — drives the
6972                /// admin "Trash" page, restore flows, GDPR purge
6973                /// scans, etc.
6974                ///
6975                /// One-liner over `QuerySet::<Self>::default()
6976                /// .only_trashed().fetch(pool)`. Closes #821
6977                /// partial.
6978                ///
6979                /// # Errors
6980                /// As [`#root::sql::FetcherPool::fetch`].
6981                pub async fn only_trashed(
6982                    pool: &#root::sql::Pool,
6983                ) -> ::core::result::Result<
6984                    ::std::vec::Vec<Self>,
6985                    #root::sql::ExecError,
6986                > {
6987                    use #root::sql::FetcherPool as _;
6988                    #root::query::QuerySet::<Self>::default()
6989                        .only_trashed()
6990                        .fetch(pool)
6991                        .await
6992                }
6993
6994                /// Fetch every row, both live and soft-deleted.
6995                /// Eloquent `Model::withTrashed()->get()` parity.
6996                ///
6997                /// Today every queryset already includes trashed rows
6998                /// (rustango has no global-scope tracking yet — issue
6999                /// #820), so this is functionally equivalent to
7000                /// [`Self::all_pool`]. Exposed as a named shortcut so
7001                /// soft-delete-aware code reads `Model::with_trashed_pool`
7002                /// rather than `Model::all_pool` — keeps intent visible
7003                /// in callers and stays correct when auto-scoping lands.
7004                ///
7005                /// Closes #821 partial.
7006                ///
7007                /// # Errors
7008                /// As [`#root::sql::FetcherPool::fetch`].
7009                pub async fn with_trashed(
7010                    pool: &#root::sql::Pool,
7011                ) -> ::core::result::Result<
7012                    ::std::vec::Vec<Self>,
7013                    #root::sql::ExecError,
7014                > {
7015                    use #root::sql::FetcherPool as _;
7016                    #root::query::QuerySet::<Self>::default()
7017                        .with_trashed()
7018                        .fetch(pool)
7019                        .await
7020                }
7021
7022            }
7023        } else {
7024            quote!()
7025        };
7026        quote! {
7027            /// Delete the row identified by this instance's primary key.
7028            ///
7029            /// Returns the number of rows affected (0 or 1).
7030            ///
7031            /// # Errors
7032            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7033            /// driver failures.
7034            #[cfg(feature = "postgres")]
7035            pub async fn delete(
7036                &self,
7037                pool: &#root::sql::sqlx::PgPool,
7038            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7039                #pool_to_delete_on
7040            }
7041
7042            /// Like [`Self::delete`] but accepts any sqlx executor —
7043            /// for tenant-scoped deletes against an explicitly-acquired
7044            /// connection. See [`Self::save_on`] for the rationale.
7045            ///
7046            /// # Errors
7047            /// As [`Self::delete`].
7048            #[cfg(feature = "postgres")]
7049            pub async fn delete_on #executor_generics (
7050                &self,
7051                #executor_param,
7052            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7053            #executor_where
7054            {
7055                let query = #root::core::DeleteQuery {
7056                    model: <Self as #root::core::Model>::SCHEMA,
7057                    where_clause: #root::core::WhereExpr::Predicate(
7058                        #root::core::Filter {
7059                            column: #pk_column_lit,
7060                            op: #root::core::Op::Eq,
7061                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
7062                                ::core::clone::Clone::clone(&self.#pk_ident)
7063                            ),
7064                        }
7065                    ),
7066                };
7067                let _affected = #root::sql::__macro_internals::delete_on(
7068                    #executor_passes_to_data_write,
7069                    &query,
7070                ).await?;
7071                #audit_delete_emit
7072                ::core::result::Result::Ok(_affected)
7073            }
7074
7075            /// Per-call audit-source override for [`Self::delete_on`].
7076            /// See [`Self::save_on_with`] for shape rationale.
7077            ///
7078            /// # Errors
7079            /// As [`Self::delete_on`].
7080            #[cfg(feature = "postgres")]
7081            pub async fn delete_on_with #executor_generics (
7082                &self,
7083                #executor_param,
7084                source: #root::audit::AuditSource,
7085            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7086            #executor_where
7087            {
7088                #root::audit::with_source(source, self.delete_on(_executor)).await
7089            }
7090            #pool_delete_method
7091            #pool_insert_method
7092            #pool_save_method
7093            #refresh_replicate_methods
7094            #tx_delete_method
7095            #tx_insert_method
7096            #tx_save_method
7097            #soft_delete_methods
7098
7099            /// Returns `true` when `other` represents the same DB
7100            /// row as `self` — i.e. their primary keys compare
7101            /// equal. Eloquent `$model->is($other)` parity.
7102            ///
7103            /// Because both arguments are typed `&Self`, the
7104            /// model/table check is automatic — `Post::is` cannot
7105            /// be invoked against a `Comment` at compile time. Only
7106            /// the PK has to be compared at runtime.
7107            pub fn is(&self, other: &Self) -> bool {
7108                self.#pk_ident == other.#pk_ident
7109            }
7110
7111            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7112            /// parity.
7113            pub fn is_not(&self, other: &Self) -> bool {
7114                self.#pk_ident != other.#pk_ident
7115            }
7116
7117            /// Returns this row's primary-key value as an
7118            /// [`#root::core::SqlValue`]. Eloquent
7119            /// `$model->getKey()` parity.
7120            ///
7121            /// Useful when you need to thread the PK through a
7122            /// generic `Into<SqlValue>`-bound API without knowing the
7123            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7124            #[must_use]
7125            pub fn get_key(&self) -> #root::core::SqlValue {
7126                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7127            }
7128
7129        }
7130    });
7131
7132    let insert_method = if fields.has_auto {
7133        let pushes = &fields.insert_pushes;
7134        let returning_cols = &fields.returning_cols;
7135        let auto_assigns = &fields.auto_assigns;
7136        quote! {
7137            /// Insert this row into its table. Skips columns whose
7138            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7139            /// sequence fills them in, then reads each `Auto` column
7140            /// back via `RETURNING` and stores it on `self`.
7141            ///
7142            /// # Errors
7143            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7144            /// driver failures.
7145            #[cfg(feature = "postgres")]
7146            pub async fn insert(
7147                &mut self,
7148                pool: &#root::sql::sqlx::PgPool,
7149            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7150                #pool_to_insert_on
7151            }
7152
7153            /// Like [`Self::insert`] but accepts any sqlx executor.
7154            /// See [`Self::save_on`] for tenancy-scoped rationale.
7155            ///
7156            /// # Errors
7157            /// As [`Self::insert`].
7158            #[cfg(feature = "postgres")]
7159            pub async fn insert_on #executor_generics (
7160                &mut self,
7161                #executor_param,
7162            ) -> ::core::result::Result<(), #root::sql::ExecError>
7163            #executor_where
7164            {
7165                let mut _columns: ::std::vec::Vec<&'static str> =
7166                    ::std::vec::Vec::new();
7167                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7168                    ::std::vec::Vec::new();
7169                #( #pushes )*
7170                let query = #root::core::InsertQuery {
7171                    model: <Self as #root::core::Model>::SCHEMA,
7172                    columns: _columns,
7173                    values: _values,
7174                    returning: ::std::vec![ #( #returning_cols ),* ],
7175                    on_conflict: ::core::option::Option::None,
7176                };
7177                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7178                    #executor_passes_to_data_write,
7179                    &query,
7180                ).await?;
7181                let _returning_row = &_returning_row_v;
7182                #( #auto_assigns )*
7183                #audit_insert_emit
7184                ::core::result::Result::Ok(())
7185            }
7186
7187            /// Per-call audit-source override for [`Self::insert_on`].
7188            /// See [`Self::save_on_with`] for shape rationale.
7189            ///
7190            /// # Errors
7191            /// As [`Self::insert_on`].
7192            #[cfg(feature = "postgres")]
7193            pub async fn insert_on_with #executor_generics (
7194                &mut self,
7195                #executor_param,
7196                source: #root::audit::AuditSource,
7197            ) -> ::core::result::Result<(), #root::sql::ExecError>
7198            #executor_where
7199            {
7200                #root::audit::with_source(source, self.insert_on(_executor)).await
7201            }
7202        }
7203    } else {
7204        let insert_columns = &fields.insert_columns;
7205        let insert_values = &fields.insert_values;
7206        quote! {
7207            /// Insert this row into its table.
7208            ///
7209            /// # Errors
7210            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7211            /// driver failures.
7212            #[cfg(feature = "postgres")]
7213            pub async fn insert(
7214                &self,
7215                pool: &#root::sql::sqlx::PgPool,
7216            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7217                self.insert_on(pool).await
7218            }
7219
7220            /// Like [`Self::insert`] but accepts any sqlx executor.
7221            /// See [`Self::save_on`] for tenancy-scoped rationale.
7222            ///
7223            /// # Errors
7224            /// As [`Self::insert`].
7225            #[cfg(feature = "postgres")]
7226            pub async fn insert_on<'_c, _E>(
7227                &self,
7228                _executor: _E,
7229            ) -> ::core::result::Result<(), #root::sql::ExecError>
7230            where
7231                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7232            {
7233                let query = #root::core::InsertQuery {
7234                    model: <Self as #root::core::Model>::SCHEMA,
7235                    columns: ::std::vec![ #( #insert_columns ),* ],
7236                    values: ::std::vec![ #( #insert_values ),* ],
7237                    returning: ::std::vec::Vec::new(),
7238                    on_conflict: ::core::option::Option::None,
7239                };
7240                #root::sql::__macro_internals::insert_on(_executor, &query).await
7241            }
7242        }
7243    };
7244
7245    let bulk_insert_method = if fields.has_auto {
7246        let cols_no_auto = &fields.bulk_columns_no_auto;
7247        let cols_all = &fields.bulk_columns_all;
7248        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7249        let pushes_all = &fields.bulk_pushes_all;
7250        let returning_cols = &fields.returning_cols;
7251        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7252        let uniformity = &fields.bulk_auto_uniformity;
7253        let first_auto_ident = fields
7254            .first_auto_ident
7255            .as_ref()
7256            .expect("has_auto implies first_auto_ident is Some");
7257        quote! {
7258            /// Bulk-insert `rows` in a single round-trip. Every row's
7259            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7260            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7261            /// (caller-supplied values). Mixed Set/Unset is rejected
7262            /// — call `insert` per row for that case.
7263            ///
7264            /// Empty slice is a no-op. Each row's `Auto` fields are
7265            /// populated from the `RETURNING` clause in input order
7266            /// before this returns.
7267            ///
7268            /// # Errors
7269            /// Returns [`#root::sql::ExecError`] for validation,
7270            /// SQL-writing, mixed-Auto rejection, or driver failures.
7271            #[cfg(feature = "postgres")]
7272            pub async fn bulk_insert(
7273                rows: &mut [Self],
7274                pool: &#root::sql::sqlx::PgPool,
7275            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7276                #pool_to_bulk_insert_on
7277            }
7278
7279            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7280            /// See [`Self::save_on`] for tenancy-scoped rationale.
7281            ///
7282            /// # Errors
7283            /// As [`Self::bulk_insert`].
7284            #[cfg(feature = "postgres")]
7285            pub async fn bulk_insert_on #executor_generics (
7286                rows: &mut [Self],
7287                #executor_param,
7288            ) -> ::core::result::Result<(), #root::sql::ExecError>
7289            #executor_where
7290            {
7291                if rows.is_empty() {
7292                    return ::core::result::Result::Ok(());
7293                }
7294                let _first_unset = matches!(
7295                    rows[0].#first_auto_ident,
7296                    #root::sql::Auto::Unset
7297                );
7298                #( #uniformity )*
7299
7300                let mut _all_rows: ::std::vec::Vec<
7301                    ::std::vec::Vec<#root::core::SqlValue>,
7302                > = ::std::vec::Vec::with_capacity(rows.len());
7303                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7304                    for _row in rows.iter() {
7305                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7306                            ::std::vec::Vec::new();
7307                        #( #pushes_no_auto )*
7308                        _all_rows.push(_row_vals);
7309                    }
7310                    ::std::vec![ #( #cols_no_auto ),* ]
7311                } else {
7312                    for _row in rows.iter() {
7313                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7314                            ::std::vec::Vec::new();
7315                        #( #pushes_all )*
7316                        _all_rows.push(_row_vals);
7317                    }
7318                    ::std::vec![ #( #cols_all ),* ]
7319                };
7320
7321                let _query = #root::core::BulkInsertQuery {
7322                    model: <Self as #root::core::Model>::SCHEMA,
7323                    columns: _columns,
7324                    rows: _all_rows,
7325                    returning: ::std::vec![ #( #returning_cols ),* ],
7326                    on_conflict: ::core::option::Option::None,
7327                };
7328                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7329                    #executor_passes_to_data_write,
7330                    &_query,
7331                ).await?;
7332                if _returned.len() != rows.len() {
7333                    return ::core::result::Result::Err(
7334                        #root::sql::ExecError::Sql(
7335                            #root::sql::SqlError::BulkInsertReturningMismatch {
7336                                expected: rows.len(),
7337                                actual: _returned.len(),
7338                            }
7339                        )
7340                    );
7341                }
7342                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7343                    #auto_assigns_for_row
7344                }
7345                #audit_bulk_insert_emit
7346                ::core::result::Result::Ok(())
7347            }
7348        }
7349    } else {
7350        let cols_all = &fields.bulk_columns_all;
7351        let pushes_all = &fields.bulk_pushes_all;
7352        quote! {
7353            /// Bulk-insert `rows` in a single round-trip. Every row's
7354            /// fields are written verbatim — there are no `Auto<T>`
7355            /// fields on this model.
7356            ///
7357            /// Empty slice is a no-op.
7358            ///
7359            /// # Errors
7360            /// Returns [`#root::sql::ExecError`] for validation,
7361            /// SQL-writing, or driver failures.
7362            #[cfg(feature = "postgres")]
7363            pub async fn bulk_insert(
7364                rows: &[Self],
7365                pool: &#root::sql::sqlx::PgPool,
7366            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7367                Self::bulk_insert_on(rows, pool).await
7368            }
7369
7370            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7371            /// See [`Self::save_on`] for tenancy-scoped rationale.
7372            ///
7373            /// # Errors
7374            /// As [`Self::bulk_insert`].
7375            #[cfg(feature = "postgres")]
7376            pub async fn bulk_insert_on<'_c, _E>(
7377                rows: &[Self],
7378                _executor: _E,
7379            ) -> ::core::result::Result<(), #root::sql::ExecError>
7380            where
7381                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7382            {
7383                if rows.is_empty() {
7384                    return ::core::result::Result::Ok(());
7385                }
7386                let mut _all_rows: ::std::vec::Vec<
7387                    ::std::vec::Vec<#root::core::SqlValue>,
7388                > = ::std::vec::Vec::with_capacity(rows.len());
7389                for _row in rows.iter() {
7390                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7391                        ::std::vec::Vec::new();
7392                    #( #pushes_all )*
7393                    _all_rows.push(_row_vals);
7394                }
7395                let _query = #root::core::BulkInsertQuery {
7396                    model: <Self as #root::core::Model>::SCHEMA,
7397                    columns: ::std::vec![ #( #cols_all ),* ],
7398                    rows: _all_rows,
7399                    returning: ::std::vec::Vec::new(),
7400                    on_conflict: ::core::option::Option::None,
7401                };
7402                let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7403                ::core::result::Result::Ok(())
7404            }
7405        }
7406    };
7407
7408    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7409    // (no postgres-feature gate); routes through the existing
7410    // `bulk_insert_pool` + per-dialect conflict writer.
7411    //
7412    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7413    // sequence picks the PK for fresh inserts; the UPDATE branch never
7414    // touches the Auto column.
7415    let bulk_upsert_pool_method = {
7416        // Pick the "no Auto" columns when the model has Auto fields,
7417        // else every column.
7418        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7419            (
7420                fields.bulk_columns_no_auto.clone(),
7421                fields.bulk_pushes_no_auto.clone(),
7422            )
7423        } else {
7424            (
7425                fields.bulk_columns_all.clone(),
7426                fields.bulk_pushes_all.clone(),
7427            )
7428        };
7429        quote! {
7430            /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7431            /// canonical "import a batch idempotently" shape. Issue #267
7432            /// / T1.5.
7433            ///
7434            /// Per-row values are extracted and lowered into a
7435            /// [`#root::core::BulkInsertQuery`] with
7436            /// `on_conflict = DoUpdate { target, update_columns }`. The
7437            /// writer dispatches per-dialect:
7438            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7439            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7440            ///
7441            /// `target` names the column(s) whose unique constraint
7442            /// defines the conflict (typically a `unique` or
7443            /// `unique_together` natural-key column, NOT the `Auto<T>`
7444            /// PK). `update_cols` names the columns to overwrite on
7445            /// conflict — every other column is left untouched on the
7446            /// existing row.
7447            ///
7448            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7449            /// picks the PK on insert; the update path never touches
7450            /// the Auto column). Auto-set rows trigger a hard error.
7451            /// Empty slice is a no-op.
7452            ///
7453            /// # Errors
7454            /// Returns [`#root::sql::ExecError`] for validation,
7455            /// SQL-writing, or driver failures.
7456            pub async fn bulk_upsert_pool(
7457                rows: &[Self],
7458                target: &[&'static str],
7459                update_cols: &[&'static str],
7460                pool: &#root::sql::Pool,
7461            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7462                if rows.is_empty() {
7463                    return ::core::result::Result::Ok(());
7464                }
7465                let mut _all_rows: ::std::vec::Vec<
7466                    ::std::vec::Vec<#root::core::SqlValue>,
7467                > = ::std::vec::Vec::with_capacity(rows.len());
7468                for _row in rows.iter() {
7469                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7470                        ::std::vec::Vec::new();
7471                    #( #upsert_pushes )*
7472                    _all_rows.push(_row_vals);
7473                }
7474                let _query = #root::core::BulkInsertQuery {
7475                    model: <Self as #root::core::Model>::SCHEMA,
7476                    columns: ::std::vec![ #( #upsert_cols ),* ],
7477                    rows: _all_rows,
7478                    returning: ::std::vec::Vec::new(),
7479                    on_conflict: ::core::option::Option::Some(
7480                        #root::core::ConflictClause::DoUpdate {
7481                            target: target.to_vec(),
7482                            update_columns: update_cols.to_vec(),
7483                        }
7484                    ),
7485                };
7486                #root::sql::bulk_insert_pool(pool, &_query).await
7487            }
7488
7489            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7490            /// skip rows that would violate a unique constraint. Issue
7491            /// #267 / T1.5. Same per-dialect dispatch as
7492            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7493            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7494            /// <pivot> = <pivot>` (MySQL no-op write).
7495            ///
7496            /// # Errors
7497            /// As [`Self::bulk_upsert_pool`].
7498            pub async fn bulk_insert_or_ignore_pool(
7499                rows: &[Self],
7500                pool: &#root::sql::Pool,
7501            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7502                if rows.is_empty() {
7503                    return ::core::result::Result::Ok(());
7504                }
7505                let mut _all_rows: ::std::vec::Vec<
7506                    ::std::vec::Vec<#root::core::SqlValue>,
7507                > = ::std::vec::Vec::with_capacity(rows.len());
7508                for _row in rows.iter() {
7509                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7510                        ::std::vec::Vec::new();
7511                    #( #upsert_pushes )*
7512                    _all_rows.push(_row_vals);
7513                }
7514                let _query = #root::core::BulkInsertQuery {
7515                    model: <Self as #root::core::Model>::SCHEMA,
7516                    columns: ::std::vec![ #( #upsert_cols ),* ],
7517                    rows: _all_rows,
7518                    returning: ::std::vec::Vec::new(),
7519                    on_conflict: ::core::option::Option::Some(
7520                        #root::core::ConflictClause::DoNothing
7521                    ),
7522                };
7523                #root::sql::bulk_insert_pool(pool, &_query).await
7524            }
7525        }
7526    };
7527
7528    // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7529    // `QuerySet.bulk_update`. The SQL/IR/executor stack
7530    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7531    // `write_bulk_update_*` writers) already existed; what was missing
7532    // was the per-model constructor that maps `&[Self]` + a runtime
7533    // column list into rows of `[pk, col_vals…]` so callers don't
7534    // hand-build the IR. Emitted only when the model has a primary key
7535    // (the PK is the join key and can't itself be updated).
7536    let bulk_update_method = match &fields.primary_key {
7537        None => quote! {},
7538        Some((pk_ident, pk_col)) => {
7539            // One pair of match arms per non-PK column: a validation arm
7540            // resolving the runtime name to its `&'static str` column,
7541            // and a value arm pushing that field off the row.
7542            let mut col_arms: Vec<TokenStream2> = Vec::new();
7543            let mut val_arms: Vec<TokenStream2> = Vec::new();
7544            for entry in &fields.column_entries {
7545                if &entry.column == pk_col {
7546                    continue;
7547                }
7548                let col = &entry.column;
7549                let ident = &entry.ident;
7550                col_arms.push(quote! { #col => #col, });
7551                val_arms.push(quote! {
7552                    #col => _row_vals.push(
7553                        ::core::convert::Into::<#root::core::SqlValue>::into(
7554                            ::core::clone::Clone::clone(&_o.#ident)
7555                        )
7556                    ),
7557                });
7558            }
7559            quote! {
7560                /// Django's `QuerySet.bulk_update(objs, fields)` — write
7561                /// per-row-different values for the named `fields` across
7562                /// every object in `objs` in a single statement, matched
7563                /// by primary key.
7564                ///
7565                /// `fields` names the **columns** to update. The primary
7566                /// key identifies each row and cannot itself be updated
7567                /// (pass it and you get
7568                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7569                /// Empty `objs` or `fields` is a no-op returning `0`.
7570                /// Objects whose PK matches no row are simply not updated.
7571                /// Returns the number of rows affected.
7572                ///
7573                /// Tri-dialect: lowers to one
7574                /// [`#root::core::BulkUpdateQuery`] and dispatches
7575                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7576                /// a CTE + correlated subquery on SQLite, an inner
7577                /// `JOIN (VALUES …)` on MySQL.
7578                ///
7579                /// # Errors
7580                /// [`#root::core::QueryError::UnknownField`] for a name
7581                /// that isn't a column on this model,
7582                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7583                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7584                /// SQL-writing / driver failures.
7585                pub async fn bulk_update(
7586                    objs: &[Self],
7587                    fields: &[&str],
7588                    pool: &#root::sql::Pool,
7589                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7590                    if objs.is_empty() || fields.is_empty() {
7591                        return ::core::result::Result::Ok(0);
7592                    }
7593                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7594                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7595                        ::std::vec::Vec::with_capacity(fields.len());
7596                    for &_f in fields {
7597                        let _col: &'static str = match _f {
7598                            #pk_col => {
7599                                return ::core::result::Result::Err(
7600                                    ::core::convert::Into::into(
7601                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7602                                            model: _model_name,
7603                                            field: ::std::string::ToString::to_string(_f),
7604                                        }
7605                                    )
7606                                );
7607                            }
7608                            #( #col_arms )*
7609                            _ => {
7610                                return ::core::result::Result::Err(
7611                                    ::core::convert::Into::into(
7612                                        #root::core::QueryError::UnknownField {
7613                                            model: _model_name,
7614                                            field: ::std::string::ToString::to_string(_f),
7615                                        }
7616                                    )
7617                                );
7618                            }
7619                        };
7620                        _update_columns.push(_col);
7621                    }
7622                    let mut _rows: ::std::vec::Vec<
7623                        ::std::vec::Vec<#root::core::SqlValue>,
7624                    > = ::std::vec::Vec::with_capacity(objs.len());
7625                    for _o in objs.iter() {
7626                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7627                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7628                        // PK first — the writers expect `[pk, …update cols]`.
7629                        _row_vals.push(
7630                            ::core::convert::Into::<#root::core::SqlValue>::into(
7631                                ::core::clone::Clone::clone(&_o.#pk_ident)
7632                            )
7633                        );
7634                        for &_f in fields {
7635                            match _f {
7636                                #( #val_arms )*
7637                                // Unreachable: every name was validated
7638                                // against the same arm set above.
7639                                _ => {}
7640                            }
7641                        }
7642                        _rows.push(_row_vals);
7643                    }
7644                    let _query = #root::core::BulkUpdateQuery {
7645                        model: <Self as #root::core::Model>::SCHEMA,
7646                        update_columns: _update_columns,
7647                        rows: _rows,
7648                    };
7649                    #root::sql::bulk_update_pool(pool, &_query).await
7650                }
7651            }
7652        }
7653    };
7654
7655    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7656        quote! {
7657            /// Hidden runtime accessor for the primary-key value as a
7658            /// [`SqlValue`]. Used by reverse-relation helpers
7659            /// (`<parent>::<child>_set`) emitted from sibling models'
7660            /// FK fields. Not part of the public API.
7661            #[doc(hidden)]
7662            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7663                ::core::convert::Into::<#root::core::SqlValue>::into(
7664                    ::core::clone::Clone::clone(&self.#pk_ident)
7665                )
7666            }
7667        }
7668    });
7669
7670    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7671        quote! {
7672            impl #root::sql::HasPkValue for #struct_name {
7673                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7674                    ::core::convert::Into::<#root::core::SqlValue>::into(
7675                        ::core::clone::Clone::clone(&self.#pk_ident)
7676                    )
7677                }
7678            }
7679        }
7680    });
7681
7682    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7683
7684    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7685    // dispatch to the right per-backend body without the macro emitting
7686    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7687    // so audited models with non-Auto PKs (which still go through
7688    // `insert_one_with_audit` → `apply_auto_pk`) link.
7689    let assign_auto_pk_pool_impl = {
7690        let auto_assigns = &fields.auto_assigns;
7691        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7692        // so the body is structurally identical to `auto_assigns` —
7693        // only the helper name swaps from `try_get_returning` to
7694        // `try_get_returning_sqlite` so the closure typechecks against
7695        // a `SqliteRow` instead of a `PgRow`.
7696        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7697            .auto_field_idents
7698            .iter()
7699            .map(|(ident, column)| {
7700                quote! {
7701                    self.#ident = #root::sql::try_get_returning_sqlite(
7702                        _returning_row, #column
7703                    )?;
7704                }
7705            })
7706            .collect();
7707        // #1028 — decode each `generated_as` column from the same
7708        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7709        // own type drives the decode (no `Auto<T>` wrapper).
7710        let generated_assigns: Vec<TokenStream2> = fields
7711            .generated_field_idents
7712            .iter()
7713            .map(|(ident, column)| {
7714                quote! {
7715                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7716                }
7717            })
7718            .collect();
7719        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7720            .generated_field_idents
7721            .iter()
7722            .map(|(ident, column)| {
7723                quote! {
7724                    self.#ident = #root::sql::try_get_returning_sqlite(
7725                        _returning_row, #column
7726                    )?;
7727                }
7728            })
7729            .collect();
7730        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7731            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7732            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7733            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7734            // those models can't use AUTO_INCREMENT). The trait is only
7735            // touched on the MySQL arm at runtime, so PG-only consumers
7736            // never see the bound failure.
7737            //
7738            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7739            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7740            // runtime with "multi-column RETURNING". MySQL has no
7741            // multi-column RETURNING semantic and a follow-up SELECT
7742            // would need cross-trait plumbing. Pragmatic shape: succeed
7743            // with the FIRST Auto field populated from LAST_INSERT_ID();
7744            // any other Auto fields stay `Auto::Unset`. Callers that
7745            // need the DB-defaulted timestamp / UUID can re-fetch the
7746            // row by PK after `save_pool`. Fixes the cookbook chapter
7747            // 12 dialect divergence.
7748            let value_ty = fields
7749                .first_auto_value_ty
7750                .as_ref()
7751                .expect("first_auto_value_ty set whenever first_auto_ident is");
7752            quote! {
7753                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7754                    ::rustango_from_mysql_auto_id(_id)?;
7755                self.#first = #root::sql::Auto::Set(_converted);
7756                ::core::result::Result::Ok(())
7757            }
7758        } else {
7759            quote! {
7760                let _ = _id;
7761                ::core::result::Result::Ok(())
7762            }
7763        };
7764        quote! {
7765            impl #root::sql::AssignAutoPkPool for #struct_name {
7766                fn __rustango_assign_from_pg_row(
7767                    &mut self,
7768                    _returning_row: &#root::sql::PgReturningRow,
7769                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7770                    #( #auto_assigns )*
7771                    #( #generated_assigns )*
7772                    ::core::result::Result::Ok(())
7773                }
7774                fn __rustango_assign_from_mysql_id(
7775                    &mut self,
7776                    _id: i64,
7777                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7778                    #mysql_body
7779                }
7780                fn __rustango_assign_from_sqlite_row(
7781                    &mut self,
7782                    _returning_row: &#root::sql::SqliteReturningRow,
7783                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7784                    #( #auto_assigns_sqlite )*
7785                    #( #generated_assigns_sqlite )*
7786                    ::core::result::Result::Ok(())
7787                }
7788            }
7789        }
7790    };
7791
7792    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7793    let aliased_row_helper = quote! {
7794        /// Decode a row's aliased target columns (produced by
7795        /// `select_related`'s LEFT JOIN) into a fresh instance of
7796        /// this model. Reads each column via
7797        /// `format!("{prefix}__{col}")`, matching the alias the
7798        /// SELECT writer emitted. Slice 9.0d.
7799        #[doc(hidden)]
7800        #[cfg(feature = "postgres")]
7801        pub fn __rustango_from_aliased_row(
7802            row: &#root::sql::sqlx::postgres::PgRow,
7803            prefix: &str,
7804        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7805            ::core::result::Result::Ok(Self {
7806                #( #from_aliased_row_inits ),*
7807            })
7808        }
7809    };
7810    // v0.23.0-batch8 — MySQL counterpart, gated through the
7811    // cfg-aware macro_rules so PG-only builds expand to nothing.
7812    let aliased_row_helper_my = quote! {
7813        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7814            #( #from_aliased_row_inits ),*
7815        });
7816    };
7817
7818    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7819    // pattern + cfg gate on the `sqlite` feature.
7820    let aliased_row_helper_sqlite = quote! {
7821        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7822            #( #from_aliased_row_inits ),*
7823        });
7824    };
7825
7826    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7827    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7828    let load_related_impl_sqlite =
7829        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7830
7831    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7832    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7833    // default `Self::objects()`. Each accessor returns a fresh
7834    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7835    // methods the user defined.
7836    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7837        .iter()
7838        .map(|fn_ident| {
7839            let model_name_str = struct_name.to_string();
7840            let fn_name_str = fn_ident.to_string();
7841            let doc = format!(
7842                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7843                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7844                 equivalent to `Self::objects()`. Chains with any \
7845                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7846                 extension methods."
7847            );
7848            quote! {
7849                #[doc = #doc]
7850                #[must_use]
7851                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7852                    #root::query::QuerySet::new()
7853                }
7854            }
7855        })
7856        .collect();
7857
7858    quote! {
7859        impl #struct_name {
7860            /// Start a new `QuerySet` over this model. Django shape.
7861            #[must_use]
7862            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7863                #root::query::QuerySet::new()
7864            }
7865
7866            /// Eloquent-shape alias of [`Self::objects`]. Returns
7867            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7868            /// `.where_()` / etc. Matches Laravel muscle-memory:
7869            ///
7870            /// ```ignore
7871            /// // Eloquent:    Post::query()->where('published', true)
7872            /// // Django:      Post.objects.filter(published=True)
7873            /// // rustango:    Post::query().filter("published", true)
7874            /// //         or:  Post::objects().filter("published", true)
7875            /// ```
7876            ///
7877            /// Both names point at the same underlying constructor;
7878            /// neither is preferred.
7879            #[must_use]
7880            pub fn query() -> #root::query::QuerySet<#struct_name> {
7881                #root::query::QuerySet::new()
7882            }
7883
7884            #( #extra_manager_fns )*
7885
7886            #insert_method
7887
7888            #bulk_insert_method
7889
7890            #bulk_upsert_pool_method
7891
7892            #bulk_update_method
7893
7894            #save_method
7895
7896            #pk_methods
7897
7898            #pk_value_helper
7899
7900            #aliased_row_helper
7901
7902            #column_consts
7903        }
7904
7905        #aliased_row_helper_my
7906
7907        #aliased_row_helper_sqlite
7908
7909        #load_related_impl
7910
7911        #load_related_impl_my
7912
7913        #load_related_impl_sqlite
7914
7915        #has_pk_value_impl
7916
7917        #fk_pk_access_impl
7918
7919        #assign_auto_pk_pool_impl
7920    }
7921}
7922
7923/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7924/// `auto_assigns` but reading from `_returning_row` and writing to
7925/// `_row_mut` instead of `self`.
7926fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7927    let root = rustango_root();
7928    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7929        let col_lit = column.as_str();
7930        quote! {
7931            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7932                _returning_row,
7933                #col_lit,
7934            )?;
7935        }
7936    });
7937    quote! { #( #lines )* }
7938}
7939
7940/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7941fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7942    let lines = entries.iter().map(|e| {
7943        let ident = &e.ident;
7944        let col_ty = column_type_ident(ident);
7945        quote! {
7946            #[allow(non_upper_case_globals)]
7947            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7948        }
7949    });
7950    quote! { #(#lines)* }
7951}
7952
7953/// Emit a hidden per-model module carrying one zero-sized type per field,
7954/// each with a `Column` impl pointing back at the model.
7955fn column_module_tokens(
7956    module_ident: &syn::Ident,
7957    struct_name: &syn::Ident,
7958    entries: &[ColumnEntry],
7959) -> TokenStream2 {
7960    let root = rustango_root();
7961    let items = entries.iter().map(|e| {
7962        let col_ty = column_type_ident(&e.ident);
7963        let value_ty = &e.value_ty;
7964        let name = &e.name;
7965        let column = &e.column;
7966        let field_type_tokens = &e.field_type_tokens;
7967        quote! {
7968            #[derive(::core::clone::Clone, ::core::marker::Copy)]
7969            pub struct #col_ty;
7970
7971            impl #root::core::Column for #col_ty {
7972                type Model = super::#struct_name;
7973                type Value = #value_ty;
7974                const NAME: &'static str = #name;
7975                const COLUMN: &'static str = #column;
7976                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7977            }
7978        }
7979    });
7980    quote! {
7981        #[doc(hidden)]
7982        #[allow(non_camel_case_types, non_snake_case)]
7983        pub mod #module_ident {
7984            // Re-import the parent scope so field types referencing
7985            // sibling models (e.g. `ForeignKey<Author>`) resolve
7986            // inside this submodule. Without this we'd hit
7987            // `proc_macro_derive_resolution_fallback` warnings.
7988            #[allow(unused_imports)]
7989            use super::*;
7990            #(#items)*
7991        }
7992    }
7993}
7994
7995fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7996    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7997}
7998
7999fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
8000    syn::Ident::new(
8001        &format!("__rustango_cols_{struct_name}"),
8002        struct_name.span(),
8003    )
8004}
8005
8006fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
8007    let root = rustango_root();
8008    // The Postgres impl is always emitted — every rustango build pulls in
8009    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8010    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8011    // macro_rules whose body collapses to nothing when rustango is built
8012    // without the `mysql` feature. No user-facing feature shim required.
8013    //
8014    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8015    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8016    // back into a comma-separated list inside square brackets. Since each
8017    // entry is already in `field: expr` shape, the existing tokens slot in.
8018    quote! {
8019        #[cfg(feature = "postgres")]
8020        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8021            for #struct_name
8022        {
8023            fn from_row(
8024                row: &'r #root::sql::sqlx::postgres::PgRow,
8025            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8026                ::core::result::Result::Ok(Self {
8027                    #( #from_row_inits ),*
8028                })
8029            }
8030        }
8031
8032        #root::__impl_my_from_row!(#struct_name, |row| {
8033            #( #from_row_inits ),*
8034        });
8035
8036        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8037            #( #from_row_inits ),*
8038        });
8039    }
8040}
8041
8042struct ContainerAttrs {
8043    table: Option<String>,
8044    display: Option<(String, proc_macro2::Span)>,
8045    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8046    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8047    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8048    /// at runtime — this attribute is the override for cases where
8049    /// the inference is wrong (e.g. a model that conceptually belongs
8050    /// to one app but is physically in another module).
8051    app: Option<String>,
8052    /// Django ModelAdmin-shape per-model knobs from
8053    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8054    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8055    /// admin code falls back to `AdminConfig::DEFAULT`.
8056    admin: Option<AdminAttrs>,
8057    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8058    /// `None` when the model isn't audited — write paths emit no
8059    /// audit entries. When present, single-row writes capture
8060    /// before/after for the listed fields and bulk writes batch
8061    /// snapshots into one INSERT into `rustango_audit_log`.
8062    audit: Option<AuditAttrs>,
8063    /// `true` when `#[rustango(permissions)]` is present. Signals that
8064    /// `auto_create_permissions` should seed the four CRUD codenames for
8065    /// this model.
8066    permissions: bool,
8067    /// Many-to-many relations declared via
8068    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8069    ///                 src = "post_id", dst = "tag_id"))]`.
8070    m2m: Vec<M2MAttr>,
8071    /// Polymorphic M2M relations declared via
8072    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8073    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8074    ///   related_column = "tag_id"))]` (issue #818).
8075    generic_m2m: Vec<GenericM2MAttr>,
8076    /// Composite indexes declared via
8077    /// `#[rustango(index("col1, col2"))]` or
8078    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8079    /// Single-column indexes from `#[rustango(index)]` on fields are
8080    /// accumulated here during field collection.
8081    indexes: Vec<IndexAttr>,
8082    /// Table-level CHECK constraints declared via
8083    /// `#[rustango(check(name = "…", expr = "…"))]`.
8084    checks: Vec<CheckAttr>,
8085    /// Table-level PG `EXCLUDE` constraints declared via
8086    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8087    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8088    /// migration writer renders them on Postgres and skips with a
8089    /// warning on MySQL/SQLite. Issue #319.
8090    excludes: Vec<ExcludeAttr>,
8091    /// Composite (multi-column) FKs declared via
8092    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8093    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8094    composite_fks: Vec<CompositeFkAttr>,
8095    /// Generic ("any model") FKs declared via
8096    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8097    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8098    generic_fks: Vec<GenericFkAttr>,
8099    /// Where this model lives in a tenancy deployment, declared via
8100    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8101    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8102    /// to partition diff output between registry-scoped and
8103    /// tenant-scoped migration files.
8104    scope: Option<String>,
8105    /// Custom-Manager extension-trait name from
8106    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8107    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8108    /// adjacent to the model so users can write
8109    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8110    /// and discover the convention from the model definition.
8111    manager_ext: Option<syn::Ident>,
8112    /// Extra QuerySet accessor names from
8113    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8114    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8115    /// the default `Self::objects()`. Multiple attributes allowed.
8116    manager_fns: Vec<syn::Ident>,
8117    /// Default ordering declared via `#[rustango(default_order =
8118    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8119    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8120    /// `-` prefix means descending; the `+` prefix or no prefix means
8121    /// ascending.
8122    default_order: Vec<(String, bool, proc_macro2::Span)>,
8123    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8124    /// Routes the emitted schema's `is_view = true` so the migration
8125    /// snapshot skips this model (its underlying SQL view is operator-
8126    /// managed, not rustango-managed).
8127    is_view: bool,
8128    /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8129    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8130    /// the migration snapshot skips this model so `makemigrations` /
8131    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8132    /// TABLE` against it (operator-managed schema).
8133    managed: bool,
8134    /// Django-shape `Meta.base_manager_name` from
8135    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8136    /// `ModelSchema::base_manager_name`. Declarative-only today.
8137    base_manager_name: Option<String>,
8138    /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8139    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8140    /// field this model's instances are ordered relative to.
8141    /// Declarative-only today; threaded onto
8142    /// `ModelSchema::order_with_respect_to`.
8143    order_with_respect_to: Option<String>,
8144    /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8145    /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8146    /// shares its DB table with another struct. Threaded into
8147    /// `ModelSchema::proxy` so future codegen can skip table-owning
8148    /// behavior for proxies.
8149    proxy: bool,
8150    /// Django-shape `Meta.required_db_features` from
8151    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8152    /// Each comma-separated capability token surfaces on
8153    /// `ModelSchema::required_db_features` so `manage check --deploy`
8154    /// can warn when the active dialect lacks one.
8155    required_db_features: Vec<String>,
8156    /// Django-shape `Meta.required_db_vendor` from
8157    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8158    /// Normalized to the dialect name `manage check --deploy`
8159    /// compares against `Settings.database.backend`. Aliases
8160    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8161    /// stored under the canonical name.
8162    required_db_vendor: Option<String>,
8163    /// Django-shape `Meta.default_related_name` from
8164    /// `#[rustango(default_related_name = "...")]`. Threaded into
8165    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8166    /// name to use when an FK / M2M field doesn't override it.
8167    /// Today rustango doesn't auto-emit reverse managers; the
8168    /// metadata is the foundation for that work.
8169    default_related_name: Option<String>,
8170    /// Django-shape `Meta.db_table_comment` (4.2+) from
8171    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8172    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8173    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8174    /// inline `COMMENT='...'`, SQLite: no-op).
8175    db_table_comment: Option<String>,
8176    /// Django-shape `Meta.get_latest_by` from
8177    /// `#[rustango(get_latest_by = "created_at")]` /
8178    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8179    /// `(column, descending)` where `descending = true` when the
8180    /// attribute value starts with `-`. Threaded into
8181    /// `ModelSchema::get_latest_by`.
8182    get_latest_by: Option<(String, bool)>,
8183    /// Django-shape `Meta.permissions = [(codename, name), ...]`
8184    /// from `#[rustango(extra_permissions = "approve:Can approve,
8185    /// archive:Can archive")]`. Comma-separated `codename:label`
8186    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8187    extra_permissions: Vec<(String, String)>,
8188    /// Django-shape `Meta.default_permissions` — which CRUD codenames
8189    /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8190    /// auto-creates. Empty `Vec` (default) means **all four** — matches
8191    /// Django's behavior when the operator omits the option. Set via
8192    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8193    /// Validated at parse time; unknown actions fail with a span-pointing
8194    /// error.
8195    default_permissions: Vec<String>,
8196    /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8197    /// human-readable singular label for the model. Threaded into
8198    /// `ModelSchema::verbose_name` so admin section headers /
8199    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8200    /// over the Rust struct identifier.
8201    verbose_name: Option<String>,
8202    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8203    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8204    /// When unset, `display_label_plural()` auto-suffixes `s`.
8205    verbose_name_plural: Option<String>,
8206    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8207    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8208    /// a name (used by `QuerySet::without_global_scope`) with a
8209    /// `fn() -> WhereExpr` path that the macro emits into
8210    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8211    global_scopes: Vec<GlobalScopeAttr>,
8212    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8213    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8214    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8215    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8216    /// entry emits an inherent `<name>_through(&self)` accessor that
8217    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8218    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8219    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8220    through_relations: Vec<ThroughAttr>,
8221    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8222    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8223    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8224    /// Each entry emits two associated functions on the parent —
8225    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8226    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8227    /// over a correlated subquery against the child table. Users
8228    /// drop the result into `QuerySet::where_raw(...)`.
8229    reverse_has_relations: Vec<ReverseHasAttr>,
8230    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8231    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8232    /// entry so the relation-existence family resolves polymorphic
8233    /// children by name.
8234    generic_has_relations: Vec<GenericHasAttr>,
8235}
8236
8237/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8238/// declaration. Each entry becomes one `core::GlobalScope` in the
8239/// emitted schema literal; `apply` resolves at macro-expand time
8240/// against the consumer's scope so the function must be in scope at
8241/// the use site. Issue #820.
8242struct GlobalScopeAttr {
8243    name: String,
8244    apply: syn::Path,
8245}
8246
8247/// Parsed `#[rustango(through(...))]` declaration. Issue
8248/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8249/// `hasManyThrough` / `hasOneThrough` parity.
8250///
8251/// `Country hasManyThrough Post via User` declares as:
8252///
8253/// ```ignore
8254/// #[rustango(through(
8255///     name                   = "posts",
8256///     far                    = "Post",
8257///     far_fk_column          = "author_id",
8258///     intermediate           = "User",
8259///     intermediate_fk_column = "country_id",
8260/// ))]
8261/// struct Country { ... }
8262/// ```
8263///
8264/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8265/// which returns a queryset filtered via a correlated subquery —
8266/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8267/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8268/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8269/// because the subquery lives inside a `WhereExpr::InSubquery` node
8270/// and the outer queryset's pending list stays empty.
8271///
8272/// All four required arguments use **SQL column / table names**
8273/// (not Rust field names) to sidestep the multi-hop-filter substrate
8274/// gap. Once that substrate lands, a higher-level Rust-field-name
8275/// shorthand can be added without breaking this surface.
8276struct ThroughAttr {
8277    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8278    name: String,
8279    /// Far model type identifier. `far = "Post"` → returns
8280    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8281    /// the derive expands.
8282    far: syn::Ident,
8283    /// SQL column on the far model's table that references the
8284    /// intermediate model's primary key. For `Post`'s
8285    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8286    /// default `<field>_id` convention) or whatever the user
8287    /// declared via `#[rustango(db_column = "...")]`.
8288    far_fk_column: String,
8289    /// Intermediate model type identifier. Needed to look up its
8290    /// `SCHEMA` so the subquery's `FROM` clause points at the
8291    /// intermediate table. `intermediate = "User"`.
8292    intermediate: syn::Ident,
8293    /// SQL column on the intermediate's table that references the
8294    /// source (this) model's primary key. For `User`'s
8295    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8296    intermediate_fk_column: String,
8297    /// SQL primary-key column on the intermediate's table — the
8298    /// column the subquery projects. Optional; defaults to `"id"`
8299    /// (rustango's default PK column name). Override when the
8300    /// intermediate declares a custom PK column.
8301    intermediate_pk_column: String,
8302}
8303
8304/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8305/// child_fk_column = "..."))]` declaration. Issue
8306/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8307/// `whereHas` / `whereDoesntHave` parity.
8308///
8309/// `Post hasMany Comment` declares as:
8310///
8311/// ```ignore
8312/// #[rustango(reverse_has(
8313///     name             = "comments",
8314///     child            = "Comment",
8315///     child_fk_column  = "post_id",
8316/// ))]
8317/// struct Post { ... }
8318/// ```
8319///
8320/// The macro emits two associated functions on `Post`:
8321///
8322/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8323///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8324///   <outer>.<self_pk_column>)`.
8325/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8326///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8327///
8328/// User code:
8329///
8330/// ```ignore
8331/// // Posts with at least one comment:
8332/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8333/// // Posts with no comments:
8334/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8335/// ```
8336///
8337/// As with #817, all column / table identifiers are **SQL names**
8338/// (not Rust field names) so the substrate is independent of the
8339/// outstanding multi-hop filter resolver gap. The emitted
8340/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8341/// SQL-emit time via the writer's scope stack.
8342struct ReverseHasAttr {
8343    /// Accessor name. `name = "comments"` → emits
8344    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8345    name: String,
8346    /// Child model type identifier. `child = "Comment"` — needed to
8347    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8348    /// points at the child table.
8349    child: syn::Ident,
8350    /// SQL column on the child's table that references this model's
8351    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8352    /// column is `"post_id"`.
8353    child_fk_column: String,
8354    /// SQL primary-key column on **this** model's table — the column
8355    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8356    /// (rustango's default PK column name).
8357    self_pk_column: String,
8358}
8359
8360/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8361/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8362/// relation-existence family (issue #830). The child is a polymorphic,
8363/// content-type-discriminated model; emits a `Model::
8364/// generic_reverse_relations()` entry the queryset resolves by name.
8365struct GenericHasAttr {
8366    /// Accessor name, e.g. `name = "tags"`.
8367    name: String,
8368    /// Child model type identifier (`child = "Tag"`) — looked up for its
8369    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8370    child: syn::Ident,
8371    /// Column on the child table holding the parent's content-type id.
8372    /// Optional; defaults to `"content_type_id"`.
8373    ct_column: String,
8374    /// Column on the child table holding the parent's PK value.
8375    /// Optional; defaults to `"object_pk"`.
8376    pk_column: String,
8377    /// SQL primary-key column on **this** (parent) model's table.
8378    /// Optional; defaults to `"id"`.
8379    self_pk_column: String,
8380}
8381
8382/// Parsed form of one index declaration (field-level or container-level).
8383struct IndexAttr {
8384    /// Index name; auto-derived when `None` at parse time.
8385    name: Option<String>,
8386    /// Column names in the index.
8387    columns: Vec<String>,
8388    /// `true` for `CREATE UNIQUE INDEX`.
8389    unique: bool,
8390    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8391    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8392    /// `"btree"` when the attribute is absent — the DDL writer omits
8393    /// the `USING` clause and the backend uses its own default
8394    /// (btree on every supported dialect).
8395    method: String,
8396    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8397    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8398    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8399    where_clause: Option<String>,
8400    /// Django `Index(fields=..., include=[...])` covering-index
8401    /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8402    /// default) means "no covering columns".
8403    include: Vec<String>,
8404}
8405
8406/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8407struct CheckAttr {
8408    name: String,
8409    expr: String,
8410}
8411
8412/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8413/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8414/// PG-only — surfaced on every backend in the macro emit; the migration
8415/// writer skips the op on MySQL/SQLite. Issue #319.
8416struct ExcludeAttr {
8417    /// Constraint name (free-form Rust identifier).
8418    name: String,
8419    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8420    using: String,
8421    /// Comma-separated `(column, operator)` pairs, in declaration
8422    /// order. Parsed from `"col WITH op, col WITH op"`.
8423    elements: Vec<(String, String)>,
8424    /// Optional `WHERE` predicate (raw SQL).
8425    where_clause: Option<String>,
8426}
8427
8428/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8429/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8430/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8431/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8432/// the model, not the field.
8433struct CompositeFkAttr {
8434    /// Logical relation name (free-form Rust identifier).
8435    name: String,
8436    /// SQL table name of the target.
8437    to: String,
8438    /// Source-side column names, in declaration order.
8439    from: Vec<String>,
8440    /// Target-side column names, same length / order as `from`.
8441    on: Vec<String>,
8442}
8443
8444/// Parsed form of one `#[rustango(generic_fk(name = "target",
8445/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8446/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8447/// generic ("any model") FKs live on the model, not the field.
8448struct GenericFkAttr {
8449    /// Logical relation name (free-form Rust identifier).
8450    name: String,
8451    /// Source-side column carrying the `content_type_id` value.
8452    ct_column: String,
8453    /// Source-side column carrying the target row's primary key.
8454    pk_column: String,
8455}
8456
8457/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8458struct M2MAttr {
8459    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8460    name: String,
8461    /// Target table (e.g. `"app_tags"`).
8462    to: String,
8463    /// Junction table (e.g. `"post_tags"`).
8464    through: String,
8465    /// Source FK column in the junction table (e.g. `"post_id"`).
8466    src: String,
8467    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8468    dst: String,
8469    /// Whether the migration writer should auto-create the junction
8470    /// table. Default `true`. Set `auto_create = false` (#324) when
8471    /// the operator declares the through table as its own
8472    /// `#[derive(Model)]` struct with extra columns.
8473    auto_create: bool,
8474}
8475
8476/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8477/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8478/// junction carries a ContentType discriminator so unrelated models
8479/// share one pivot + related set.
8480struct GenericM2MAttr {
8481    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8482    name: String,
8483    /// Polymorphic junction table (e.g. `"taggables"`).
8484    through: String,
8485    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8486    pk_column: String,
8487    /// Junction column holding the owning model's `content_type_id`
8488    /// discriminator (e.g. `"taggable_type"`).
8489    ct_column: String,
8490    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8491    related_column: String,
8492}
8493
8494/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8495/// "user"))]`. `track` is a comma-separated list of field names whose
8496/// before/after values land in the JSONB `changes` column. `source`
8497/// is informational only — it pins a default source when the model
8498/// is written outside any `audit::with_source(...)` scope (rare).
8499#[derive(Default)]
8500struct AuditAttrs {
8501    /// Field names to capture in the `changes` JSONB. Validated
8502    /// against declared scalar fields at compile time. Empty means
8503    /// "track every scalar field" — Django's audit-everything default.
8504    track: Option<(Vec<String>, proc_macro2::Span)>,
8505}
8506
8507/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8508/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8509/// comma-separated strings; we validate each ident against the model's
8510/// declared fields at compile time.
8511#[derive(Default)]
8512struct AdminAttrs {
8513    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8514    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8515    list_per_page: Option<usize>,
8516    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8517    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8518    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8519    /// Bulk action names. No field-validation against model fields —
8520    /// these are action handlers, not column references.
8521    actions: Option<(Vec<String>, proc_macro2::Span)>,
8522    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8523    /// sections, comma-separated fields per section, optional
8524    /// `Title:` prefix. Empty title omits the `<legend>`.
8525    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8526    /// `admin(list_display_links = "title")` — Django-shape. Names
8527    /// from `list_display` whose cells should link to detail/edit.
8528    /// Issue #350.
8529    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8530    /// `admin(search_help_text = "...")` — Django-shape. Short
8531    /// caption rendered beside the admin list view's search box.
8532    /// Issue #353.
8533    search_help_text: Option<String>,
8534    /// `admin(actions_on_top = false)` — Django-shape. Hides the
8535    /// action-bar above the table. Default `true`. Issue #354.
8536    actions_on_top: Option<bool>,
8537    /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8538    /// additional action-bar below the table. Default `false`.
8539    /// Issue #354.
8540    actions_on_bottom: Option<bool>,
8541    /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8542    /// a date / datetime field whose values render as a clickable
8543    /// year / month / day drill-down strip above the list table.
8544    /// Empty / unset disables the strip. Issue #355.
8545    date_hierarchy: Option<String>,
8546    /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8547    /// Each entry is `target:source[+source2]`; multiple entries are
8548    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8549    /// The admin change-form emits JS that slugifies the source values
8550    /// into the target field on every keystroke. Issue #356.
8551    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8552    /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8553    /// of FK fields whose change-form widget renders a lookup link
8554    /// next to the input. Issue #357.
8555    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8556    /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8557    /// Names of FK fields whose change-form widget renders an
8558    /// Ajax-driven typeahead populated from a `__autocomplete`
8559    /// endpoint on the target model. Issue #358.
8560    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8561    /// `admin(list_select_related = "all" | "none" | "author, …")`
8562    /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8563    /// policy. Default `"all"` matches rustango's join-everything
8564    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8565    list_select_related: Option<String>,
8566    /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8567    /// Django-shape. Each entry is `field_name:widget_name`; multiple
8568    /// entries comma-separated. Empty / unset → no overrides. The
8569    /// list of widget names supported is documented on
8570    /// `AdminConfig::formfield_overrides`. Issue #359.
8571    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8572}
8573
8574fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8575    let mut out = ContainerAttrs {
8576        table: None,
8577        display: None,
8578        app: None,
8579        admin: None,
8580        audit: None,
8581        // Default `permissions = true` so every `#[derive(Model)]`
8582        // gets the four CRUD codenames seeded by `auto_create_permissions`
8583        // and is visible to non-superusers in the tenant admin without
8584        // manual per-model annotation. Models that intentionally don't
8585        // want permission rows (registry-internal types, framework
8586        // tables operators shouldn't manage directly) opt out via
8587        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8588        // out-of-the-box admin invisibility regression (#62).
8589        permissions: true,
8590        m2m: Vec::new(),
8591        generic_m2m: Vec::new(),
8592        indexes: Vec::new(),
8593        checks: Vec::new(),
8594        excludes: Vec::new(),
8595        composite_fks: Vec::new(),
8596        generic_fks: Vec::new(),
8597        scope: None,
8598        manager_ext: None,
8599        manager_fns: Vec::new(),
8600        default_order: Vec::new(),
8601        is_view: false,
8602        managed: true,
8603        verbose_name: None,
8604        verbose_name_plural: None,
8605        base_manager_name: None,
8606        order_with_respect_to: None,
8607        proxy: false,
8608        required_db_features: Vec::new(),
8609        required_db_vendor: None,
8610        default_related_name: None,
8611        db_table_comment: None,
8612        get_latest_by: None,
8613        extra_permissions: Vec::new(),
8614        default_permissions: Vec::new(),
8615        global_scopes: Vec::new(),
8616        through_relations: Vec::new(),
8617        reverse_has_relations: Vec::new(),
8618        generic_has_relations: Vec::new(),
8619    };
8620    for attr in &input.attrs {
8621        if !attr.path().is_ident("rustango") {
8622            continue;
8623        }
8624        attr.parse_nested_meta(|meta| {
8625            if meta.path.is_ident("table") {
8626                let s: LitStr = meta.value()?.parse()?;
8627                let name = s.value();
8628                // v0.27.3 (#65) — macro-time guard against table names
8629                // that compile but break SQL downstream. Hyphens are
8630                // the common footgun: PostgreSQL accepts a quoted
8631                // `"intermediate-region"` in CREATE TABLE, but the
8632                // FK / index name derivation in `migrate::ddl`
8633                // emits `intermediate-region_field_fkey` unquoted,
8634                // which then fails the SQL parser. Same shape rule
8635                // as Postgres regular identifiers so the safe path
8636                // is the only path.
8637                validate_table_name(&name, s.span())?;
8638                out.table = Some(name);
8639                return Ok(());
8640            }
8641            if meta.path.is_ident("display") {
8642                let s: LitStr = meta.value()?.parse()?;
8643                out.display = Some((s.value(), s.span()));
8644                return Ok(());
8645            }
8646            if meta.path.is_ident("app") {
8647                let s: LitStr = meta.value()?.parse()?;
8648                out.app = Some(s.value());
8649                return Ok(());
8650            }
8651            if meta.path.is_ident("scope") {
8652                let s: LitStr = meta.value()?.parse()?;
8653                let val = s.value();
8654                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8655                    return Err(meta.error(format!(
8656                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8657                    )));
8658                }
8659                out.scope = Some(val);
8660                return Ok(());
8661            }
8662            if meta.path.is_ident("admin") {
8663                let mut admin = AdminAttrs::default();
8664                meta.parse_nested_meta(|inner| {
8665                    if inner.path.is_ident("list_display") {
8666                        let s: LitStr = inner.value()?.parse()?;
8667                        admin.list_display =
8668                            Some((split_field_list(&s.value()), s.span()));
8669                        return Ok(());
8670                    }
8671                    if inner.path.is_ident("search_fields") {
8672                        let s: LitStr = inner.value()?.parse()?;
8673                        admin.search_fields =
8674                            Some((split_field_list(&s.value()), s.span()));
8675                        return Ok(());
8676                    }
8677                    if inner.path.is_ident("readonly_fields") {
8678                        let s: LitStr = inner.value()?.parse()?;
8679                        admin.readonly_fields =
8680                            Some((split_field_list(&s.value()), s.span()));
8681                        return Ok(());
8682                    }
8683                    if inner.path.is_ident("list_per_page") {
8684                        let lit: syn::LitInt = inner.value()?.parse()?;
8685                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8686                        return Ok(());
8687                    }
8688                    if inner.path.is_ident("ordering") {
8689                        let s: LitStr = inner.value()?.parse()?;
8690                        admin.ordering = Some((
8691                            parse_ordering_list(&s.value()),
8692                            s.span(),
8693                        ));
8694                        return Ok(());
8695                    }
8696                    if inner.path.is_ident("list_filter") {
8697                        let s: LitStr = inner.value()?.parse()?;
8698                        admin.list_filter =
8699                            Some((split_field_list(&s.value()), s.span()));
8700                        return Ok(());
8701                    }
8702                    if inner.path.is_ident("actions") {
8703                        let s: LitStr = inner.value()?.parse()?;
8704                        admin.actions =
8705                            Some((split_field_list(&s.value()), s.span()));
8706                        return Ok(());
8707                    }
8708                    if inner.path.is_ident("fieldsets") {
8709                        let s: LitStr = inner.value()?.parse()?;
8710                        admin.fieldsets =
8711                            Some((parse_fieldset_list(&s.value()), s.span()));
8712                        return Ok(());
8713                    }
8714                    if inner.path.is_ident("list_display_links") {
8715                        let s: LitStr = inner.value()?.parse()?;
8716                        admin.list_display_links =
8717                            Some((split_field_list(&s.value()), s.span()));
8718                        return Ok(());
8719                    }
8720                    if inner.path.is_ident("search_help_text") {
8721                        let s: LitStr = inner.value()?.parse()?;
8722                        admin.search_help_text = Some(s.value());
8723                        return Ok(());
8724                    }
8725                    if inner.path.is_ident("actions_on_top") {
8726                        let lit: syn::LitBool = inner.value()?.parse()?;
8727                        admin.actions_on_top = Some(lit.value);
8728                        return Ok(());
8729                    }
8730                    if inner.path.is_ident("actions_on_bottom") {
8731                        let lit: syn::LitBool = inner.value()?.parse()?;
8732                        admin.actions_on_bottom = Some(lit.value);
8733                        return Ok(());
8734                    }
8735                    if inner.path.is_ident("date_hierarchy") {
8736                        let s: LitStr = inner.value()?.parse()?;
8737                        admin.date_hierarchy = Some(s.value());
8738                        return Ok(());
8739                    }
8740                    if inner.path.is_ident("prepopulated_fields") {
8741                        let s: LitStr = inner.value()?.parse()?;
8742                        admin.prepopulated_fields =
8743                            Some((parse_prepopulated_list(&s.value()), s.span()));
8744                        return Ok(());
8745                    }
8746                    if inner.path.is_ident("raw_id_fields") {
8747                        let s: LitStr = inner.value()?.parse()?;
8748                        admin.raw_id_fields =
8749                            Some((split_field_list(&s.value()), s.span()));
8750                        return Ok(());
8751                    }
8752                    if inner.path.is_ident("autocomplete_fields") {
8753                        let s: LitStr = inner.value()?.parse()?;
8754                        admin.autocomplete_fields =
8755                            Some((split_field_list(&s.value()), s.span()));
8756                        return Ok(());
8757                    }
8758                    if inner.path.is_ident("list_select_related") {
8759                        let s: LitStr = inner.value()?.parse()?;
8760                        admin.list_select_related = Some(s.value());
8761                        return Ok(());
8762                    }
8763                    if inner.path.is_ident("formfield_overrides") {
8764                        let s: LitStr = inner.value()?.parse()?;
8765                        admin.formfield_overrides =
8766                            Some((parse_formfield_overrides(&s.value()), s.span()));
8767                        return Ok(());
8768                    }
8769                    Err(inner.error(
8770                        "unknown admin attribute (supported: \
8771                         `list_display`, `list_display_links`, \
8772                         `search_fields`, `search_help_text`, \
8773                         `readonly_fields`, \
8774                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8775                         `actions_on_top`, `actions_on_bottom`, \
8776                         `date_hierarchy`, \
8777                         `prepopulated_fields`, \
8778                         `raw_id_fields`, \
8779                         `autocomplete_fields`, \
8780                         `list_select_related`, \
8781                         `formfield_overrides`, \
8782                         `fieldsets`)",
8783                    ))
8784                })?;
8785                out.admin = Some(admin);
8786                return Ok(());
8787            }
8788            if meta.path.is_ident("manager") {
8789                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8790                // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8791                // shape) is left as a follow-up — the issue's primary
8792                // acceptance is the `ext = ...` trait emission.
8793                meta.parse_nested_meta(|inner| {
8794                    if inner.path.is_ident("ext") {
8795                        let s: LitStr = inner.value()?.parse()?;
8796                        let name = s.value();
8797                        if name.is_empty() {
8798                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8799                        }
8800                        out.manager_ext =
8801                            Some(syn::Ident::new(&name, s.span()));
8802                        return Ok(());
8803                    }
8804                    Err(inner.error(
8805                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8806                    ))
8807                })?;
8808                return Ok(());
8809            }
8810            if meta.path.is_ident("manager_fn") {
8811                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8812                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8813                // next to the default `Self::objects()`. Multiple
8814                // attributes accumulate.
8815                let s: LitStr = meta.value()?.parse()?;
8816                let name = s.value();
8817                if name.is_empty() {
8818                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8819                }
8820                if name == "objects" {
8821                    return Err(meta.error(
8822                        "`manager_fn = \"objects\"` collides with the default \
8823                         accessor — pick a different name",
8824                    ));
8825                }
8826                let ident = syn::Ident::new(&name, s.span());
8827                if out.manager_fns.iter().any(|prev| *prev == ident) {
8828                    return Err(meta.error(format!(
8829                        "duplicate `manager_fn = \"{name}\"`"
8830                    )));
8831                }
8832                out.manager_fns.push(ident);
8833                return Ok(());
8834            }
8835            if meta.path.is_ident("default_order") {
8836                // `#[rustango(default_order = "-created_at, status")]`
8837                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8838                // means descending, `+prefix` or bare name means ascending.
8839                // Per-query opt-in via `QuerySet::with_default_order()`.
8840                let s: LitStr = meta.value()?.parse()?;
8841                let raw = s.value();
8842                let span = s.span();
8843                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8844                    Vec::new();
8845                for entry in raw.split(',') {
8846                    let trimmed = entry.trim();
8847                    if trimmed.is_empty() {
8848                        return Err(syn::Error::new(
8849                            span,
8850                            "`default_order = \"...\"` has an empty entry — \
8851                             check for a stray comma",
8852                        ));
8853                    }
8854                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8855                        (true, rest.trim().to_owned())
8856                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8857                        (false, rest.trim().to_owned())
8858                    } else {
8859                        (false, trimmed.to_owned())
8860                    };
8861                    if name.is_empty() {
8862                        return Err(syn::Error::new(
8863                            span,
8864                            "`default_order` entry has no column name after the prefix",
8865                        ));
8866                    }
8867                    if parsed.iter().any(|(n, _, _)| *n == name) {
8868                        return Err(syn::Error::new(
8869                            span,
8870                            format!("duplicate column `{name}` in `default_order`"),
8871                        ));
8872                    }
8873                    parsed.push((name, desc, span));
8874                }
8875                if parsed.is_empty() {
8876                    return Err(syn::Error::new(
8877                        span,
8878                        "`default_order = \"...\"` cannot be empty",
8879                    ));
8880                }
8881                out.default_order = parsed;
8882                return Ok(());
8883            }
8884            if meta.path.is_ident("global_scope") {
8885                // `#[rustango(global_scope(name = "active", apply =
8886                //  path::to::fn))]` — issue #820. The apply function
8887                // path resolves at macro-expand time in the consumer's
8888                // scope; the macro re-emits it verbatim into the
8889                // `ModelSchema::global_scopes` slice literal.
8890                let span = meta.path.span();
8891                let mut scope_name: Option<String> = None;
8892                let mut apply_path: Option<syn::Path> = None;
8893                meta.parse_nested_meta(|inner| {
8894                    if inner.path.is_ident("name") {
8895                        let s: LitStr = inner.value()?.parse()?;
8896                        let raw = s.value();
8897                        if raw.trim().is_empty() {
8898                            return Err(syn::Error::new(
8899                                s.span(),
8900                                "`global_scope(name = \"...\")` must not be empty",
8901                            ));
8902                        }
8903                        scope_name = Some(raw);
8904                        return Ok(());
8905                    }
8906                    if inner.path.is_ident("apply") {
8907                        let p: syn::Path = inner.value()?.parse()?;
8908                        apply_path = Some(p);
8909                        return Ok(());
8910                    }
8911                    Err(inner.error(
8912                        "unknown `global_scope` attribute (supported: \
8913                         `name`, `apply`)",
8914                    ))
8915                })?;
8916                let Some(name) = scope_name else {
8917                    return Err(syn::Error::new(
8918                        span,
8919                        "`global_scope` requires `name = \"...\"`",
8920                    ));
8921                };
8922                let Some(apply) = apply_path else {
8923                    return Err(syn::Error::new(
8924                        span,
8925                        "`global_scope` requires `apply = fn_path`",
8926                    ));
8927                };
8928                if out.global_scopes.iter().any(|s| s.name == name) {
8929                    return Err(syn::Error::new(
8930                        span,
8931                        format!(
8932                            "duplicate global scope name `{name}` — \
8933                             pick a unique identifier so \
8934                             `QuerySet::without_global_scope(\"{name}\")` \
8935                             is unambiguous"
8936                        ),
8937                    ));
8938                }
8939                out.global_scopes.push(GlobalScopeAttr { name, apply });
8940                return Ok(());
8941            }
8942            if meta.path.is_ident("through") {
8943                // `#[rustango(through(name = "posts", far = "Post",
8944                //  far_fk_column = "author_id", intermediate = "User",
8945                //  intermediate_fk_column = "country_id"
8946                //  [, intermediate_pk_column = "id"]))]` — issue #817.
8947                let span = meta.path.span();
8948                let mut accessor_name: Option<String> = None;
8949                let mut far_ident: Option<syn::Ident> = None;
8950                let mut far_fk_column: Option<String> = None;
8951                let mut intermediate_ident: Option<syn::Ident> = None;
8952                let mut intermediate_fk_column: Option<String> = None;
8953                let mut intermediate_pk_column: Option<String> = None;
8954                fn parse_nonempty_string(
8955                    inner: &syn::meta::ParseNestedMeta<'_>,
8956                    field: &str,
8957                ) -> syn::Result<String> {
8958                    let s: LitStr = inner.value()?.parse()?;
8959                    let raw = s.value();
8960                    let trimmed = raw.trim();
8961                    if trimmed.is_empty() {
8962                        return Err(syn::Error::new(
8963                            s.span(),
8964                            format!("`through({field} = \"...\")` must not be empty"),
8965                        ));
8966                    }
8967                    Ok(trimmed.to_owned())
8968                }
8969                meta.parse_nested_meta(|inner| {
8970                    if inner.path.is_ident("name") {
8971                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8972                        return Ok(());
8973                    }
8974                    if inner.path.is_ident("far") {
8975                        // Far model name accepted as a string literal
8976                        // so the attribute fits inside the existing
8977                        // `parse_nested_meta` shape; parsed back into a
8978                        // `syn::Ident` so the emitted accessor can
8979                        // reference the type directly.
8980                        let s: LitStr = inner.value()?.parse()?;
8981                        let raw = s.value();
8982                        let trimmed = raw.trim();
8983                        if trimmed.is_empty() {
8984                            return Err(syn::Error::new(
8985                                s.span(),
8986                                "`through(far = \"...\")` must not be empty",
8987                            ));
8988                        }
8989                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
8990                        return Ok(());
8991                    }
8992                    if inner.path.is_ident("far_fk_column") {
8993                        far_fk_column =
8994                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
8995                        return Ok(());
8996                    }
8997                    if inner.path.is_ident("intermediate") {
8998                        let s: LitStr = inner.value()?.parse()?;
8999                        let raw = s.value();
9000                        let trimmed = raw.trim();
9001                        if trimmed.is_empty() {
9002                            return Err(syn::Error::new(
9003                                s.span(),
9004                                "`through(intermediate = \"...\")` must not be empty",
9005                            ));
9006                        }
9007                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
9008                        return Ok(());
9009                    }
9010                    if inner.path.is_ident("intermediate_fk_column") {
9011                        intermediate_fk_column =
9012                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9013                        return Ok(());
9014                    }
9015                    if inner.path.is_ident("intermediate_pk_column") {
9016                        intermediate_pk_column =
9017                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9018                        return Ok(());
9019                    }
9020                    Err(inner.error(
9021                        "unknown `through` attribute (supported: \
9022                         `name`, `far`, `far_fk_column`, \
9023                         `intermediate`, `intermediate_fk_column`, \
9024                         `intermediate_pk_column`)",
9025                    ))
9026                })?;
9027                let Some(name) = accessor_name else {
9028                    return Err(syn::Error::new(
9029                        span,
9030                        "`through` requires `name = \"...\"`",
9031                    ));
9032                };
9033                let Some(far) = far_ident else {
9034                    return Err(syn::Error::new(
9035                        span,
9036                        "`through` requires `far = \"FarModelType\"`",
9037                    ));
9038                };
9039                let Some(far_fk_column) = far_fk_column else {
9040                    return Err(syn::Error::new(
9041                        span,
9042                        "`through` requires `far_fk_column = \"<column>\"`",
9043                    ));
9044                };
9045                let Some(intermediate) = intermediate_ident else {
9046                    return Err(syn::Error::new(
9047                        span,
9048                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9049                    ));
9050                };
9051                let Some(intermediate_fk_column) = intermediate_fk_column else {
9052                    return Err(syn::Error::new(
9053                        span,
9054                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9055                    ));
9056                };
9057                let intermediate_pk_column =
9058                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9059                if out.through_relations.iter().any(|t| t.name == name) {
9060                    return Err(syn::Error::new(
9061                        span,
9062                        format!(
9063                            "duplicate `through(name = \"{name}\")` — \
9064                             pick a unique accessor name"
9065                        ),
9066                    ));
9067                }
9068                out.through_relations.push(ThroughAttr {
9069                    name,
9070                    far,
9071                    far_fk_column,
9072                    intermediate,
9073                    intermediate_fk_column,
9074                    intermediate_pk_column,
9075                });
9076                return Ok(());
9077            }
9078            if meta.path.is_ident("reverse_has") {
9079                // `#[rustango(reverse_has(name = "comments",
9080                //  child = "Comment", child_fk_column = "post_id"
9081                //  [, self_pk_column = "id"]))]` — issue #830.
9082                let span = meta.path.span();
9083                let mut accessor_name: Option<String> = None;
9084                let mut child_ident: Option<syn::Ident> = None;
9085                let mut child_fk_column: Option<String> = None;
9086                let mut self_pk_column: Option<String> = None;
9087                meta.parse_nested_meta(|inner| {
9088                    if inner.path.is_ident("name") {
9089                        let s: LitStr = inner.value()?.parse()?;
9090                        let raw = s.value();
9091                        if raw.trim().is_empty() {
9092                            return Err(syn::Error::new(
9093                                s.span(),
9094                                "`reverse_has(name = \"...\")` must not be empty",
9095                            ));
9096                        }
9097                        accessor_name = Some(raw);
9098                        return Ok(());
9099                    }
9100                    if inner.path.is_ident("child") {
9101                        let s: LitStr = inner.value()?.parse()?;
9102                        let raw = s.value();
9103                        let trimmed = raw.trim();
9104                        if trimmed.is_empty() {
9105                            return Err(syn::Error::new(
9106                                s.span(),
9107                                "`reverse_has(child = \"...\")` must not be empty",
9108                            ));
9109                        }
9110                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9111                        return Ok(());
9112                    }
9113                    if inner.path.is_ident("child_fk_column") {
9114                        let s: LitStr = inner.value()?.parse()?;
9115                        let raw = s.value();
9116                        let trimmed = raw.trim();
9117                        if trimmed.is_empty() {
9118                            return Err(syn::Error::new(
9119                                s.span(),
9120                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9121                            ));
9122                        }
9123                        child_fk_column = Some(trimmed.to_owned());
9124                        return Ok(());
9125                    }
9126                    if inner.path.is_ident("self_pk_column") {
9127                        let s: LitStr = inner.value()?.parse()?;
9128                        let raw = s.value();
9129                        let trimmed = raw.trim();
9130                        if trimmed.is_empty() {
9131                            return Err(syn::Error::new(
9132                                s.span(),
9133                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9134                            ));
9135                        }
9136                        self_pk_column = Some(trimmed.to_owned());
9137                        return Ok(());
9138                    }
9139                    Err(inner.error(
9140                        "unknown `reverse_has` attribute (supported: \
9141                         `name`, `child`, `child_fk_column`, \
9142                         `self_pk_column`)",
9143                    ))
9144                })?;
9145                let Some(name) = accessor_name else {
9146                    return Err(syn::Error::new(
9147                        span,
9148                        "`reverse_has` requires `name = \"...\"`",
9149                    ));
9150                };
9151                let Some(child) = child_ident else {
9152                    return Err(syn::Error::new(
9153                        span,
9154                        "`reverse_has` requires `child = \"ChildModelType\"`",
9155                    ));
9156                };
9157                let Some(child_fk_column) = child_fk_column else {
9158                    return Err(syn::Error::new(
9159                        span,
9160                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9161                    ));
9162                };
9163                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9164                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9165                    return Err(syn::Error::new(
9166                        span,
9167                        format!(
9168                            "duplicate `reverse_has(name = \"{name}\")` — \
9169                             pick a unique accessor name"
9170                        ),
9171                    ));
9172                }
9173                out.reverse_has_relations.push(ReverseHasAttr {
9174                    name,
9175                    child,
9176                    child_fk_column,
9177                    self_pk_column,
9178                });
9179                return Ok(());
9180            }
9181            if meta.path.is_ident("generic_has") {
9182                // `#[rustango(generic_has(name = "tags",
9183                //  child = "Tag", ct_column = "content_type_id",
9184                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9185                //  — issue #830, the reverse generic-FK (GFK) arm of the
9186                //  relation-existence family.
9187                let span = meta.path.span();
9188                let mut accessor_name: Option<String> = None;
9189                let mut child_ident: Option<syn::Ident> = None;
9190                let mut ct_column: Option<String> = None;
9191                let mut pk_column: Option<String> = None;
9192                let mut self_pk_column: Option<String> = None;
9193                meta.parse_nested_meta(|inner| {
9194                    if inner.path.is_ident("name") {
9195                        let s: LitStr = inner.value()?.parse()?;
9196                        let raw = s.value();
9197                        if raw.trim().is_empty() {
9198                            return Err(syn::Error::new(
9199                                s.span(),
9200                                "`generic_has(name = \"...\")` must not be empty",
9201                            ));
9202                        }
9203                        accessor_name = Some(raw);
9204                        return Ok(());
9205                    }
9206                    if inner.path.is_ident("child") {
9207                        let s: LitStr = inner.value()?.parse()?;
9208                        let trimmed = s.value().trim().to_owned();
9209                        if trimmed.is_empty() {
9210                            return Err(syn::Error::new(
9211                                s.span(),
9212                                "`generic_has(child = \"...\")` must not be empty",
9213                            ));
9214                        }
9215                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9216                        return Ok(());
9217                    }
9218                    if inner.path.is_ident("ct_column") {
9219                        let s: LitStr = inner.value()?.parse()?;
9220                        let trimmed = s.value().trim().to_owned();
9221                        if trimmed.is_empty() {
9222                            return Err(syn::Error::new(
9223                                s.span(),
9224                                "`generic_has(ct_column = \"...\")` must not be empty",
9225                            ));
9226                        }
9227                        ct_column = Some(trimmed);
9228                        return Ok(());
9229                    }
9230                    if inner.path.is_ident("pk_column") {
9231                        let s: LitStr = inner.value()?.parse()?;
9232                        let trimmed = s.value().trim().to_owned();
9233                        if trimmed.is_empty() {
9234                            return Err(syn::Error::new(
9235                                s.span(),
9236                                "`generic_has(pk_column = \"...\")` must not be empty",
9237                            ));
9238                        }
9239                        pk_column = Some(trimmed);
9240                        return Ok(());
9241                    }
9242                    if inner.path.is_ident("self_pk_column") {
9243                        let s: LitStr = inner.value()?.parse()?;
9244                        let trimmed = s.value().trim().to_owned();
9245                        if trimmed.is_empty() {
9246                            return Err(syn::Error::new(
9247                                s.span(),
9248                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9249                            ));
9250                        }
9251                        self_pk_column = Some(trimmed);
9252                        return Ok(());
9253                    }
9254                    Err(inner.error(
9255                        "unknown `generic_has` attribute (supported: \
9256                         `name`, `child`, `ct_column`, `pk_column`, \
9257                         `self_pk_column`)",
9258                    ))
9259                })?;
9260                let Some(name) = accessor_name else {
9261                    return Err(syn::Error::new(
9262                        span,
9263                        "`generic_has` requires `name = \"...\"`",
9264                    ));
9265                };
9266                let Some(child) = child_ident else {
9267                    return Err(syn::Error::new(
9268                        span,
9269                        "`generic_has` requires `child = \"ChildModelType\"`",
9270                    ));
9271                };
9272                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9273                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9274                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9275                if out.generic_has_relations.iter().any(|r| r.name == name) {
9276                    return Err(syn::Error::new(
9277                        span,
9278                        format!(
9279                            "duplicate `generic_has(name = \"{name}\")` — \
9280                             pick a unique accessor name"
9281                        ),
9282                    ));
9283                }
9284                out.generic_has_relations.push(GenericHasAttr {
9285                    name,
9286                    child,
9287                    ct_column,
9288                    pk_column,
9289                    self_pk_column,
9290                });
9291                return Ok(());
9292            }
9293            if meta.path.is_ident("audit") {
9294                let mut audit = AuditAttrs::default();
9295                meta.parse_nested_meta(|inner| {
9296                    if inner.path.is_ident("track") {
9297                        let s: LitStr = inner.value()?.parse()?;
9298                        audit.track =
9299                            Some((split_field_list(&s.value()), s.span()));
9300                        return Ok(());
9301                    }
9302                    Err(inner.error(
9303                        "unknown audit attribute (supported: `track`)",
9304                    ))
9305                })?;
9306                out.audit = Some(audit);
9307                return Ok(());
9308            }
9309            if meta.path.is_ident("permissions") {
9310                // Two forms accepted:
9311                //   #[rustango(permissions)]          — flag form, true
9312                //   #[rustango(permissions = false)]  — explicit opt-out
9313                //   #[rustango(permissions = true)]   — explicit opt-in
9314                if let Ok(v) = meta.value() {
9315                    let lit: syn::LitBool = v.parse()?;
9316                    out.permissions = lit.value;
9317                } else {
9318                    out.permissions = true;
9319                }
9320                return Ok(());
9321            }
9322            if meta.path.is_ident("view") {
9323                // Issue #293 / T2.10. Two forms accepted, matching
9324                // the `permissions` flag pattern:
9325                //   #[rustango(view)]          — flag form, true
9326                //   #[rustango(view = false)]  — explicit opt-out
9327                //   #[rustango(view = true)]   — explicit opt-in
9328                if let Ok(v) = meta.value() {
9329                    let lit: syn::LitBool = v.parse()?;
9330                    out.is_view = lit.value;
9331                } else {
9332                    out.is_view = true;
9333                }
9334                return Ok(());
9335            }
9336            if meta.path.is_ident("managed") {
9337                // Django-shape Meta.managed. Issue #321.
9338                //   #[rustango(managed = false)]  — operator-managed table
9339                //   #[rustango(managed = true)]   — rustango-managed (the default)
9340                // Bare-flag form is intentionally not accepted: writing
9341                // `#[rustango(managed)]` reads as "yes please manage it"
9342                // which is already the default. The opt-out is the only
9343                // useful state, so it must be explicit.
9344                let v = meta.value()?;
9345                let lit: syn::LitBool = v.parse()?;
9346                out.managed = lit.value;
9347                return Ok(());
9348            }
9349            if meta.path.is_ident("verbose_name") {
9350                let s: LitStr = meta.value()?.parse()?;
9351                out.verbose_name = Some(s.value());
9352                return Ok(());
9353            }
9354            if meta.path.is_ident("verbose_name_plural") {
9355                let s: LitStr = meta.value()?.parse()?;
9356                out.verbose_name_plural = Some(s.value());
9357                return Ok(());
9358            }
9359            if meta.path.is_ident("db_table_comment") {
9360                // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9361                // table-level comment attached to the DB catalog.
9362                let s: LitStr = meta.value()?.parse()?;
9363                out.db_table_comment = Some(s.value());
9364                return Ok(());
9365            }
9366            if meta.path.is_ident("proxy") {
9367                // Django-shape `Meta.proxy = True` — declarative flag
9368                // marking the struct as a proxy of another model that
9369                // shares its DB table. Stored on `ModelSchema::proxy`
9370                // so future codegen can skip `CreateTable` emission
9371                // for proxies (parent owns the table) and route
9372                // per-instance method resolution to the proxy class.
9373                //
9374                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9375                let value = if meta.input.peek(syn::Token![=]) {
9376                    meta.value()?.parse::<syn::LitBool>()?.value
9377                } else {
9378                    true
9379                };
9380                out.proxy = value;
9381                return Ok(());
9382            }
9383            if meta.path.is_ident("order_with_respect_to") {
9384                // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9385                // the model's instances are intrinsically ordered
9386                // relative to their parent FK. Django auto-generates
9387                // a `_order` integer column + admin reordering UI.
9388                //
9389                // rustango stores the FK field name on
9390                // `ModelSchema::order_with_respect_to`. Declarative-only
9391                // today: the migration writer + admin surfaces still
9392                // treat every model identically. Future codegen will
9393                // key off the metadata to auto-emit the `_order`
9394                // column and reorder helpers.
9395                //
9396                // Validated as a Rust-shape identifier so the macro
9397                // can reject typos at derive time.
9398                let s: LitStr = meta.value()?.parse()?;
9399                let raw = s.value();
9400                if raw.is_empty() {
9401                    return Err(syn::Error::new(
9402                        s.span(),
9403                        "`order_with_respect_to` must be a non-empty FK field name",
9404                    ));
9405                }
9406                let valid = raw
9407                    .chars()
9408                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9409                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9410                if !valid {
9411                    return Err(syn::Error::new(
9412                        s.span(),
9413                        format!(
9414                            "`order_with_respect_to` must be a valid Rust \
9415                             identifier (letters / digits / underscores, \
9416                             not starting with a digit); got `{raw}`"
9417                        ),
9418                    ));
9419                }
9420                out.order_with_respect_to = Some(raw);
9421                return Ok(());
9422            }
9423            if meta.path.is_ident("required_db_features") {
9424                // Django-shape `Meta.required_db_features` — capability
9425                // tokens the model needs (e.g. `"json_extract"`,
9426                // `"window_functions"`, `"row_security"`). Comma-separated.
9427                // `manage check --deploy` walks every model and warns
9428                // when the active backend doesn't advertise the
9429                // capability.
9430                //
9431                // rustango ships a small registry of capability tokens
9432                // each dialect supports — see `Dialect::supports`.
9433                // Unknown tokens still parse (they end up on the
9434                // schema and show up in the warning) so projects can
9435                // declare aspirational capabilities and the check
9436                // verb will keep nagging until the dialect implements
9437                // them.
9438                let s: LitStr = meta.value()?.parse()?;
9439                let raw = s.value();
9440                let features: Vec<String> = raw
9441                    .split(',')
9442                    .map(str::trim)
9443                    .filter(|s| !s.is_empty())
9444                    .map(str::to_owned)
9445                    .collect();
9446                if features.is_empty() {
9447                    return Err(syn::Error::new(
9448                        s.span(),
9449                        "`required_db_features` must list at least one \
9450                         comma-separated capability token",
9451                    ));
9452                }
9453                out.required_db_features = features;
9454                return Ok(());
9455            }
9456            if meta.path.is_ident("required_db_vendor") {
9457                // Django-shape `Meta.required_db_vendor` — the model
9458                // is only meant to run against the named DB backend.
9459                // `manage check --deploy` flags a mismatch so
9460                // ops catches "I forgot to switch DATABASE_URL" at
9461                // deploy time rather than runtime.
9462                //
9463                // Django spells it as a free-form string; rustango
9464                // restricts to the three backends it ships dialects
9465                // for so the check verb can compare reliably.
9466                let s: LitStr = meta.value()?.parse()?;
9467                let raw = s.value().to_ascii_lowercase();
9468                match raw.as_str() {
9469                    "postgresql" | "postgres" | "pg" => {
9470                        out.required_db_vendor = Some("postgres".to_owned());
9471                    }
9472                    "mysql" | "mariadb" => {
9473                        out.required_db_vendor = Some("mysql".to_owned());
9474                    }
9475                    "sqlite" | "sqlite3" => {
9476                        out.required_db_vendor = Some("sqlite".to_owned());
9477                    }
9478                    _ => {
9479                        return Err(syn::Error::new(
9480                            s.span(),
9481                            format!(
9482                                "unknown required_db_vendor `{raw}` — \
9483                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9484                                 `mysql` (alias: `mariadb`), or `sqlite` \
9485                                 (alias: `sqlite3`)"
9486                            ),
9487                        ));
9488                    }
9489                }
9490                return Ok(());
9491            }
9492            if meta.path.is_ident("base_manager_name") {
9493                // Django-shape `Meta.base_manager_name` — name of the
9494                // Manager subclass that `<instance>.<relation>_set`
9495                // uses when resolving reverse-relation managers.
9496                // Distinct from `default_manager_name` (what
9497                // `Model.objects` returns at the class level).
9498                // Stored on `ModelSchema::base_manager_name`.
9499                //
9500                // Validated as a Rust identifier so it stays safe to
9501                // re-emit as code in future reverse-manager codegen.
9502                let s: LitStr = meta.value()?.parse()?;
9503                let raw = s.value();
9504                if raw.is_empty() {
9505                    return Err(syn::Error::new(
9506                        s.span(),
9507                        "`base_manager_name` must be a non-empty string",
9508                    ));
9509                }
9510                let valid = raw
9511                    .chars()
9512                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9513                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9514                if !valid {
9515                    return Err(syn::Error::new(
9516                        s.span(),
9517                        format!(
9518                            "`base_manager_name` must be a valid Rust \
9519                             identifier (letters / digits / underscores, \
9520                             not starting with a digit); got `{raw}`"
9521                        ),
9522                    ));
9523                }
9524                out.base_manager_name = Some(raw);
9525                return Ok(());
9526            }
9527            if meta.path.is_ident("default_related_name") {
9528                // Django-shape `Meta.default_related_name` — the name
9529                // reverse-relation accessors use when callers don't
9530                // override `related_name=...` on the FK / M2M field.
9531                // Stored on `ModelSchema::default_related_name` so
9532                // future reverse-manager codegen / DRF schema emit /
9533                // admin templates can pick the right accessor name
9534                // (today rustango doesn't auto-emit reverse managers;
9535                // the metadata is the foundation for that work).
9536                //
9537                // Django requires snake_case + no `+` suffix; we
9538                // enforce non-empty + ASCII identifier-shape so the
9539                // string is safe to use as a Rust ident later.
9540                let s: LitStr = meta.value()?.parse()?;
9541                let raw = s.value();
9542                if raw.is_empty() {
9543                    return Err(syn::Error::new(
9544                        s.span(),
9545                        "`default_related_name` must be a non-empty string",
9546                    ));
9547                }
9548                let valid = raw
9549                    .chars()
9550                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9551                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9552                if !valid {
9553                    return Err(syn::Error::new(
9554                        s.span(),
9555                        format!(
9556                            "`default_related_name` must be snake_case ASCII \
9557                             (lowercase letters / digits / underscores, not \
9558                             starting with a digit); got `{raw}`"
9559                        ),
9560                    ));
9561                }
9562                out.default_related_name = Some(raw);
9563                return Ok(());
9564            }
9565            if meta.path.is_ident("extra_permissions") {
9566                // Django-shape `Meta.permissions = [(codename, name), ...]`.
9567                // Comma-separated `codename:label` pairs.
9568                let s: LitStr = meta.value()?.parse()?;
9569                let raw = s.value();
9570                let mut pairs = Vec::new();
9571                for entry in raw.split(',') {
9572                    let entry = entry.trim();
9573                    if entry.is_empty() {
9574                        continue;
9575                    }
9576                    let (codename, label) = match entry.split_once(':') {
9577                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9578                        None => (entry.to_owned(), entry.to_owned()),
9579                    };
9580                    if codename.is_empty() {
9581                        return Err(meta.error(
9582                            "`extra_permissions` entries must be `codename:label` pairs",
9583                        ));
9584                    }
9585                    pairs.push((codename, label));
9586                }
9587                if pairs.is_empty() {
9588                    return Err(meta
9589                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9590                }
9591                out.extra_permissions = pairs;
9592                return Ok(());
9593            }
9594            if meta.path.is_ident("default_permissions") {
9595                // Django-shape `Meta.default_permissions = ('view',
9596                // 'change')`. Comma-separated subset of the CRUD
9597                // action set. Empty means all four (the framework
9598                // default — matches Django when the option is
9599                // omitted).
9600                let s: LitStr = meta.value()?.parse()?;
9601                let raw = s.value();
9602                let mut actions: Vec<String> = Vec::new();
9603                for entry in raw.split(',') {
9604                    let action = entry.trim().to_ascii_lowercase();
9605                    if action.is_empty() {
9606                        continue;
9607                    }
9608                    match action.as_str() {
9609                        "add" | "change" | "delete" | "view" => {}
9610                        other => {
9611                            return Err(syn::Error::new(
9612                                s.span(),
9613                                format!(
9614                                    "unknown default_permissions action `{other}` — \
9615                                     expected one of `add`, `change`, `delete`, `view`"
9616                                ),
9617                            ));
9618                        }
9619                    }
9620                    if !actions.contains(&action) {
9621                        actions.push(action);
9622                    }
9623                }
9624                if actions.is_empty() {
9625                    return Err(syn::Error::new(
9626                        s.span(),
9627                        "`default_permissions = \"…\"` must list at least one action; \
9628                         use `permissions = false` on the container if you want NO \
9629                         permissions seeded for this model.",
9630                    ));
9631                }
9632                out.default_permissions = actions;
9633                return Ok(());
9634            }
9635            if meta.path.is_ident("get_latest_by") {
9636                // Django-shape `Meta.get_latest_by`. The `-` prefix
9637                // selects descending order (Django muscle memory).
9638                let s: LitStr = meta.value()?.parse()?;
9639                let raw = s.value();
9640                let trimmed = raw.trim();
9641                if trimmed.is_empty() {
9642                    return Err(meta.error("`get_latest_by` must name a column"));
9643                }
9644                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9645                    (stripped.to_owned(), true)
9646                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9647                    (stripped.to_owned(), false)
9648                } else {
9649                    (trimmed.to_owned(), false)
9650                };
9651                if col.is_empty() {
9652                    return Err(meta.error("`get_latest_by` must name a column"));
9653                }
9654                out.get_latest_by = Some((col, desc));
9655                return Ok(());
9656            }
9657            if meta.path.is_ident("unique_together") {
9658                // Django-shape composite UNIQUE index. Two syntaxes:
9659                //
9660                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9661                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9662                //
9663                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9664                // (col1, col2)`, where <name> defaults to
9665                // `<table>_<col1>_<col2>_uq` when not supplied.
9666                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9667                out.indexes.push(IndexAttr {
9668                    name,
9669                    columns,
9670                    unique: true,
9671                    method: "btree".to_owned(),
9672                    where_clause: None,
9673                include: Vec::new(),
9674                });
9675                return Ok(());
9676            }
9677            if meta.path.is_ident("index_together") {
9678                // Django-shape composite (non-unique) index. Two syntaxes
9679                // mirroring `unique_together`.
9680                //
9681                //   #[rustango(index_together = "created_at, status")]
9682                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9683                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9684                out.indexes.push(IndexAttr {
9685                    name,
9686                    columns,
9687                    unique: false,
9688                    method: "btree".to_owned(),
9689                    where_clause: None,
9690                include: Vec::new(),
9691                });
9692                return Ok(());
9693            }
9694            if meta.path.is_ident("unique_when") {
9695                // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9696                // partial unique index. Issue #265 / T1.3.
9697                //
9698                //   #[rustango(unique_when(
9699                //       columns   = "email",
9700                //       condition = "deleted_at IS NULL",
9701                //       name      = "unique_active_email"
9702                //   ))]
9703                //
9704                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9705                // on PG / SQLite (both ship partial indexes natively).
9706                // MySQL falls back to a plain UNIQUE index — the
9707                // condition is lost; document the limitation in the
9708                // generated migration.
9709                let mut columns: Option<Vec<String>> = None;
9710                let mut condition: Option<String> = None;
9711                let mut name: Option<String> = None;
9712                let mut include: Vec<String> = Vec::new();
9713                meta.parse_nested_meta(|inner| {
9714                    if inner.path.is_ident("columns") {
9715                        let s: LitStr = inner.value()?.parse()?;
9716                        columns = Some(split_field_list(&s.value()));
9717                        return Ok(());
9718                    }
9719                    if inner.path.is_ident("condition") {
9720                        let s: LitStr = inner.value()?.parse()?;
9721                        condition = Some(s.value());
9722                        return Ok(());
9723                    }
9724                    if inner.path.is_ident("name") {
9725                        let s: LitStr = inner.value()?.parse()?;
9726                        name = Some(s.value());
9727                        return Ok(());
9728                    }
9729                    if inner.path.is_ident("include") {
9730                        // Django `UniqueConstraint(include=[...])` — PG
9731                        // 11+ covering-index columns. Non-key columns
9732                        // travel with the index leaf for index-only
9733                        // scans. Dropped on MySQL/SQLite by the writer.
9734                        let s: LitStr = inner.value()?.parse()?;
9735                        include = split_field_list(&s.value());
9736                        return Ok(());
9737                    }
9738                    Err(inner.error(
9739                        "unknown unique_when attribute (supported: \
9740                         `columns = \"...\"`, `condition = \"...\"`, \
9741                         `name = \"...\"`, `include = \"...\"`)",
9742                    ))
9743                })?;
9744                let columns = columns.ok_or_else(|| {
9745                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9746                })?;
9747                let condition = condition.ok_or_else(|| {
9748                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9749                })?;
9750                if columns.is_empty() {
9751                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9752                }
9753                out.indexes.push(IndexAttr {
9754                    name,
9755                    columns,
9756                    unique: true,
9757                    method: "btree".to_owned(),
9758                    where_clause: Some(condition),
9759                    include,
9760                });
9761                return Ok(());
9762            }
9763            if meta.path.is_ident("index_when") {
9764                // Django `Index(fields=..., condition=Q(...))` parity —
9765                // non-unique partial index. Sibling of `unique_when`
9766                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9767                //
9768                //   #[rustango(index_when(
9769                //       columns   = "status, created_at",
9770                //       condition = "deleted_at IS NULL",
9771                //       name      = "active_status_created_idx"
9772                //   ))]
9773                //
9774                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9775                // on PG / SQLite (both ship partial indexes natively).
9776                // MySQL has no native partial-index support — the writer
9777                // emits a plain CREATE INDEX and the condition is lost;
9778                // operators wanting that selectivity on MySQL should
9779                // declare a covering index plus an application-level
9780                // filter.
9781                let mut columns: Option<Vec<String>> = None;
9782                let mut condition: Option<String> = None;
9783                let mut name: Option<String> = None;
9784                let mut method: String = "btree".to_owned();
9785                let mut include: Vec<String> = Vec::new();
9786                meta.parse_nested_meta(|inner| {
9787                    if inner.path.is_ident("columns") {
9788                        let s: LitStr = inner.value()?.parse()?;
9789                        columns = Some(split_field_list(&s.value()));
9790                        return Ok(());
9791                    }
9792                    if inner.path.is_ident("condition") {
9793                        let s: LitStr = inner.value()?.parse()?;
9794                        condition = Some(s.value());
9795                        return Ok(());
9796                    }
9797                    if inner.path.is_ident("name") {
9798                        let s: LitStr = inner.value()?.parse()?;
9799                        name = Some(s.value());
9800                        return Ok(());
9801                    }
9802                    if inner.path.is_ident("method") {
9803                        let s: LitStr = inner.value()?.parse()?;
9804                        method = s.value();
9805                        return Ok(());
9806                    }
9807                    if inner.path.is_ident("include") {
9808                        // Django `Index(include=[...])` — PG 11+
9809                        // covering-index columns; non-key columns
9810                        // travel with the index leaf. Dropped on
9811                        // MySQL/SQLite.
9812                        let s: LitStr = inner.value()?.parse()?;
9813                        include = split_field_list(&s.value());
9814                        return Ok(());
9815                    }
9816                    Err(inner.error(
9817                        "unknown index_when attribute (supported: \
9818                         `columns = \"...\"`, `condition = \"...\"`, \
9819                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9820                         `include = \"...\"`)",
9821                    ))
9822                })?;
9823                let columns = columns
9824                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9825                let condition = condition.ok_or_else(|| {
9826                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9827                })?;
9828                if columns.is_empty() {
9829                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9830                }
9831                out.indexes.push(IndexAttr {
9832                    name,
9833                    columns,
9834                    unique: false,
9835                    method,
9836                    where_clause: Some(condition),
9837                    include,
9838                });
9839                return Ok(());
9840            }
9841            if meta.path.is_ident("index") {
9842                // Container-level composite index. Two syntaxes:
9843                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9844                //   #[rustango(index("col1, col2"))]    — call form (same result)
9845                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9846                // The call form takes the column list as a leading string
9847                // literal, then optional `unique` / `name = "..."` /
9848                // `method = "..."` flags (a leading literal can't compose
9849                // under `parse_nested_meta`, so the paren body is parsed by
9850                // hand). `unique_together` / `index_together` remain the
9851                // Django-shape aliases for the same feature.
9852                let cols_lit: LitStr;
9853                let mut unique = false;
9854                let mut name: Option<String> = None;
9855                let mut method = "btree".to_owned();
9856                if meta.input.peek(syn::token::Paren) {
9857                    let content;
9858                    syn::parenthesized!(content in meta.input);
9859                    cols_lit = content.parse()?;
9860                    while content.peek(syn::Token![,]) {
9861                        content.parse::<syn::Token![,]>()?;
9862                        if content.is_empty() {
9863                            break;
9864                        }
9865                        let flag: syn::Ident = content.parse()?;
9866                        if flag == "unique" {
9867                            unique = true;
9868                        } else if flag == "name" {
9869                            content.parse::<syn::Token![=]>()?;
9870                            let s: LitStr = content.parse()?;
9871                            name = Some(s.value());
9872                        } else if flag == "method" {
9873                            content.parse::<syn::Token![=]>()?;
9874                            let s: LitStr = content.parse()?;
9875                            let v = s.value();
9876                            match v.as_str() {
9877                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9878                                | "bloom" => method = v,
9879                                other => {
9880                                    return Err(syn::Error::new(
9881                                        s.span(),
9882                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9883                                    ));
9884                                }
9885                            }
9886                        } else {
9887                            return Err(syn::Error::new(
9888                                flag.span(),
9889                                "unknown index flag (supported: `unique`, `name`, `method`)",
9890                            ));
9891                        }
9892                    }
9893                } else {
9894                    cols_lit = meta.value()?.parse()?;
9895                }
9896                let columns = split_field_list(&cols_lit.value());
9897                out.indexes.push(IndexAttr {
9898                    name,
9899                    columns,
9900                    unique,
9901                    method,
9902                    where_clause: None,
9903                    include: Vec::new(),
9904                });
9905                return Ok(());
9906            }
9907            if meta.path.is_ident("check") {
9908                // #[rustango(check(name = "…", expr = "…"))]
9909                let mut name: Option<String> = None;
9910                let mut expr: Option<String> = None;
9911                meta.parse_nested_meta(|inner| {
9912                    if inner.path.is_ident("name") {
9913                        let s: LitStr = inner.value()?.parse()?;
9914                        name = Some(s.value());
9915                        return Ok(());
9916                    }
9917                    if inner.path.is_ident("expr") {
9918                        let s: LitStr = inner.value()?.parse()?;
9919                        expr = Some(s.value());
9920                        return Ok(());
9921                    }
9922                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9923                })?;
9924                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9925                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9926                out.checks.push(CheckAttr { name, expr });
9927                return Ok(());
9928            }
9929            if meta.path.is_ident("exclude") {
9930                // #[rustango(exclude(name = "…", using = "gist",
9931                //                    elements = "col WITH op, col WITH op",
9932                //                    where = "…"))]
9933                let mut name: Option<String> = None;
9934                let mut using: Option<String> = None;
9935                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9936                let mut where_clause: Option<String> = None;
9937                meta.parse_nested_meta(|inner| {
9938                    if inner.path.is_ident("name") {
9939                        let s: LitStr = inner.value()?.parse()?;
9940                        name = Some(s.value());
9941                        return Ok(());
9942                    }
9943                    if inner.path.is_ident("using") {
9944                        let s: LitStr = inner.value()?.parse()?;
9945                        using = Some(s.value());
9946                        return Ok(());
9947                    }
9948                    if inner.path.is_ident("elements") {
9949                        let s: LitStr = inner.value()?.parse()?;
9950                        elements_raw = Some((s.value(), s.span()));
9951                        return Ok(());
9952                    }
9953                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9954                        let s: LitStr = inner.value()?.parse()?;
9955                        where_clause = Some(s.value());
9956                        return Ok(());
9957                    }
9958                    Err(inner.error(
9959                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9960                    ))
9961                })?;
9962                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9963                let using = using.unwrap_or_else(|| "gist".to_owned());
9964                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9965                    meta.error(
9966                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
9967                    )
9968                })?;
9969                // Parse `col WITH op` pairs separated by commas.
9970                let mut elements: Vec<(String, String)> = Vec::new();
9971                for pair in elements_str.split(',') {
9972                    let pair = pair.trim();
9973                    if pair.is_empty() {
9974                        continue;
9975                    }
9976                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9977                    let col = split.next().unwrap_or("").trim();
9978                    let rest = split.next().unwrap_or("").trim();
9979                    // `WITH op` — case-insensitive on `WITH`, then op.
9980                    let rest_lc = rest.to_ascii_lowercase();
9981                    let op = rest_lc
9982                        .strip_prefix("with")
9983                        .map(|r| r.trim_start())
9984                        .filter(|r| !r.is_empty())
9985                        .map(|_| {
9986                            // Pull the original-case op from `rest` after the
9987                            // `WITH ` token (5 chars).
9988                            rest[4..].trim_start().to_owned()
9989                        });
9990                    let Some(op) = op else {
9991                        return Err(syn::Error::new(
9992                            elements_span,
9993                            format!(
9994                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9995                                 (e.g. `room_id WITH =` or `during WITH &&`)"
9996                            ),
9997                        ));
9998                    };
9999                    if col.is_empty() || op.is_empty() {
10000                        return Err(syn::Error::new(
10001                            elements_span,
10002                            format!(
10003                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
10004                                 (both sides non-empty)"
10005                            ),
10006                        ));
10007                    }
10008                    elements.push((col.to_owned(), op));
10009                }
10010                if elements.is_empty() {
10011                    return Err(syn::Error::new(
10012                        elements_span,
10013                        "exclude requires at least one `col WITH op` element",
10014                    ));
10015                }
10016                out.excludes.push(ExcludeAttr {
10017                    name,
10018                    using,
10019                    elements,
10020                    where_clause,
10021                });
10022                return Ok(());
10023            }
10024            if meta.path.is_ident("generic_fk") {
10025                let mut gfk = GenericFkAttr {
10026                    name: String::new(),
10027                    ct_column: String::new(),
10028                    pk_column: String::new(),
10029                };
10030                meta.parse_nested_meta(|inner| {
10031                    if inner.path.is_ident("name") {
10032                        let s: LitStr = inner.value()?.parse()?;
10033                        gfk.name = s.value();
10034                        return Ok(());
10035                    }
10036                    if inner.path.is_ident("ct_column") {
10037                        let s: LitStr = inner.value()?.parse()?;
10038                        gfk.ct_column = s.value();
10039                        return Ok(());
10040                    }
10041                    if inner.path.is_ident("pk_column") {
10042                        let s: LitStr = inner.value()?.parse()?;
10043                        gfk.pk_column = s.value();
10044                        return Ok(());
10045                    }
10046                    Err(inner.error(
10047                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10048                    ))
10049                })?;
10050                if gfk.name.is_empty() {
10051                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10052                }
10053                if gfk.ct_column.is_empty() {
10054                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10055                }
10056                if gfk.pk_column.is_empty() {
10057                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10058                }
10059                out.generic_fks.push(gfk);
10060                return Ok(());
10061            }
10062            if meta.path.is_ident("fk_composite") {
10063                let mut fk = CompositeFkAttr {
10064                    name: String::new(),
10065                    to: String::new(),
10066                    from: Vec::new(),
10067                    on: Vec::new(),
10068                };
10069                meta.parse_nested_meta(|inner| {
10070                    if inner.path.is_ident("name") {
10071                        let s: LitStr = inner.value()?.parse()?;
10072                        fk.name = s.value();
10073                        return Ok(());
10074                    }
10075                    if inner.path.is_ident("to") {
10076                        let s: LitStr = inner.value()?.parse()?;
10077                        fk.to = s.value();
10078                        return Ok(());
10079                    }
10080                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10081                    // comma-list of string literals.
10082                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10083                        let value = inner.value()?;
10084                        let content;
10085                        syn::parenthesized!(content in value);
10086                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10087                            content.parse_terminated(
10088                                |p| p.parse::<syn::LitStr>(),
10089                                syn::Token![,],
10090                            )?;
10091                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10092                        if inner.path.is_ident("on") {
10093                            fk.on = cols;
10094                        } else {
10095                            fk.from = cols;
10096                        }
10097                        return Ok(());
10098                    }
10099                    Err(inner.error(
10100                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10101                    ))
10102                })?;
10103                if fk.name.is_empty() {
10104                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10105                }
10106                if fk.to.is_empty() {
10107                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10108                }
10109                if fk.from.is_empty() || fk.on.is_empty() {
10110                    return Err(meta.error(
10111                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10112                    ));
10113                }
10114                if fk.from.len() != fk.on.len() {
10115                    return Err(meta.error(format!(
10116                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10117                        fk.from.len(),
10118                        fk.on.len(),
10119                    )));
10120                }
10121                out.composite_fks.push(fk);
10122                return Ok(());
10123            }
10124            if meta.path.is_ident("m2m") {
10125                let mut m2m = M2MAttr {
10126                    name: String::new(),
10127                    to: String::new(),
10128                    through: String::new(),
10129                    src: String::new(),
10130                    dst: String::new(),
10131                    auto_create: true,
10132                };
10133                meta.parse_nested_meta(|inner| {
10134                    if inner.path.is_ident("name") {
10135                        let s: LitStr = inner.value()?.parse()?;
10136                        m2m.name = s.value();
10137                        return Ok(());
10138                    }
10139                    if inner.path.is_ident("to") {
10140                        let s: LitStr = inner.value()?.parse()?;
10141                        m2m.to = s.value();
10142                        return Ok(());
10143                    }
10144                    if inner.path.is_ident("through") {
10145                        let s: LitStr = inner.value()?.parse()?;
10146                        m2m.through = s.value();
10147                        return Ok(());
10148                    }
10149                    if inner.path.is_ident("src") {
10150                        let s: LitStr = inner.value()?.parse()?;
10151                        m2m.src = s.value();
10152                        return Ok(());
10153                    }
10154                    if inner.path.is_ident("dst") {
10155                        let s: LitStr = inner.value()?.parse()?;
10156                        m2m.dst = s.value();
10157                        return Ok(());
10158                    }
10159                    if inner.path.is_ident("auto_create") {
10160                        let lit: syn::LitBool = inner.value()?.parse()?;
10161                        m2m.auto_create = lit.value;
10162                        return Ok(());
10163                    }
10164                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10165                })?;
10166                if m2m.name.is_empty() {
10167                    return Err(meta.error("m2m requires `name = \"...\"`"));
10168                }
10169                if m2m.to.is_empty() {
10170                    return Err(meta.error("m2m requires `to = \"...\"`"));
10171                }
10172                if m2m.through.is_empty() {
10173                    return Err(meta.error("m2m requires `through = \"...\"`"));
10174                }
10175                if m2m.src.is_empty() {
10176                    return Err(meta.error("m2m requires `src = \"...\"`"));
10177                }
10178                if m2m.dst.is_empty() {
10179                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10180                }
10181                out.m2m.push(m2m);
10182                return Ok(());
10183            }
10184            if meta.path.is_ident("generic_m2m") {
10185                let mut gm = GenericM2MAttr {
10186                    name: String::new(),
10187                    through: String::new(),
10188                    pk_column: String::new(),
10189                    ct_column: String::new(),
10190                    related_column: String::new(),
10191                };
10192                meta.parse_nested_meta(|inner| {
10193                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10194                        let s: LitStr = inner.value()?.parse()?;
10195                        Ok(s.value())
10196                    };
10197                    if inner.path.is_ident("name") {
10198                        gm.name = field(&inner)?;
10199                        return Ok(());
10200                    }
10201                    if inner.path.is_ident("through") {
10202                        gm.through = field(&inner)?;
10203                        return Ok(());
10204                    }
10205                    if inner.path.is_ident("pk_column") {
10206                        gm.pk_column = field(&inner)?;
10207                        return Ok(());
10208                    }
10209                    if inner.path.is_ident("ct_column") {
10210                        gm.ct_column = field(&inner)?;
10211                        return Ok(());
10212                    }
10213                    if inner.path.is_ident("related_column") {
10214                        gm.related_column = field(&inner)?;
10215                        return Ok(());
10216                    }
10217                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10218                })?;
10219                for (val, label) in [
10220                    (&gm.name, "name"),
10221                    (&gm.through, "through"),
10222                    (&gm.pk_column, "pk_column"),
10223                    (&gm.ct_column, "ct_column"),
10224                    (&gm.related_column, "related_column"),
10225                ] {
10226                    if val.is_empty() {
10227                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10228                    }
10229                }
10230                out.generic_m2m.push(gm);
10231                return Ok(());
10232            }
10233            Err(meta.error("unknown rustango container attribute"))
10234        })?;
10235    }
10236    Ok(out)
10237}
10238
10239/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10240/// owned field names, trimming whitespace and skipping empty entries.
10241/// Field-name validation against the model is done by the caller.
10242fn split_field_list(raw: &str) -> Vec<String> {
10243    raw.split(',')
10244        .map(str::trim)
10245        .filter(|s| !s.is_empty())
10246        .map(str::to_owned)
10247        .collect()
10248}
10249
10250/// Shared parser for `unique_together` and `index_together` container
10251/// attrs. Accepts both shapes:
10252///
10253///   * `attr = "col1, col2"`              — auto-derived index name.
10254///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10255///
10256/// Returns `(columns, name)`.
10257fn parse_together_attr(
10258    meta: &syn::meta::ParseNestedMeta<'_>,
10259    attr: &str,
10260) -> syn::Result<(Vec<String>, Option<String>)> {
10261    // Disambiguate by whether the next token is `=` (key-value) or
10262    // `(` (parenthesized).
10263    if meta.input.peek(syn::Token![=]) {
10264        let cols_lit: LitStr = meta.value()?.parse()?;
10265        let columns = split_field_list(&cols_lit.value());
10266        check_together_columns(meta, attr, &columns)?;
10267        return Ok((columns, None));
10268    }
10269    let mut columns: Option<Vec<String>> = None;
10270    let mut name: Option<String> = None;
10271    meta.parse_nested_meta(|inner| {
10272        if inner.path.is_ident("columns") {
10273            let s: LitStr = inner.value()?.parse()?;
10274            columns = Some(split_field_list(&s.value()));
10275            return Ok(());
10276        }
10277        if inner.path.is_ident("name") {
10278            let s: LitStr = inner.value()?.parse()?;
10279            name = Some(s.value());
10280            return Ok(());
10281        }
10282        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10283    })?;
10284    let columns = columns.ok_or_else(|| {
10285        meta.error(format!(
10286            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10287        ))
10288    })?;
10289    check_together_columns(meta, attr, &columns)?;
10290    Ok((columns, name))
10291}
10292
10293fn check_together_columns(
10294    meta: &syn::meta::ParseNestedMeta<'_>,
10295    attr: &str,
10296    columns: &[String],
10297) -> syn::Result<()> {
10298    if columns.len() < 2 {
10299        let single = if attr == "unique_together" {
10300            "#[rustango(unique)] on the field"
10301        } else {
10302            "#[rustango(index)] on the field"
10303        };
10304        return Err(meta.error(format!(
10305            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10306        )));
10307    }
10308    Ok(())
10309}
10310
10311/// Parse the fieldsets DSL: pipe-separated sections, optional
10312/// `"Title:"` prefix on each, comma-separated field names after.
10313/// Examples:
10314/// * `"name, office"` → one untitled section with two fields
10315/// * `"Identity: name, office | Metadata: created_at"` → two titled
10316///   sections
10317///
10318/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10319fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10320    raw.split('|')
10321        .map(str::trim)
10322        .filter(|s| !s.is_empty())
10323        .map(|section| {
10324            // Split off an optional `Title:` prefix (first colon).
10325            let (title, rest) = match section.split_once(':') {
10326                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10327                _ => (String::new(), section),
10328            };
10329            let fields = split_field_list(rest);
10330            (title, fields)
10331        })
10332        .collect()
10333}
10334
10335/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10336/// comma-separated entry maps a target field to one or more source
10337/// fields joined with `+`. Whitespace around tokens is trimmed.
10338/// Entries missing `:` or with empty target/source lists are dropped.
10339fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10340    raw.split(',')
10341        .map(str::trim)
10342        .filter(|s| !s.is_empty())
10343        .filter_map(|entry| {
10344            let (target, sources_raw) = entry.split_once(':')?;
10345            let target = target.trim().to_owned();
10346            if target.is_empty() {
10347                return None;
10348            }
10349            let sources: Vec<String> = sources_raw
10350                .split('+')
10351                .map(|s| s.trim().to_owned())
10352                .filter(|s| !s.is_empty())
10353                .collect();
10354            if sources.is_empty() {
10355                return None;
10356            }
10357            Some((target, sources))
10358        })
10359        .collect()
10360}
10361
10362/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10363/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10364/// and empty halves drop silently — the macro layer only enforces shape,
10365/// not field-name vs. widget-name validity (those checks happen at
10366/// `AdminConfig` consumption time). Issue #359.
10367fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10368    raw.split(',')
10369        .map(str::trim)
10370        .filter(|s| !s.is_empty())
10371        .filter_map(|entry| {
10372            let (field, widget) = entry.split_once(':')?;
10373            let field = field.trim().to_owned();
10374            let widget = widget.trim().to_owned();
10375            if field.is_empty() || widget.is_empty() {
10376                return None;
10377            }
10378            Some((field, widget))
10379        })
10380        .collect()
10381}
10382
10383/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10384/// Returns `(field_name, desc)` pairs in the same order as the input.
10385fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10386    raw.split(',')
10387        .map(str::trim)
10388        .filter(|s| !s.is_empty())
10389        .map(|spec| {
10390            spec.strip_prefix('-')
10391                .map_or((spec.to_owned(), false), |rest| {
10392                    (rest.trim().to_owned(), true)
10393                })
10394        })
10395        .collect()
10396}
10397
10398struct FieldAttrs {
10399    column: Option<String>,
10400    primary_key: bool,
10401    fk: Option<String>,
10402    o2o: Option<String>,
10403    on: Option<String>,
10404    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10405    /// "set_default" | "no_action")]` — Django-shape
10406    /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10407    /// also set; the macro errors at compile time if applied to a
10408    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10409    /// DDL writer renders `ON DELETE <action>` after the constraint
10410    /// clause when this is `Some`; `None` falls back to the database
10411    /// default (NO ACTION on every backend rustango supports).
10412    on_delete: Option<String>,
10413    /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10414    /// reverse-accessor override. When set, the derive emits
10415    /// `Parent::<related_name>[_pool]` instead of the container-level
10416    /// `default_related_name` or the `<child_snake>_set[_pool]`
10417    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10418    /// silently ignored on non-FK fields. Follow-up to #816.
10419    related_name: Option<String>,
10420    max_length: Option<u32>,
10421    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10422    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10423    /// unconstrained `vector` column.
10424    vector_dims: Option<u32>,
10425    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10426    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10427    /// unconstrained `geometry(Point)` column (SRID 0).
10428    geometry_srid: Option<u32>,
10429    min: Option<i64>,
10430    max: Option<i64>,
10431    default: Option<String>,
10432    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10433    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10434    /// "gen_random_uuid()"`. The Rust field type must be
10435    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10436    /// INSERTs so the DB DEFAULT fires.
10437    auto_uuid: bool,
10438    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10439    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10440    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10441    /// the field is `Auto::Unset`, then bound as a normal parameter
10442    /// rather than relying on a per-dialect DB function. Issue #823
10443    /// (Eloquent `HasUuids`).
10444    ///
10445    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10446    /// Composes with every backend (PG / MySQL / SQLite) — no
10447    /// `gen_random_uuid()` requirement on the database.
10448    default_uuid_v7: bool,
10449    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10450    /// Server-set on insert, immutable from app code afterwards.
10451    /// Implies `auto + default = "now()"`. Field type must be
10452    /// `DateTime<Utc>`.
10453    auto_now_add: bool,
10454    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10455    /// every insert AND every update. Implies `auto + default =
10456    /// "now()"`; the macro additionally rewrites `update_on` /
10457    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10458    /// field value.
10459    auto_now: bool,
10460    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10461    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10462    /// `soft_delete_on(executor)` and `restore_on(executor)`
10463    /// methods on the model.
10464    soft_delete: bool,
10465    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10466    /// the column in the generated DDL.
10467    unique: bool,
10468    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10469    /// generates a `CREATE INDEX` for this column. `unique` here means
10470    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10471    index: bool,
10472    index_unique: bool,
10473    index_name: Option<String>,
10474    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10475    /// `"btree"`. Issue #34.
10476    index_method: String,
10477    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10478    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10479    /// the macro skips this column from every INSERT and UPDATE
10480    /// path, so the database always recomputes the value from
10481    /// `EXPR`. Backlog item #35.
10482    generated_as: Option<String>,
10483    /// `#[rustango(help_text = "…")]` — Django-shape help text
10484    /// rendered below the admin form's input. Threaded into
10485    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10486    /// layers can read it.
10487    help_text: Option<String>,
10488    /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10489    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10490    /// as a `&'static [(&'static str, &'static str)]` slice. When
10491    /// present, the admin form renders a `<select>` instead of `<input>`
10492    /// and the validator rejects values not in the list. Only meaningful
10493    /// for `FieldType::String`; the macro errors at compile time if
10494    /// applied to a non-string field.
10495    choices: Option<Vec<(String, String)>>,
10496    /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10497    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10498    /// the comment in CREATE TABLE; Postgres emits a separate
10499    /// `COMMENT ON COLUMN` statement after the table is created;
10500    /// SQLite silently drops the value (no native column comments).
10501    db_comment: Option<String>,
10502    /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10503    /// label for the field. Threaded into `FieldSchema::verbose_name`
10504    /// so admin column headers, form labels, and other display
10505    /// surfaces can prefer the friendly caption over the Rust
10506    /// identifier. `None` means renderers fall back to the field name.
10507    verbose_name: Option<String>,
10508    /// `#[rustango(editable = false)]` — Django-shape opt-out from
10509    /// auto-generated form rendering. Defaults to `true` so existing
10510    /// fields keep their current admin / form behavior; setting
10511    /// `false` removes the field from the admin change-form entirely
10512    /// (the value is still visible on detail / list views, just not
10513    /// editable).
10514    editable: bool,
10515    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10516    /// "form may submit empty even when DB is NOT NULL". Threaded into
10517    /// `FieldSchema::blank`. Defaults to `false`.
10518    blank: bool,
10519    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10520    /// Django-shape `CITextField`. Threaded into
10521    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10522    /// fields; the macro errors at derive time if applied elsewhere.
10523    case_insensitive: bool,
10524    /// `#[rustango(validators = "email,url")]` — Django-shape
10525    /// model-level validator chain. Comma-separated names that
10526    /// dispatch to the `validators::*` family in `validate_value`.
10527    /// Empty by default; fires on every typed INSERT/UPDATE.
10528    validators: Vec<String>,
10529}
10530
10531fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10532    let mut out = FieldAttrs {
10533        column: None,
10534        primary_key: false,
10535        fk: None,
10536        o2o: None,
10537        on: None,
10538        on_delete: None,
10539        related_name: None,
10540        max_length: None,
10541        vector_dims: None,
10542        geometry_srid: None,
10543        min: None,
10544        max: None,
10545        default: None,
10546        auto_uuid: false,
10547        default_uuid_v7: false,
10548        auto_now_add: false,
10549        auto_now: false,
10550        soft_delete: false,
10551        unique: false,
10552        index: false,
10553        index_unique: false,
10554        index_name: None,
10555        index_method: "btree".to_owned(),
10556        generated_as: None,
10557        help_text: None,
10558        choices: None,
10559        db_comment: None,
10560        verbose_name: None,
10561        editable: true,
10562        blank: false,
10563        case_insensitive: false,
10564        validators: Vec::new(),
10565    };
10566    for attr in &field.attrs {
10567        if !attr.path().is_ident("rustango") {
10568            continue;
10569        }
10570        attr.parse_nested_meta(|meta| {
10571            if meta.path.is_ident("column") {
10572                let s: LitStr = meta.value()?.parse()?;
10573                let name = s.value();
10574                validate_sql_identifier(&name, "column", s.span())?;
10575                out.column = Some(name);
10576                return Ok(());
10577            }
10578            if meta.path.is_ident("primary_key") {
10579                out.primary_key = true;
10580                return Ok(());
10581            }
10582            if meta.path.is_ident("fk") {
10583                let s: LitStr = meta.value()?.parse()?;
10584                out.fk = Some(s.value());
10585                return Ok(());
10586            }
10587            if meta.path.is_ident("o2o") {
10588                let s: LitStr = meta.value()?.parse()?;
10589                out.o2o = Some(s.value());
10590                return Ok(());
10591            }
10592            if meta.path.is_ident("on") {
10593                let s: LitStr = meta.value()?.parse()?;
10594                out.on = Some(s.value());
10595                return Ok(());
10596            }
10597            if meta.path.is_ident("on_delete") {
10598                let s: LitStr = meta.value()?.parse()?;
10599                let raw = s.value();
10600                let normalized = raw.trim().to_ascii_lowercase();
10601                // Validate at parse time so the user gets a clear span
10602                // rather than a downstream compile error in the emit.
10603                match normalized.as_str() {
10604                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10605                    _ => {
10606                        return Err(syn::Error::new(
10607                            s.span(),
10608                            format!(
10609                                "unknown on_delete action `{raw}`; expected one of \
10610                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10611                            ),
10612                        ));
10613                    }
10614                }
10615                out.on_delete = Some(normalized);
10616                return Ok(());
10617            }
10618            if meta.path.is_ident("related_name") {
10619                let s: LitStr = meta.value()?.parse()?;
10620                let raw = s.value();
10621                if raw.trim().is_empty() {
10622                    return Err(syn::Error::new(
10623                        s.span(),
10624                        "`related_name` must be a non-empty identifier",
10625                    ));
10626                }
10627                // Validate as a Rust-ident shape — the value becomes
10628                // a method name on the parent type. Same rule as
10629                // `default_related_name` so the two surfaces match.
10630                if !raw
10631                    .chars()
10632                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10633                    || raw.starts_with(char::is_numeric)
10634                {
10635                    return Err(syn::Error::new(
10636                        s.span(),
10637                        "`related_name` must be snake_case ASCII (lowercase letters, \
10638                         digits, underscores; no leading digit)",
10639                    ));
10640                }
10641                out.related_name = Some(raw);
10642                return Ok(());
10643            }
10644            if meta.path.is_ident("max_length") {
10645                let lit: syn::LitInt = meta.value()?.parse()?;
10646                out.max_length = Some(lit.base10_parse::<u32>()?);
10647                return Ok(());
10648            }
10649            // `#[rustango(vector(dims = N))]` — pgvector column
10650            // dimension (#824). Nested-meta form so it reads like the
10651            // other typed-column attrs.
10652            if meta.path.is_ident("vector") {
10653                meta.parse_nested_meta(|inner| {
10654                    if inner.path.is_ident("dims") {
10655                        let lit: syn::LitInt = inner.value()?.parse()?;
10656                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10657                        return Ok(());
10658                    }
10659                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10660                })?;
10661                return Ok(());
10662            }
10663            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10664            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10665            if meta.path.is_ident("geometry") {
10666                meta.parse_nested_meta(|inner| {
10667                    if inner.path.is_ident("srid") {
10668                        let lit: syn::LitInt = inner.value()?.parse()?;
10669                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10670                        return Ok(());
10671                    }
10672                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10673                })?;
10674                return Ok(());
10675            }
10676            if meta.path.is_ident("min") {
10677                out.min = Some(parse_signed_i64(&meta)?);
10678                return Ok(());
10679            }
10680            if meta.path.is_ident("max") {
10681                out.max = Some(parse_signed_i64(&meta)?);
10682                return Ok(());
10683            }
10684            if meta.path.is_ident("default") {
10685                let s: LitStr = meta.value()?.parse()?;
10686                out.default = Some(s.value());
10687                return Ok(());
10688            }
10689            if meta.path.is_ident("generated_as") {
10690                let s: LitStr = meta.value()?.parse()?;
10691                out.generated_as = Some(s.value());
10692                return Ok(());
10693            }
10694            if meta.path.is_ident("help_text") {
10695                let s: LitStr = meta.value()?.parse()?;
10696                out.help_text = Some(s.value());
10697                return Ok(());
10698            }
10699            if meta.path.is_ident("choices") {
10700                let s: LitStr = meta.value()?.parse()?;
10701                let raw = s.value();
10702                let mut pairs: Vec<(String, String)> = Vec::new();
10703                for chunk in raw.split(',') {
10704                    let chunk = chunk.trim();
10705                    if chunk.is_empty() {
10706                        continue;
10707                    }
10708                    let (value, label) = match chunk.split_once(':') {
10709                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10710                        None => (chunk.to_owned(), chunk.to_owned()),
10711                    };
10712                    if value.is_empty() {
10713                        return Err(syn::Error::new(
10714                            s.span(),
10715                            "`choices` entry has empty value before `:`",
10716                        ));
10717                    }
10718                    pairs.push((value, label));
10719                }
10720                if pairs.is_empty() {
10721                    return Err(syn::Error::new(
10722                        s.span(),
10723                        "`choices = \"…\"` must contain at least one value",
10724                    ));
10725                }
10726                out.choices = Some(pairs);
10727                return Ok(());
10728            }
10729            if meta.path.is_ident("db_comment") {
10730                let s: LitStr = meta.value()?.parse()?;
10731                out.db_comment = Some(s.value());
10732                return Ok(());
10733            }
10734            if meta.path.is_ident("verbose_name") {
10735                let s: LitStr = meta.value()?.parse()?;
10736                out.verbose_name = Some(s.value());
10737                return Ok(());
10738            }
10739            if meta.path.is_ident("editable") {
10740                // Two forms accepted:
10741                //   #[rustango(editable = false)] / true — explicit
10742                //   #[rustango(editable)] — flag form (= true, the
10743                //   default, so harmless; included for symmetry)
10744                if let Ok(v) = meta.value() {
10745                    let lit: syn::LitBool = v.parse()?;
10746                    out.editable = lit.value;
10747                } else {
10748                    out.editable = true;
10749                }
10750                return Ok(());
10751            }
10752            if meta.path.is_ident("blank") {
10753                // Two forms accepted:
10754                //   #[rustango(blank)] — flag form, true
10755                //   #[rustango(blank = false)] / true — explicit
10756                if let Ok(v) = meta.value() {
10757                    let lit: syn::LitBool = v.parse()?;
10758                    out.blank = lit.value;
10759                } else {
10760                    out.blank = true;
10761                }
10762                return Ok(());
10763            }
10764            if meta.path.is_ident("citext") {
10765                // Django-parity CITextField (#344). Two forms:
10766                //   #[rustango(citext)]          — flag form, true
10767                //   #[rustango(citext = true)]   — explicit
10768                //   #[rustango(citext = false)]  — explicit opt-out
10769                // String-only validation lives in the field-type
10770                // emitter (the FieldType discriminant is computed in
10771                // `detect_type`); the macro records the flag and the
10772                // DDL writer emits dialect-specific COLLATE / CITEXT.
10773                if let Ok(v) = meta.value() {
10774                    let lit: syn::LitBool = v.parse()?;
10775                    out.case_insensitive = lit.value;
10776                } else {
10777                    out.case_insensitive = true;
10778                }
10779                return Ok(());
10780            }
10781            if meta.path.is_ident("validators") {
10782                let s: LitStr = meta.value()?.parse()?;
10783                let raw = s.value();
10784                out.validators = raw
10785                    .split(',')
10786                    .map(str::trim)
10787                    .filter(|s| !s.is_empty())
10788                    .map(str::to_owned)
10789                    .collect();
10790                if out.validators.is_empty() {
10791                    return Err(syn::Error::new(
10792                        s.span(),
10793                        "`validators = \"…\"` must list at least one name",
10794                    ));
10795                }
10796                return Ok(());
10797            }
10798            if meta.path.is_ident("auto_uuid") {
10799                out.auto_uuid = true;
10800                // Implied: PK + auto + DEFAULT gen_random_uuid().
10801                // Each is also explicitly settable; the explicit
10802                // value wins if conflicting.
10803                out.primary_key = true;
10804                if out.default.is_none() {
10805                    out.default = Some("gen_random_uuid()".into());
10806                }
10807                return Ok(());
10808            }
10809            if meta.path.is_ident("default_uuid_v7") {
10810                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10811                // No SQL DEFAULT (the macro fills the value Rust-side
10812                // before binding); just mark the field as PK + Auto
10813                // so the insert path is the `Auto::Unset → generate`
10814                // branch.
10815                out.default_uuid_v7 = true;
10816                out.primary_key = true;
10817                return Ok(());
10818            }
10819            if meta.path.is_ident("auto_now_add") {
10820                out.auto_now_add = true;
10821                if out.default.is_none() {
10822                    out.default = Some("now()".into());
10823                }
10824                return Ok(());
10825            }
10826            if meta.path.is_ident("auto_now") {
10827                out.auto_now = true;
10828                if out.default.is_none() {
10829                    out.default = Some("now()".into());
10830                }
10831                return Ok(());
10832            }
10833            if meta.path.is_ident("soft_delete") {
10834                out.soft_delete = true;
10835                return Ok(());
10836            }
10837            if meta.path.is_ident("unique") {
10838                out.unique = true;
10839                return Ok(());
10840            }
10841            if meta.path.is_ident("index") {
10842                out.index = true;
10843                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10844                if meta.input.peek(syn::token::Paren) {
10845                    meta.parse_nested_meta(|inner| {
10846                        if inner.path.is_ident("unique") {
10847                            out.index_unique = true;
10848                            return Ok(());
10849                        }
10850                        if inner.path.is_ident("name") {
10851                            let s: LitStr = inner.value()?.parse()?;
10852                            out.index_name = Some(s.value());
10853                            return Ok(());
10854                        }
10855                        if inner.path.is_ident("method") {
10856                            let s: LitStr = inner.value()?.parse()?;
10857                            let v = s.value();
10858                            match v.as_str() {
10859                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10860                                    out.index_method = v;
10861                                }
10862                                other => {
10863                                    return Err(inner.error(format!(
10864                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10865                                    )));
10866                                }
10867                            }
10868                            return Ok(());
10869                        }
10870                        Err(inner.error(
10871                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10872                        ))
10873                    })?;
10874                }
10875                return Ok(());
10876            }
10877            Err(meta.error("unknown rustango field attribute"))
10878        })?;
10879    }
10880    Ok(out)
10881}
10882
10883/// Parse a signed integer literal, accepting optional leading `-`.
10884fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10885    let expr: syn::Expr = meta.value()?.parse()?;
10886    match expr {
10887        syn::Expr::Lit(syn::ExprLit {
10888            lit: syn::Lit::Int(lit),
10889            ..
10890        }) => lit.base10_parse::<i64>(),
10891        syn::Expr::Unary(syn::ExprUnary {
10892            op: syn::UnOp::Neg(_),
10893            expr,
10894            ..
10895        }) => {
10896            if let syn::Expr::Lit(syn::ExprLit {
10897                lit: syn::Lit::Int(lit),
10898                ..
10899            }) = *expr
10900            {
10901                let v: i64 = lit.base10_parse()?;
10902                Ok(-v)
10903            } else {
10904                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10905            }
10906        }
10907        other => Err(syn::Error::new_spanned(
10908            other,
10909            "expected integer literal (signed)",
10910        )),
10911    }
10912}
10913
10914struct FieldInfo<'a> {
10915    ident: &'a syn::Ident,
10916    column: String,
10917    primary_key: bool,
10918    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10919    /// skip this column when `Auto::Unset` and emit it under
10920    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10921    auto: bool,
10922    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10923    /// the `Column::Value` associated type for typed-column tokens.
10924    value_ty: &'a Type,
10925    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10926    field_type_tokens: TokenStream2,
10927    schema: TokenStream2,
10928    from_row_init: TokenStream2,
10929    /// Variant of [`Self::from_row_init`] that reads the column via
10930    /// `format!("{prefix}__{col}")` so a model can be decoded out of
10931    /// the aliased columns of a JOINed row. Drives slice 9.0d's
10932    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10933    /// helper that `select_related` calls when stitching loaded FKs.
10934    from_aliased_row_init: TokenStream2,
10935    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10936    /// relation helper emit (`Author::<child>_set`) needs to know `T`
10937    /// to point the generated method at the right child model.
10938    fk_inner: Option<Type>,
10939    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10940    /// `kind` (since ForeignKey detection sets `kind` to K's
10941    /// underlying type) but stored separately for clarity at the
10942    /// `FkRelation` construction site, which only sees the FK's
10943    /// surface fields.
10944    fk_pk_kind: DetectedKind,
10945    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10946    /// the bare `ForeignKey<T, K>`. Routes the load_related and
10947    /// fk_pk_access emitters to wrap assignments / accessors in
10948    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10949    /// FK column compiles end-to-end. The DDL writer reads this off
10950    /// the field schema (`nullable` flag); the macro just needs to
10951    /// keep the Rust-side codegen consistent.
10952    nullable: bool,
10953    /// `true` when this column was marked `#[rustango(auto_now)]` —
10954    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10955    /// column instead of the user-supplied value, so `updated_at`
10956    /// always reflects the latest write without the caller having
10957    /// to remember to set it.
10958    auto_now: bool,
10959    /// `true` when this column was marked `#[rustango(auto_now_add)]`
10960    /// — the column is server-set on INSERT (DB DEFAULT) and
10961    /// **immutable** afterwards. `update_on` / `save_on` skip the
10962    /// column entirely so a stale `created_at` value in memory never
10963    /// rewrites the persisted timestamp.
10964    auto_now_add: bool,
10965    /// `true` when this column was marked `#[rustango(soft_delete)]`.
10966    /// Triggers emission of `soft_delete_on(executor)` and
10967    /// `restore_on(executor)` on the model's inherent impl. There is
10968    /// at most one such column per model — emission asserts this.
10969    soft_delete: bool,
10970    /// `Some` when this column was marked
10971    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10972    /// every INSERT and UPDATE path; the database recomputes the
10973    /// value from `EXPR`. Backlog item #35.
10974    generated_as: Option<String>,
10975    /// `true` when this column was marked
10976    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10977    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10978    /// with `Uuid::now_v7()` before binding, so the PK is generated
10979    /// Rust-side and the column is always present in the INSERT
10980    /// statement (no DB DEFAULT requirement). Issue #823.
10981    default_uuid_v7: bool,
10982    /// `Some` when this FK field carried `#[rustango(related_name =
10983    /// "...")]`. Threaded into [`FkRelation::related_name`] and
10984    /// consumed by [`reverse_helper_tokens`] to override the default
10985    /// `<child_snake>_set` accessor name. Follow-up to #816.
10986    related_name: Option<String>,
10987}
10988
10989/// Reject table names that won't survive SQL identifier
10990/// derivation downstream. Postgres' regular-identifier rule
10991/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10992/// through the framework's unquoted FK / index / constraint name
10993/// emission without surprises. We also disallow leading-digit and
10994/// the empty string for clarity.
10995///
10996/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10997/// here — those produce a runtime error from the SQL parser,
10998/// which is loud enough; statically enumerating reserved words
10999/// across the three supported dialects is more friction than help.
11000///
11001/// Backlog item #65.
11002fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
11003    validate_sql_identifier(name, "table", span)
11004}
11005
11006/// Reject SQL identifiers that compile but break downstream SQL
11007/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
11008/// `kind` is "table" / "column" — used for the error message so users
11009/// see which attribute caused the failure.
11010fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11011    if name.is_empty() {
11012        return Err(syn::Error::new(
11013            span,
11014            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11015        ));
11016    }
11017    let mut chars = name.chars();
11018    let first = chars.next().unwrap();
11019    if !(first.is_ascii_alphabetic() || first == '_') {
11020        return Err(syn::Error::new(
11021            span,
11022            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11023        ));
11024    }
11025    for c in chars {
11026        if !(c.is_ascii_alphanumeric() || c == '_') {
11027            return Err(syn::Error::new(
11028                span,
11029                format!(
11030                    "{kind} name `{name}` contains invalid character {c:?} — \
11031                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11032                     Hyphens in particular break FK / index name derivation \
11033                     downstream; use underscores instead (e.g. `{}`)",
11034                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11035                ),
11036            ));
11037        }
11038    }
11039    Ok(())
11040}
11041
11042fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11043    let root = rustango_root();
11044    let attrs = parse_field_attrs(field)?;
11045    let ident = field
11046        .ident
11047        .as_ref()
11048        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11049    let name = ident.to_string();
11050    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11051    let primary_key = attrs.primary_key;
11052    let DetectedType {
11053        kind,
11054        nullable,
11055        auto: detected_auto,
11056        fk_inner,
11057    } = detect_type(&field.ty)?;
11058    check_bound_compatibility(field, &attrs, kind)?;
11059    let auto = detected_auto;
11060    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11061    // INSERT path: the user must wrap the field in `Auto<T>`, which
11062    // marks the column as DB-default-supplied. The mixin attrs then
11063    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11064    // for `auto_now`, force the value on UPDATE too.
11065    if attrs.auto_uuid {
11066        if kind != DetectedKind::Uuid {
11067            return Err(syn::Error::new_spanned(
11068                field,
11069                "`#[rustango(auto_uuid)]` requires the field type to be \
11070                 `Auto<uuid::Uuid>`",
11071            ));
11072        }
11073        if !detected_auto {
11074            return Err(syn::Error::new_spanned(
11075                field,
11076                "`#[rustango(auto_uuid)]` requires the field type to be \
11077                 wrapped in `Auto<...>` so the macro skips the column on \
11078                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11079            ));
11080        }
11081    }
11082    if attrs.default_uuid_v7 {
11083        if kind != DetectedKind::Uuid {
11084            return Err(syn::Error::new_spanned(
11085                field,
11086                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11087                 `Auto<uuid::Uuid>`",
11088            ));
11089        }
11090        if !detected_auto {
11091            return Err(syn::Error::new_spanned(
11092                field,
11093                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11094                 wrapped in `Auto<...>` so the macro can detect the \
11095                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11096            ));
11097        }
11098        if attrs.auto_uuid {
11099            return Err(syn::Error::new_spanned(
11100                field,
11101                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11102                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11103                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11104                 Pick one.",
11105            ));
11106        }
11107    }
11108    if attrs.auto_now_add || attrs.auto_now {
11109        if kind != DetectedKind::DateTime {
11110            return Err(syn::Error::new_spanned(
11111                field,
11112                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11113                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11114            ));
11115        }
11116        if !detected_auto {
11117            return Err(syn::Error::new_spanned(
11118                field,
11119                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11120                 the field type to be wrapped in `Auto<...>` so the macro skips \
11121                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11122            ));
11123        }
11124    }
11125    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11126        return Err(syn::Error::new_spanned(
11127            field,
11128            "`#[rustango(soft_delete)]` requires the field type to be \
11129             `Option<chrono::DateTime<chrono::Utc>>`",
11130        ));
11131    }
11132    let is_mixin_auto =
11133        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11134    if detected_auto && !primary_key && !is_mixin_auto {
11135        return Err(syn::Error::new_spanned(
11136            field,
11137            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11138             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11139             `auto_now`",
11140        ));
11141    }
11142    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11143        return Err(syn::Error::new_spanned(
11144            field,
11145            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11146             SERIAL / BIGSERIAL already supplies a default sequence.",
11147        ));
11148    }
11149    if fk_inner.is_some() && primary_key {
11150        return Err(syn::Error::new_spanned(
11151            field,
11152            "`ForeignKey<T>` is not allowed on a primary-key field — \
11153             a row's PK is its own identity, not a reference to a parent.",
11154        ));
11155    }
11156    if attrs.generated_as.is_some() {
11157        if primary_key {
11158            return Err(syn::Error::new_spanned(
11159                field,
11160                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11161                 primary-key field — a PK must be writable so the row \
11162                 has an identity at INSERT time.",
11163            ));
11164        }
11165        if attrs.default.is_some() {
11166            return Err(syn::Error::new_spanned(
11167                field,
11168                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11169                 `default = \"…\"` — Postgres rejects DEFAULT on \
11170                 generated columns. The expression IS the default.",
11171            ));
11172        }
11173        if detected_auto {
11174            return Err(syn::Error::new_spanned(
11175                field,
11176                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11177                 an `Auto<T>` field — generated columns are computed \
11178                 by the DB, not server-assigned via a sequence. Use a \
11179                 plain Rust type (e.g. `f64`).",
11180            ));
11181        }
11182        if fk_inner.is_some() {
11183            return Err(syn::Error::new_spanned(
11184                field,
11185                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11186                 ForeignKey field.",
11187            ));
11188        }
11189    }
11190    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11191    let column_lit = column.as_str();
11192    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11193    // dimension that the bare `Vector` Rust type can't carry, so emit
11194    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11195    // fallback of `Vector(0)`.
11196    let field_type_tokens = if kind == DetectedKind::Vector {
11197        let root = rustango_root();
11198        let dims = attrs.vector_dims.unwrap_or(0);
11199        quote!(#root::core::FieldType::Vector(#dims))
11200    } else if kind == DetectedKind::Geometry {
11201        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11202        // SRID that the bare `Point` Rust type can't carry, so emit
11203        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11204        let root = rustango_root();
11205        let srid = attrs.geometry_srid.unwrap_or(0);
11206        quote!(#root::core::FieldType::Geometry(#srid))
11207    } else {
11208        kind.variant_tokens()
11209    };
11210    let max_length = optional_u32(attrs.max_length);
11211    let min = optional_i64(attrs.min);
11212    let max = optional_i64(attrs.max);
11213    let default = optional_str(attrs.default.as_deref());
11214
11215    let unique = attrs.unique;
11216    let generated_as = optional_str(attrs.generated_as.as_deref());
11217    let help_text = optional_str(attrs.help_text.as_deref());
11218    let choices = optional_choices(attrs.choices.as_deref());
11219    let db_comment = optional_str(attrs.db_comment.as_deref());
11220    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11221    let editable = attrs.editable;
11222    let blank = attrs.blank;
11223    let case_insensitive = attrs.case_insensitive;
11224    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11225    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11226        return Err(syn::Error::new_spanned(
11227            field,
11228            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11229             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11230             non-FK column.",
11231        ));
11232    }
11233    let fk_on_delete = match attrs.on_delete.as_deref() {
11234        None => quote!(::core::option::Option::None),
11235        Some(action) => {
11236            let variant = match action {
11237                "cascade" => quote!(Cascade),
11238                "restrict" => quote!(Restrict),
11239                "set_null" => quote!(SetNull),
11240                "set_default" => quote!(SetDefault),
11241                "no_action" => quote!(NoAction),
11242                // parse_field_attrs already validated this — guard against future drift.
11243                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11244            };
11245            quote!(::core::option::Option::Some(
11246                #root::core::OnDeleteAction::#variant
11247            ))
11248        }
11249    };
11250    let schema = quote! {
11251        #root::core::FieldSchema {
11252            name: #name,
11253            column: #column_lit,
11254            ty: #field_type_tokens,
11255            nullable: #nullable,
11256            primary_key: #primary_key,
11257            relation: #relation,
11258            max_length: #max_length,
11259            min: #min,
11260            max: #max,
11261            default: #default,
11262            auto: #auto,
11263            unique: #unique,
11264            generated_as: #generated_as,
11265            help_text: #help_text,
11266            choices: #choices,
11267            db_comment: #db_comment,
11268            verbose_name: #verbose_name,
11269            editable: #editable,
11270            blank: #blank,
11271            case_insensitive: #case_insensitive,
11272            fk_on_delete: #fk_on_delete,
11273            validators: &[ #(#validators_lits),* ],
11274        }
11275    };
11276
11277    let from_row_init = quote! {
11278        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11279    };
11280    let from_aliased_row_init = quote! {
11281        #ident: #root::sql::sqlx::Row::try_get(
11282            row,
11283            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11284        )?
11285    };
11286
11287    Ok(FieldInfo {
11288        ident,
11289        column,
11290        primary_key,
11291        auto,
11292        value_ty: &field.ty,
11293        field_type_tokens,
11294        schema,
11295        from_row_init,
11296        from_aliased_row_init,
11297        fk_inner: fk_inner.cloned(),
11298        fk_pk_kind: kind,
11299        nullable,
11300        auto_now: attrs.auto_now,
11301        auto_now_add: attrs.auto_now_add,
11302        soft_delete: attrs.soft_delete,
11303        generated_as: attrs.generated_as.clone(),
11304        default_uuid_v7: attrs.default_uuid_v7,
11305        related_name: attrs.related_name.clone(),
11306    })
11307}
11308
11309fn check_bound_compatibility(
11310    field: &syn::Field,
11311    attrs: &FieldAttrs,
11312    kind: DetectedKind,
11313) -> syn::Result<()> {
11314    if attrs.max_length.is_some() && kind != DetectedKind::String {
11315        return Err(syn::Error::new_spanned(
11316            field,
11317            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11318        ));
11319    }
11320    if attrs.choices.is_some() && kind != DetectedKind::String {
11321        return Err(syn::Error::new_spanned(
11322            field,
11323            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11324             integer-valued enumerations should be modeled with a Rust enum and \
11325             custom (de)serializer for now",
11326        ));
11327    }
11328    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11329        return Err(syn::Error::new_spanned(
11330            field,
11331            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11332        ));
11333    }
11334    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11335        if min > max {
11336            return Err(syn::Error::new_spanned(
11337                field,
11338                format!("`min` ({min}) is greater than `max` ({max})"),
11339            ));
11340        }
11341    }
11342    Ok(())
11343}
11344
11345fn optional_u32(value: Option<u32>) -> TokenStream2 {
11346    if let Some(v) = value {
11347        quote!(::core::option::Option::Some(#v))
11348    } else {
11349        quote!(::core::option::Option::None)
11350    }
11351}
11352
11353fn optional_i64(value: Option<i64>) -> TokenStream2 {
11354    if let Some(v) = value {
11355        quote!(::core::option::Option::Some(#v))
11356    } else {
11357        quote!(::core::option::Option::None)
11358    }
11359}
11360
11361fn optional_str(value: Option<&str>) -> TokenStream2 {
11362    if let Some(v) = value {
11363        quote!(::core::option::Option::Some(#v))
11364    } else {
11365        quote!(::core::option::Option::None)
11366    }
11367}
11368
11369fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11370    let Some(pairs) = pairs else {
11371        return quote!(::core::option::Option::None);
11372    };
11373    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11374    quote!(::core::option::Option::Some(&[#(#entries),*]))
11375}
11376
11377fn relation_tokens(
11378    field: &syn::Field,
11379    attrs: &FieldAttrs,
11380    fk_inner: Option<&syn::Type>,
11381    table: &str,
11382) -> syn::Result<TokenStream2> {
11383    let root = rustango_root();
11384    if let Some(inner) = fk_inner {
11385        if attrs.fk.is_some() || attrs.o2o.is_some() {
11386            return Err(syn::Error::new_spanned(
11387                field,
11388                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11389                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11390            ));
11391        }
11392        let on = attrs.on.as_deref().unwrap_or("id");
11393        return Ok(quote! {
11394            ::core::option::Option::Some(#root::core::Relation::Fk {
11395                to: <#inner as #root::core::Model>::SCHEMA.table,
11396                on: #on,
11397            })
11398        });
11399    }
11400    match (&attrs.fk, &attrs.o2o) {
11401        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11402            field,
11403            "`fk` and `o2o` are mutually exclusive",
11404        )),
11405        (Some(to), None) => {
11406            let on = attrs.on.as_deref().unwrap_or("id");
11407            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11408            // the model's own table. Threaded as a literal string at
11409            // macro-expansion time to sidestep the const-eval cycle
11410            // that `Self::SCHEMA.table` would create when referenced
11411            // inside Self::SCHEMA's own initializer.
11412            let resolved = if to == "self" { table } else { to };
11413            Ok(quote! {
11414                ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11415            })
11416        }
11417        (None, Some(to)) => {
11418            let on = attrs.on.as_deref().unwrap_or("id");
11419            let resolved = if to == "self" { table } else { to };
11420            Ok(quote! {
11421                ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11422            })
11423        }
11424        (None, None) => {
11425            if attrs.on.is_some() {
11426                return Err(syn::Error::new_spanned(
11427                    field,
11428                    "`on` requires `fk` or `o2o`",
11429                ));
11430            }
11431            Ok(quote!(::core::option::Option::None))
11432        }
11433    }
11434}
11435
11436/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11437/// about kinds without depending on `rustango-core` (which would require a
11438/// proc-macro/normal split it doesn't have today).
11439#[derive(Clone, Copy, PartialEq, Eq)]
11440enum DetectedKind {
11441    I16,
11442    I32,
11443    I64,
11444    F32,
11445    F64,
11446    Bool,
11447    String,
11448    DateTime,
11449    Date,
11450    Time,
11451    Uuid,
11452    Json,
11453    Decimal,
11454    Binary,
11455    /// `Array<String>` → PG `text[]` (#341).
11456    ArrayText,
11457    /// `Array<i32>` → PG `integer[]` (#341).
11458    ArrayInt,
11459    /// `Array<i64>` → PG `bigint[]` (#341).
11460    ArrayBigInt,
11461    /// `Range<i32>` → PG `int4range` (#343).
11462    RangeInt,
11463    /// `Range<i64>` → PG `int8range` (#343).
11464    RangeBigInt,
11465    /// `Range<Decimal>` → PG `numrange` (#343).
11466    RangeNumeric,
11467    /// `Range<NaiveDate>` → PG `daterange` (#343).
11468    RangeDate,
11469    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11470    RangeDateTime,
11471    /// `HStore` → PG `hstore` (#342).
11472    HStore,
11473    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11474    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11475    /// in at the `FieldType` emission site (not carried on this enum).
11476    Vector,
11477    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11478    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11479    /// threaded in at the `FieldType` emission site.
11480    Geometry,
11481}
11482
11483impl DetectedKind {
11484    fn variant_tokens(self) -> TokenStream2 {
11485        let root = rustango_root();
11486        match self {
11487            Self::I16 => quote!(#root::core::FieldType::I16),
11488            Self::I32 => quote!(#root::core::FieldType::I32),
11489            Self::I64 => quote!(#root::core::FieldType::I64),
11490            Self::F32 => quote!(#root::core::FieldType::F32),
11491            Self::F64 => quote!(#root::core::FieldType::F64),
11492            Self::Bool => quote!(#root::core::FieldType::Bool),
11493            Self::String => quote!(#root::core::FieldType::String),
11494            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11495            Self::Date => quote!(#root::core::FieldType::Date),
11496            Self::Time => quote!(#root::core::FieldType::Time),
11497            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11498            Self::Json => quote!(#root::core::FieldType::Json),
11499            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11500            Self::Binary => quote!(#root::core::FieldType::Binary),
11501            Self::ArrayText => {
11502                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11503            }
11504            Self::ArrayInt => {
11505                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11506            }
11507            Self::ArrayBigInt => {
11508                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11509            }
11510            Self::RangeInt => {
11511                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11512            }
11513            Self::RangeBigInt => {
11514                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11515            }
11516            Self::RangeNumeric => {
11517                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11518            }
11519            Self::RangeDate => {
11520                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11521            }
11522            Self::RangeDateTime => {
11523                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11524            }
11525            Self::HStore => quote!(#root::core::FieldType::HStore),
11526            // Dimension comes from the `vector(dims = N)` attribute,
11527            // applied at the emission site; `0` here is just a fallback.
11528            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11529            // SRID comes from the `geometry(srid = N)` attribute, applied
11530            // at the emission site; `0` here is just a fallback.
11531            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11532        }
11533    }
11534
11535    fn is_integer(self) -> bool {
11536        matches!(self, Self::I16 | Self::I32 | Self::I64)
11537    }
11538
11539    /// `(SqlValue::<Variant>, default expr)` for emitting the
11540    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11541    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11542    /// fires only when the parent's `__rustango_pk_value` returns a
11543    /// different variant than expected, which is a compile-time bug —
11544    /// but we still need a value-typed fallback to keep the match
11545    /// total.
11546    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11547        let root = rustango_root();
11548        match self {
11549            Self::I16 => (quote!(I16), quote!(0i16)),
11550            Self::I32 => (quote!(I32), quote!(0i32)),
11551            Self::I64 => (quote!(I64), quote!(0i64)),
11552            Self::F32 => (quote!(F32), quote!(0f32)),
11553            Self::F64 => (quote!(F64), quote!(0f64)),
11554            Self::Bool => (quote!(Bool), quote!(false)),
11555            Self::String => (quote!(String), quote!(::std::string::String::new())),
11556            Self::DateTime => (
11557                quote!(DateTime),
11558                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11559            ),
11560            Self::Date => (
11561                quote!(Date),
11562                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11563            ),
11564            Self::Time => (
11565                quote!(Time),
11566                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11567            ),
11568            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11569            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11570            Self::Decimal => (
11571                quote!(Decimal),
11572                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11573            ),
11574            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11575            // Arrays (#341) can never be a foreign-key primary key, so
11576            // this arm is never reached at runtime — but the match must
11577            // stay total. A bare empty `SqlValue::Array` is the dummy.
11578            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11579                (quote!(Array), quote!(::std::vec::Vec::new()))
11580            }
11581            // Ranges (#343) likewise can't be a FK PK — never reached.
11582            Self::RangeInt
11583            | Self::RangeBigInt
11584            | Self::RangeNumeric
11585            | Self::RangeDate
11586            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11587            // HStore (#342) can't be a FK PK — never reached.
11588            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11589            // Vector (#824) can't be a FK PK — never reached.
11590            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11591            // Geometry (#443) can't be a FK PK — never reached (arm is
11592            // exhaustiveness-only; never interpolated into emitted code).
11593            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11594        }
11595    }
11596}
11597
11598/// Result of walking a field's Rust type. `kind` is the underlying
11599/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11600/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11601/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11602/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11603#[derive(Clone, Copy)]
11604struct DetectedType<'a> {
11605    kind: DetectedKind,
11606    nullable: bool,
11607    auto: bool,
11608    fk_inner: Option<&'a syn::Type>,
11609}
11610
11611/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11612/// non-`Auto` types — the caller should already have routed Auto-only
11613/// codegen through this helper, so a `None` indicates a macro-internal
11614/// invariant break.
11615fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11616    let Type::Path(TypePath { path, qself: None }) = ty else {
11617        return None;
11618    };
11619    let last = path.segments.last()?;
11620    if last.ident != "Auto" {
11621        return None;
11622    }
11623    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11624        return None;
11625    };
11626    args.args.iter().find_map(|a| match a {
11627        syn::GenericArgument::Type(t) => Some(t),
11628        _ => None,
11629    })
11630}
11631
11632fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11633    let Type::Path(TypePath { path, qself: None }) = ty else {
11634        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11635    };
11636    let last = path
11637        .segments
11638        .last()
11639        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11640
11641    if last.ident == "Option" {
11642        let inner = generic_inner(ty, &last.arguments, "Option")?;
11643        let inner_det = detect_type(inner)?;
11644        if inner_det.nullable {
11645            return Err(syn::Error::new_spanned(
11646                ty,
11647                "nested Option is not supported",
11648            ));
11649        }
11650        if inner_det.auto {
11651            return Err(syn::Error::new_spanned(
11652                ty,
11653                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11654            ));
11655        }
11656        return Ok(DetectedType {
11657            nullable: true,
11658            ..inner_det
11659        });
11660    }
11661
11662    if last.ident == "Auto" {
11663        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11664        let inner_det = detect_type(inner)?;
11665        if inner_det.auto {
11666            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11667        }
11668        if inner_det.nullable {
11669            return Err(syn::Error::new_spanned(
11670                ty,
11671                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11672            ));
11673        }
11674        if inner_det.fk_inner.is_some() {
11675            return Err(syn::Error::new_spanned(
11676                ty,
11677                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11678            ));
11679        }
11680        if !matches!(
11681            inner_det.kind,
11682            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11683        ) {
11684            return Err(syn::Error::new_spanned(
11685                ty,
11686                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11687                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11688                 (DEFAULT now())",
11689            ));
11690        }
11691        return Ok(DetectedType {
11692            auto: true,
11693            ..inner_det
11694        });
11695    }
11696
11697    if last.ident == "ForeignKey" {
11698        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11699        // Resolve the FK column's underlying SQL type from `K`. When the
11700        // user wrote `ForeignKey<T>` without a key parameter, the type
11701        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11702        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11703        // recurse into K so the column DDL emits the right SQL type
11704        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11705        // emitter knows which `SqlValue` variant to match.
11706        let kind = match key_ty {
11707            Some(k) => detect_type(k)?.kind,
11708            None => DetectedKind::I64,
11709        };
11710        return Ok(DetectedType {
11711            kind,
11712            nullable: false,
11713            auto: false,
11714            fk_inner: Some(inner),
11715        });
11716    }
11717
11718    let kind = match last.ident.to_string().as_str() {
11719        "i16" => DetectedKind::I16,
11720        "i32" => DetectedKind::I32,
11721        "i64" => DetectedKind::I64,
11722        "f32" => DetectedKind::F32,
11723        "f64" => DetectedKind::F64,
11724        "bool" => DetectedKind::Bool,
11725        "String" => DetectedKind::String,
11726        "DateTime" => DetectedKind::DateTime,
11727        "NaiveDate" => DetectedKind::Date,
11728        "NaiveTime" => DetectedKind::Time,
11729        "Uuid" => DetectedKind::Uuid,
11730        "Value" => DetectedKind::Json,
11731        "Decimal" => DetectedKind::Decimal,
11732        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11733        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11734        // — that would emit Binary DDL and decode-fail at runtime.
11735        "Vec" => {
11736            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11737            if let Type::Path(TypePath { path, qself: None }) = inner {
11738                if let Some(seg) = path.segments.last() {
11739                    if seg.ident == "u8" && seg.arguments.is_empty() {
11740                        return Ok(DetectedType {
11741                            kind: DetectedKind::Binary,
11742                            nullable: false,
11743                            auto: false,
11744                            fk_inner: None,
11745                        });
11746                    }
11747                }
11748            }
11749            return Err(syn::Error::new_spanned(
11750                ty,
11751                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11752                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11753            ));
11754        }
11755        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11756        // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11757        "Array" => {
11758            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11759            let elem = match inner {
11760                Type::Path(TypePath { path, qself: None }) => {
11761                    path.segments.last().map(|s| s.ident.to_string())
11762                }
11763                _ => None,
11764            };
11765            let kind = match elem.as_deref() {
11766                Some("String") => DetectedKind::ArrayText,
11767                Some("i32") => DetectedKind::ArrayInt,
11768                Some("i64") => DetectedKind::ArrayBigInt,
11769                _ => {
11770                    return Err(syn::Error::new_spanned(
11771                        ty,
11772                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11773                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11774                    ));
11775                }
11776            };
11777            return Ok(DetectedType {
11778                kind,
11779                nullable: false,
11780                auto: false,
11781                fk_inner: None,
11782            });
11783        }
11784        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11785        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11786        // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11787        // `RangeField` family, #343).
11788        "Range" => {
11789            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11790            let elem = match inner {
11791                Type::Path(TypePath { path, qself: None }) => {
11792                    path.segments.last().map(|s| s.ident.to_string())
11793                }
11794                _ => None,
11795            };
11796            let kind = match elem.as_deref() {
11797                Some("i32") => DetectedKind::RangeInt,
11798                Some("i64") => DetectedKind::RangeBigInt,
11799                Some("Decimal") => DetectedKind::RangeNumeric,
11800                Some("NaiveDate") => DetectedKind::RangeDate,
11801                Some("DateTime") => DetectedKind::RangeDateTime,
11802                _ => {
11803                    return Err(syn::Error::new_spanned(
11804                        ty,
11805                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11806                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11807                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11808                         (→ tstzrange) are supported (#343)",
11809                    ));
11810                }
11811            };
11812            return Ok(DetectedType {
11813                kind,
11814                nullable: false,
11815                auto: false,
11816                fk_inner: None,
11817            });
11818        }
11819        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11820        // (the `CastValue` impl bridges logical↔stored); the field's own
11821        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11822        // schema just needs `FieldType::String`.
11823        "Cast" => {
11824            return Ok(DetectedType {
11825                kind: DetectedKind::String,
11826                nullable: false,
11827                auto: false,
11828                fk_inner: None,
11829            });
11830        }
11831        // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11832        // parameter — always a string→string map.
11833        "HStore" => {
11834            return Ok(DetectedType {
11835                kind: DetectedKind::HStore,
11836                nullable: false,
11837                auto: false,
11838                fk_inner: None,
11839            });
11840        }
11841        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11842        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11843        "Vector" => {
11844            return Ok(DetectedType {
11845                kind: DetectedKind::Vector,
11846                nullable: false,
11847                auto: false,
11848                fk_inner: None,
11849            });
11850        }
11851        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11852        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11853        "Point" => {
11854            return Ok(DetectedType {
11855                kind: DetectedKind::Geometry,
11856                nullable: false,
11857                auto: false,
11858                fk_inner: None,
11859            });
11860        }
11861        other => {
11862            return Err(syn::Error::new_spanned(
11863                ty,
11864                format!("unsupported field type `{other}`; supports i16/i32/i64/f32/f64/bool/String/DateTime/NaiveDate/NaiveTime/Uuid/serde_json::Value/Decimal/Vec<u8>, optionally wrapped in Option or Auto (Auto only on integers/Uuid/DateTime)"),
11865            ));
11866        }
11867    };
11868    Ok(DetectedType {
11869        kind,
11870        nullable: false,
11871        auto: false,
11872        fk_inner: None,
11873    })
11874}
11875
11876fn generic_inner<'a>(
11877    ty: &'a Type,
11878    arguments: &'a PathArguments,
11879    wrapper: &str,
11880) -> syn::Result<&'a Type> {
11881    let PathArguments::AngleBracketed(args) = arguments else {
11882        return Err(syn::Error::new_spanned(
11883            ty,
11884            format!("{wrapper} requires a generic argument"),
11885        ));
11886    };
11887    args.args
11888        .iter()
11889        .find_map(|a| match a {
11890            GenericArgument::Type(t) => Some(t),
11891            _ => None,
11892        })
11893        .ok_or_else(|| {
11894            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11895        })
11896}
11897
11898/// Like [`generic_inner`] but pulls *two* type args — the first is
11899/// required, the second is optional. Used by the `ForeignKey<T, K>`
11900/// detection where K defaults to `i64` when omitted.
11901fn generic_pair<'a>(
11902    ty: &'a Type,
11903    arguments: &'a PathArguments,
11904    wrapper: &str,
11905) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11906    let PathArguments::AngleBracketed(args) = arguments else {
11907        return Err(syn::Error::new_spanned(
11908            ty,
11909            format!("{wrapper} requires a generic argument"),
11910        ));
11911    };
11912    let mut types = args.args.iter().filter_map(|a| match a {
11913        GenericArgument::Type(t) => Some(t),
11914        _ => None,
11915    });
11916    let first = types.next().ok_or_else(|| {
11917        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11918    })?;
11919    let second = types.next();
11920    Ok((first, second))
11921}
11922
11923fn to_snake_case(s: &str) -> String {
11924    let mut out = String::with_capacity(s.len() + 4);
11925    for (i, ch) in s.chars().enumerate() {
11926        if ch.is_ascii_uppercase() {
11927            if i > 0 {
11928                out.push('_');
11929            }
11930            out.push(ch.to_ascii_lowercase());
11931        } else {
11932            out.push(ch);
11933        }
11934    }
11935    out
11936}
11937
11938// ============================================================
11939//  #[derive(Form)]  —  slice 8.4B
11940// ============================================================
11941
11942/// Per-field `#[form(...)]` attributes recognised by the derive.
11943#[derive(Default)]
11944struct FormFieldAttrs {
11945    min: Option<i64>,
11946    max: Option<i64>,
11947    min_length: Option<u32>,
11948    max_length: Option<u32>,
11949    /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11950    /// The named static method on the form struct is called after the
11951    /// field's typed parse + length/range checks; it gets the parsed
11952    /// value by reference and returns `Result<<FieldType>, String>`.
11953    /// On Ok, the returned value replaces the parsed one; on Err, the
11954    /// message is attached to the field error list. Issue #372.
11955    clean: Option<syn::Ident>,
11956}
11957
11958/// Container-level `#[form(...)]` attributes. Currently only the
11959/// Django-shape cross-field `validate` hook (issue #373).
11960#[derive(Default)]
11961struct FormContainerAttrs {
11962    /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11963    /// After every per-field parse succeeds, the named method on the
11964    /// form struct is called with `&self` and may return
11965    /// `Result<(), FormErrors>`. Errors merge into the field error
11966    /// list. Issue #373.
11967    validate: Option<syn::Ident>,
11968}
11969
11970/// Detected shape of a form field's Rust type.
11971#[derive(Clone, Copy)]
11972enum FormFieldKind {
11973    String,
11974    I16,
11975    I32,
11976    I64,
11977    F32,
11978    F64,
11979    Bool,
11980}
11981
11982impl FormFieldKind {
11983    fn parse_method(self) -> &'static str {
11984        match self {
11985            Self::I16 => "i16",
11986            Self::I32 => "i32",
11987            Self::I64 => "i64",
11988            Self::F32 => "f32",
11989            Self::F64 => "f64",
11990            // String + Bool don't go through `str::parse`; the codegen
11991            // handles them inline.
11992            Self::String | Self::Bool => "",
11993        }
11994    }
11995}
11996
11997fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11998    let root = rustango_root();
11999    let struct_name = &input.ident;
12000
12001    let Data::Struct(data) = &input.data else {
12002        return Err(syn::Error::new_spanned(
12003            struct_name,
12004            "Form can only be derived on structs",
12005        ));
12006    };
12007    let Fields::Named(named) = &data.fields else {
12008        return Err(syn::Error::new_spanned(
12009            struct_name,
12010            "Form requires a struct with named fields",
12011        ));
12012    };
12013
12014    // #373 — container-level `#[form(validate = "fn")]` hook.
12015    let container = parse_form_container_attrs(input)?;
12016    let post_field_clean: Vec<TokenStream2> = Vec::new();
12017    let _ = post_field_clean;
12018
12019    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12020    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12021
12022    for field in &named.named {
12023        let ident = field
12024            .ident
12025            .as_ref()
12026            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12027        let attrs = parse_form_field_attrs(field)?;
12028        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12029
12030        let name_lit = ident.to_string();
12031        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12032        // #372 — append the per-field `clean_<field>` call right after
12033        // the parse block when the attribute is set. The clean fn
12034        // takes &T and returns Result<T, String>; on Err we attach
12035        // the message to the field error list without aborting
12036        // (matches Django's "collect all field errors" shape).
12037        let clean_block = if let Some(clean_fn) = &attrs.clean {
12038            quote! {
12039                if __errors.fields().get(#name_lit).is_none() {
12040                    match Self::#clean_fn(&#ident) {
12041                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12042                        ::core::result::Result::Err(__msg) => {
12043                            __errors.add(#name_lit, __msg);
12044                        }
12045                    }
12046                }
12047            }
12048        } else {
12049            quote! {}
12050        };
12051        field_blocks.push(quote! {
12052            #parse_block
12053            #clean_block
12054        });
12055        field_idents.push(ident);
12056    }
12057
12058    // #373 — after every per-field parse + clean succeeds, call the
12059    // cross-field validator if declared. Errors merge into the
12060    // outgoing FormErrors via the existing `FormErrors::merge` helper
12061    // (same primitive the DRF serializer cross-field hook uses).
12062    let cross_field_call = if let Some(validate_fn) = &container.validate {
12063        quote! {
12064            if __errors.is_empty() {
12065                let __candidate = Self { #( #field_idents ),* };
12066                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12067                    __errors.merge(__other);
12068                }
12069                if !__errors.is_empty() {
12070                    return ::core::result::Result::Err(__errors);
12071                }
12072                return ::core::result::Result::Ok(__candidate);
12073            }
12074        }
12075    } else {
12076        quote! {}
12077    };
12078
12079    Ok(quote! {
12080        impl #root::forms::Form for #struct_name {
12081            fn parse(
12082                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12083            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12084                let mut __errors = #root::forms::FormErrors::default();
12085                #( #field_blocks )*
12086                #cross_field_call
12087                if !__errors.is_empty() {
12088                    return ::core::result::Result::Err(__errors);
12089                }
12090                ::core::result::Result::Ok(Self {
12091                    #( #field_idents ),*
12092                })
12093            }
12094        }
12095    })
12096}
12097
12098fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12099    let mut out = FormContainerAttrs::default();
12100    for attr in &input.attrs {
12101        if !attr.path().is_ident("form") {
12102            continue;
12103        }
12104        attr.parse_nested_meta(|meta| {
12105            if meta.path.is_ident("validate") {
12106                let s: LitStr = meta.value()?.parse()?;
12107                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12108                return Ok(());
12109            }
12110            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12111        })?;
12112    }
12113    Ok(out)
12114}
12115
12116fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12117    let mut out = FormFieldAttrs::default();
12118    for attr in &field.attrs {
12119        if !attr.path().is_ident("form") {
12120            continue;
12121        }
12122        attr.parse_nested_meta(|meta| {
12123            if meta.path.is_ident("min") {
12124                let lit: syn::LitInt = meta.value()?.parse()?;
12125                out.min = Some(lit.base10_parse::<i64>()?);
12126                return Ok(());
12127            }
12128            if meta.path.is_ident("max") {
12129                let lit: syn::LitInt = meta.value()?.parse()?;
12130                out.max = Some(lit.base10_parse::<i64>()?);
12131                return Ok(());
12132            }
12133            if meta.path.is_ident("min_length") {
12134                let lit: syn::LitInt = meta.value()?.parse()?;
12135                out.min_length = Some(lit.base10_parse::<u32>()?);
12136                return Ok(());
12137            }
12138            if meta.path.is_ident("max_length") {
12139                let lit: syn::LitInt = meta.value()?.parse()?;
12140                out.max_length = Some(lit.base10_parse::<u32>()?);
12141                return Ok(());
12142            }
12143            if meta.path.is_ident("clean") {
12144                let s: LitStr = meta.value()?.parse()?;
12145                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12146                return Ok(());
12147            }
12148            Err(meta.error(
12149                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12150            ))
12151        })?;
12152    }
12153    Ok(out)
12154}
12155
12156fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12157    let Type::Path(TypePath { path, qself: None }) = ty else {
12158        return Err(syn::Error::new(
12159            span,
12160            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12161        ));
12162    };
12163    let last = path
12164        .segments
12165        .last()
12166        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12167
12168    if last.ident == "Option" {
12169        let inner = generic_inner(ty, &last.arguments, "Option")?;
12170        let (kind, nested) = detect_form_field(inner, span)?;
12171        if nested {
12172            return Err(syn::Error::new(
12173                span,
12174                "nested Option in Form fields is not supported",
12175            ));
12176        }
12177        return Ok((kind, true));
12178    }
12179
12180    let kind = match last.ident.to_string().as_str() {
12181        "String" => FormFieldKind::String,
12182        "i16" => FormFieldKind::I16,
12183        "i32" => FormFieldKind::I32,
12184        "i64" => FormFieldKind::I64,
12185        "f32" => FormFieldKind::F32,
12186        "f64" => FormFieldKind::F64,
12187        "bool" => FormFieldKind::Bool,
12188        other => {
12189            return Err(syn::Error::new(
12190                span,
12191                format!(
12192                    "Form field type `{other}` is not supported in v0.8 — use String / \
12193                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12194                ),
12195            ));
12196        }
12197    };
12198    Ok((kind, false))
12199}
12200
12201#[allow(clippy::too_many_lines)]
12202fn render_form_field_parse(
12203    ident: &syn::Ident,
12204    name_lit: &str,
12205    kind: FormFieldKind,
12206    nullable: bool,
12207    attrs: &FormFieldAttrs,
12208) -> TokenStream2 {
12209    // Pull the raw &str from the payload. Uses variable name `data` to
12210    // match the new `Form::parse(data: &HashMap<…>)` signature.
12211    let lookup = quote! {
12212        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12213    };
12214
12215    let parsed_value = match kind {
12216        FormFieldKind::Bool => quote! {
12217            let __v: bool = match __raw {
12218                ::core::option::Option::None => false,
12219                ::core::option::Option::Some(__s) => !matches!(
12220                    __s.to_ascii_lowercase().as_str(),
12221                    "" | "false" | "0" | "off" | "no"
12222                ),
12223            };
12224        },
12225        FormFieldKind::String => {
12226            if nullable {
12227                quote! {
12228                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12229                        ::core::option::Option::None => ::core::option::Option::None,
12230                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12231                            ::core::option::Option::None
12232                        }
12233                        ::core::option::Option::Some(__s) => {
12234                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12235                        }
12236                    };
12237                }
12238            } else {
12239                quote! {
12240                    let __v: ::std::string::String = match __raw {
12241                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12242                            ::core::clone::Clone::clone(__s)
12243                        }
12244                        _ => {
12245                            __errors.add(#name_lit, "This field is required.");
12246                            ::std::string::String::new()
12247                        }
12248                    };
12249                }
12250            }
12251        }
12252        FormFieldKind::I16
12253        | FormFieldKind::I32
12254        | FormFieldKind::I64
12255        | FormFieldKind::F32
12256        | FormFieldKind::F64 => {
12257            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12258            let ty_lit = kind.parse_method();
12259            let default_val = match kind {
12260                FormFieldKind::I16 => quote! { 0i16 },
12261                FormFieldKind::I32 => quote! { 0i32 },
12262                FormFieldKind::I64 => quote! { 0i64 },
12263                FormFieldKind::F32 => quote! { 0f32 },
12264                FormFieldKind::F64 => quote! { 0f64 },
12265                _ => quote! { Default::default() },
12266            };
12267            if nullable {
12268                quote! {
12269                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12270                        ::core::option::Option::None => ::core::option::Option::None,
12271                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12272                            ::core::option::Option::None
12273                        }
12274                        ::core::option::Option::Some(__s) => {
12275                            match __s.parse::<#parse_ty>() {
12276                                ::core::result::Result::Ok(__n) => {
12277                                    ::core::option::Option::Some(__n)
12278                                }
12279                                ::core::result::Result::Err(__e) => {
12280                                    __errors.add(
12281                                        #name_lit,
12282                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12283                                    );
12284                                    ::core::option::Option::None
12285                                }
12286                            }
12287                        }
12288                    };
12289                }
12290            } else {
12291                quote! {
12292                    let __v: #parse_ty = match __raw {
12293                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12294                            match __s.parse::<#parse_ty>() {
12295                                ::core::result::Result::Ok(__n) => __n,
12296                                ::core::result::Result::Err(__e) => {
12297                                    __errors.add(
12298                                        #name_lit,
12299                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12300                                    );
12301                                    #default_val
12302                                }
12303                            }
12304                        }
12305                        _ => {
12306                            __errors.add(#name_lit, "This field is required.");
12307                            #default_val
12308                        }
12309                    };
12310                }
12311            }
12312        }
12313    };
12314
12315    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12316
12317    quote! {
12318        // `mut` so the per-field `clean` hook (#372) can rewrite the
12319        // parsed value in-place when it returns Ok with a normalized
12320        // form (e.g. trim / lowercase).
12321        let mut #ident = {
12322            #lookup
12323            #parsed_value
12324            #validators
12325            __v
12326        };
12327    }
12328}
12329
12330fn render_form_validators(
12331    name_lit: &str,
12332    kind: FormFieldKind,
12333    nullable: bool,
12334    attrs: &FormFieldAttrs,
12335) -> TokenStream2 {
12336    let mut checks: Vec<TokenStream2> = Vec::new();
12337
12338    let val_ref = if nullable {
12339        quote! { __v.as_ref() }
12340    } else {
12341        quote! { ::core::option::Option::Some(&__v) }
12342    };
12343
12344    let is_string = matches!(kind, FormFieldKind::String);
12345    let is_numeric = matches!(
12346        kind,
12347        FormFieldKind::I16
12348            | FormFieldKind::I32
12349            | FormFieldKind::I64
12350            | FormFieldKind::F32
12351            | FormFieldKind::F64
12352    );
12353
12354    if is_string {
12355        if let Some(min_len) = attrs.min_length {
12356            let min_len_usize = min_len as usize;
12357            checks.push(quote! {
12358                if let ::core::option::Option::Some(__s) = #val_ref {
12359                    if __s.len() < #min_len_usize {
12360                        __errors.add(
12361                            #name_lit,
12362                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12363                        );
12364                    }
12365                }
12366            });
12367        }
12368        if let Some(max_len) = attrs.max_length {
12369            let max_len_usize = max_len as usize;
12370            checks.push(quote! {
12371                if let ::core::option::Option::Some(__s) = #val_ref {
12372                    if __s.len() > #max_len_usize {
12373                        __errors.add(
12374                            #name_lit,
12375                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12376                        );
12377                    }
12378                }
12379            });
12380        }
12381    }
12382
12383    if is_numeric {
12384        if let Some(min) = attrs.min {
12385            checks.push(quote! {
12386                if let ::core::option::Option::Some(__n) = #val_ref {
12387                    if (*__n as f64) < (#min as f64) {
12388                        __errors.add(
12389                            #name_lit,
12390                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12391                        );
12392                    }
12393                }
12394            });
12395        }
12396        if let Some(max) = attrs.max {
12397            checks.push(quote! {
12398                if let ::core::option::Option::Some(__n) = #val_ref {
12399                    if (*__n as f64) > (#max as f64) {
12400                        __errors.add(
12401                            #name_lit,
12402                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12403                        );
12404                    }
12405                }
12406            });
12407        }
12408    }
12409
12410    quote! { #( #checks )* }
12411}
12412
12413// ============================================================
12414//  #[derive(ViewSet)]
12415// ============================================================
12416
12417struct ViewSetAttrs {
12418    model: syn::Path,
12419    fields: Option<Vec<String>>,
12420    filter_fields: Vec<String>,
12421    search_fields: Vec<String>,
12422    /// (field_name, desc)
12423    ordering: Vec<(String, bool)>,
12424    page_size: Option<usize>,
12425    read_only: bool,
12426    perms: ViewSetPermsAttrs,
12427    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12428    /// retrieve / create responses through this `#[derive(Serializer)]`
12429    /// type instead of the default field-level projection (requires the
12430    /// `serializer` feature). Tri-dialect.
12431    serializer: Option<syn::Path>,
12432}
12433
12434#[derive(Default)]
12435struct ViewSetPermsAttrs {
12436    list: Vec<String>,
12437    retrieve: Vec<String>,
12438    create: Vec<String>,
12439    update: Vec<String>,
12440    destroy: Vec<String>,
12441}
12442
12443fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12444    let root = rustango_root();
12445    let struct_name = &input.ident;
12446
12447    // Must be a unit struct or an empty named struct.
12448    match &input.data {
12449        Data::Struct(s) => match &s.fields {
12450            Fields::Unit | Fields::Named(_) => {}
12451            Fields::Unnamed(_) => {
12452                return Err(syn::Error::new_spanned(
12453                    struct_name,
12454                    "ViewSet can only be derived on a unit struct or an empty named struct",
12455                ));
12456            }
12457        },
12458        _ => {
12459            return Err(syn::Error::new_spanned(
12460                struct_name,
12461                "ViewSet can only be derived on a struct",
12462            ));
12463        }
12464    }
12465
12466    let attrs = parse_viewset_attrs(input)?;
12467    let model_path = &attrs.model;
12468
12469    // `.fields(&[...])` call — None means skip (use all scalar fields).
12470    let fields_call = if let Some(ref fields) = attrs.fields {
12471        let lits = fields.iter().map(|f| f.as_str());
12472        quote!(.fields(&[ #(#lits),* ]))
12473    } else {
12474        quote!()
12475    };
12476
12477    let filter_fields_call = if attrs.filter_fields.is_empty() {
12478        quote!()
12479    } else {
12480        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12481        quote!(.filter_fields(&[ #(#lits),* ]))
12482    };
12483
12484    let search_fields_call = if attrs.search_fields.is_empty() {
12485        quote!()
12486    } else {
12487        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12488        quote!(.search_fields(&[ #(#lits),* ]))
12489    };
12490
12491    let ordering_call = if attrs.ordering.is_empty() {
12492        quote!()
12493    } else {
12494        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12495            let f = f.as_str();
12496            quote!((#f, #desc))
12497        });
12498        quote!(.ordering(&[ #(#pairs),* ]))
12499    };
12500
12501    let page_size_call = if let Some(n) = attrs.page_size {
12502        quote!(.page_size(#n))
12503    } else {
12504        quote!()
12505    };
12506
12507    let read_only_call = if attrs.read_only {
12508        quote!(.read_only())
12509    } else {
12510        quote!()
12511    };
12512
12513    // `.serializer::<S>()` — reshape responses through a derived
12514    // serializer. Requires the downstream crate to enable the
12515    // `serializer` feature (the method is gated on it).
12516    let serializer_call = if let Some(ref ser) = attrs.serializer {
12517        quote!(.serializer::<#ser>())
12518    } else {
12519        quote!()
12520    };
12521
12522    let perms = &attrs.perms;
12523    let perms_call = if perms.list.is_empty()
12524        && perms.retrieve.is_empty()
12525        && perms.create.is_empty()
12526        && perms.update.is_empty()
12527        && perms.destroy.is_empty()
12528    {
12529        quote!()
12530    } else {
12531        let list_lits = perms.list.iter().map(|s| s.as_str());
12532        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12533        let create_lits = perms.create.iter().map(|s| s.as_str());
12534        let update_lits = perms.update.iter().map(|s| s.as_str());
12535        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12536        quote! {
12537            .permissions(#root::viewset::ViewSetPerms {
12538                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12539                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12540                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12541                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12542                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12543            })
12544        }
12545    };
12546
12547    Ok(quote! {
12548        impl #struct_name {
12549            /// Build an `axum::Router` with the six standard REST endpoints
12550            /// for this ViewSet, mounted at `prefix`.
12551            pub fn router(prefix: &str, pool: #root::sql::sqlx::PgPool) -> #root::__axum::Router {
12552                #root::viewset::ViewSet::for_model(
12553                    <#model_path as #root::core::Model>::SCHEMA
12554                )
12555                    #fields_call
12556                    #filter_fields_call
12557                    #search_fields_call
12558                    #ordering_call
12559                    #page_size_call
12560                    #perms_call
12561                    #read_only_call
12562                    #serializer_call
12563                    .router(prefix, pool)
12564            }
12565        }
12566    })
12567}
12568
12569fn parse_viewset_attrs(input: &DeriveInput) -> syn::Result<ViewSetAttrs> {
12570    let mut model: Option<syn::Path> = None;
12571    let mut fields: Option<Vec<String>> = None;
12572    let mut filter_fields: Vec<String> = Vec::new();
12573    let mut search_fields: Vec<String> = Vec::new();
12574    let mut ordering: Vec<(String, bool)> = Vec::new();
12575    let mut page_size: Option<usize> = None;
12576    let mut read_only = false;
12577    let mut perms = ViewSetPermsAttrs::default();
12578    let mut serializer: Option<syn::Path> = None;
12579
12580    for attr in &input.attrs {
12581        if !attr.path().is_ident("viewset") {
12582            continue;
12583        }
12584        attr.parse_nested_meta(|meta| {
12585            if meta.path.is_ident("model") {
12586                let path: syn::Path = meta.value()?.parse()?;
12587                model = Some(path);
12588                return Ok(());
12589            }
12590            if meta.path.is_ident("serializer") {
12591                let path: syn::Path = meta.value()?.parse()?;
12592                serializer = Some(path);
12593                return Ok(());
12594            }
12595            if meta.path.is_ident("fields") {
12596                let s: LitStr = meta.value()?.parse()?;
12597                fields = Some(split_field_list(&s.value()));
12598                return Ok(());
12599            }
12600            if meta.path.is_ident("filter_fields") {
12601                let s: LitStr = meta.value()?.parse()?;
12602                filter_fields = split_field_list(&s.value());
12603                return Ok(());
12604            }
12605            if meta.path.is_ident("search_fields") {
12606                let s: LitStr = meta.value()?.parse()?;
12607                search_fields = split_field_list(&s.value());
12608                return Ok(());
12609            }
12610            if meta.path.is_ident("ordering") {
12611                let s: LitStr = meta.value()?.parse()?;
12612                ordering = parse_ordering_list(&s.value());
12613                return Ok(());
12614            }
12615            if meta.path.is_ident("page_size") {
12616                let lit: syn::LitInt = meta.value()?.parse()?;
12617                page_size = Some(lit.base10_parse::<usize>()?);
12618                return Ok(());
12619            }
12620            if meta.path.is_ident("read_only") {
12621                read_only = true;
12622                return Ok(());
12623            }
12624            if meta.path.is_ident("permissions") {
12625                meta.parse_nested_meta(|inner| {
12626                    let parse_codenames = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<Vec<String>> {
12627                        let s: LitStr = inner.value()?.parse()?;
12628                        Ok(split_field_list(&s.value()))
12629                    };
12630                    if inner.path.is_ident("list") {
12631                        perms.list = parse_codenames(&inner)?;
12632                    } else if inner.path.is_ident("retrieve") {
12633                        perms.retrieve = parse_codenames(&inner)?;
12634                    } else if inner.path.is_ident("create") {
12635                        perms.create = parse_codenames(&inner)?;
12636                    } else if inner.path.is_ident("update") {
12637                        perms.update = parse_codenames(&inner)?;
12638                    } else if inner.path.is_ident("destroy") {
12639                        perms.destroy = parse_codenames(&inner)?;
12640                    } else {
12641                        return Err(inner.error(
12642                            "unknown permissions key (supported: list, retrieve, create, update, destroy)",
12643                        ));
12644                    }
12645                    Ok(())
12646                })?;
12647                return Ok(());
12648            }
12649            Err(meta.error(
12650                "unknown viewset attribute (supported: model, fields, filter_fields, \
12651                 search_fields, ordering, page_size, read_only, serializer, permissions(...))",
12652            ))
12653        })?;
12654    }
12655
12656    let model = model.ok_or_else(|| {
12657        syn::Error::new_spanned(&input.ident, "`#[viewset(model = SomeModel)]` is required")
12658    })?;
12659
12660    Ok(ViewSetAttrs {
12661        model,
12662        fields,
12663        filter_fields,
12664        search_fields,
12665        ordering,
12666        page_size,
12667        read_only,
12668        perms,
12669        serializer,
12670    })
12671}
12672
12673// ============================================================ #[derive(Serializer)]
12674
12675struct SerializerContainerAttrs {
12676    model: syn::Path,
12677    /// `#[serializer(validate = "fn_name")]` on the struct — DRF-shape
12678    /// cross-field validation hook (#436). The named inherent method
12679    /// must take `&self` and return
12680    /// `Result<(), rustango::forms::FormErrors>`. The macro-emitted
12681    /// `validate()` runs every per-field validator first; then calls
12682    /// the cross-field method if declared; aggregates all errors into
12683    /// one `FormErrors`.
12684    cross_validate: Option<syn::Ident>,
12685}
12686
12687#[derive(Default)]
12688struct SerializerFieldAttrs {
12689    read_only: bool,
12690    write_only: bool,
12691    source: Option<String>,
12692    skip: bool,
12693    /// `#[serializer(method = "fn_name")]` — DRF SerializerMethodField
12694    /// analog. The macro emits `from_model` initializer that calls
12695    /// `Self::fn_name(&model)` and stores the return value.
12696    method: Option<String>,
12697    /// `#[serializer(validate = "fn_name")]` — per-field validator
12698    /// callable run by `Self::validate(&self)`. Must return
12699    /// `Result<(), String>`. Errors land in `FormErrors` keyed by
12700    /// the field name.
12701    validate: Option<String>,
12702    /// `#[serializer(nested)]` on a field whose type is another
12703    /// `Serializer` — the macro emits `from_model` initializer that
12704    /// reads the parent via `model.<source>.value()` then calls the
12705    /// child serializer's `from_model(parent)`. When the FK is
12706    /// unloaded the field falls back to `Default::default()` (does
12707    /// NOT panic) so a missing prefetch in prod degrades gracefully.
12708    /// Source field on the model defaults to the field name; override
12709    /// with `source = "..."`. Combine with `strict` to keep the v0.18.1
12710    /// panic-on-unloaded behavior for tests.
12711    nested: bool,
12712    /// `#[serializer(nested, strict)]` — opt back into the v0.18.1
12713    /// strict behavior: panic when the FK isn't loaded. Useful in
12714    /// test code where forgetting select_related must trip a hard
12715    /// failure rather than render a blank nested object.
12716    nested_strict: bool,
12717    /// `#[serializer(many = TagSerializer)]` — declare the field as
12718    /// a list of nested serializers. Field type must be `Vec<S>`
12719    /// where `S` is the inner serializer. The macro initializes the
12720    /// field to `Vec::new()` in `from_model` and emits a typed
12721    /// `set_<field>(&mut self, models: &[<S::Model>])` helper that
12722    /// maps each model row through `S::from_model`. Auto-load isn't
12723    /// possible (the M2M / one-to-many accessor is async); callers
12724    /// fetch the children + call the setter post-from_model.
12725    many: Option<syn::Type>,
12726    /// `#[serializer(slug = "name")]` — DRF `SlugRelatedField` analog.
12727    /// Source field on the model must be a `ForeignKey<T>`; the
12728    /// macro emits `from_model` glue that walks
12729    /// `model.<source>.value()?.<slug>` and clones it. Field type on
12730    /// the serializer is typically `String` (whatever type the slug
12731    /// column has). When the FK is unloaded the field falls back to
12732    /// `Default::default()`, same graceful-degrade contract as
12733    /// `nested`. Source defaults to the field name; override with
12734    /// `source = "..."`. v0.44.
12735    slug: Option<String>,
12736    /// `#[serializer(max_length = N)]` — DRF `MaxLengthValidator`. Caps
12737    /// the character count of a string field on write. Overrides the
12738    /// model's `max_length`; when absent the model value is inherited.
12739    max_length: Option<u64>,
12740    /// `#[serializer(min_length = N)]` — DRF `MinLengthValidator`.
12741    /// Serializer-only (the model has no `min_length` column).
12742    min_length: Option<u64>,
12743    /// `#[serializer(min = N)]` — DRF `MinValueValidator`. Inclusive
12744    /// integer lower bound; overrides the model's `min` when given.
12745    min: Option<i64>,
12746    /// `#[serializer(max = N)]` — DRF `MaxValueValidator`. Inclusive
12747    /// integer upper bound; overrides the model's `max` when given.
12748    max: Option<i64>,
12749}
12750
12751fn parse_serializer_container_attrs(input: &DeriveInput) -> syn::Result<SerializerContainerAttrs> {
12752    let mut model: Option<syn::Path> = None;
12753    let mut cross_validate: Option<syn::Ident> = None;
12754    for attr in &input.attrs {
12755        if !attr.path().is_ident("serializer") {
12756            continue;
12757        }
12758        attr.parse_nested_meta(|meta| {
12759            if meta.path.is_ident("model") {
12760                let _eq: syn::Token![=] = meta.input.parse()?;
12761                model = Some(meta.input.parse()?);
12762                return Ok(());
12763            }
12764            if meta.path.is_ident("validate") {
12765                // #436 — container-level `validate = "fn_name"` for the
12766                // DRF cross-field-validation shape. Field-level
12767                // `#[serializer(validate = "...")]` on a field is
12768                // parsed separately in `parse_serializer_field_attrs`.
12769                let s: LitStr = meta.value()?.parse()?;
12770                cross_validate = Some(syn::Ident::new(&s.value(), s.span()));
12771                return Ok(());
12772            }
12773            Err(meta.error(
12774                "unknown serializer container attribute \
12775                 (supported: `model`, `validate`)",
12776            ))
12777        })?;
12778    }
12779    let model = model.ok_or_else(|| {
12780        syn::Error::new_spanned(
12781            &input.ident,
12782            "`#[serializer(model = SomeModel)]` is required",
12783        )
12784    })?;
12785    Ok(SerializerContainerAttrs {
12786        model,
12787        cross_validate,
12788    })
12789}
12790
12791fn parse_serializer_field_attrs(field: &syn::Field) -> syn::Result<SerializerFieldAttrs> {
12792    let mut out = SerializerFieldAttrs::default();
12793    for attr in &field.attrs {
12794        if !attr.path().is_ident("serializer") {
12795            continue;
12796        }
12797        attr.parse_nested_meta(|meta| {
12798            if meta.path.is_ident("read_only") {
12799                out.read_only = true;
12800                return Ok(());
12801            }
12802            if meta.path.is_ident("write_only") {
12803                out.write_only = true;
12804                return Ok(());
12805            }
12806            if meta.path.is_ident("skip") {
12807                out.skip = true;
12808                return Ok(());
12809            }
12810            if meta.path.is_ident("source") {
12811                let s: LitStr = meta.value()?.parse()?;
12812                out.source = Some(s.value());
12813                return Ok(());
12814            }
12815            if meta.path.is_ident("method") {
12816                let s: LitStr = meta.value()?.parse()?;
12817                out.method = Some(s.value());
12818                return Ok(());
12819            }
12820            if meta.path.is_ident("validate") {
12821                let s: LitStr = meta.value()?.parse()?;
12822                out.validate = Some(s.value());
12823                return Ok(());
12824            }
12825            if meta.path.is_ident("many") {
12826                let _eq: syn::Token![=] = meta.input.parse()?;
12827                out.many = Some(meta.input.parse()?);
12828                return Ok(());
12829            }
12830            if meta.path.is_ident("nested") {
12831                out.nested = true;
12832                // Optional strict flag inside parentheses:
12833                //   #[serializer(nested(strict))]
12834                if meta.input.peek(syn::token::Paren) {
12835                    meta.parse_nested_meta(|inner| {
12836                        if inner.path.is_ident("strict") {
12837                            out.nested_strict = true;
12838                            return Ok(());
12839                        }
12840                        Err(inner.error("unknown nested sub-attribute (supported: `strict`)"))
12841                    })?;
12842                }
12843                return Ok(());
12844            }
12845            if meta.path.is_ident("slug") {
12846                let s: LitStr = meta.value()?.parse()?;
12847                out.slug = Some(s.value());
12848                return Ok(());
12849            }
12850            if meta.path.is_ident("max_length") {
12851                let lit: syn::LitInt = meta.value()?.parse()?;
12852                out.max_length = Some(lit.base10_parse::<u64>()?);
12853                return Ok(());
12854            }
12855            if meta.path.is_ident("min_length") {
12856                let lit: syn::LitInt = meta.value()?.parse()?;
12857                out.min_length = Some(lit.base10_parse::<u64>()?);
12858                return Ok(());
12859            }
12860            if meta.path.is_ident("min") {
12861                let lit: syn::LitInt = meta.value()?.parse()?;
12862                out.min = Some(lit.base10_parse::<i64>()?);
12863                return Ok(());
12864            }
12865            if meta.path.is_ident("max") {
12866                let lit: syn::LitInt = meta.value()?.parse()?;
12867                out.max = Some(lit.base10_parse::<i64>()?);
12868                return Ok(());
12869            }
12870            Err(meta.error(
12871                "unknown serializer field attribute (supported: \
12872                 `read_only`, `write_only`, `source`, `skip`, `method`, \
12873                 `validate`, `nested`, `many`, `slug`, `max_length`, \
12874                 `min_length`, `min`, `max`)",
12875            ))
12876        })?;
12877    }
12878    // Validate: read_only + write_only is nonsensical
12879    if out.read_only && out.write_only {
12880        return Err(syn::Error::new_spanned(
12881            field,
12882            "a field cannot be both `read_only` and `write_only`",
12883        ));
12884    }
12885    if out.method.is_some() && out.source.is_some() {
12886        return Err(syn::Error::new_spanned(
12887            field,
12888            "`method` and `source` are mutually exclusive — `method` computes \
12889             the value from a method, `source` reads it from a different model field",
12890        ));
12891    }
12892    if out.slug.is_some() && (out.method.is_some() || out.nested || out.many.is_some()) {
12893        return Err(syn::Error::new_spanned(
12894            field,
12895            "`slug` is mutually exclusive with `method`, `nested`, and `many` \
12896             — pick one strategy for populating the field",
12897        ));
12898    }
12899    Ok(out)
12900}
12901
12902fn expand_serializer(input: &DeriveInput) -> syn::Result<TokenStream2> {
12903    let root = rustango_root();
12904    let struct_name = &input.ident;
12905    let struct_name_lit = struct_name.to_string();
12906
12907    let Data::Struct(data) = &input.data else {
12908        return Err(syn::Error::new_spanned(
12909            struct_name,
12910            "Serializer can only be derived on structs",
12911        ));
12912    };
12913    let Fields::Named(named) = &data.fields else {
12914        return Err(syn::Error::new_spanned(
12915            struct_name,
12916            "Serializer requires a struct with named fields",
12917        ));
12918    };
12919
12920    let container = parse_serializer_container_attrs(input)?;
12921    let model_path = &container.model;
12922
12923    // Classify each field. `ty` is only consumed by the
12924    // `#[cfg(feature = "openapi")]` block below, but we always
12925    // capture it to keep the field-info build a single pass.
12926    #[allow(dead_code)]
12927    struct FieldInfo {
12928        ident: syn::Ident,
12929        ty: syn::Type,
12930        attrs: SerializerFieldAttrs,
12931    }
12932    let mut fields_info: Vec<FieldInfo> = Vec::new();
12933    for field in &named.named {
12934        let ident = field.ident.clone().expect("named field has ident");
12935        let attrs = parse_serializer_field_attrs(field)?;
12936        fields_info.push(FieldInfo {
12937            ident,
12938            ty: field.ty.clone(),
12939            attrs,
12940        });
12941    }
12942
12943    // Generate from_model body: struct literal with each field assigned.
12944    let from_model_fields = fields_info.iter().map(|fi| {
12945        let ident = &fi.ident;
12946        let ty = &fi.ty;
12947        if let Some(_inner) = &fi.attrs.many {
12948            // Many — collection field. Initialize empty; caller
12949            // populates via the macro-emitted set_<field> helper
12950            // after fetching the M2M children.
12951            quote! { #ident: ::std::vec::Vec::new() }
12952        } else if let Some(method) = &fi.attrs.method {
12953            // SerializerMethodField: call Self::<method>(&model) to
12954            // compute the value. Method signature must be
12955            // `fn <method>(model: &T) -> <field type>`.
12956            let method_ident = syn::Ident::new(method, ident.span());
12957            quote! { #ident: Self::#method_ident(model) }
12958        } else if let Some(slug_field) = &fi.attrs.slug {
12959            // v0.44 — SlugRelatedField. Source defaults to the field
12960            // name on this struct; override via `source = "..."`. The
12961            // source field on the model is expected to be a
12962            // `ForeignKey<T>`; the slug field on the parent is named
12963            // by the attribute value. When the FK is unloaded the
12964            // field falls back to `Default::default()` — same
12965            // graceful-degrade contract as `nested`.
12966            let src_name = fi
12967                .attrs
12968                .source
12969                .as_deref()
12970                .unwrap_or(&fi.ident.to_string())
12971                .to_owned();
12972            let src_ident = syn::Ident::new(&src_name, ident.span());
12973            let slug_ident = syn::Ident::new(slug_field, ident.span());
12974            quote! {
12975                #ident: match model.#src_ident.value() {
12976                    ::core::option::Option::Some(__loaded) =>
12977                        ::core::clone::Clone::clone(&__loaded.#slug_ident),
12978                    ::core::option::Option::None =>
12979                        ::core::default::Default::default(),
12980                }
12981            }
12982        } else if fi.attrs.nested {
12983            // Nested serializer. Source defaults to the field name on
12984            // this struct; override via `source = "..."`. The source
12985            // field on the model is expected to be a `ForeignKey<T>`
12986            // whose `.value()` returns `Option<&T>` after lazy-load.
12987            //
12988            // Behavior matrix (tweakable per-field):
12989            //   * FK loaded   → nested object materializes via
12990            //                   ChildSerializer::from_model(parent).
12991            //   * FK unloaded → fall back to ChildSerializer::default()
12992            //                   (so prod doesn't crash on a missing
12993            //                   prefetch — just renders a blank nested
12994            //                   object). Add `#[serializer(nested,
12995            //                   strict)]` to keep the v0.18.1
12996            //                   panic-on-unloaded behavior for tests
12997            //                   that want hard guardrails.
12998            let src_name = fi.attrs.source.as_deref().unwrap_or(&fi.ident.to_string()).to_owned();
12999            let src_ident = syn::Ident::new(&src_name, ident.span());
13000            if fi.attrs.nested_strict {
13001                let panic_msg = format!(
13002                    "nested(strict) serializer for `{ident}` requires `model.{src_name}` to be loaded — \
13003                     call .get(&pool).await? or .select_related(\"{src_name}\") on the model first",
13004                );
13005                quote! {
13006                    #ident: <#ty as #root::serializer::ModelSerializer>::from_model(
13007                        model.#src_ident.value().expect(#panic_msg),
13008                    )
13009                }
13010            } else {
13011                quote! {
13012                    #ident: match model.#src_ident.value() {
13013                        ::core::option::Option::Some(__loaded) =>
13014                            <#ty as #root::serializer::ModelSerializer>::from_model(__loaded),
13015                        ::core::option::Option::None =>
13016                            ::core::default::Default::default(),
13017                    }
13018                }
13019            }
13020        } else if fi.attrs.write_only || fi.attrs.skip {
13021            // Not read from model — use default
13022            quote! { #ident: ::core::default::Default::default() }
13023        } else if let Some(src) = &fi.attrs.source {
13024            let src_ident = syn::Ident::new(src, ident.span());
13025            quote! { #ident: ::core::clone::Clone::clone(&model.#src_ident) }
13026        } else {
13027            quote! { #ident: ::core::clone::Clone::clone(&model.#ident) }
13028        }
13029    });
13030
13031    // is_writable predicate (also used by writable_lits /
13032    // writable_source_lits below).
13033    let is_writable = |fi: &&FieldInfo| {
13034        !fi.attrs.read_only
13035            && !fi.attrs.skip
13036            && fi.attrs.method.is_none()
13037            && !fi.attrs.nested
13038            && fi.attrs.many.is_none()
13039            && fi.attrs.slug.is_none()
13040    };
13041
13042    // Declarative field constraints (DRF `validators=[...]`): one block
13043    // per writable field, run inside `validate()`. Each resolves its
13044    // bounds as the serializer attr when given (`#[serializer(max_length
13045    // = N)]`), else the model's `FieldSchema` (`max_length` / `min` /
13046    // `max` / `choices`), then dispatches on the field's JSON value via
13047    // `forms::validators::check_value` (string → length/choices, integer
13048    // → min/max).
13049    let opt_usize = |v: Option<u64>| match v {
13050        Some(n) => {
13051            let n = n as usize;
13052            quote!(::core::option::Option::Some(#n))
13053        }
13054        None => quote!(::core::option::Option::None),
13055    };
13056    let opt_i64 = |v: Option<i64>| match v {
13057        Some(n) => quote!(::core::option::Option::Some(#n)),
13058        None => quote!(::core::option::Option::None),
13059    };
13060    let constraint_blocks: Vec<_> = fields_info
13061        .iter()
13062        .filter(is_writable)
13063        .map(|fi| {
13064            let ident = &fi.ident;
13065            let fname = ident.to_string();
13066            let mname = fi
13067                .attrs
13068                .source
13069                .clone()
13070                .unwrap_or_else(|| fi.ident.to_string());
13071            let attr_max_len = opt_usize(fi.attrs.max_length);
13072            let attr_min_len = opt_usize(fi.attrs.min_length);
13073            let attr_min = opt_i64(fi.attrs.min);
13074            let attr_max = opt_i64(fi.attrs.max);
13075            quote! {
13076                {
13077                    let __sf = <#model_path as #root::core::Model>::SCHEMA.field(#mname);
13078                    let __max_length: ::core::option::Option<usize> = #attr_max_len
13079                        .or_else(|| __sf.and_then(|__f| __f.max_length).map(|__n| __n as usize));
13080                    let __min_length: ::core::option::Option<usize> = #attr_min_len;
13081                    let __min: ::core::option::Option<i64> =
13082                        #attr_min.or_else(|| __sf.and_then(|__f| __f.min));
13083                    let __max: ::core::option::Option<i64> =
13084                        #attr_max.or_else(|| __sf.and_then(|__f| __f.max));
13085                    let __choices = __sf.and_then(|__f| __f.choices);
13086                    let __v = #root::__serde_json::to_value(&self.#ident)
13087                        .unwrap_or(#root::__serde_json::Value::Null);
13088                    #root::forms::validators::check_value(
13089                        #fname, &__v, __max_length, __min_length, __min, __max, __choices,
13090                        &mut __errors,
13091                    );
13092                }
13093            }
13094        })
13095        .collect();
13096    let has_constraints = !constraint_blocks.is_empty();
13097
13098    // Per-field validators (DRF-shape `validators=[...]`). Emit a
13099    // `validate(&self)` method that runs each user-defined validator
13100    // and aggregates errors into `FormErrors`.
13101    let validator_calls: Vec<_> = fields_info
13102        .iter()
13103        .filter_map(|fi| {
13104            let ident = &fi.ident;
13105            let name_lit = ident.to_string();
13106            let method = fi.attrs.validate.as_ref()?;
13107            let method_ident = syn::Ident::new(method, ident.span());
13108            Some(quote! {
13109                if let ::core::result::Result::Err(__e) = Self::#method_ident(&self.#ident) {
13110                    __errors.add(#name_lit.to_owned(), __e);
13111                }
13112            })
13113        })
13114        .collect();
13115    // #436 — DRF cross-field `validate(self)` shape. If the
13116    // container declared `#[serializer(validate = "fn_name")]`,
13117    // the macro-generated `validate(&self)` runs every per-field
13118    // validator first, then calls the user's cross-field method,
13119    // merging its `FormErrors` into the per-field errors. Either
13120    // alone is enough to emit the wrapper.
13121    let cross_validate_call = container.cross_validate.as_ref().map(|method_ident| {
13122        quote! {
13123            // Merge cross-field errors into the per-field bucket so
13124            // a single .validate() call surfaces both layers.
13125            if let ::core::result::Result::Err(__cross) = self.#method_ident() {
13126                __errors.merge(__cross);
13127            }
13128        }
13129    });
13130    let has_validators = !validator_calls.is_empty() || container.cross_validate.is_some();
13131    // Anything to run? Declarative constraints (for any writable field) +
13132    // per-field validators + cross-field hook.
13133    let has_run_validations = has_validators || has_constraints;
13134    // Shared body: declarative field constraints first (length / range /
13135    // choices, serializer-attr-or-model), then per-field validators, then
13136    // the cross-field hook.
13137    let validate_body = quote! {
13138        let mut __errors = #root::forms::FormErrors::default();
13139        #( #constraint_blocks )*
13140        #( #validator_calls )*
13141        #cross_validate_call
13142        if __errors.is_empty() {
13143            ::core::result::Result::Ok(())
13144        } else {
13145            ::core::result::Result::Err(__errors)
13146        }
13147    };
13148    // Inherent `validate(&self)` — kept for back-compat with direct
13149    // `serializer.validate()` calls that don't import `ModelSerializer`.
13150    // Emitted only when the serializer declares per-field/cross-field
13151    // validators (unchanged rule) so it never collides with a
13152    // hand-written inherent `validate`.
13153    let validate_method = if has_validators {
13154        quote! {
13155            impl #struct_name {
13156                /// Run the declarative field constraints, every
13157                /// `#[serializer(validate = "...")]` per-field validator,
13158                /// and (when declared) the container-level cross-field
13159                /// validator. Aggregates errors into `FormErrors` keyed by
13160                /// the field name. Returns `Ok(())` when all pass.
13161                pub fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13162                    #validate_body
13163                }
13164            }
13165        }
13166    } else {
13167        quote! {}
13168    };
13169    // Trait-method override so the type-erased ViewSet write path can
13170    // dispatch `ModelSerializer::validate` generically. Emitted whenever
13171    // there's anything to run (declarative constraints inherited from the
13172    // model count); otherwise the trait's default no-op is used.
13173    let trait_validate_override = if has_run_validations {
13174        quote! {
13175            fn validate(&self) -> ::core::result::Result<(), #root::forms::FormErrors> {
13176                #validate_body
13177            }
13178        }
13179    } else {
13180        quote! {}
13181    };
13182
13183    // For every `#[serializer(many = S)]` field, emit a
13184    // `pub fn set_<field>(&mut self, models: &[<S::Model>]) -> &mut Self`
13185    // helper that maps the parents through `S::from_model`.
13186    let many_setters: Vec<_> = fields_info
13187        .iter()
13188        .filter_map(|fi| {
13189            let many_ty = fi.attrs.many.as_ref()?;
13190            let ident = &fi.ident;
13191            let setter = syn::Ident::new(&format!("set_{ident}"), ident.span());
13192            Some(quote! {
13193                /// Populate this `many` field by mapping each parent model
13194                /// through the inner serializer's `from_model`. Call after
13195                /// fetching the M2M / one-to-many children since
13196                /// `from_model` itself can't await an SQL query.
13197                pub fn #setter(
13198                    &mut self,
13199                    models: &[<#many_ty as #root::serializer::ModelSerializer>::Model],
13200                ) -> &mut Self {
13201                    self.#ident = models.iter()
13202                        .map(<#many_ty as #root::serializer::ModelSerializer>::from_model)
13203                        .collect();
13204                    self
13205                }
13206            })
13207        })
13208        .collect();
13209    let many_setters_impl = if many_setters.is_empty() {
13210        quote! {}
13211    } else {
13212        quote! {
13213            impl #struct_name {
13214                #( #many_setters )*
13215            }
13216        }
13217    };
13218
13219    // Generate custom Serialize: skip write_only fields
13220    let output_fields: Vec<_> = fields_info
13221        .iter()
13222        .filter(|fi| !fi.attrs.write_only)
13223        .collect();
13224    let output_field_count = output_fields.len();
13225    let serialize_fields = output_fields.iter().map(|fi| {
13226        let ident = &fi.ident;
13227        let name_lit = ident.to_string();
13228        quote! { __state.serialize_field(#name_lit, &self.#ident)?; }
13229    });
13230
13231    // writable_fields: normal + write_only.
13232    // Exclude:
13233    //   - `read_only` — server-computed.
13234    //   - `skip` — caller sets manually post-from_model.
13235    //   - `method` — computed from a Self::fn(&model) call; accepting
13236    //     it on write is meaningless.
13237    //   - `nested` / `many` — populated from related-model data, not
13238    //     from a field on the wire body.
13239    // v0.44 fix: pre-v0.44 the macro included `method` / `nested` /
13240    // `many` in `writable_fields()`, which made the ViewSet write
13241    // path accept those fields from the JSON body and try to bind
13242    // them to the SQL UPDATE — a silent no-op at best, a type
13243    // mismatch at worst.
13244    // (`is_writable` is defined above, near the constraint codegen.)
13245    let writable_lits: Vec<_> = fields_info
13246        .iter()
13247        .filter(is_writable)
13248        .map(|fi| fi.ident.to_string())
13249        .collect();
13250
13251    // `writable_source_fields`: the MODEL field names of writable
13252    // serializer fields (source-resolved). The ViewSet write path skips
13253    // every model column NOT in this set, so `read_only` / `method` /
13254    // computed fields a client posts are ignored instead of written.
13255    let writable_source_lits: Vec<String> = fields_info
13256        .iter()
13257        .filter(is_writable)
13258        .map(|fi| {
13259            fi.attrs
13260                .source
13261                .clone()
13262                .unwrap_or_else(|| fi.ident.to_string())
13263        })
13264        .collect();
13265
13266    // `from_writable_json`: build a partial instance for input
13267    // validation. Writable fields are parsed from the JSON body (keyed
13268    // by serializer field name); every other field defaults. Per-field
13269    // type errors collect into `FormErrors` keyed by the field name.
13270    // Construction is field-by-field (not `Self::default()`) so it works
13271    // for serializers regardless of a struct-level `Default`.
13272    let from_writable_json_inits: Vec<_> = fields_info
13273        .iter()
13274        .map(|fi| {
13275            let ident = &fi.ident;
13276            let fname = ident.to_string();
13277            let ty = &fi.ty;
13278            if is_writable(&fi) {
13279                quote! {
13280                    #ident: match __obj.and_then(|__o| __o.get(#fname)) {
13281                        ::core::option::Option::Some(__v) => {
13282                            match #root::__serde_json::from_value::<#ty>(
13283                                ::core::clone::Clone::clone(__v),
13284                            ) {
13285                                ::core::result::Result::Ok(__x) => __x,
13286                                ::core::result::Result::Err(__e) => {
13287                                    __errors.add(#fname.to_owned(), __e.to_string());
13288                                    ::core::default::Default::default()
13289                                }
13290                            }
13291                        }
13292                        ::core::option::Option::None => ::core::default::Default::default(),
13293                    }
13294                }
13295            } else {
13296                quote! { #ident: ::core::default::Default::default() }
13297            }
13298        })
13299        .collect();
13300
13301    // OpenAPI: emit `impl OpenApiSchema` when our `openapi` feature is on.
13302    // Only includes fields shown in JSON output (skips write_only). For each
13303    // `Option<T>` field, omit from `required` and add `.nullable()`.
13304    let openapi_impl = {
13305        #[cfg(feature = "openapi")]
13306        {
13307            let property_calls = output_fields.iter().map(|fi| {
13308                let ident = &fi.ident;
13309                let name_lit = ident.to_string();
13310                let ty = &fi.ty;
13311                let nullable_call = if is_option(ty) {
13312                    quote! { .nullable() }
13313                } else {
13314                    quote! {}
13315                };
13316                quote! {
13317                    .property(
13318                        #name_lit,
13319                        <#ty as #root::openapi::OpenApiSchema>::openapi_schema()
13320                            #nullable_call,
13321                    )
13322                }
13323            });
13324            let required_lits: Vec<_> = output_fields
13325                .iter()
13326                .filter(|fi| !is_option(&fi.ty))
13327                .map(|fi| fi.ident.to_string())
13328                .collect();
13329            quote! {
13330                impl #root::openapi::OpenApiSchema for #struct_name {
13331                    fn openapi_schema() -> #root::openapi::Schema {
13332                        #root::openapi::Schema::object()
13333                            #( #property_calls )*
13334                            .required([ #( #required_lits ),* ])
13335                    }
13336                }
13337            }
13338        }
13339        #[cfg(not(feature = "openapi"))]
13340        {
13341            quote! {}
13342        }
13343    };
13344
13345    Ok(quote! {
13346        impl #root::serializer::ModelSerializer for #struct_name {
13347            type Model = #model_path;
13348
13349            fn from_model(model: &Self::Model) -> Self {
13350                Self {
13351                    #( #from_model_fields ),*
13352                }
13353            }
13354
13355            fn writable_fields() -> &'static [&'static str] {
13356                &[ #( #writable_lits ),* ]
13357            }
13358
13359            fn writable_source_fields() -> &'static [&'static str] {
13360                &[ #( #writable_source_lits ),* ]
13361            }
13362
13363            fn from_writable_json(
13364                __body: &#root::__serde_json::Value,
13365            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
13366                let mut __errors = #root::forms::FormErrors::default();
13367                let __obj = __body.as_object();
13368                let __out = Self {
13369                    #( #from_writable_json_inits ),*
13370                };
13371                if __errors.is_empty() {
13372                    ::core::result::Result::Ok(__out)
13373                } else {
13374                    ::core::result::Result::Err(__errors)
13375                }
13376            }
13377
13378            #trait_validate_override
13379        }
13380
13381        impl #root::__serde::Serialize for #struct_name {
13382            fn serialize<S>(&self, serializer: S)
13383                -> ::core::result::Result<S::Ok, S::Error>
13384            where
13385                S: #root::__serde::Serializer,
13386            {
13387                use #root::__serde::ser::SerializeStruct;
13388                let mut __state = serializer.serialize_struct(
13389                    #struct_name_lit,
13390                    #output_field_count,
13391                )?;
13392                #( #serialize_fields )*
13393                __state.end()
13394            }
13395        }
13396
13397        #openapi_impl
13398
13399        #validate_method
13400
13401        #many_setters_impl
13402    })
13403}
13404
13405/// Returns true if `ty` looks like `Option<T>` (any path ending in `Option`).
13406/// Only used by the `openapi`-gated emission of `OpenApiSchema`; muted
13407/// when the feature is off.
13408#[cfg_attr(not(feature = "openapi"), allow(dead_code))]
13409fn is_option(ty: &syn::Type) -> bool {
13410    if let syn::Type::Path(p) = ty {
13411        if let Some(last) = p.path.segments.last() {
13412            return last.ident == "Option";
13413        }
13414    }
13415    false
13416}