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        // The escaped-LIKE lookups delegate to the typed Column
436        // methods, which escape the value and pair it with the
437        // *Escaped ops so `%` / `_` match literally on every dialect
438        // (#1257) — the macro no longer owns any pattern wrapping.
439        "iexact" => quote! {
440            #root::core::Column::iexact(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
441        },
442        "contains" => quote! {
443            #root::core::Column::contains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
444        },
445        "icontains" => quote! {
446            #root::core::Column::icontains(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
447        },
448        "startswith" => quote! {
449            #root::core::Column::startswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
450        },
451        "istartswith" => quote! {
452            #root::core::Column::istartswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
453        },
454        "endswith" => quote! {
455            #root::core::Column::endswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
456        },
457        "iendswith" => quote! {
458            #root::core::Column::iendswith(#path::#base_ident, ::std::string::ToString::to_string(&(#value)))
459        },
460        "in" => quote! {
461            #root::core::Column::is_in(#path::#base_ident, #value)
462        },
463        "not_in" => quote! {
464            #root::core::Column::not_in(#path::#base_ident, #value)
465        },
466        "isnull" => {
467            // Must be a bool literal at macro time so we can route to
468            // is_null vs is_not_null without a runtime branch.
469            let b = match value {
470                syn::Expr::Lit(syn::ExprLit {
471                    lit: syn::Lit::Bool(b),
472                    ..
473                }) => b.value(),
474                _ => {
475                    return Err(syn::Error::new_spanned(
476                        value,
477                        "Q!(): `__isnull` requires a `true` or `false` literal",
478                    ));
479                }
480            };
481            if b {
482                quote! { #root::core::Column::is_null(#path::#base_ident) }
483            } else {
484                quote! { #root::core::Column::is_not_null(#path::#base_ident) }
485            }
486        }
487        "between" => {
488            // Accept a tuple literal `(lo, hi)`.
489            let tuple = match value {
490                syn::Expr::Tuple(t) if t.elems.len() == 2 => t,
491                _ => {
492                    return Err(syn::Error::new_spanned(
493                        value,
494                        "Q!(): `__between` requires a tuple literal `(lo, hi)`",
495                    ));
496                }
497            };
498            let lo = &tuple.elems[0];
499            let hi = &tuple.elems[1];
500            quote! { #root::core::Column::between(#path::#base_ident, #lo, #hi) }
501        }
502        "regex" => quote! {
503            #root::core::Column::regex(#path::#base_ident, #value)
504        },
505        "iregex" => quote! {
506            #root::core::Column::iregex(#path::#base_ident, #value)
507        },
508        _ => {
509            return Err(syn::Error::new(
510                field_span,
511                format!(
512                    "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",
513                    suffix
514                ),
515            ));
516        }
517    };
518    Ok(expanded)
519}
520
521fn expand_embed_migrations(input: TokenStream2) -> syn::Result<TokenStream2> {
522    // Default to "./migrations" if invoked without args.
523    let path_str = if input.is_empty() {
524        "./migrations".to_string()
525    } else {
526        let lit: LitStr = syn::parse2(input)?;
527        lit.value()
528    };
529
530    let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| {
531        syn::Error::new(
532            proc_macro2::Span::call_site(),
533            "embed_migrations! must be invoked during a Cargo build (CARGO_MANIFEST_DIR not set)",
534        )
535    })?;
536    let abs = std::path::Path::new(&manifest).join(&path_str);
537
538    let mut entries: Vec<(String, std::path::PathBuf)> = Vec::new();
539    if abs.is_dir() {
540        let read = std::fs::read_dir(&abs).map_err(|e| {
541            syn::Error::new(
542                proc_macro2::Span::call_site(),
543                format!("embed_migrations!: cannot read {}: {e}", abs.display()),
544            )
545        })?;
546        for entry in read.flatten() {
547            let path = entry.path();
548            if !path.is_file() {
549                continue;
550            }
551            if path.extension().and_then(|s| s.to_str()) != Some("json") {
552                continue;
553            }
554            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
555                continue;
556            };
557            entries.push((stem.to_owned(), path));
558        }
559    }
560    entries.sort_by(|a, b| a.0.cmp(&b.0));
561
562    // Compile-time chain validation: read each migration's JSON,
563    // pull `name` and `prev` (file-stem-keyed for the chain check),
564    // and verify every `prev` points to another migration in the
565    // slice. Mismatches between the file stem and the embedded
566    // `name` field — and broken `prev` chains — fail at MACRO
567    // EXPANSION time so a misshapen migration set never compiles.
568    //
569    // This is the v0.4 Slice 5 distinguisher: rustango's JSON
570    // migrations + a Rust proc-macro that reads them is the unique
571    // combo nothing else in the Django-shape Rust camp can match
572    // (Cot's are imperative Rust code, Loco's are SeaORM up/down,
573    // Rwf's are raw SQL — none have a static chain to validate).
574    let mut chain_names: Vec<String> = Vec::with_capacity(entries.len());
575    let mut prev_refs: Vec<(String, Option<String>)> = Vec::with_capacity(entries.len());
576    for (stem, path) in &entries {
577        let raw = std::fs::read_to_string(path).map_err(|e| {
578            syn::Error::new(
579                proc_macro2::Span::call_site(),
580                format!(
581                    "embed_migrations!: cannot read {} for chain validation: {e}",
582                    path.display()
583                ),
584            )
585        })?;
586        let json: serde_json::Value = serde_json::from_str(&raw).map_err(|e| {
587            syn::Error::new(
588                proc_macro2::Span::call_site(),
589                format!(
590                    "embed_migrations!: {} is not valid JSON: {e}",
591                    path.display()
592                ),
593            )
594        })?;
595        let name = json
596            .get("name")
597            .and_then(|v| v.as_str())
598            .ok_or_else(|| {
599                syn::Error::new(
600                    proc_macro2::Span::call_site(),
601                    format!(
602                        "embed_migrations!: {} is missing the `name` field",
603                        path.display()
604                    ),
605                )
606            })?
607            .to_owned();
608        if name != *stem {
609            return Err(syn::Error::new(
610                proc_macro2::Span::call_site(),
611                format!(
612                    "embed_migrations!: file stem `{stem}` does not match the migration's \
613                     `name` field `{name}` — rename the file or fix the JSON",
614                ),
615            ));
616        }
617        let prev = json.get("prev").and_then(|v| v.as_str()).map(str::to_owned);
618        chain_names.push(name.clone());
619        prev_refs.push((name, prev));
620    }
621
622    let name_set: std::collections::HashSet<&str> =
623        chain_names.iter().map(String::as_str).collect();
624    for (name, prev) in &prev_refs {
625        if let Some(p) = prev {
626            if !name_set.contains(p.as_str()) {
627                return Err(syn::Error::new(
628                    proc_macro2::Span::call_site(),
629                    format!(
630                        "embed_migrations!: broken migration chain — `{name}` declares \
631                         prev=`{p}` but no migration with that name exists in {}",
632                        abs.display()
633                    ),
634                ));
635            }
636        }
637    }
638
639    let pairs: Vec<TokenStream2> = entries
640        .iter()
641        .map(|(name, path)| {
642            let path_lit = path.display().to_string();
643            quote! { (#name, ::core::include_str!(#path_lit)) }
644        })
645        .collect();
646
647    Ok(quote! {
648        {
649            const __RUSTANGO_EMBEDDED: &[(&'static str, &'static str)] = &[#(#pairs),*];
650            __RUSTANGO_EMBEDDED
651        }
652    })
653}
654
655fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
656    let root = rustango_root();
657    let struct_name = &input.ident;
658
659    let Data::Struct(data) = &input.data else {
660        return Err(syn::Error::new_spanned(
661            struct_name,
662            "Model can only be derived on structs",
663        ));
664    };
665    let Fields::Named(named) = &data.fields else {
666        return Err(syn::Error::new_spanned(
667            struct_name,
668            "Model requires a struct with named fields",
669        ));
670    };
671
672    let container = parse_container_attrs(input)?;
673    let table = container
674        .table
675        .unwrap_or_else(|| to_snake_case(&struct_name.to_string()));
676    let model_name = struct_name.to_string();
677
678    let collected = collect_fields(named, &table)?;
679
680    // Validate that #[rustango(display = "…")] names a real field.
681    if let Some((ref display, span)) = container.display {
682        if !collected.field_names.iter().any(|n| n == display) {
683            return Err(syn::Error::new(
684                span,
685                format!("`display = \"{display}\"` does not match any field on this struct"),
686            ));
687        }
688    }
689    let display = container.display.map(|(name, _)| name);
690    let app_label = container.app.clone();
691
692    // Validate admin field-name lists against declared field names.
693    // Note: `list_display` is intentionally NOT validated here. As of
694    // v0.32 it may also reference inventory-registered computed
695    // fields (via `register_admin_computed!`) whose existence the
696    // macro can't see at compile time — they're submitted from any
697    // crate that depends on rustango. The runtime list-view resolves
698    // unknown names against the inventory + silently drops the
699    // truly-bogus ones, which is the cheaper trade-off versus
700    // forcing a per-Model attr to opt out.
701    if let Some(admin) = &container.admin {
702        for (label, list) in [
703            ("search_fields", &admin.search_fields),
704            ("readonly_fields", &admin.readonly_fields),
705            ("list_filter", &admin.list_filter),
706        ] {
707            if let Some((names, span)) = list {
708                for name in names {
709                    if !collected.field_names.iter().any(|n| n == name) {
710                        return Err(syn::Error::new(
711                            *span,
712                            format!(
713                                "`{label} = \"{name}\"`: \"{name}\" is not a declared field on this struct"
714                            ),
715                        ));
716                    }
717                }
718            }
719        }
720        if let Some((pairs, span)) = &admin.ordering {
721            for (name, _) in pairs {
722                if !collected.field_names.iter().any(|n| n == name) {
723                    return Err(syn::Error::new(
724                        *span,
725                        format!(
726                            "`ordering = \"{name}\"`: \"{name}\" is not a declared field on this struct"
727                        ),
728                    ));
729                }
730            }
731        }
732        if let Some((groups, span)) = &admin.fieldsets {
733            for (_, fields) in groups {
734                for name in fields {
735                    if !collected.field_names.iter().any(|n| n == name) {
736                        return Err(syn::Error::new(
737                            *span,
738                            format!(
739                                "`fieldsets`: \"{name}\" is not a declared field on this struct"
740                            ),
741                        ));
742                    }
743                }
744            }
745        }
746    }
747    if let Some(audit) = &container.audit {
748        if let Some((names, span)) = &audit.track {
749            for name in names {
750                if !collected.field_names.iter().any(|n| n == name) {
751                    return Err(syn::Error::new(
752                        *span,
753                        format!(
754                            "`audit(track = \"{name}\")`: \"{name}\" is not a declared field on this struct"
755                        ),
756                    ));
757                }
758            }
759        }
760    }
761
762    // Issue #291 / T2.5 — validate each `default_order` column name
763    // against the model's collected fields. Typos fail at macro-expand
764    // time, not at the database.
765    for (col, _desc, span) in &container.default_order {
766        if !collected.field_names.iter().any(|n| n == col) {
767            return Err(syn::Error::new(
768                *span,
769                format!(
770                    "`default_order = \"...\"`: \"{col}\" is not a declared field on this struct"
771                ),
772            ));
773        }
774    }
775
776    // Build the audit_track list for ModelSchema: None when no audit attr,
777    // Some(empty) when audit present without track, Some(names) when explicit.
778    let audit_track_names: Option<Vec<String>> = container.audit.as_ref().map(|audit| {
779        audit
780            .track
781            .as_ref()
782            .map(|(names, _)| names.clone())
783            .unwrap_or_default()
784    });
785
786    // Merge field-level indexes into the container's index list.
787    let mut all_indexes: Vec<IndexAttr> = container.indexes;
788    for field in &named.named {
789        let ident = field.ident.as_ref().expect("named");
790        let col = to_snake_case(&ident.to_string()); // column name fallback
791                                                     // Re-parse field attrs to check for index flag
792        if let Ok(fa) = parse_field_attrs(field) {
793            if fa.index {
794                let col_name = fa.column.clone().unwrap_or_else(|| col.clone());
795                let auto_name = if fa.index_unique {
796                    format!("{table}_{col_name}_uq_idx")
797                } else {
798                    format!("{table}_{col_name}_idx")
799                };
800                all_indexes.push(IndexAttr {
801                    name: fa.index_name.or(Some(auto_name)),
802                    columns: vec![col_name],
803                    unique: fa.index_unique,
804                    method: fa.index_method,
805                    where_clause: None,
806                    include: Vec::new(),
807                });
808            }
809        }
810    }
811
812    let model_impl = model_impl_tokens(
813        struct_name,
814        &model_name,
815        &table,
816        display.as_deref(),
817        app_label.as_deref(),
818        container.admin.as_ref(),
819        &container.default_order,
820        &collected.field_schemas,
821        collected.soft_delete_column.as_deref(),
822        container.permissions,
823        audit_track_names.as_deref(),
824        &container.m2m,
825        &all_indexes,
826        &container.checks,
827        &container.excludes,
828        &container.composite_fks,
829        &container.generic_fks,
830        container.scope.as_deref(),
831        container.is_view,
832        container.verbose_name.as_deref(),
833        container.verbose_name_plural.as_deref(),
834        container.managed,
835        container.base_manager_name.as_deref(),
836        container.order_with_respect_to.as_deref(),
837        container.proxy,
838        &container.required_db_features,
839        container.required_db_vendor.as_deref(),
840        container.default_related_name.as_deref(),
841        container.db_table_comment.as_deref(),
842        container
843            .get_latest_by
844            .as_ref()
845            .map(|(c, d)| (c.as_str(), *d)),
846        &container.extra_permissions,
847        &container.default_permissions,
848        &container.global_scopes,
849        &container.reverse_has_relations,
850        &container.generic_has_relations,
851    );
852    let module_ident = column_module_ident(struct_name);
853    let column_consts = column_const_tokens(&module_ident, &collected.column_entries);
854    let audited_fields: Option<Vec<&ColumnEntry>> = container.audit.as_ref().map(|audit| {
855        let track_set: Option<std::collections::HashSet<&str>> = audit
856            .track
857            .as_ref()
858            .map(|(names, _)| names.iter().map(String::as_str).collect());
859        collected
860            .column_entries
861            .iter()
862            .filter(|c| {
863                track_set
864                    .as_ref()
865                    .map_or(true, |s| s.contains(c.name.as_str()))
866            })
867            .collect()
868    });
869    let inherent_impl = inherent_impl_tokens(
870        struct_name,
871        &collected,
872        collected.primary_key.as_ref(),
873        &column_consts,
874        audited_fields.as_deref(),
875        &all_indexes,
876        &container.manager_fns,
877    );
878    let column_module = column_module_tokens(&module_ident, struct_name, &collected.column_entries);
879    let from_row_impl = from_row_impl_tokens(struct_name, &collected.from_row_inits);
880    let reverse_helpers = reverse_helper_tokens(
881        struct_name,
882        &collected.fk_relations,
883        container.default_related_name.as_deref(),
884    );
885    let m2m_accessors = m2m_accessor_tokens(struct_name, &container.m2m);
886    let generic_m2m_accessors = generic_m2m_accessor_tokens(struct_name, &container.generic_m2m);
887    // Issue #817 — `#[rustango(through(...))]` accessors.
888    let through_accessors = through_accessor_tokens(struct_name, &container.through_relations);
889    // Issue #830 — `#[rustango(reverse_has(...))]` static accessors.
890    let reverse_has_accessors =
891        reverse_has_accessor_tokens(struct_name, &container.reverse_has_relations);
892    let generic_fk_accessors = generic_fk_accessor_tokens(
893        struct_name,
894        &container.generic_fks,
895        &collected.column_entries,
896    );
897
898    // Issue #271 / T1.9 — `#[rustango(manager(ext = "FooManagerExt"))]`
899    // emits an empty extension trait so users can add methods via
900    // `impl FooManagerExt for QuerySet<Foo>` without hand-writing the
901    // trait declaration. See `crates/rustango/src/manager.rs` for the
902    // pattern this replaces.
903    let manager_trait = container.manager_ext.as_ref().map(|name| {
904        let model_name_str = struct_name.to_string();
905        let doc = format!(
906            "Custom-Manager extension trait for [`{model_name_str}`]. \
907             Generated by `#[rustango(manager(ext = ...))]`. Add methods \
908             via `impl {name} for QuerySet<{model_name_str}> {{ ... }}`."
909        );
910        quote! {
911            #[doc = #doc]
912            pub trait #name: ::core::marker::Sized {}
913        }
914    });
915
916    Ok(quote! {
917        #model_impl
918        #inherent_impl
919        #from_row_impl
920        #column_module
921        #reverse_helpers
922        #m2m_accessors
923        #generic_m2m_accessors
924        #through_accessors
925        #reverse_has_accessors
926        #generic_fk_accessors
927        #manager_trait
928
929        #root::core::inventory::submit! {
930            #root::core::ModelEntry {
931                schema: <#struct_name as #root::core::Model>::SCHEMA,
932                // `module_path!()` evaluates at the registration site,
933                // so a Model declared in `crate::blog::models` records
934                // `"<crate>::blog::models"` and `resolved_app_label()`
935                // can infer "blog" without an explicit attribute.
936                module_path: ::core::module_path!(),
937            }
938        }
939    })
940}
941
942/// Emit `impl LoadRelated for #StructName` — slice 9.0d. Pattern-
943/// matches `field_name` against the model's FK fields and, for a
944/// match, decodes the FK target via the parent's macro-generated
945/// `__rustango_from_aliased_row`, reads the parent's PK, and stores
946/// `ForeignKey::Loaded` on `self`.
947///
948/// Always emitted (with empty arms for FK-less models, which
949/// return `Ok(false)` for any field name) so the `T: LoadRelated`
950/// trait bound on `fetch_on` is universally satisfied — users
951/// never have to think about implementing it.
952fn load_related_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
953    let root = rustango_root();
954    let arms = fk_relations.iter().map(|rel| {
955        let parent_ty = &rel.parent_type;
956        let fk_col = rel.fk_column.as_str();
957        // FK field's Rust ident matches its SQL column name in v0.8
958        // (no `column = "..."` rename ships on FK fields).
959        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
960        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
961        let assign = if rel.nullable {
962            quote! {
963                self.#field_ident = ::core::option::Option::Some(
964                    #root::sql::ForeignKey::loaded(_pk, _parent),
965                );
966            }
967        } else {
968            quote! {
969                self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
970            }
971        };
972        quote! {
973            #fk_col => {
974                let mut _parent: #parent_ty = <#parent_ty>::__rustango_from_aliased_row(row, alias)?;
975                // Audit #451 — multi-hop `select_related("a__b__c")`:
976                // stitch the deeper relation onto this parent first,
977                // decoding it at the accumulated `__next_alias`. The
978                // parent type also impls `LoadRelated`, so this recurses
979                // the FK chain to arbitrary depth.
980                if let ::core::option::Option::Some(__r) = __rest {
981                    let _ = #root::sql::LoadRelated::__rustango_load_related(
982                        &mut _parent, row, __r, &__next_alias,
983                    )?;
984                }
985                // Loud-in-debug, default-in-release: a divergence
986                // between the FK field's declared `K` (drives the
987                // expected `SqlValue::<Variant>`) and the parent's
988                // `__rustango_pk_value` output is a macro-internal
989                // invariant break — surfacing the panic in dev
990                // catches it before users hit silent PK=0 corruption.
991                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
992                    #root::core::SqlValue::#variant_ident(v) => v,
993                    _other => {
994                        ::core::debug_assert!(
995                            false,
996                            "rustango macro bug: load_related on FK `{}` expected \
997                             SqlValue::{} from parent's __rustango_pk_value but got \
998                             {:?} — file a bug at https://github.com/ujeenet/rustango",
999                            #fk_col,
1000                            ::core::stringify!(#variant_ident),
1001                            _other,
1002                        );
1003                        #default_expr
1004                    }
1005                };
1006                #assign
1007                ::core::result::Result::Ok(true)
1008            }
1009        }
1010    });
1011    quote! {
1012        #[cfg(feature = "postgres")]
1013        impl #root::sql::LoadRelated for #struct_name {
1014            #[allow(unused_variables)]
1015            fn __rustango_load_related(
1016                &mut self,
1017                row: &#root::sql::sqlx::postgres::PgRow,
1018                field_name: &str,
1019                alias: &str,
1020            ) -> ::core::result::Result<bool, #root::sql::sqlx::Error> {
1021                // Audit #451 — split the multi-hop path: `base` is the FK
1022                // on THIS model, `__rest` (if any) is the remaining chain
1023                // to stitch onto the loaded parent. `__next_alias` is the
1024                // accumulated join alias the parent's columns live under
1025                // (`{alias}__{next-hop}`), matching `lower_select_related`.
1026                let (__base, __rest): (&str, ::core::option::Option<&str>) =
1027                    match field_name.split_once("__") {
1028                        ::core::option::Option::Some((b, r)) => (b, ::core::option::Option::Some(r)),
1029                        ::core::option::Option::None => (field_name, ::core::option::Option::None),
1030                    };
1031                let __next_alias: ::std::string::String = match __rest {
1032                    ::core::option::Option::Some(__r) => {
1033                        let __rb = __r.split_once("__").map(|(b, _)| b).unwrap_or(__r);
1034                        ::std::format!("{}__{}", alias, __rb)
1035                    }
1036                    ::core::option::Option::None => ::std::string::String::new(),
1037                };
1038                match __base {
1039                    #( #arms )*
1040                    _ => ::core::result::Result::Ok(false),
1041                }
1042            }
1043        }
1044    }
1045}
1046
1047/// MySQL counterpart of [`load_related_impl_tokens`] — v0.23.0-batch8.
1048/// Emits a call to the cfg-gated `__impl_my_load_related!` macro_rules,
1049/// which expands to a `LoadRelatedMy` impl when rustango is built with
1050/// the `mysql` feature, and to nothing otherwise. The decoded parent
1051/// is read via `__rustango_from_aliased_my_row` (the MySQL aliased
1052/// decoder, also batch8) so the dual emission is symmetric across
1053/// backends.
1054fn load_related_impl_my_tokens(
1055    struct_name: &syn::Ident,
1056    fk_relations: &[FkRelation],
1057) -> TokenStream2 {
1058    let root = rustango_root();
1059    let arms = fk_relations.iter().map(|rel| {
1060        let parent_ty = &rel.parent_type;
1061        let fk_col = rel.fk_column.as_str();
1062        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1063        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1064        let assign = if rel.nullable {
1065            quote! {
1066                __self.#field_ident = ::core::option::Option::Some(
1067                    #root::sql::ForeignKey::loaded(_pk, _parent),
1068                );
1069            }
1070        } else {
1071            quote! {
1072                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1073            }
1074        };
1075        // `self` IS hygiene-tracked through macro_rules — emitted from
1076        // a different context than the `&mut self` parameter inside
1077        // the macro_rules-expanded fn. Pass it through as `__self`
1078        // and let the macro_rules rebind it to the receiver.
1079        quote! {
1080            #fk_col => {
1081                let mut _parent: #parent_ty =
1082                    <#parent_ty>::__rustango_from_aliased_my_row(row, alias)?;
1083                // Audit #451 — multi-hop: stitch the deeper relation onto
1084                // the parent at the accumulated alias (see PG twin).
1085                if let ::core::option::Option::Some(__r) = __rest {
1086                    let _ = #root::sql::LoadRelatedMy::__rustango_load_related_my(
1087                        &mut _parent, row, __r, &__next_alias,
1088                    )?;
1089                }
1090                // See note in `load_related_impl_tokens` (PG twin) —
1091                // the same loud-in-debug invariant guard.
1092                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1093                    #root::core::SqlValue::#variant_ident(v) => v,
1094                    _other => {
1095                        ::core::debug_assert!(
1096                            false,
1097                            "rustango macro bug: load_related on FK `{}` expected \
1098                             SqlValue::{} from parent's __rustango_pk_value but got \
1099                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1100                            #fk_col,
1101                            ::core::stringify!(#variant_ident),
1102                            _other,
1103                        );
1104                        #default_expr
1105                    }
1106                };
1107                #assign
1108                ::core::result::Result::Ok(true)
1109            }
1110        }
1111    });
1112    quote! {
1113        #root::__impl_my_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1114            #( #arms )*
1115        });
1116    }
1117}
1118
1119/// Same shape as [`load_related_impl_my_tokens`] but for SQLite.
1120/// Emits a call to `__impl_sqlite_load_related!` which expands to a
1121/// `LoadRelatedSqlite` impl when the `sqlite` feature is on.
1122fn load_related_impl_sqlite_tokens(
1123    struct_name: &syn::Ident,
1124    fk_relations: &[FkRelation],
1125) -> TokenStream2 {
1126    let root = rustango_root();
1127    let arms = fk_relations.iter().map(|rel| {
1128        let parent_ty = &rel.parent_type;
1129        let fk_col = rel.fk_column.as_str();
1130        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1131        let (variant_ident, default_expr) = rel.pk_kind.sqlvalue_match_arm();
1132        let assign = if rel.nullable {
1133            quote! {
1134                __self.#field_ident = ::core::option::Option::Some(
1135                    #root::sql::ForeignKey::loaded(_pk, _parent),
1136                );
1137            }
1138        } else {
1139            quote! {
1140                __self.#field_ident = #root::sql::ForeignKey::loaded(_pk, _parent);
1141            }
1142        };
1143        quote! {
1144            #fk_col => {
1145                let mut _parent: #parent_ty =
1146                    <#parent_ty>::__rustango_from_aliased_sqlite_row(row, alias)?;
1147                // Audit #451 — multi-hop: stitch the deeper relation onto
1148                // the parent at the accumulated alias (see PG twin).
1149                if let ::core::option::Option::Some(__r) = __rest {
1150                    let _ = #root::sql::LoadRelatedSqlite::__rustango_load_related_sqlite(
1151                        &mut _parent, row, __r, &__next_alias,
1152                    )?;
1153                }
1154                let _pk = match <#parent_ty>::__rustango_pk_value(&_parent) {
1155                    #root::core::SqlValue::#variant_ident(v) => v,
1156                    _other => {
1157                        ::core::debug_assert!(
1158                            false,
1159                            "rustango macro bug: load_related on FK `{}` expected \
1160                             SqlValue::{} from parent's __rustango_pk_value but got \
1161                             {:?} — file a bug at https://github.com/ujeenet/rustango",
1162                            #fk_col,
1163                            ::core::stringify!(#variant_ident),
1164                            _other,
1165                        );
1166                        #default_expr
1167                    }
1168                };
1169                #assign
1170                ::core::result::Result::Ok(true)
1171            }
1172        }
1173    });
1174    quote! {
1175        #root::__impl_sqlite_load_related!(#struct_name, |__self, row, field_name, alias, __rest, __next_alias| {
1176            #( #arms )*
1177        });
1178    }
1179}
1180
1181/// Emit `impl FkPkAccess for #StructName` — slice 9.0e. Pattern-
1182/// matches `field_name` against the model's FK fields and returns
1183/// the FK's stored PK as `i64`. Used by `fetch_with_prefetch` to
1184/// group children by parent PK.
1185///
1186/// Always emitted (with `_ => None` for FK-less models) so the
1187/// trait bound on `fetch_with_prefetch` is universally satisfied.
1188fn fk_pk_access_impl_tokens(struct_name: &syn::Ident, fk_relations: &[FkRelation]) -> TokenStream2 {
1189    let root = rustango_root();
1190    let arms = fk_relations.iter().map(|rel| {
1191        let fk_col = rel.fk_column.as_str();
1192        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1193        if rel.pk_kind == DetectedKind::I64 {
1194            // i64 FK — return the stored PK so prefetch_related can
1195            // group children by it. Nullable variant unwraps via
1196            // `as_ref().map(...)`: an unset (NULL) FK column yields
1197            // `None` and that child sits out of the grouping (correct
1198            // semantics — it has no parent to attach to).
1199            if rel.nullable {
1200                quote! {
1201                    #fk_col => self.#field_ident
1202                        .as_ref()
1203                        .map(|fk| #root::sql::ForeignKey::pk(fk)),
1204                }
1205            } else {
1206                quote! {
1207                    #fk_col => ::core::option::Option::Some(self.#field_ident.pk()),
1208                }
1209            }
1210        } else {
1211            // Non-i64 FK PKs (e.g. `ForeignKey<T, String>`,
1212            // `ForeignKey<T, Uuid>`) opt out of `prefetch_related`'s
1213            // i64-keyed grouping path — the trait signature is
1214            // `Option<i64>` and a non-i64 PK can't lower into it.
1215            // The FK still works for everything else (CRUD, lazy
1216            // load via `.get()`, select_related JOINs); only the
1217            // bulk prefetch grouper needs the integer key.
1218            quote! {
1219                #fk_col => ::core::option::Option::None,
1220            }
1221        }
1222    });
1223    // PK-type-agnostic version: every FK arm emits an
1224    // `Option<SqlValue>` so `fetch_with_prefetch` can group by any
1225    // PK type (i64, i32, String, Uuid). Models with non-i64 FK PKs
1226    // opt OUT of the legacy i64 method (it returns None) but opt IN
1227    // here.
1228    let value_arms = fk_relations.iter().map(|rel| {
1229        let fk_col = rel.fk_column.as_str();
1230        let field_ident = syn::Ident::new(fk_col, proc_macro2::Span::call_site());
1231        if rel.nullable {
1232            quote! {
1233                #fk_col => self.#field_ident
1234                    .as_ref()
1235                    .map(|fk| ::core::convert::Into::<#root::core::SqlValue>::into(
1236                        #root::sql::ForeignKey::pk(fk)
1237                    )),
1238            }
1239        } else {
1240            quote! {
1241                #fk_col => ::core::option::Option::Some(
1242                    ::core::convert::Into::<#root::core::SqlValue>::into(
1243                        self.#field_ident.pk()
1244                    )
1245                ),
1246            }
1247        }
1248    });
1249    quote! {
1250        impl #root::sql::FkPkAccess for #struct_name {
1251            #[allow(unused_variables)]
1252            fn __rustango_fk_pk(&self, field_name: &str) -> ::core::option::Option<i64> {
1253                match field_name {
1254                    #( #arms )*
1255                    _ => ::core::option::Option::None,
1256                }
1257            }
1258            #[allow(unused_variables)]
1259            fn __rustango_fk_pk_value(
1260                &self,
1261                field_name: &str,
1262            ) -> ::core::option::Option<#root::core::SqlValue> {
1263                match field_name {
1264                    #( #value_arms )*
1265                    _ => ::core::option::Option::None,
1266                }
1267            }
1268        }
1269    }
1270}
1271
1272/// For every `ForeignKey<Parent>` field on `Child`, emit
1273/// `impl Parent { pub async fn <child_table>_set(&self, executor) -> Vec<Child> }`.
1274/// Reads the parent's PK via the macro-generated `__rustango_pk_value`
1275/// and runs a single `SELECT … FROM <child_table> WHERE <fk_column> = $1`
1276/// — the canonical reverse-FK fetch. One round trip, no N+1.
1277///
1278/// **PG-only emission**: the accessor is bounded on
1279/// `sqlx::Executor<Database = sqlx::Postgres>` and calls `fetch_on`,
1280/// both of which are gated behind the `postgres` cargo feature. The
1281/// emitted code is wrapped in `#[cfg(feature = "postgres")]` so the
1282/// model derive itself compiles on tri-dialect / sqlite-only
1283/// downstream builds — the accessor just isn't materialised. A tri-
1284/// dialect `_set_pool` variant is a separate follow-up.
1285fn reverse_helper_tokens(
1286    child_ident: &syn::Ident,
1287    fk_relations: &[FkRelation],
1288    default_related_name: Option<&str>,
1289) -> TokenStream2 {
1290    let root = rustango_root();
1291    if fk_relations.is_empty() {
1292        return TokenStream2::new();
1293    }
1294    // Method-name resolution per FK (issue #816 + follow-up):
1295    //   1. Field-level `#[rustango(related_name = "...")]` on the FK
1296    //      itself — wins over everything else. Django's
1297    //      `ForeignKey(related_name="...")`.
1298    //   2. Container-level `default_related_name = "..."` on the
1299    //      child — Django's `class Meta: default_related_name`.
1300    //      Applies to every FK on this model that didn't override.
1301    //   3. Fallback: `<child_snake>_set` — Django's `<child>_set`
1302    //      convention. `Post` → `post_set`, `BlogComment` →
1303    //      `blog_comment_set`. Avoids English-plural edge cases.
1304    //
1305    // The PG-on-executor variant keeps the resolved name; the
1306    // tri-dialect `_pool` variant appends `_pool` to it (matches the
1307    // framework's convention for the `&Pool` flavor of every helper).
1308    let default_pg_suffix = default_related_name
1309        .map(str::to_owned)
1310        .unwrap_or_else(|| format!("{}_set", to_snake_case(&child_ident.to_string())));
1311    let impls = fk_relations.iter().map(|rel| {
1312        let pg_suffix = rel
1313            .related_name
1314            .clone()
1315            .unwrap_or_else(|| default_pg_suffix.clone());
1316        let pool_suffix = format!("{}_pool", pg_suffix);
1317        let pg_method_ident = syn::Ident::new(&pg_suffix, child_ident.span());
1318        let pool_method_ident = syn::Ident::new(&pool_suffix, child_ident.span());
1319        let parent_ty = &rel.parent_type;
1320        let fk_col = rel.fk_column.as_str();
1321        let doc = format!(
1322            "Fetch every `{child_ident}` whose `{fk_col}` foreign key points at this row. \
1323             Single SQL query — `SELECT … FROM <{child_ident} table> WHERE {fk_col} = $1` — \
1324             generated from the FK declaration on `{child_ident}::{fk_col}`. Composes with \
1325             further `{child_ident}::objects()` filters via direct queryset use."
1326        );
1327        let pool_doc = format!(
1328            "Tri-dialect counterpart of [`Self::{pg_suffix}`] — takes \
1329             [`#root::sql::Pool`] and dispatches per backend so the \
1330             reverse-FK fetch works on PG / MySQL / SQLite under one method. \
1331             Use this from framework code that holds a `&Pool` (admin, \
1332             tenancy resolver, viewset handlers); reach for the executor- \
1333             bound variant when you already have a typed `sqlx::Executor`."
1334        );
1335        quote! {
1336            #[cfg(feature = "postgres")]
1337            impl #parent_ty {
1338                #[doc = #doc]
1339                ///
1340                /// # Errors
1341                /// Returns [`#root::sql::ExecError`] for SQL-writing
1342                /// or driver failures.
1343                pub async fn #pg_method_ident<'_c, _E>(
1344                    &self,
1345                    _executor: _E,
1346                ) -> ::core::result::Result<
1347                    ::std::vec::Vec<#child_ident>,
1348                    #root::sql::ExecError,
1349                >
1350                where
1351                    _E: #root::sql::sqlx::Executor<
1352                        '_c,
1353                        Database = #root::sql::sqlx::Postgres,
1354                    >,
1355                {
1356                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1357                    #root::query::QuerySet::<#child_ident>::new()
1358                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1359                        .fetch_on(_executor)
1360                        .await
1361                }
1362            }
1363
1364            impl #parent_ty {
1365                #[doc = #pool_doc]
1366                ///
1367                /// # Errors
1368                /// Returns [`#root::sql::ExecError`] for SQL-writing
1369                /// or driver failures.
1370                pub async fn #pool_method_ident(
1371                    &self,
1372                    pool: &#root::sql::Pool,
1373                ) -> ::core::result::Result<
1374                    ::std::vec::Vec<#child_ident>,
1375                    #root::sql::ExecError,
1376                > {
1377                    use #root::sql::FetcherPool as _;
1378                    let _pk: #root::core::SqlValue = self.__rustango_pk_value();
1379                    #root::query::QuerySet::<#child_ident>::new()
1380                        .filter_op(#fk_col, #root::core::Op::Eq, _pk)
1381                        .fetch(pool)
1382                        .await
1383                }
1384            }
1385        }
1386    });
1387    quote! { #( #impls )* }
1388}
1389
1390/// Emit `<name>_m2m(&self) -> M2MManager` inherent methods for every M2M
1391/// relation declared on the model.
1392/// Emit `{name}_pool` accessor + `set_{name}_for` setter for every
1393/// `#[rustango(generic_fk(name, ct_column, pk_column))]` declaration.
1394///
1395/// Closes #239 + #240 — the Django-shape `comment.content_object` /
1396/// `comment.content_object = post` ergonomics on top of the existing
1397/// `GenericForeignKey { content_type_id, object_pk }` primitive.
1398///
1399/// `column_entries` is passed so we can resolve each `ct_column` /
1400/// `pk_column` SQL name back to its Rust field ident — the macro
1401/// only sees the column-side strings in the attribute, but the
1402/// emitted accessor needs to read the actual struct field.
1403fn generic_fk_accessor_tokens(
1404    struct_name: &syn::Ident,
1405    generic_fks: &[GenericFkAttr],
1406    column_entries: &[ColumnEntry],
1407) -> TokenStream2 {
1408    let root = rustango_root();
1409    if generic_fks.is_empty() {
1410        return TokenStream2::new();
1411    }
1412    let methods = generic_fks.iter().filter_map(|gfk| {
1413        // Resolve `ct_column` + `pk_column` to the struct's Rust
1414        // field idents. A typo (column name doesn't match any field)
1415        // emits no method for that registration — the user will see
1416        // the compiler reject the SCHEMA literal anyway, so there's
1417        // a clear error path without us double-reporting.
1418        let ct_ident = column_entries
1419            .iter()
1420            .find(|c| c.column == gfk.ct_column)
1421            .map(|c| c.ident.clone())?;
1422        let pk_ident = column_entries
1423            .iter()
1424            .find(|c| c.column == gfk.pk_column)
1425            .map(|c| c.ident.clone())?;
1426
1427        let accessor_ident =
1428            syn::Ident::new(&format!("{}_pool", gfk.name), struct_name.span());
1429        let setter_ident =
1430            syn::Ident::new(&format!("set_{}_for", gfk.name), struct_name.span());
1431        let name_literal = gfk.name.as_str();
1432
1433        Some(quote! {
1434            #[doc = concat!(
1435                "Resolve the polymorphic `",
1436                #name_literal,
1437                "` relation. Reads `self.",
1438                stringify!(#ct_ident),
1439                "` + `self.",
1440                stringify!(#pk_ident),
1441                "`, looks up the matching `ContentType`, and fetches the target row as a JSON map.\n\n",
1442                "Returns `Ok(None)` when the ContentType is stale / unseeded or the target row was deleted. Emitted by `#[rustango(generic_fk(name = \"",
1443                #name_literal,
1444                "\", ...))]`."
1445            )]
1446            pub async fn #accessor_ident(
1447                &self,
1448                pool: &#root::sql::Pool,
1449            ) -> ::core::result::Result<
1450                ::core::option::Option<#root::__serde_json::Value>,
1451                #root::sql::ExecError,
1452            > {
1453                let gfk = #root::contenttypes::GenericForeignKey::new(
1454                    self.#ct_ident as i64,
1455                    self.#pk_ident as i64,
1456                );
1457                gfk.get_object(pool).await
1458            }
1459
1460            #[doc = concat!(
1461                "Set the polymorphic `",
1462                #name_literal,
1463                "` target. Looks up the `ContentType` for `T` via the cached registry, then assigns both `self.",
1464                stringify!(#ct_ident),
1465                "` and `self.",
1466                stringify!(#pk_ident),
1467                "`.\n\nFollow with `self.insert(pool)` or `self.update(pool)` to persist. Emitted by `#[rustango(generic_fk(name = \"",
1468                #name_literal,
1469                "\", ...))]`."
1470            )]
1471            pub async fn #setter_ident<T: #root::core::Model>(
1472                &mut self,
1473                pool: &#root::sql::Pool,
1474                target_pk: i64,
1475            ) -> ::core::result::Result<(), #root::sql::ExecError> {
1476                let gfk = #root::contenttypes::GenericForeignKey::for_target::<T>(
1477                    pool,
1478                    target_pk,
1479                ).await?;
1480                self.#ct_ident = gfk.content_type_id as _;
1481                self.#pk_ident = gfk.object_pk as _;
1482                ::core::result::Result::Ok(())
1483            }
1484        })
1485    });
1486    quote! {
1487        impl #struct_name {
1488            #( #methods )*
1489        }
1490    }
1491}
1492
1493fn m2m_accessor_tokens(struct_name: &syn::Ident, m2m_relations: &[M2MAttr]) -> TokenStream2 {
1494    let root = rustango_root();
1495    if m2m_relations.is_empty() {
1496        return TokenStream2::new();
1497    }
1498    let methods = m2m_relations.iter().map(|rel| {
1499        let method_name = format!("{}_m2m", rel.name);
1500        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1501        let through = rel.through.as_str();
1502        let src_col = rel.src.as_str();
1503        let dst_col = rel.dst.as_str();
1504        quote! {
1505            pub fn #method_ident(&self) -> #root::sql::M2MManager {
1506                #root::sql::M2MManager {
1507                    src_pk: self.__rustango_pk_value(),
1508                    through: #through,
1509                    src_col: #src_col,
1510                    dst_col: #dst_col,
1511                }
1512            }
1513        }
1514    });
1515    quote! {
1516        impl #struct_name {
1517            #( #methods )*
1518        }
1519    }
1520}
1521
1522/// Emit `<name>_m2m(&self) -> GenericM2MManager` inherent methods for
1523/// every `#[rustango(generic_m2m(...))]` (polymorphic M2M, issue #818).
1524fn generic_m2m_accessor_tokens(
1525    struct_name: &syn::Ident,
1526    relations: &[GenericM2MAttr],
1527) -> TokenStream2 {
1528    let root = rustango_root();
1529    if relations.is_empty() {
1530        return TokenStream2::new();
1531    }
1532    let methods = relations.iter().map(|rel| {
1533        let method_ident = syn::Ident::new(&format!("{}_m2m", rel.name), struct_name.span());
1534        let through = rel.through.as_str();
1535        let pk_col = rel.pk_column.as_str();
1536        let ct_col = rel.ct_column.as_str();
1537        let related_col = rel.related_column.as_str();
1538        quote! {
1539            pub fn #method_ident(&self) -> #root::sql::GenericM2MManager {
1540                #root::sql::GenericM2MManager {
1541                    src_pk: self.__rustango_pk_value(),
1542                    src_schema: <Self as #root::core::Model>::SCHEMA,
1543                    through: #through,
1544                    pk_col: #pk_col,
1545                    ct_col: #ct_col,
1546                    dst_col: #related_col,
1547                }
1548            }
1549        }
1550    });
1551    quote! {
1552        impl #struct_name {
1553            #( #methods )*
1554        }
1555    }
1556}
1557
1558/// Emit `<name>_exists_expr()` + `<name>_not_exists_expr()`
1559/// associated functions for each `#[rustango(reverse_has(...))]`
1560/// attribute. Issue #830.
1561///
1562/// The two emitted functions return ready-to-use `WhereExpr` nodes
1563/// that downstream callers drop into
1564/// `QuerySet::<Self>::where_raw(...)`:
1565///
1566/// - `<name>_exists_expr()` → `EXISTS (SELECT 1 FROM <child> WHERE
1567///   <child_fk_column> = OuterRef("<self_pk_column>"))`. Eloquent
1568///   `whereHas` parity (without the closure-style sub-predicate
1569///   refinement — that's a follow-up; users can layer additional
1570///   predicates by constructing the SelectQuery themselves and
1571///   calling `where_raw(exists(query))` from `crate::core::subquery`).
1572/// - `<name>_not_exists_expr()` → same but `NOT EXISTS`. Eloquent
1573///   `whereDoesntHave` parity.
1574///
1575/// Tri-dialect: `EXISTS` / `NOT EXISTS` over a correlated subquery
1576/// is portable across PG / MySQL / SQLite. The writer's scope-stack
1577/// machinery threads the outer-table reference through automatically
1578/// (`OuterRef(col)` resolves to `<outer>.<col>` at emit time).
1579fn reverse_has_accessor_tokens(
1580    struct_name: &syn::Ident,
1581    reverse_has_relations: &[ReverseHasAttr],
1582) -> TokenStream2 {
1583    let root = rustango_root();
1584    if reverse_has_relations.is_empty() {
1585        return TokenStream2::new();
1586    }
1587    let methods = reverse_has_relations.iter().map(|rel| {
1588        let exists_name = format!("{}_exists_expr", rel.name);
1589        let not_exists_name = format!("{}_not_exists_expr", rel.name);
1590        let count_name = format!("{}_count", rel.name);
1591        let fetch_name = format!("{}_fetch", rel.name);
1592        let first_name = format!("{}_first", rel.name);
1593        let pluck_name = format!("{}_pluck", rel.name);
1594        let accessor_name = rel.name.as_str();
1595        let exists_ident = syn::Ident::new(&exists_name, struct_name.span());
1596        let not_exists_ident = syn::Ident::new(&not_exists_name, struct_name.span());
1597        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1598        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1599        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1600        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1601        let accessor_ident = syn::Ident::new(accessor_name, struct_name.span());
1602        let child = &rel.child;
1603        let child_fk_column = rel.child_fk_column.as_str();
1604        let self_pk_column = rel.self_pk_column.as_str();
1605        let exists_doc = format!(
1606            "Eloquent `whereHas` analog — yields `EXISTS (SELECT 1 \
1607             FROM <{child}> WHERE {child_fk_column} = <outer>.{self_pk_column})`. \
1608             Drop into `QuerySet::<{struct_name}>::where_raw(...)` to \
1609             filter to {struct_name}s with at least one matching child.",
1610        );
1611        let not_exists_doc = format!(
1612            "Eloquent `whereDoesntHave` analog — yields `NOT EXISTS \
1613             (SELECT 1 FROM <{child}> WHERE {child_fk_column} = \
1614             <outer>.{self_pk_column})`. Drop into \
1615             `QuerySet::<{struct_name}>::where_raw(...)` to filter to \
1616             {struct_name}s with **no** matching child.",
1617        );
1618        let count_doc = format!(
1619            "Eloquent `$model->{name}->count()` analog — returns \
1620             the number of `{child}` rows whose `{child_fk_column}` \
1621             matches this `{struct_name}` instance's primary key. \
1622             Issued as `SELECT COUNT(*) FROM <{child}> WHERE \
1623             {child_fk_column} = <self.pk>`.",
1624            name = rel.name,
1625        );
1626        let accessor_doc = format!(
1627            "Eloquent `$model->{name}` accessor — returns a \
1628             `QuerySet<{child}>` filtered to rows whose \
1629             `{child_fk_column}` matches this `{struct_name}` \
1630             instance's primary key. **Chainable**: compose `.filter()` \
1631             / `.order_by()` / `.limit()` etc. on top, then call \
1632             `.fetch(&pool)` (the QuerySet trait method) when \
1633             done. For the simple \"fetch all\" hot path with no \
1634             further composition, prefer the bare-name \
1635             `{name}_fetch(&pool)` companion.",
1636            name = rel.name,
1637        );
1638        let fetch_doc = format!(
1639            "Eloquent `$model->{name}->get()` — bare-name hot-path \
1640             over `{name}(&self).fetch(&pool)`. Use this when \
1641             you don't need further `.filter()` / `.order_by()` \
1642             composition; falls back to the chainable accessor when \
1643             you do. Avoids the `_pool` suffix on the most common \
1644             call-site shape.",
1645            name = rel.name,
1646        );
1647        quote! {
1648            #[doc = #accessor_doc]
1649            pub fn #accessor_ident(&self) -> #root::query::QuerySet<#child> {
1650                #root::query::QuerySet::<#child>::new()
1651                    .filter(#child_fk_column, self.__rustango_pk_value())
1652            }
1653
1654            #[doc = #fetch_doc]
1655            pub async fn #fetch_ident(
1656                &self,
1657                pool: &#root::sql::Pool,
1658            ) -> ::core::result::Result<
1659                ::std::vec::Vec<#child>,
1660                #root::sql::ExecError,
1661            > {
1662                use #root::sql::FetcherPool as _;
1663                self.#accessor_ident().fetch(pool).await
1664            }
1665
1666            /// Eloquent `$model->relation->first()` / `hasOne`
1667            /// semantics — bare-name shortcut over
1668            /// `self.<name>().first(&pool)`. Returns `None` when no
1669            /// child rows match. Useful when the relation is
1670            /// nominally many-to-one in shape but at most one row is
1671            /// expected (latest comment, primary tag, etc.).
1672            pub async fn #first_ident(
1673                &self,
1674                pool: &#root::sql::Pool,
1675            ) -> ::core::result::Result<
1676                ::core::option::Option<#child>,
1677                #root::sql::ExecError,
1678            > {
1679                self.#accessor_ident().first(pool).await
1680            }
1681
1682            /// Eloquent `$model->relation->pluck($col)` — project a
1683            /// single column from the child rows into a `Vec<U>`.
1684            /// Skips the typed `Child` decode, which is cheaper when
1685            /// you only need one column (e.g. `post.comments_pluck::<String>("body", &pool)`).
1686            pub async fn #pluck_ident<U>(
1687                &self,
1688                col: &'static str,
1689                pool: &#root::sql::Pool,
1690            ) -> ::core::result::Result<
1691                ::std::vec::Vec<U>,
1692                #root::sql::ExecError,
1693            >
1694            where
1695                U: #root::sql::MaybePgScalar
1696                    + #root::sql::MaybeMyScalar
1697                    + #root::sql::MaybeSqliteScalar
1698                    + ::core::marker::Send
1699                    + ::core::marker::Unpin,
1700            {
1701                self.#accessor_ident()
1702                    .values_list_flat(col)
1703                    .fetch::<U>(pool)
1704                    .await
1705            }
1706
1707            #[doc = #exists_doc]
1708            pub fn #exists_ident() -> #root::core::WhereExpr {
1709                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1710                let child_schema =
1711                    <#child as #root::core::Model>::SCHEMA;
1712                let inner = SelectQuery {
1713                    where_clause: WhereExpr::ExprCompare {
1714                        lhs: Expr::Column(#child_fk_column),
1715                        op: Op::Eq,
1716                        rhs: Expr::OuterRef(#self_pk_column),
1717                    },
1718                    ..SelectQuery::new(child_schema)
1719                };
1720                WhereExpr::Exists(::std::boxed::Box::new(inner))
1721            }
1722
1723            #[doc = #not_exists_doc]
1724            pub fn #not_exists_ident() -> #root::core::WhereExpr {
1725                use #root::core::{Expr, Model as _, Op, SelectQuery, WhereExpr};
1726                let child_schema =
1727                    <#child as #root::core::Model>::SCHEMA;
1728                let inner = SelectQuery {
1729                    where_clause: WhereExpr::ExprCompare {
1730                        lhs: Expr::Column(#child_fk_column),
1731                        op: Op::Eq,
1732                        rhs: Expr::OuterRef(#self_pk_column),
1733                    },
1734                    ..SelectQuery::new(child_schema)
1735                };
1736                WhereExpr::NotExists(::std::boxed::Box::new(inner))
1737            }
1738
1739            #[doc = #count_doc]
1740            pub async fn #count_ident(
1741                &self,
1742                pool: &#root::sql::Pool,
1743            ) -> ::core::result::Result<
1744                i64,
1745                #root::sql::ExecError,
1746            > {
1747                use #root::sql::CounterPool as _;
1748                #root::query::QuerySet::<#child>::new()
1749                    .filter(#child_fk_column, self.__rustango_pk_value())
1750                    .count(pool)
1751                    .await
1752            }
1753        }
1754    });
1755    quote! {
1756        impl #struct_name {
1757            #( #methods )*
1758        }
1759    }
1760}
1761
1762/// Emit `<name>_through(&self) -> QuerySet<Far>` accessors for each
1763/// `#[rustango(through(...))]` attribute. Issue #817.
1764///
1765/// Each accessor builds a correlated subquery via
1766/// `WhereExpr::InSubquery`: the inner `SelectQuery` reads from the
1767/// intermediate table, filters on the FK column pointing at this
1768/// model, and projects the intermediate PK. The outer queryset
1769/// filters the far model by its FK-to-intermediate column being in
1770/// that set.
1771///
1772/// The returned `QuerySet<Far>` is **chainable** — the subquery
1773/// lives inside a `where_raw` clause so the user's later
1774/// `.filter()` / `.order_by()` / `.limit()` compositions don't
1775/// disturb it.
1776///
1777/// Tri-dialect: `IN (subquery)` is portable across PG / MySQL /
1778/// SQLite — no LATERAL or backend-specific syntax involved.
1779fn through_accessor_tokens(
1780    struct_name: &syn::Ident,
1781    through_relations: &[ThroughAttr],
1782) -> TokenStream2 {
1783    let root = rustango_root();
1784    if through_relations.is_empty() {
1785        return TokenStream2::new();
1786    }
1787    let methods = through_relations.iter().map(|rel| {
1788        let method_name = format!("{}_through", rel.name);
1789        let count_name = format!("{}_through_count", rel.name);
1790        let fetch_name = format!("{}_through_fetch", rel.name);
1791        let first_name = format!("{}_through_first", rel.name);
1792        let pluck_name = format!("{}_through_pluck", rel.name);
1793        let method_ident = syn::Ident::new(&method_name, struct_name.span());
1794        let count_ident = syn::Ident::new(&count_name, struct_name.span());
1795        let fetch_ident = syn::Ident::new(&fetch_name, struct_name.span());
1796        let first_ident = syn::Ident::new(&first_name, struct_name.span());
1797        let pluck_ident = syn::Ident::new(&pluck_name, struct_name.span());
1798        let far = &rel.far;
1799        let intermediate = &rel.intermediate;
1800        let far_fk_column = rel.far_fk_column.as_str();
1801        let intermediate_fk_column = rel.intermediate_fk_column.as_str();
1802        let intermediate_pk_column = rel.intermediate_pk_column.as_str();
1803        let doc = format!(
1804            "Eloquent `hasManyThrough` accessor — returns a \
1805             `QuerySet<{far}>` whose rows reach this `{struct_name}` \
1806             instance through the intermediate `{intermediate}` table. \
1807             Generated SQL shape: \
1808             `… WHERE {far_fk_column} IN (SELECT \
1809             {intermediate_pk_column} FROM <{intermediate}> WHERE \
1810             {intermediate_fk_column} = self.pk)`. Chainable like any \
1811             other QuerySet — compose `.filter()` / `.order_by()` / \
1812             `.limit()` etc. on top.",
1813        );
1814        let count_doc = format!(
1815            "Eloquent `$model->{name}->count()` analog for the \
1816             through-relation — returns the number of `{far}` rows \
1817             reachable through `{intermediate}`. Equivalent to \
1818             `self.{name}_through().count(pool)` but spelled \
1819             as a bare instance method for parity with the \
1820             `reverse_has` `<name>_count` shape.",
1821            name = rel.name,
1822        );
1823        quote! {
1824            #[doc = #doc]
1825            pub fn #method_ident(&self) -> #root::query::QuerySet<#far> {
1826                use #root::core::{Filter, Model as _, Op, SelectQuery, WhereExpr};
1827                let intermediate_schema =
1828                    <#intermediate as #root::core::Model>::SCHEMA;
1829                let sub = SelectQuery {
1830                    where_clause: WhereExpr::Predicate(Filter {
1831                        column: #intermediate_fk_column,
1832                        op: Op::Eq,
1833                        value: self.__rustango_pk_value(),
1834                    }),
1835                    projection: ::core::option::Option::Some(
1836                        ::std::vec![#intermediate_pk_column],
1837                    ),
1838                    ..SelectQuery::new(intermediate_schema)
1839                };
1840                #root::query::QuerySet::<#far>::new().where_raw(
1841                    WhereExpr::InSubquery {
1842                        column: #far_fk_column,
1843                        negated: false,
1844                        subquery: ::std::boxed::Box::new(sub),
1845                    },
1846                )
1847            }
1848
1849            #[doc = #count_doc]
1850            pub async fn #count_ident(
1851                &self,
1852                pool: &#root::sql::Pool,
1853            ) -> ::core::result::Result<
1854                i64,
1855                #root::sql::ExecError,
1856            > {
1857                use #root::sql::CounterPool as _;
1858                self.#method_ident().count(pool).await
1859            }
1860
1861            /// Eloquent `$model->relation->get()` for the
1862            /// through-relation — bare-name hot-path over
1863            /// `self.<name>_through().fetch(&pool)`. Use this
1864            /// when you don't need further `.filter()` /
1865            /// `.order_by()` composition; falls back to the
1866            /// chainable accessor when you do. Avoids the `_pool`
1867            /// suffix on the most common call-site shape.
1868            pub async fn #fetch_ident(
1869                &self,
1870                pool: &#root::sql::Pool,
1871            ) -> ::core::result::Result<
1872                ::std::vec::Vec<#far>,
1873                #root::sql::ExecError,
1874            > {
1875                use #root::sql::FetcherPool as _;
1876                self.#method_ident().fetch(pool).await
1877            }
1878
1879            /// Eloquent `hasOneThrough` analog — bare-name shortcut
1880            /// over `self.<name>_through().first(&pool)`. Returns
1881            /// `None` when no far rows are reachable through the
1882            /// intermediate. Useful when at most one row is
1883            /// expected (latest comment by country, primary tag,
1884            /// etc.).
1885            pub async fn #first_ident(
1886                &self,
1887                pool: &#root::sql::Pool,
1888            ) -> ::core::result::Result<
1889                ::core::option::Option<#far>,
1890                #root::sql::ExecError,
1891            > {
1892                self.#method_ident().first(pool).await
1893            }
1894
1895            /// Pluck a single column from the far rows into `Vec<U>`
1896            /// — cheaper than the typed `<Far>` decode when only one
1897            /// scalar column is needed.
1898            pub async fn #pluck_ident<U>(
1899                &self,
1900                col: &'static str,
1901                pool: &#root::sql::Pool,
1902            ) -> ::core::result::Result<
1903                ::std::vec::Vec<U>,
1904                #root::sql::ExecError,
1905            >
1906            where
1907                U: #root::sql::MaybePgScalar
1908                    + #root::sql::MaybeMyScalar
1909                    + #root::sql::MaybeSqliteScalar
1910                    + ::core::marker::Send
1911                    + ::core::marker::Unpin,
1912            {
1913                self.#method_ident()
1914                    .values_list_flat(col)
1915                    .fetch::<U>(pool)
1916                    .await
1917            }
1918        }
1919    });
1920    quote! {
1921        impl #struct_name {
1922            #( #methods )*
1923        }
1924    }
1925}
1926
1927struct ColumnEntry {
1928    /// The struct field ident, used both for the inherent const name on
1929    /// the model and for the inner column type's name.
1930    ident: syn::Ident,
1931    /// The struct's field type, used as `Column::Value`.
1932    value_ty: Type,
1933    /// Rust-side field name (e.g. `"id"`).
1934    name: String,
1935    /// SQL-side column name (e.g. `"user_id"`).
1936    column: String,
1937    /// `#root::core::FieldType::I64` etc.
1938    field_type_tokens: TokenStream2,
1939}
1940
1941struct CollectedFields {
1942    field_schemas: Vec<TokenStream2>,
1943    from_row_inits: Vec<TokenStream2>,
1944    /// Aliased counterparts of `from_row_inits` — read columns via
1945    /// `format!("{prefix}__{col}")` aliases so a Model can be
1946    /// decoded from a JOINed row's projected target columns.
1947    from_aliased_row_inits: Vec<TokenStream2>,
1948    /// Static column-name list — used by the simple insert path
1949    /// (no `Auto<T>` fields). Aligned with `insert_values`.
1950    insert_columns: Vec<TokenStream2>,
1951    /// Static `Into<SqlValue>` expressions, one per field. Aligned
1952    /// with `insert_columns`. Used by the simple insert path only.
1953    insert_values: Vec<TokenStream2>,
1954    /// Per-field push expressions for the dynamic (Auto-aware)
1955    /// insert path. Each statement either unconditionally pushes
1956    /// `(column, value)` or, for an `Auto<T>` field, conditionally
1957    /// pushes only when `Auto::Set(_)`. Built only when `has_auto`.
1958    insert_pushes: Vec<TokenStream2>,
1959    /// SQL columns for `RETURNING` — one per `Auto<T>` field. Empty
1960    /// when `has_auto == false`.
1961    returning_cols: Vec<TokenStream2>,
1962    /// `self.<field> = Row::try_get(&row, "<col>")?;` for each Auto
1963    /// field. Run after `insert_returning` to populate the model.
1964    auto_assigns: Vec<TokenStream2>,
1965    /// `(ident, column_literal)` pairs for every Auto field. Used by
1966    /// the bulk_insert codegen to rebuild assigns against `_row_mut`
1967    /// instead of `self`.
1968    auto_field_idents: Vec<(syn::Ident, String)>,
1969    /// #1028 — `(ident, column)` for each `generated_as` field. Drives
1970    /// the PG/SQLite RETURNING refresh that decodes the DB-computed value
1971    /// back into the struct after insert (MySQL has no RETURNING → the
1972    /// field stays at its placeholder, deferred, matching Django 6.0).
1973    generated_field_idents: Vec<(syn::Ident, String)>,
1974    /// Inner `T` of the first `Auto<T>` field, for the MySQL
1975    /// `LAST_INSERT_ID()` assignment in `AssignAutoPkPool`.
1976    first_auto_value_ty: Option<Type>,
1977    /// Bulk-insert per-row pushes for **non-Auto fields only**. Used
1978    /// by the all-Auto-Unset bulk path (Auto cols dropped from
1979    /// `columns`).
1980    bulk_pushes_no_auto: Vec<TokenStream2>,
1981    /// Bulk-insert per-row pushes for **all fields including Auto**.
1982    /// Used by the all-Auto-Set bulk path (Auto col included with the
1983    /// caller-supplied value).
1984    bulk_pushes_all: Vec<TokenStream2>,
1985    /// Column-name literals for non-Auto fields only (paired with
1986    /// `bulk_pushes_no_auto`).
1987    bulk_columns_no_auto: Vec<TokenStream2>,
1988    /// Column-name literals for every field including Auto (paired
1989    /// with `bulk_pushes_all`).
1990    bulk_columns_all: Vec<TokenStream2>,
1991    /// `let _i_unset_<n> = matches!(rows[0].<auto_field>, Auto::Unset);`
1992    /// + the loop that asserts every row matches. One pair per Auto
1993    /// field. Empty when `has_auto == false`.
1994    bulk_auto_uniformity: Vec<TokenStream2>,
1995    /// Identifier of the first Auto field, used as the witness for
1996    /// "all rows agree on Set vs Unset". Set only when `has_auto`.
1997    first_auto_ident: Option<syn::Ident>,
1998    /// `true` if any field on the struct is `Auto<T>`.
1999    has_auto: bool,
2000    /// `true` when the primary-key field's Rust type is `Auto<T>`.
2001    /// Gates `save()` codegen — only Auto PKs let us infer
2002    /// insert-vs-update from the in-memory value.
2003    pk_is_auto: bool,
2004    /// `Assignment` constructors for every non-PK column. Drives the
2005    /// UPDATE branch of `save()`.
2006    update_assignments: Vec<TokenStream2>,
2007    /// Column name literals (`"col"`) for every non-PK, non-auto_now_add column.
2008    /// Drives the `ON CONFLICT ... DO UPDATE SET` clause in `upsert_on`.
2009    upsert_update_columns: Vec<TokenStream2>,
2010    primary_key: Option<(syn::Ident, String)>,
2011    column_entries: Vec<ColumnEntry>,
2012    /// Rust-side field names, in declaration order. Used to validate
2013    /// container attributes like `display = "…"`.
2014    field_names: Vec<String>,
2015    /// FK fields on this child model. Drives the reverse-relation
2016    /// helper emit — for each FK, the macro adds an inherent
2017    /// `<parent>::<child_table>_set(&self, executor) -> Vec<Self>`
2018    /// method on the parent type.
2019    fk_relations: Vec<FkRelation>,
2020    /// SQL column name of the `#[rustango(soft_delete)]` field, if
2021    /// the model has one. Drives emission of the `soft_delete_on` /
2022    /// `restore_on` inherent methods. At most one such column per
2023    /// model is allowed; collect_fields rejects duplicates.
2024    soft_delete_column: Option<String>,
2025    /// Rust field ident of the `#[rustango(soft_delete)]` field —
2026    /// companion to `soft_delete_column` for emitting predicates
2027    /// that need to read the field off `&self` (e.g. `trashed()`).
2028    soft_delete_field_ident: Option<syn::Ident>,
2029}
2030
2031#[derive(Clone)]
2032struct FkRelation {
2033    /// Inner type of `ForeignKey<T, K>` — the parent model. The reverse
2034    /// helper is emitted as `impl <ParentType> { … }`.
2035    parent_type: Type,
2036    /// SQL column name on the child table for this FK (e.g. `"author"`).
2037    /// Used in the generated `WHERE <fk_column> = $1` clause.
2038    fk_column: String,
2039    /// `K`'s underlying scalar kind — drives the `match SqlValue { … }`
2040    /// arm emitted by [`load_related_impl_tokens`]. `I64` for the
2041    /// default `ForeignKey<T>` (no explicit K); other kinds when the
2042    /// user wrote `ForeignKey<T, String>`, `ForeignKey<T, Uuid>`, etc.
2043    pk_kind: DetectedKind,
2044    /// `true` when the field is `Option<ForeignKey<T, K>>` (nullable
2045    /// FK column). Drives the `Some(...)` wrapping in load_related
2046    /// assignment and `.as_ref().map(...)` in the FK PK accessor so
2047    /// the codegen matches the field's declared shape.
2048    nullable: bool,
2049    /// `#[rustango(related_name = "...")]` per-FK reverse-accessor
2050    /// override. When set, the reverse helper picks this name instead
2051    /// of `default_related_name` / `<child_snake>_set`. Follow-up to
2052    /// #816 (issue's "Related" note re: per-FK override).
2053    related_name: Option<String>,
2054}
2055
2056fn collect_fields(named: &syn::FieldsNamed, table: &str) -> syn::Result<CollectedFields> {
2057    let root = rustango_root();
2058    let cap = named.named.len();
2059    let mut out = CollectedFields {
2060        field_schemas: Vec::with_capacity(cap),
2061        from_row_inits: Vec::with_capacity(cap),
2062        from_aliased_row_inits: Vec::with_capacity(cap),
2063        insert_columns: Vec::with_capacity(cap),
2064        insert_values: Vec::with_capacity(cap),
2065        insert_pushes: Vec::with_capacity(cap),
2066        returning_cols: Vec::new(),
2067        auto_assigns: Vec::new(),
2068        auto_field_idents: Vec::new(),
2069        generated_field_idents: Vec::new(),
2070        first_auto_value_ty: None,
2071        bulk_pushes_no_auto: Vec::with_capacity(cap),
2072        bulk_pushes_all: Vec::with_capacity(cap),
2073        bulk_columns_no_auto: Vec::with_capacity(cap),
2074        bulk_columns_all: Vec::with_capacity(cap),
2075        bulk_auto_uniformity: Vec::new(),
2076        first_auto_ident: None,
2077        has_auto: false,
2078        pk_is_auto: false,
2079        update_assignments: Vec::with_capacity(cap),
2080        upsert_update_columns: Vec::with_capacity(cap),
2081        primary_key: None,
2082        column_entries: Vec::with_capacity(cap),
2083        field_names: Vec::with_capacity(cap),
2084        fk_relations: Vec::new(),
2085        soft_delete_column: None,
2086        soft_delete_field_ident: None,
2087    };
2088
2089    for field in &named.named {
2090        let info = process_field(field, table)?;
2091        out.field_names.push(info.ident.to_string());
2092        out.field_schemas.push(info.schema);
2093        out.from_row_inits.push(info.from_row_init);
2094        out.from_aliased_row_inits.push(info.from_aliased_row_init);
2095        if let Some(parent_ty) = info.fk_inner.clone() {
2096            out.fk_relations.push(FkRelation {
2097                parent_type: parent_ty,
2098                fk_column: info.column.clone(),
2099                pk_kind: info.fk_pk_kind,
2100                nullable: info.nullable,
2101                related_name: info.related_name.clone(),
2102            });
2103        }
2104        if info.soft_delete {
2105            if out.soft_delete_column.is_some() {
2106                return Err(syn::Error::new_spanned(
2107                    field,
2108                    "only one field may be marked `#[rustango(soft_delete)]`",
2109                ));
2110            }
2111            out.soft_delete_column = Some(info.column.clone());
2112            out.soft_delete_field_ident = Some(info.ident.clone());
2113        }
2114        let column = info.column.as_str();
2115        let ident = info.ident;
2116        // Generated columns (`#[rustango(generated_as = "EXPR")]`)
2117        // skip every write path — Postgres recomputes the value
2118        // from EXPR. Push only the column-entry record (so typed
2119        // column constants still exist for filtering / projection)
2120        // and the schema literal (already pushed above) and move
2121        // on. No insert_columns/values, no insert_pushes, no
2122        // bulk_*, no update_assignments, no upsert_update_columns,
2123        // no returning_cols.
2124        if info.generated_as.is_some() {
2125            out.column_entries.push(ColumnEntry {
2126                ident: ident.clone(),
2127                value_ty: info.value_ty.clone(),
2128                name: ident.to_string(),
2129                column: info.column.clone(),
2130                field_type_tokens: info.field_type_tokens,
2131            });
2132            // #1028 — refresh the DB-computed value after insert on
2133            // RETURNING-capable backends. The column joins `returning_cols`
2134            // (so PG/SQLite `INSERT … RETURNING` includes it) and the
2135            // ident is recorded so the `AssignAutoPkPool` impl decodes it
2136            // back into the struct. MySQL has no `INSERT … RETURNING`, so
2137            // it keeps the placeholder (deferred refresh, matching Django).
2138            out.returning_cols.push(quote!(#column));
2139            out.generated_field_idents
2140                .push((ident.clone(), info.column.clone()));
2141            continue;
2142        }
2143        out.insert_columns.push(quote!(#column));
2144        out.insert_values.push(quote! {
2145            ::core::convert::Into::<#root::core::SqlValue>::into(
2146                ::core::clone::Clone::clone(&self.#ident)
2147            )
2148        });
2149        if info.auto {
2150            out.has_auto = true;
2151            if out.first_auto_ident.is_none() {
2152                out.first_auto_ident = Some(ident.clone());
2153                out.first_auto_value_ty = auto_inner_type(info.value_ty).cloned();
2154            }
2155            // `default_uuid_v7` (issue #823) generates the PK Rust-side
2156            // before binding, so the value is already in
2157            // `self.#ident` after the insert_push — RETURNING is
2158            // redundant. Skip adding this column to returning_cols /
2159            // auto_assigns / auto_field_idents to avoid (a) an
2160            // unnecessary RETURNING column on every dialect, and (b)
2161            // the MySQL `LAST_INSERT_ID()` path that can only fill an
2162            // integer PK.
2163            if !info.default_uuid_v7 {
2164                out.returning_cols.push(quote!(#column));
2165                out.auto_field_idents
2166                    .push((ident.clone(), info.column.clone()));
2167                out.auto_assigns.push(quote! {
2168                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
2169                });
2170            }
2171            if info.default_uuid_v7 {
2172                // Rust-side UUIDv7 generation (issue #823, Eloquent
2173                // `HasUuids`). Auto::Unset → fill with `Uuid::now_v7()`
2174                // and bind; Auto::Set → bind the user's value. The
2175                // column is ALWAYS present in the INSERT statement —
2176                // no RETURNING / no DB DEFAULT needed.
2177                out.insert_pushes.push(quote! {
2178                    if matches!(&self.#ident, #root::sql::Auto::Unset) {
2179                        self.#ident = #root::sql::Auto::Set(
2180                            #root::__uuid::Uuid::now_v7(),
2181                        );
2182                    }
2183                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2184                        _columns.push(#column);
2185                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2186                            ::core::clone::Clone::clone(_v)
2187                        ));
2188                    }
2189                });
2190            } else {
2191                out.insert_pushes.push(quote! {
2192                    if let #root::sql::Auto::Set(_v) = &self.#ident {
2193                        _columns.push(#column);
2194                        _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2195                            ::core::clone::Clone::clone(_v)
2196                        ));
2197                    }
2198                });
2199            }
2200            // Bulk: Auto fields appear only in the all-Set path,
2201            // never in the Unset path (we drop them from `columns`).
2202            out.bulk_columns_all.push(quote!(#column));
2203            out.bulk_pushes_all.push(quote! {
2204                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2205                    ::core::clone::Clone::clone(&_row.#ident)
2206                ));
2207            });
2208            // Uniformity check: every row's Auto state must match the
2209            // first row's. Mixed Set/Unset within one bulk_insert is
2210            // rejected here so the column list stays consistent.
2211            let ident_clone = ident.clone();
2212            out.bulk_auto_uniformity.push(quote! {
2213                for _r in rows.iter().skip(1) {
2214                    if matches!(_r.#ident_clone, #root::sql::Auto::Unset) != _first_unset {
2215                        return ::core::result::Result::Err(
2216                            #root::sql::ExecError::Sql(
2217                                #root::sql::SqlError::BulkAutoMixed
2218                            )
2219                        );
2220                    }
2221                }
2222            });
2223        } else {
2224            out.insert_pushes.push(quote! {
2225                _columns.push(#column);
2226                _values.push(::core::convert::Into::<#root::core::SqlValue>::into(
2227                    ::core::clone::Clone::clone(&self.#ident)
2228                ));
2229            });
2230            // Bulk: non-Auto fields appear in BOTH paths.
2231            out.bulk_columns_no_auto.push(quote!(#column));
2232            out.bulk_columns_all.push(quote!(#column));
2233            let push_expr = quote! {
2234                _row_vals.push(::core::convert::Into::<#root::core::SqlValue>::into(
2235                    ::core::clone::Clone::clone(&_row.#ident)
2236                ));
2237            };
2238            out.bulk_pushes_no_auto.push(push_expr.clone());
2239            out.bulk_pushes_all.push(push_expr);
2240        }
2241        if info.primary_key {
2242            if out.primary_key.is_some() {
2243                return Err(syn::Error::new_spanned(
2244                    field,
2245                    "only one field may be marked `#[rustango(primary_key)]`",
2246                ));
2247            }
2248            out.primary_key = Some((ident.clone(), info.column.clone()));
2249            if info.auto {
2250                out.pk_is_auto = true;
2251            }
2252        } else if info.auto_now_add {
2253            // Immutable post-insert: skip from UPDATE entirely.
2254        } else if info.auto_now {
2255            // `auto_now` columns: bind `chrono::Utc::now()` on every
2256            // UPDATE so the column is always overridden with the
2257            // wall-clock at write time, regardless of what value the
2258            // user left in the struct field.
2259            out.update_assignments.push(quote! {
2260                #root::core::Assignment {
2261                    column: #column,
2262                    value: ::core::convert::Into::<#root::core::Expr>::into(
2263                        ::core::convert::Into::<#root::core::SqlValue>::into(
2264                            #root::__chrono::Utc::now()
2265                        )
2266                    ),
2267                }
2268            });
2269            out.upsert_update_columns.push(quote!(#column));
2270        } else {
2271            out.update_assignments.push(quote! {
2272                #root::core::Assignment {
2273                    column: #column,
2274                    value: ::core::convert::Into::<#root::core::Expr>::into(
2275                        ::core::convert::Into::<#root::core::SqlValue>::into(
2276                            ::core::clone::Clone::clone(&self.#ident)
2277                        )
2278                    ),
2279                }
2280            });
2281            out.upsert_update_columns.push(quote!(#column));
2282        }
2283        out.column_entries.push(ColumnEntry {
2284            ident: ident.clone(),
2285            value_ty: info.value_ty.clone(),
2286            name: ident.to_string(),
2287            column: info.column.clone(),
2288            field_type_tokens: info.field_type_tokens,
2289        });
2290    }
2291    Ok(out)
2292}
2293
2294fn model_impl_tokens(
2295    struct_name: &syn::Ident,
2296    model_name: &str,
2297    table: &str,
2298    display: Option<&str>,
2299    app_label: Option<&str>,
2300    admin: Option<&AdminAttrs>,
2301    default_order: &[(String, bool, proc_macro2::Span)],
2302    field_schemas: &[TokenStream2],
2303    soft_delete_column: Option<&str>,
2304    permissions: bool,
2305    audit_track: Option<&[String]>,
2306    m2m_relations: &[M2MAttr],
2307    indexes: &[IndexAttr],
2308    checks: &[CheckAttr],
2309    excludes: &[ExcludeAttr],
2310    composite_fks: &[CompositeFkAttr],
2311    generic_fks: &[GenericFkAttr],
2312    scope: Option<&str>,
2313    is_view: bool,
2314    verbose_name: Option<&str>,
2315    verbose_name_plural: Option<&str>,
2316    managed: bool,
2317    base_manager_name: Option<&str>,
2318    order_with_respect_to: Option<&str>,
2319    proxy: bool,
2320    required_db_features: &[String],
2321    required_db_vendor: Option<&str>,
2322    default_related_name: Option<&str>,
2323    db_table_comment: Option<&str>,
2324    get_latest_by: Option<(&str, bool)>,
2325    extra_permissions: &[(String, String)],
2326    default_permissions: &[String],
2327    global_scopes: &[GlobalScopeAttr],
2328    reverse_has_relations: &[ReverseHasAttr],
2329    generic_has_relations: &[GenericHasAttr],
2330) -> TokenStream2 {
2331    let root = rustango_root();
2332    let display_tokens = if let Some(name) = display {
2333        quote!(::core::option::Option::Some(#name))
2334    } else {
2335        quote!(::core::option::Option::None)
2336    };
2337    let app_label_tokens = if let Some(name) = app_label {
2338        quote!(::core::option::Option::Some(#name))
2339    } else {
2340        quote!(::core::option::Option::None)
2341    };
2342    let soft_delete_tokens = if let Some(col) = soft_delete_column {
2343        quote!(::core::option::Option::Some(#col))
2344    } else {
2345        quote!(::core::option::Option::None)
2346    };
2347    let audit_track_tokens = match audit_track {
2348        None => quote!(::core::option::Option::None),
2349        Some(names) => {
2350            let lits = names.iter().map(|n| n.as_str());
2351            quote!(::core::option::Option::Some(&[ #(#lits),* ]))
2352        }
2353    };
2354    let admin_tokens = admin_config_tokens(admin);
2355    // Default `tenant` so single-tenant projects (no `scope` attr
2356    // anywhere) keep the v0.24.x behavior. Container-attr parser
2357    // already validated the value is "registry" or "tenant".
2358    let scope_tokens = match scope.map(|s| s.to_ascii_lowercase()).as_deref() {
2359        Some("registry") => quote!(#root::core::ModelScope::Registry),
2360        _ => quote!(#root::core::ModelScope::Tenant),
2361    };
2362    let verbose_name_tokens = optional_str(verbose_name);
2363    let verbose_name_plural_tokens = optional_str(verbose_name_plural);
2364    let base_manager_name_tokens = optional_str(base_manager_name);
2365    let order_with_respect_to_tokens = optional_str(order_with_respect_to);
2366    let required_db_features_lits: Vec<&str> =
2367        required_db_features.iter().map(String::as_str).collect();
2368    let required_db_vendor_tokens = optional_str(required_db_vendor);
2369    let default_related_name_tokens = optional_str(default_related_name);
2370    let db_table_comment_tokens = optional_str(db_table_comment);
2371    let get_latest_by_tokens = match get_latest_by {
2372        Some((col, desc)) => {
2373            quote!(::core::option::Option::Some((#col, #desc)))
2374        }
2375        None => quote!(::core::option::Option::None),
2376    };
2377    let extra_permission_tokens: Vec<_> = extra_permissions
2378        .iter()
2379        .map(|(c, l)| quote!((#c, #l)))
2380        .collect();
2381    let default_permission_tokens: Vec<_> = default_permissions
2382        .iter()
2383        .map(|action| quote!(#action))
2384        .collect();
2385    let indexes_tokens = indexes.iter().map(|idx| {
2386        // When no explicit `name = "..."` was given, derive a stable,
2387        // collision-free name from the table + columns (Django-shape
2388        // `<table>_<col>_<col>_idx`) instead of a shared literal that
2389        // would clash the moment a model declares two unnamed indexes.
2390        // Capped at Postgres's 63-char identifier limit.
2391        let derived_name = idx.name.clone().unwrap_or_else(|| {
2392            let mut n = format!("{table}_{}_idx", idx.columns.join("_"));
2393            if n.len() > 63 {
2394                n.truncate(63);
2395            }
2396            n
2397        });
2398        let name = derived_name.as_str();
2399        let cols: Vec<&str> = idx.columns.iter().map(String::as_str).collect();
2400        let unique = idx.unique;
2401        // Map the parsed method string onto the IndexMethod enum
2402        // variant — kept at the codegen layer so the IR doesn't
2403        // carry the string form.
2404        let method_variant = match idx.method.as_str() {
2405            "gin" => quote!(#root::core::IndexMethod::Gin),
2406            "gist" => quote!(#root::core::IndexMethod::Gist),
2407            "brin" => quote!(#root::core::IndexMethod::Brin),
2408            "spgist" => quote!(#root::core::IndexMethod::SpGist),
2409            "hash" => quote!(#root::core::IndexMethod::Hash),
2410            "bloom" => quote!(#root::core::IndexMethod::Bloom),
2411            _ => quote!(#root::core::IndexMethod::BTree),
2412        };
2413        let where_clause = match &idx.where_clause {
2414            Some(s) => quote!(::core::option::Option::Some(#s)),
2415            None => quote!(::core::option::Option::None),
2416        };
2417        let include_lits: Vec<&str> = idx.include.iter().map(String::as_str).collect();
2418        quote! {
2419            #root::core::IndexSchema {
2420                name: #name,
2421                columns: &[ #(#cols),* ],
2422                unique: #unique,
2423                method: #method_variant,
2424                where_clause: #where_clause,
2425                include: &[ #(#include_lits),* ],
2426            }
2427        }
2428    });
2429    let checks_tokens = checks.iter().map(|c| {
2430        let name = c.name.as_str();
2431        let expr = c.expr.as_str();
2432        quote! {
2433            #root::core::CheckConstraint {
2434                name: #name,
2435                expr: #expr,
2436            }
2437        }
2438    });
2439    let excludes_tokens = excludes.iter().map(|e| {
2440        let name = e.name.as_str();
2441        let using = e.using.as_str();
2442        let element_tokens = e.elements.iter().map(|(col, op)| {
2443            let col_s = col.as_str();
2444            let op_s = op.as_str();
2445            quote!((#col_s, #op_s))
2446        });
2447        let where_tokens = match e.where_clause.as_deref() {
2448            Some(w) => quote!(::core::option::Option::Some(#w)),
2449            None => quote!(::core::option::Option::None),
2450        };
2451        quote! {
2452            #root::core::ExclusionConstraint {
2453                name: #name,
2454                using: #using,
2455                elements: &[ #(#element_tokens),* ],
2456                where_clause: #where_tokens,
2457            }
2458        }
2459    });
2460    let composite_fk_tokens = composite_fks.iter().map(|rel| {
2461        let name = rel.name.as_str();
2462        let to = rel.to.as_str();
2463        let from_cols: Vec<&str> = rel.from.iter().map(String::as_str).collect();
2464        let on_cols: Vec<&str> = rel.on.iter().map(String::as_str).collect();
2465        quote! {
2466            #root::core::CompositeFkRelation {
2467                name: #name,
2468                to: #to,
2469                from: &[ #(#from_cols),* ],
2470                on: &[ #(#on_cols),* ],
2471            }
2472        }
2473    });
2474    let generic_fk_tokens = generic_fks.iter().map(|rel| {
2475        let name = rel.name.as_str();
2476        let ct_col = rel.ct_column.as_str();
2477        let pk_col = rel.pk_column.as_str();
2478        quote! {
2479            #root::core::GenericRelation {
2480                name: #name,
2481                ct_column: #ct_col,
2482                pk_column: #pk_col,
2483            }
2484        }
2485    });
2486    // Issue #291 / T2.5 — `default_order` slice literal. Empty when
2487    // no `#[rustango(default_order = "...")]` attribute was supplied.
2488    let default_order_tokens = default_order.iter().map(|(col, desc, _)| {
2489        let col_lit = col.as_str();
2490        quote! { (#col_lit, #desc) }
2491    });
2492
2493    // Issue #820 — `global_scopes` slice literal. Empty when no
2494    // `#[rustango(global_scope(...))]` attribute was supplied. The
2495    // `apply` path is re-emitted verbatim so it resolves in the
2496    // consumer's scope; the name is stored as a string literal.
2497    let global_scope_tokens = global_scopes.iter().map(|s| {
2498        let name = s.name.as_str();
2499        let apply = &s.apply;
2500        quote! {
2501            #root::core::GlobalScope {
2502                name: #name,
2503                apply: #apply,
2504            }
2505        }
2506    });
2507
2508    let m2m_tokens = m2m_relations.iter().map(|rel| {
2509        let name = rel.name.as_str();
2510        let to = rel.to.as_str();
2511        let through = rel.through.as_str();
2512        let src = rel.src.as_str();
2513        let dst = rel.dst.as_str();
2514        let auto_create = rel.auto_create;
2515        quote! {
2516            #root::core::M2MRelation {
2517                name: #name,
2518                to: #to,
2519                through: #through,
2520                src_col: #src,
2521                dst_col: #dst,
2522                auto_create: #auto_create,
2523            }
2524        }
2525    });
2526    // Issue #830 sub-piece: emit `Model::reverse_relations()` override
2527    // when the model declares `#[rustango(reverse_has(...))]`. Each
2528    // entry uses `<Child as Model>::SCHEMA.table` so the literal stays
2529    // a const expression. Models without reverse_has fall through to
2530    // the trait's empty default — no override emitted.
2531    let reverse_relations_override = if reverse_has_relations.is_empty() {
2532        quote!()
2533    } else {
2534        let entries = reverse_has_relations.iter().map(|rel| {
2535            let name = rel.name.as_str();
2536            let child = &rel.child;
2537            let child_fk_column = rel.child_fk_column.as_str();
2538            let self_pk_column = rel.self_pk_column.as_str();
2539            quote! {
2540                #root::core::ReverseRelation {
2541                    name: #name,
2542                    child_schema: <#child as #root::core::Model>::SCHEMA,
2543                    child_fk_column: #child_fk_column,
2544                    self_pk_column: #self_pk_column,
2545                }
2546            }
2547        });
2548        quote! {
2549            fn reverse_relations() -> &'static [#root::core::ReverseRelation] {
2550                const RELS: &[#root::core::ReverseRelation] = &[ #(#entries),* ];
2551                RELS
2552            }
2553        }
2554    };
2555    // Issue #830 — `Model::generic_reverse_relations()` override for
2556    // `#[rustango(generic_has(...))]` (the reverse generic-FK arm).
2557    let generic_reverse_relations_override = if generic_has_relations.is_empty() {
2558        quote!()
2559    } else {
2560        let entries = generic_has_relations.iter().map(|rel| {
2561            let name = rel.name.as_str();
2562            let child = &rel.child;
2563            let ct_column = rel.ct_column.as_str();
2564            let pk_column = rel.pk_column.as_str();
2565            let self_pk_column = rel.self_pk_column.as_str();
2566            quote! {
2567                #root::core::GenericReverseRelation {
2568                    name: #name,
2569                    child_schema: <#child as #root::core::Model>::SCHEMA,
2570                    ct_column: #ct_column,
2571                    pk_column: #pk_column,
2572                    self_pk_column: #self_pk_column,
2573                }
2574            }
2575        });
2576        quote! {
2577            fn generic_reverse_relations() -> &'static [#root::core::GenericReverseRelation] {
2578                const RELS: &[#root::core::GenericReverseRelation] = &[ #(#entries),* ];
2579                RELS
2580            }
2581        }
2582    };
2583    quote! {
2584        impl #root::core::Model for #struct_name {
2585            const SCHEMA: &'static #root::core::ModelSchema = &#root::core::ModelSchema {
2586                name: #model_name,
2587                table: #table,
2588                fields: &[ #(#field_schemas),* ],
2589                display: #display_tokens,
2590                app_label: #app_label_tokens,
2591                admin: #admin_tokens,
2592                soft_delete_column: #soft_delete_tokens,
2593                permissions: #permissions,
2594                audit_track: #audit_track_tokens,
2595                m2m: &[ #(#m2m_tokens),* ],
2596                indexes: &[ #(#indexes_tokens),* ],
2597                check_constraints: &[ #(#checks_tokens),* ],
2598                exclusion_constraints: &[ #(#excludes_tokens),* ],
2599                composite_relations: &[ #(#composite_fk_tokens),* ],
2600                generic_relations: &[ #(#generic_fk_tokens),* ],
2601                scope: #scope_tokens,
2602                default_order: &[ #(#default_order_tokens),* ],
2603                is_view: #is_view,
2604                verbose_name: #verbose_name_tokens,
2605                verbose_name_plural: #verbose_name_plural_tokens,
2606                managed: #managed,
2607                base_manager_name: #base_manager_name_tokens,
2608                order_with_respect_to: #order_with_respect_to_tokens,
2609                proxy: #proxy,
2610                required_db_features: &[ #(#required_db_features_lits),* ],
2611                required_db_vendor: #required_db_vendor_tokens,
2612                default_related_name: #default_related_name_tokens,
2613                db_table_comment: #db_table_comment_tokens,
2614                get_latest_by: #get_latest_by_tokens,
2615                extra_permissions: &[ #(#extra_permission_tokens),* ],
2616                default_permissions: &[ #(#default_permission_tokens),* ],
2617                global_scopes: &[ #(#global_scope_tokens),* ],
2618            };
2619
2620            #reverse_relations_override
2621            #generic_reverse_relations_override
2622        }
2623    }
2624}
2625
2626/// Emit the `admin: Option<&'static AdminConfig>` field for the model
2627/// schema. `None` when the user wrote no `#[rustango(admin(...))]`;
2628/// otherwise a static reference to a populated `AdminConfig`.
2629fn admin_config_tokens(admin: Option<&AdminAttrs>) -> TokenStream2 {
2630    let root = rustango_root();
2631    let Some(admin) = admin else {
2632        return quote!(::core::option::Option::None);
2633    };
2634
2635    let list_display = admin
2636        .list_display
2637        .as_ref()
2638        .map(|(v, _)| v.as_slice())
2639        .unwrap_or(&[]);
2640    let list_display_lits = list_display.iter().map(|s| s.as_str());
2641
2642    let search_fields = admin
2643        .search_fields
2644        .as_ref()
2645        .map(|(v, _)| v.as_slice())
2646        .unwrap_or(&[]);
2647    let search_fields_lits = search_fields.iter().map(|s| s.as_str());
2648
2649    let readonly_fields = admin
2650        .readonly_fields
2651        .as_ref()
2652        .map(|(v, _)| v.as_slice())
2653        .unwrap_or(&[]);
2654    let readonly_fields_lits = readonly_fields.iter().map(|s| s.as_str());
2655
2656    let list_filter = admin
2657        .list_filter
2658        .as_ref()
2659        .map(|(v, _)| v.as_slice())
2660        .unwrap_or(&[]);
2661    let list_filter_lits = list_filter.iter().map(|s| s.as_str());
2662
2663    let actions = admin
2664        .actions
2665        .as_ref()
2666        .map(|(v, _)| v.as_slice())
2667        .unwrap_or(&[]);
2668    let actions_lits = actions.iter().map(|s| s.as_str());
2669
2670    let fieldsets = admin
2671        .fieldsets
2672        .as_ref()
2673        .map(|(v, _)| v.as_slice())
2674        .unwrap_or(&[]);
2675    let fieldset_tokens = fieldsets.iter().map(|(title, fields)| {
2676        let title = title.as_str();
2677        let field_lits = fields.iter().map(|s| s.as_str());
2678        quote!(#root::core::Fieldset {
2679            title: #title,
2680            fields: &[ #( #field_lits ),* ],
2681        })
2682    });
2683
2684    let list_display_links = admin
2685        .list_display_links
2686        .as_ref()
2687        .map(|(v, _)| v.as_slice())
2688        .unwrap_or(&[]);
2689    let list_display_links_lits = list_display_links.iter().map(|s| s.as_str());
2690
2691    let search_help_text = admin.search_help_text.as_deref().unwrap_or("");
2692    let actions_on_top = admin.actions_on_top.unwrap_or(true);
2693    let actions_on_bottom = admin.actions_on_bottom.unwrap_or(false);
2694    let date_hierarchy = admin.date_hierarchy.as_deref().unwrap_or("");
2695
2696    let prepopulated = admin
2697        .prepopulated_fields
2698        .as_ref()
2699        .map(|(v, _)| v.as_slice())
2700        .unwrap_or(&[]);
2701    let prepopulated_tokens = prepopulated.iter().map(|(target, sources)| {
2702        let target = target.as_str();
2703        let source_lits = sources.iter().map(|s| s.as_str());
2704        quote!(#root::core::PrepopulatedField {
2705            target: #target,
2706            sources: &[ #( #source_lits ),* ],
2707        })
2708    });
2709
2710    let raw_id_fields = admin
2711        .raw_id_fields
2712        .as_ref()
2713        .map(|(v, _)| v.as_slice())
2714        .unwrap_or(&[]);
2715    let raw_id_fields_lits = raw_id_fields.iter().map(|s| s.as_str());
2716
2717    let autocomplete_fields = admin
2718        .autocomplete_fields
2719        .as_ref()
2720        .map(|(v, _)| v.as_slice())
2721        .unwrap_or(&[]);
2722    let autocomplete_fields_lits = autocomplete_fields.iter().map(|s| s.as_str());
2723
2724    // #352 — list_select_related accepts "all" | "none" | "field, field, …".
2725    let list_select_related_tokens = match admin.list_select_related.as_deref() {
2726        None | Some("all") => quote!(#root::core::ListSelectRelated::All),
2727        Some("none") => quote!(#root::core::ListSelectRelated::None),
2728        Some(raw) => {
2729            let names: Vec<&str> = raw
2730                .split(',')
2731                .map(str::trim)
2732                .filter(|s| !s.is_empty())
2733                .collect();
2734            quote!(#root::core::ListSelectRelated::Only(&[ #( #names ),* ]))
2735        }
2736    };
2737
2738    // #359 — formfield_overrides: parse "field:widget,field2:widget2" into
2739    // a Vec<(String, String)>. Empty / unset → no overrides.
2740    let formfield_pairs: Vec<(&str, &str)> = admin
2741        .formfield_overrides
2742        .as_ref()
2743        .map(|(v, _)| v.iter().map(|(f, w)| (f.as_str(), w.as_str())).collect())
2744        .unwrap_or_default();
2745    let formfield_tokens = formfield_pairs.iter().map(|(field, widget)| {
2746        let field = *field;
2747        let widget = *widget;
2748        quote!((#field, #widget))
2749    });
2750
2751    let list_per_page = admin.list_per_page.unwrap_or(0);
2752
2753    let ordering_pairs = admin
2754        .ordering
2755        .as_ref()
2756        .map(|(v, _)| v.as_slice())
2757        .unwrap_or(&[]);
2758    let ordering_tokens = ordering_pairs.iter().map(|(name, desc)| {
2759        let name = name.as_str();
2760        let desc = *desc;
2761        quote!((#name, #desc))
2762    });
2763
2764    quote! {
2765        ::core::option::Option::Some(&#root::core::AdminConfig {
2766            list_display: &[ #( #list_display_lits ),* ],
2767            search_fields: &[ #( #search_fields_lits ),* ],
2768            list_per_page: #list_per_page,
2769            ordering: &[ #( #ordering_tokens ),* ],
2770            readonly_fields: &[ #( #readonly_fields_lits ),* ],
2771            list_filter: &[ #( #list_filter_lits ),* ],
2772            actions: &[ #( #actions_lits ),* ],
2773            fieldsets: &[ #( #fieldset_tokens ),* ],
2774            list_display_links: &[ #( #list_display_links_lits ),* ],
2775            search_help_text: #search_help_text,
2776            actions_on_top: #actions_on_top,
2777            actions_on_bottom: #actions_on_bottom,
2778            date_hierarchy: #date_hierarchy,
2779            prepopulated_fields: &[ #( #prepopulated_tokens ),* ],
2780            raw_id_fields: &[ #( #raw_id_fields_lits ),* ],
2781            autocomplete_fields: &[ #( #autocomplete_fields_lits ),* ],
2782            list_select_related: #list_select_related_tokens,
2783            formfield_overrides: &[ #( #formfield_tokens ),* ],
2784        })
2785    }
2786}
2787
2788fn inherent_impl_tokens(
2789    struct_name: &syn::Ident,
2790    fields: &CollectedFields,
2791    primary_key: Option<&(syn::Ident, String)>,
2792    column_consts: &TokenStream2,
2793    audited_fields: Option<&[&ColumnEntry]>,
2794    indexes: &[IndexAttr],
2795    manager_fns: &[syn::Ident],
2796) -> TokenStream2 {
2797    let root = rustango_root();
2798    // Audit-emit fragments threaded into write paths. Non-empty only
2799    // when the model carries `#[rustango(audit(...))]`. They reborrow
2800    // `_executor` (a `&mut PgConnection` for audited models — the
2801    // macro switches the signature below) so the data write and the
2802    // audit INSERT both run on the same caller-supplied connection.
2803    let executor_passes_to_data_write = if audited_fields.is_some() {
2804        quote!(&mut *_executor)
2805    } else {
2806        quote!(_executor)
2807    };
2808    let executor_param = if audited_fields.is_some() {
2809        quote!(_executor: &mut #root::sql::sqlx::PgConnection)
2810    } else {
2811        quote!(_executor: _E)
2812    };
2813    let executor_generics = if audited_fields.is_some() {
2814        quote!()
2815    } else {
2816        quote!(<'_c, _E>)
2817    };
2818    let executor_where = if audited_fields.is_some() {
2819        quote!()
2820    } else {
2821        quote! {
2822            where
2823                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
2824        }
2825    };
2826    // For audited models the `_on` methods take `&mut PgConnection`, so
2827    // the &PgPool convenience wrappers (`save`, `insert`, `delete`)
2828    // must acquire a connection first. Non-audited models keep the
2829    // direct delegation since `&PgPool` IS an Executor.
2830    let pool_to_save_on = if audited_fields.is_some() {
2831        quote! {
2832            let mut _conn = pool.acquire().await?;
2833            self.save_on(&mut *_conn).await
2834        }
2835    } else {
2836        quote!(self.save_on(pool).await)
2837    };
2838    let pool_to_insert_on = if audited_fields.is_some() {
2839        quote! {
2840            let mut _conn = pool.acquire().await?;
2841            self.insert_on(&mut *_conn).await
2842        }
2843    } else {
2844        quote!(self.insert_on(pool).await)
2845    };
2846    let pool_to_delete_on = if audited_fields.is_some() {
2847        quote! {
2848            let mut _conn = pool.acquire().await?;
2849            self.delete_on(&mut *_conn).await
2850        }
2851    } else {
2852        quote!(self.delete_on(pool).await)
2853    };
2854    let pool_to_bulk_insert_on = if audited_fields.is_some() {
2855        quote! {
2856            let mut _conn = pool.acquire().await?;
2857            Self::bulk_insert_on(rows, &mut *_conn).await
2858        }
2859    } else {
2860        quote!(Self::bulk_insert_on(rows, pool).await)
2861    };
2862    // Pre-existing bug surfaced by batch 22's first audited Auto<T>
2863    // PK test model: `upsert(&PgPool)` body called `self.upsert_on(pool)`
2864    // directly, but `upsert_on` for audited models takes
2865    // `&mut PgConnection` (the audit emit needs a real connection).
2866    // Add the missing acquire shim to keep audited Auto-PK upsert
2867    // compiling.
2868    let pool_to_upsert_on = if audited_fields.is_some() {
2869        quote! {
2870            let mut _conn = pool.acquire().await?;
2871            self.upsert_on(&mut *_conn).await
2872        }
2873    } else {
2874        quote!(self.upsert_on(pool).await)
2875    };
2876
2877    // `insert_pool(&Pool)` — v0.23.0-batch9. Non-audited models only
2878    // (audit-on-connection over &Pool needs a bi-dialect transaction
2879    // helper, deferred). Two body shapes:
2880    // - has_auto: build InsertQuery skipping Auto::Unset columns,
2881    //   request Auto cols in `returning`, dispatch via
2882    //   `insert_returning_pool`, then on the returned `PgRow` /
2883    //   `MySqlAutoId(id)` enum — pull each Auto field from the PG
2884    //   row OR drop the single i64 into the first Auto field on MySQL
2885    //   (multi-Auto models on MySQL error at runtime since
2886    //   `LAST_INSERT_ID()` only reports one)
2887    // - non-Auto: build InsertQuery with explicit columns/values and
2888    //   call `insert_pool` (no returning needed)
2889    // pool_insert_method body for the audited Auto-PK case is moved
2890    // to after audit_pair_tokens / audit_pk_to_string (they live
2891    // ~150 lines below). This block keeps the non-audited and
2892    // non-Auto branches in place — the audited Auto-PK arm is
2893    // computed below and merged via the dispatch helper variable.
2894    let pool_insert_method = if audited_fields.is_some() && !fields.has_auto {
2895        // Audited models with explicit (non-Auto) PKs go through
2896        // the non-Auto insert path below — the audit emit is one
2897        // round-trip after the INSERT inside the same tx via
2898        // audit::save_one_with_audit? No, INSERT semantics
2899        // differ. For non-Auto PK + audited, route through a
2900        // dedicated insert + audit emit on the same tx, but defer
2901        // the macro emission to the audit-bundle-aware block below
2902        // — this `quote!()` placeholder gets overwritten there.
2903        quote!()
2904    } else if audited_fields.is_some() && fields.has_auto {
2905        // Audited Auto-PK insert_pool — assembled after the audit
2906        // bundles. Placeholder; real emission below.
2907        quote!()
2908    } else if fields.has_auto {
2909        let pushes = &fields.insert_pushes;
2910        let returning_cols = &fields.returning_cols;
2911        // When every `Auto<T>` field is filled Rust-side
2912        // (`default_uuid_v7`, issue #823), there is no column to read
2913        // back from the database — `returning_cols` is empty. Route
2914        // through plain `insert_pool` instead of
2915        // `insert_returning_pool` to skip the redundant RETURNING /
2916        // LAST_INSERT_ID round-trip.
2917        if fields.returning_cols.is_empty() {
2918            quote! {
2919                /// Insert this row against either backend. Every
2920                /// `Auto<T>` PK on this model is filled Rust-side
2921                /// (e.g. `default_uuid_v7`) before binding, so no
2922                /// RETURNING round-trip is needed.
2923                ///
2924                /// # Errors
2925                /// As [`Self::insert`].
2926                pub async fn insert_pool(
2927                    &mut self,
2928                    pool: &#root::sql::Pool,
2929                ) -> ::core::result::Result<(), #root::sql::ExecError> {
2930                    let mut _columns: ::std::vec::Vec<&'static str> =
2931                        ::std::vec::Vec::new();
2932                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2933                        ::std::vec::Vec::new();
2934                    #( #pushes )*
2935                    let _query = #root::core::InsertQuery {
2936                        model: <Self as #root::core::Model>::SCHEMA,
2937                        columns: _columns,
2938                        values: _values,
2939                        returning: ::std::vec::Vec::new(),
2940                        on_conflict: ::core::option::Option::None,
2941                    };
2942                    #root::sql::insert_pool(pool, &_query).await
2943                }
2944
2945                /// Eloquent `Model::insertOrIgnore()` — same shape
2946                /// as the auto-PK branch above. Returns `Ok(true)`
2947                /// when inserted, `Ok(false)` when a conflict caused
2948                /// the INSERT to silently skip.
2949                ///
2950                /// # Errors
2951                /// As [`Self::insert`].
2952                pub async fn insert_or_ignore(
2953                    &mut self,
2954                    pool: &#root::sql::Pool,
2955                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
2956                    let mut _columns: ::std::vec::Vec<&'static str> =
2957                        ::std::vec::Vec::new();
2958                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2959                        ::std::vec::Vec::new();
2960                    #( #pushes )*
2961                    let _query = #root::core::InsertQuery {
2962                        model: <Self as #root::core::Model>::SCHEMA,
2963                        columns: _columns,
2964                        values: _values,
2965                        returning: ::std::vec::Vec::new(),
2966                        on_conflict: ::core::option::Option::Some(
2967                            #root::core::ConflictClause::DoNothing,
2968                        ),
2969                    };
2970                    let dialect = pool.dialect();
2971                    let stmt = dialect.compile_insert(&_query)?;
2972                    let rows = #root::sql::raw_execute_pool(
2973                        pool, &stmt.sql, stmt.params,
2974                    ).await?;
2975                    ::core::result::Result::Ok(rows > 0)
2976                }
2977            }
2978        } else {
2979            quote! {
2980                /// Insert this row against either backend, populating any
2981                /// `Auto<T>` PK from the auto-assigned value.
2982                ///
2983                /// # Errors
2984                /// As [`Self::insert`].
2985                pub async fn insert_pool(
2986                    &mut self,
2987                    pool: &#root::sql::Pool,
2988                ) -> ::core::result::Result<(), #root::sql::ExecError> {
2989                    let mut _columns: ::std::vec::Vec<&'static str> =
2990                        ::std::vec::Vec::new();
2991                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
2992                        ::std::vec::Vec::new();
2993                    #( #pushes )*
2994                    let _query = #root::core::InsertQuery {
2995                        model: <Self as #root::core::Model>::SCHEMA,
2996                        columns: _columns,
2997                        values: _values,
2998                        returning: ::std::vec![ #( #returning_cols ),* ],
2999                        on_conflict: ::core::option::Option::None,
3000                    };
3001                    let _result = #root::sql::insert_returning_pool(
3002                        pool, &_query,
3003                    ).await?;
3004                    #root::sql::apply_auto_pk(_result, self)
3005                }
3006
3007                /// Eloquent `Model::insertOrIgnore()` — INSERT this
3008                /// row or silently skip on unique-constraint
3009                /// violation. Returns `Ok(true)` when a row was
3010                /// inserted, `Ok(false)` when a conflict caused the
3011                /// INSERT to silently skip.
3012                ///
3013                /// **Caveat on auto-PK models**: when the row is
3014                /// skipped (conflict), this instance's `Auto<T>`
3015                /// fields stay `Unset` — no PK is back-populated
3016                /// because the server didn't auto-assign one. For
3017                /// "insert then read back the PK or the existing
3018                /// row's PK", use the `upsert` family or
3019                /// `get_or_create`.
3020                ///
3021                /// # Errors
3022                /// As [`Self::insert`].
3023                pub async fn insert_or_ignore(
3024                    &mut self,
3025                    pool: &#root::sql::Pool,
3026                ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3027                    let mut _columns: ::std::vec::Vec<&'static str> =
3028                        ::std::vec::Vec::new();
3029                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3030                        ::std::vec::Vec::new();
3031                    #( #pushes )*
3032                    let _query = #root::core::InsertQuery {
3033                        model: <Self as #root::core::Model>::SCHEMA,
3034                        columns: _columns,
3035                        values: _values,
3036                        returning: ::std::vec::Vec::new(),
3037                        on_conflict: ::core::option::Option::Some(
3038                            #root::core::ConflictClause::DoNothing,
3039                        ),
3040                    };
3041                    let dialect = pool.dialect();
3042                    let stmt = dialect.compile_insert(&_query)?;
3043                    let rows = #root::sql::raw_execute_pool(
3044                        pool, &stmt.sql, stmt.params,
3045                    ).await?;
3046                    ::core::result::Result::Ok(rows > 0)
3047                }
3048            }
3049        }
3050    } else {
3051        let insert_columns = &fields.insert_columns;
3052        let insert_values = &fields.insert_values;
3053        quote! {
3054            /// Insert this row into its table against either backend.
3055            /// Equivalent to [`Self::insert`] but takes
3056            /// [`#root::sql::Pool`].
3057            ///
3058            /// # Errors
3059            /// As [`Self::insert`].
3060            pub async fn insert_pool(
3061                &self,
3062                pool: &#root::sql::Pool,
3063            ) -> ::core::result::Result<(), #root::sql::ExecError> {
3064                let _query = #root::core::InsertQuery {
3065                    model: <Self as #root::core::Model>::SCHEMA,
3066                    columns: ::std::vec![ #( #insert_columns ),* ],
3067                    values: ::std::vec![ #( #insert_values ),* ],
3068                    returning: ::std::vec::Vec::new(),
3069                    on_conflict: ::core::option::Option::None,
3070                };
3071                #root::sql::insert_pool(pool, &_query).await
3072            }
3073
3074            /// Eloquent `Model::insertOrIgnore()` — INSERT this row
3075            /// or silently skip on unique-constraint violation. Maps
3076            /// to per-dialect "INSERT ... DO NOTHING on conflict":
3077            /// PG `INSERT … ON CONFLICT DO NOTHING`, SQLite
3078            /// `INSERT … ON CONFLICT DO NOTHING` (3.24+), MySQL
3079            /// `INSERT IGNORE INTO …`.
3080            ///
3081            /// Returns `Ok(true)` when a row was inserted,
3082            /// `Ok(false)` when a conflict caused the INSERT to
3083            /// silently skip.
3084            ///
3085            /// Use for "create-if-absent" patterns where you don't
3086            /// need the row back. For "find-or-create with the row
3087            /// returned", use the queryset-level
3088            /// `crate::sql::get_or_create` free function.
3089            ///
3090            /// # Errors
3091            /// As [`Self::insert`], plus any dialect-specific
3092            /// translation error from the ConflictClause writer.
3093            pub async fn insert_or_ignore(
3094                &self,
3095                pool: &#root::sql::Pool,
3096            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
3097                let _query = #root::core::InsertQuery {
3098                    model: <Self as #root::core::Model>::SCHEMA,
3099                    columns: ::std::vec![ #( #insert_columns ),* ],
3100                    values: ::std::vec![ #( #insert_values ),* ],
3101                    returning: ::std::vec::Vec::new(),
3102                    on_conflict: ::core::option::Option::Some(
3103                        #root::core::ConflictClause::DoNothing,
3104                    ),
3105                };
3106                let dialect = pool.dialect();
3107                let stmt = dialect.compile_insert(&_query)?;
3108                let rows = #root::sql::raw_execute_pool(pool, &stmt.sql, stmt.params).await?;
3109                ::core::result::Result::Ok(rows > 0)
3110            }
3111        }
3112    };
3113
3114    // pool_save_method moved to after audit_pair_tokens /
3115    // audit_pk_to_string (they live ~70 lines below) — needed for
3116    // the audited branch which builds an UpdateQuery + PendingEntry
3117    // and dispatches via audit::save_one_with_audit.
3118
3119    // pool_delete_method moved to after audit_pair_tokens / audit_pk_to_string
3120    // are computed (they live ~80 lines below).
3121
3122    // Build the (column, JSON value) pair list used by every
3123    // snapshot-style audit emission. Reused across delete_on,
3124    // soft_delete_on, restore_on, and (later) bulk paths. Empty
3125    // when the model isn't audited.
3126    let audit_pair_tokens: Vec<TokenStream2> = audited_fields
3127        .map(|tracked| {
3128            tracked
3129                .iter()
3130                .map(|c| {
3131                    let column_lit = c.column.as_str();
3132                    let ident = &c.ident;
3133                    quote! {
3134                        (
3135                            #column_lit,
3136                            #root::__serde_json::to_value(&self.#ident)
3137                                .unwrap_or(#root::__serde_json::Value::Null),
3138                        )
3139                    }
3140                })
3141                .collect()
3142        })
3143        .unwrap_or_default();
3144    let audit_pk_to_string = if let Some((pk_ident, _)) = primary_key {
3145        if fields.pk_is_auto {
3146            quote!(self.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
3147        } else {
3148            quote!(::std::format!("{}", &self.#pk_ident))
3149        }
3150    } else {
3151        quote!(::std::string::String::new())
3152    };
3153    let make_op_emit = |op_path: TokenStream2| -> TokenStream2 {
3154        if audited_fields.is_some() {
3155            let pairs = audit_pair_tokens.iter();
3156            let pk_str = audit_pk_to_string.clone();
3157            quote! {
3158                let _audit_entry = #root::audit::PendingEntry {
3159                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
3160                    entity_pk: #pk_str,
3161                    operation: #op_path,
3162                    source: #root::audit::current_source(),
3163                    changes: #root::audit::snapshot_changes(&[
3164                        #( #pairs ),*
3165                    ]),
3166                };
3167                #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
3168            }
3169        } else {
3170            quote!()
3171        }
3172    };
3173    let audit_insert_emit = make_op_emit(quote!(#root::audit::AuditOp::Create));
3174    let audit_delete_emit = make_op_emit(quote!(#root::audit::AuditOp::Delete));
3175    let audit_softdelete_emit = make_op_emit(quote!(#root::audit::AuditOp::SoftDelete));
3176    let audit_restore_emit = make_op_emit(quote!(#root::audit::AuditOp::Restore));
3177
3178    // `save_pool(&Pool)` — emitted for every model with a PK.
3179    // Audited Auto-PK models are deferred (the Auto::Unset →
3180    // insert_pool path needs the audited-insert flow from a future
3181    // batch). Three body shapes:
3182    // - non-audited, plain PK: build UpdateQuery + dispatch through
3183    //   sql::update_pool
3184    // - non-audited, Auto-PK: same, but Auto::Unset routes to
3185    //   self.insert_pool which already handles RETURNING / LAST_INSERT_ID
3186    // - audited, plain PK: build UpdateQuery + PendingEntry, dispatch
3187    //   through audit::save_one_with_audit (per-backend tx wraps
3188    //   UPDATE + audit emit atomically). Snapshot-style audit (post-
3189    //   write field values) — diff-style audit (with pre-UPDATE
3190    //   SELECT for `before` values) needs per-tracked-column codegen
3191    //   that doesn't fit the runtime-helper pattern; legacy &PgPool
3192    //   `save` keeps the diff for now.
3193    let pool_save_method = if let Some((pk_ident, pk_col)) = primary_key {
3194        let pk_column_lit = pk_col.as_str();
3195        let assignments = &fields.update_assignments;
3196        if audited_fields.is_some() {
3197            if fields.pk_is_auto {
3198                // Auto-PK + audited: defer. The Auto::Unset insert
3199                // path needs a transactional INSERT + LAST_INSERT_ID
3200                // + audit emit flow — that's a follow-up batch.
3201                quote!()
3202            } else {
3203                let pairs = audit_pair_tokens.iter();
3204                let pairs2 = audit_pair_tokens.iter();
3205                let pk_str = audit_pk_to_string.clone();
3206                let pk_str2 = audit_pk_to_string.clone();
3207                quote! {
3208                    /// Save (UPDATE) this row against either backend
3209                    /// with audit emission inside the same transaction.
3210                    /// Bi-dialect counterpart of [`Self::save`] for
3211                    /// audited models with non-`Auto<T>` PKs.
3212                    ///
3213                    /// Captures **post-write** field state (snapshot
3214                    /// audit). The legacy &PgPool [`Self::save`]
3215                    /// captures BEFORE+AFTER for true diff audit;
3216                    /// porting that to the &Pool path needs runtime
3217                    /// per-tracked-column decoding and is deferred.
3218                    ///
3219                    /// # Errors
3220                    /// As [`Self::save`].
3221                    pub async fn save_pool(
3222                        &mut self,
3223                        pool: &#root::sql::Pool,
3224                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3225                        let _query = #root::core::UpdateQuery {
3226                            model: <Self as #root::core::Model>::SCHEMA,
3227                            set: ::std::vec![ #( #assignments ),* ],
3228                            where_clause: #root::core::WhereExpr::Predicate(
3229                                #root::core::Filter {
3230                                    column: #pk_column_lit,
3231                                    op: #root::core::Op::Eq,
3232                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3233                                        ::core::clone::Clone::clone(&self.#pk_ident)
3234                                    ),
3235                                }
3236                            ),
3237                        };
3238                        let _audit_entry = #root::audit::PendingEntry {
3239                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3240                            entity_pk: #pk_str,
3241                            operation: #root::audit::AuditOp::Update,
3242                            source: #root::audit::current_source(),
3243                            changes: #root::audit::snapshot_changes(&[
3244                                #( #pairs ),*
3245                            ]),
3246                        };
3247                        let _affected = #root::audit::save_one_with_audit(
3248                            pool, &_query, &_audit_entry,
3249                        ).await?;
3250                        ::core::result::Result::Ok(_affected)
3251                    }
3252
3253                    /// `save_pool` narrowed to a Rust-field allowlist — issue #66
3254                    /// (Django `Model.save(update_fields=[...])`).
3255                    /// Audit emission shrinks to the same column set so
3256                    /// the audit log reflects exactly what was written.
3257                    ///
3258                    /// # Errors
3259                    /// As [`Self::save_pool`], plus
3260                    /// [`#root::core::QueryError::UnknownField`] wrapped
3261                    /// in `ExecError::Query` for unknown field names.
3262                    pub async fn save_partial(
3263                        &mut self,
3264                        fields: &[&str],
3265                        pool: &#root::sql::Pool,
3266                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3267                        if fields.is_empty() {
3268                            #root::__tracing::warn!(
3269                                target: "rustango::save_partial",
3270                                model = <Self as #root::core::Model>::SCHEMA.name,
3271                                "save_partial called with empty field list — no-op"
3272                            );
3273                            return ::core::result::Result::Ok(0);
3274                        }
3275                        let _schema = <Self as #root::core::Model>::SCHEMA;
3276                        let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3277                            ::std::collections::HashSet::with_capacity(fields.len());
3278                        for f in fields {
3279                            match _schema.field(f) {
3280                                ::core::option::Option::Some(fs) => {
3281                                    _wanted_cols.insert(fs.column);
3282                                }
3283                                ::core::option::Option::None => {
3284                                    return ::core::result::Result::Err(
3285                                        #root::sql::ExecError::Query(
3286                                            #root::core::QueryError::UnknownField {
3287                                                model: _schema.name,
3288                                                field: (*f).to_owned(),
3289                                            }
3290                                        )
3291                                    );
3292                                }
3293                            }
3294                        }
3295                        let _full: ::std::vec::Vec<#root::core::Assignment> =
3296                            ::std::vec![ #( #assignments ),* ];
3297                        let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3298                            .into_iter()
3299                            .filter(|a| _wanted_cols.contains(a.column))
3300                            .collect();
3301                        if _filtered.is_empty() {
3302                            #root::__tracing::warn!(
3303                                target: "rustango::save_partial",
3304                                model = _schema.name,
3305                                "save_partial: every named field maps to a non-assignable column — no-op"
3306                            );
3307                            return ::core::result::Result::Ok(0);
3308                        }
3309                        let _query = #root::core::UpdateQuery {
3310                            model: _schema,
3311                            set: _filtered,
3312                            where_clause: #root::core::WhereExpr::Predicate(
3313                                #root::core::Filter {
3314                                    column: #pk_column_lit,
3315                                    op: #root::core::Op::Eq,
3316                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3317                                        ::core::clone::Clone::clone(&self.#pk_ident)
3318                                    ),
3319                                }
3320                            ),
3321                        };
3322                        // Narrow the audit snapshot to the same column set.
3323                        let _all_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3324                            ::std::vec![ #( #pairs2 ),* ];
3325                        let _narrowed: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3326                            _all_pairs
3327                                .into_iter()
3328                                .filter(|(col, _)| _wanted_cols.contains(col))
3329                                .collect();
3330                        let _audit_entry = #root::audit::PendingEntry {
3331                            entity_table: _schema.table,
3332                            entity_pk: #pk_str2,
3333                            operation: #root::audit::AuditOp::Update,
3334                            source: #root::audit::current_source(),
3335                            changes: #root::audit::snapshot_changes(&_narrowed),
3336                        };
3337                        let _affected = #root::audit::save_one_with_audit(
3338                            pool, &_query, &_audit_entry,
3339                        ).await?;
3340                        ::core::result::Result::Ok(_affected)
3341                    }
3342
3343                    /// Typed-column counterpart of [`Self::save_partial`] —
3344                    /// issue #67. `fields` is a tuple of [`Column`]
3345                    /// constants whose `Model` matches `Self`; typos and
3346                    /// model mismatches surface at *compile time*
3347                    /// (`Author::name` inside a `Post::save_partial_typed`
3348                    /// call is a type error, no runtime check).
3349                    ///
3350                    /// ```ignore
3351                    /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3352                    /// ```
3353                    ///
3354                    /// Lowers to [`Self::save_partial`] under the hood;
3355                    /// audit narrowing + every other semantic is identical.
3356                    ///
3357                    /// [`Column`]: #root::core::Column
3358                    ///
3359                    /// # Errors
3360                    /// As [`Self::save_partial`].
3361                    pub async fn save_partial_typed<
3362                        L: #root::core::TypedFieldList<Self>,
3363                    >(
3364                        &mut self,
3365                        fields: L,
3366                        pool: &#root::sql::Pool,
3367                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3368                        let _names = fields.rust_field_names();
3369                        let _refs: ::std::vec::Vec<&str> =
3370                            _names.iter().copied().collect();
3371                        self.save_partial(&_refs, pool).await
3372                    }
3373                }
3374            }
3375        } else {
3376            let dispatch_unset = if fields.pk_is_auto {
3377                quote! {
3378                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3379                        return self.insert_pool(pool).await.map(|()| 1u64);
3380                    }
3381                }
3382            } else {
3383                quote!()
3384            };
3385            quote! {
3386                /// Save this row to its table against either backend.
3387                /// `INSERT` when the `Auto<T>` PK is `Unset`, else
3388                /// `UPDATE` keyed on the PK.
3389                ///
3390                /// # Errors
3391                /// As [`Self::save`].
3392                pub async fn save_pool(
3393                    &mut self,
3394                    pool: &#root::sql::Pool,
3395                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3396                    #dispatch_unset
3397                    let _query = #root::core::UpdateQuery {
3398                        model: <Self as #root::core::Model>::SCHEMA,
3399                        set: ::std::vec![ #( #assignments ),* ],
3400                        where_clause: #root::core::WhereExpr::Predicate(
3401                            #root::core::Filter {
3402                                column: #pk_column_lit,
3403                                op: #root::core::Op::Eq,
3404                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3405                                    ::core::clone::Clone::clone(&self.#pk_ident)
3406                                ),
3407                            }
3408                        ),
3409                    };
3410                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3411                    ::core::result::Result::Ok(_affected)
3412                }
3413
3414                /// Save (UPDATE) only the listed Rust-side fields,
3415                /// leaving every other column untouched. Issue #66 —
3416                /// Django's `Model.save(update_fields=[...])` shape.
3417                ///
3418                /// `fields` are Rust-side struct field names; the macro
3419                /// resolves each to its SQL column. Unknown field
3420                /// names return [`#root::core::QueryError::UnknownField`]
3421                /// wrapped in `ExecError::Query`. An empty list is a
3422                /// no-op (returns `Ok(())` and logs a `tracing::warn!`),
3423                /// matching Django's "nothing to do" semantic.
3424                ///
3425                /// Use this when:
3426                /// * you only mutated a couple of fields on a wide row
3427                ///   (avoid re-writing every column on every save), or
3428                /// * two writers diverged after their initial read and
3429                ///   you want to preserve the other writer's changes to
3430                ///   columns you didn't touch.
3431                ///
3432                /// Auto-PK models with an unset PK return
3433                /// [`#root::core::QueryError::UnknownField`] with
3434                /// field name `<pk>` — `save_partial` is an
3435                /// UPDATE-only path. Call [`Self::insert_pool`]
3436                /// (or [`Self::save_pool`] which dispatches based on
3437                /// PK state) for the INSERT case.
3438                ///
3439                /// # Errors
3440                /// As [`Self::save_pool`], plus `UnknownField` for
3441                /// unknown / empty / Auto-Unset cases.
3442                pub async fn save_partial(
3443                    &mut self,
3444                    fields: &[&str],
3445                    pool: &#root::sql::Pool,
3446                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3447                    if fields.is_empty() {
3448                        #root::__tracing::warn!(
3449                            target: "rustango::save_partial",
3450                            model = <Self as #root::core::Model>::SCHEMA.name,
3451                            "save_partial called with empty field list — no-op"
3452                        );
3453                        return ::core::result::Result::Ok(0);
3454                    }
3455                    let _schema = <Self as #root::core::Model>::SCHEMA;
3456                    // Validate field names against the schema.
3457                    let mut _wanted_cols: ::std::collections::HashSet<&'static str> =
3458                        ::std::collections::HashSet::with_capacity(fields.len());
3459                    for f in fields {
3460                        match _schema.field(f) {
3461                            ::core::option::Option::Some(fs) => {
3462                                _wanted_cols.insert(fs.column);
3463                            }
3464                            ::core::option::Option::None => {
3465                                return ::core::result::Result::Err(
3466                                    #root::sql::ExecError::Query(
3467                                        #root::core::QueryError::UnknownField {
3468                                            model: _schema.name,
3469                                            field: (*f).to_owned(),
3470                                        }
3471                                    )
3472                                );
3473                            }
3474                        }
3475                    }
3476                    // Build the full assignment vec, then keep only the
3477                    // assignments whose column is in `_wanted_cols`.
3478                    let _full: ::std::vec::Vec<#root::core::Assignment> =
3479                        ::std::vec![ #( #assignments ),* ];
3480                    let _filtered: ::std::vec::Vec<#root::core::Assignment> = _full
3481                        .into_iter()
3482                        .filter(|a| _wanted_cols.contains(a.column))
3483                        .collect();
3484                    if _filtered.is_empty() {
3485                        // All field names valid, but they all map to
3486                        // non-assignable slots (PK column, computed/
3487                        // virtual fields, relations without an
3488                        // assignment). Same no-op semantic as Django.
3489                        #root::__tracing::warn!(
3490                            target: "rustango::save_partial",
3491                            model = _schema.name,
3492                            "save_partial: every named field maps to a non-assignable column — no-op"
3493                        );
3494                        return ::core::result::Result::Ok(0);
3495                    }
3496                    let _query = #root::core::UpdateQuery {
3497                        model: _schema,
3498                        set: _filtered,
3499                        where_clause: #root::core::WhereExpr::Predicate(
3500                            #root::core::Filter {
3501                                column: #pk_column_lit,
3502                                op: #root::core::Op::Eq,
3503                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3504                                    ::core::clone::Clone::clone(&self.#pk_ident)
3505                                ),
3506                            }
3507                        ),
3508                    };
3509                    let _affected = #root::sql::update_pool(pool, &_query).await?;
3510                    ::core::result::Result::Ok(_affected)
3511                }
3512
3513                /// Typed-column counterpart of [`Self::save_partial`] —
3514                /// issue #67. `fields` is a tuple of [`Column`]
3515                /// constants whose `Model` matches `Self`; typos and
3516                /// model mismatches surface at *compile time*
3517                /// (`Author::name` inside a `Post::save_partial_typed`
3518                /// call is a type error, no runtime check).
3519                ///
3520                /// ```ignore
3521                /// post.save_partial_typed((Post::title, Post::slug), &pool).await?;
3522                /// ```
3523                ///
3524                /// Lowers to [`Self::save_partial`] under the hood — the
3525                /// tuple is reduced to a `&[&str]` slice of Rust-side
3526                /// field names and forwarded.
3527                ///
3528                /// [`Column`]: #root::core::Column
3529                ///
3530                /// # Errors
3531                /// As [`Self::save_partial`].
3532                pub async fn save_partial_typed<
3533                    L: #root::core::TypedFieldList<Self>,
3534                >(
3535                    &mut self,
3536                    fields: L,
3537                    pool: &#root::sql::Pool,
3538                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3539                    let _names = fields.rust_field_names();
3540                    let _refs: ::std::vec::Vec<&str> =
3541                        _names.iter().copied().collect();
3542                    self.save_partial(&_refs, pool).await
3543                }
3544            }
3545        }
3546    } else {
3547        quote!()
3548    };
3549
3550    // Audited `insert_pool` (overrides the placeholder set higher up
3551    // in the function). v0.23.0-batch22 — both Auto-PK and non-Auto-PK
3552    // audited models get insert_pool routing through
3553    // audit::insert_one_with_audit (per-backend tx wraps INSERT
3554    // + auto-PK readback + audit emit). Snapshot-style audit (the
3555    // PendingEntry's `changes` carries post-write field values).
3556    let pool_insert_method = if audited_fields.is_some() {
3557        if let Some(_) = primary_key {
3558            let pushes = if fields.has_auto {
3559                fields.insert_pushes.clone()
3560            } else {
3561                // For non-Auto-PK models, the macro normally builds
3562                // {columns, values} from fields.insert_columns +
3563                // fields.insert_values rather than insert_pushes.
3564                // Map those into the pushes shape.
3565                fields
3566                    .insert_columns
3567                    .iter()
3568                    .zip(&fields.insert_values)
3569                    .map(|(col, val)| {
3570                        quote! {
3571                            _columns.push(#col);
3572                            _values.push(#val);
3573                        }
3574                    })
3575                    .collect()
3576            };
3577            let returning_cols: Vec<proc_macro2::TokenStream> = if fields.has_auto {
3578                fields.returning_cols.clone()
3579            } else {
3580                // Non-Auto-PK: still need RETURNING something for the
3581                // audit helper's contract (it errors on empty
3582                // returning). Return the PK column so the audit row
3583                // can carry the assigned PK back. Some non-Auto PKs
3584                // are server-side-default (e.g. UUIDv4 default), so
3585                // RETURNING is genuinely useful.
3586                primary_key
3587                    .map(|(_, col)| {
3588                        let lit = col.as_str();
3589                        vec![quote!(#lit)]
3590                    })
3591                    .unwrap_or_default()
3592            };
3593            let pairs = audit_pair_tokens.iter();
3594            let pk_str = audit_pk_to_string.clone();
3595            quote! {
3596                /// Insert this row against either backend with audit
3597                /// emission inside the same transaction. Bi-dialect
3598                /// counterpart of [`Self::insert`] for audited models.
3599                ///
3600                /// Snapshot-style audit (post-write field values).
3601                ///
3602                /// # Errors
3603                /// As [`Self::insert`].
3604                pub async fn insert_pool(
3605                    &mut self,
3606                    pool: &#root::sql::Pool,
3607                ) -> ::core::result::Result<(), #root::sql::ExecError> {
3608                    let mut _columns: ::std::vec::Vec<&'static str> =
3609                        ::std::vec::Vec::new();
3610                    let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
3611                        ::std::vec::Vec::new();
3612                    #( #pushes )*
3613                    let _query = #root::core::InsertQuery {
3614                        model: <Self as #root::core::Model>::SCHEMA,
3615                        columns: _columns,
3616                        values: _values,
3617                        returning: ::std::vec![ #( #returning_cols ),* ],
3618                        on_conflict: ::core::option::Option::None,
3619                    };
3620                    let _audit_entry = #root::audit::PendingEntry {
3621                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
3622                        entity_pk: #pk_str,
3623                        operation: #root::audit::AuditOp::Create,
3624                        source: #root::audit::current_source(),
3625                        changes: #root::audit::snapshot_changes(&[
3626                            #( #pairs ),*
3627                        ]),
3628                    };
3629                    let _result = #root::audit::insert_one_with_audit(
3630                        pool, &_query, &_audit_entry,
3631                    ).await?;
3632                    #root::sql::apply_auto_pk(_result, self)
3633                }
3634            }
3635        } else {
3636            quote!()
3637        }
3638    } else {
3639        // Keep the non-audited pool_insert_method we built earlier.
3640        pool_insert_method
3641    };
3642
3643    // Update audited save_pool: now that insert_pool is wired for
3644    // audited Auto-PK models, save_pool can dispatch Auto::Unset →
3645    // insert_pool. Non-audited save_pool already does this.
3646    // v0.23.0-batch25 — diff-style audit on the audited save_pool path.
3647    // Replaces the snapshot-only emission with a per-backend transaction
3648    // body that:
3649    //  1. SELECTs the tracked columns by PK (typed Row::try_get per
3650    //     column), capturing BEFORE values
3651    //  2. compiles the UPDATE via pool.dialect() and runs it on the tx
3652    //  3. builds AFTER pairs from &self
3653    //  4. diffs BEFORE/AFTER, emits one PendingEntry with
3654    //     AuditOp::Update + diff_changes(...) on the same tx connection
3655    //  5. commits
3656    //
3657    // Per-backend arms inline the SQL string + placeholder shape, then
3658    // share the `audit_before_pair_tokens` decoder block (Row::try_get
3659    // is polymorphic over Row type — the same tokens work against
3660    // PgRow and MySqlRow as long as the field's Rust type implements
3661    // both Decode<Postgres> and Decode<MySql>, which Auto<T> +
3662    // primitives + chrono/uuid/serde_json::Value all do).
3663    let pool_save_method = if let Some(tracked) = audited_fields {
3664        if let Some((pk_ident, pk_col)) = primary_key {
3665            let pk_column_lit = pk_col.as_str();
3666            // Two iterators — quote!'s `#(#var)*` consumes the
3667            // iterator, and we need to splice the same after-pairs
3668            // sequence into both per-backend arms.
3669            let after_pairs_pg = audit_pair_tokens.iter().collect::<Vec<_>>();
3670            let pk_str = audit_pk_to_string.clone();
3671            // Per-tracked-column BEFORE-pair token list. Each entry
3672            // is `(col_lit, try_get_returning<value_ty>(row, col_lit) → Json)`.
3673            // The Row alias resolves to PgRow / MySqlRow per call site,
3674            // so the same template generates both the PG and MySQL bodies.
3675            let mk_before_pairs =
3676                |getter: proc_macro2::TokenStream| -> Vec<proc_macro2::TokenStream> {
3677                    tracked
3678                        .iter()
3679                        .map(|c| {
3680                            let column_lit = c.column.as_str();
3681                            let value_ty = &c.value_ty;
3682                            quote! {
3683                                (
3684                                    #column_lit,
3685                                    match #getter::<#value_ty>(
3686                                        _audit_before_row, #column_lit,
3687                                    ) {
3688                                        ::core::result::Result::Ok(v) => {
3689                                            #root::__serde_json::to_value(&v)
3690                                                .unwrap_or(#root::__serde_json::Value::Null)
3691                                        }
3692                                        ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
3693                                    },
3694                                )
3695                            }
3696                        })
3697                        .collect()
3698                };
3699            let before_pairs_pg: Vec<proc_macro2::TokenStream> =
3700                mk_before_pairs(quote!(#root::sql::try_get_returning));
3701            let before_pairs_my: Vec<proc_macro2::TokenStream> =
3702                mk_before_pairs(quote!(#root::sql::try_get_returning_my));
3703            let before_pairs_sqlite: Vec<proc_macro2::TokenStream> =
3704                mk_before_pairs(quote!(#root::sql::try_get_returning_sqlite));
3705            let pg_select_cols: String = tracked
3706                .iter()
3707                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
3708                .collect::<Vec<_>>()
3709                .join(", ");
3710            let my_select_cols: String = tracked
3711                .iter()
3712                .map(|c| format!("`{}`", c.column.replace('`', "``")))
3713                .collect::<Vec<_>>()
3714                .join(", ");
3715            // SQLite uses double-quote identifier quoting (same as
3716            // Postgres in default config), so the column-list shape
3717            // matches PG.
3718            let sqlite_select_cols: String = pg_select_cols.clone();
3719            let pk_value_for_bind = if fields.pk_is_auto {
3720                quote!(self.#pk_ident.get().copied().unwrap_or_default())
3721            } else {
3722                quote!(::core::clone::Clone::clone(&self.#pk_ident))
3723            };
3724            let assignments = &fields.update_assignments;
3725            let unset_dispatch = if fields.has_auto {
3726                quote! {
3727                    if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
3728                        return self.insert_pool(pool).await.map(|()| 1u64);
3729                    }
3730                }
3731            } else {
3732                quote!()
3733            };
3734            quote! {
3735                /// Save this row against either backend with audit
3736                /// emission (diff-style: BEFORE+AFTER) inside the
3737                /// same transaction. Auto::Unset PK routes to
3738                /// insert_pool. Bi-dialect counterpart of
3739                /// [`Self::save`] for audited models.
3740                ///
3741                /// The audit row's `changes` JSON contains one
3742                /// `{ "field": { "before": …, "after": … } }` entry
3743                /// per tracked column whose value actually changed
3744                /// — same shape as the existing &PgPool save() emits.
3745                ///
3746                /// # Errors
3747                /// As [`Self::save`].
3748                pub async fn save_pool(
3749                    &mut self,
3750                    pool: &#root::sql::Pool,
3751                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3752                    #unset_dispatch
3753                    let _query = #root::core::UpdateQuery {
3754                        model: <Self as #root::core::Model>::SCHEMA,
3755                        set: ::std::vec![ #( #assignments ),* ],
3756                        where_clause: #root::core::WhereExpr::Predicate(
3757                            #root::core::Filter {
3758                                column: #pk_column_lit,
3759                                op: #root::core::Op::Eq,
3760                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
3761                                    ::core::clone::Clone::clone(&self.#pk_ident)
3762                                ),
3763                            }
3764                        ),
3765                    };
3766                    let _after_pairs: ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
3767                        ::std::vec![ #( #after_pairs_pg ),* ];
3768                    #root::audit::save_one_with_diff(
3769                        pool,
3770                        &_query,
3771                        #pk_column_lit,
3772                        ::core::convert::Into::<#root::core::SqlValue>::into(
3773                            #pk_value_for_bind,
3774                        ),
3775                        <Self as #root::core::Model>::SCHEMA.table,
3776                        #pk_str,
3777                        _after_pairs,
3778                        #pg_select_cols,
3779                        #my_select_cols,
3780                        #sqlite_select_cols,
3781                        |_audit_before_row| ::std::vec![ #( #before_pairs_pg ),* ],
3782                        |_audit_before_row| ::std::vec![ #( #before_pairs_my ),* ],
3783                        |_audit_before_row| ::std::vec![ #( #before_pairs_sqlite ),* ],
3784                    ).await
3785                }
3786            }
3787        } else {
3788            quote!()
3789        }
3790    } else {
3791        pool_save_method
3792    };
3793
3794    // `delete_pool(&Pool)` — emitted for every model with a PK. Two
3795    // body shapes:
3796    // - non-audited: simple dispatch through `sql::delete_pool`
3797    // - audited: routes through `audit::delete_one_with_audit`,
3798    //   which opens a per-backend transaction wrapping DELETE +
3799    //   audit emit so the data write and audit row commit atomically.
3800    let pool_delete_method = {
3801        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
3802        let pk_ident_for_pool = primary_key.map(|(ident, _)| ident);
3803        if let Some(pk_ident) = pk_ident_for_pool {
3804            if audited_fields.is_some() {
3805                let pairs = audit_pair_tokens.iter();
3806                let pk_str = audit_pk_to_string.clone();
3807                quote! {
3808                    /// Delete this row against either backend with audit
3809                    /// emission inside the same transaction. Bi-dialect
3810                    /// counterpart of [`Self::delete`] for audited models.
3811                    ///
3812                    /// # Errors
3813                    /// As [`Self::delete`].
3814                    pub async fn delete_pool(
3815                        &self,
3816                        pool: &#root::sql::Pool,
3817                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3818                        let _query = #root::core::DeleteQuery {
3819                            model: <Self as #root::core::Model>::SCHEMA,
3820                            where_clause: #root::core::WhereExpr::Predicate(
3821                                #root::core::Filter {
3822                                    column: #pk_column_lit,
3823                                    op: #root::core::Op::Eq,
3824                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3825                                        ::core::clone::Clone::clone(&self.#pk_ident)
3826                                    ),
3827                                }
3828                            ),
3829                        };
3830                        let _audit_entry = #root::audit::PendingEntry {
3831                            entity_table: <Self as #root::core::Model>::SCHEMA.table,
3832                            entity_pk: #pk_str,
3833                            operation: #root::audit::AuditOp::Delete,
3834                            source: #root::audit::current_source(),
3835                            changes: #root::audit::snapshot_changes(&[
3836                                #( #pairs ),*
3837                            ]),
3838                        };
3839                        #root::audit::delete_one_with_audit(
3840                            pool, &_query, &_audit_entry,
3841                        ).await
3842                    }
3843                }
3844            } else {
3845                quote! {
3846                    /// Delete the row identified by this instance's primary key
3847                    /// against either backend. Equivalent to [`Self::delete`] but
3848                    /// takes [`#root::sql::Pool`] and dispatches per backend.
3849                    ///
3850                    /// # Errors
3851                    /// As [`Self::delete`].
3852                    pub async fn delete_pool(
3853                        &self,
3854                        pool: &#root::sql::Pool,
3855                    ) -> ::core::result::Result<u64, #root::sql::ExecError> {
3856                        let _query = #root::core::DeleteQuery {
3857                            model: <Self as #root::core::Model>::SCHEMA,
3858                            where_clause: #root::core::WhereExpr::Predicate(
3859                                #root::core::Filter {
3860                                    column: #pk_column_lit,
3861                                    op: #root::core::Op::Eq,
3862                                    value: ::core::convert::Into::<#root::core::SqlValue>::into(
3863                                        ::core::clone::Clone::clone(&self.#pk_ident)
3864                                    ),
3865                                }
3866                            ),
3867                        };
3868                        #root::sql::delete_pool(pool, &_query).await
3869                    }
3870                }
3871            }
3872        } else {
3873            quote!()
3874        }
3875    };
3876
3877    // `refresh_from_db_pool(&mut self, pool)` — re-SELECT the row
3878    // matching this instance's PK and overwrite the in-memory state
3879    // with the freshly-fetched columns. Django's `refresh_from_db`.
3880    // Issue #825. Only emitted when the model declares a PK; non-PK
3881    // models can't address a specific row.
3882    //
3883    // `replicate(&self)` — Eloquent-style clone-as-insertable. Copies
3884    // every field from `self`; resets the PK to `Auto::Unset` when
3885    // `pk_is_auto` so the next `save_pool` / `insert_pool` allocates
3886    // a fresh autoincrement value. Non-Auto PKs preserve the source
3887    // PK — the caller must overwrite before insert. Pure-Rust, no
3888    // I/O, no dialect surface.
3889    let refresh_replicate_methods = if let Some((pk_ident, _)) = primary_key {
3890        let other_field_clones: Vec<TokenStream2> = fields
3891            .column_entries
3892            .iter()
3893            .filter(|c| &c.ident != pk_ident)
3894            .map(|c| {
3895                let ident = &c.ident;
3896                quote! {
3897                    #ident: ::core::clone::Clone::clone(&self.#ident)
3898                }
3899            })
3900            .collect();
3901        let pk_clone_token = if fields.pk_is_auto {
3902            quote! { #pk_ident: #root::sql::Auto::Unset }
3903        } else {
3904            quote! { #pk_ident: ::core::clone::Clone::clone(&self.#pk_ident) }
3905        };
3906        let replicate_doc = if fields.pk_is_auto {
3907            quote! {
3908                /// Eloquent-style `replicate()` — return a clone of this
3909                /// row with the primary key reset to [`Auto::Unset`] so
3910                /// the copy is ready for `insert_pool` / `save_pool` to
3911                /// allocate a fresh autoincrement value. Every other
3912                /// field is `Clone`d verbatim. Issue #825.
3913                ///
3914                /// `auto_now_add` / `auto_now` timestamp fields are
3915                /// **not** reset (Eloquent's `replicate` doesn't reset
3916                /// them either) — pass them through the normal insert
3917                /// path if you want fresh values, or assign them
3918                /// explicitly after the call.
3919            }
3920        } else {
3921            quote! {
3922                /// Eloquent-style `replicate()` — clone this row
3923                /// verbatim. Because the primary key is **not** an
3924                /// `Auto<T>`, the clone keeps the source PK; the
3925                /// caller must overwrite `copy.<pk>` before inserting
3926                /// to avoid a unique-key violation. Issue #825.
3927            }
3928        };
3929        // 2026-06-07 — field-name / shortcut collision guard.
3930        //
3931        // The macro emits both `pub const <field>: <field>_col = ...`
3932        // (per-field typed-column const, used by the typed-builder
3933        // surface as `Post::id.eq(...)`) AND `pub async fn <shortcut>(...)`
3934        // for the Eloquent shortcuts (`count`, `sum`, `min` …). When a
3935        // model has a field named e.g. `count`, both items would land
3936        // in the same inherent impl with the same name, and the
3937        // compiler rejects the derive with "duplicate definitions".
3938        //
3939        // Drop the conflicting shortcut for that model. Callers can
3940        // still reach the same behavior via
3941        // `QuerySet::<Model>::default().count(&pool)`.
3942        let column_names: ::std::collections::HashSet<String> = fields
3943            .column_entries
3944            .iter()
3945            .map(|c| c.ident.to_string())
3946            .collect();
3947        let emit_if_no_field_collision = |name: &str, tokens: TokenStream2| -> TokenStream2 {
3948            if column_names.contains(name) {
3949                quote! {}
3950            } else {
3951                tokens
3952            }
3953        };
3954        let count_method = emit_if_no_field_collision(
3955            "count",
3956            quote! {
3957                /// Count rows of this model — `SELECT COUNT(*) FROM
3958                /// <table>`. Eloquent `Model::count()` parity.
3959                ///
3960                /// Skipped on models that already declare a field named
3961                /// `count`. Drop into `QuerySet::<Self>::default().count(&pool)`
3962                /// in that case.
3963                ///
3964                /// # Errors
3965                /// As [`CounterPool::count`].
3966                ///
3967                /// [`CounterPool::count`]: rustango::sql::CounterPool::count
3968                pub async fn count(
3969                    pool: &#root::sql::Pool,
3970                ) -> ::core::result::Result<i64, #root::sql::ExecError> {
3971                    use #root::sql::CounterPool as _;
3972                    #root::query::QuerySet::<Self>::default()
3973                        .count(pool)
3974                        .await
3975                }
3976            },
3977        );
3978        let value_method = emit_if_no_field_collision(
3979            "value",
3980            quote! {
3981                /// Pluck a single scalar from the first row.
3982                /// Eloquent `Model::query()->value($col)` parity.
3983                ///
3984                /// Skipped on models that already declare a field named
3985                /// `value`. Drop into
3986                /// `QuerySet::<Self>::default().values_list_flat(col).first::<U>(&pool)` instead.
3987                ///
3988                /// # Errors
3989                /// As `ValuesFlatQuerySet::first`.
3990                pub async fn value<U>(
3991                    col: &str,
3992                    pool: &#root::sql::Pool,
3993                ) -> ::core::result::Result<
3994                    ::core::option::Option<U>,
3995                    #root::sql::ExecError,
3996                >
3997                where
3998                    U: #root::sql::MaybePgScalar
3999                        + #root::sql::MaybeMyScalar
4000                        + #root::sql::MaybeSqliteScalar
4001                        + ::core::marker::Send
4002                        + ::core::marker::Unpin,
4003                {
4004                    let _col_static: &'static str = Self::__resolve_col(col)?;
4005                    #root::query::QuerySet::<Self>::default()
4006                        .values_list_flat(_col_static)
4007                        .first::<U>(pool)
4008                        .await
4009                }
4010            },
4011        );
4012        let sum_method = emit_if_no_field_collision(
4013            "sum",
4014            quote! {
4015                /// `SUM(col)` over every row. Eloquent `Model::sum($col)`.
4016                /// Skipped on models that already declare a field named
4017                /// `sum`.
4018                ///
4019                /// # Errors
4020                /// As [`#root::sql::fetch_aggregate_pool`].
4021                pub async fn sum<U>(
4022                    col: &str,
4023                    pool: &#root::sql::Pool,
4024                ) -> ::core::result::Result<
4025                    ::core::option::Option<U>,
4026                    #root::sql::ExecError,
4027                >
4028                where
4029                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4030                        + #root::sql::MaybeMyFromRow
4031                        + #root::sql::MaybeSqliteFromRow
4032                        + ::core::marker::Send
4033                        + ::core::marker::Unpin,
4034                {
4035                    Self::__aggregate_one_pool::<U>(
4036                        col,
4037                        |c| #root::core::AggregateExpr::Sum(c),
4038                        pool,
4039                    )
4040                    .await
4041                }
4042            },
4043        );
4044        let avg_method = emit_if_no_field_collision(
4045            "avg",
4046            quote! {
4047                /// `AVG(col)`. Eloquent `Model::avg($col)`. Skipped on
4048                /// models that already declare a field named `avg`.
4049                ///
4050                /// # Errors
4051                /// As [`#root::sql::fetch_aggregate_pool`].
4052                pub async fn avg<U>(
4053                    col: &str,
4054                    pool: &#root::sql::Pool,
4055                ) -> ::core::result::Result<
4056                    ::core::option::Option<U>,
4057                    #root::sql::ExecError,
4058                >
4059                where
4060                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4061                        + #root::sql::MaybeMyFromRow
4062                        + #root::sql::MaybeSqliteFromRow
4063                        + ::core::marker::Send
4064                        + ::core::marker::Unpin,
4065                {
4066                    Self::__aggregate_one_pool::<U>(
4067                        col,
4068                        |c| #root::core::AggregateExpr::Avg(c),
4069                        pool,
4070                    )
4071                    .await
4072                }
4073            },
4074        );
4075        let min_method = emit_if_no_field_collision(
4076            "min",
4077            quote! {
4078                /// `MIN(col)`. Eloquent `Model::min($col)`. Skipped on
4079                /// models that already declare a field named `min`.
4080                ///
4081                /// # Errors
4082                /// As [`#root::sql::fetch_aggregate_pool`].
4083                pub async fn min<U>(
4084                    col: &str,
4085                    pool: &#root::sql::Pool,
4086                ) -> ::core::result::Result<
4087                    ::core::option::Option<U>,
4088                    #root::sql::ExecError,
4089                >
4090                where
4091                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4092                        + #root::sql::MaybeMyFromRow
4093                        + #root::sql::MaybeSqliteFromRow
4094                        + ::core::marker::Send
4095                        + ::core::marker::Unpin,
4096                {
4097                    Self::__aggregate_one_pool::<U>(
4098                        col,
4099                        |c| #root::core::AggregateExpr::Min(c),
4100                        pool,
4101                    )
4102                    .await
4103                }
4104            },
4105        );
4106        let max_method = emit_if_no_field_collision(
4107            "max",
4108            quote! {
4109                /// `MAX(col)`. Eloquent `Model::max($col)`. Skipped on
4110                /// models that already declare a field named `max`.
4111                ///
4112                /// # Errors
4113                /// As [`#root::sql::fetch_aggregate_pool`].
4114                pub async fn max<U>(
4115                    col: &str,
4116                    pool: &#root::sql::Pool,
4117                ) -> ::core::result::Result<
4118                    ::core::option::Option<U>,
4119                    #root::sql::ExecError,
4120                >
4121                where
4122                    (::core::option::Option<U>,): #root::sql::MaybePgFromRow
4123                        + #root::sql::MaybeMyFromRow
4124                        + #root::sql::MaybeSqliteFromRow
4125                        + ::core::marker::Send
4126                        + ::core::marker::Unpin,
4127                {
4128                    Self::__aggregate_one_pool::<U>(
4129                        col,
4130                        |c| #root::core::AggregateExpr::Max(c),
4131                        pool,
4132                    )
4133                    .await
4134                }
4135            },
4136        );
4137        let first_method = emit_if_no_field_collision(
4138            "first",
4139            quote! {
4140                /// First row of this model. Eloquent `Model::first()`.
4141                /// Skipped on models that already declare a field named
4142                /// `first`. Drop into `QuerySet::<Self>::default().first(&pool)`.
4143                ///
4144                /// # Errors
4145                /// As `QuerySet::first`.
4146                pub async fn first(
4147                    pool: &#root::sql::Pool,
4148                ) -> ::core::result::Result<
4149                    ::core::option::Option<Self>,
4150                    #root::sql::ExecError,
4151                > {
4152                    #root::query::QuerySet::<Self>::default()
4153                        .first(pool)
4154                        .await
4155                }
4156            },
4157        );
4158        let last_method = emit_if_no_field_collision(
4159            "last",
4160            quote! {
4161                /// Last row of this model by primary-key DESC.
4162                /// Eloquent `Model::query()->latest('id')->first()`
4163                /// parity — fetches the highest-PK row without
4164                /// requiring the caller to spell the PK column.
4165                /// Returns `None` on an empty table.
4166                ///
4167                /// Equivalent to `QuerySet::<Self>::default().last(&pool)`.
4168                /// Skipped on models that already declare a field
4169                /// named `last`.
4170                ///
4171                /// # Errors
4172                /// As `QuerySet::last`.
4173                pub async fn last(
4174                    pool: &#root::sql::Pool,
4175                ) -> ::core::result::Result<
4176                    ::core::option::Option<Self>,
4177                    #root::sql::ExecError,
4178                > {
4179                    #root::query::QuerySet::<Self>::default()
4180                        .last(pool)
4181                        .await
4182                }
4183            },
4184        );
4185        quote! {
4186            /// Re-SELECT this row by its primary key and overwrite
4187            /// every in-memory field with the freshly-fetched value.
4188            /// Django's [`Model.refresh_from_db`]. Issue #825.
4189            ///
4190            /// Use this when the row may have been modified by another
4191            /// process / connection / job since you read it — e.g. after
4192            /// a queued task callback, or to re-sync stale UI state
4193            /// before re-saving.
4194            ///
4195            /// Returns [`ExecError::Driver(sqlx::Error::RowNotFound)`]
4196            /// when the primary key no longer matches any row (e.g.
4197            /// the row was deleted concurrently).
4198            ///
4199            /// # Errors
4200            /// As [`FetcherPool::fetch`]; also `RowNotFound` when
4201            /// the PK no longer exists.
4202            ///
4203            /// [`Model.refresh_from_db`]: https://docs.djangoproject.com/en/5.1/ref/models/instances/#django.db.models.Model.refresh_from_db
4204            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4205            pub async fn refresh_from_db(
4206                &mut self,
4207                pool: &#root::sql::Pool,
4208            ) -> ::core::result::Result<(), #root::sql::ExecError> {
4209                use #root::sql::FetcherPool as _;
4210                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4211                    ::core::clone::Clone::clone(&self.#pk_ident),
4212                );
4213                let mut _rows: ::std::vec::Vec<Self> =
4214                    #root::query::QuerySet::<Self>::default()
4215                        .filter(::core::stringify!(#pk_ident), _pk_val)
4216                        .limit(1)
4217                        .fetch(pool)
4218                        .await?;
4219                match _rows.into_iter().next() {
4220                    ::core::option::Option::Some(_fresh) => {
4221                        *self = _fresh;
4222                        ::core::result::Result::Ok(())
4223                    }
4224                    ::core::option::Option::None => ::core::result::Result::Err(
4225                        #root::sql::ExecError::Driver(
4226                            #root::sql::sqlx::Error::RowNotFound,
4227                        ),
4228                    ),
4229                }
4230            }
4231
4232            /// Atomically increment the integer column `col` by
4233            /// `by` for this row. Equivalent to
4234            /// `UPDATE <table> SET <col> = <col> + $1 WHERE <pk> = $2`.
4235            /// Eloquent `Model::increment($col, $by)` / Django
4236            /// `Model.objects.filter(pk=…).update(col=F('col')+$by)`
4237            /// parity.
4238            ///
4239            /// **Doesn't mutate `self`** — the in-memory copy is now
4240            /// stale; call [`Self::refresh_from_db_pool`] /
4241            /// [`Self::fresh_pool`] to re-sync. Returns the rows-
4242            /// affected count (0 when the PK doesn't match any row,
4243            /// 1 on success).
4244            ///
4245            /// `col` is the Rust field name as a string; unknown
4246            /// fields surface as `UnknownField` at runtime. Negative
4247            /// `by` values atomically decrement (see also
4248            /// [`Self::decrement_pool`]).
4249            ///
4250            /// # Errors
4251            /// As [`UpdaterPool::execute_pool`].
4252            ///
4253            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4254            pub async fn increment(
4255                &self,
4256                col: &str,
4257                by: i64,
4258                pool: &#root::sql::Pool,
4259            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4260                Self::__increment_one(self, col, by, pool).await
4261            }
4262
4263            /// Sibling of [`Self::increment`] — atomically
4264            /// decrement this row's `col` by `by`. Eloquent
4265            /// `$model->decrement($col, $by)` parity. Equivalent to
4266            /// `self.increment(col, -by, &pool)`; the separate name
4267            /// keeps call sites readable.
4268            ///
4269            /// # Errors
4270            /// As [`Self::increment`].
4271            pub async fn decrement(
4272                &self,
4273                col: &str,
4274                by: i64,
4275                pool: &#root::sql::Pool,
4276            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4277                Self::__increment_one(self, col, -by, pool).await
4278            }
4279
4280            /// Bulk-increment: add `by` to `col` on every row of the
4281            /// table. Eloquent `Model::query()->increment($col, $by)`
4282            /// parity. Use for counters, score adjustments, view
4283            /// rollups.
4284            ///
4285            /// # Errors
4286            /// As [`UpdaterPool::execute_pool`].
4287            ///
4288            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
4289            pub async fn increment_each(
4290                col: &str,
4291                by: i64,
4292                pool: &#root::sql::Pool,
4293            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4294                Self::__increment_all(col, by, pool).await
4295            }
4296
4297            /// Sibling of [`Self::increment_each`] — bulk-decrement.
4298            ///
4299            /// # Errors
4300            /// As [`Self::increment_each`].
4301            pub async fn decrement_each(
4302                col: &str,
4303                by: i64,
4304                pool: &#root::sql::Pool,
4305            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4306                Self::__increment_all(col, -by, pool).await
4307            }
4308
4309            /// Internal: forward to
4310            /// [`#root::sql::model_shortcuts::increment_one_pool`].
4311            /// One-line wrapper kept as a per-Model method so the
4312            /// macro's emitted `increment` / `decrement` instance
4313            /// calls don't have to thread `Self` through manually.
4314            #[doc(hidden)]
4315            pub async fn __increment_one(
4316                this: &Self,
4317                col: &str,
4318                by: i64,
4319                pool: &#root::sql::Pool,
4320            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4321                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4322                    ::core::clone::Clone::clone(&this.#pk_ident),
4323                );
4324                #root::sql::model_shortcuts::increment_one_pool::<Self>(
4325                    ::core::stringify!(#pk_ident),
4326                    _pk_val,
4327                    col,
4328                    by,
4329                    pool,
4330                )
4331                .await
4332            }
4333
4334            /// Internal: forward to
4335            /// [`#root::sql::model_shortcuts::increment_all_pool`].
4336            #[doc(hidden)]
4337            pub async fn __increment_all(
4338                col: &str,
4339                by: i64,
4340                pool: &#root::sql::Pool,
4341            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4342                #root::sql::model_shortcuts::increment_all_pool::<Self>(col, by, pool).await
4343            }
4344
4345            /// Internal: forward to
4346            /// [`#root::sql::model_shortcuts::resolve_col`].
4347            #[doc(hidden)]
4348            pub fn __resolve_col(
4349                col: &str,
4350            ) -> ::core::result::Result<&'static str, #root::sql::ExecError> {
4351                #root::sql::model_shortcuts::resolve_col::<Self>(col)
4352            }
4353
4354            /// Internal: forward to
4355            /// [`#root::sql::model_shortcuts::add_signed_expr`].
4356            #[doc(hidden)]
4357            #[must_use]
4358            pub fn __add_signed_expr(
4359                col_static: &'static str,
4360                signed_by: i64,
4361            ) -> #root::core::Expr {
4362                #root::sql::model_shortcuts::add_signed_expr(col_static, signed_by)
4363            }
4364
4365            /// Re-SELECT this row by its primary key and return a
4366            /// **new** instance with the freshly-fetched fields.
4367            /// Eloquent `Model::fresh()` parity — non-mutating
4368            /// counterpart of [`Self::refresh_from_db_pool`].
4369            ///
4370            /// Returns `Ok(None)` when the row was deleted
4371            /// concurrently — vs [`Self::refresh_from_db_pool`]
4372            /// which surfaces that as `RowNotFound` because
4373            /// in-place mutation has nothing to write to.
4374            ///
4375            /// Useful when you want to compare the in-memory
4376            /// instance against the persisted state (audit-style
4377            /// diffs, conflict detection) without mutating the
4378            /// reference you already hold.
4379            ///
4380            /// # Errors
4381            /// As [`FetcherPool::fetch`].
4382            ///
4383            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4384            pub async fn fresh(
4385                &self,
4386                pool: &#root::sql::Pool,
4387            ) -> ::core::result::Result<
4388                ::core::option::Option<Self>,
4389                #root::sql::ExecError,
4390            > {
4391                use #root::sql::FetcherPool as _;
4392                let _pk_val: #root::core::SqlValue = ::core::convert::Into::into(
4393                    ::core::clone::Clone::clone(&self.#pk_ident),
4394                );
4395                let _rows: ::std::vec::Vec<Self> =
4396                    #root::query::QuerySet::<Self>::default()
4397                        .filter(::core::stringify!(#pk_ident), _pk_val)
4398                        .limit(1)
4399                        .fetch(pool)
4400                        .await?;
4401                ::core::result::Result::Ok(_rows.into_iter().next())
4402            }
4403
4404            #replicate_doc
4405            #[must_use]
4406            pub fn replicate(&self) -> Self {
4407                Self {
4408                    #pk_clone_token,
4409                    #( #other_field_clones, )*
4410                }
4411            }
4412
4413            #first_method
4414
4415            #last_method
4416
4417            /// Throwing counterpart of [`Self::first_pool`] —
4418            /// errors with `RowNotFound` when the table is empty.
4419            /// Eloquent `Model::firstOrFail()` parity.
4420            ///
4421            /// # Errors
4422            /// As [`Self::first_pool`]; additionally
4423            /// [`sqlx::Error::RowNotFound`] on empty tables.
4424            ///
4425            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
4426            pub async fn first_or_fail(
4427                pool: &#root::sql::Pool,
4428            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
4429                // Route through the queryset rather than `Self::first`,
4430                // which is suppressed on models with a field named
4431                // `first` (field/shortcut collision guard).
4432                match #root::query::QuerySet::<Self>::default().first(pool).await? {
4433                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
4434                    ::core::option::Option::None => ::core::result::Result::Err(
4435                        #root::sql::ExecError::Driver(
4436                            #root::sql::sqlx::Error::RowNotFound,
4437                        ),
4438                    ),
4439                }
4440            }
4441
4442            /// Single-column projection — `SELECT <col> FROM
4443            /// <table>`. Returns `Vec<U>` where each element is the
4444            /// decoded value of the column. Eloquent
4445            /// `Model::pluck($column)` / Django
4446            /// `Model.objects.values_list('col', flat=True)` parity.
4447            ///
4448            /// Thin wrapper over `QuerySet::<Self>::default()
4449            /// .values_list_flat(col).fetch::<U>(pool)`. `U` must
4450            /// be decodable from the column's SQL type on every
4451            /// dialect the binary targets (common picks: `i64` /
4452            /// `i32` / `String` / `bool` / `f64`).
4453            ///
4454            /// # Errors
4455            /// As `ValuesFlatQuerySet::fetch`.
4456            pub async fn pluck<U>(
4457                col: &'static str,
4458                pool: &#root::sql::Pool,
4459            ) -> ::core::result::Result<::std::vec::Vec<U>, #root::sql::ExecError>
4460            where
4461                U: #root::sql::MaybePgScalar
4462                    + #root::sql::MaybeMyScalar
4463                    + #root::sql::MaybeSqliteScalar
4464                    + ::core::marker::Send
4465                    + ::core::marker::Unpin,
4466            {
4467                #root::query::QuerySet::<Self>::default()
4468                    .values_list_flat(col)
4469                    .fetch::<U>(pool)
4470                    .await
4471            }
4472
4473            /// Eloquent `Model::chunk($n, fn ($chunk) { ... })` —
4474            /// stream every row of this model in batches of `n`,
4475            /// invoking the callback once per batch. Stable PK-ASC
4476            /// ordering so the LIMIT/OFFSET pagination is
4477            /// deterministic across drivers.
4478            ///
4479            /// The callback is async — it can do further DB work
4480            /// (writes, related-row lookups, queue dispatch) per
4481            /// batch. Return `Err(...)` from the callback to abort
4482            /// the iteration early; the error bubbles up.
4483            ///
4484            /// **When to use**: bulk processing flows that can't
4485            /// fit the whole table in memory — sending newsletters,
4486            /// running data migrations, computing summary
4487            /// statistics. For small / known-bounded tables, plain
4488            /// `Self::all(&pool)` is simpler.
4489            ///
4490            /// **Caveat**: ascending OFFSET pagination is O(N²) on
4491            /// large tables. For multi-million-row scans prefer
4492            /// keyset-by-PK (the standard "WHERE id > last_seen"
4493            /// shape) over `chunk(...)`.
4494            ///
4495            /// Skipped on models without a primary key — chunking
4496            /// needs a stable order to avoid skipping / repeating
4497            /// rows across batches.
4498            pub async fn chunk<F, Fut>(
4499                n: i64,
4500                pool: &#root::sql::Pool,
4501                mut cb: F,
4502            ) -> ::core::result::Result<(), #root::sql::ExecError>
4503            where
4504                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4505                Fut: ::core::future::Future<
4506                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4507                >,
4508            {
4509                use #root::sql::FetcherPool as _;
4510                let pk_col = match Self::primary_key_column() {
4511                    ::core::option::Option::Some(c) => c,
4512                    ::core::option::Option::None => {
4513                        return ::core::result::Result::Ok(());
4514                    }
4515                };
4516                let mut offset: i64 = 0;
4517                loop {
4518                    let rows: ::std::vec::Vec<Self> =
4519                        #root::query::QuerySet::<Self>::default()
4520                            .order_by(&[(pk_col, false)])
4521                            .limit(n)
4522                            .offset(offset)
4523                            .fetch(pool)
4524                            .await?;
4525                    if rows.is_empty() {
4526                        return ::core::result::Result::Ok(());
4527                    }
4528                    let len = rows.len() as i64;
4529                    cb(rows).await?;
4530                    if len < n {
4531                        return ::core::result::Result::Ok(());
4532                    }
4533                    offset += n;
4534                }
4535            }
4536
4537            /// Eloquent `Model::chunkById($n, fn (...))` — same
4538            /// per-batch callback shape as [`Self::chunk`], but uses
4539            /// **keyset pagination** (`WHERE pk > last_seen LIMIT n`)
4540            /// instead of OFFSET. O(N) total scan vs OFFSET's O(N²)
4541            /// — the right choice for multi-million-row sweeps.
4542            ///
4543            /// Requires the primary key to be a signed integer type
4544            /// (`i64` / `i32`); the keyset comparison rides on
4545            /// `__rustango_pk_value()` lowering through
4546            /// `SqlValue::I64` / `SqlValue::I32`. Skipped on
4547            /// non-integer PKs (UUID, String) — those should use
4548            /// the OFFSET-shaped [`Self::chunk`] or a hand-rolled
4549            /// keyset loop.
4550            ///
4551            /// Callback errors abort iteration; the error bubbles up
4552            /// unchanged. Empty table → callback invoked zero times.
4553            pub async fn chunk_by_id<F, Fut>(
4554                n: i64,
4555                pool: &#root::sql::Pool,
4556                mut cb: F,
4557            ) -> ::core::result::Result<(), #root::sql::ExecError>
4558            where
4559                F: ::core::ops::FnMut(::std::vec::Vec<Self>) -> Fut,
4560                Fut: ::core::future::Future<
4561                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4562                >,
4563            {
4564                use #root::sql::FetcherPool as _;
4565                let pk_col = match Self::primary_key_column() {
4566                    ::core::option::Option::Some(c) => c,
4567                    ::core::option::Option::None => {
4568                        return ::core::result::Result::Ok(());
4569                    }
4570                };
4571                // Track the largest PK seen so the next batch picks
4572                // up from there. `i64::MIN` as the sentinel — the
4573                // very first iteration's `> MIN` matches every row,
4574                // so the loop entry is uniform with subsequent
4575                // iterations.
4576                let mut last_seen: i64 = i64::MIN;
4577                loop {
4578                    let key = ::std::format!("{}__gt", pk_col);
4579                    let rows: ::std::vec::Vec<Self> =
4580                        #root::query::QuerySet::<Self>::default()
4581                            .filter(key.as_str(), last_seen)
4582                            .order_by(&[(pk_col, false)])
4583                            .limit(n)
4584                            .fetch(pool)
4585                            .await?;
4586                    if rows.is_empty() {
4587                        return ::core::result::Result::Ok(());
4588                    }
4589                    let len = rows.len() as i64;
4590                    // Capture the last row's PK BEFORE moving rows
4591                    // into the callback.
4592                    let max_pk = match rows
4593                        .last()
4594                        .map(|r| r.__rustango_pk_value())
4595                    {
4596                        ::core::option::Option::Some(
4597                            #root::core::SqlValue::I64(v),
4598                        ) => v,
4599                        ::core::option::Option::Some(
4600                            #root::core::SqlValue::I32(v),
4601                        ) => i64::from(v),
4602                        _ => return ::core::result::Result::Ok(()),
4603                    };
4604                    cb(rows).await?;
4605                    if len < n {
4606                        return ::core::result::Result::Ok(());
4607                    }
4608                    last_seen = max_pk;
4609                }
4610            }
4611
4612            /// Eloquent `Model::each(fn ($row) { ... }, $n)` —
4613            /// per-row callback companion to [`Self::chunk`].
4614            /// Streams every row in keyset-paginated batches of
4615            /// `batch` size, calling `cb` once per row.
4616            ///
4617            /// Inherits the keyset-paginated scan of
4618            /// [`Self::chunk_by_id`] (O(N) total, integer PK only —
4619            /// non-integer PKs are silently a no-op).
4620            ///
4621            /// ```ignore
4622            /// Post::each(500, &pool, |p| async move {
4623            ///     reindex(p).await?;
4624            ///     Ok(())
4625            /// }).await?;
4626            /// ```
4627            ///
4628            /// Return `Err(...)` from the callback to abort
4629            /// iteration; the error bubbles up unchanged.
4630            pub async fn each<F, Fut>(
4631                batch: i64,
4632                pool: &#root::sql::Pool,
4633                mut cb: F,
4634            ) -> ::core::result::Result<(), #root::sql::ExecError>
4635            where
4636                F: ::core::ops::FnMut(Self) -> Fut,
4637                Fut: ::core::future::Future<
4638                    Output = ::core::result::Result<(), #root::sql::ExecError>,
4639                >,
4640            {
4641                use #root::sql::FetcherPool as _;
4642                let pk_col = match Self::primary_key_column() {
4643                    ::core::option::Option::Some(c) => c,
4644                    ::core::option::Option::None => {
4645                        return ::core::result::Result::Ok(());
4646                    }
4647                };
4648                let mut last_seen: i64 = i64::MIN;
4649                loop {
4650                    let key = ::std::format!("{}__gt", pk_col);
4651                    let rows: ::std::vec::Vec<Self> =
4652                        #root::query::QuerySet::<Self>::default()
4653                            .filter(key.as_str(), last_seen)
4654                            .order_by(&[(pk_col, false)])
4655                            .limit(batch)
4656                            .fetch(pool)
4657                            .await?;
4658                    if rows.is_empty() {
4659                        return ::core::result::Result::Ok(());
4660                    }
4661                    let len = rows.len() as i64;
4662                    let max_pk = match rows
4663                        .last()
4664                        .map(|r| r.__rustango_pk_value())
4665                    {
4666                        ::core::option::Option::Some(
4667                            #root::core::SqlValue::I64(v),
4668                        ) => v,
4669                        ::core::option::Option::Some(
4670                            #root::core::SqlValue::I32(v),
4671                        ) => i64::from(v),
4672                        _ => return ::core::result::Result::Ok(()),
4673                    };
4674                    for row in rows {
4675                        cb(row).await?;
4676                    }
4677                    if len < batch {
4678                        return ::core::result::Result::Ok(());
4679                    }
4680                    last_seen = max_pk;
4681                }
4682            }
4683
4684            /// Delete every row of this model — `TRUNCATE TABLE
4685            /// <table> RESTART IDENTITY CASCADE` on Postgres,
4686            /// `DELETE FROM <table>` on MySQL / SQLite (which don't
4687            /// support `TRUNCATE` inside foreign-key constraints
4688            /// or — for SQLite — at all). Eloquent `Model::truncate()`
4689            /// / Django `Model.objects.all().delete()` parity.
4690            ///
4691            /// **Use only in tests / fixture-reset flows.** Production
4692            /// writes through this would silently bypass the
4693            /// `pre_delete` / `post_delete` signals (no per-row hooks
4694            /// fire on a TRUNCATE / bulk DELETE FROM) and lose every
4695            /// row's audit-log entry.
4696            ///
4697            /// # Errors
4698            /// As [`raw_execute_pool`].
4699            ///
4700            /// [`raw_execute_pool`]: rustango::sql::raw_execute_pool
4701            pub async fn truncate(
4702                pool: &#root::sql::Pool,
4703            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
4704                let _table = <Self as #root::core::Model>::SCHEMA.table;
4705                let _dialect = pool.dialect();
4706                let _quoted = _dialect.quote_ident(_table);
4707                let _sql = if _dialect.name() == "postgres" {
4708                    ::std::format!("TRUNCATE TABLE {} RESTART IDENTITY CASCADE", _quoted)
4709                } else {
4710                    ::std::format!("DELETE FROM {}", _quoted)
4711                };
4712                #root::sql::raw_execute_pool(pool, &_sql, ::std::vec::Vec::new()).await
4713            }
4714
4715            /// Bulk-delete every row whose primary key is in
4716            /// `pks` — `DELETE FROM <table> WHERE <pk> IN (...)`.
4717            /// Returns the affected row count.
4718            ///
4719            /// Eloquent `Model::destroy([1, 2, 3])` / Django
4720            /// `Model.objects.filter(pk__in=[...]).delete()` parity.
4721            /// Empty `pks` is a no-op (returns 0).
4722            ///
4723            /// Accepts any iterable whose elements are
4724            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`,
4725            /// `[i64; N]`, etc.
4726            ///
4727            /// # Errors
4728            /// As `delete_pool`.
4729            pub async fn destroy<V>(
4730                pks: impl ::core::iter::IntoIterator<Item = V>,
4731                pool: &#root::sql::Pool,
4732            ) -> ::core::result::Result<u64, #root::sql::ExecError>
4733            where
4734                V: ::core::convert::Into<#root::core::SqlValue>,
4735            {
4736                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4737                    pks.into_iter().map(::core::convert::Into::into).collect();
4738                if _values.is_empty() {
4739                    return ::core::result::Result::Ok(0);
4740                }
4741                let _query = #root::core::DeleteQuery {
4742                    model: <Self as #root::core::Model>::SCHEMA,
4743                    where_clause: #root::core::WhereExpr::Predicate(
4744                        #root::core::Filter {
4745                            column: <Self as #root::core::Model>::SCHEMA
4746                                .primary_key()
4747                                .ok_or_else(|| {
4748                                    #root::sql::ExecError::Sql(
4749                                        #root::sql::SqlError::MissingPrimaryKey,
4750                                    )
4751                                })?
4752                                .column,
4753                            op: #root::core::Op::In,
4754                            value: #root::core::SqlValue::List(_values),
4755                        },
4756                    ),
4757                };
4758                #root::sql::delete_pool(pool, &_query).await
4759            }
4760
4761            /// Fetch every row where `<col> = <val>`. Eloquent
4762            /// `Model::where($col, $val)->get()` / Django
4763            /// `Model.objects.filter(col=val).all()` parity.
4764            ///
4765            /// Thin wrapper over `QuerySet::<Self>::default()
4766            /// .filter(col, val).fetch(pool)`. For one row,
4767            /// use [`Self::first_where_pool`]; for a chain that
4768            /// needs further `.filter()` / `.order_by()` /
4769            /// `.limit()`, drop down to `Self::query().filter(...)`
4770            /// directly.
4771            ///
4772            /// `val` accepts any value `Into<SqlValue>` so plain
4773            /// strings, ints, UUIDs, etc. all work.
4774            ///
4775            /// # Errors
4776            /// As [`FetcherPool::fetch`].
4777            ///
4778            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4779            pub async fn where_(
4780                col: &str,
4781                val: impl ::core::convert::Into<#root::core::SqlValue>,
4782                pool: &#root::sql::Pool,
4783            ) -> ::core::result::Result<
4784                ::std::vec::Vec<Self>,
4785                #root::sql::ExecError,
4786            > {
4787                use #root::sql::FetcherPool as _;
4788                #root::query::QuerySet::<Self>::default()
4789                    .filter(col, val)
4790                    .fetch(pool)
4791                    .await
4792            }
4793
4794            /// Fetch every row where `<col> IN (vals)`. Eloquent
4795            /// `Model::whereIn($col, $vals)->get()` parity. Empty
4796            /// `vals` returns no rows (matches SQL's empty-IN
4797            /// semantics).
4798            ///
4799            /// `vals` accepts any iterable whose items are
4800            /// `Into<SqlValue>` — `Vec<i64>`, `&[&str]`, `[Uuid; N]`,
4801            /// etc.
4802            ///
4803            /// # Errors
4804            /// As [`FetcherPool::fetch`].
4805            ///
4806            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4807            pub async fn where_in<V>(
4808                col: &str,
4809                vals: impl ::core::iter::IntoIterator<Item = V>,
4810                pool: &#root::sql::Pool,
4811            ) -> ::core::result::Result<
4812                ::std::vec::Vec<Self>,
4813                #root::sql::ExecError,
4814            >
4815            where
4816                V: ::core::convert::Into<#root::core::SqlValue>,
4817            {
4818                use #root::sql::FetcherPool as _;
4819                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4820                    vals.into_iter().map(::core::convert::Into::into).collect();
4821                if _values.is_empty() {
4822                    return ::core::result::Result::Ok(::std::vec::Vec::new());
4823                }
4824                let _key = ::std::format!("{}__in", col);
4825                #root::query::QuerySet::<Self>::default()
4826                    .filter(&_key, #root::core::SqlValue::List(_values))
4827                    .fetch(pool)
4828                    .await
4829            }
4830
4831            /// Fetch every row where `<col> NOT IN (vals)`. Eloquent
4832            /// `Model::whereNotIn($col, $vals)->get()` parity. Empty
4833            /// `vals` returns every row (matches SQL's empty-NOT-IN
4834            /// semantics — vacuously true for every row).
4835            ///
4836            /// # Errors
4837            /// As [`FetcherPool::fetch`].
4838            ///
4839            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4840            pub async fn where_not_in<V>(
4841                col: &str,
4842                vals: impl ::core::iter::IntoIterator<Item = V>,
4843                pool: &#root::sql::Pool,
4844            ) -> ::core::result::Result<
4845                ::std::vec::Vec<Self>,
4846                #root::sql::ExecError,
4847            >
4848            where
4849                V: ::core::convert::Into<#root::core::SqlValue>,
4850            {
4851                use #root::sql::FetcherPool as _;
4852                let _values: ::std::vec::Vec<#root::core::SqlValue> =
4853                    vals.into_iter().map(::core::convert::Into::into).collect();
4854                if _values.is_empty() {
4855                    return #root::query::QuerySet::<Self>::default()
4856                        .fetch(pool)
4857                        .await;
4858                }
4859                let _key = ::std::format!("{}__not_in", col);
4860                #root::query::QuerySet::<Self>::default()
4861                    .filter(&_key, #root::core::SqlValue::List(_values))
4862                    .fetch(pool)
4863                    .await
4864            }
4865
4866            /// Fetch every row where `<col> IS NULL`. Eloquent
4867            /// `Model::whereNull($col)->get()` parity.
4868            ///
4869            /// # Errors
4870            /// As [`FetcherPool::fetch`].
4871            ///
4872            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4873            pub async fn where_null(
4874                col: &str,
4875                pool: &#root::sql::Pool,
4876            ) -> ::core::result::Result<
4877                ::std::vec::Vec<Self>,
4878                #root::sql::ExecError,
4879            > {
4880                use #root::sql::FetcherPool as _;
4881                let _key = ::std::format!("{}__isnull", col);
4882                #root::query::QuerySet::<Self>::default()
4883                    .filter(&_key, #root::core::SqlValue::Bool(true))
4884                    .fetch(pool)
4885                    .await
4886            }
4887
4888            /// Fetch every row where `<col> IS NOT NULL`. Eloquent
4889            /// `Model::whereNotNull($col)->get()` parity.
4890            ///
4891            /// # Errors
4892            /// As [`FetcherPool::fetch`].
4893            ///
4894            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4895            pub async fn where_not_null(
4896                col: &str,
4897                pool: &#root::sql::Pool,
4898            ) -> ::core::result::Result<
4899                ::std::vec::Vec<Self>,
4900                #root::sql::ExecError,
4901            > {
4902                use #root::sql::FetcherPool as _;
4903                let _key = ::std::format!("{}__isnull", col);
4904                #root::query::QuerySet::<Self>::default()
4905                    .filter(&_key, #root::core::SqlValue::Bool(false))
4906                    .fetch(pool)
4907                    .await
4908            }
4909
4910            /// Fetch up to `n` rows in random order. Eloquent
4911            /// `Model::inRandomOrder()->limit($n)->get()` /
4912            /// `Model::query()->inRandomOrder()->get()->take($n)`
4913            /// parity. **Performance caveat**: random ordering
4914            /// forces a full table scan + per-row random key sort;
4915            /// the optimizer cannot use an index. Prefer a
4916            /// `pk >= rand_offset LIMIT N` walk for huge tables.
4917            ///
4918            /// # Errors
4919            /// As [`FetcherPool::fetch`].
4920            ///
4921            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4922            pub async fn random_n(
4923                n: i64,
4924                pool: &#root::sql::Pool,
4925            ) -> ::core::result::Result<
4926                ::std::vec::Vec<Self>,
4927                #root::sql::ExecError,
4928            > {
4929                use #root::sql::FetcherPool as _;
4930                #root::query::QuerySet::<Self>::default()
4931                    .order_random()
4932                    .limit(n)
4933                    .fetch(pool)
4934                    .await
4935            }
4936
4937            /// Fetch one row in random order. Eloquent
4938            /// `Model::inRandomOrder()->first()` parity. Same
4939            /// performance caveat as [`Self::random_n_pool`].
4940            ///
4941            /// # Errors
4942            /// As [`FetcherPool::fetch`].
4943            ///
4944            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4945            pub async fn random(
4946                pool: &#root::sql::Pool,
4947            ) -> ::core::result::Result<
4948                ::core::option::Option<Self>,
4949                #root::sql::ExecError,
4950            > {
4951                ::core::result::Result::Ok(
4952                    Self::random_n(1, pool).await?.into_iter().next(),
4953                )
4954            }
4955
4956            /// Fetch every row ordered ASC by `field`. Eloquent
4957            /// `Model::oldest($field)->get()` parity — the multi-row
4958            /// counterpart of [`Self::earliest_pool`].
4959            ///
4960            /// # Errors
4961            /// As [`FetcherPool::fetch`].
4962            ///
4963            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4964            pub async fn oldest(
4965                field: &str,
4966                pool: &#root::sql::Pool,
4967            ) -> ::core::result::Result<
4968                ::std::vec::Vec<Self>,
4969                #root::sql::ExecError,
4970            > {
4971                use #root::sql::FetcherPool as _;
4972                #root::query::QuerySet::<Self>::default()
4973                    .order_by(&[(field, false)])
4974                    .fetch(pool)
4975                    .await
4976            }
4977
4978            /// Fetch every row ordered DESC by `field`. Eloquent
4979            /// `Model::latest($field)->get()` parity — the multi-row
4980            /// counterpart of [`Self::latest_pool`].
4981            ///
4982            /// # Errors
4983            /// As [`FetcherPool::fetch`].
4984            ///
4985            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
4986            pub async fn newest(
4987                field: &str,
4988                pool: &#root::sql::Pool,
4989            ) -> ::core::result::Result<
4990                ::std::vec::Vec<Self>,
4991                #root::sql::ExecError,
4992            > {
4993                use #root::sql::FetcherPool as _;
4994                #root::query::QuerySet::<Self>::default()
4995                    .order_by(&[(field, true)])
4996                    .fetch(pool)
4997                    .await
4998            }
4999
5000            /// Fetch every row where `EXTRACT(YEAR FROM <col>) = year`.
5001            /// Eloquent `Model::whereYear($col, $year)->get()` parity.
5002            /// Routes through the existing `__year` lookup suffix
5003            /// (issue #829).
5004            ///
5005            /// # Errors
5006            /// As [`FetcherPool::fetch`].
5007            ///
5008            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5009            pub async fn where_year(
5010                col: &str,
5011                year: i64,
5012                pool: &#root::sql::Pool,
5013            ) -> ::core::result::Result<
5014                ::std::vec::Vec<Self>,
5015                #root::sql::ExecError,
5016            > {
5017                use #root::sql::FetcherPool as _;
5018                let _key = ::std::format!("{}__year", col);
5019                #root::query::QuerySet::<Self>::default()
5020                    .filter(&_key, #root::core::SqlValue::I64(year))
5021                    .fetch(pool)
5022                    .await
5023            }
5024
5025            /// Fetch every row where `EXTRACT(MONTH FROM <col>) = month`.
5026            /// Eloquent `Model::whereMonth($col, $m)->get()` parity.
5027            /// `month` is 1–12.
5028            ///
5029            /// # Errors
5030            /// As [`FetcherPool::fetch`].
5031            ///
5032            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5033            pub async fn where_month(
5034                col: &str,
5035                month: i64,
5036                pool: &#root::sql::Pool,
5037            ) -> ::core::result::Result<
5038                ::std::vec::Vec<Self>,
5039                #root::sql::ExecError,
5040            > {
5041                use #root::sql::FetcherPool as _;
5042                let _key = ::std::format!("{}__month", col);
5043                #root::query::QuerySet::<Self>::default()
5044                    .filter(&_key, #root::core::SqlValue::I64(month))
5045                    .fetch(pool)
5046                    .await
5047            }
5048
5049            /// Fetch every row where `EXTRACT(DAY FROM <col>) = day`.
5050            /// Eloquent `Model::whereDay($col, $d)->get()` parity.
5051            /// `day` is 1–31.
5052            ///
5053            /// # Errors
5054            /// As [`FetcherPool::fetch`].
5055            ///
5056            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5057            pub async fn where_day(
5058                col: &str,
5059                day: i64,
5060                pool: &#root::sql::Pool,
5061            ) -> ::core::result::Result<
5062                ::std::vec::Vec<Self>,
5063                #root::sql::ExecError,
5064            > {
5065                use #root::sql::FetcherPool as _;
5066                let _key = ::std::format!("{}__day", col);
5067                #root::query::QuerySet::<Self>::default()
5068                    .filter(&_key, #root::core::SqlValue::I64(day))
5069                    .fetch(pool)
5070                    .await
5071            }
5072
5073            /// Fetch every row where `EXTRACT(HOUR FROM <col>) = hour`.
5074            /// Eloquent `Model::whereHour($col, $h)->get()` parity.
5075            /// `hour` is 0–23.
5076            ///
5077            /// # Errors
5078            /// As [`FetcherPool::fetch`].
5079            ///
5080            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5081            pub async fn where_hour(
5082                col: &str,
5083                hour: i64,
5084                pool: &#root::sql::Pool,
5085            ) -> ::core::result::Result<
5086                ::std::vec::Vec<Self>,
5087                #root::sql::ExecError,
5088            > {
5089                use #root::sql::FetcherPool as _;
5090                let _key = ::std::format!("{}__hour", col);
5091                #root::query::QuerySet::<Self>::default()
5092                    .filter(&_key, #root::core::SqlValue::I64(hour))
5093                    .fetch(pool)
5094                    .await
5095            }
5096
5097            /// Fetch every row where `EXTRACT(MINUTE FROM <col>) = minute`.
5098            /// Eloquent `Model::whereMinute($col, $m)->get()` parity.
5099            /// `minute` is 0–59.
5100            ///
5101            /// # Errors
5102            /// As [`FetcherPool::fetch`].
5103            ///
5104            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5105            pub async fn where_minute(
5106                col: &str,
5107                minute: i64,
5108                pool: &#root::sql::Pool,
5109            ) -> ::core::result::Result<
5110                ::std::vec::Vec<Self>,
5111                #root::sql::ExecError,
5112            > {
5113                use #root::sql::FetcherPool as _;
5114                let _key = ::std::format!("{}__minute", col);
5115                #root::query::QuerySet::<Self>::default()
5116                    .filter(&_key, #root::core::SqlValue::I64(minute))
5117                    .fetch(pool)
5118                    .await
5119            }
5120
5121            /// Fetch every row where `<col> LIKE <pattern>` —
5122            /// caller-supplied pattern (must include `%` / `_`
5123            /// wildcards manually). Eloquent `Model::whereLike`
5124            /// parity.
5125            ///
5126            /// # Errors
5127            /// As [`FetcherPool::fetch`].
5128            ///
5129            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5130            pub async fn where_like(
5131                col: &str,
5132                pattern: impl ::core::convert::Into<::std::string::String>,
5133                pool: &#root::sql::Pool,
5134            ) -> ::core::result::Result<
5135                ::std::vec::Vec<Self>,
5136                #root::sql::ExecError,
5137            > {
5138                use #root::sql::FetcherPool as _;
5139                let _key = ::std::format!("{}__like", col);
5140                #root::query::QuerySet::<Self>::default()
5141                    .filter(
5142                        &_key,
5143                        #root::core::SqlValue::String(pattern.into()),
5144                    )
5145                    .fetch(pool)
5146                    .await
5147            }
5148
5149            /// Fetch every row where `<col> ILIKE <pattern>` —
5150            /// case-insensitive LIKE (PG native, MySQL/SQLite
5151            /// emulated via `LOWER(col) LIKE LOWER(pattern)`).
5152            ///
5153            /// # Errors
5154            /// As [`FetcherPool::fetch`].
5155            ///
5156            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5157            pub async fn where_ilike(
5158                col: &str,
5159                pattern: impl ::core::convert::Into<::std::string::String>,
5160                pool: &#root::sql::Pool,
5161            ) -> ::core::result::Result<
5162                ::std::vec::Vec<Self>,
5163                #root::sql::ExecError,
5164            > {
5165                use #root::sql::FetcherPool as _;
5166                let _key = ::std::format!("{}__ilike", col);
5167                #root::query::QuerySet::<Self>::default()
5168                    .filter(
5169                        &_key,
5170                        #root::core::SqlValue::String(pattern.into()),
5171                    )
5172                    .fetch(pool)
5173                    .await
5174            }
5175
5176            /// Fetch every row where `<col>` starts with `prefix`
5177            /// (auto-appends `%`). Django `__startswith` / Eloquent
5178            /// `whereLike("col", "$prefix%")` parity.
5179            ///
5180            /// # Errors
5181            /// As [`FetcherPool::fetch`].
5182            ///
5183            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5184            pub async fn where_starts_with(
5185                col: &str,
5186                prefix: impl ::core::convert::Into<::std::string::String>,
5187                pool: &#root::sql::Pool,
5188            ) -> ::core::result::Result<
5189                ::std::vec::Vec<Self>,
5190                #root::sql::ExecError,
5191            > {
5192                use #root::sql::FetcherPool as _;
5193                let _key = ::std::format!("{}__startswith", col);
5194                #root::query::QuerySet::<Self>::default()
5195                    .filter(
5196                        &_key,
5197                        #root::core::SqlValue::String(prefix.into()),
5198                    )
5199                    .fetch(pool)
5200                    .await
5201            }
5202
5203            /// Fetch every row where `<col>` ends with `suffix`
5204            /// (auto-prepends `%`). Django `__endswith` / Eloquent
5205            /// `whereLike("col", "%$suffix")` parity.
5206            ///
5207            /// # Errors
5208            /// As [`FetcherPool::fetch`].
5209            ///
5210            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5211            pub async fn where_ends_with(
5212                col: &str,
5213                suffix: impl ::core::convert::Into<::std::string::String>,
5214                pool: &#root::sql::Pool,
5215            ) -> ::core::result::Result<
5216                ::std::vec::Vec<Self>,
5217                #root::sql::ExecError,
5218            > {
5219                use #root::sql::FetcherPool as _;
5220                let _key = ::std::format!("{}__endswith", col);
5221                #root::query::QuerySet::<Self>::default()
5222                    .filter(
5223                        &_key,
5224                        #root::core::SqlValue::String(suffix.into()),
5225                    )
5226                    .fetch(pool)
5227                    .await
5228            }
5229
5230            /// Fetch every row where `<col>` contains `substr`
5231            /// (auto-wraps with `%`). Django `__contains` /
5232            /// Eloquent `whereLike("col", "%$substr%")` parity.
5233            ///
5234            /// # Errors
5235            /// As [`FetcherPool::fetch`].
5236            ///
5237            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5238            pub async fn where_contains(
5239                col: &str,
5240                substr: impl ::core::convert::Into<::std::string::String>,
5241                pool: &#root::sql::Pool,
5242            ) -> ::core::result::Result<
5243                ::std::vec::Vec<Self>,
5244                #root::sql::ExecError,
5245            > {
5246                use #root::sql::FetcherPool as _;
5247                let _key = ::std::format!("{}__contains", col);
5248                #root::query::QuerySet::<Self>::default()
5249                    .filter(
5250                        &_key,
5251                        #root::core::SqlValue::String(substr.into()),
5252                    )
5253                    .fetch(pool)
5254                    .await
5255            }
5256
5257            /// Fetch every row where `<col> > val`. Eloquent
5258            /// `Model::where($col, ">", $val)->get()` parity.
5259            ///
5260            /// # Errors
5261            /// As [`FetcherPool::fetch`].
5262            ///
5263            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5264            pub async fn where_gt(
5265                col: &str,
5266                val: impl ::core::convert::Into<#root::core::SqlValue>,
5267                pool: &#root::sql::Pool,
5268            ) -> ::core::result::Result<
5269                ::std::vec::Vec<Self>,
5270                #root::sql::ExecError,
5271            > {
5272                use #root::sql::FetcherPool as _;
5273                let _key = ::std::format!("{}__gt", col);
5274                #root::query::QuerySet::<Self>::default()
5275                    .filter(&_key, val)
5276                    .fetch(pool)
5277                    .await
5278            }
5279
5280            /// Fetch every row where `<col> >= val`. Eloquent
5281            /// `Model::where($col, ">=", $val)->get()` parity.
5282            ///
5283            /// # Errors
5284            /// As [`FetcherPool::fetch`].
5285            ///
5286            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5287            pub async fn where_gte(
5288                col: &str,
5289                val: impl ::core::convert::Into<#root::core::SqlValue>,
5290                pool: &#root::sql::Pool,
5291            ) -> ::core::result::Result<
5292                ::std::vec::Vec<Self>,
5293                #root::sql::ExecError,
5294            > {
5295                use #root::sql::FetcherPool as _;
5296                let _key = ::std::format!("{}__gte", col);
5297                #root::query::QuerySet::<Self>::default()
5298                    .filter(&_key, val)
5299                    .fetch(pool)
5300                    .await
5301            }
5302
5303            /// Fetch every row where `<col> < val`. Eloquent
5304            /// `Model::where($col, "<", $val)->get()` parity.
5305            ///
5306            /// # Errors
5307            /// As [`FetcherPool::fetch`].
5308            ///
5309            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5310            pub async fn where_lt(
5311                col: &str,
5312                val: impl ::core::convert::Into<#root::core::SqlValue>,
5313                pool: &#root::sql::Pool,
5314            ) -> ::core::result::Result<
5315                ::std::vec::Vec<Self>,
5316                #root::sql::ExecError,
5317            > {
5318                use #root::sql::FetcherPool as _;
5319                let _key = ::std::format!("{}__lt", col);
5320                #root::query::QuerySet::<Self>::default()
5321                    .filter(&_key, val)
5322                    .fetch(pool)
5323                    .await
5324            }
5325
5326            /// Fetch every row where `<col> <= val`. Eloquent
5327            /// `Model::where($col, "<=", $val)->get()` parity.
5328            ///
5329            /// # Errors
5330            /// As [`FetcherPool::fetch`].
5331            ///
5332            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5333            pub async fn where_lte(
5334                col: &str,
5335                val: impl ::core::convert::Into<#root::core::SqlValue>,
5336                pool: &#root::sql::Pool,
5337            ) -> ::core::result::Result<
5338                ::std::vec::Vec<Self>,
5339                #root::sql::ExecError,
5340            > {
5341                use #root::sql::FetcherPool as _;
5342                let _key = ::std::format!("{}__lte", col);
5343                #root::query::QuerySet::<Self>::default()
5344                    .filter(&_key, val)
5345                    .fetch(pool)
5346                    .await
5347            }
5348
5349            /// Fetch every row where `<col> <> val`. Eloquent
5350            /// `Model::where($col, "!=", $val)->get()` parity.
5351            ///
5352            /// # Errors
5353            /// As [`FetcherPool::fetch`].
5354            ///
5355            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5356            pub async fn where_ne(
5357                col: &str,
5358                val: impl ::core::convert::Into<#root::core::SqlValue>,
5359                pool: &#root::sql::Pool,
5360            ) -> ::core::result::Result<
5361                ::std::vec::Vec<Self>,
5362                #root::sql::ExecError,
5363            > {
5364                use #root::sql::FetcherPool as _;
5365                let _key = ::std::format!("{}__ne", col);
5366                #root::query::QuerySet::<Self>::default()
5367                    .filter(&_key, val)
5368                    .fetch(pool)
5369                    .await
5370            }
5371
5372            /// Fetch every row matching `<col> = val` for ANY of the
5373            /// listed columns. Eloquent `Model::whereAny($cols, $val)`
5374            /// parity. Empty `cols` returns no rows.
5375            ///
5376            /// Resolves each `&str` column to its SCHEMA-registered
5377            /// `&'static str` once and builds a single OR-composed
5378            /// `Q` expression (`col1 = ? OR col2 = ? OR …`).
5379            ///
5380            /// # Errors
5381            /// As [`FetcherPool::fetch`]; `QueryError::UnknownField`
5382            /// when any column is not declared on the model.
5383            ///
5384            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5385            pub async fn where_any(
5386                cols: &[&str],
5387                val: impl ::core::convert::Into<#root::core::SqlValue>,
5388                pool: &#root::sql::Pool,
5389            ) -> ::core::result::Result<
5390                ::std::vec::Vec<Self>,
5391                #root::sql::ExecError,
5392            > {
5393                Self::__where_multi(cols, val, false, pool).await
5394            }
5395
5396            /// Fetch every row matching `<col> = val` for ALL listed
5397            /// columns. Eloquent `Model::whereAll($cols, $val)` parity.
5398            /// Empty `cols` returns every row (vacuous AND).
5399            ///
5400            /// # Errors
5401            /// As [`Self::where_any`].
5402            pub async fn where_all(
5403                cols: &[&str],
5404                val: impl ::core::convert::Into<#root::core::SqlValue>,
5405                pool: &#root::sql::Pool,
5406            ) -> ::core::result::Result<
5407                ::std::vec::Vec<Self>,
5408                #root::sql::ExecError,
5409            > {
5410                Self::__where_multi(cols, val, true, pool).await
5411            }
5412
5413            /// Internal: build a Q expression composing `cols` via
5414            /// AND (`all`) or OR (`!all`), then fetch. Backs
5415            /// `where_any` / `where_all`.
5416            #[doc(hidden)]
5417            pub async fn __where_multi(
5418                cols: &[&str],
5419                val: impl ::core::convert::Into<#root::core::SqlValue>,
5420                all: bool,
5421                pool: &#root::sql::Pool,
5422            ) -> ::core::result::Result<
5423                ::std::vec::Vec<Self>,
5424                #root::sql::ExecError,
5425            > {
5426                #root::sql::model_shortcuts::where_multi_pool::<Self>(cols, val, all, pool)
5427                    .await
5428            }
5429
5430            /// Fetch up to `n` rows. Eloquent `Model::take($n)->get()`
5431            /// parity / Django `Model.objects.all()[:n]`. PK-ordered
5432            /// is NOT guaranteed without an explicit `order_by` —
5433            /// drop into `Self::query()` for that.
5434            ///
5435            /// # Errors
5436            /// As [`FetcherPool::fetch`].
5437            ///
5438            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5439            pub async fn take(
5440                n: i64,
5441                pool: &#root::sql::Pool,
5442            ) -> ::core::result::Result<
5443                ::std::vec::Vec<Self>,
5444                #root::sql::ExecError,
5445            > {
5446                use #root::sql::FetcherPool as _;
5447                #root::query::QuerySet::<Self>::default()
5448                    .limit(n)
5449                    .fetch(pool)
5450                    .await
5451            }
5452
5453            /// Fetch the page-th window of `per_page` rows
5454            /// (1-indexed). Eloquent
5455            /// `Model::query()->forPage($page, $perPage)->get()`
5456            /// parity. The DB scans an `OFFSET (page - 1) * per_page
5457            /// LIMIT per_page`; for large offsets this is O(N) —
5458            /// prefer keyset pagination via PK on hot paths.
5459            ///
5460            /// # Errors
5461            /// As [`FetcherPool::fetch`].
5462            ///
5463            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5464            pub async fn for_page(
5465                page: i64,
5466                per_page: i64,
5467                pool: &#root::sql::Pool,
5468            ) -> ::core::result::Result<
5469                ::std::vec::Vec<Self>,
5470                #root::sql::ExecError,
5471            > {
5472                use #root::sql::FetcherPool as _;
5473                let _offset = if page > 1 { (page - 1) * per_page } else { 0 };
5474                #root::query::QuerySet::<Self>::default()
5475                    .limit(per_page)
5476                    .offset(_offset)
5477                    .fetch(pool)
5478                    .await
5479            }
5480
5481            /// Eloquent `Model::paginate($per_page, $page)` — fetch
5482            /// one page of rows AND the total row count in one
5483            /// call. Returns `(rows, total)`. Two queries: a
5484            /// LIMIT/OFFSET SELECT for the page + a `SELECT COUNT(*)`
5485            /// for the total.
5486            ///
5487            /// Useful for paginated UIs that need both the visible
5488            /// rows AND a "Page X of Y" / total-count widget. For
5489            /// large tables prefer keyset pagination + cached count;
5490            /// every call to `paginate` re-counts the full table.
5491            ///
5492            /// Same 1-indexed `page` convention as [`Self::for_page`].
5493            ///
5494            /// # Errors
5495            /// As [`Self::for_page`] and [`Self::count`].
5496            pub async fn paginate(
5497                page: i64,
5498                per_page: i64,
5499                pool: &#root::sql::Pool,
5500            ) -> ::core::result::Result<
5501                (::std::vec::Vec<Self>, i64),
5502                #root::sql::ExecError,
5503            > {
5504                // Route through the queryset rather than `Self::count`:
5505                // the `count` inherent method is suppressed on models
5506                // that declare a field named `count` (field/shortcut
5507                // collision guard), and `paginate` must still compile
5508                // for those models.
5509                let total = {
5510                    use #root::sql::CounterPool as _;
5511                    #root::query::QuerySet::<Self>::default()
5512                        .count(pool)
5513                        .await?
5514                };
5515                let rows = Self::for_page(page, per_page, pool).await?;
5516                ::core::result::Result::Ok((rows, total))
5517            }
5518
5519            /// Bulk-update — set `set_col = set_val` on every row
5520            /// matching `where_col = where_val`. Returns affected row
5521            /// count. Eloquent
5522            /// `Model::where($where_col, $where_val)->update([$set_col => $set_val])`
5523            /// parity.
5524            ///
5525            /// For multi-column updates drop into the queryset
5526            /// builder: `Self::query().filter(...).update().set(...).set(...).execute_pool(&pool)`.
5527            ///
5528            /// # Errors
5529            /// As [`UpdaterPool::execute_pool`].
5530            ///
5531            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5532            pub async fn update_where(
5533                where_col: &str,
5534                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5535                set_col: &str,
5536                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5537                pool: &#root::sql::Pool,
5538            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5539                use #root::sql::UpdaterPool as _;
5540                #root::query::QuerySet::<Self>::default()
5541                    .filter(where_col, where_val)
5542                    .update()
5543                    .set(set_col, set_val)
5544                    .execute_pool(pool)
5545                    .await
5546            }
5547
5548            /// Bulk-delete — remove every row matching
5549            /// `where_col = where_val`. Returns affected row count.
5550            /// Eloquent
5551            /// `Model::where($where_col, $where_val)->delete()` parity.
5552            ///
5553            /// For more complex filters drop into the queryset
5554            /// builder + `Self::query().filter(...).delete().execute_pool(&pool)`.
5555            ///
5556            /// # Errors
5557            /// As [`#root::sql::delete_pool`].
5558            pub async fn delete_where(
5559                where_col: &str,
5560                where_val: impl ::core::convert::Into<#root::core::SqlValue>,
5561                pool: &#root::sql::Pool,
5562            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5563                let _query = #root::core::DeleteQuery {
5564                    model: <Self as #root::core::Model>::SCHEMA,
5565                    where_clause: #root::core::WhereExpr::Predicate(
5566                        #root::core::Filter {
5567                            column: <Self as #root::core::Model>::SCHEMA
5568                                .field(where_col)
5569                                .ok_or_else(|| {
5570                                    #root::sql::ExecError::Query(
5571                                        #root::core::QueryError::UnknownField {
5572                                            model: <Self as #root::core::Model>::SCHEMA.name,
5573                                            field: ::std::string::ToString::to_string(where_col),
5574                                        },
5575                                    )
5576                                })?
5577                                .column,
5578                            op: #root::core::Op::Eq,
5579                            value: ::core::convert::Into::into(where_val),
5580                        },
5581                    ),
5582                };
5583                #root::sql::delete_pool(pool, &_query).await
5584            }
5585
5586            /// Bulk-update — set `set_col = set_val` on EVERY row of
5587            /// this model's table. **No WHERE clause** — use with
5588            /// care. Eloquent `Model::query()->update([$col => $val])`
5589            /// parity.
5590            ///
5591            /// Use for backfills, one-shot reset flows, etc. The
5592            /// returned count is rows affected.
5593            ///
5594            /// # Errors
5595            /// As [`UpdaterPool::execute_pool`].
5596            ///
5597            /// [`UpdaterPool::execute_pool`]: rustango::sql::UpdaterPool::execute_pool
5598            pub async fn update_all(
5599                set_col: &str,
5600                set_val: impl ::core::convert::Into<#root::core::SqlValue>,
5601                pool: &#root::sql::Pool,
5602            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
5603                use #root::sql::UpdaterPool as _;
5604                #root::query::QuerySet::<Self>::default()
5605                    .update()
5606                    .set(set_col, set_val)
5607                    .execute_pool(pool)
5608                    .await
5609            }
5610
5611            /// Fetch every row where `<col> NOT LIKE <pattern>`.
5612            /// Eloquent `Model::whereNotLike` parity. Pattern is
5613            /// passed verbatim — caller controls `%` / `_`.
5614            ///
5615            /// # Errors
5616            /// As [`FetcherPool::fetch`].
5617            ///
5618            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5619            pub async fn where_not_like(
5620                col: &str,
5621                pattern: impl ::core::convert::Into<::std::string::String>,
5622                pool: &#root::sql::Pool,
5623            ) -> ::core::result::Result<
5624                ::std::vec::Vec<Self>,
5625                #root::sql::ExecError,
5626            > {
5627                use #root::sql::FetcherPool as _;
5628                let _key = ::std::format!("{}__not_like", col);
5629                #root::query::QuerySet::<Self>::default()
5630                    .filter(
5631                        &_key,
5632                        #root::core::SqlValue::String(pattern.into()),
5633                    )
5634                    .fetch(pool)
5635                    .await
5636            }
5637
5638            /// Fetch every row where `<col> NOT ILIKE <pattern>` —
5639            /// case-insensitive `NOT LIKE` (PG native, MySQL /
5640            /// SQLite emulated via `LOWER(col) NOT LIKE LOWER(pattern)`).
5641            ///
5642            /// # Errors
5643            /// As [`FetcherPool::fetch`].
5644            ///
5645            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5646            pub async fn where_not_ilike(
5647                col: &str,
5648                pattern: impl ::core::convert::Into<::std::string::String>,
5649                pool: &#root::sql::Pool,
5650            ) -> ::core::result::Result<
5651                ::std::vec::Vec<Self>,
5652                #root::sql::ExecError,
5653            > {
5654                use #root::sql::FetcherPool as _;
5655                let _key = ::std::format!("{}__not_ilike", col);
5656                #root::query::QuerySet::<Self>::default()
5657                    .filter(
5658                        &_key,
5659                        #root::core::SqlValue::String(pattern.into()),
5660                    )
5661                    .fetch(pool)
5662                    .await
5663            }
5664
5665            /// Fetch every row where `<col> NOT BETWEEN lo AND hi`.
5666            /// Eloquent `Model::whereNotBetween($col, [$lo, $hi])`
5667            /// parity.
5668            ///
5669            /// # Errors
5670            /// As [`FetcherPool::fetch`].
5671            ///
5672            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5673            pub async fn where_not_between(
5674                col: &str,
5675                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5676                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5677                pool: &#root::sql::Pool,
5678            ) -> ::core::result::Result<
5679                ::std::vec::Vec<Self>,
5680                #root::sql::ExecError,
5681            > {
5682                use #root::sql::FetcherPool as _;
5683                let _key = ::std::format!("{}__not_between", col);
5684                let _vals = #root::core::SqlValue::List(::std::vec![
5685                    ::core::convert::Into::into(lo),
5686                    ::core::convert::Into::into(hi),
5687                ]);
5688                #root::query::QuerySet::<Self>::default()
5689                    .filter(&_key, _vals)
5690                    .fetch(pool)
5691                    .await
5692            }
5693
5694            /// Returns the SQL table name for this model. Eloquent
5695            /// `$model->getTable()` parity.
5696            #[must_use]
5697            pub fn table_name() -> &'static str {
5698                <Self as #root::core::Model>::SCHEMA.table
5699            }
5700
5701            /// Returns the SQL column name of this model's primary
5702            /// key, or `None` when the model has no
5703            /// `#[rustango(primary_key)]`. Eloquent
5704            /// `$model->getKeyName()` parity.
5705            #[must_use]
5706            pub fn primary_key_column() -> ::core::option::Option<&'static str> {
5707                <Self as #root::core::Model>::SCHEMA
5708                    .primary_key()
5709                    .map(|f| f.column)
5710            }
5711
5712            /// Fetch every row where `<col> BETWEEN lo AND hi`
5713            /// (inclusive on both ends — same as SQL). Eloquent
5714            /// `Model::whereBetween($col, [$lo, $hi])->get()` parity.
5715            ///
5716            /// # Errors
5717            /// As [`FetcherPool::fetch`].
5718            ///
5719            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5720            pub async fn where_between(
5721                col: &str,
5722                lo: impl ::core::convert::Into<#root::core::SqlValue>,
5723                hi: impl ::core::convert::Into<#root::core::SqlValue>,
5724                pool: &#root::sql::Pool,
5725            ) -> ::core::result::Result<
5726                ::std::vec::Vec<Self>,
5727                #root::sql::ExecError,
5728            > {
5729                use #root::sql::FetcherPool as _;
5730                let _key = ::std::format!("{}__between", col);
5731                let _vals = #root::core::SqlValue::List(::std::vec![
5732                    ::core::convert::Into::into(lo),
5733                    ::core::convert::Into::into(hi),
5734                ]);
5735                #root::query::QuerySet::<Self>::default()
5736                    .filter(&_key, _vals)
5737                    .fetch(pool)
5738                    .await
5739            }
5740
5741            /// Fetch the first row where `<col> = <val>`. Returns
5742            /// `Ok(None)` when no row matches. Eloquent
5743            /// `Model::firstWhere($col, $val)` / Django
5744            /// `Model.objects.filter(col=val).first()` parity.
5745            ///
5746            /// Thin wrapper over `QuerySet::<Self>::default()
5747            /// .filter(col, val).first(pool)`. Use this when you
5748            /// want one row identified by a non-PK column (e.g.
5749            /// `User::first_where_pool("email", "x@y.com", &pool)`).
5750            ///
5751            /// `val` accepts any value `Into<SqlValue>` so plain
5752            /// strings, ints, UUIDs, etc. all work.
5753            ///
5754            /// # Errors
5755            /// As `QuerySet::first`.
5756            pub async fn first_where(
5757                col: &str,
5758                val: impl ::core::convert::Into<#root::core::SqlValue>,
5759                pool: &#root::sql::Pool,
5760            ) -> ::core::result::Result<
5761                ::core::option::Option<Self>,
5762                #root::sql::ExecError,
5763            > {
5764                #root::query::QuerySet::<Self>::default()
5765                    .filter(col, val)
5766                    .first(pool)
5767                    .await
5768            }
5769
5770            #value_method
5771
5772            /// Fetch the row with the largest `field` value —
5773            /// `SELECT … ORDER BY <field> DESC LIMIT 1`. Returns
5774            /// `Ok(None)` for an empty table. Eloquent
5775            /// `Model::latest($field)->first()` / Django
5776            /// `Model.objects.latest(field)` (non-throwing) parity.
5777            /// Thin wrapper over `QuerySet::<Self>::default()
5778            /// .latest(field, pool)`.
5779            ///
5780            /// **Field name** is the Rust field ident as a string
5781            /// (not the SQL column). Unknown fields surface as
5782            /// `ExecError::Query(QueryError::UnknownField)` at
5783            /// compile time.
5784            ///
5785            /// # Errors
5786            /// As `QuerySet::latest`.
5787            pub async fn latest(
5788                field: &str,
5789                pool: &#root::sql::Pool,
5790            ) -> ::core::result::Result<
5791                ::core::option::Option<Self>,
5792                #root::sql::ExecError,
5793            > {
5794                #root::query::QuerySet::<Self>::default()
5795                    .latest(field, pool)
5796                    .await
5797            }
5798
5799            /// Sibling of [`Self::latest_pool`] — fetches the row
5800            /// with the smallest `field` value (`ORDER BY <field>
5801            /// ASC LIMIT 1`). Eloquent `Model::oldest($field)
5802            /// ->first()` / Django `Model.objects.earliest(field)`
5803            /// parity.
5804            ///
5805            /// # Errors
5806            /// As [`Self::latest_pool`].
5807            pub async fn earliest(
5808                field: &str,
5809                pool: &#root::sql::Pool,
5810            ) -> ::core::result::Result<
5811                ::core::option::Option<Self>,
5812                #root::sql::ExecError,
5813            > {
5814                #root::query::QuerySet::<Self>::default()
5815                    .earliest(field, pool)
5816                    .await
5817            }
5818
5819            #count_method
5820
5821            /// `true` when the table contains at least one row.
5822            /// Eloquent `Model::query()->exists()` / Django
5823            /// `Model.objects.exists()` parity. Thin wrapper over
5824            /// `QuerySet::<Self>::default().exists(pool)`.
5825            ///
5826            /// # Errors
5827            /// As [`ExistsPool::exists`].
5828            ///
5829            /// [`ExistsPool::exists`]: rustango::sql::ExistsPool::exists
5830            pub async fn exists(
5831                pool: &#root::sql::Pool,
5832            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5833                use #root::sql::ExistsPool as _;
5834                #root::query::QuerySet::<Self>::default()
5835                    .exists(pool)
5836                    .await
5837            }
5838
5839            /// Inverse of [`Self::exists`] — returns `true` when
5840            /// the table has zero rows. Eloquent
5841            /// `Model::doesntExist()` parity.
5842            ///
5843            /// # Errors
5844            /// As [`Self::exists`].
5845            pub async fn doesnt_exist(
5846                pool: &#root::sql::Pool,
5847            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5848                Self::exists(pool).await.map(|e| !e)
5849            }
5850
5851            /// Eloquent `Model::query()->whereKey($pk)->exists()` —
5852            /// `true` when a row with primary key `pk` exists in
5853            /// the table. Sugar over
5854            /// `QuerySet::<Self>::default().contains_pk(pool, pk)`.
5855            ///
5856            /// Differs from [`Self::exists`] (which checks "any row
5857            /// in the table") by checking a specific PK existence.
5858            /// Cheaper than `Self::find(pk, &pool).await?.is_some()`
5859            /// because the row is never materialized — the SQL is
5860            /// `SELECT COUNT(*) > 0 FROM <table> WHERE pk = ?`.
5861            ///
5862            /// # Errors
5863            /// As [`#root::sql::ExistsPool::contains_pk`].
5864            pub async fn contains_pk(
5865                pk: impl ::core::convert::Into<#root::core::SqlValue> + ::core::marker::Send,
5866                pool: &#root::sql::Pool,
5867            ) -> ::core::result::Result<bool, #root::sql::ExecError> {
5868                use #root::sql::ExistsPool as _;
5869                #root::query::QuerySet::<Self>::default()
5870                    .contains_pk(pool, pk)
5871                    .await
5872            }
5873
5874            #sum_method
5875            #avg_method
5876            #min_method
5877            #max_method
5878
5879            /// Internal: forward to
5880            /// [`#root::sql::model_shortcuts::aggregate_one_pool`].
5881            /// Backs `sum` / `avg` / `min` / `max`.
5882            #[doc(hidden)]
5883            pub async fn __aggregate_one_pool<U>(
5884                col: &str,
5885                build: fn(&'static str) -> #root::core::AggregateExpr,
5886                pool: &#root::sql::Pool,
5887            ) -> ::core::result::Result<
5888                ::core::option::Option<U>,
5889                #root::sql::ExecError,
5890            >
5891            where
5892                (::core::option::Option<U>,): #root::sql::MaybePgFromRow
5893                    + #root::sql::MaybeMyFromRow
5894                    + #root::sql::MaybeSqliteFromRow
5895                    + ::core::marker::Send
5896                    + ::core::marker::Unpin,
5897            {
5898                #root::sql::model_shortcuts::aggregate_one_pool::<Self, U>(col, build, pool)
5899                    .await
5900            }
5901
5902            /// Fetch every row of this model from `pool`. Eloquent
5903            /// `Model::all()` parity — a thin wrapper over
5904            /// `QuerySet::<Self>::default().fetch(pool)`.
5905            ///
5906            /// **Use with care on large tables** — there's no
5907            /// pagination or limit; the entire table is materialized
5908            /// into memory. For anything beyond fixture / lookup
5909            /// tables, page through `QuerySet::<Self>::default()
5910            /// .order_by(...).limit(N).offset(M).fetch(pool)`
5911            /// or stream via `.iterator(chunk_size)`.
5912            ///
5913            /// # Errors
5914            /// As [`FetcherPool::fetch`].
5915            ///
5916            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5917            pub async fn all(
5918                pool: &#root::sql::Pool,
5919            ) -> ::core::result::Result<::std::vec::Vec<Self>, #root::sql::ExecError>
5920            {
5921                use #root::sql::FetcherPool as _;
5922                #root::query::QuerySet::<Self>::default()
5923                    .fetch(pool)
5924                    .await
5925            }
5926
5927            /// Look up every row whose primary key is in `pks`.
5928            /// Returns the matching rows in **inventory** order — NOT
5929            /// the order of `pks`. Empty `pks` returns an empty
5930            /// `Vec`. Eloquent `Model::find([1, 2, 3])` (when called
5931            /// with a list) / Django `Model.objects.filter(pk__in=[...])`
5932            /// parity.
5933            ///
5934            /// Thin wrapper over `QuerySet::<Self>::default()
5935            /// .filter("<pk>__in", SqlValue::List([...])).fetch(pool)`.
5936            /// Caller-supplied PKs that don't match a row are
5937            /// silently skipped (the returned vec is shorter than
5938            /// the input list). For an order-preserving / "fail
5939            /// when any missing" variant, build the queryset
5940            /// explicitly with `in_bulk` instead.
5941            ///
5942            /// Accepts any iterable whose elements are
5943            /// `Into<SqlValue>` — `Vec<i64>`, `&[i64]`, `[i64; N]`,
5944            /// `Vec<Uuid>`, etc.
5945            ///
5946            /// # Errors
5947            /// As [`FetcherPool::fetch`].
5948            ///
5949            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5950            pub async fn find_many<V>(
5951                pks: impl ::core::iter::IntoIterator<Item = V>,
5952                pool: &#root::sql::Pool,
5953            ) -> ::core::result::Result<
5954                ::std::vec::Vec<Self>,
5955                #root::sql::ExecError,
5956            >
5957            where
5958                V: ::core::convert::Into<#root::core::SqlValue>,
5959            {
5960                use #root::sql::FetcherPool as _;
5961                let _values: ::std::vec::Vec<#root::core::SqlValue> =
5962                    pks.into_iter().map(::core::convert::Into::into).collect();
5963                if _values.is_empty() {
5964                    return ::core::result::Result::Ok(::std::vec::Vec::new());
5965                }
5966                let _key = ::std::format!("{}__in", ::core::stringify!(#pk_ident));
5967                #root::query::QuerySet::<Self>::default()
5968                    .filter(&_key, #root::core::SqlValue::List(_values))
5969                    .fetch(pool)
5970                    .await
5971            }
5972
5973            /// Look up the row whose primary key equals `pk`. Returns
5974            /// `Ok(None)` when no row matches; this is the
5975            /// non-throwing counterpart of Django's `.get(pk=…)`
5976            /// (which raises `DoesNotExist`). Eloquent `Model::find`
5977            /// shape — accepts any value `Into<SqlValue>`.
5978            ///
5979            /// One-liner shortcut for the common
5980            /// `QuerySet::<Self>::default().filter("<pk_field>", pk)
5981            /// .limit(1).fetch(pool).await?.into_iter().next()`
5982            /// dance.
5983            ///
5984            /// # Errors
5985            /// As [`FetcherPool::fetch`].
5986            ///
5987            /// [`FetcherPool::fetch`]: rustango::sql::FetcherPool::fetch
5988            pub async fn find(
5989                pk: impl ::core::convert::Into<#root::core::SqlValue>,
5990                pool: &#root::sql::Pool,
5991            ) -> ::core::result::Result<::core::option::Option<Self>, #root::sql::ExecError>
5992            {
5993                use #root::sql::FetcherPool as _;
5994                let _pk_val: #root::core::SqlValue = pk.into();
5995                let mut _rows: ::std::vec::Vec<Self> =
5996                    #root::query::QuerySet::<Self>::default()
5997                        .filter(::core::stringify!(#pk_ident), _pk_val)
5998                        .limit(1)
5999                        .fetch(pool)
6000                        .await?;
6001                ::core::result::Result::Ok(_rows.into_iter().next())
6002            }
6003
6004            /// Look up the row whose primary key equals `pk`. Errors
6005            /// when no row matches — the throwing counterpart of
6006            /// [`Self::find_pool`]. Eloquent `Model::findOrFail` /
6007            /// Django `Model.objects.get(pk=…)` (which raises
6008            /// `DoesNotExist`) parity.
6009            ///
6010            /// Translates the miss into
6011            /// [`ExecError::Driver`]\([`sqlx::Error::RowNotFound`])\)
6012            /// so callers can `?`-bubble straight through the typical
6013            /// `ExecError` error chain.
6014            ///
6015            /// # Errors
6016            /// As [`Self::find_pool`]; additionally
6017            /// [`sqlx::Error::RowNotFound`] when no row matches.
6018            ///
6019            /// [`ExecError::Driver`]: rustango::sql::ExecError::Driver
6020            /// [`sqlx::Error::RowNotFound`]: rustango::sql::sqlx::Error::RowNotFound
6021            pub async fn find_or_fail(
6022                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6023                pool: &#root::sql::Pool,
6024            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6025                match Self::find(pk, pool).await? {
6026                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok(_row),
6027                    ::core::option::Option::None => ::core::result::Result::Err(
6028                        #root::sql::ExecError::Driver(
6029                            #root::sql::sqlx::Error::RowNotFound,
6030                        ),
6031                    ),
6032                }
6033            }
6034
6035            /// Look up multiple rows by primary key, **failing** if
6036            /// any requested PK is missing. Eloquent
6037            /// `Model::findOrFail([1, 2, 3])` parity. Returns rows in
6038            /// inventory order (NOT request order — same as
6039            /// [`Self::find_many`]).
6040            ///
6041            /// Empty `pks` returns an empty `Vec` (no rows requested
6042            /// → nothing to fail on).
6043            ///
6044            /// Differs from [`Self::find_many`] in that this method
6045            /// requires every PK to resolve. If even one is missing,
6046            /// the call surfaces `sqlx::Error::RowNotFound` (wrapped
6047            /// in `ExecError::Driver`). Rows are deduped at the SQL
6048            /// layer, so passing the same PK twice in `pks` counts as
6049            /// one expected row.
6050            ///
6051            /// # Errors
6052            /// As [`Self::find_many`], plus `RowNotFound` when the
6053            /// returned row count is less than the count of distinct
6054            /// requested PKs.
6055            pub async fn find_many_or_fail<V>(
6056                pks: impl ::core::iter::IntoIterator<Item = V>,
6057                pool: &#root::sql::Pool,
6058            ) -> ::core::result::Result<
6059                ::std::vec::Vec<Self>,
6060                #root::sql::ExecError,
6061            >
6062            where
6063                V: ::core::convert::Into<#root::core::SqlValue>,
6064            {
6065                let _values: ::std::vec::Vec<#root::core::SqlValue> =
6066                    pks.into_iter().map(::core::convert::Into::into).collect();
6067                if _values.is_empty() {
6068                    return ::core::result::Result::Ok(::std::vec::Vec::new());
6069                }
6070                // Dedup the requested PK list before counting so
6071                // duplicate-PK requests don't false-fail (the SQL
6072                // `IN (...)` clause naturally dedups too).
6073                let mut _seen: ::std::collections::HashSet<
6074                    ::std::string::String,
6075                > = ::std::collections::HashSet::new();
6076                for v in &_values {
6077                    _seen.insert(v.to_display_string());
6078                }
6079                let _expected = _seen.len();
6080                let _rows = Self::find_many(_values, pool).await?;
6081                if _rows.len() < _expected {
6082                    return ::core::result::Result::Err(
6083                        #root::sql::ExecError::Driver(
6084                            #root::sql::sqlx::Error::RowNotFound,
6085                        ),
6086                    );
6087                }
6088                ::core::result::Result::Ok(_rows)
6089            }
6090
6091            /// Find by primary key or run `fallback` to produce a
6092            /// default row to return. Eloquent
6093            /// `Model::findOr($pk, fn() => …)` parity.
6094            ///
6095            /// Unlike [`Self::find_or_fail_pool`] (which raises on
6096            /// miss), this is the "give me something sensible"
6097            /// branch: typical use is "fetch the user's row, else
6098            /// fall back to an anonymous/guest stub".
6099            ///
6100            /// `fallback` runs only when no row matches — the DB
6101            /// round-trip happens unconditionally.
6102            ///
6103            /// # Errors
6104            /// As [`Self::find_pool`].
6105            pub async fn find_or<F>(
6106                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6107                pool: &#root::sql::Pool,
6108                fallback: F,
6109            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6110            where
6111                F: ::core::ops::FnOnce() -> Self,
6112            {
6113                ::core::result::Result::Ok(
6114                    Self::find(pk, pool).await?.unwrap_or_else(fallback),
6115                )
6116            }
6117
6118            /// Same as [`Self::find_or`] but also returns a `bool`
6119            /// indicating whether the row was found in the DB
6120            /// (`true`) or freshly built from `fallback` (`false`).
6121            /// Eloquent `Model::findOrNew($pk, [attrs])` parity —
6122            /// in PHP the returned model also exposes `->exists`;
6123            /// here that's surfaced as the second tuple element.
6124            ///
6125            /// Useful for edit-or-create form handlers where the
6126            /// caller needs to know whether to PATCH or POST when
6127            /// the user submits the form.
6128            ///
6129            /// # Errors
6130            /// As [`Self::find`].
6131            pub async fn find_or_new<F>(
6132                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6133                pool: &#root::sql::Pool,
6134                fallback: F,
6135            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6136            where
6137                F: ::core::ops::FnOnce() -> Self,
6138            {
6139                match Self::find(pk, pool).await? {
6140                    ::core::option::Option::Some(_row) => ::core::result::Result::Ok((_row, true)),
6141                    ::core::option::Option::None => {
6142                        ::core::result::Result::Ok((fallback(), false))
6143                    }
6144                }
6145            }
6146
6147            /// Eloquent `Model::findOrCreate(pk, defaults)` — like
6148            /// [`Self::find_or_new`] but **persists** the new row
6149            /// when the PK isn't found. Returns `(row, exists: bool)`
6150            /// — `exists=true` when the PK was found,
6151            /// `false` when a fresh row was inserted.
6152            ///
6153            /// ```ignore
6154            /// let (post, found) = Post::find_or_insert(
6155            ///     pk,
6156            ///     &pool,
6157            ///     || Post { id: Auto::default(), title: "new".into() },
6158            /// ).await?;
6159            /// // `found` true → returned existing row;
6160            /// // `found` false → fallback was inserted, `post.id` populated.
6161            /// ```
6162            ///
6163            /// **Caveat**: two concurrent calls that both miss the
6164            /// find can both INSERT, violating uniqueness. Wrap in a
6165            /// transaction or rely on a UNIQUE constraint + handle
6166            /// the conflict error if you need race-free semantics.
6167            ///
6168            /// # Errors
6169            /// As [`Self::find`] and [`Self::save_pool`].
6170            pub async fn find_or_insert<F>(
6171                pk: impl ::core::convert::Into<#root::core::SqlValue>,
6172                pool: &#root::sql::Pool,
6173                fallback: F,
6174            ) -> ::core::result::Result<(Self, bool), #root::sql::ExecError>
6175            where
6176                F: ::core::ops::FnOnce() -> Self,
6177            {
6178                if let ::core::option::Option::Some(_row) = Self::find(pk, pool).await? {
6179                    return ::core::result::Result::Ok((_row, true));
6180                }
6181                let mut _new = fallback();
6182                _new.save_pool(pool).await?;
6183                ::core::result::Result::Ok((_new, false))
6184            }
6185
6186            /// Fetch the first row of the table, or run `fallback`
6187            /// when the table is empty. Eloquent
6188            /// `Model::firstOr(fn() => …)` parity.
6189            ///
6190            /// # Errors
6191            /// As [`Self::first_pool`].
6192            pub async fn first_or<F>(
6193                pool: &#root::sql::Pool,
6194                fallback: F,
6195            ) -> ::core::result::Result<Self, #root::sql::ExecError>
6196            where
6197                F: ::core::ops::FnOnce() -> Self,
6198            {
6199                // Route through the queryset rather than `Self::first`,
6200                // which is suppressed on models with a field named
6201                // `first` (field/shortcut collision guard).
6202                ::core::result::Result::Ok(
6203                    #root::query::QuerySet::<Self>::default()
6204                        .first(pool)
6205                        .await?
6206                        .unwrap_or_else(fallback),
6207                )
6208            }
6209
6210            /// Fetch exactly one row matching `<col> = <val>`. Errors
6211            /// when zero rows match (`ExecError::Driver(RowNotFound)`)
6212            /// or more than one matches
6213            /// (`ExecError::Query(QueryError::Sql(MultipleRowsReturned))`).
6214            /// Eloquent `Model::sole($col, $val)` parity.
6215            ///
6216            /// # Errors
6217            /// As [`Self::where_pool`] plus the explicit
6218            /// `RowNotFound` / `MultipleRowsReturned` cases above.
6219            pub async fn sole(
6220                col: &str,
6221                val: impl ::core::convert::Into<#root::core::SqlValue>,
6222                pool: &#root::sql::Pool,
6223            ) -> ::core::result::Result<Self, #root::sql::ExecError> {
6224                let mut _rows = Self::where_(col, val, pool).await?;
6225                match _rows.len() {
6226                    0 => ::core::result::Result::Err(
6227                        #root::sql::ExecError::Driver(
6228                            #root::sql::sqlx::Error::RowNotFound,
6229                        ),
6230                    ),
6231                    1 => ::core::result::Result::Ok(_rows.remove(0)),
6232                    n => ::core::result::Result::Err(
6233                        #root::sql::ExecError::MultipleRowsReturned {
6234                            op: "sole",
6235                            table: <Self as #root::core::Model>::SCHEMA.name,
6236                            count: n,
6237                        },
6238                    ),
6239                }
6240            }
6241        }
6242    } else {
6243        quote!()
6244    };
6245
6246    // `_tx` family — `insert_tx`, `save_tx`, `delete_tx`. These mirror
6247    // the non-audited `_pool` methods but execute against an open
6248    // `PoolTx` so the writes participate in the caller's transaction.
6249    // Auditing inside TX is deferred; these always use the plain
6250    // executor primitives regardless of whether the model is audited.
6251    let tx_insert_method = if fields.has_auto {
6252        let pushes = &fields.insert_pushes;
6253        let returning_cols = &fields.returning_cols;
6254        quote! {
6255            /// Insert this row inside an open transaction, populating
6256            /// any `Auto<T>` PK from the auto-assigned value. Works
6257            /// against any backend that `tx` wraps.
6258            ///
6259            /// # Errors
6260            /// As [`Self::insert_pool`].
6261            pub async fn insert_tx(
6262                &mut self,
6263                tx: &mut #root::sql::PoolTx<'_>,
6264            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6265                let mut _columns: ::std::vec::Vec<&'static str> =
6266                    ::std::vec::Vec::new();
6267                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6268                    ::std::vec::Vec::new();
6269                #( #pushes )*
6270                let _query = #root::core::InsertQuery {
6271                    model: <Self as #root::core::Model>::SCHEMA,
6272                    columns: _columns,
6273                    values: _values,
6274                    returning: ::std::vec![ #( #returning_cols ),* ],
6275                    on_conflict: ::core::option::Option::None,
6276                };
6277                let _result = #root::sql::insert_returning_tx(tx, &_query).await?;
6278                #root::sql::apply_auto_pk(_result, self)
6279            }
6280        }
6281    } else {
6282        let insert_columns = &fields.insert_columns;
6283        let insert_values = &fields.insert_values;
6284        quote! {
6285            /// Insert this row inside an open transaction.
6286            ///
6287            /// # Errors
6288            /// As [`Self::insert_pool`].
6289            pub async fn insert_tx(
6290                &self,
6291                tx: &mut #root::sql::PoolTx<'_>,
6292            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6293                let _query = #root::core::InsertQuery {
6294                    model: <Self as #root::core::Model>::SCHEMA,
6295                    columns: ::std::vec![ #( #insert_columns ),* ],
6296                    values: ::std::vec![ #( #insert_values ),* ],
6297                    returning: ::std::vec::Vec::new(),
6298                    on_conflict: ::core::option::Option::None,
6299                };
6300                #root::sql::insert_tx(tx, &_query).await
6301            }
6302        }
6303    };
6304
6305    let tx_save_method = if let Some((pk_ident, pk_col)) = primary_key {
6306        let pk_column_lit = pk_col.as_str();
6307        let assignments = &fields.update_assignments;
6308        let dispatch_unset = if fields.pk_is_auto {
6309            quote! {
6310                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6311                    return self.insert_tx(tx).await.map(|()| 1u64);
6312                }
6313            }
6314        } else {
6315            quote!()
6316        };
6317        quote! {
6318            /// Save this row inside an open transaction. `INSERT` when
6319            /// the `Auto<T>` PK is `Unset`, else `UPDATE` keyed on the
6320            /// PK. Works against any backend that `tx` wraps.
6321            ///
6322            /// # Errors
6323            /// As [`Self::save_pool`].
6324            pub async fn save_tx(
6325                &mut self,
6326                tx: &mut #root::sql::PoolTx<'_>,
6327            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6328                #dispatch_unset
6329                let _query = #root::core::UpdateQuery {
6330                    model: <Self as #root::core::Model>::SCHEMA,
6331                    set: ::std::vec![ #( #assignments ),* ],
6332                    where_clause: #root::core::WhereExpr::Predicate(
6333                        #root::core::Filter {
6334                            column: #pk_column_lit,
6335                            op: #root::core::Op::Eq,
6336                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6337                                ::core::clone::Clone::clone(&self.#pk_ident)
6338                            ),
6339                        }
6340                    ),
6341                };
6342                let _affected = #root::sql::update_tx(tx, &_query).await?;
6343                ::core::result::Result::Ok(_affected)
6344            }
6345        }
6346    } else {
6347        quote!()
6348    };
6349
6350    let tx_delete_method = {
6351        let pk_column_lit = primary_key.map(|(_, col)| col.as_str()).unwrap_or("id");
6352        let pk_ident_for_tx = primary_key.map(|(ident, _)| ident);
6353        if let Some(pk_ident) = pk_ident_for_tx {
6354            quote! {
6355                /// Delete the row identified by this instance's PK
6356                /// inside an open transaction. Works against any backend
6357                /// that `tx` wraps.
6358                ///
6359                /// # Errors
6360                /// As [`Self::delete_pool`].
6361                pub async fn delete_tx(
6362                    &self,
6363                    tx: &mut #root::sql::PoolTx<'_>,
6364                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6365                    let _query = #root::core::DeleteQuery {
6366                        model: <Self as #root::core::Model>::SCHEMA,
6367                        where_clause: #root::core::WhereExpr::Predicate(
6368                            #root::core::Filter {
6369                                column: #pk_column_lit,
6370                                op: #root::core::Op::Eq,
6371                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6372                                    ::core::clone::Clone::clone(&self.#pk_ident)
6373                                ),
6374                            }
6375                        ),
6376                    };
6377                    #root::sql::delete_tx(tx, &_query).await
6378                }
6379            }
6380        } else {
6381            quote!()
6382        }
6383    };
6384
6385    // Update emission captures both BEFORE and AFTER state — runs an
6386    // extra SELECT against `_executor` BEFORE the UPDATE, captures
6387    // each tracked field's prior value, then after the UPDATE diffs
6388    // against the in-memory `&self`. `diff_changes` drops unchanged
6389    // columns so the JSON only contains the actual delta.
6390    //
6391    // Two-fragment shape: `audit_update_pre` runs before the UPDATE
6392    // and binds `_audit_before_pairs`; `audit_update_post` runs
6393    // after the UPDATE and emits the PendingEntry.
6394    let (audit_update_pre, audit_update_post): (TokenStream2, TokenStream2) = if let Some(tracked) =
6395        audited_fields
6396    {
6397        if tracked.is_empty() {
6398            (quote!(), quote!())
6399        } else {
6400            let select_cols: String = tracked
6401                .iter()
6402                .map(|c| format!("\"{}\"", c.column.replace('"', "\"\"")))
6403                .collect::<Vec<_>>()
6404                .join(", ");
6405            let pk_column_for_select = primary_key.map(|(_, col)| col.clone()).unwrap_or_default();
6406            let select_cols_lit = select_cols;
6407            let pk_column_lit_for_select = pk_column_for_select;
6408            let pk_value_for_bind = if let Some((pk_ident, _)) = primary_key {
6409                if fields.pk_is_auto {
6410                    quote!(self.#pk_ident.get().copied().unwrap_or_default())
6411                } else {
6412                    quote!(::core::clone::Clone::clone(&self.#pk_ident))
6413                }
6414            } else {
6415                quote!(0_i64)
6416            };
6417            let before_pairs = tracked.iter().map(|c| {
6418                let column_lit = c.column.as_str();
6419                let value_ty = &c.value_ty;
6420                quote! {
6421                    (
6422                        #column_lit,
6423                        match #root::sql::sqlx::Row::try_get::<#value_ty, _>(
6424                            &_audit_before_row, #column_lit,
6425                        ) {
6426                            ::core::result::Result::Ok(v) => {
6427                                #root::__serde_json::to_value(&v)
6428                                    .unwrap_or(#root::__serde_json::Value::Null)
6429                            }
6430                            ::core::result::Result::Err(_) => #root::__serde_json::Value::Null,
6431                        },
6432                    )
6433                }
6434            });
6435            let after_pairs = tracked.iter().map(|c| {
6436                let column_lit = c.column.as_str();
6437                let ident = &c.ident;
6438                quote! {
6439                    (
6440                        #column_lit,
6441                        #root::__serde_json::to_value(&self.#ident)
6442                            .unwrap_or(#root::__serde_json::Value::Null),
6443                    )
6444                }
6445            });
6446            let pk_str = audit_pk_to_string.clone();
6447            let pre = quote! {
6448                let _audit_select_sql = ::std::format!(
6449                    r#"SELECT {} FROM "{}" WHERE "{}" = $1"#,
6450                    #select_cols_lit,
6451                    <Self as #root::core::Model>::SCHEMA.table,
6452                    #pk_column_lit_for_select,
6453                );
6454                let _audit_before_pairs:
6455                    ::std::option::Option<::std::vec::Vec<(&'static str, #root::__serde_json::Value)>> =
6456                    match #root::sql::sqlx::query(&_audit_select_sql)
6457                        .bind(#pk_value_for_bind)
6458                        .fetch_optional(&mut *_executor)
6459                        .await
6460                    {
6461                        ::core::result::Result::Ok(::core::option::Option::Some(_audit_before_row)) => {
6462                            ::core::option::Option::Some(::std::vec![ #( #before_pairs ),* ])
6463                        }
6464                        _ => ::core::option::Option::None,
6465                    };
6466            };
6467            let post = quote! {
6468                if let ::core::option::Option::Some(_audit_before) = _audit_before_pairs {
6469                    let _audit_after:
6470                        ::std::vec::Vec<(&'static str, #root::__serde_json::Value)> =
6471                        ::std::vec![ #( #after_pairs ),* ];
6472                    let _audit_entry = #root::audit::PendingEntry {
6473                        entity_table: <Self as #root::core::Model>::SCHEMA.table,
6474                        entity_pk: #pk_str,
6475                        operation: #root::audit::AuditOp::Update,
6476                        source: #root::audit::current_source(),
6477                        changes: #root::audit::diff_changes(
6478                            &_audit_before,
6479                            &_audit_after,
6480                        ),
6481                    };
6482                    #root::audit::emit_one(&mut *_executor, &_audit_entry).await?;
6483                }
6484            };
6485            (pre, post)
6486        }
6487    } else {
6488        (quote!(), quote!())
6489    };
6490
6491    // Bulk-insert audit: capture every row's tracked fields after the
6492    // RETURNING populates each PK, then push one batched INSERT INTO
6493    // audit_log via `emit_many`. One round-trip regardless of N rows.
6494    let audit_bulk_insert_emit: TokenStream2 = if audited_fields.is_some() {
6495        let row_pk_str = if let Some((pk_ident, _)) = primary_key {
6496            if fields.pk_is_auto {
6497                quote!(_row.#pk_ident.get().map(|v| ::std::format!("{}", v)).unwrap_or_default())
6498            } else {
6499                quote!(::std::format!("{}", &_row.#pk_ident))
6500            }
6501        } else {
6502            quote!(::std::string::String::new())
6503        };
6504        let row_pairs = audited_fields.unwrap_or(&[]).iter().map(|c| {
6505            let column_lit = c.column.as_str();
6506            let ident = &c.ident;
6507            quote! {
6508                (
6509                    #column_lit,
6510                    #root::__serde_json::to_value(&_row.#ident)
6511                        .unwrap_or(#root::__serde_json::Value::Null),
6512                )
6513            }
6514        });
6515        quote! {
6516            let _audit_source = #root::audit::current_source();
6517            let mut _audit_entries:
6518                ::std::vec::Vec<#root::audit::PendingEntry> =
6519                    ::std::vec::Vec::with_capacity(rows.len());
6520            for _row in rows.iter() {
6521                _audit_entries.push(#root::audit::PendingEntry {
6522                    entity_table: <Self as #root::core::Model>::SCHEMA.table,
6523                    entity_pk: #row_pk_str,
6524                    operation: #root::audit::AuditOp::Create,
6525                    source: _audit_source.clone(),
6526                    changes: #root::audit::snapshot_changes(&[
6527                        #( #row_pairs ),*
6528                    ]),
6529                });
6530            }
6531            #root::audit::emit_many(&mut *_executor, &_audit_entries).await?;
6532        }
6533    } else {
6534        quote!()
6535    };
6536
6537    let save_method = if fields.pk_is_auto {
6538        let (pk_ident, pk_column) = primary_key.expect("pk_is_auto implies primary_key is Some");
6539        let pk_column_lit = pk_column.as_str();
6540        let assignments = &fields.update_assignments;
6541        let upsert_cols = &fields.upsert_update_columns;
6542        let upsert_pushes = &fields.insert_pushes;
6543        let upsert_returning = &fields.returning_cols;
6544        let upsert_auto_assigns = &fields.auto_assigns;
6545        // Conflict target: prefer the first declared `unique_together`
6546        // when it exists. Plain `Auto<T>` PKs are server-assigned via
6547        // `BIGSERIAL` and never collide on insert, so a PK-only target
6548        // would silently turn `upsert()` into "always-insert" for
6549        // surrogate-PK models with composite UNIQUE constraints — see
6550        // `RolePermission` / `UserRole` / `UserPermission` in the
6551        // tenancy permission engine. When no `unique_together` is
6552        // declared we keep the PK target (the original behaviour).
6553        let upsert_target_columns: Vec<String> = indexes
6554            .iter()
6555            .find(|i| i.unique && !i.columns.is_empty())
6556            .map(|i| i.columns.clone())
6557            .unwrap_or_else(|| vec![pk_column.clone()]);
6558        let upsert_target_lits = upsert_target_columns
6559            .iter()
6560            .map(String::as_str)
6561            .collect::<Vec<_>>();
6562        let conflict_clause = if fields.upsert_update_columns.is_empty() {
6563            quote!(#root::core::ConflictClause::DoNothing)
6564        } else {
6565            quote!(#root::core::ConflictClause::DoUpdate {
6566                target: ::std::vec![ #( #upsert_target_lits ),* ],
6567                update_columns: ::std::vec![ #( #upsert_cols ),* ],
6568            })
6569        };
6570        Some(quote! {
6571            /// Insert this row if its `Auto<T>` primary key is
6572            /// `Unset`, otherwise update the existing row matching the
6573            /// PK. Mirrors Django's `save()` — caller doesn't need to
6574            /// pick `insert` vs the bulk-update path manually.
6575            ///
6576            /// On the insert branch, populates the PK from `RETURNING`
6577            /// (same behavior as `insert`). On the update branch,
6578            /// writes every non-PK column back; if no row matches the
6579            /// PK, returns `Ok(())` silently.
6580            ///
6581            /// Only generated when the primary key is declared as
6582            /// `Auto<T>`. Models with a manually-managed PK must use
6583            /// `insert` or the QuerySet update builder.
6584            ///
6585            /// # Errors
6586            /// Returns [`#root::sql::ExecError`] for SQL-writing
6587            /// or driver failures.
6588            #[cfg(feature = "postgres")]
6589            pub async fn save(
6590                &mut self,
6591                pool: &#root::sql::sqlx::PgPool,
6592            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6593                #pool_to_save_on
6594            }
6595
6596            /// Like [`Self::save`] but accepts any sqlx executor —
6597            /// `&PgPool`, `&mut PgConnection`, or a transaction. The
6598            /// escape hatch for tenant-scoped writes: schema-mode
6599            /// tenants share the registry pool but rely on a per-
6600            /// checkout `SET search_path`, so passing `&PgPool` would
6601            /// silently hit the wrong schema. Acquire a connection
6602            /// via `TenantPools::acquire(&org)` and pass `&mut *conn`.
6603            ///
6604            /// # Errors
6605            /// As [`Self::save`].
6606            #[cfg(feature = "postgres")]
6607            pub async fn save_on #executor_generics (
6608                &mut self,
6609                #executor_param,
6610            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6611            #executor_where
6612            {
6613                // #1029 — INSERT writes exactly one row → 1; UPDATE returns
6614                // the rows-affected count (0 when the PK no longer exists,
6615                // the Django 6.0 `Model.NotUpdated` signal).
6616                if matches!(self.#pk_ident, #root::sql::Auto::Unset) {
6617                    return self.insert_on(#executor_passes_to_data_write).await.map(|()| 1u64);
6618                }
6619                #audit_update_pre
6620                let _query = #root::core::UpdateQuery {
6621                    model: <Self as #root::core::Model>::SCHEMA,
6622                    set: ::std::vec![ #( #assignments ),* ],
6623                    where_clause: #root::core::WhereExpr::Predicate(
6624                        #root::core::Filter {
6625                            column: #pk_column_lit,
6626                            op: #root::core::Op::Eq,
6627                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
6628                                ::core::clone::Clone::clone(&self.#pk_ident)
6629                            ),
6630                        }
6631                    ),
6632                };
6633                let _affected = #root::sql::__macro_internals::update_on(
6634                    #executor_passes_to_data_write,
6635                    &_query,
6636                ).await?;
6637                #audit_update_post
6638                ::core::result::Result::Ok(_affected)
6639            }
6640
6641            /// Per-call override for the audit source. Runs
6642            /// [`Self::save_on`] inside an [`#root::audit::with_source`]
6643            /// scope so the resulting audit entry records `source`
6644            /// instead of the task-local default. Useful for seed
6645            /// scripts and one-off CLI tools that don't sit inside an
6646            /// admin handler. The override applies only to this call;
6647            /// no global state changes.
6648            ///
6649            /// # Errors
6650            /// As [`Self::save_on`].
6651            #[cfg(feature = "postgres")]
6652            pub async fn save_on_with #executor_generics (
6653                &mut self,
6654                #executor_param,
6655                source: #root::audit::AuditSource,
6656            ) -> ::core::result::Result<u64, #root::sql::ExecError>
6657            #executor_where
6658            {
6659                #root::audit::with_source(source, self.save_on(_executor)).await
6660            }
6661
6662            /// Insert this row or update it in-place if the primary key already
6663            /// exists — single round-trip via `INSERT … ON CONFLICT (pk) DO UPDATE`.
6664            ///
6665            /// With `Auto::Unset` PK the server assigns a new key and no conflict
6666            /// can occur (equivalent to `insert`). With `Auto::Set` PK the row is
6667            /// inserted if absent or all non-PK columns are overwritten if present.
6668            ///
6669            /// # Errors
6670            /// As [`Self::insert_on`].
6671            #[cfg(feature = "postgres")]
6672            pub async fn upsert(
6673                &mut self,
6674                pool: &#root::sql::sqlx::PgPool,
6675            ) -> ::core::result::Result<(), #root::sql::ExecError> {
6676                #pool_to_upsert_on
6677            }
6678
6679            /// Like [`Self::upsert`] but accepts any sqlx executor.
6680            /// See [`Self::save_on`] for tenancy-scoped rationale.
6681            ///
6682            /// # Errors
6683            /// As [`Self::upsert`].
6684            #[cfg(feature = "postgres")]
6685            pub async fn upsert_on #executor_generics (
6686                &mut self,
6687                #executor_param,
6688            ) -> ::core::result::Result<(), #root::sql::ExecError>
6689            #executor_where
6690            {
6691                let mut _columns: ::std::vec::Vec<&'static str> =
6692                    ::std::vec::Vec::new();
6693                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
6694                    ::std::vec::Vec::new();
6695                #( #upsert_pushes )*
6696                let query = #root::core::InsertQuery {
6697                    model: <Self as #root::core::Model>::SCHEMA,
6698                    columns: _columns,
6699                    values: _values,
6700                    returning: ::std::vec![ #( #upsert_returning ),* ],
6701                    on_conflict: ::core::option::Option::Some(#conflict_clause),
6702                };
6703                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
6704                    #executor_passes_to_data_write,
6705                    &query,
6706                ).await?;
6707                let _returning_row = &_returning_row_v;
6708                #( #upsert_auto_assigns )*
6709                ::core::result::Result::Ok(())
6710            }
6711        })
6712    } else {
6713        None
6714    };
6715
6716    let pk_methods = primary_key.map(|(pk_ident, pk_column)| {
6717        let pk_column_lit = pk_column.as_str();
6718        // Optional `soft_delete_on` / `restore_on` companions when the
6719        // model has a `#[rustango(soft_delete)]` column. They land
6720        // alongside the regular `delete_on` so callers have both
6721        // options — a hard delete (audit-tracked as a real DELETE) and
6722        // a logical delete (audit-tracked as an UPDATE setting the
6723        // deleted_at column to NOW()).
6724        let soft_delete_methods = if let Some(col) = fields.soft_delete_column.as_deref() {
6725            let col_lit = col;
6726            let sd_field_ident = fields
6727                .soft_delete_field_ident
6728                .clone()
6729                .expect("soft_delete_column without ident");
6730            quote! {
6731                /// Soft-delete this row by setting its
6732                /// `#[rustango(soft_delete)]` column to `NOW()`.
6733                /// Mirrors Django's `SoftDeleteModel.delete()` shape:
6734                /// the row stays in the table; query helpers can
6735                /// filter it out by checking the column for `IS NOT
6736                /// NULL`.
6737                ///
6738                /// # Errors
6739                /// As [`Self::delete`].
6740                #[cfg(feature = "postgres")]
6741                pub async fn soft_delete_on #executor_generics (
6742                    &self,
6743                    #executor_param,
6744                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6745                #executor_where
6746                {
6747                    let _query = #root::core::UpdateQuery {
6748                        model: <Self as #root::core::Model>::SCHEMA,
6749                        set: ::std::vec![
6750                            #root::core::Assignment {
6751                                column: #col_lit,
6752                                value: ::core::convert::Into::<#root::core::Expr>::into(
6753                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6754                                        #root::__chrono::Utc::now()
6755                                    )
6756                                ),
6757                            },
6758                        ],
6759                        where_clause: #root::core::WhereExpr::Predicate(
6760                            #root::core::Filter {
6761                                column: #pk_column_lit,
6762                                op: #root::core::Op::Eq,
6763                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6764                                    ::core::clone::Clone::clone(&self.#pk_ident)
6765                                ),
6766                            }
6767                        ),
6768                    };
6769                    let _affected = #root::sql::__macro_internals::update_on(
6770                        #executor_passes_to_data_write,
6771                        &_query,
6772                    ).await?;
6773                    #audit_softdelete_emit
6774                    ::core::result::Result::Ok(_affected)
6775                }
6776
6777                /// Inverse of [`Self::soft_delete_on`] — clears the
6778                /// soft-delete column back to NULL so the row is
6779                /// considered live again.
6780                ///
6781                /// # Errors
6782                /// As [`Self::delete`].
6783                #[cfg(feature = "postgres")]
6784                pub async fn restore_on #executor_generics (
6785                    &self,
6786                    #executor_param,
6787                ) -> ::core::result::Result<u64, #root::sql::ExecError>
6788                #executor_where
6789                {
6790                    let _query = #root::core::UpdateQuery {
6791                        model: <Self as #root::core::Model>::SCHEMA,
6792                        set: ::std::vec![
6793                            #root::core::Assignment {
6794                                column: #col_lit,
6795                                value: ::core::convert::Into::<#root::core::Expr>::into(
6796                                    #root::core::SqlValue::Null
6797                                ),
6798                            },
6799                        ],
6800                        where_clause: #root::core::WhereExpr::Predicate(
6801                            #root::core::Filter {
6802                                column: #pk_column_lit,
6803                                op: #root::core::Op::Eq,
6804                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6805                                    ::core::clone::Clone::clone(&self.#pk_ident)
6806                                ),
6807                            }
6808                        ),
6809                    };
6810                    let _affected = #root::sql::__macro_internals::update_on(
6811                        #executor_passes_to_data_write,
6812                        &_query,
6813                    ).await?;
6814                    #audit_restore_emit
6815                    ::core::result::Result::Ok(_affected)
6816                }
6817
6818                /// Tri-dialect counterpart of [`Self::soft_delete_on`]
6819                /// — takes [`#root::sql::Pool`] and dispatches per
6820                /// backend. Eloquent `Model::delete()` semantics on
6821                /// soft-delete-enabled models (closes #821 partial).
6822                ///
6823                /// Sets the `#[rustango(soft_delete)]` column to
6824                /// `NOW()` on every backend. Query helpers
6825                /// (`QuerySet::active()` / `only_trashed()`,
6826                /// `soft_delete::active_filter` /
6827                /// `compose_with_active`) filter trashed rows out by
6828                /// reading `IS NULL` on the same column.
6829                ///
6830                /// # Errors
6831                /// As [`#root::sql::update_pool`].
6832                pub async fn soft_delete(
6833                    &self,
6834                    pool: &#root::sql::Pool,
6835                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6836                    let _query = #root::core::UpdateQuery {
6837                        model: <Self as #root::core::Model>::SCHEMA,
6838                        set: ::std::vec![
6839                            #root::core::Assignment {
6840                                column: #col_lit,
6841                                value: ::core::convert::Into::<#root::core::Expr>::into(
6842                                    ::core::convert::Into::<#root::core::SqlValue>::into(
6843                                        #root::__chrono::Utc::now()
6844                                    )
6845                                ),
6846                            },
6847                        ],
6848                        where_clause: #root::core::WhereExpr::Predicate(
6849                            #root::core::Filter {
6850                                column: #pk_column_lit,
6851                                op: #root::core::Op::Eq,
6852                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6853                                    ::core::clone::Clone::clone(&self.#pk_ident)
6854                                ),
6855                            }
6856                        ),
6857                    };
6858                    #root::sql::update_pool(pool, &_query).await
6859                }
6860
6861                /// Tri-dialect counterpart of [`Self::restore_on`].
6862                /// Clears the `#[rustango(soft_delete)]` column back
6863                /// to `NULL`, marking the row live again. Eloquent
6864                /// `Model::restore()` parity.
6865                ///
6866                /// # Errors
6867                /// As [`#root::sql::update_pool`].
6868                pub async fn restore(
6869                    &self,
6870                    pool: &#root::sql::Pool,
6871                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6872                    let _query = #root::core::UpdateQuery {
6873                        model: <Self as #root::core::Model>::SCHEMA,
6874                        set: ::std::vec![
6875                            #root::core::Assignment {
6876                                column: #col_lit,
6877                                value: ::core::convert::Into::<#root::core::Expr>::into(
6878                                    #root::core::SqlValue::Null
6879                                ),
6880                            },
6881                        ],
6882                        where_clause: #root::core::WhereExpr::Predicate(
6883                            #root::core::Filter {
6884                                column: #pk_column_lit,
6885                                op: #root::core::Op::Eq,
6886                                value: ::core::convert::Into::<#root::core::SqlValue>::into(
6887                                    ::core::clone::Clone::clone(&self.#pk_ident)
6888                                ),
6889                            }
6890                        ),
6891                    };
6892                    #root::sql::update_pool(pool, &_query).await
6893                }
6894
6895                /// Hard-delete this row, ignoring the soft-delete
6896                /// column. Eloquent `Model::forceDelete()` parity —
6897                /// the escape hatch when you need to actually purge
6898                /// data (GDPR, fixture cleanup, etc.).
6899                ///
6900                /// Equivalent to [`Self::delete_pool`] (the framework's
6901                /// non-soft delete) — exposed under the Eloquent name
6902                /// for muscle-memory + so soft-delete-enabled models
6903                /// have all three operations (soft / restore / force)
6904                /// in one place.
6905                ///
6906                /// # Errors
6907                /// As [`Self::delete_pool`].
6908                pub async fn force_delete(
6909                    &self,
6910                    pool: &#root::sql::Pool,
6911                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
6912                    Self::delete_pool(self, pool).await
6913                }
6914
6915                /// Returns `true` when this row is soft-deleted (its
6916                /// `#[rustango(soft_delete)]` column is currently
6917                /// set — Eloquent `$model->trashed()` parity).
6918                ///
6919                /// Pure in-memory predicate over `&self`; does not
6920                /// hit the database. Useful in admin/template code
6921                /// like `{% if post.trashed() %}…{% endif %}` and in
6922                /// guard clauses on restore/force-delete flows.
6923                pub fn trashed(&self) -> bool {
6924                    ::core::option::Option::is_some(&self.#sd_field_ident)
6925                }
6926
6927                /// Fetch every row whose `#[rustango(soft_delete)]`
6928                /// column is `NULL` (a.k.a. the live, non-trashed
6929                /// rows). Eloquent's default `Model::all()` behavior on
6930                /// a soft-delete model (Eloquent auto-scopes trashed
6931                /// rows out; rustango doesn't have auto-scoping yet —
6932                /// see issue #820 — so this is the explicit shortcut).
6933                ///
6934                /// One-liner over `QuerySet::<Self>::default()
6935                /// .active().fetch(pool)`. Closes #821 partial.
6936                ///
6937                /// # Errors
6938                /// As [`#root::sql::FetcherPool::fetch`].
6939                pub async fn active(
6940                    pool: &#root::sql::Pool,
6941                ) -> ::core::result::Result<
6942                    ::std::vec::Vec<Self>,
6943                    #root::sql::ExecError,
6944                > {
6945                    use #root::sql::FetcherPool as _;
6946                    #root::query::QuerySet::<Self>::default()
6947                        .active()
6948                        .fetch(pool)
6949                        .await
6950                }
6951
6952                /// Fetch ONLY soft-deleted rows. Eloquent
6953                /// `Model::onlyTrashed()->get()` parity — drives the
6954                /// admin "Trash" page, restore flows, GDPR purge
6955                /// scans, etc.
6956                ///
6957                /// One-liner over `QuerySet::<Self>::default()
6958                /// .only_trashed().fetch(pool)`. Closes #821
6959                /// partial.
6960                ///
6961                /// # Errors
6962                /// As [`#root::sql::FetcherPool::fetch`].
6963                pub async fn only_trashed(
6964                    pool: &#root::sql::Pool,
6965                ) -> ::core::result::Result<
6966                    ::std::vec::Vec<Self>,
6967                    #root::sql::ExecError,
6968                > {
6969                    use #root::sql::FetcherPool as _;
6970                    #root::query::QuerySet::<Self>::default()
6971                        .only_trashed()
6972                        .fetch(pool)
6973                        .await
6974                }
6975
6976                /// Fetch every row, both live and soft-deleted.
6977                /// Eloquent `Model::withTrashed()->get()` parity.
6978                ///
6979                /// Today every queryset already includes trashed rows
6980                /// (rustango has no global-scope tracking yet — issue
6981                /// #820), so this is functionally equivalent to
6982                /// [`Self::all_pool`]. Exposed as a named shortcut so
6983                /// soft-delete-aware code reads `Model::with_trashed_pool`
6984                /// rather than `Model::all_pool` — keeps intent visible
6985                /// in callers and stays correct when auto-scoping lands.
6986                ///
6987                /// Closes #821 partial.
6988                ///
6989                /// # Errors
6990                /// As [`#root::sql::FetcherPool::fetch`].
6991                pub async fn with_trashed(
6992                    pool: &#root::sql::Pool,
6993                ) -> ::core::result::Result<
6994                    ::std::vec::Vec<Self>,
6995                    #root::sql::ExecError,
6996                > {
6997                    use #root::sql::FetcherPool as _;
6998                    #root::query::QuerySet::<Self>::default()
6999                        .with_trashed()
7000                        .fetch(pool)
7001                        .await
7002                }
7003
7004            }
7005        } else {
7006            quote!()
7007        };
7008        quote! {
7009            /// Delete the row identified by this instance's primary key.
7010            ///
7011            /// Returns the number of rows affected (0 or 1).
7012            ///
7013            /// # Errors
7014            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7015            /// driver failures.
7016            #[cfg(feature = "postgres")]
7017            pub async fn delete(
7018                &self,
7019                pool: &#root::sql::sqlx::PgPool,
7020            ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7021                #pool_to_delete_on
7022            }
7023
7024            /// Like [`Self::delete`] but accepts any sqlx executor —
7025            /// for tenant-scoped deletes against an explicitly-acquired
7026            /// connection. See [`Self::save_on`] for the rationale.
7027            ///
7028            /// # Errors
7029            /// As [`Self::delete`].
7030            #[cfg(feature = "postgres")]
7031            pub async fn delete_on #executor_generics (
7032                &self,
7033                #executor_param,
7034            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7035            #executor_where
7036            {
7037                let query = #root::core::DeleteQuery {
7038                    model: <Self as #root::core::Model>::SCHEMA,
7039                    where_clause: #root::core::WhereExpr::Predicate(
7040                        #root::core::Filter {
7041                            column: #pk_column_lit,
7042                            op: #root::core::Op::Eq,
7043                            value: ::core::convert::Into::<#root::core::SqlValue>::into(
7044                                ::core::clone::Clone::clone(&self.#pk_ident)
7045                            ),
7046                        }
7047                    ),
7048                };
7049                let _affected = #root::sql::__macro_internals::delete_on(
7050                    #executor_passes_to_data_write,
7051                    &query,
7052                ).await?;
7053                #audit_delete_emit
7054                ::core::result::Result::Ok(_affected)
7055            }
7056
7057            /// Per-call audit-source override for [`Self::delete_on`].
7058            /// See [`Self::save_on_with`] for shape rationale.
7059            ///
7060            /// # Errors
7061            /// As [`Self::delete_on`].
7062            #[cfg(feature = "postgres")]
7063            pub async fn delete_on_with #executor_generics (
7064                &self,
7065                #executor_param,
7066                source: #root::audit::AuditSource,
7067            ) -> ::core::result::Result<u64, #root::sql::ExecError>
7068            #executor_where
7069            {
7070                #root::audit::with_source(source, self.delete_on(_executor)).await
7071            }
7072            #pool_delete_method
7073            #pool_insert_method
7074            #pool_save_method
7075            #refresh_replicate_methods
7076            #tx_delete_method
7077            #tx_insert_method
7078            #tx_save_method
7079            #soft_delete_methods
7080
7081            /// Returns `true` when `other` represents the same DB
7082            /// row as `self` — i.e. their primary keys compare
7083            /// equal. Eloquent `$model->is($other)` parity.
7084            ///
7085            /// Because both arguments are typed `&Self`, the
7086            /// model/table check is automatic — `Post::is` cannot
7087            /// be invoked against a `Comment` at compile time. Only
7088            /// the PK has to be compared at runtime.
7089            pub fn is(&self, other: &Self) -> bool {
7090                self.#pk_ident == other.#pk_ident
7091            }
7092
7093            /// Inverse of [`Self::is`]. Eloquent `$model->isNot($other)`
7094            /// parity.
7095            pub fn is_not(&self, other: &Self) -> bool {
7096                self.#pk_ident != other.#pk_ident
7097            }
7098
7099            /// Returns this row's primary-key value as an
7100            /// [`#root::core::SqlValue`]. Eloquent
7101            /// `$model->getKey()` parity.
7102            ///
7103            /// Useful when you need to thread the PK through a
7104            /// generic `Into<SqlValue>`-bound API without knowing the
7105            /// concrete PK type (`i64` vs `Uuid` vs `String`).
7106            #[must_use]
7107            pub fn get_key(&self) -> #root::core::SqlValue {
7108                ::core::convert::Into::into(::core::clone::Clone::clone(&self.#pk_ident))
7109            }
7110
7111        }
7112    });
7113
7114    let insert_method = if fields.has_auto {
7115        let pushes = &fields.insert_pushes;
7116        let returning_cols = &fields.returning_cols;
7117        let auto_assigns = &fields.auto_assigns;
7118        quote! {
7119            /// Insert this row into its table. Skips columns whose
7120            /// `Auto<T>` value is `Unset` so Postgres' SERIAL/BIGSERIAL
7121            /// sequence fills them in, then reads each `Auto` column
7122            /// back via `RETURNING` and stores it on `self`.
7123            ///
7124            /// # Errors
7125            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7126            /// driver failures.
7127            #[cfg(feature = "postgres")]
7128            pub async fn insert(
7129                &mut self,
7130                pool: &#root::sql::sqlx::PgPool,
7131            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7132                #pool_to_insert_on
7133            }
7134
7135            /// Like [`Self::insert`] but accepts any sqlx executor.
7136            /// See [`Self::save_on`] for tenancy-scoped rationale.
7137            ///
7138            /// # Errors
7139            /// As [`Self::insert`].
7140            #[cfg(feature = "postgres")]
7141            pub async fn insert_on #executor_generics (
7142                &mut self,
7143                #executor_param,
7144            ) -> ::core::result::Result<(), #root::sql::ExecError>
7145            #executor_where
7146            {
7147                let mut _columns: ::std::vec::Vec<&'static str> =
7148                    ::std::vec::Vec::new();
7149                let mut _values: ::std::vec::Vec<#root::core::SqlValue> =
7150                    ::std::vec::Vec::new();
7151                #( #pushes )*
7152                let query = #root::core::InsertQuery {
7153                    model: <Self as #root::core::Model>::SCHEMA,
7154                    columns: _columns,
7155                    values: _values,
7156                    returning: ::std::vec![ #( #returning_cols ),* ],
7157                    on_conflict: ::core::option::Option::None,
7158                };
7159                let _returning_row_v = #root::sql::__macro_internals::insert_returning_on(
7160                    #executor_passes_to_data_write,
7161                    &query,
7162                ).await?;
7163                let _returning_row = &_returning_row_v;
7164                #( #auto_assigns )*
7165                #audit_insert_emit
7166                ::core::result::Result::Ok(())
7167            }
7168
7169            /// Per-call audit-source override for [`Self::insert_on`].
7170            /// See [`Self::save_on_with`] for shape rationale.
7171            ///
7172            /// # Errors
7173            /// As [`Self::insert_on`].
7174            #[cfg(feature = "postgres")]
7175            pub async fn insert_on_with #executor_generics (
7176                &mut self,
7177                #executor_param,
7178                source: #root::audit::AuditSource,
7179            ) -> ::core::result::Result<(), #root::sql::ExecError>
7180            #executor_where
7181            {
7182                #root::audit::with_source(source, self.insert_on(_executor)).await
7183            }
7184        }
7185    } else {
7186        let insert_columns = &fields.insert_columns;
7187        let insert_values = &fields.insert_values;
7188        quote! {
7189            /// Insert this row into its table.
7190            ///
7191            /// # Errors
7192            /// Returns [`#root::sql::ExecError`] for SQL-writing or
7193            /// driver failures.
7194            #[cfg(feature = "postgres")]
7195            pub async fn insert(
7196                &self,
7197                pool: &#root::sql::sqlx::PgPool,
7198            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7199                self.insert_on(pool).await
7200            }
7201
7202            /// Like [`Self::insert`] but accepts any sqlx executor.
7203            /// See [`Self::save_on`] for tenancy-scoped rationale.
7204            ///
7205            /// # Errors
7206            /// As [`Self::insert`].
7207            #[cfg(feature = "postgres")]
7208            pub async fn insert_on<'_c, _E>(
7209                &self,
7210                _executor: _E,
7211            ) -> ::core::result::Result<(), #root::sql::ExecError>
7212            where
7213                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7214            {
7215                let query = #root::core::InsertQuery {
7216                    model: <Self as #root::core::Model>::SCHEMA,
7217                    columns: ::std::vec![ #( #insert_columns ),* ],
7218                    values: ::std::vec![ #( #insert_values ),* ],
7219                    returning: ::std::vec::Vec::new(),
7220                    on_conflict: ::core::option::Option::None,
7221                };
7222                #root::sql::__macro_internals::insert_on(_executor, &query).await
7223            }
7224        }
7225    };
7226
7227    let bulk_insert_method = if fields.has_auto {
7228        let cols_no_auto = &fields.bulk_columns_no_auto;
7229        let cols_all = &fields.bulk_columns_all;
7230        let pushes_no_auto = &fields.bulk_pushes_no_auto;
7231        let pushes_all = &fields.bulk_pushes_all;
7232        let returning_cols = &fields.returning_cols;
7233        let auto_assigns_for_row = bulk_auto_assigns_for_row(fields);
7234        let uniformity = &fields.bulk_auto_uniformity;
7235        let first_auto_ident = fields
7236            .first_auto_ident
7237            .as_ref()
7238            .expect("has_auto implies first_auto_ident is Some");
7239        quote! {
7240            /// Bulk-insert `rows` in a single round-trip. Every row's
7241            /// `Auto<T>` PK fields must uniformly be `Auto::Unset`
7242            /// (sequence fills them in) or uniformly `Auto::Set(_)`
7243            /// (caller-supplied values). Mixed Set/Unset is rejected
7244            /// — call `insert` per row for that case.
7245            ///
7246            /// Empty slice is a no-op. Each row's `Auto` fields are
7247            /// populated from the `RETURNING` clause in input order
7248            /// before this returns.
7249            ///
7250            /// # Errors
7251            /// Returns [`#root::sql::ExecError`] for validation,
7252            /// SQL-writing, mixed-Auto rejection, or driver failures.
7253            #[cfg(feature = "postgres")]
7254            pub async fn bulk_insert(
7255                rows: &mut [Self],
7256                pool: &#root::sql::sqlx::PgPool,
7257            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7258                #pool_to_bulk_insert_on
7259            }
7260
7261            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7262            /// See [`Self::save_on`] for tenancy-scoped rationale.
7263            ///
7264            /// # Errors
7265            /// As [`Self::bulk_insert`].
7266            #[cfg(feature = "postgres")]
7267            pub async fn bulk_insert_on #executor_generics (
7268                rows: &mut [Self],
7269                #executor_param,
7270            ) -> ::core::result::Result<(), #root::sql::ExecError>
7271            #executor_where
7272            {
7273                if rows.is_empty() {
7274                    return ::core::result::Result::Ok(());
7275                }
7276                let _first_unset = matches!(
7277                    rows[0].#first_auto_ident,
7278                    #root::sql::Auto::Unset
7279                );
7280                #( #uniformity )*
7281
7282                let mut _all_rows: ::std::vec::Vec<
7283                    ::std::vec::Vec<#root::core::SqlValue>,
7284                > = ::std::vec::Vec::with_capacity(rows.len());
7285                let _columns: ::std::vec::Vec<&'static str> = if _first_unset {
7286                    for _row in rows.iter() {
7287                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7288                            ::std::vec::Vec::new();
7289                        #( #pushes_no_auto )*
7290                        _all_rows.push(_row_vals);
7291                    }
7292                    ::std::vec![ #( #cols_no_auto ),* ]
7293                } else {
7294                    for _row in rows.iter() {
7295                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7296                            ::std::vec::Vec::new();
7297                        #( #pushes_all )*
7298                        _all_rows.push(_row_vals);
7299                    }
7300                    ::std::vec![ #( #cols_all ),* ]
7301                };
7302
7303                let _query = #root::core::BulkInsertQuery {
7304                    model: <Self as #root::core::Model>::SCHEMA,
7305                    columns: _columns,
7306                    rows: _all_rows,
7307                    returning: ::std::vec![ #( #returning_cols ),* ],
7308                    on_conflict: ::core::option::Option::None,
7309                };
7310                let _returned = #root::sql::__macro_internals::bulk_insert_on(
7311                    #executor_passes_to_data_write,
7312                    &_query,
7313                ).await?;
7314                if _returned.len() != rows.len() {
7315                    return ::core::result::Result::Err(
7316                        #root::sql::ExecError::Sql(
7317                            #root::sql::SqlError::BulkInsertReturningMismatch {
7318                                expected: rows.len(),
7319                                actual: _returned.len(),
7320                            }
7321                        )
7322                    );
7323                }
7324                for (_returning_row, _row_mut) in _returned.iter().zip(rows.iter_mut()) {
7325                    #auto_assigns_for_row
7326                }
7327                #audit_bulk_insert_emit
7328                ::core::result::Result::Ok(())
7329            }
7330        }
7331    } else {
7332        let cols_all = &fields.bulk_columns_all;
7333        let pushes_all = &fields.bulk_pushes_all;
7334        quote! {
7335            /// Bulk-insert `rows` in a single round-trip. Every row's
7336            /// fields are written verbatim — there are no `Auto<T>`
7337            /// fields on this model.
7338            ///
7339            /// Empty slice is a no-op.
7340            ///
7341            /// # Errors
7342            /// Returns [`#root::sql::ExecError`] for validation,
7343            /// SQL-writing, or driver failures.
7344            #[cfg(feature = "postgres")]
7345            pub async fn bulk_insert(
7346                rows: &[Self],
7347                pool: &#root::sql::sqlx::PgPool,
7348            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7349                Self::bulk_insert_on(rows, pool).await
7350            }
7351
7352            /// Like [`Self::bulk_insert`] but accepts any sqlx executor.
7353            /// See [`Self::save_on`] for tenancy-scoped rationale.
7354            ///
7355            /// # Errors
7356            /// As [`Self::bulk_insert`].
7357            #[cfg(feature = "postgres")]
7358            pub async fn bulk_insert_on<'_c, _E>(
7359                rows: &[Self],
7360                _executor: _E,
7361            ) -> ::core::result::Result<(), #root::sql::ExecError>
7362            where
7363                _E: #root::sql::sqlx::Executor<'_c, Database = #root::sql::sqlx::Postgres>,
7364            {
7365                if rows.is_empty() {
7366                    return ::core::result::Result::Ok(());
7367                }
7368                let mut _all_rows: ::std::vec::Vec<
7369                    ::std::vec::Vec<#root::core::SqlValue>,
7370                > = ::std::vec::Vec::with_capacity(rows.len());
7371                for _row in rows.iter() {
7372                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7373                        ::std::vec::Vec::new();
7374                    #( #pushes_all )*
7375                    _all_rows.push(_row_vals);
7376                }
7377                let _query = #root::core::BulkInsertQuery {
7378                    model: <Self as #root::core::Model>::SCHEMA,
7379                    columns: ::std::vec![ #( #cols_all ),* ],
7380                    rows: _all_rows,
7381                    returning: ::std::vec::Vec::new(),
7382                    on_conflict: ::core::option::Option::None,
7383                };
7384                let _ = #root::sql::__macro_internals::bulk_insert_on(_executor, &_query).await?;
7385                ::core::result::Result::Ok(())
7386            }
7387        }
7388    };
7389
7390    // Tri-dialect `bulk_upsert_pool` — issue #267 / T1.5. Always emitted
7391    // (no postgres-feature gate); routes through the existing
7392    // `bulk_insert_pool` + per-dialect conflict writer.
7393    //
7394    // Auto<T> PKs are required to be `Auto::Unset` for every row so the
7395    // sequence picks the PK for fresh inserts; the UPDATE branch never
7396    // touches the Auto column.
7397    let bulk_upsert_pool_method = {
7398        // Pick the "no Auto" columns when the model has Auto fields,
7399        // else every column.
7400        let (upsert_cols, upsert_pushes): (Vec<_>, Vec<_>) = if fields.has_auto {
7401            (
7402                fields.bulk_columns_no_auto.clone(),
7403                fields.bulk_pushes_no_auto.clone(),
7404            )
7405        } else {
7406            (
7407                fields.bulk_columns_all.clone(),
7408                fields.bulk_pushes_all.clone(),
7409            )
7410        };
7411        quote! {
7412            /// Tri-dialect `bulk_create(update_conflicts=True)` — Django's
7413            /// canonical "import a batch idempotently" shape. Issue #267
7414            /// / T1.5.
7415            ///
7416            /// Per-row values are extracted and lowered into a
7417            /// [`#root::core::BulkInsertQuery`] with
7418            /// `on_conflict = DoUpdate { target, update_columns }`. The
7419            /// writer dispatches per-dialect:
7420            /// * Postgres / SQLite: `INSERT … ON CONFLICT (target) DO UPDATE SET col = EXCLUDED.col`
7421            /// * MySQL: `INSERT … ON DUPLICATE KEY UPDATE col = VALUES(col)` (target ignored — MySQL matches every UNIQUE index)
7422            ///
7423            /// `target` names the column(s) whose unique constraint
7424            /// defines the conflict (typically a `unique` or
7425            /// `unique_together` natural-key column, NOT the `Auto<T>`
7426            /// PK). `update_cols` names the columns to overwrite on
7427            /// conflict — every other column is left untouched on the
7428            /// existing row.
7429            ///
7430            /// Auto-PK rows must all have `Auto::Unset` (the sequence
7431            /// picks the PK on insert; the update path never touches
7432            /// the Auto column). Auto-set rows trigger a hard error.
7433            /// Empty slice is a no-op.
7434            ///
7435            /// # Errors
7436            /// Returns [`#root::sql::ExecError`] for validation,
7437            /// SQL-writing, or driver failures.
7438            pub async fn bulk_upsert_pool(
7439                rows: &[Self],
7440                target: &[&'static str],
7441                update_cols: &[&'static str],
7442                pool: &#root::sql::Pool,
7443            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7444                if rows.is_empty() {
7445                    return ::core::result::Result::Ok(());
7446                }
7447                let mut _all_rows: ::std::vec::Vec<
7448                    ::std::vec::Vec<#root::core::SqlValue>,
7449                > = ::std::vec::Vec::with_capacity(rows.len());
7450                for _row in rows.iter() {
7451                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7452                        ::std::vec::Vec::new();
7453                    #( #upsert_pushes )*
7454                    _all_rows.push(_row_vals);
7455                }
7456                let _query = #root::core::BulkInsertQuery {
7457                    model: <Self as #root::core::Model>::SCHEMA,
7458                    columns: ::std::vec![ #( #upsert_cols ),* ],
7459                    rows: _all_rows,
7460                    returning: ::std::vec::Vec::new(),
7461                    on_conflict: ::core::option::Option::Some(
7462                        #root::core::ConflictClause::DoUpdate {
7463                            target: target.to_vec(),
7464                            update_columns: update_cols.to_vec(),
7465                        }
7466                    ),
7467                };
7468                #root::sql::bulk_insert_pool(pool, &_query).await
7469            }
7470
7471            /// Tri-dialect `bulk_create(ignore_conflicts=True)` — silently
7472            /// skip rows that would violate a unique constraint. Issue
7473            /// #267 / T1.5. Same per-dialect dispatch as
7474            /// [`Self::bulk_upsert_pool`] but with `ON CONFLICT … DO
7475            /// NOTHING` (Postgres / SQLite) / `ON DUPLICATE KEY UPDATE
7476            /// <pivot> = <pivot>` (MySQL no-op write).
7477            ///
7478            /// # Errors
7479            /// As [`Self::bulk_upsert_pool`].
7480            pub async fn bulk_insert_or_ignore_pool(
7481                rows: &[Self],
7482                pool: &#root::sql::Pool,
7483            ) -> ::core::result::Result<(), #root::sql::ExecError> {
7484                if rows.is_empty() {
7485                    return ::core::result::Result::Ok(());
7486                }
7487                let mut _all_rows: ::std::vec::Vec<
7488                    ::std::vec::Vec<#root::core::SqlValue>,
7489                > = ::std::vec::Vec::with_capacity(rows.len());
7490                for _row in rows.iter() {
7491                    let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7492                        ::std::vec::Vec::new();
7493                    #( #upsert_pushes )*
7494                    _all_rows.push(_row_vals);
7495                }
7496                let _query = #root::core::BulkInsertQuery {
7497                    model: <Self as #root::core::Model>::SCHEMA,
7498                    columns: ::std::vec![ #( #upsert_cols ),* ],
7499                    rows: _all_rows,
7500                    returning: ::std::vec::Vec::new(),
7501                    on_conflict: ::core::option::Option::Some(
7502                        #root::core::ConflictClause::DoNothing
7503                    ),
7504                };
7505                #root::sql::bulk_insert_pool(pool, &_query).await
7506            }
7507        }
7508    };
7509
7510    // Ergonomic `Model::bulk_update(objs, fields)` — Django's
7511    // `QuerySet.bulk_update`. The SQL/IR/executor stack
7512    // (`BulkUpdateQuery` + `bulk_update_pool` + the per-dialect
7513    // `write_bulk_update_*` writers) already existed; what was missing
7514    // was the per-model constructor that maps `&[Self]` + a runtime
7515    // column list into rows of `[pk, col_vals…]` so callers don't
7516    // hand-build the IR. Emitted only when the model has a primary key
7517    // (the PK is the join key and can't itself be updated).
7518    let bulk_update_method = match &fields.primary_key {
7519        None => quote! {},
7520        Some((pk_ident, pk_col)) => {
7521            // One pair of match arms per non-PK column: a validation arm
7522            // resolving the runtime name to its `&'static str` column,
7523            // and a value arm pushing that field off the row.
7524            let mut col_arms: Vec<TokenStream2> = Vec::new();
7525            let mut val_arms: Vec<TokenStream2> = Vec::new();
7526            for entry in &fields.column_entries {
7527                if &entry.column == pk_col {
7528                    continue;
7529                }
7530                let col = &entry.column;
7531                let ident = &entry.ident;
7532                col_arms.push(quote! { #col => #col, });
7533                val_arms.push(quote! {
7534                    #col => _row_vals.push(
7535                        ::core::convert::Into::<#root::core::SqlValue>::into(
7536                            ::core::clone::Clone::clone(&_o.#ident)
7537                        )
7538                    ),
7539                });
7540            }
7541            quote! {
7542                /// Django's `QuerySet.bulk_update(objs, fields)` — write
7543                /// per-row-different values for the named `fields` across
7544                /// every object in `objs` in a single statement, matched
7545                /// by primary key.
7546                ///
7547                /// `fields` names the **columns** to update. The primary
7548                /// key identifies each row and cannot itself be updated
7549                /// (pass it and you get
7550                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`]).
7551                /// Empty `objs` or `fields` is a no-op returning `0`.
7552                /// Objects whose PK matches no row are simply not updated.
7553                /// Returns the number of rows affected.
7554                ///
7555                /// Tri-dialect: lowers to one
7556                /// [`#root::core::BulkUpdateQuery`] and dispatches
7557                /// per-backend — `UPDATE … FROM (VALUES …)` on Postgres,
7558                /// a CTE + correlated subquery on SQLite, an inner
7559                /// `JOIN (VALUES …)` on MySQL.
7560                ///
7561                /// # Errors
7562                /// [`#root::core::QueryError::UnknownField`] for a name
7563                /// that isn't a column on this model,
7564                /// [`#root::core::QueryError::BulkUpdatePrimaryKey`] if
7565                /// `fields` names the PK, or [`#root::sql::ExecError`] for
7566                /// SQL-writing / driver failures.
7567                pub async fn bulk_update(
7568                    objs: &[Self],
7569                    fields: &[&str],
7570                    pool: &#root::sql::Pool,
7571                ) -> ::core::result::Result<u64, #root::sql::ExecError> {
7572                    if objs.is_empty() || fields.is_empty() {
7573                        return ::core::result::Result::Ok(0);
7574                    }
7575                    let _model_name = <Self as #root::core::Model>::SCHEMA.name;
7576                    let mut _update_columns: ::std::vec::Vec<&'static str> =
7577                        ::std::vec::Vec::with_capacity(fields.len());
7578                    for &_f in fields {
7579                        let _col: &'static str = match _f {
7580                            #pk_col => {
7581                                return ::core::result::Result::Err(
7582                                    ::core::convert::Into::into(
7583                                        #root::core::QueryError::BulkUpdatePrimaryKey {
7584                                            model: _model_name,
7585                                            field: ::std::string::ToString::to_string(_f),
7586                                        }
7587                                    )
7588                                );
7589                            }
7590                            #( #col_arms )*
7591                            _ => {
7592                                return ::core::result::Result::Err(
7593                                    ::core::convert::Into::into(
7594                                        #root::core::QueryError::UnknownField {
7595                                            model: _model_name,
7596                                            field: ::std::string::ToString::to_string(_f),
7597                                        }
7598                                    )
7599                                );
7600                            }
7601                        };
7602                        // A model whose only field is the primary key emits no
7603                        // non-PK arms, so every arm above diverges and this push
7604                        // is genuinely unreachable — correct, since such a model
7605                        // has nothing to bulk-update. Scoped to the statement
7606                        // rather than the fn so a real unreachability elsewhere
7607                        // in this body still warns. Without it every PK-only
7608                        // model warns in the *user's* crate, which they cannot
7609                        // act on (#1290).
7610                        #[allow(unreachable_code)]
7611                        _update_columns.push(_col);
7612                    }
7613                    let mut _rows: ::std::vec::Vec<
7614                        ::std::vec::Vec<#root::core::SqlValue>,
7615                    > = ::std::vec::Vec::with_capacity(objs.len());
7616                    for _o in objs.iter() {
7617                        let mut _row_vals: ::std::vec::Vec<#root::core::SqlValue> =
7618                            ::std::vec::Vec::with_capacity(fields.len() + 1);
7619                        // PK first — the writers expect `[pk, …update cols]`.
7620                        _row_vals.push(
7621                            ::core::convert::Into::<#root::core::SqlValue>::into(
7622                                ::core::clone::Clone::clone(&_o.#pk_ident)
7623                            )
7624                        );
7625                        for &_f in fields {
7626                            match _f {
7627                                #( #val_arms )*
7628                                // Unreachable: every name was validated
7629                                // against the same arm set above.
7630                                _ => {}
7631                            }
7632                        }
7633                        _rows.push(_row_vals);
7634                    }
7635                    let _query = #root::core::BulkUpdateQuery {
7636                        model: <Self as #root::core::Model>::SCHEMA,
7637                        update_columns: _update_columns,
7638                        rows: _rows,
7639                    };
7640                    #root::sql::bulk_update_pool(pool, &_query).await
7641                }
7642            }
7643        }
7644    };
7645
7646    let pk_value_helper = primary_key.map(|(pk_ident, _)| {
7647        quote! {
7648            /// Hidden runtime accessor for the primary-key value as a
7649            /// [`SqlValue`]. Used by reverse-relation helpers
7650            /// (`<parent>::<child>_set`) emitted from sibling models'
7651            /// FK fields. Not part of the public API.
7652            #[doc(hidden)]
7653            pub fn __rustango_pk_value(&self) -> #root::core::SqlValue {
7654                ::core::convert::Into::<#root::core::SqlValue>::into(
7655                    ::core::clone::Clone::clone(&self.#pk_ident)
7656                )
7657            }
7658        }
7659    });
7660
7661    let has_pk_value_impl = primary_key.map(|(pk_ident, _)| {
7662        quote! {
7663            impl #root::sql::HasPkValue for #struct_name {
7664                fn __rustango_pk_value_impl(&self) -> #root::core::SqlValue {
7665                    ::core::convert::Into::<#root::core::SqlValue>::into(
7666                        ::core::clone::Clone::clone(&self.#pk_ident)
7667                    )
7668                }
7669            }
7670        }
7671    });
7672
7673    let fk_pk_access_impl = fk_pk_access_impl_tokens(struct_name, &fields.fk_relations);
7674
7675    // Slice 17.1 — `AssignAutoPkPool` impl lets `apply_auto_pk`
7676    // dispatch to the right per-backend body without the macro emitting
7677    // any `#[cfg(feature = …)]` arm into consumer code. Always emitted
7678    // so audited models with non-Auto PKs (which still go through
7679    // `insert_one_with_audit` → `apply_auto_pk`) link.
7680    let assign_auto_pk_pool_impl = {
7681        let auto_assigns = &fields.auto_assigns;
7682        // SQLite ≥ 3.35 supports the same RETURNING shape as Postgres,
7683        // so the body is structurally identical to `auto_assigns` —
7684        // only the helper name swaps from `try_get_returning` to
7685        // `try_get_returning_sqlite` so the closure typechecks against
7686        // a `SqliteRow` instead of a `PgRow`.
7687        let auto_assigns_sqlite: Vec<TokenStream2> = fields
7688            .auto_field_idents
7689            .iter()
7690            .map(|(ident, column)| {
7691                quote! {
7692                    self.#ident = #root::sql::try_get_returning_sqlite(
7693                        _returning_row, #column
7694                    )?;
7695                }
7696            })
7697            .collect();
7698        // #1028 — decode each `generated_as` column from the same
7699        // RETURNING row (PG/SQLite). Plain-typed fields, so the field's
7700        // own type drives the decode (no `Auto<T>` wrapper).
7701        let generated_assigns: Vec<TokenStream2> = fields
7702            .generated_field_idents
7703            .iter()
7704            .map(|(ident, column)| {
7705                quote! {
7706                    self.#ident = #root::sql::try_get_returning(_returning_row, #column)?;
7707                }
7708            })
7709            .collect();
7710        let generated_assigns_sqlite: Vec<TokenStream2> = fields
7711            .generated_field_idents
7712            .iter()
7713            .map(|(ident, column)| {
7714                quote! {
7715                    self.#ident = #root::sql::try_get_returning_sqlite(
7716                        _returning_row, #column
7717                    )?;
7718                }
7719            })
7720            .collect();
7721        let mysql_body = if let Some(first) = fields.first_auto_ident.as_ref() {
7722            // The MySQL `LAST_INSERT_ID()` is always i64. Route through
7723            // `MysqlAutoIdSet` so Auto<i32> narrows safely and
7724            // Auto<Uuid>/etc. fail to link against MySQL (intended —
7725            // those models can't use AUTO_INCREMENT). The trait is only
7726            // touched on the MySQL arm at runtime, so PG-only consumers
7727            // never see the bound failure.
7728            //
7729            // Pre-v0.20: models with multiple `Auto<T>` fields (e.g.
7730            // Auto<i64> PK + auto_now_add timestamp) errored hard at
7731            // runtime with "multi-column RETURNING". MySQL has no
7732            // multi-column RETURNING semantic and a follow-up SELECT
7733            // would need cross-trait plumbing. Pragmatic shape: succeed
7734            // with the FIRST Auto field populated from LAST_INSERT_ID();
7735            // any other Auto fields stay `Auto::Unset`. Callers that
7736            // need the DB-defaulted timestamp / UUID can re-fetch the
7737            // row by PK after `save_pool`. Fixes the cookbook chapter
7738            // 12 dialect divergence.
7739            let value_ty = fields
7740                .first_auto_value_ty
7741                .as_ref()
7742                .expect("first_auto_value_ty set whenever first_auto_ident is");
7743            quote! {
7744                let _converted = <#value_ty as #root::sql::MysqlAutoIdSet>
7745                    ::rustango_from_mysql_auto_id(_id)?;
7746                self.#first = #root::sql::Auto::Set(_converted);
7747                ::core::result::Result::Ok(())
7748            }
7749        } else {
7750            quote! {
7751                let _ = _id;
7752                ::core::result::Result::Ok(())
7753            }
7754        };
7755        quote! {
7756            impl #root::sql::AssignAutoPkPool for #struct_name {
7757                fn __rustango_assign_from_pg_row(
7758                    &mut self,
7759                    _returning_row: &#root::sql::PgReturningRow,
7760                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7761                    #( #auto_assigns )*
7762                    #( #generated_assigns )*
7763                    ::core::result::Result::Ok(())
7764                }
7765                fn __rustango_assign_from_mysql_id(
7766                    &mut self,
7767                    _id: i64,
7768                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7769                    #mysql_body
7770                }
7771                fn __rustango_assign_from_sqlite_row(
7772                    &mut self,
7773                    _returning_row: &#root::sql::SqliteReturningRow,
7774                ) -> ::core::result::Result<(), #root::sql::ExecError> {
7775                    #( #auto_assigns_sqlite )*
7776                    #( #generated_assigns_sqlite )*
7777                    ::core::result::Result::Ok(())
7778                }
7779            }
7780        }
7781    };
7782
7783    let from_aliased_row_inits = &fields.from_aliased_row_inits;
7784    let aliased_row_helper = quote! {
7785        /// Decode a row's aliased target columns (produced by
7786        /// `select_related`'s LEFT JOIN) into a fresh instance of
7787        /// this model. Reads each column via
7788        /// `format!("{prefix}__{col}")`, matching the alias the
7789        /// SELECT writer emitted. Slice 9.0d.
7790        #[doc(hidden)]
7791        #[cfg(feature = "postgres")]
7792        pub fn __rustango_from_aliased_row(
7793            row: &#root::sql::sqlx::postgres::PgRow,
7794            prefix: &str,
7795        ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
7796            ::core::result::Result::Ok(Self {
7797                #( #from_aliased_row_inits ),*
7798            })
7799        }
7800    };
7801    // v0.23.0-batch8 — MySQL counterpart, gated through the
7802    // cfg-aware macro_rules so PG-only builds expand to nothing.
7803    let aliased_row_helper_my = quote! {
7804        #root::__impl_my_aliased_row_decoder!(#struct_name, |row, prefix| {
7805            #( #from_aliased_row_inits ),*
7806        });
7807    };
7808
7809    // v0.27 Phase 3 — SQLite counterpart, same hygiene-aware closure
7810    // pattern + cfg gate on the `sqlite` feature.
7811    let aliased_row_helper_sqlite = quote! {
7812        #root::__impl_sqlite_aliased_row_decoder!(#struct_name, |row, prefix| {
7813            #( #from_aliased_row_inits ),*
7814        });
7815    };
7816
7817    let load_related_impl = load_related_impl_tokens(struct_name, &fields.fk_relations);
7818    let load_related_impl_my = load_related_impl_my_tokens(struct_name, &fields.fk_relations);
7819    let load_related_impl_sqlite =
7820        load_related_impl_sqlite_tokens(struct_name, &fields.fk_relations);
7821
7822    // Issue #289 / T2.6 — `#[rustango(manager_fn = "active")]` emits
7823    // extra `Self::<name>() -> QuerySet<Self>` accessors next to the
7824    // default `Self::objects()`. Each accessor returns a fresh
7825    // QuerySet that resolves any `impl <FooManagerExt> for QuerySet<Foo>`
7826    // methods the user defined.
7827    let extra_manager_fns: Vec<TokenStream2> = manager_fns
7828        .iter()
7829        .map(|fn_ident| {
7830            let model_name_str = struct_name.to_string();
7831            let fn_name_str = fn_ident.to_string();
7832            let doc = format!(
7833                "Custom-named QuerySet accessor for [`{model_name_str}`]. \
7834                 Generated by `#[rustango(manager_fn = \"{fn_name_str}\")]` — \
7835                 equivalent to `Self::objects()`. Chains with any \
7836                 `impl ... for QuerySet<{model_name_str}> {{ ... }}` \
7837                 extension methods."
7838            );
7839            quote! {
7840                #[doc = #doc]
7841                #[must_use]
7842                pub fn #fn_ident() -> #root::query::QuerySet<#struct_name> {
7843                    #root::query::QuerySet::new()
7844                }
7845            }
7846        })
7847        .collect();
7848
7849    quote! {
7850        impl #struct_name {
7851            /// Start a new `QuerySet` over this model. Django shape.
7852            #[must_use]
7853            pub fn objects() -> #root::query::QuerySet<#struct_name> {
7854                #root::query::QuerySet::new()
7855            }
7856
7857            /// Eloquent-shape alias of [`Self::objects`]. Returns
7858            /// a fresh `QuerySet<Self>` ready for `.filter()` /
7859            /// `.where_()` / etc. Matches Laravel muscle-memory:
7860            ///
7861            /// ```ignore
7862            /// // Eloquent:    Post::query()->where('published', true)
7863            /// // Django:      Post.objects.filter(published=True)
7864            /// // rustango:    Post::query().filter("published", true)
7865            /// //         or:  Post::objects().filter("published", true)
7866            /// ```
7867            ///
7868            /// Both names point at the same underlying constructor;
7869            /// neither is preferred.
7870            #[must_use]
7871            pub fn query() -> #root::query::QuerySet<#struct_name> {
7872                #root::query::QuerySet::new()
7873            }
7874
7875            #( #extra_manager_fns )*
7876
7877            #insert_method
7878
7879            #bulk_insert_method
7880
7881            #bulk_upsert_pool_method
7882
7883            #bulk_update_method
7884
7885            #save_method
7886
7887            #pk_methods
7888
7889            #pk_value_helper
7890
7891            #aliased_row_helper
7892
7893            #column_consts
7894        }
7895
7896        #aliased_row_helper_my
7897
7898        #aliased_row_helper_sqlite
7899
7900        #load_related_impl
7901
7902        #load_related_impl_my
7903
7904        #load_related_impl_sqlite
7905
7906        #has_pk_value_impl
7907
7908        #fk_pk_access_impl
7909
7910        #assign_auto_pk_pool_impl
7911    }
7912}
7913
7914/// Per-row Auto-field assigns for `bulk_insert` — equivalent to
7915/// `auto_assigns` but reading from `_returning_row` and writing to
7916/// `_row_mut` instead of `self`.
7917fn bulk_auto_assigns_for_row(fields: &CollectedFields) -> TokenStream2 {
7918    let root = rustango_root();
7919    let lines = fields.auto_field_idents.iter().map(|(ident, column)| {
7920        let col_lit = column.as_str();
7921        quote! {
7922            _row_mut.#ident = #root::sql::sqlx::Row::try_get(
7923                _returning_row,
7924                #col_lit,
7925            )?;
7926        }
7927    });
7928    quote! { #( #lines )* }
7929}
7930
7931/// Emit `pub const id: …Id = …Id;` per field, inside the inherent impl.
7932fn column_const_tokens(module_ident: &syn::Ident, entries: &[ColumnEntry]) -> TokenStream2 {
7933    let lines = entries.iter().map(|e| {
7934        let ident = &e.ident;
7935        let col_ty = column_type_ident(ident);
7936        quote! {
7937            #[allow(non_upper_case_globals)]
7938            pub const #ident: #module_ident::#col_ty = #module_ident::#col_ty;
7939        }
7940    });
7941    quote! { #(#lines)* }
7942}
7943
7944/// Emit a hidden per-model module carrying one zero-sized type per field,
7945/// each with a `Column` impl pointing back at the model.
7946fn column_module_tokens(
7947    module_ident: &syn::Ident,
7948    struct_name: &syn::Ident,
7949    entries: &[ColumnEntry],
7950) -> TokenStream2 {
7951    let root = rustango_root();
7952    let items = entries.iter().map(|e| {
7953        let col_ty = column_type_ident(&e.ident);
7954        let value_ty = &e.value_ty;
7955        let name = &e.name;
7956        let column = &e.column;
7957        let field_type_tokens = &e.field_type_tokens;
7958        quote! {
7959            #[derive(::core::clone::Clone, ::core::marker::Copy)]
7960            pub struct #col_ty;
7961
7962            impl #root::core::Column for #col_ty {
7963                type Model = super::#struct_name;
7964                type Value = #value_ty;
7965                const NAME: &'static str = #name;
7966                const COLUMN: &'static str = #column;
7967                const FIELD_TYPE: #root::core::FieldType = #field_type_tokens;
7968            }
7969        }
7970    });
7971    quote! {
7972        #[doc(hidden)]
7973        #[allow(non_camel_case_types, non_snake_case)]
7974        pub mod #module_ident {
7975            // Re-import the parent scope so field types referencing
7976            // sibling models (e.g. `ForeignKey<Author>`) resolve
7977            // inside this submodule. Without this we'd hit
7978            // `proc_macro_derive_resolution_fallback` warnings.
7979            #[allow(unused_imports)]
7980            use super::*;
7981            #(#items)*
7982        }
7983    }
7984}
7985
7986fn column_type_ident(field_ident: &syn::Ident) -> syn::Ident {
7987    syn::Ident::new(&format!("{field_ident}_col"), field_ident.span())
7988}
7989
7990fn column_module_ident(struct_name: &syn::Ident) -> syn::Ident {
7991    syn::Ident::new(
7992        &format!("__rustango_cols_{struct_name}"),
7993        struct_name.span(),
7994    )
7995}
7996
7997fn from_row_impl_tokens(struct_name: &syn::Ident, from_row_inits: &[TokenStream2]) -> TokenStream2 {
7998    let root = rustango_root();
7999    // The Postgres impl is always emitted — every rustango build pulls in
8000    // sqlx-postgres via the default `postgres` feature. The MySQL impl is
8001    // routed through `#root::__impl_my_from_row!`, a cfg-gated
8002    // macro_rules whose body collapses to nothing when rustango is built
8003    // without the `mysql` feature. No user-facing feature shim required.
8004    //
8005    // The macro_rules pattern expects `[ field: expr, … ]` — we need to
8006    // re-shape `from_row_inits` (each token is `field: row.try_get(...)`)
8007    // back into a comma-separated list inside square brackets. Since each
8008    // entry is already in `field: expr` shape, the existing tokens slot in.
8009    quote! {
8010        #[cfg(feature = "postgres")]
8011        impl<'r> #root::sql::sqlx::FromRow<'r, #root::sql::sqlx::postgres::PgRow>
8012            for #struct_name
8013        {
8014            fn from_row(
8015                row: &'r #root::sql::sqlx::postgres::PgRow,
8016            ) -> ::core::result::Result<Self, #root::sql::sqlx::Error> {
8017                ::core::result::Result::Ok(Self {
8018                    #( #from_row_inits ),*
8019                })
8020            }
8021        }
8022
8023        #root::__impl_my_from_row!(#struct_name, |row| {
8024            #( #from_row_inits ),*
8025        });
8026
8027        #root::__impl_sqlite_from_row!(#struct_name, |row| {
8028            #( #from_row_inits ),*
8029        });
8030    }
8031}
8032
8033struct ContainerAttrs {
8034    table: Option<String>,
8035    display: Option<(String, proc_macro2::Span)>,
8036    /// Explicit Django-style app label from `#[rustango(app = "blog")]`.
8037    /// Recorded on the emitted `ModelSchema.app_label`. When unset,
8038    /// `ModelEntry::resolved_app_label()` infers from `module_path!()`
8039    /// at runtime — this attribute is the override for cases where
8040    /// the inference is wrong (e.g. a model that conceptually belongs
8041    /// to one app but is physically in another module).
8042    app: Option<String>,
8043    /// Django ModelAdmin-shape per-model knobs from
8044    /// `#[rustango(admin(...))]`. `None` when the user didn't write the
8045    /// attribute — the emitted `ModelSchema.admin` becomes `None` and
8046    /// admin code falls back to `AdminConfig::DEFAULT`.
8047    admin: Option<AdminAttrs>,
8048    /// Per-model audit configuration from `#[rustango(audit(...))]`.
8049    /// `None` when the model isn't audited — write paths emit no
8050    /// audit entries. When present, single-row writes capture
8051    /// before/after for the listed fields and bulk writes batch
8052    /// snapshots into one INSERT into `rustango_audit_log`.
8053    audit: Option<AuditAttrs>,
8054    /// `true` when `#[rustango(permissions)]` is present. Signals that
8055    /// `auto_create_permissions` should seed the four CRUD codenames for
8056    /// this model.
8057    permissions: bool,
8058    /// Many-to-many relations declared via
8059    /// `#[rustango(m2m(name = "tags", to = "app_tags", through = "post_tags",
8060    ///                 src = "post_id", dst = "tag_id"))]`.
8061    m2m: Vec<M2MAttr>,
8062    /// Polymorphic M2M relations declared via
8063    /// `#[rustango(generic_m2m(name = "tags", through = "taggables",
8064    ///   pk_column = "taggable_id", ct_column = "taggable_type",
8065    ///   related_column = "tag_id"))]` (issue #818).
8066    generic_m2m: Vec<GenericM2MAttr>,
8067    /// Composite indexes declared via
8068    /// `#[rustango(index("col1, col2"))]` or
8069    /// `#[rustango(index("col1, col2", unique, name = "my_idx"))]`.
8070    /// Single-column indexes from `#[rustango(index)]` on fields are
8071    /// accumulated here during field collection.
8072    indexes: Vec<IndexAttr>,
8073    /// Table-level CHECK constraints declared via
8074    /// `#[rustango(check(name = "…", expr = "…"))]`.
8075    checks: Vec<CheckAttr>,
8076    /// Table-level PG `EXCLUDE` constraints declared via
8077    /// `#[rustango(exclude(name = "…", using = "gist", elements =
8078    /// "col WITH op, col WITH op", where = "…"))]`. PG-only — the
8079    /// migration writer renders them on Postgres and skips with a
8080    /// warning on MySQL/SQLite. Issue #319.
8081    excludes: Vec<ExcludeAttr>,
8082    /// Composite (multi-column) FKs declared via
8083    /// `#[rustango(fk_composite(name = "…", to = "…", on = (…), from = (…)))]`.
8084    /// Sub-slice F.2 of the v0.15.0 ContentType plan.
8085    composite_fks: Vec<CompositeFkAttr>,
8086    /// Generic ("any model") FKs declared via
8087    /// `#[rustango(generic_fk(name = "…", ct_column = "…", pk_column = "…"))]`.
8088    /// Sub-slice F.4 of the v0.15.0 ContentType plan.
8089    generic_fks: Vec<GenericFkAttr>,
8090    /// Where this model lives in a tenancy deployment, declared via
8091    /// `#[rustango(scope = "registry")]` or `#[rustango(scope = "tenant")]`.
8092    /// Defaults to `"tenant"` when unset; `makemigrations` uses this
8093    /// to partition diff output between registry-scoped and
8094    /// tenant-scoped migration files.
8095    scope: Option<String>,
8096    /// Custom-Manager extension-trait name from
8097    /// `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8098    /// When set, the macro emits an empty `pub trait <name>: Sized {}`
8099    /// adjacent to the model so users can write
8100    /// `impl FooManagerExt for QuerySet<Foo> { fn published(self) -> Self ... }`
8101    /// and discover the convention from the model definition.
8102    manager_ext: Option<syn::Ident>,
8103    /// Extra QuerySet accessor names from
8104    /// `#[rustango(manager_fn = "active")]`. Issue #289 / T2.6.
8105    /// Each value adds a `pub fn <name>() -> QuerySet<Self>` next to
8106    /// the default `Self::objects()`. Multiple attributes allowed.
8107    manager_fns: Vec<syn::Ident>,
8108    /// Default ordering declared via `#[rustango(default_order =
8109    /// "-created_at, status")]`. Issue #291 / T2.5. Each entry is
8110    /// `(column_name, desc_flag, span_for_error_reporting)` — the
8111    /// `-` prefix means descending; the `+` prefix or no prefix means
8112    /// ascending.
8113    default_order: Vec<(String, bool, proc_macro2::Span)>,
8114    /// `true` when `#[rustango(view)]` is present. Issue #293 / T2.10.
8115    /// Routes the emitted schema's `is_view = true` so the migration
8116    /// snapshot skips this model (its underlying SQL view is operator-
8117    /// managed, not rustango-managed).
8118    is_view: bool,
8119    /// Django-shape `Meta.managed` from `#[rustango(managed = false)]`.
8120    /// Issue #321. Defaults to `true`; when explicitly set to `false`,
8121    /// the migration snapshot skips this model so `makemigrations` /
8122    /// `migrate` never emit `CREATE TABLE` / `ALTER TABLE` / `DROP
8123    /// TABLE` against it (operator-managed schema).
8124    managed: bool,
8125    /// Django-shape `Meta.base_manager_name` from
8126    /// `#[rustango(base_manager_name = "...")]`. Threaded into
8127    /// `ModelSchema::base_manager_name`. Declarative-only today.
8128    base_manager_name: Option<String>,
8129    /// Django-shape `Meta.order_with_respect_to = "parent_fk"` from
8130    /// `#[rustango(order_with_respect_to = "...")]`. Names the FK
8131    /// field this model's instances are ordered relative to.
8132    /// Declarative-only today; threaded onto
8133    /// `ModelSchema::order_with_respect_to`.
8134    order_with_respect_to: Option<String>,
8135    /// Django-shape `Meta.proxy = True` from `#[rustango(proxy)]` /
8136    /// `#[rustango(proxy = true)]`. Marks the model as a proxy that
8137    /// shares its DB table with another struct. Threaded into
8138    /// `ModelSchema::proxy` so future codegen can skip table-owning
8139    /// behavior for proxies.
8140    proxy: bool,
8141    /// Django-shape `Meta.required_db_features` from
8142    /// `#[rustango(required_db_features = "json_extract,window_functions")]`.
8143    /// Each comma-separated capability token surfaces on
8144    /// `ModelSchema::required_db_features` so `manage check --deploy`
8145    /// can warn when the active dialect lacks one.
8146    required_db_features: Vec<String>,
8147    /// Django-shape `Meta.required_db_vendor` from
8148    /// `#[rustango(required_db_vendor = "postgres|mysql|sqlite")]`.
8149    /// Normalized to the dialect name `manage check --deploy`
8150    /// compares against `Settings.database.backend`. Aliases
8151    /// (`postgresql` / `pg` / `mariadb` / `sqlite3`) accepted but
8152    /// stored under the canonical name.
8153    required_db_vendor: Option<String>,
8154    /// Django-shape `Meta.default_related_name` from
8155    /// `#[rustango(default_related_name = "...")]`. Threaded into
8156    /// `ModelSchema::default_related_name`. Reverse-relation accessor
8157    /// name to use when an FK / M2M field doesn't override it.
8158    /// Today rustango doesn't auto-emit reverse managers; the
8159    /// metadata is the foundation for that work.
8160    default_related_name: Option<String>,
8161    /// Django-shape `Meta.db_table_comment` (4.2+) from
8162    /// `#[rustango(db_table_comment = "...")]`. Threaded into
8163    /// `ModelSchema::db_table_comment` so the DDL writer attaches the
8164    /// comment to the underlying table (PG: `COMMENT ON TABLE`, MySQL:
8165    /// inline `COMMENT='...'`, SQLite: no-op).
8166    db_table_comment: Option<String>,
8167    /// Django-shape `Meta.get_latest_by` from
8168    /// `#[rustango(get_latest_by = "created_at")]` /
8169    /// `#[rustango(get_latest_by = "-priority")]`. Parsed into
8170    /// `(column, descending)` where `descending = true` when the
8171    /// attribute value starts with `-`. Threaded into
8172    /// `ModelSchema::get_latest_by`.
8173    get_latest_by: Option<(String, bool)>,
8174    /// Django-shape `Meta.permissions = [(codename, name), ...]`
8175    /// from `#[rustango(extra_permissions = "approve:Can approve,
8176    /// archive:Can archive")]`. Comma-separated `codename:label`
8177    /// pairs. Threaded into `ModelSchema::extra_permissions`.
8178    extra_permissions: Vec<(String, String)>,
8179    /// Django-shape `Meta.default_permissions` — which CRUD codenames
8180    /// (`"add"` / `"change"` / `"delete"` / `"view"`) the framework
8181    /// auto-creates. Empty `Vec` (default) means **all four** — matches
8182    /// Django's behavior when the operator omits the option. Set via
8183    /// `#[rustango(default_permissions = "view,change")]` to opt out.
8184    /// Validated at parse time; unknown actions fail with a span-pointing
8185    /// error.
8186    default_permissions: Vec<String>,
8187    /// `#[rustango(verbose_name = "blog post")]` — Django-shape
8188    /// human-readable singular label for the model. Threaded into
8189    /// `ModelSchema::verbose_name` so admin section headers /
8190    /// breadcrumbs / "Add X" buttons can prefer the friendly caption
8191    /// over the Rust struct identifier.
8192    verbose_name: Option<String>,
8193    /// `#[rustango(verbose_name_plural = "blog posts")]` — explicit
8194    /// plural form. Threaded into `ModelSchema::verbose_name_plural`.
8195    /// When unset, `display_label_plural()` auto-suffixes `s`.
8196    verbose_name_plural: Option<String>,
8197    /// Eloquent-shape **global scopes** from `#[rustango(global_scope(name
8198    /// = "...", apply = path::to::fn))]` — issue #820. Each entry pairs
8199    /// a name (used by `QuerySet::without_global_scope`) with a
8200    /// `fn() -> WhereExpr` path that the macro emits into
8201    /// `ModelSchema::global_scopes`. Multiple attributes accumulate.
8202    global_scopes: Vec<GlobalScopeAttr>,
8203    /// Eloquent `hasManyThrough` / `hasOneThrough` declarations from
8204    /// `#[rustango(through(name, far, far_fk_column, intermediate,
8205    /// intermediate_fk_column, intermediate_pk_column))]` — issue
8206    /// [#817](https://github.com/ujeenet/rustango/issues/817). Each
8207    /// entry emits an inherent `<name>_through(&self)` accessor that
8208    /// returns a `QuerySet<Far>` filtered via a correlated subquery
8209    /// (`WHERE far_fk_column IN (SELECT intermediate_pk_column FROM
8210    /// intermediate WHERE intermediate_fk_column = <my_pk>)`).
8211    through_relations: Vec<ThroughAttr>,
8212    /// Eloquent `whereHas` / `whereDoesntHave` declarations from
8213    /// `#[rustango(reverse_has(name, child, child_fk_column))]` —
8214    /// issue [#830](https://github.com/ujeenet/rustango/issues/830).
8215    /// Each entry emits two associated functions on the parent —
8216    /// `<name>_exists_expr()` and `<name>_not_exists_expr()` —
8217    /// returning a `WhereExpr::Exists` / `WhereExpr::NotExists`
8218    /// over a correlated subquery against the child table. Users
8219    /// drop the result into `QuerySet::where_raw(...)`.
8220    reverse_has_relations: Vec<ReverseHasAttr>,
8221    /// `#[rustango(generic_has(...))]` reverse generic-FK declarations —
8222    /// issue #830. Each emits a `Model::generic_reverse_relations()`
8223    /// entry so the relation-existence family resolves polymorphic
8224    /// children by name.
8225    generic_has_relations: Vec<GenericHasAttr>,
8226}
8227
8228/// Parsed `#[rustango(global_scope(name = "...", apply = fn_path))]`
8229/// declaration. Each entry becomes one `core::GlobalScope` in the
8230/// emitted schema literal; `apply` resolves at macro-expand time
8231/// against the consumer's scope so the function must be in scope at
8232/// the use site. Issue #820.
8233struct GlobalScopeAttr {
8234    name: String,
8235    apply: syn::Path,
8236}
8237
8238/// Parsed `#[rustango(through(...))]` declaration. Issue
8239/// [#817](https://github.com/ujeenet/rustango/issues/817) — Eloquent
8240/// `hasManyThrough` / `hasOneThrough` parity.
8241///
8242/// `Country hasManyThrough Post via User` declares as:
8243///
8244/// ```ignore
8245/// #[rustango(through(
8246///     name                   = "posts",
8247///     far                    = "Post",
8248///     far_fk_column          = "author_id",
8249///     intermediate           = "User",
8250///     intermediate_fk_column = "country_id",
8251/// ))]
8252/// struct Country { ... }
8253/// ```
8254///
8255/// The macro emits `Country::posts_through(&self) -> QuerySet<Post>`
8256/// which returns a queryset filtered via a correlated subquery —
8257/// `Post WHERE author_id IN (SELECT id FROM tr_user WHERE country_id
8258/// = <my_pk>)`. The returned `QuerySet<Post>` is **chainable**:
8259/// `.filter()` / `.order_by()` / `.limit()` etc. compose normally
8260/// because the subquery lives inside a `WhereExpr::InSubquery` node
8261/// and the outer queryset's pending list stays empty.
8262///
8263/// All four required arguments use **SQL column / table names**
8264/// (not Rust field names) to sidestep the multi-hop-filter substrate
8265/// gap. Once that substrate lands, a higher-level Rust-field-name
8266/// shorthand can be added without breaking this surface.
8267struct ThroughAttr {
8268    /// Accessor method name. `name = "posts"` → emits `posts_through()`.
8269    name: String,
8270    /// Far model type identifier. `far = "Post"` → returns
8271    /// `QuerySet<Post>`. Resolved verbatim against the scope where
8272    /// the derive expands.
8273    far: syn::Ident,
8274    /// SQL column on the far model's table that references the
8275    /// intermediate model's primary key. For `Post`'s
8276    /// `author: ForeignKey<User>` the column is `"author_id"` (rustango's
8277    /// default `<field>_id` convention) or whatever the user
8278    /// declared via `#[rustango(db_column = "...")]`.
8279    far_fk_column: String,
8280    /// Intermediate model type identifier. Needed to look up its
8281    /// `SCHEMA` so the subquery's `FROM` clause points at the
8282    /// intermediate table. `intermediate = "User"`.
8283    intermediate: syn::Ident,
8284    /// SQL column on the intermediate's table that references the
8285    /// source (this) model's primary key. For `User`'s
8286    /// `country: ForeignKey<Country>` the column is `"country_id"`.
8287    intermediate_fk_column: String,
8288    /// SQL primary-key column on the intermediate's table — the
8289    /// column the subquery projects. Optional; defaults to `"id"`
8290    /// (rustango's default PK column name). Override when the
8291    /// intermediate declares a custom PK column.
8292    intermediate_pk_column: String,
8293}
8294
8295/// Parsed `#[rustango(reverse_has(name = "...", child = "...",
8296/// child_fk_column = "..."))]` declaration. Issue
8297/// [#830](https://github.com/ujeenet/rustango/issues/830) — Eloquent
8298/// `whereHas` / `whereDoesntHave` parity.
8299///
8300/// `Post hasMany Comment` declares as:
8301///
8302/// ```ignore
8303/// #[rustango(reverse_has(
8304///     name             = "comments",
8305///     child            = "Comment",
8306///     child_fk_column  = "post_id",
8307/// ))]
8308/// struct Post { ... }
8309/// ```
8310///
8311/// The macro emits two associated functions on `Post`:
8312///
8313/// - `Post::comments_exists_expr() -> WhereExpr` — yields
8314///   `EXISTS (SELECT … FROM comment WHERE comment.post_id =
8315///   <outer>.<self_pk_column>)`.
8316/// - `Post::comments_not_exists_expr() -> WhereExpr` — same shape
8317///   but `NOT EXISTS`, the `whereDoesntHave` analog.
8318///
8319/// User code:
8320///
8321/// ```ignore
8322/// // Posts with at least one comment:
8323/// Post::objects().where_raw(Post::comments_exists_expr()).fetch(&pool)
8324/// // Posts with no comments:
8325/// Post::objects().where_raw(Post::comments_not_exists_expr()).fetch(&pool)
8326/// ```
8327///
8328/// As with #817, all column / table identifiers are **SQL names**
8329/// (not Rust field names) so the substrate is independent of the
8330/// outstanding multi-hop filter resolver gap. The emitted
8331/// `Expr::OuterRef("…")` resolves to the outer queryset's table at
8332/// SQL-emit time via the writer's scope stack.
8333struct ReverseHasAttr {
8334    /// Accessor name. `name = "comments"` → emits
8335    /// `comments_exists_expr()` + `comments_not_exists_expr()`.
8336    name: String,
8337    /// Child model type identifier. `child = "Comment"` — needed to
8338    /// look up the child's `SCHEMA` so the subquery's `FROM` clause
8339    /// points at the child table.
8340    child: syn::Ident,
8341    /// SQL column on the child's table that references this model's
8342    /// primary key. For `Comment`'s `post: ForeignKey<Post>` the
8343    /// column is `"post_id"`.
8344    child_fk_column: String,
8345    /// SQL primary-key column on **this** model's table — the column
8346    /// the `OuterRef` resolves to. Optional; defaults to `"id"`
8347    /// (rustango's default PK column name).
8348    self_pk_column: String,
8349}
8350
8351/// Parsed `#[rustango(generic_has(name, child, ct_column, pk_column
8352/// [, self_pk_column]))]` — the reverse generic-FK (GFK) arm of the
8353/// relation-existence family (issue #830). The child is a polymorphic,
8354/// content-type-discriminated model; emits a `Model::
8355/// generic_reverse_relations()` entry the queryset resolves by name.
8356struct GenericHasAttr {
8357    /// Accessor name, e.g. `name = "tags"`.
8358    name: String,
8359    /// Child model type identifier (`child = "Tag"`) — looked up for its
8360    /// `SCHEMA` so the subquery's `FROM` points at the child table.
8361    child: syn::Ident,
8362    /// Column on the child table holding the parent's content-type id.
8363    /// Optional; defaults to `"content_type_id"`.
8364    ct_column: String,
8365    /// Column on the child table holding the parent's PK value.
8366    /// Optional; defaults to `"object_pk"`.
8367    pk_column: String,
8368    /// SQL primary-key column on **this** (parent) model's table.
8369    /// Optional; defaults to `"id"`.
8370    self_pk_column: String,
8371}
8372
8373/// Parsed form of one index declaration (field-level or container-level).
8374struct IndexAttr {
8375    /// Index name; auto-derived when `None` at parse time.
8376    name: Option<String>,
8377    /// Column names in the index.
8378    columns: Vec<String>,
8379    /// `true` for `CREATE UNIQUE INDEX`.
8380    unique: bool,
8381    /// Access method token (`"btree"`, `"gin"`, `"gist"`, `"brin"`,
8382    /// `"spgist"`, `"hash"`, `"bloom"`). Issue #34. Defaults to
8383    /// `"btree"` when the attribute is absent — the DDL writer omits
8384    /// the `USING` clause and the backend uses its own default
8385    /// (btree on every supported dialect).
8386    method: String,
8387    /// Optional `WHERE <expr>` clause for partial indexes. Issue #265 /
8388    /// T1.3. Set via `#[rustango(unique_when(columns = "...",
8389    /// condition = "...", name = "..."))]`. `None` for plain indexes.
8390    where_clause: Option<String>,
8391    /// Django `Index(fields=..., include=[...])` covering-index
8392    /// columns (PG 11+ `INCLUDE (...)` clause). Empty `Vec` (the
8393    /// default) means "no covering columns".
8394    include: Vec<String>,
8395}
8396
8397/// Parsed form of one `#[rustango(check(name = "…", expr = "…"))]` declaration.
8398struct CheckAttr {
8399    name: String,
8400    expr: String,
8401}
8402
8403/// Parsed form of one `#[rustango(exclude(name = "…", using = "gist",
8404/// elements = "col WITH op, col WITH op", where = "…"))]` declaration.
8405/// PG-only — surfaced on every backend in the macro emit; the migration
8406/// writer skips the op on MySQL/SQLite. Issue #319.
8407struct ExcludeAttr {
8408    /// Constraint name (free-form Rust identifier).
8409    name: String,
8410    /// Index method — `"gist"` (default), `"btree_gist"`, `"spgist"`.
8411    using: String,
8412    /// Comma-separated `(column, operator)` pairs, in declaration
8413    /// order. Parsed from `"col WITH op, col WITH op"`.
8414    elements: Vec<(String, String)>,
8415    /// Optional `WHERE` predicate (raw SQL).
8416    where_clause: Option<String>,
8417}
8418
8419/// Parsed form of one `#[rustango(fk_composite(name = "audit_target",
8420/// to = "rustango_audit_log", on = ("entity_table", "entity_pk"),
8421/// from = ("table_name", "row_pk")))]` declaration. Sub-slice F.2 of
8422/// the v0.15.0 ContentType plan — multi-column foreign keys live on
8423/// the model, not the field.
8424struct CompositeFkAttr {
8425    /// Logical relation name (free-form Rust identifier).
8426    name: String,
8427    /// SQL table name of the target.
8428    to: String,
8429    /// Source-side column names, in declaration order.
8430    from: Vec<String>,
8431    /// Target-side column names, same length / order as `from`.
8432    on: Vec<String>,
8433}
8434
8435/// Parsed form of one `#[rustango(generic_fk(name = "target",
8436/// ct_column = "content_type_id", pk_column = "object_pk"))]`
8437/// declaration. Sub-slice F.4 of the v0.15.0 ContentType plan —
8438/// generic ("any model") FKs live on the model, not the field.
8439struct GenericFkAttr {
8440    /// Logical relation name (free-form Rust identifier).
8441    name: String,
8442    /// Source-side column carrying the `content_type_id` value.
8443    ct_column: String,
8444    /// Source-side column carrying the target row's primary key.
8445    pk_column: String,
8446}
8447
8448/// Parsed form of one `#[rustango(m2m(...))]` declaration.
8449struct M2MAttr {
8450    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8451    name: String,
8452    /// Target table (e.g. `"app_tags"`).
8453    to: String,
8454    /// Junction table (e.g. `"post_tags"`).
8455    through: String,
8456    /// Source FK column in the junction table (e.g. `"post_id"`).
8457    src: String,
8458    /// Destination FK column in the junction table (e.g. `"tag_id"`).
8459    dst: String,
8460    /// Whether the migration writer should auto-create the junction
8461    /// table. Default `true`. Set `auto_create = false` (#324) when
8462    /// the operator declares the through table as its own
8463    /// `#[derive(Model)]` struct with extra columns.
8464    auto_create: bool,
8465}
8466
8467/// Parsed form of one `#[rustango(generic_m2m(...))]` declaration —
8468/// polymorphic many-to-many (Eloquent `morphToMany`, issue #818). The
8469/// junction carries a ContentType discriminator so unrelated models
8470/// share one pivot + related set.
8471struct GenericM2MAttr {
8472    /// Accessor suffix: `tags` → generates `tags_m2m()`.
8473    name: String,
8474    /// Polymorphic junction table (e.g. `"taggables"`).
8475    through: String,
8476    /// Junction column holding the owning instance PK (e.g. `"taggable_id"`).
8477    pk_column: String,
8478    /// Junction column holding the owning model's `content_type_id`
8479    /// discriminator (e.g. `"taggable_type"`).
8480    ct_column: String,
8481    /// Junction column holding the related model PK (e.g. `"tag_id"`).
8482    related_column: String,
8483}
8484
8485/// Parsed shape of `#[rustango(audit(track = "name, body", source =
8486/// "user"))]`. `track` is a comma-separated list of field names whose
8487/// before/after values land in the JSONB `changes` column. `source`
8488/// is informational only — it pins a default source when the model
8489/// is written outside any `audit::with_source(...)` scope (rare).
8490#[derive(Default)]
8491struct AuditAttrs {
8492    /// Field names to capture in the `changes` JSONB. Validated
8493    /// against declared scalar fields at compile time. Empty means
8494    /// "track every scalar field" — Django's audit-everything default.
8495    track: Option<(Vec<String>, proc_macro2::Span)>,
8496}
8497
8498/// Parsed shape of `#[rustango(admin(list_display = "…", search_fields =
8499/// "…", list_per_page = N, ordering = "…"))]`. Field-name lists are
8500/// comma-separated strings; we validate each ident against the model's
8501/// declared fields at compile time.
8502#[derive(Default)]
8503struct AdminAttrs {
8504    list_display: Option<(Vec<String>, proc_macro2::Span)>,
8505    search_fields: Option<(Vec<String>, proc_macro2::Span)>,
8506    list_per_page: Option<usize>,
8507    ordering: Option<(Vec<(String, bool)>, proc_macro2::Span)>,
8508    readonly_fields: Option<(Vec<String>, proc_macro2::Span)>,
8509    list_filter: Option<(Vec<String>, proc_macro2::Span)>,
8510    /// Bulk action names. No field-validation against model fields —
8511    /// these are action handlers, not column references.
8512    actions: Option<(Vec<String>, proc_macro2::Span)>,
8513    /// Form fieldsets — `Vec<(title, [field_names])>`. Pipe-separated
8514    /// sections, comma-separated fields per section, optional
8515    /// `Title:` prefix. Empty title omits the `<legend>`.
8516    fieldsets: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8517    /// `admin(list_display_links = "title")` — Django-shape. Names
8518    /// from `list_display` whose cells should link to detail/edit.
8519    /// Issue #350.
8520    list_display_links: Option<(Vec<String>, proc_macro2::Span)>,
8521    /// `admin(search_help_text = "...")` — Django-shape. Short
8522    /// caption rendered beside the admin list view's search box.
8523    /// Issue #353.
8524    search_help_text: Option<String>,
8525    /// `admin(actions_on_top = false)` — Django-shape. Hides the
8526    /// action-bar above the table. Default `true`. Issue #354.
8527    actions_on_top: Option<bool>,
8528    /// `admin(actions_on_bottom = true)` — Django-shape. Renders an
8529    /// additional action-bar below the table. Default `false`.
8530    /// Issue #354.
8531    actions_on_bottom: Option<bool>,
8532    /// `admin(date_hierarchy = "created_at")` — Django-shape. Name of
8533    /// a date / datetime field whose values render as a clickable
8534    /// year / month / day drill-down strip above the list table.
8535    /// Empty / unset disables the strip. Issue #355.
8536    date_hierarchy: Option<String>,
8537    /// `admin(prepopulated_fields = "slug:title")` — Django-shape.
8538    /// Each entry is `target:source[+source2]`; multiple entries are
8539    /// comma-separated, e.g. `"slug:title,short_code:section+title"`.
8540    /// The admin change-form emits JS that slugifies the source values
8541    /// into the target field on every keystroke. Issue #356.
8542    prepopulated_fields: Option<(Vec<(String, Vec<String>)>, proc_macro2::Span)>,
8543    /// `admin(raw_id_fields = "parent, owner")` — Django-shape. Names
8544    /// of FK fields whose change-form widget renders a lookup link
8545    /// next to the input. Issue #357.
8546    raw_id_fields: Option<(Vec<String>, proc_macro2::Span)>,
8547    /// `admin(autocomplete_fields = "author_id")` — Django-shape.
8548    /// Names of FK fields whose change-form widget renders an
8549    /// Ajax-driven typeahead populated from a `__autocomplete`
8550    /// endpoint on the target model. Issue #358.
8551    autocomplete_fields: Option<(Vec<String>, proc_macro2::Span)>,
8552    /// `admin(list_select_related = "all" | "none" | "author, …")`
8553    /// — Django-shape. Tunes the admin list view's FK auto-JOIN
8554    /// policy. Default `"all"` matches rustango's join-everything
8555    /// behavior; `"none"` opts out; CSV restricts. Issue #352.
8556    list_select_related: Option<String>,
8557    /// `admin(formfield_overrides = "field:widget, field2:widget2")` —
8558    /// Django-shape. Each entry is `field_name:widget_name`; multiple
8559    /// entries comma-separated. Empty / unset → no overrides. The
8560    /// list of widget names supported is documented on
8561    /// `AdminConfig::formfield_overrides`. Issue #359.
8562    formfield_overrides: Option<(Vec<(String, String)>, proc_macro2::Span)>,
8563}
8564
8565fn parse_container_attrs(input: &DeriveInput) -> syn::Result<ContainerAttrs> {
8566    let mut out = ContainerAttrs {
8567        table: None,
8568        display: None,
8569        app: None,
8570        admin: None,
8571        audit: None,
8572        // Default `permissions = true` so every `#[derive(Model)]`
8573        // gets the four CRUD codenames seeded by `auto_create_permissions`
8574        // and is visible to non-superusers in the tenant admin without
8575        // manual per-model annotation. Models that intentionally don't
8576        // want permission rows (registry-internal types, framework
8577        // tables operators shouldn't manage directly) opt out via
8578        // `#[rustango(permissions = false)]`. v0.27.2 — fixes the
8579        // out-of-the-box admin invisibility regression (#62).
8580        permissions: true,
8581        m2m: Vec::new(),
8582        generic_m2m: Vec::new(),
8583        indexes: Vec::new(),
8584        checks: Vec::new(),
8585        excludes: Vec::new(),
8586        composite_fks: Vec::new(),
8587        generic_fks: Vec::new(),
8588        scope: None,
8589        manager_ext: None,
8590        manager_fns: Vec::new(),
8591        default_order: Vec::new(),
8592        is_view: false,
8593        managed: true,
8594        verbose_name: None,
8595        verbose_name_plural: None,
8596        base_manager_name: None,
8597        order_with_respect_to: None,
8598        proxy: false,
8599        required_db_features: Vec::new(),
8600        required_db_vendor: None,
8601        default_related_name: None,
8602        db_table_comment: None,
8603        get_latest_by: None,
8604        extra_permissions: Vec::new(),
8605        default_permissions: Vec::new(),
8606        global_scopes: Vec::new(),
8607        through_relations: Vec::new(),
8608        reverse_has_relations: Vec::new(),
8609        generic_has_relations: Vec::new(),
8610    };
8611    for attr in &input.attrs {
8612        if !attr.path().is_ident("rustango") {
8613            continue;
8614        }
8615        attr.parse_nested_meta(|meta| {
8616            if meta.path.is_ident("table") {
8617                let s: LitStr = meta.value()?.parse()?;
8618                let name = s.value();
8619                // v0.27.3 (#65) — macro-time guard against table names
8620                // that compile but break SQL downstream. Hyphens are
8621                // the common footgun: PostgreSQL accepts a quoted
8622                // `"intermediate-region"` in CREATE TABLE, but the
8623                // FK / index name derivation in `migrate::ddl`
8624                // emits `intermediate-region_field_fkey` unquoted,
8625                // which then fails the SQL parser. Same shape rule
8626                // as Postgres regular identifiers so the safe path
8627                // is the only path.
8628                validate_table_name(&name, s.span())?;
8629                out.table = Some(name);
8630                return Ok(());
8631            }
8632            if meta.path.is_ident("display") {
8633                let s: LitStr = meta.value()?.parse()?;
8634                out.display = Some((s.value(), s.span()));
8635                return Ok(());
8636            }
8637            if meta.path.is_ident("app") {
8638                let s: LitStr = meta.value()?.parse()?;
8639                out.app = Some(s.value());
8640                return Ok(());
8641            }
8642            if meta.path.is_ident("scope") {
8643                let s: LitStr = meta.value()?.parse()?;
8644                let val = s.value();
8645                if !matches!(val.to_ascii_lowercase().as_str(), "registry" | "tenant") {
8646                    return Err(meta.error(format!(
8647                        "`scope` must be \"registry\" or \"tenant\", got {val:?}"
8648                    )));
8649                }
8650                out.scope = Some(val);
8651                return Ok(());
8652            }
8653            if meta.path.is_ident("admin") {
8654                let mut admin = AdminAttrs::default();
8655                meta.parse_nested_meta(|inner| {
8656                    if inner.path.is_ident("list_display") {
8657                        let s: LitStr = inner.value()?.parse()?;
8658                        admin.list_display =
8659                            Some((split_field_list(&s.value()), s.span()));
8660                        return Ok(());
8661                    }
8662                    if inner.path.is_ident("search_fields") {
8663                        let s: LitStr = inner.value()?.parse()?;
8664                        admin.search_fields =
8665                            Some((split_field_list(&s.value()), s.span()));
8666                        return Ok(());
8667                    }
8668                    if inner.path.is_ident("readonly_fields") {
8669                        let s: LitStr = inner.value()?.parse()?;
8670                        admin.readonly_fields =
8671                            Some((split_field_list(&s.value()), s.span()));
8672                        return Ok(());
8673                    }
8674                    if inner.path.is_ident("list_per_page") {
8675                        let lit: syn::LitInt = inner.value()?.parse()?;
8676                        admin.list_per_page = Some(lit.base10_parse::<usize>()?);
8677                        return Ok(());
8678                    }
8679                    if inner.path.is_ident("ordering") {
8680                        let s: LitStr = inner.value()?.parse()?;
8681                        admin.ordering = Some((
8682                            parse_ordering_list(&s.value()),
8683                            s.span(),
8684                        ));
8685                        return Ok(());
8686                    }
8687                    if inner.path.is_ident("list_filter") {
8688                        let s: LitStr = inner.value()?.parse()?;
8689                        admin.list_filter =
8690                            Some((split_field_list(&s.value()), s.span()));
8691                        return Ok(());
8692                    }
8693                    if inner.path.is_ident("actions") {
8694                        let s: LitStr = inner.value()?.parse()?;
8695                        admin.actions =
8696                            Some((split_field_list(&s.value()), s.span()));
8697                        return Ok(());
8698                    }
8699                    if inner.path.is_ident("fieldsets") {
8700                        let s: LitStr = inner.value()?.parse()?;
8701                        admin.fieldsets =
8702                            Some((parse_fieldset_list(&s.value()), s.span()));
8703                        return Ok(());
8704                    }
8705                    if inner.path.is_ident("list_display_links") {
8706                        let s: LitStr = inner.value()?.parse()?;
8707                        admin.list_display_links =
8708                            Some((split_field_list(&s.value()), s.span()));
8709                        return Ok(());
8710                    }
8711                    if inner.path.is_ident("search_help_text") {
8712                        let s: LitStr = inner.value()?.parse()?;
8713                        admin.search_help_text = Some(s.value());
8714                        return Ok(());
8715                    }
8716                    if inner.path.is_ident("actions_on_top") {
8717                        let lit: syn::LitBool = inner.value()?.parse()?;
8718                        admin.actions_on_top = Some(lit.value);
8719                        return Ok(());
8720                    }
8721                    if inner.path.is_ident("actions_on_bottom") {
8722                        let lit: syn::LitBool = inner.value()?.parse()?;
8723                        admin.actions_on_bottom = Some(lit.value);
8724                        return Ok(());
8725                    }
8726                    if inner.path.is_ident("date_hierarchy") {
8727                        let s: LitStr = inner.value()?.parse()?;
8728                        admin.date_hierarchy = Some(s.value());
8729                        return Ok(());
8730                    }
8731                    if inner.path.is_ident("prepopulated_fields") {
8732                        let s: LitStr = inner.value()?.parse()?;
8733                        admin.prepopulated_fields =
8734                            Some((parse_prepopulated_list(&s.value()), s.span()));
8735                        return Ok(());
8736                    }
8737                    if inner.path.is_ident("raw_id_fields") {
8738                        let s: LitStr = inner.value()?.parse()?;
8739                        admin.raw_id_fields =
8740                            Some((split_field_list(&s.value()), s.span()));
8741                        return Ok(());
8742                    }
8743                    if inner.path.is_ident("autocomplete_fields") {
8744                        let s: LitStr = inner.value()?.parse()?;
8745                        admin.autocomplete_fields =
8746                            Some((split_field_list(&s.value()), s.span()));
8747                        return Ok(());
8748                    }
8749                    if inner.path.is_ident("list_select_related") {
8750                        let s: LitStr = inner.value()?.parse()?;
8751                        admin.list_select_related = Some(s.value());
8752                        return Ok(());
8753                    }
8754                    if inner.path.is_ident("formfield_overrides") {
8755                        let s: LitStr = inner.value()?.parse()?;
8756                        admin.formfield_overrides =
8757                            Some((parse_formfield_overrides(&s.value()), s.span()));
8758                        return Ok(());
8759                    }
8760                    Err(inner.error(
8761                        "unknown admin attribute (supported: \
8762                         `list_display`, `list_display_links`, \
8763                         `search_fields`, `search_help_text`, \
8764                         `readonly_fields`, \
8765                         `list_filter`, `list_per_page`, `ordering`, `actions`, \
8766                         `actions_on_top`, `actions_on_bottom`, \
8767                         `date_hierarchy`, \
8768                         `prepopulated_fields`, \
8769                         `raw_id_fields`, \
8770                         `autocomplete_fields`, \
8771                         `list_select_related`, \
8772                         `formfield_overrides`, \
8773                         `fieldsets`)",
8774                    ))
8775                })?;
8776                out.admin = Some(admin);
8777                return Ok(());
8778            }
8779            if meta.path.is_ident("manager") {
8780                // `#[rustango(manager(ext = "FooManagerExt"))]`. Issue #271 / T1.9.
8781                // Stretch `from_queryset = "..."` (Django Manager.from_queryset
8782                // shape) is left as a follow-up — the issue's primary
8783                // acceptance is the `ext = ...` trait emission.
8784                meta.parse_nested_meta(|inner| {
8785                    if inner.path.is_ident("ext") {
8786                        let s: LitStr = inner.value()?.parse()?;
8787                        let name = s.value();
8788                        if name.is_empty() {
8789                            return Err(inner.error("manager(ext = \"...\") cannot be empty"));
8790                        }
8791                        out.manager_ext =
8792                            Some(syn::Ident::new(&name, s.span()));
8793                        return Ok(());
8794                    }
8795                    Err(inner.error(
8796                        "unknown manager attribute (supported: `ext = \"TraitName\"`)",
8797                    ))
8798                })?;
8799                return Ok(());
8800            }
8801            if meta.path.is_ident("manager_fn") {
8802                // `#[rustango(manager_fn = "active")]` — issue #289 / T2.6.
8803                // Adds a `pub fn <name>() -> QuerySet<Self>` accessor
8804                // next to the default `Self::objects()`. Multiple
8805                // attributes accumulate.
8806                let s: LitStr = meta.value()?.parse()?;
8807                let name = s.value();
8808                if name.is_empty() {
8809                    return Err(meta.error("`manager_fn = \"...\"` cannot be empty"));
8810                }
8811                if name == "objects" {
8812                    return Err(meta.error(
8813                        "`manager_fn = \"objects\"` collides with the default \
8814                         accessor — pick a different name",
8815                    ));
8816                }
8817                let ident = syn::Ident::new(&name, s.span());
8818                if out.manager_fns.iter().any(|prev| *prev == ident) {
8819                    return Err(meta.error(format!(
8820                        "duplicate `manager_fn = \"{name}\"`"
8821                    )));
8822                }
8823                out.manager_fns.push(ident);
8824                return Ok(());
8825            }
8826            if meta.path.is_ident("default_order") {
8827                // `#[rustango(default_order = "-created_at, status")]`
8828                // — issue #291 / T2.5. Comma-separated list; `-prefix`
8829                // means descending, `+prefix` or bare name means ascending.
8830                // Per-query opt-in via `QuerySet::with_default_order()`.
8831                let s: LitStr = meta.value()?.parse()?;
8832                let raw = s.value();
8833                let span = s.span();
8834                let mut parsed: Vec<(String, bool, proc_macro2::Span)> =
8835                    Vec::new();
8836                for entry in raw.split(',') {
8837                    let trimmed = entry.trim();
8838                    if trimmed.is_empty() {
8839                        return Err(syn::Error::new(
8840                            span,
8841                            "`default_order = \"...\"` has an empty entry — \
8842                             check for a stray comma",
8843                        ));
8844                    }
8845                    let (desc, name) = if let Some(rest) = trimmed.strip_prefix('-') {
8846                        (true, rest.trim().to_owned())
8847                    } else if let Some(rest) = trimmed.strip_prefix('+') {
8848                        (false, rest.trim().to_owned())
8849                    } else {
8850                        (false, trimmed.to_owned())
8851                    };
8852                    if name.is_empty() {
8853                        return Err(syn::Error::new(
8854                            span,
8855                            "`default_order` entry has no column name after the prefix",
8856                        ));
8857                    }
8858                    if parsed.iter().any(|(n, _, _)| *n == name) {
8859                        return Err(syn::Error::new(
8860                            span,
8861                            format!("duplicate column `{name}` in `default_order`"),
8862                        ));
8863                    }
8864                    parsed.push((name, desc, span));
8865                }
8866                if parsed.is_empty() {
8867                    return Err(syn::Error::new(
8868                        span,
8869                        "`default_order = \"...\"` cannot be empty",
8870                    ));
8871                }
8872                out.default_order = parsed;
8873                return Ok(());
8874            }
8875            if meta.path.is_ident("global_scope") {
8876                // `#[rustango(global_scope(name = "active", apply =
8877                //  path::to::fn))]` — issue #820. The apply function
8878                // path resolves at macro-expand time in the consumer's
8879                // scope; the macro re-emits it verbatim into the
8880                // `ModelSchema::global_scopes` slice literal.
8881                let span = meta.path.span();
8882                let mut scope_name: Option<String> = None;
8883                let mut apply_path: Option<syn::Path> = None;
8884                meta.parse_nested_meta(|inner| {
8885                    if inner.path.is_ident("name") {
8886                        let s: LitStr = inner.value()?.parse()?;
8887                        let raw = s.value();
8888                        if raw.trim().is_empty() {
8889                            return Err(syn::Error::new(
8890                                s.span(),
8891                                "`global_scope(name = \"...\")` must not be empty",
8892                            ));
8893                        }
8894                        scope_name = Some(raw);
8895                        return Ok(());
8896                    }
8897                    if inner.path.is_ident("apply") {
8898                        let p: syn::Path = inner.value()?.parse()?;
8899                        apply_path = Some(p);
8900                        return Ok(());
8901                    }
8902                    Err(inner.error(
8903                        "unknown `global_scope` attribute (supported: \
8904                         `name`, `apply`)",
8905                    ))
8906                })?;
8907                let Some(name) = scope_name else {
8908                    return Err(syn::Error::new(
8909                        span,
8910                        "`global_scope` requires `name = \"...\"`",
8911                    ));
8912                };
8913                let Some(apply) = apply_path else {
8914                    return Err(syn::Error::new(
8915                        span,
8916                        "`global_scope` requires `apply = fn_path`",
8917                    ));
8918                };
8919                if out.global_scopes.iter().any(|s| s.name == name) {
8920                    return Err(syn::Error::new(
8921                        span,
8922                        format!(
8923                            "duplicate global scope name `{name}` — \
8924                             pick a unique identifier so \
8925                             `QuerySet::without_global_scope(\"{name}\")` \
8926                             is unambiguous"
8927                        ),
8928                    ));
8929                }
8930                out.global_scopes.push(GlobalScopeAttr { name, apply });
8931                return Ok(());
8932            }
8933            if meta.path.is_ident("through") {
8934                // `#[rustango(through(name = "posts", far = "Post",
8935                //  far_fk_column = "author_id", intermediate = "User",
8936                //  intermediate_fk_column = "country_id"
8937                //  [, intermediate_pk_column = "id"]))]` — issue #817.
8938                let span = meta.path.span();
8939                let mut accessor_name: Option<String> = None;
8940                let mut far_ident: Option<syn::Ident> = None;
8941                let mut far_fk_column: Option<String> = None;
8942                let mut intermediate_ident: Option<syn::Ident> = None;
8943                let mut intermediate_fk_column: Option<String> = None;
8944                let mut intermediate_pk_column: Option<String> = None;
8945                fn parse_nonempty_string(
8946                    inner: &syn::meta::ParseNestedMeta<'_>,
8947                    field: &str,
8948                ) -> syn::Result<String> {
8949                    let s: LitStr = inner.value()?.parse()?;
8950                    let raw = s.value();
8951                    let trimmed = raw.trim();
8952                    if trimmed.is_empty() {
8953                        return Err(syn::Error::new(
8954                            s.span(),
8955                            format!("`through({field} = \"...\")` must not be empty"),
8956                        ));
8957                    }
8958                    Ok(trimmed.to_owned())
8959                }
8960                meta.parse_nested_meta(|inner| {
8961                    if inner.path.is_ident("name") {
8962                        accessor_name = Some(parse_nonempty_string(&inner, "name")?);
8963                        return Ok(());
8964                    }
8965                    if inner.path.is_ident("far") {
8966                        // Far model name accepted as a string literal
8967                        // so the attribute fits inside the existing
8968                        // `parse_nested_meta` shape; parsed back into a
8969                        // `syn::Ident` so the emitted accessor can
8970                        // reference the type directly.
8971                        let s: LitStr = inner.value()?.parse()?;
8972                        let raw = s.value();
8973                        let trimmed = raw.trim();
8974                        if trimmed.is_empty() {
8975                            return Err(syn::Error::new(
8976                                s.span(),
8977                                "`through(far = \"...\")` must not be empty",
8978                            ));
8979                        }
8980                        far_ident = Some(syn::Ident::new(trimmed, s.span()));
8981                        return Ok(());
8982                    }
8983                    if inner.path.is_ident("far_fk_column") {
8984                        far_fk_column =
8985                            Some(parse_nonempty_string(&inner, "far_fk_column")?);
8986                        return Ok(());
8987                    }
8988                    if inner.path.is_ident("intermediate") {
8989                        let s: LitStr = inner.value()?.parse()?;
8990                        let raw = s.value();
8991                        let trimmed = raw.trim();
8992                        if trimmed.is_empty() {
8993                            return Err(syn::Error::new(
8994                                s.span(),
8995                                "`through(intermediate = \"...\")` must not be empty",
8996                            ));
8997                        }
8998                        intermediate_ident = Some(syn::Ident::new(trimmed, s.span()));
8999                        return Ok(());
9000                    }
9001                    if inner.path.is_ident("intermediate_fk_column") {
9002                        intermediate_fk_column =
9003                            Some(parse_nonempty_string(&inner, "intermediate_fk_column")?);
9004                        return Ok(());
9005                    }
9006                    if inner.path.is_ident("intermediate_pk_column") {
9007                        intermediate_pk_column =
9008                            Some(parse_nonempty_string(&inner, "intermediate_pk_column")?);
9009                        return Ok(());
9010                    }
9011                    Err(inner.error(
9012                        "unknown `through` attribute (supported: \
9013                         `name`, `far`, `far_fk_column`, \
9014                         `intermediate`, `intermediate_fk_column`, \
9015                         `intermediate_pk_column`)",
9016                    ))
9017                })?;
9018                let Some(name) = accessor_name else {
9019                    return Err(syn::Error::new(
9020                        span,
9021                        "`through` requires `name = \"...\"`",
9022                    ));
9023                };
9024                let Some(far) = far_ident else {
9025                    return Err(syn::Error::new(
9026                        span,
9027                        "`through` requires `far = \"FarModelType\"`",
9028                    ));
9029                };
9030                let Some(far_fk_column) = far_fk_column else {
9031                    return Err(syn::Error::new(
9032                        span,
9033                        "`through` requires `far_fk_column = \"<column>\"`",
9034                    ));
9035                };
9036                let Some(intermediate) = intermediate_ident else {
9037                    return Err(syn::Error::new(
9038                        span,
9039                        "`through` requires `intermediate = \"IntermediateModelType\"`",
9040                    ));
9041                };
9042                let Some(intermediate_fk_column) = intermediate_fk_column else {
9043                    return Err(syn::Error::new(
9044                        span,
9045                        "`through` requires `intermediate_fk_column = \"<column>\"`",
9046                    ));
9047                };
9048                let intermediate_pk_column =
9049                    intermediate_pk_column.unwrap_or_else(|| "id".to_owned());
9050                if out.through_relations.iter().any(|t| t.name == name) {
9051                    return Err(syn::Error::new(
9052                        span,
9053                        format!(
9054                            "duplicate `through(name = \"{name}\")` — \
9055                             pick a unique accessor name"
9056                        ),
9057                    ));
9058                }
9059                out.through_relations.push(ThroughAttr {
9060                    name,
9061                    far,
9062                    far_fk_column,
9063                    intermediate,
9064                    intermediate_fk_column,
9065                    intermediate_pk_column,
9066                });
9067                return Ok(());
9068            }
9069            if meta.path.is_ident("reverse_has") {
9070                // `#[rustango(reverse_has(name = "comments",
9071                //  child = "Comment", child_fk_column = "post_id"
9072                //  [, self_pk_column = "id"]))]` — issue #830.
9073                let span = meta.path.span();
9074                let mut accessor_name: Option<String> = None;
9075                let mut child_ident: Option<syn::Ident> = None;
9076                let mut child_fk_column: Option<String> = None;
9077                let mut self_pk_column: Option<String> = None;
9078                meta.parse_nested_meta(|inner| {
9079                    if inner.path.is_ident("name") {
9080                        let s: LitStr = inner.value()?.parse()?;
9081                        let raw = s.value();
9082                        if raw.trim().is_empty() {
9083                            return Err(syn::Error::new(
9084                                s.span(),
9085                                "`reverse_has(name = \"...\")` must not be empty",
9086                            ));
9087                        }
9088                        accessor_name = Some(raw);
9089                        return Ok(());
9090                    }
9091                    if inner.path.is_ident("child") {
9092                        let s: LitStr = inner.value()?.parse()?;
9093                        let raw = s.value();
9094                        let trimmed = raw.trim();
9095                        if trimmed.is_empty() {
9096                            return Err(syn::Error::new(
9097                                s.span(),
9098                                "`reverse_has(child = \"...\")` must not be empty",
9099                            ));
9100                        }
9101                        child_ident = Some(syn::Ident::new(trimmed, s.span()));
9102                        return Ok(());
9103                    }
9104                    if inner.path.is_ident("child_fk_column") {
9105                        let s: LitStr = inner.value()?.parse()?;
9106                        let raw = s.value();
9107                        let trimmed = raw.trim();
9108                        if trimmed.is_empty() {
9109                            return Err(syn::Error::new(
9110                                s.span(),
9111                                "`reverse_has(child_fk_column = \"...\")` must not be empty",
9112                            ));
9113                        }
9114                        child_fk_column = Some(trimmed.to_owned());
9115                        return Ok(());
9116                    }
9117                    if inner.path.is_ident("self_pk_column") {
9118                        let s: LitStr = inner.value()?.parse()?;
9119                        let raw = s.value();
9120                        let trimmed = raw.trim();
9121                        if trimmed.is_empty() {
9122                            return Err(syn::Error::new(
9123                                s.span(),
9124                                "`reverse_has(self_pk_column = \"...\")` must not be empty",
9125                            ));
9126                        }
9127                        self_pk_column = Some(trimmed.to_owned());
9128                        return Ok(());
9129                    }
9130                    Err(inner.error(
9131                        "unknown `reverse_has` attribute (supported: \
9132                         `name`, `child`, `child_fk_column`, \
9133                         `self_pk_column`)",
9134                    ))
9135                })?;
9136                let Some(name) = accessor_name else {
9137                    return Err(syn::Error::new(
9138                        span,
9139                        "`reverse_has` requires `name = \"...\"`",
9140                    ));
9141                };
9142                let Some(child) = child_ident else {
9143                    return Err(syn::Error::new(
9144                        span,
9145                        "`reverse_has` requires `child = \"ChildModelType\"`",
9146                    ));
9147                };
9148                let Some(child_fk_column) = child_fk_column else {
9149                    return Err(syn::Error::new(
9150                        span,
9151                        "`reverse_has` requires `child_fk_column = \"<column>\"`",
9152                    ));
9153                };
9154                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9155                if out.reverse_has_relations.iter().any(|r| r.name == name) {
9156                    return Err(syn::Error::new(
9157                        span,
9158                        format!(
9159                            "duplicate `reverse_has(name = \"{name}\")` — \
9160                             pick a unique accessor name"
9161                        ),
9162                    ));
9163                }
9164                out.reverse_has_relations.push(ReverseHasAttr {
9165                    name,
9166                    child,
9167                    child_fk_column,
9168                    self_pk_column,
9169                });
9170                return Ok(());
9171            }
9172            if meta.path.is_ident("generic_has") {
9173                // `#[rustango(generic_has(name = "tags",
9174                //  child = "Tag", ct_column = "content_type_id",
9175                //  pk_column = "object_pk" [, self_pk_column = "id"]))]`
9176                //  — issue #830, the reverse generic-FK (GFK) arm of the
9177                //  relation-existence family.
9178                let span = meta.path.span();
9179                let mut accessor_name: Option<String> = None;
9180                let mut child_ident: Option<syn::Ident> = None;
9181                let mut ct_column: Option<String> = None;
9182                let mut pk_column: Option<String> = None;
9183                let mut self_pk_column: Option<String> = None;
9184                meta.parse_nested_meta(|inner| {
9185                    if inner.path.is_ident("name") {
9186                        let s: LitStr = inner.value()?.parse()?;
9187                        let raw = s.value();
9188                        if raw.trim().is_empty() {
9189                            return Err(syn::Error::new(
9190                                s.span(),
9191                                "`generic_has(name = \"...\")` must not be empty",
9192                            ));
9193                        }
9194                        accessor_name = Some(raw);
9195                        return Ok(());
9196                    }
9197                    if inner.path.is_ident("child") {
9198                        let s: LitStr = inner.value()?.parse()?;
9199                        let trimmed = s.value().trim().to_owned();
9200                        if trimmed.is_empty() {
9201                            return Err(syn::Error::new(
9202                                s.span(),
9203                                "`generic_has(child = \"...\")` must not be empty",
9204                            ));
9205                        }
9206                        child_ident = Some(syn::Ident::new(&trimmed, s.span()));
9207                        return Ok(());
9208                    }
9209                    if inner.path.is_ident("ct_column") {
9210                        let s: LitStr = inner.value()?.parse()?;
9211                        let trimmed = s.value().trim().to_owned();
9212                        if trimmed.is_empty() {
9213                            return Err(syn::Error::new(
9214                                s.span(),
9215                                "`generic_has(ct_column = \"...\")` must not be empty",
9216                            ));
9217                        }
9218                        ct_column = Some(trimmed);
9219                        return Ok(());
9220                    }
9221                    if inner.path.is_ident("pk_column") {
9222                        let s: LitStr = inner.value()?.parse()?;
9223                        let trimmed = s.value().trim().to_owned();
9224                        if trimmed.is_empty() {
9225                            return Err(syn::Error::new(
9226                                s.span(),
9227                                "`generic_has(pk_column = \"...\")` must not be empty",
9228                            ));
9229                        }
9230                        pk_column = Some(trimmed);
9231                        return Ok(());
9232                    }
9233                    if inner.path.is_ident("self_pk_column") {
9234                        let s: LitStr = inner.value()?.parse()?;
9235                        let trimmed = s.value().trim().to_owned();
9236                        if trimmed.is_empty() {
9237                            return Err(syn::Error::new(
9238                                s.span(),
9239                                "`generic_has(self_pk_column = \"...\")` must not be empty",
9240                            ));
9241                        }
9242                        self_pk_column = Some(trimmed);
9243                        return Ok(());
9244                    }
9245                    Err(inner.error(
9246                        "unknown `generic_has` attribute (supported: \
9247                         `name`, `child`, `ct_column`, `pk_column`, \
9248                         `self_pk_column`)",
9249                    ))
9250                })?;
9251                let Some(name) = accessor_name else {
9252                    return Err(syn::Error::new(
9253                        span,
9254                        "`generic_has` requires `name = \"...\"`",
9255                    ));
9256                };
9257                let Some(child) = child_ident else {
9258                    return Err(syn::Error::new(
9259                        span,
9260                        "`generic_has` requires `child = \"ChildModelType\"`",
9261                    ));
9262                };
9263                let ct_column = ct_column.unwrap_or_else(|| "content_type_id".to_owned());
9264                let pk_column = pk_column.unwrap_or_else(|| "object_pk".to_owned());
9265                let self_pk_column = self_pk_column.unwrap_or_else(|| "id".to_owned());
9266                if out.generic_has_relations.iter().any(|r| r.name == name) {
9267                    return Err(syn::Error::new(
9268                        span,
9269                        format!(
9270                            "duplicate `generic_has(name = \"{name}\")` — \
9271                             pick a unique accessor name"
9272                        ),
9273                    ));
9274                }
9275                out.generic_has_relations.push(GenericHasAttr {
9276                    name,
9277                    child,
9278                    ct_column,
9279                    pk_column,
9280                    self_pk_column,
9281                });
9282                return Ok(());
9283            }
9284            if meta.path.is_ident("audit") {
9285                let mut audit = AuditAttrs::default();
9286                meta.parse_nested_meta(|inner| {
9287                    if inner.path.is_ident("track") {
9288                        let s: LitStr = inner.value()?.parse()?;
9289                        audit.track =
9290                            Some((split_field_list(&s.value()), s.span()));
9291                        return Ok(());
9292                    }
9293                    Err(inner.error(
9294                        "unknown audit attribute (supported: `track`)",
9295                    ))
9296                })?;
9297                out.audit = Some(audit);
9298                return Ok(());
9299            }
9300            if meta.path.is_ident("permissions") {
9301                // Two forms accepted:
9302                //   #[rustango(permissions)]          — flag form, true
9303                //   #[rustango(permissions = false)]  — explicit opt-out
9304                //   #[rustango(permissions = true)]   — explicit opt-in
9305                if let Ok(v) = meta.value() {
9306                    let lit: syn::LitBool = v.parse()?;
9307                    out.permissions = lit.value;
9308                } else {
9309                    out.permissions = true;
9310                }
9311                return Ok(());
9312            }
9313            if meta.path.is_ident("view") {
9314                // Issue #293 / T2.10. Two forms accepted, matching
9315                // the `permissions` flag pattern:
9316                //   #[rustango(view)]          — flag form, true
9317                //   #[rustango(view = false)]  — explicit opt-out
9318                //   #[rustango(view = true)]   — explicit opt-in
9319                if let Ok(v) = meta.value() {
9320                    let lit: syn::LitBool = v.parse()?;
9321                    out.is_view = lit.value;
9322                } else {
9323                    out.is_view = true;
9324                }
9325                return Ok(());
9326            }
9327            if meta.path.is_ident("managed") {
9328                // Django-shape Meta.managed. Issue #321.
9329                //   #[rustango(managed = false)]  — operator-managed table
9330                //   #[rustango(managed = true)]   — rustango-managed (the default)
9331                // Bare-flag form is intentionally not accepted: writing
9332                // `#[rustango(managed)]` reads as "yes please manage it"
9333                // which is already the default. The opt-out is the only
9334                // useful state, so it must be explicit.
9335                let v = meta.value()?;
9336                let lit: syn::LitBool = v.parse()?;
9337                out.managed = lit.value;
9338                return Ok(());
9339            }
9340            if meta.path.is_ident("verbose_name") {
9341                let s: LitStr = meta.value()?.parse()?;
9342                out.verbose_name = Some(s.value());
9343                return Ok(());
9344            }
9345            if meta.path.is_ident("verbose_name_plural") {
9346                let s: LitStr = meta.value()?.parse()?;
9347                out.verbose_name_plural = Some(s.value());
9348                return Ok(());
9349            }
9350            if meta.path.is_ident("db_table_comment") {
9351                // Django-shape `Meta.db_table_comment` (4.2+) — free-form
9352                // table-level comment attached to the DB catalog.
9353                let s: LitStr = meta.value()?.parse()?;
9354                out.db_table_comment = Some(s.value());
9355                return Ok(());
9356            }
9357            if meta.path.is_ident("proxy") {
9358                // Django-shape `Meta.proxy = True` — declarative flag
9359                // marking the struct as a proxy of another model that
9360                // shares its DB table. Stored on `ModelSchema::proxy`
9361                // so future codegen can skip `CreateTable` emission
9362                // for proxies (parent owns the table) and route
9363                // per-instance method resolution to the proxy class.
9364                //
9365                // Accepts `proxy` (bare → true) and `proxy = true/false`.
9366                let value = if meta.input.peek(syn::Token![=]) {
9367                    meta.value()?.parse::<syn::LitBool>()?.value
9368                } else {
9369                    true
9370                };
9371                out.proxy = value;
9372                return Ok(());
9373            }
9374            if meta.path.is_ident("order_with_respect_to") {
9375                // Django-shape `Meta.order_with_respect_to = "parent_fk"` —
9376                // the model's instances are intrinsically ordered
9377                // relative to their parent FK. Django auto-generates
9378                // a `_order` integer column + admin reordering UI.
9379                //
9380                // rustango stores the FK field name on
9381                // `ModelSchema::order_with_respect_to`. Declarative-only
9382                // today: the migration writer + admin surfaces still
9383                // treat every model identically. Future codegen will
9384                // key off the metadata to auto-emit the `_order`
9385                // column and reorder helpers.
9386                //
9387                // Validated as a Rust-shape identifier so the macro
9388                // can reject typos at derive time.
9389                let s: LitStr = meta.value()?.parse()?;
9390                let raw = s.value();
9391                if raw.is_empty() {
9392                    return Err(syn::Error::new(
9393                        s.span(),
9394                        "`order_with_respect_to` must be a non-empty FK field name",
9395                    ));
9396                }
9397                let valid = raw
9398                    .chars()
9399                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9400                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9401                if !valid {
9402                    return Err(syn::Error::new(
9403                        s.span(),
9404                        format!(
9405                            "`order_with_respect_to` must be a valid Rust \
9406                             identifier (letters / digits / underscores, \
9407                             not starting with a digit); got `{raw}`"
9408                        ),
9409                    ));
9410                }
9411                out.order_with_respect_to = Some(raw);
9412                return Ok(());
9413            }
9414            if meta.path.is_ident("required_db_features") {
9415                // Django-shape `Meta.required_db_features` — capability
9416                // tokens the model needs (e.g. `"json_extract"`,
9417                // `"window_functions"`, `"row_security"`). Comma-separated.
9418                // `manage check --deploy` walks every model and warns
9419                // when the active backend doesn't advertise the
9420                // capability.
9421                //
9422                // rustango ships a small registry of capability tokens
9423                // each dialect supports — see `Dialect::supports`.
9424                // Unknown tokens still parse (they end up on the
9425                // schema and show up in the warning) so projects can
9426                // declare aspirational capabilities and the check
9427                // verb will keep nagging until the dialect implements
9428                // them.
9429                let s: LitStr = meta.value()?.parse()?;
9430                let raw = s.value();
9431                let features: Vec<String> = raw
9432                    .split(',')
9433                    .map(str::trim)
9434                    .filter(|s| !s.is_empty())
9435                    .map(str::to_owned)
9436                    .collect();
9437                if features.is_empty() {
9438                    return Err(syn::Error::new(
9439                        s.span(),
9440                        "`required_db_features` must list at least one \
9441                         comma-separated capability token",
9442                    ));
9443                }
9444                out.required_db_features = features;
9445                return Ok(());
9446            }
9447            if meta.path.is_ident("required_db_vendor") {
9448                // Django-shape `Meta.required_db_vendor` — the model
9449                // is only meant to run against the named DB backend.
9450                // `manage check --deploy` flags a mismatch so
9451                // ops catches "I forgot to switch DATABASE_URL" at
9452                // deploy time rather than runtime.
9453                //
9454                // Django spells it as a free-form string; rustango
9455                // restricts to the three backends it ships dialects
9456                // for so the check verb can compare reliably.
9457                let s: LitStr = meta.value()?.parse()?;
9458                let raw = s.value().to_ascii_lowercase();
9459                match raw.as_str() {
9460                    "postgresql" | "postgres" | "pg" => {
9461                        out.required_db_vendor = Some("postgres".to_owned());
9462                    }
9463                    "mysql" | "mariadb" => {
9464                        out.required_db_vendor = Some("mysql".to_owned());
9465                    }
9466                    "sqlite" | "sqlite3" => {
9467                        out.required_db_vendor = Some("sqlite".to_owned());
9468                    }
9469                    _ => {
9470                        return Err(syn::Error::new(
9471                            s.span(),
9472                            format!(
9473                                "unknown required_db_vendor `{raw}` — \
9474                                 expected `postgres` (aliases: `postgresql`, `pg`), \
9475                                 `mysql` (alias: `mariadb`), or `sqlite` \
9476                                 (alias: `sqlite3`)"
9477                            ),
9478                        ));
9479                    }
9480                }
9481                return Ok(());
9482            }
9483            if meta.path.is_ident("base_manager_name") {
9484                // Django-shape `Meta.base_manager_name` — name of the
9485                // Manager subclass that `<instance>.<relation>_set`
9486                // uses when resolving reverse-relation managers.
9487                // Distinct from `default_manager_name` (what
9488                // `Model.objects` returns at the class level).
9489                // Stored on `ModelSchema::base_manager_name`.
9490                //
9491                // Validated as a Rust identifier so it stays safe to
9492                // re-emit as code in future reverse-manager codegen.
9493                let s: LitStr = meta.value()?.parse()?;
9494                let raw = s.value();
9495                if raw.is_empty() {
9496                    return Err(syn::Error::new(
9497                        s.span(),
9498                        "`base_manager_name` must be a non-empty string",
9499                    ));
9500                }
9501                let valid = raw
9502                    .chars()
9503                    .all(|c| c == '_' || c.is_ascii_alphanumeric())
9504                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9505                if !valid {
9506                    return Err(syn::Error::new(
9507                        s.span(),
9508                        format!(
9509                            "`base_manager_name` must be a valid Rust \
9510                             identifier (letters / digits / underscores, \
9511                             not starting with a digit); got `{raw}`"
9512                        ),
9513                    ));
9514                }
9515                out.base_manager_name = Some(raw);
9516                return Ok(());
9517            }
9518            if meta.path.is_ident("default_related_name") {
9519                // Django-shape `Meta.default_related_name` — the name
9520                // reverse-relation accessors use when callers don't
9521                // override `related_name=...` on the FK / M2M field.
9522                // Stored on `ModelSchema::default_related_name` so
9523                // future reverse-manager codegen / DRF schema emit /
9524                // admin templates can pick the right accessor name
9525                // (today rustango doesn't auto-emit reverse managers;
9526                // the metadata is the foundation for that work).
9527                //
9528                // Django requires snake_case + no `+` suffix; we
9529                // enforce non-empty + ASCII identifier-shape so the
9530                // string is safe to use as a Rust ident later.
9531                let s: LitStr = meta.value()?.parse()?;
9532                let raw = s.value();
9533                if raw.is_empty() {
9534                    return Err(syn::Error::new(
9535                        s.span(),
9536                        "`default_related_name` must be a non-empty string",
9537                    ));
9538                }
9539                let valid = raw
9540                    .chars()
9541                    .all(|c| c == '_' || c.is_ascii_lowercase() || c.is_ascii_digit())
9542                    && !raw.chars().next().is_some_and(|c| c.is_ascii_digit());
9543                if !valid {
9544                    return Err(syn::Error::new(
9545                        s.span(),
9546                        format!(
9547                            "`default_related_name` must be snake_case ASCII \
9548                             (lowercase letters / digits / underscores, not \
9549                             starting with a digit); got `{raw}`"
9550                        ),
9551                    ));
9552                }
9553                out.default_related_name = Some(raw);
9554                return Ok(());
9555            }
9556            if meta.path.is_ident("extra_permissions") {
9557                // Django-shape `Meta.permissions = [(codename, name), ...]`.
9558                // Comma-separated `codename:label` pairs.
9559                let s: LitStr = meta.value()?.parse()?;
9560                let raw = s.value();
9561                let mut pairs = Vec::new();
9562                for entry in raw.split(',') {
9563                    let entry = entry.trim();
9564                    if entry.is_empty() {
9565                        continue;
9566                    }
9567                    let (codename, label) = match entry.split_once(':') {
9568                        Some((c, l)) => (c.trim().to_owned(), l.trim().to_owned()),
9569                        None => (entry.to_owned(), entry.to_owned()),
9570                    };
9571                    if codename.is_empty() {
9572                        return Err(meta.error(
9573                            "`extra_permissions` entries must be `codename:label` pairs",
9574                        ));
9575                    }
9576                    pairs.push((codename, label));
9577                }
9578                if pairs.is_empty() {
9579                    return Err(meta
9580                        .error("`extra_permissions = \"…\"` must list at least one pair"));
9581                }
9582                out.extra_permissions = pairs;
9583                return Ok(());
9584            }
9585            if meta.path.is_ident("default_permissions") {
9586                // Django-shape `Meta.default_permissions = ('view',
9587                // 'change')`. Comma-separated subset of the CRUD
9588                // action set. Empty means all four (the framework
9589                // default — matches Django when the option is
9590                // omitted).
9591                let s: LitStr = meta.value()?.parse()?;
9592                let raw = s.value();
9593                let mut actions: Vec<String> = Vec::new();
9594                for entry in raw.split(',') {
9595                    let action = entry.trim().to_ascii_lowercase();
9596                    if action.is_empty() {
9597                        continue;
9598                    }
9599                    match action.as_str() {
9600                        "add" | "change" | "delete" | "view" => {}
9601                        other => {
9602                            return Err(syn::Error::new(
9603                                s.span(),
9604                                format!(
9605                                    "unknown default_permissions action `{other}` — \
9606                                     expected one of `add`, `change`, `delete`, `view`"
9607                                ),
9608                            ));
9609                        }
9610                    }
9611                    if !actions.contains(&action) {
9612                        actions.push(action);
9613                    }
9614                }
9615                if actions.is_empty() {
9616                    return Err(syn::Error::new(
9617                        s.span(),
9618                        "`default_permissions = \"…\"` must list at least one action; \
9619                         use `permissions = false` on the container if you want NO \
9620                         permissions seeded for this model.",
9621                    ));
9622                }
9623                out.default_permissions = actions;
9624                return Ok(());
9625            }
9626            if meta.path.is_ident("get_latest_by") {
9627                // Django-shape `Meta.get_latest_by`. The `-` prefix
9628                // selects descending order (Django muscle memory).
9629                let s: LitStr = meta.value()?.parse()?;
9630                let raw = s.value();
9631                let trimmed = raw.trim();
9632                if trimmed.is_empty() {
9633                    return Err(meta.error("`get_latest_by` must name a column"));
9634                }
9635                let (col, desc) = if let Some(stripped) = trimmed.strip_prefix('-') {
9636                    (stripped.to_owned(), true)
9637                } else if let Some(stripped) = trimmed.strip_prefix('+') {
9638                    (stripped.to_owned(), false)
9639                } else {
9640                    (trimmed.to_owned(), false)
9641                };
9642                if col.is_empty() {
9643                    return Err(meta.error("`get_latest_by` must name a column"));
9644                }
9645                out.get_latest_by = Some((col, desc));
9646                return Ok(());
9647            }
9648            if meta.path.is_ident("unique_together") {
9649                // Django-shape composite UNIQUE index. Two syntaxes:
9650                //
9651                //   #[rustango(unique_together = "org_id, user_id")]                       — auto-derived name
9652                //   #[rustango(unique_together(columns = "org_id, user_id", name = "x"))]  — explicit name
9653                //
9654                // Both produce `CREATE UNIQUE INDEX <name> ON <table>
9655                // (col1, col2)`, where <name> defaults to
9656                // `<table>_<col1>_<col2>_uq` when not supplied.
9657                let (columns, name) = parse_together_attr(&meta, "unique_together")?;
9658                out.indexes.push(IndexAttr {
9659                    name,
9660                    columns,
9661                    unique: true,
9662                    method: "btree".to_owned(),
9663                    where_clause: None,
9664                include: Vec::new(),
9665                });
9666                return Ok(());
9667            }
9668            if meta.path.is_ident("index_together") {
9669                // Django-shape composite (non-unique) index. Two syntaxes
9670                // mirroring `unique_together`.
9671                //
9672                //   #[rustango(index_together = "created_at, status")]
9673                //   #[rustango(index_together(columns = "created_at, status", name = "x"))]
9674                let (columns, name) = parse_together_attr(&meta, "index_together")?;
9675                out.indexes.push(IndexAttr {
9676                    name,
9677                    columns,
9678                    unique: false,
9679                    method: "btree".to_owned(),
9680                    where_clause: None,
9681                include: Vec::new(),
9682                });
9683                return Ok(());
9684            }
9685            if meta.path.is_ident("unique_when") {
9686                // Django 4.0+ `UniqueConstraint(condition=Q(...))` —
9687                // partial unique index. Issue #265 / T1.3.
9688                //
9689                //   #[rustango(unique_when(
9690                //       columns   = "email",
9691                //       condition = "deleted_at IS NULL",
9692                //       name      = "unique_active_email"
9693                //   ))]
9694                //
9695                // → `CREATE UNIQUE INDEX <name> ON <table> (cols) WHERE <condition>`
9696                // on PG / SQLite (both ship partial indexes natively).
9697                // MySQL falls back to a plain UNIQUE index — the
9698                // condition is lost; document the limitation in the
9699                // generated migration.
9700                let mut columns: Option<Vec<String>> = None;
9701                let mut condition: Option<String> = None;
9702                let mut name: Option<String> = None;
9703                let mut include: Vec<String> = Vec::new();
9704                meta.parse_nested_meta(|inner| {
9705                    if inner.path.is_ident("columns") {
9706                        let s: LitStr = inner.value()?.parse()?;
9707                        columns = Some(split_field_list(&s.value()));
9708                        return Ok(());
9709                    }
9710                    if inner.path.is_ident("condition") {
9711                        let s: LitStr = inner.value()?.parse()?;
9712                        condition = Some(s.value());
9713                        return Ok(());
9714                    }
9715                    if inner.path.is_ident("name") {
9716                        let s: LitStr = inner.value()?.parse()?;
9717                        name = Some(s.value());
9718                        return Ok(());
9719                    }
9720                    if inner.path.is_ident("include") {
9721                        // Django `UniqueConstraint(include=[...])` — PG
9722                        // 11+ covering-index columns. Non-key columns
9723                        // travel with the index leaf for index-only
9724                        // scans. Dropped on MySQL/SQLite by the writer.
9725                        let s: LitStr = inner.value()?.parse()?;
9726                        include = split_field_list(&s.value());
9727                        return Ok(());
9728                    }
9729                    Err(inner.error(
9730                        "unknown unique_when attribute (supported: \
9731                         `columns = \"...\"`, `condition = \"...\"`, \
9732                         `name = \"...\"`, `include = \"...\"`)",
9733                    ))
9734                })?;
9735                let columns = columns.ok_or_else(|| {
9736                    meta.error("`unique_when(...)` requires `columns = \"...\"`")
9737                })?;
9738                let condition = condition.ok_or_else(|| {
9739                    meta.error("`unique_when(...)` requires `condition = \"...\"`")
9740                })?;
9741                if columns.is_empty() {
9742                    return Err(meta.error("`unique_when(columns = \"\")` is empty"));
9743                }
9744                out.indexes.push(IndexAttr {
9745                    name,
9746                    columns,
9747                    unique: true,
9748                    method: "btree".to_owned(),
9749                    where_clause: Some(condition),
9750                    include,
9751                });
9752                return Ok(());
9753            }
9754            if meta.path.is_ident("index_when") {
9755                // Django `Index(fields=..., condition=Q(...))` parity —
9756                // non-unique partial index. Sibling of `unique_when`
9757                // (which emits `CREATE UNIQUE INDEX ... WHERE ...`).
9758                //
9759                //   #[rustango(index_when(
9760                //       columns   = "status, created_at",
9761                //       condition = "deleted_at IS NULL",
9762                //       name      = "active_status_created_idx"
9763                //   ))]
9764                //
9765                // → `CREATE INDEX <name> ON <table> (cols) WHERE <condition>`
9766                // on PG / SQLite (both ship partial indexes natively).
9767                // MySQL has no native partial-index support — the writer
9768                // emits a plain CREATE INDEX and the condition is lost;
9769                // operators wanting that selectivity on MySQL should
9770                // declare a covering index plus an application-level
9771                // filter.
9772                let mut columns: Option<Vec<String>> = None;
9773                let mut condition: Option<String> = None;
9774                let mut name: Option<String> = None;
9775                let mut method: String = "btree".to_owned();
9776                let mut include: Vec<String> = Vec::new();
9777                meta.parse_nested_meta(|inner| {
9778                    if inner.path.is_ident("columns") {
9779                        let s: LitStr = inner.value()?.parse()?;
9780                        columns = Some(split_field_list(&s.value()));
9781                        return Ok(());
9782                    }
9783                    if inner.path.is_ident("condition") {
9784                        let s: LitStr = inner.value()?.parse()?;
9785                        condition = Some(s.value());
9786                        return Ok(());
9787                    }
9788                    if inner.path.is_ident("name") {
9789                        let s: LitStr = inner.value()?.parse()?;
9790                        name = Some(s.value());
9791                        return Ok(());
9792                    }
9793                    if inner.path.is_ident("method") {
9794                        let s: LitStr = inner.value()?.parse()?;
9795                        method = s.value();
9796                        return Ok(());
9797                    }
9798                    if inner.path.is_ident("include") {
9799                        // Django `Index(include=[...])` — PG 11+
9800                        // covering-index columns; non-key columns
9801                        // travel with the index leaf. Dropped on
9802                        // MySQL/SQLite.
9803                        let s: LitStr = inner.value()?.parse()?;
9804                        include = split_field_list(&s.value());
9805                        return Ok(());
9806                    }
9807                    Err(inner.error(
9808                        "unknown index_when attribute (supported: \
9809                         `columns = \"...\"`, `condition = \"...\"`, \
9810                         `name = \"...\"`, `method = \"btree|gin|gist|...\"`, \
9811                         `include = \"...\"`)",
9812                    ))
9813                })?;
9814                let columns = columns
9815                    .ok_or_else(|| meta.error("`index_when(...)` requires `columns = \"...\"`"))?;
9816                let condition = condition.ok_or_else(|| {
9817                    meta.error("`index_when(...)` requires `condition = \"...\"`")
9818                })?;
9819                if columns.is_empty() {
9820                    return Err(meta.error("`index_when(columns = \"\")` is empty"));
9821                }
9822                out.indexes.push(IndexAttr {
9823                    name,
9824                    columns,
9825                    unique: false,
9826                    method,
9827                    where_clause: Some(condition),
9828                    include,
9829                });
9830                return Ok(());
9831            }
9832            if meta.path.is_ident("index") {
9833                // Container-level composite index. Two syntaxes:
9834                //   #[rustango(index = "col1, col2")]   — bare, non-unique btree
9835                //   #[rustango(index("col1, col2"))]    — call form (same result)
9836                //   #[rustango(index("col1, col2", unique, name = "my_idx", method = "gin"))]
9837                // The call form takes the column list as a leading string
9838                // literal, then optional `unique` / `name = "..."` /
9839                // `method = "..."` flags (a leading literal can't compose
9840                // under `parse_nested_meta`, so the paren body is parsed by
9841                // hand). `unique_together` / `index_together` remain the
9842                // Django-shape aliases for the same feature.
9843                let cols_lit: LitStr;
9844                let mut unique = false;
9845                let mut name: Option<String> = None;
9846                let mut method = "btree".to_owned();
9847                if meta.input.peek(syn::token::Paren) {
9848                    let content;
9849                    syn::parenthesized!(content in meta.input);
9850                    cols_lit = content.parse()?;
9851                    while content.peek(syn::Token![,]) {
9852                        content.parse::<syn::Token![,]>()?;
9853                        if content.is_empty() {
9854                            break;
9855                        }
9856                        let flag: syn::Ident = content.parse()?;
9857                        if flag == "unique" {
9858                            unique = true;
9859                        } else if flag == "name" {
9860                            content.parse::<syn::Token![=]>()?;
9861                            let s: LitStr = content.parse()?;
9862                            name = Some(s.value());
9863                        } else if flag == "method" {
9864                            content.parse::<syn::Token![=]>()?;
9865                            let s: LitStr = content.parse()?;
9866                            let v = s.value();
9867                            match v.as_str() {
9868                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash"
9869                                | "bloom" => method = v,
9870                                other => {
9871                                    return Err(syn::Error::new(
9872                                        s.span(),
9873                                        format!("unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)"),
9874                                    ));
9875                                }
9876                            }
9877                        } else {
9878                            return Err(syn::Error::new(
9879                                flag.span(),
9880                                "unknown index flag (supported: `unique`, `name`, `method`)",
9881                            ));
9882                        }
9883                    }
9884                } else {
9885                    cols_lit = meta.value()?.parse()?;
9886                }
9887                let columns = split_field_list(&cols_lit.value());
9888                out.indexes.push(IndexAttr {
9889                    name,
9890                    columns,
9891                    unique,
9892                    method,
9893                    where_clause: None,
9894                    include: Vec::new(),
9895                });
9896                return Ok(());
9897            }
9898            if meta.path.is_ident("check") {
9899                // #[rustango(check(name = "…", expr = "…"))]
9900                let mut name: Option<String> = None;
9901                let mut expr: Option<String> = None;
9902                meta.parse_nested_meta(|inner| {
9903                    if inner.path.is_ident("name") {
9904                        let s: LitStr = inner.value()?.parse()?;
9905                        name = Some(s.value());
9906                        return Ok(());
9907                    }
9908                    if inner.path.is_ident("expr") {
9909                        let s: LitStr = inner.value()?.parse()?;
9910                        expr = Some(s.value());
9911                        return Ok(());
9912                    }
9913                    Err(inner.error("unknown check attribute (supported: `name`, `expr`)"))
9914                })?;
9915                let name = name.ok_or_else(|| meta.error("check requires `name = \"...\"`"))?;
9916                let expr = expr.ok_or_else(|| meta.error("check requires `expr = \"...\"`"))?;
9917                out.checks.push(CheckAttr { name, expr });
9918                return Ok(());
9919            }
9920            if meta.path.is_ident("exclude") {
9921                // #[rustango(exclude(name = "…", using = "gist",
9922                //                    elements = "col WITH op, col WITH op",
9923                //                    where = "…"))]
9924                let mut name: Option<String> = None;
9925                let mut using: Option<String> = None;
9926                let mut elements_raw: Option<(String, proc_macro2::Span)> = None;
9927                let mut where_clause: Option<String> = None;
9928                meta.parse_nested_meta(|inner| {
9929                    if inner.path.is_ident("name") {
9930                        let s: LitStr = inner.value()?.parse()?;
9931                        name = Some(s.value());
9932                        return Ok(());
9933                    }
9934                    if inner.path.is_ident("using") {
9935                        let s: LitStr = inner.value()?.parse()?;
9936                        using = Some(s.value());
9937                        return Ok(());
9938                    }
9939                    if inner.path.is_ident("elements") {
9940                        let s: LitStr = inner.value()?.parse()?;
9941                        elements_raw = Some((s.value(), s.span()));
9942                        return Ok(());
9943                    }
9944                    if inner.path.is_ident("where") || inner.path.is_ident("where_clause") {
9945                        let s: LitStr = inner.value()?.parse()?;
9946                        where_clause = Some(s.value());
9947                        return Ok(());
9948                    }
9949                    Err(inner.error(
9950                        "unknown exclude attribute (supported: `name`, `using`, `elements`, `where`)",
9951                    ))
9952                })?;
9953                let name = name.ok_or_else(|| meta.error("exclude requires `name = \"...\"`"))?;
9954                let using = using.unwrap_or_else(|| "gist".to_owned());
9955                let (elements_str, elements_span) = elements_raw.ok_or_else(|| {
9956                    meta.error(
9957                        "exclude requires `elements = \"col WITH op, col WITH op\"`",
9958                    )
9959                })?;
9960                // Parse `col WITH op` pairs separated by commas.
9961                let mut elements: Vec<(String, String)> = Vec::new();
9962                for pair in elements_str.split(',') {
9963                    let pair = pair.trim();
9964                    if pair.is_empty() {
9965                        continue;
9966                    }
9967                    let mut split = pair.splitn(2, |c: char| c.is_whitespace());
9968                    let col = split.next().unwrap_or("").trim();
9969                    let rest = split.next().unwrap_or("").trim();
9970                    // `WITH op` — case-insensitive on `WITH`, then op.
9971                    let rest_lc = rest.to_ascii_lowercase();
9972                    let op = rest_lc
9973                        .strip_prefix("with")
9974                        .map(|r| r.trim_start())
9975                        .filter(|r| !r.is_empty())
9976                        .map(|_| {
9977                            // Pull the original-case op from `rest` after the
9978                            // `WITH ` token (5 chars).
9979                            rest[4..].trim_start().to_owned()
9980                        });
9981                    let Some(op) = op else {
9982                        return Err(syn::Error::new(
9983                            elements_span,
9984                            format!(
9985                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9986                                 (e.g. `room_id WITH =` or `during WITH &&`)"
9987                            ),
9988                        ));
9989                    };
9990                    if col.is_empty() || op.is_empty() {
9991                        return Err(syn::Error::new(
9992                            elements_span,
9993                            format!(
9994                                "exclude elements: `{pair}` must be `<col> WITH <op>` \
9995                                 (both sides non-empty)"
9996                            ),
9997                        ));
9998                    }
9999                    elements.push((col.to_owned(), op));
10000                }
10001                if elements.is_empty() {
10002                    return Err(syn::Error::new(
10003                        elements_span,
10004                        "exclude requires at least one `col WITH op` element",
10005                    ));
10006                }
10007                out.excludes.push(ExcludeAttr {
10008                    name,
10009                    using,
10010                    elements,
10011                    where_clause,
10012                });
10013                return Ok(());
10014            }
10015            if meta.path.is_ident("generic_fk") {
10016                let mut gfk = GenericFkAttr {
10017                    name: String::new(),
10018                    ct_column: String::new(),
10019                    pk_column: String::new(),
10020                };
10021                meta.parse_nested_meta(|inner| {
10022                    if inner.path.is_ident("name") {
10023                        let s: LitStr = inner.value()?.parse()?;
10024                        gfk.name = s.value();
10025                        return Ok(());
10026                    }
10027                    if inner.path.is_ident("ct_column") {
10028                        let s: LitStr = inner.value()?.parse()?;
10029                        gfk.ct_column = s.value();
10030                        return Ok(());
10031                    }
10032                    if inner.path.is_ident("pk_column") {
10033                        let s: LitStr = inner.value()?.parse()?;
10034                        gfk.pk_column = s.value();
10035                        return Ok(());
10036                    }
10037                    Err(inner.error(
10038                        "unknown generic_fk attribute (supported: `name`, `ct_column`, `pk_column`)",
10039                    ))
10040                })?;
10041                if gfk.name.is_empty() {
10042                    return Err(meta.error("generic_fk requires `name = \"...\"`"));
10043                }
10044                if gfk.ct_column.is_empty() {
10045                    return Err(meta.error("generic_fk requires `ct_column = \"...\"`"));
10046                }
10047                if gfk.pk_column.is_empty() {
10048                    return Err(meta.error("generic_fk requires `pk_column = \"...\"`"));
10049                }
10050                out.generic_fks.push(gfk);
10051                return Ok(());
10052            }
10053            if meta.path.is_ident("fk_composite") {
10054                let mut fk = CompositeFkAttr {
10055                    name: String::new(),
10056                    to: String::new(),
10057                    from: Vec::new(),
10058                    on: Vec::new(),
10059                };
10060                meta.parse_nested_meta(|inner| {
10061                    if inner.path.is_ident("name") {
10062                        let s: LitStr = inner.value()?.parse()?;
10063                        fk.name = s.value();
10064                        return Ok(());
10065                    }
10066                    if inner.path.is_ident("to") {
10067                        let s: LitStr = inner.value()?.parse()?;
10068                        fk.to = s.value();
10069                        return Ok(());
10070                    }
10071                    // `on = ("col1", "col2", ...)` — parse a parenthesised
10072                    // comma-list of string literals.
10073                    if inner.path.is_ident("on") || inner.path.is_ident("from") {
10074                        let value = inner.value()?;
10075                        let content;
10076                        syn::parenthesized!(content in value);
10077                        let lits: syn::punctuated::Punctuated<syn::LitStr, syn::Token![,]> =
10078                            content.parse_terminated(
10079                                |p| p.parse::<syn::LitStr>(),
10080                                syn::Token![,],
10081                            )?;
10082                        let cols: Vec<String> = lits.iter().map(syn::LitStr::value).collect();
10083                        if inner.path.is_ident("on") {
10084                            fk.on = cols;
10085                        } else {
10086                            fk.from = cols;
10087                        }
10088                        return Ok(());
10089                    }
10090                    Err(inner.error(
10091                        "unknown fk_composite attribute (supported: `name`, `to`, `on`, `from`)",
10092                    ))
10093                })?;
10094                if fk.name.is_empty() {
10095                    return Err(meta.error("fk_composite requires `name = \"...\"`"));
10096                }
10097                if fk.to.is_empty() {
10098                    return Err(meta.error("fk_composite requires `to = \"...\"`"));
10099                }
10100                if fk.from.is_empty() || fk.on.is_empty() {
10101                    return Err(meta.error(
10102                        "fk_composite requires non-empty `from = (...)` and `on = (...)` tuples",
10103                    ));
10104                }
10105                if fk.from.len() != fk.on.len() {
10106                    return Err(meta.error(format!(
10107                        "fk_composite `from` ({} cols) and `on` ({} cols) must be the same length",
10108                        fk.from.len(),
10109                        fk.on.len(),
10110                    )));
10111                }
10112                out.composite_fks.push(fk);
10113                return Ok(());
10114            }
10115            if meta.path.is_ident("m2m") {
10116                let mut m2m = M2MAttr {
10117                    name: String::new(),
10118                    to: String::new(),
10119                    through: String::new(),
10120                    src: String::new(),
10121                    dst: String::new(),
10122                    auto_create: true,
10123                };
10124                meta.parse_nested_meta(|inner| {
10125                    if inner.path.is_ident("name") {
10126                        let s: LitStr = inner.value()?.parse()?;
10127                        m2m.name = s.value();
10128                        return Ok(());
10129                    }
10130                    if inner.path.is_ident("to") {
10131                        let s: LitStr = inner.value()?.parse()?;
10132                        m2m.to = s.value();
10133                        return Ok(());
10134                    }
10135                    if inner.path.is_ident("through") {
10136                        let s: LitStr = inner.value()?.parse()?;
10137                        m2m.through = s.value();
10138                        return Ok(());
10139                    }
10140                    if inner.path.is_ident("src") {
10141                        let s: LitStr = inner.value()?.parse()?;
10142                        m2m.src = s.value();
10143                        return Ok(());
10144                    }
10145                    if inner.path.is_ident("dst") {
10146                        let s: LitStr = inner.value()?.parse()?;
10147                        m2m.dst = s.value();
10148                        return Ok(());
10149                    }
10150                    if inner.path.is_ident("auto_create") {
10151                        let lit: syn::LitBool = inner.value()?.parse()?;
10152                        m2m.auto_create = lit.value;
10153                        return Ok(());
10154                    }
10155                    Err(inner.error("unknown m2m attribute (supported: `name`, `to`, `through`, `src`, `dst`, `auto_create`)"))
10156                })?;
10157                if m2m.name.is_empty() {
10158                    return Err(meta.error("m2m requires `name = \"...\"`"));
10159                }
10160                if m2m.to.is_empty() {
10161                    return Err(meta.error("m2m requires `to = \"...\"`"));
10162                }
10163                if m2m.through.is_empty() {
10164                    return Err(meta.error("m2m requires `through = \"...\"`"));
10165                }
10166                if m2m.src.is_empty() {
10167                    return Err(meta.error("m2m requires `src = \"...\"`"));
10168                }
10169                if m2m.dst.is_empty() {
10170                    return Err(meta.error("m2m requires `dst = \"...\"`"));
10171                }
10172                out.m2m.push(m2m);
10173                return Ok(());
10174            }
10175            if meta.path.is_ident("generic_m2m") {
10176                let mut gm = GenericM2MAttr {
10177                    name: String::new(),
10178                    through: String::new(),
10179                    pk_column: String::new(),
10180                    ct_column: String::new(),
10181                    related_column: String::new(),
10182                };
10183                meta.parse_nested_meta(|inner| {
10184                    let field = |inner: &syn::meta::ParseNestedMeta| -> syn::Result<String> {
10185                        let s: LitStr = inner.value()?.parse()?;
10186                        Ok(s.value())
10187                    };
10188                    if inner.path.is_ident("name") {
10189                        gm.name = field(&inner)?;
10190                        return Ok(());
10191                    }
10192                    if inner.path.is_ident("through") {
10193                        gm.through = field(&inner)?;
10194                        return Ok(());
10195                    }
10196                    if inner.path.is_ident("pk_column") {
10197                        gm.pk_column = field(&inner)?;
10198                        return Ok(());
10199                    }
10200                    if inner.path.is_ident("ct_column") {
10201                        gm.ct_column = field(&inner)?;
10202                        return Ok(());
10203                    }
10204                    if inner.path.is_ident("related_column") {
10205                        gm.related_column = field(&inner)?;
10206                        return Ok(());
10207                    }
10208                    Err(inner.error("unknown generic_m2m attribute (supported: `name`, `through`, `pk_column`, `ct_column`, `related_column`)"))
10209                })?;
10210                for (val, label) in [
10211                    (&gm.name, "name"),
10212                    (&gm.through, "through"),
10213                    (&gm.pk_column, "pk_column"),
10214                    (&gm.ct_column, "ct_column"),
10215                    (&gm.related_column, "related_column"),
10216                ] {
10217                    if val.is_empty() {
10218                        return Err(meta.error(format!("generic_m2m requires `{label} = \"...\"`")));
10219                    }
10220                }
10221                out.generic_m2m.push(gm);
10222                return Ok(());
10223            }
10224            Err(meta.error("unknown rustango container attribute"))
10225        })?;
10226    }
10227    Ok(out)
10228}
10229
10230/// Split a comma-separated field-name list (e.g. `"name, office"`) into
10231/// owned field names, trimming whitespace and skipping empty entries.
10232/// Field-name validation against the model is done by the caller.
10233fn split_field_list(raw: &str) -> Vec<String> {
10234    raw.split(',')
10235        .map(str::trim)
10236        .filter(|s| !s.is_empty())
10237        .map(str::to_owned)
10238        .collect()
10239}
10240
10241/// Shared parser for `unique_together` and `index_together` container
10242/// attrs. Accepts both shapes:
10243///
10244///   * `attr = "col1, col2"`              — auto-derived index name.
10245///   * `attr(columns = "col1, col2", name = "...")` — explicit name.
10246///
10247/// Returns `(columns, name)`.
10248fn parse_together_attr(
10249    meta: &syn::meta::ParseNestedMeta<'_>,
10250    attr: &str,
10251) -> syn::Result<(Vec<String>, Option<String>)> {
10252    // Disambiguate by whether the next token is `=` (key-value) or
10253    // `(` (parenthesized).
10254    if meta.input.peek(syn::Token![=]) {
10255        let cols_lit: LitStr = meta.value()?.parse()?;
10256        let columns = split_field_list(&cols_lit.value());
10257        check_together_columns(meta, attr, &columns)?;
10258        return Ok((columns, None));
10259    }
10260    let mut columns: Option<Vec<String>> = None;
10261    let mut name: Option<String> = None;
10262    meta.parse_nested_meta(|inner| {
10263        if inner.path.is_ident("columns") {
10264            let s: LitStr = inner.value()?.parse()?;
10265            columns = Some(split_field_list(&s.value()));
10266            return Ok(());
10267        }
10268        if inner.path.is_ident("name") {
10269            let s: LitStr = inner.value()?.parse()?;
10270            name = Some(s.value());
10271            return Ok(());
10272        }
10273        Err(inner.error("unknown sub-attribute (supported: `columns`, `name`)"))
10274    })?;
10275    let columns = columns.ok_or_else(|| {
10276        meta.error(format!(
10277            "{attr}(...) requires a `columns = \"col1, col2\"` argument",
10278        ))
10279    })?;
10280    check_together_columns(meta, attr, &columns)?;
10281    Ok((columns, name))
10282}
10283
10284fn check_together_columns(
10285    meta: &syn::meta::ParseNestedMeta<'_>,
10286    attr: &str,
10287    columns: &[String],
10288) -> syn::Result<()> {
10289    if columns.len() < 2 {
10290        let single = if attr == "unique_together" {
10291            "#[rustango(unique)] on the field"
10292        } else {
10293            "#[rustango(index)] on the field"
10294        };
10295        return Err(meta.error(format!(
10296            "{attr} expects two or more columns; for a single-column equivalent use {single}",
10297        )));
10298    }
10299    Ok(())
10300}
10301
10302/// Parse the fieldsets DSL: pipe-separated sections, optional
10303/// `"Title:"` prefix on each, comma-separated field names after.
10304/// Examples:
10305/// * `"name, office"` → one untitled section with two fields
10306/// * `"Identity: name, office | Metadata: created_at"` → two titled
10307///   sections
10308///
10309/// Returns `(title, fields)` pairs. Title is `""` when no prefix.
10310fn parse_fieldset_list(raw: &str) -> Vec<(String, Vec<String>)> {
10311    raw.split('|')
10312        .map(str::trim)
10313        .filter(|s| !s.is_empty())
10314        .map(|section| {
10315            // Split off an optional `Title:` prefix (first colon).
10316            let (title, rest) = match section.split_once(':') {
10317                Some((title, rest)) if !title.contains(',') => (title.trim().to_owned(), rest),
10318                _ => (String::new(), section),
10319            };
10320            let fields = split_field_list(rest);
10321            (title, fields)
10322        })
10323        .collect()
10324}
10325
10326/// Parse `prepopulated_fields = "target:source[+src2,...]"` — each
10327/// comma-separated entry maps a target field to one or more source
10328/// fields joined with `+`. Whitespace around tokens is trimmed.
10329/// Entries missing `:` or with empty target/source lists are dropped.
10330fn parse_prepopulated_list(raw: &str) -> Vec<(String, Vec<String>)> {
10331    raw.split(',')
10332        .map(str::trim)
10333        .filter(|s| !s.is_empty())
10334        .filter_map(|entry| {
10335            let (target, sources_raw) = entry.split_once(':')?;
10336            let target = target.trim().to_owned();
10337            if target.is_empty() {
10338                return None;
10339            }
10340            let sources: Vec<String> = sources_raw
10341                .split('+')
10342                .map(|s| s.trim().to_owned())
10343                .filter(|s| !s.is_empty())
10344                .collect();
10345            if sources.is_empty() {
10346                return None;
10347            }
10348            Some((target, sources))
10349        })
10350        .collect()
10351}
10352
10353/// Parse Django-shape `formfield_overrides` — `"field:widget,field2:widget2"`
10354/// into `(field_name, widget_name)` pairs. Empty entries, missing `:`,
10355/// and empty halves drop silently — the macro layer only enforces shape,
10356/// not field-name vs. widget-name validity (those checks happen at
10357/// `AdminConfig` consumption time). Issue #359.
10358fn parse_formfield_overrides(raw: &str) -> Vec<(String, String)> {
10359    raw.split(',')
10360        .map(str::trim)
10361        .filter(|s| !s.is_empty())
10362        .filter_map(|entry| {
10363            let (field, widget) = entry.split_once(':')?;
10364            let field = field.trim().to_owned();
10365            let widget = widget.trim().to_owned();
10366            if field.is_empty() || widget.is_empty() {
10367                return None;
10368            }
10369            Some((field, widget))
10370        })
10371        .collect()
10372}
10373
10374/// Parse Django-shape ordering — `"name"` is ASC, `"-name"` is DESC.
10375/// Returns `(field_name, desc)` pairs in the same order as the input.
10376fn parse_ordering_list(raw: &str) -> Vec<(String, bool)> {
10377    raw.split(',')
10378        .map(str::trim)
10379        .filter(|s| !s.is_empty())
10380        .map(|spec| {
10381            spec.strip_prefix('-')
10382                .map_or((spec.to_owned(), false), |rest| {
10383                    (rest.trim().to_owned(), true)
10384                })
10385        })
10386        .collect()
10387}
10388
10389struct FieldAttrs {
10390    column: Option<String>,
10391    primary_key: bool,
10392    fk: Option<String>,
10393    o2o: Option<String>,
10394    on: Option<String>,
10395    /// `#[rustango(on_delete = "cascade" | "restrict" | "set_null" |
10396    /// "set_default" | "no_action")]` — Django-shape
10397    /// `ForeignKey(on_delete=…)`. Only meaningful when `fk` / `o2o` is
10398    /// also set; the macro errors at compile time if applied to a
10399    /// non-FK field. Threaded into `FieldSchema::fk_on_delete`. The
10400    /// DDL writer renders `ON DELETE <action>` after the constraint
10401    /// clause when this is `Some`; `None` falls back to the database
10402    /// default (NO ACTION on every backend rustango supports).
10403    on_delete: Option<String>,
10404    /// `#[rustango(related_name = "...")]` — Django-shape per-FK
10405    /// reverse-accessor override. When set, the derive emits
10406    /// `Parent::<related_name>[_pool]` instead of the container-level
10407    /// `default_related_name` or the `<child_snake>_set[_pool]`
10408    /// fallback. Only meaningful when `fk` / `o2o` is also set;
10409    /// silently ignored on non-FK fields. Follow-up to #816.
10410    related_name: Option<String>,
10411    max_length: Option<u32>,
10412    /// `#[rustango(vector(dims = N))]` — pgvector column dimension (#824).
10413    /// Threaded into `FieldType::Vector(N)` at emission. `None` → an
10414    /// unconstrained `vector` column.
10415    vector_dims: Option<u32>,
10416    /// `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID (#443).
10417    /// Threaded into `FieldType::Geometry(N)` at emission. `None` → an
10418    /// unconstrained `geometry(Point)` column (SRID 0).
10419    geometry_srid: Option<u32>,
10420    min: Option<i64>,
10421    max: Option<i64>,
10422    default: Option<String>,
10423    /// `#[rustango(auto_uuid)]` — UUID PK generated by Postgres
10424    /// `gen_random_uuid()`. Implies `auto + primary_key + default =
10425    /// "gen_random_uuid()"`. The Rust field type must be
10426    /// `uuid::Uuid` (or `Auto<Uuid>`); the column is excluded from
10427    /// INSERTs so the DB DEFAULT fires.
10428    auto_uuid: bool,
10429    /// `#[rustango(default_uuid_v7)]` — backend-neutral counterpart of
10430    /// `auto_uuid`. The PK value is generated **Rust-side** at insert
10431    /// time using `uuid::Uuid::now_v7()` (time-sortable UUIDv7) when
10432    /// the field is `Auto::Unset`, then bound as a normal parameter
10433    /// rather than relying on a per-dialect DB function. Issue #823
10434    /// (Eloquent `HasUuids`).
10435    ///
10436    /// Field type must be `Auto<uuid::Uuid>`. Implies `primary_key`.
10437    /// Composes with every backend (PG / MySQL / SQLite) — no
10438    /// `gen_random_uuid()` requirement on the database.
10439    default_uuid_v7: bool,
10440    /// `#[rustango(auto_now_add)]` — `created_at`-shape column.
10441    /// Server-set on insert, immutable from app code afterwards.
10442    /// Implies `auto + default = "now()"`. Field type must be
10443    /// `DateTime<Utc>`.
10444    auto_now_add: bool,
10445    /// `#[rustango(auto_now)]` — `updated_at`-shape column. Set on
10446    /// every insert AND every update. Implies `auto + default =
10447    /// "now()"`; the macro additionally rewrites `update_on` /
10448    /// `save_on` to bind `chrono::Utc::now()` instead of the user's
10449    /// field value.
10450    auto_now: bool,
10451    /// `#[rustango(soft_delete)]` — `deleted_at`-shape column. Type
10452    /// must be `Option<DateTime<Utc>>`. Triggers macro emission of
10453    /// `soft_delete_on(executor)` and `restore_on(executor)`
10454    /// methods on the model.
10455    soft_delete: bool,
10456    /// `#[rustango(unique)]` — adds a `UNIQUE` constraint inline on
10457    /// the column in the generated DDL.
10458    unique: bool,
10459    /// `#[rustango(index)]` or `#[rustango(index(name = "…", unique))]` —
10460    /// generates a `CREATE INDEX` for this column. `unique` here means
10461    /// `CREATE UNIQUE INDEX` (distinct from the `unique` constraint above).
10462    index: bool,
10463    index_unique: bool,
10464    index_name: Option<String>,
10465    /// Index access method (`"btree"` / `"gin"` / …). Defaults to
10466    /// `"btree"`. Issue #34.
10467    index_method: String,
10468    /// `#[rustango(generated_as = "EXPR")]` — emit `GENERATED ALWAYS
10469    /// AS (EXPR) STORED` in the column DDL. Read-only from app code:
10470    /// the macro skips this column from every INSERT and UPDATE
10471    /// path, so the database always recomputes the value from
10472    /// `EXPR`. Backlog item #35.
10473    generated_as: Option<String>,
10474    /// `#[rustango(help_text = "…")]` — Django-shape help text
10475    /// rendered below the admin form's input. Threaded into
10476    /// `FieldSchema::help_text` so admin / serializer / OpenAPI
10477    /// layers can read it.
10478    help_text: Option<String>,
10479    /// `#[rustango(choices = "value:Label, value:Label")]` — Django-shape
10480    /// enumerated allowed values. Threaded into `FieldSchema::choices`
10481    /// as a `&'static [(&'static str, &'static str)]` slice. When
10482    /// present, the admin form renders a `<select>` instead of `<input>`
10483    /// and the validator rejects values not in the list. Only meaningful
10484    /// for `FieldType::String`; the macro errors at compile time if
10485    /// applied to a non-string field.
10486    choices: Option<Vec<(String, String)>>,
10487    /// `#[rustango(db_comment = "…")]` — Django-shape DB-side column
10488    /// comment. Threaded into `FieldSchema::db_comment`. MySQL inlines
10489    /// the comment in CREATE TABLE; Postgres emits a separate
10490    /// `COMMENT ON COLUMN` statement after the table is created;
10491    /// SQLite silently drops the value (no native column comments).
10492    db_comment: Option<String>,
10493    /// `#[rustango(verbose_name = "…")]` — Django-shape human-readable
10494    /// label for the field. Threaded into `FieldSchema::verbose_name`
10495    /// so admin column headers, form labels, and other display
10496    /// surfaces can prefer the friendly caption over the Rust
10497    /// identifier. `None` means renderers fall back to the field name.
10498    verbose_name: Option<String>,
10499    /// `#[rustango(editable = false)]` — Django-shape opt-out from
10500    /// auto-generated form rendering. Defaults to `true` so existing
10501    /// fields keep their current admin / form behavior; setting
10502    /// `false` removes the field from the admin change-form entirely
10503    /// (the value is still visible on detail / list views, just not
10504    /// editable).
10505    editable: bool,
10506    /// `#[rustango(blank)]` / `#[rustango(blank = true)]` — Django-shape
10507    /// "form may submit empty even when DB is NOT NULL". Threaded into
10508    /// `FieldSchema::blank`. Defaults to `false`.
10509    blank: bool,
10510    /// `#[rustango(citext)]` / `#[rustango(citext = true)]` (#344) —
10511    /// Django-shape `CITextField`. Threaded into
10512    /// `FieldSchema::case_insensitive`. Only meaningful for `String`
10513    /// fields; the macro errors at derive time if applied elsewhere.
10514    case_insensitive: bool,
10515    /// `#[rustango(validators = "email,url")]` — Django-shape
10516    /// model-level validator chain. Comma-separated names that
10517    /// dispatch to the `validators::*` family in `validate_value`.
10518    /// Empty by default; fires on every typed INSERT/UPDATE.
10519    validators: Vec<String>,
10520}
10521
10522fn parse_field_attrs(field: &syn::Field) -> syn::Result<FieldAttrs> {
10523    let mut out = FieldAttrs {
10524        column: None,
10525        primary_key: false,
10526        fk: None,
10527        o2o: None,
10528        on: None,
10529        on_delete: None,
10530        related_name: None,
10531        max_length: None,
10532        vector_dims: None,
10533        geometry_srid: None,
10534        min: None,
10535        max: None,
10536        default: None,
10537        auto_uuid: false,
10538        default_uuid_v7: false,
10539        auto_now_add: false,
10540        auto_now: false,
10541        soft_delete: false,
10542        unique: false,
10543        index: false,
10544        index_unique: false,
10545        index_name: None,
10546        index_method: "btree".to_owned(),
10547        generated_as: None,
10548        help_text: None,
10549        choices: None,
10550        db_comment: None,
10551        verbose_name: None,
10552        editable: true,
10553        blank: false,
10554        case_insensitive: false,
10555        validators: Vec::new(),
10556    };
10557    for attr in &field.attrs {
10558        if !attr.path().is_ident("rustango") {
10559            continue;
10560        }
10561        attr.parse_nested_meta(|meta| {
10562            if meta.path.is_ident("column") {
10563                let s: LitStr = meta.value()?.parse()?;
10564                let name = s.value();
10565                validate_sql_identifier(&name, "column", s.span())?;
10566                out.column = Some(name);
10567                return Ok(());
10568            }
10569            if meta.path.is_ident("primary_key") {
10570                out.primary_key = true;
10571                return Ok(());
10572            }
10573            if meta.path.is_ident("fk") {
10574                let s: LitStr = meta.value()?.parse()?;
10575                out.fk = Some(s.value());
10576                return Ok(());
10577            }
10578            if meta.path.is_ident("o2o") {
10579                let s: LitStr = meta.value()?.parse()?;
10580                out.o2o = Some(s.value());
10581                return Ok(());
10582            }
10583            if meta.path.is_ident("on") {
10584                let s: LitStr = meta.value()?.parse()?;
10585                out.on = Some(s.value());
10586                return Ok(());
10587            }
10588            if meta.path.is_ident("on_delete") {
10589                let s: LitStr = meta.value()?.parse()?;
10590                let raw = s.value();
10591                let normalized = raw.trim().to_ascii_lowercase();
10592                // Validate at parse time so the user gets a clear span
10593                // rather than a downstream compile error in the emit.
10594                match normalized.as_str() {
10595                    "cascade" | "restrict" | "set_null" | "set_default" | "no_action" => {}
10596                    _ => {
10597                        return Err(syn::Error::new(
10598                            s.span(),
10599                            format!(
10600                                "unknown on_delete action `{raw}`; expected one of \
10601                                 `cascade`, `restrict`, `set_null`, `set_default`, `no_action`"
10602                            ),
10603                        ));
10604                    }
10605                }
10606                out.on_delete = Some(normalized);
10607                return Ok(());
10608            }
10609            if meta.path.is_ident("related_name") {
10610                let s: LitStr = meta.value()?.parse()?;
10611                let raw = s.value();
10612                if raw.trim().is_empty() {
10613                    return Err(syn::Error::new(
10614                        s.span(),
10615                        "`related_name` must be a non-empty identifier",
10616                    ));
10617                }
10618                // Validate as a Rust-ident shape — the value becomes
10619                // a method name on the parent type. Same rule as
10620                // `default_related_name` so the two surfaces match.
10621                if !raw
10622                    .chars()
10623                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
10624                    || raw.starts_with(char::is_numeric)
10625                {
10626                    return Err(syn::Error::new(
10627                        s.span(),
10628                        "`related_name` must be snake_case ASCII (lowercase letters, \
10629                         digits, underscores; no leading digit)",
10630                    ));
10631                }
10632                out.related_name = Some(raw);
10633                return Ok(());
10634            }
10635            if meta.path.is_ident("max_length") {
10636                let lit: syn::LitInt = meta.value()?.parse()?;
10637                out.max_length = Some(lit.base10_parse::<u32>()?);
10638                return Ok(());
10639            }
10640            // `#[rustango(vector(dims = N))]` — pgvector column
10641            // dimension (#824). Nested-meta form so it reads like the
10642            // other typed-column attrs.
10643            if meta.path.is_ident("vector") {
10644                meta.parse_nested_meta(|inner| {
10645                    if inner.path.is_ident("dims") {
10646                        let lit: syn::LitInt = inner.value()?.parse()?;
10647                        out.vector_dims = Some(lit.base10_parse::<u32>()?);
10648                        return Ok(());
10649                    }
10650                    Err(inner.error("unknown `vector` attribute (supported: `dims`)"))
10651                })?;
10652                return Ok(());
10653            }
10654            // `#[rustango(geometry(srid = N))]` — PostGIS geometry SRID
10655            // (#443). Nested-meta form, mirroring `vector(dims = N)`.
10656            if meta.path.is_ident("geometry") {
10657                meta.parse_nested_meta(|inner| {
10658                    if inner.path.is_ident("srid") {
10659                        let lit: syn::LitInt = inner.value()?.parse()?;
10660                        out.geometry_srid = Some(lit.base10_parse::<u32>()?);
10661                        return Ok(());
10662                    }
10663                    Err(inner.error("unknown `geometry` attribute (supported: `srid`)"))
10664                })?;
10665                return Ok(());
10666            }
10667            if meta.path.is_ident("min") {
10668                out.min = Some(parse_signed_i64(&meta)?);
10669                return Ok(());
10670            }
10671            if meta.path.is_ident("max") {
10672                out.max = Some(parse_signed_i64(&meta)?);
10673                return Ok(());
10674            }
10675            if meta.path.is_ident("default") {
10676                let s: LitStr = meta.value()?.parse()?;
10677                out.default = Some(s.value());
10678                return Ok(());
10679            }
10680            if meta.path.is_ident("generated_as") {
10681                let s: LitStr = meta.value()?.parse()?;
10682                out.generated_as = Some(s.value());
10683                return Ok(());
10684            }
10685            if meta.path.is_ident("help_text") {
10686                let s: LitStr = meta.value()?.parse()?;
10687                out.help_text = Some(s.value());
10688                return Ok(());
10689            }
10690            if meta.path.is_ident("choices") {
10691                let s: LitStr = meta.value()?.parse()?;
10692                let raw = s.value();
10693                let mut pairs: Vec<(String, String)> = Vec::new();
10694                for chunk in raw.split(',') {
10695                    let chunk = chunk.trim();
10696                    if chunk.is_empty() {
10697                        continue;
10698                    }
10699                    let (value, label) = match chunk.split_once(':') {
10700                        Some((v, l)) => (v.trim().to_owned(), l.trim().to_owned()),
10701                        None => (chunk.to_owned(), chunk.to_owned()),
10702                    };
10703                    if value.is_empty() {
10704                        return Err(syn::Error::new(
10705                            s.span(),
10706                            "`choices` entry has empty value before `:`",
10707                        ));
10708                    }
10709                    pairs.push((value, label));
10710                }
10711                if pairs.is_empty() {
10712                    return Err(syn::Error::new(
10713                        s.span(),
10714                        "`choices = \"…\"` must contain at least one value",
10715                    ));
10716                }
10717                out.choices = Some(pairs);
10718                return Ok(());
10719            }
10720            if meta.path.is_ident("db_comment") {
10721                let s: LitStr = meta.value()?.parse()?;
10722                out.db_comment = Some(s.value());
10723                return Ok(());
10724            }
10725            if meta.path.is_ident("verbose_name") {
10726                let s: LitStr = meta.value()?.parse()?;
10727                out.verbose_name = Some(s.value());
10728                return Ok(());
10729            }
10730            if meta.path.is_ident("editable") {
10731                // Two forms accepted:
10732                //   #[rustango(editable = false)] / true — explicit
10733                //   #[rustango(editable)] — flag form (= true, the
10734                //   default, so harmless; included for symmetry)
10735                if let Ok(v) = meta.value() {
10736                    let lit: syn::LitBool = v.parse()?;
10737                    out.editable = lit.value;
10738                } else {
10739                    out.editable = true;
10740                }
10741                return Ok(());
10742            }
10743            if meta.path.is_ident("blank") {
10744                // Two forms accepted:
10745                //   #[rustango(blank)] — flag form, true
10746                //   #[rustango(blank = false)] / true — explicit
10747                if let Ok(v) = meta.value() {
10748                    let lit: syn::LitBool = v.parse()?;
10749                    out.blank = lit.value;
10750                } else {
10751                    out.blank = true;
10752                }
10753                return Ok(());
10754            }
10755            if meta.path.is_ident("citext") {
10756                // Django-parity CITextField (#344). Two forms:
10757                //   #[rustango(citext)]          — flag form, true
10758                //   #[rustango(citext = true)]   — explicit
10759                //   #[rustango(citext = false)]  — explicit opt-out
10760                // String-only validation lives in the field-type
10761                // emitter (the FieldType discriminant is computed in
10762                // `detect_type`); the macro records the flag and the
10763                // DDL writer emits dialect-specific COLLATE / CITEXT.
10764                if let Ok(v) = meta.value() {
10765                    let lit: syn::LitBool = v.parse()?;
10766                    out.case_insensitive = lit.value;
10767                } else {
10768                    out.case_insensitive = true;
10769                }
10770                return Ok(());
10771            }
10772            if meta.path.is_ident("validators") {
10773                let s: LitStr = meta.value()?.parse()?;
10774                let raw = s.value();
10775                out.validators = raw
10776                    .split(',')
10777                    .map(str::trim)
10778                    .filter(|s| !s.is_empty())
10779                    .map(str::to_owned)
10780                    .collect();
10781                if out.validators.is_empty() {
10782                    return Err(syn::Error::new(
10783                        s.span(),
10784                        "`validators = \"…\"` must list at least one name",
10785                    ));
10786                }
10787                return Ok(());
10788            }
10789            if meta.path.is_ident("auto_uuid") {
10790                out.auto_uuid = true;
10791                // Implied: PK + auto + DEFAULT gen_random_uuid().
10792                // Each is also explicitly settable; the explicit
10793                // value wins if conflicting.
10794                out.primary_key = true;
10795                if out.default.is_none() {
10796                    out.default = Some("gen_random_uuid()".into());
10797                }
10798                return Ok(());
10799            }
10800            if meta.path.is_ident("default_uuid_v7") {
10801                // Backend-neutral counterpart of `auto_uuid` — issue #823.
10802                // No SQL DEFAULT (the macro fills the value Rust-side
10803                // before binding); just mark the field as PK + Auto
10804                // so the insert path is the `Auto::Unset → generate`
10805                // branch.
10806                out.default_uuid_v7 = true;
10807                out.primary_key = true;
10808                return Ok(());
10809            }
10810            if meta.path.is_ident("auto_now_add") {
10811                out.auto_now_add = true;
10812                if out.default.is_none() {
10813                    out.default = Some("now()".into());
10814                }
10815                return Ok(());
10816            }
10817            if meta.path.is_ident("auto_now") {
10818                out.auto_now = true;
10819                if out.default.is_none() {
10820                    out.default = Some("now()".into());
10821                }
10822                return Ok(());
10823            }
10824            if meta.path.is_ident("soft_delete") {
10825                out.soft_delete = true;
10826                return Ok(());
10827            }
10828            if meta.path.is_ident("unique") {
10829                out.unique = true;
10830                return Ok(());
10831            }
10832            if meta.path.is_ident("index") {
10833                out.index = true;
10834                // Optional sub-attrs: #[rustango(index(unique, name = "…", method = "gin"))]
10835                if meta.input.peek(syn::token::Paren) {
10836                    meta.parse_nested_meta(|inner| {
10837                        if inner.path.is_ident("unique") {
10838                            out.index_unique = true;
10839                            return Ok(());
10840                        }
10841                        if inner.path.is_ident("name") {
10842                            let s: LitStr = inner.value()?.parse()?;
10843                            out.index_name = Some(s.value());
10844                            return Ok(());
10845                        }
10846                        if inner.path.is_ident("method") {
10847                            let s: LitStr = inner.value()?.parse()?;
10848                            let v = s.value();
10849                            match v.as_str() {
10850                                "btree" | "gin" | "gist" | "brin" | "spgist" | "hash" | "bloom" => {
10851                                    out.index_method = v;
10852                                }
10853                                other => {
10854                                    return Err(inner.error(format!(
10855                                        "unknown index method `{other}` (supported: btree, gin, gist, brin, spgist, hash, bloom)",
10856                                    )));
10857                                }
10858                            }
10859                            return Ok(());
10860                        }
10861                        Err(inner.error(
10862                            "unknown index sub-attribute (supported: `unique`, `name`, `method`)",
10863                        ))
10864                    })?;
10865                }
10866                return Ok(());
10867            }
10868            Err(meta.error("unknown rustango field attribute"))
10869        })?;
10870    }
10871    Ok(out)
10872}
10873
10874/// Parse a signed integer literal, accepting optional leading `-`.
10875fn parse_signed_i64(meta: &syn::meta::ParseNestedMeta<'_>) -> syn::Result<i64> {
10876    let expr: syn::Expr = meta.value()?.parse()?;
10877    match expr {
10878        syn::Expr::Lit(syn::ExprLit {
10879            lit: syn::Lit::Int(lit),
10880            ..
10881        }) => lit.base10_parse::<i64>(),
10882        syn::Expr::Unary(syn::ExprUnary {
10883            op: syn::UnOp::Neg(_),
10884            expr,
10885            ..
10886        }) => {
10887            if let syn::Expr::Lit(syn::ExprLit {
10888                lit: syn::Lit::Int(lit),
10889                ..
10890            }) = *expr
10891            {
10892                let v: i64 = lit.base10_parse()?;
10893                Ok(-v)
10894            } else {
10895                Err(syn::Error::new_spanned(expr, "expected integer literal"))
10896            }
10897        }
10898        other => Err(syn::Error::new_spanned(
10899            other,
10900            "expected integer literal (signed)",
10901        )),
10902    }
10903}
10904
10905struct FieldInfo<'a> {
10906    ident: &'a syn::Ident,
10907    column: String,
10908    primary_key: bool,
10909    /// `true` when the Rust type was `Auto<T>` — the INSERT path will
10910    /// skip this column when `Auto::Unset` and emit it under
10911    /// `RETURNING` so Postgres' sequence DEFAULT fills in the value.
10912    auto: bool,
10913    /// The original field type, e.g. `i64` or `Option<String>`. Emitted as
10914    /// the `Column::Value` associated type for typed-column tokens.
10915    value_ty: &'a Type,
10916    /// `FieldType` variant tokens (`#root::core::FieldType::I64`).
10917    field_type_tokens: TokenStream2,
10918    schema: TokenStream2,
10919    from_row_init: TokenStream2,
10920    /// Variant of [`Self::from_row_init`] that reads the column via
10921    /// `format!("{prefix}__{col}")` so a model can be decoded out of
10922    /// the aliased columns of a JOINed row. Drives slice 9.0d's
10923    /// `Self::__rustango_from_aliased_row(row, prefix)` per-Model
10924    /// helper that `select_related` calls when stitching loaded FKs.
10925    from_aliased_row_init: TokenStream2,
10926    /// Inner type from a `ForeignKey<T, K>` field, if any. The reverse-
10927    /// relation helper emit (`Author::<child>_set`) needs to know `T`
10928    /// to point the generated method at the right child model.
10929    fk_inner: Option<Type>,
10930    /// `K`'s scalar kind for a `ForeignKey<T, K>` field. Mirrors
10931    /// `kind` (since ForeignKey detection sets `kind` to K's
10932    /// underlying type) but stored separately for clarity at the
10933    /// `FkRelation` construction site, which only sees the FK's
10934    /// surface fields.
10935    fk_pk_kind: DetectedKind,
10936    /// `true` when the field is `Option<ForeignKey<T, K>>` rather than
10937    /// the bare `ForeignKey<T, K>`. Routes the load_related and
10938    /// fk_pk_access emitters to wrap assignments / accessors in
10939    /// `Some(...)` / `as_ref().map(...)` respectively, so a nullable
10940    /// FK column compiles end-to-end. The DDL writer reads this off
10941    /// the field schema (`nullable` flag); the macro just needs to
10942    /// keep the Rust-side codegen consistent.
10943    nullable: bool,
10944    /// `true` when this column was marked `#[rustango(auto_now)]` —
10945    /// `update_on` / `save_on` bind `chrono::Utc::now()` for this
10946    /// column instead of the user-supplied value, so `updated_at`
10947    /// always reflects the latest write without the caller having
10948    /// to remember to set it.
10949    auto_now: bool,
10950    /// `true` when this column was marked `#[rustango(auto_now_add)]`
10951    /// — the column is server-set on INSERT (DB DEFAULT) and
10952    /// **immutable** afterwards. `update_on` / `save_on` skip the
10953    /// column entirely so a stale `created_at` value in memory never
10954    /// rewrites the persisted timestamp.
10955    auto_now_add: bool,
10956    /// `true` when this column was marked `#[rustango(soft_delete)]`.
10957    /// Triggers emission of `soft_delete_on(executor)` and
10958    /// `restore_on(executor)` on the model's inherent impl. There is
10959    /// at most one such column per model — emission asserts this.
10960    soft_delete: bool,
10961    /// `Some` when this column was marked
10962    /// `#[rustango(generated_as = "EXPR")]`. The macro skips it from
10963    /// every INSERT and UPDATE path; the database recomputes the
10964    /// value from `EXPR`. Backlog item #35.
10965    generated_as: Option<String>,
10966    /// `true` when this column was marked
10967    /// `#[rustango(default_uuid_v7)]`. Routes `collect_fields` to
10968    /// emit an `insert_push` that auto-fills an `Auto::Unset` value
10969    /// with `Uuid::now_v7()` before binding, so the PK is generated
10970    /// Rust-side and the column is always present in the INSERT
10971    /// statement (no DB DEFAULT requirement). Issue #823.
10972    default_uuid_v7: bool,
10973    /// `Some` when this FK field carried `#[rustango(related_name =
10974    /// "...")]`. Threaded into [`FkRelation::related_name`] and
10975    /// consumed by [`reverse_helper_tokens`] to override the default
10976    /// `<child_snake>_set` accessor name. Follow-up to #816.
10977    related_name: Option<String>,
10978}
10979
10980/// Reject table names that won't survive SQL identifier
10981/// derivation downstream. Postgres' regular-identifier rule
10982/// (`[a-zA-Z_][a-zA-Z0-9_]*`) is the safe shape: it round-trips
10983/// through the framework's unquoted FK / index / constraint name
10984/// emission without surprises. We also disallow leading-digit and
10985/// the empty string for clarity.
10986///
10987/// Reserved-word collisions (`select`, `from`, …) aren't flagged
10988/// here — those produce a runtime error from the SQL parser,
10989/// which is loud enough; statically enumerating reserved words
10990/// across the three supported dialects is more friction than help.
10991///
10992/// Backlog item #65.
10993fn validate_table_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> {
10994    validate_sql_identifier(name, "table", span)
10995}
10996
10997/// Reject SQL identifiers that compile but break downstream SQL
10998/// generation. Same rule for tables and columns: `[a-zA-Z_][a-zA-Z0-9_]*`.
10999/// `kind` is "table" / "column" — used for the error message so users
11000/// see which attribute caused the failure.
11001fn validate_sql_identifier(name: &str, kind: &str, span: proc_macro2::Span) -> syn::Result<()> {
11002    if name.is_empty() {
11003        return Err(syn::Error::new(
11004            span,
11005            format!("`{kind} = \"\"` is not a valid SQL identifier"),
11006        ));
11007    }
11008    let mut chars = name.chars();
11009    let first = chars.next().unwrap();
11010    if !(first.is_ascii_alphabetic() || first == '_') {
11011        return Err(syn::Error::new(
11012            span,
11013            format!("{kind} name `{name}` must start with a letter or underscore (got {first:?})"),
11014        ));
11015    }
11016    for c in chars {
11017        if !(c.is_ascii_alphanumeric() || c == '_') {
11018            return Err(syn::Error::new(
11019                span,
11020                format!(
11021                    "{kind} name `{name}` contains invalid character {c:?} — \
11022                     SQL identifiers must match `[a-zA-Z_][a-zA-Z0-9_]*`. \
11023                     Hyphens in particular break FK / index name derivation \
11024                     downstream; use underscores instead (e.g. `{}`)",
11025                    name.replace(|x: char| !x.is_ascii_alphanumeric() && x != '_', "_"),
11026                ),
11027            ));
11028        }
11029    }
11030    Ok(())
11031}
11032
11033fn process_field<'a>(field: &'a syn::Field, table: &str) -> syn::Result<FieldInfo<'a>> {
11034    let root = rustango_root();
11035    let attrs = parse_field_attrs(field)?;
11036    let ident = field
11037        .ident
11038        .as_ref()
11039        .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
11040    let name = ident.to_string();
11041    let column = attrs.column.clone().unwrap_or_else(|| name.clone());
11042    let primary_key = attrs.primary_key;
11043    let DetectedType {
11044        kind,
11045        nullable,
11046        auto: detected_auto,
11047        fk_inner,
11048    } = detect_type(&field.ty)?;
11049    check_bound_compatibility(field, &attrs, kind)?;
11050    let auto = detected_auto;
11051    // Mixin attributes piggyback on the existing `Auto<T>` skip-on-
11052    // INSERT path: the user must wrap the field in `Auto<T>`, which
11053    // marks the column as DB-default-supplied. The mixin attrs then
11054    // layer in the SQL default (`now()` / `gen_random_uuid()`) and,
11055    // for `auto_now`, force the value on UPDATE too.
11056    if attrs.auto_uuid {
11057        if kind != DetectedKind::Uuid {
11058            return Err(syn::Error::new_spanned(
11059                field,
11060                "`#[rustango(auto_uuid)]` requires the field type to be \
11061                 `Auto<uuid::Uuid>`",
11062            ));
11063        }
11064        if !detected_auto {
11065            return Err(syn::Error::new_spanned(
11066                field,
11067                "`#[rustango(auto_uuid)]` requires the field type to be \
11068                 wrapped in `Auto<...>` so the macro skips the column on \
11069                 INSERT and the DB DEFAULT (`gen_random_uuid()`) fires",
11070            ));
11071        }
11072    }
11073    if attrs.default_uuid_v7 {
11074        if kind != DetectedKind::Uuid {
11075            return Err(syn::Error::new_spanned(
11076                field,
11077                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11078                 `Auto<uuid::Uuid>`",
11079            ));
11080        }
11081        if !detected_auto {
11082            return Err(syn::Error::new_spanned(
11083                field,
11084                "`#[rustango(default_uuid_v7)]` requires the field type to be \
11085                 wrapped in `Auto<...>` so the macro can detect the \
11086                 unset-vs-set state and fill a fresh UUIDv7 before INSERT",
11087            ));
11088        }
11089        if attrs.auto_uuid {
11090            return Err(syn::Error::new_spanned(
11091                field,
11092                "`#[rustango(default_uuid_v7)]` is mutually exclusive with \
11093                 `#[rustango(auto_uuid)]` — the former generates the UUID \
11094                 Rust-side, the latter relies on the DB's `gen_random_uuid()`. \
11095                 Pick one.",
11096            ));
11097        }
11098    }
11099    if attrs.auto_now_add || attrs.auto_now {
11100        if kind != DetectedKind::DateTime {
11101            return Err(syn::Error::new_spanned(
11102                field,
11103                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11104                 the field type to be `Auto<chrono::DateTime<chrono::Utc>>`",
11105            ));
11106        }
11107        if !detected_auto {
11108            return Err(syn::Error::new_spanned(
11109                field,
11110                "`#[rustango(auto_now_add)]` / `#[rustango(auto_now)]` require \
11111                 the field type to be wrapped in `Auto<...>` so the macro skips \
11112                 the column on INSERT and the DB DEFAULT (`now()`) fires",
11113            ));
11114        }
11115    }
11116    if attrs.soft_delete && !(kind == DetectedKind::DateTime && nullable) {
11117        return Err(syn::Error::new_spanned(
11118            field,
11119            "`#[rustango(soft_delete)]` requires the field type to be \
11120             `Option<chrono::DateTime<chrono::Utc>>`",
11121        ));
11122    }
11123    let is_mixin_auto =
11124        attrs.auto_uuid || attrs.default_uuid_v7 || attrs.auto_now_add || attrs.auto_now;
11125    if detected_auto && !primary_key && !is_mixin_auto {
11126        return Err(syn::Error::new_spanned(
11127            field,
11128            "`Auto<T>` is only valid on a `#[rustango(primary_key)]` field, \
11129             or on a field carrying one of `auto_uuid`, `auto_now_add`, or \
11130             `auto_now`",
11131        ));
11132    }
11133    if detected_auto && attrs.default.is_some() && !is_mixin_auto {
11134        return Err(syn::Error::new_spanned(
11135            field,
11136            "`#[rustango(default = \"…\")]` is redundant on an `Auto<T>` field — \
11137             SERIAL / BIGSERIAL already supplies a default sequence.",
11138        ));
11139    }
11140    if fk_inner.is_some() && primary_key {
11141        return Err(syn::Error::new_spanned(
11142            field,
11143            "`ForeignKey<T>` is not allowed on a primary-key field — \
11144             a row's PK is its own identity, not a reference to a parent.",
11145        ));
11146    }
11147    if attrs.generated_as.is_some() {
11148        if primary_key {
11149            return Err(syn::Error::new_spanned(
11150                field,
11151                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11152                 primary-key field — a PK must be writable so the row \
11153                 has an identity at INSERT time.",
11154            ));
11155        }
11156        if attrs.default.is_some() {
11157            return Err(syn::Error::new_spanned(
11158                field,
11159                "`#[rustango(generated_as = \"…\")]` cannot combine with \
11160                 `default = \"…\"` — Postgres rejects DEFAULT on \
11161                 generated columns. The expression IS the default.",
11162            ));
11163        }
11164        if detected_auto {
11165            return Err(syn::Error::new_spanned(
11166                field,
11167                "`#[rustango(generated_as = \"…\")]` is not allowed on \
11168                 an `Auto<T>` field — generated columns are computed \
11169                 by the DB, not server-assigned via a sequence. Use a \
11170                 plain Rust type (e.g. `f64`).",
11171            ));
11172        }
11173        if fk_inner.is_some() {
11174            return Err(syn::Error::new_spanned(
11175                field,
11176                "`#[rustango(generated_as = \"…\")]` is not allowed on a \
11177                 ForeignKey field.",
11178            ));
11179        }
11180    }
11181    let relation = relation_tokens(field, &attrs, fk_inner, table)?;
11182    let column_lit = column.as_str();
11183    // pgvector (#824): the `vector(dims = N)` attribute supplies the
11184    // dimension that the bare `Vector` Rust type can't carry, so emit
11185    // `FieldType::Vector(N)` here rather than the `variant_tokens`
11186    // fallback of `Vector(0)`.
11187    let field_type_tokens = if kind == DetectedKind::Vector {
11188        let root = rustango_root();
11189        let dims = attrs.vector_dims.unwrap_or(0);
11190        quote!(#root::core::FieldType::Vector(#dims))
11191    } else if kind == DetectedKind::Geometry {
11192        // PostGIS (#443): the `geometry(srid = N)` attribute supplies the
11193        // SRID that the bare `Point` Rust type can't carry, so emit
11194        // `FieldType::Geometry(N)` rather than the `Geometry(0)` fallback.
11195        let root = rustango_root();
11196        let srid = attrs.geometry_srid.unwrap_or(0);
11197        quote!(#root::core::FieldType::Geometry(#srid))
11198    } else {
11199        kind.variant_tokens()
11200    };
11201    let max_length = optional_u32(attrs.max_length);
11202    let min = optional_i64(attrs.min);
11203    let max = optional_i64(attrs.max);
11204    let default = optional_str(attrs.default.as_deref());
11205
11206    let unique = attrs.unique;
11207    let generated_as = optional_str(attrs.generated_as.as_deref());
11208    let help_text = optional_str(attrs.help_text.as_deref());
11209    let choices = optional_choices(attrs.choices.as_deref());
11210    let db_comment = optional_str(attrs.db_comment.as_deref());
11211    let verbose_name = optional_str(attrs.verbose_name.as_deref());
11212    let editable = attrs.editable;
11213    let blank = attrs.blank;
11214    let case_insensitive = attrs.case_insensitive;
11215    let validators_lits: Vec<&str> = attrs.validators.iter().map(String::as_str).collect();
11216    if attrs.on_delete.is_some() && attrs.fk.is_none() && attrs.o2o.is_none() {
11217        return Err(syn::Error::new_spanned(
11218            field,
11219            "`#[rustango(on_delete = \"…\")]` requires either `fk = \"<table>\"` \
11220             or `o2o = \"<table>\"` on the same field — it has no meaning on a \
11221             non-FK column.",
11222        ));
11223    }
11224    let fk_on_delete = match attrs.on_delete.as_deref() {
11225        None => quote!(::core::option::Option::None),
11226        Some(action) => {
11227            let variant = match action {
11228                "cascade" => quote!(Cascade),
11229                "restrict" => quote!(Restrict),
11230                "set_null" => quote!(SetNull),
11231                "set_default" => quote!(SetDefault),
11232                "no_action" => quote!(NoAction),
11233                // parse_field_attrs already validated this — guard against future drift.
11234                other => unreachable!("on_delete `{other}` should have been rejected at parse"),
11235            };
11236            quote!(::core::option::Option::Some(
11237                #root::core::OnDeleteAction::#variant
11238            ))
11239        }
11240    };
11241    let schema = quote! {
11242        #root::core::FieldSchema {
11243            name: #name,
11244            column: #column_lit,
11245            ty: #field_type_tokens,
11246            nullable: #nullable,
11247            primary_key: #primary_key,
11248            relation: #relation,
11249            max_length: #max_length,
11250            min: #min,
11251            max: #max,
11252            default: #default,
11253            auto: #auto,
11254            unique: #unique,
11255            generated_as: #generated_as,
11256            help_text: #help_text,
11257            choices: #choices,
11258            db_comment: #db_comment,
11259            verbose_name: #verbose_name,
11260            editable: #editable,
11261            blank: #blank,
11262            case_insensitive: #case_insensitive,
11263            fk_on_delete: #fk_on_delete,
11264            validators: &[ #(#validators_lits),* ],
11265        }
11266    };
11267
11268    let from_row_init = quote! {
11269        #ident: #root::sql::sqlx::Row::try_get(row, #column_lit)?
11270    };
11271    let from_aliased_row_init = quote! {
11272        #ident: #root::sql::sqlx::Row::try_get(
11273            row,
11274            ::std::format!("{}__{}", prefix, #column_lit).as_str(),
11275        )?
11276    };
11277
11278    Ok(FieldInfo {
11279        ident,
11280        column,
11281        primary_key,
11282        auto,
11283        value_ty: &field.ty,
11284        field_type_tokens,
11285        schema,
11286        from_row_init,
11287        from_aliased_row_init,
11288        fk_inner: fk_inner.cloned(),
11289        fk_pk_kind: kind,
11290        nullable,
11291        auto_now: attrs.auto_now,
11292        auto_now_add: attrs.auto_now_add,
11293        soft_delete: attrs.soft_delete,
11294        generated_as: attrs.generated_as.clone(),
11295        default_uuid_v7: attrs.default_uuid_v7,
11296        related_name: attrs.related_name.clone(),
11297    })
11298}
11299
11300fn check_bound_compatibility(
11301    field: &syn::Field,
11302    attrs: &FieldAttrs,
11303    kind: DetectedKind,
11304) -> syn::Result<()> {
11305    if attrs.max_length.is_some() && kind != DetectedKind::String {
11306        return Err(syn::Error::new_spanned(
11307            field,
11308            "`max_length` is only valid on `String` fields (or `Option<String>`)",
11309        ));
11310    }
11311    if attrs.choices.is_some() && kind != DetectedKind::String {
11312        return Err(syn::Error::new_spanned(
11313            field,
11314            "`choices` is only valid on `String` fields (or `Option<String>`) — \
11315             integer-valued enumerations should be modeled with a Rust enum and \
11316             custom (de)serializer for now",
11317        ));
11318    }
11319    if (attrs.min.is_some() || attrs.max.is_some()) && !kind.is_integer() {
11320        return Err(syn::Error::new_spanned(
11321            field,
11322            "`min` / `max` are only valid on integer fields (`i32`, `i64`, optionally Option-wrapped)",
11323        ));
11324    }
11325    if let (Some(min), Some(max)) = (attrs.min, attrs.max) {
11326        if min > max {
11327            return Err(syn::Error::new_spanned(
11328                field,
11329                format!("`min` ({min}) is greater than `max` ({max})"),
11330            ));
11331        }
11332    }
11333    Ok(())
11334}
11335
11336fn optional_u32(value: Option<u32>) -> TokenStream2 {
11337    if let Some(v) = value {
11338        quote!(::core::option::Option::Some(#v))
11339    } else {
11340        quote!(::core::option::Option::None)
11341    }
11342}
11343
11344fn optional_i64(value: Option<i64>) -> TokenStream2 {
11345    if let Some(v) = value {
11346        quote!(::core::option::Option::Some(#v))
11347    } else {
11348        quote!(::core::option::Option::None)
11349    }
11350}
11351
11352fn optional_str(value: Option<&str>) -> TokenStream2 {
11353    if let Some(v) = value {
11354        quote!(::core::option::Option::Some(#v))
11355    } else {
11356        quote!(::core::option::Option::None)
11357    }
11358}
11359
11360fn optional_choices(pairs: Option<&[(String, String)]>) -> TokenStream2 {
11361    let Some(pairs) = pairs else {
11362        return quote!(::core::option::Option::None);
11363    };
11364    let entries = pairs.iter().map(|(v, l)| quote!((#v, #l)));
11365    quote!(::core::option::Option::Some(&[#(#entries),*]))
11366}
11367
11368fn relation_tokens(
11369    field: &syn::Field,
11370    attrs: &FieldAttrs,
11371    fk_inner: Option<&syn::Type>,
11372    table: &str,
11373) -> syn::Result<TokenStream2> {
11374    let root = rustango_root();
11375    if let Some(inner) = fk_inner {
11376        if attrs.fk.is_some() || attrs.o2o.is_some() {
11377            return Err(syn::Error::new_spanned(
11378                field,
11379                "`ForeignKey<T>` already declares the FK target via the type parameter — \
11380                 remove the `fk = \"…\"` / `o2o = \"…\"` attribute.",
11381            ));
11382        }
11383        let on = attrs.on.as_deref().unwrap_or("id");
11384        return Ok(quote! {
11385            ::core::option::Option::Some(#root::core::Relation::Fk {
11386                to: <#inner as #root::core::Model>::SCHEMA.table,
11387                on: #on,
11388            })
11389        });
11390    }
11391    match (&attrs.fk, &attrs.o2o) {
11392        (Some(_), Some(_)) => Err(syn::Error::new_spanned(
11393            field,
11394            "`fk` and `o2o` are mutually exclusive",
11395        )),
11396        (Some(to), None) => {
11397            let on = attrs.on.as_deref().unwrap_or("id");
11398            // Self-FK sentinel — `#[rustango(fk = "self")]` resolves to
11399            // the model's own table. Threaded as a literal string at
11400            // macro-expansion time to sidestep the const-eval cycle
11401            // that `Self::SCHEMA.table` would create when referenced
11402            // inside Self::SCHEMA's own initializer.
11403            let resolved = if to == "self" { table } else { to };
11404            Ok(quote! {
11405                ::core::option::Option::Some(#root::core::Relation::Fk { to: #resolved, on: #on })
11406            })
11407        }
11408        (None, Some(to)) => {
11409            let on = attrs.on.as_deref().unwrap_or("id");
11410            let resolved = if to == "self" { table } else { to };
11411            Ok(quote! {
11412                ::core::option::Option::Some(#root::core::Relation::O2O { to: #resolved, on: #on })
11413            })
11414        }
11415        (None, None) => {
11416            if attrs.on.is_some() {
11417                return Err(syn::Error::new_spanned(
11418                    field,
11419                    "`on` requires `fk` or `o2o`",
11420                ));
11421            }
11422            Ok(quote!(::core::option::Option::None))
11423        }
11424    }
11425}
11426
11427/// Mirrors `rustango_core::FieldType`. Local copy so the macro can reason
11428/// about kinds without depending on `rustango-core` (which would require a
11429/// proc-macro/normal split it doesn't have today).
11430#[derive(Clone, Copy, PartialEq, Eq)]
11431enum DetectedKind {
11432    I16,
11433    I32,
11434    I64,
11435    F32,
11436    F64,
11437    Bool,
11438    String,
11439    DateTime,
11440    Date,
11441    Time,
11442    Uuid,
11443    Json,
11444    Decimal,
11445    Binary,
11446    /// `Array<String>` → PG `text[]` (#341).
11447    ArrayText,
11448    /// `Array<i32>` → PG `integer[]` (#341).
11449    ArrayInt,
11450    /// `Array<i64>` → PG `bigint[]` (#341).
11451    ArrayBigInt,
11452    /// `Range<i32>` → PG `int4range` (#343).
11453    RangeInt,
11454    /// `Range<i64>` → PG `int8range` (#343).
11455    RangeBigInt,
11456    /// `Range<Decimal>` → PG `numrange` (#343).
11457    RangeNumeric,
11458    /// `Range<NaiveDate>` → PG `daterange` (#343).
11459    RangeDate,
11460    /// `Range<DateTime<Utc>>` → PG `tstzrange` (#343).
11461    RangeDateTime,
11462    /// `HStore` → PG `hstore` (#342).
11463    HStore,
11464    /// `Vector` → pgvector `vector(N)` (#824). The dimension `N` comes
11465    /// from the `#[rustango(vector(dims = N))]` field attribute, threaded
11466    /// in at the `FieldType` emission site (not carried on this enum).
11467    Vector,
11468    /// `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID comes
11469    /// from the `#[rustango(geometry(srid = N))]` field attribute,
11470    /// threaded in at the `FieldType` emission site.
11471    Geometry,
11472}
11473
11474impl DetectedKind {
11475    fn variant_tokens(self) -> TokenStream2 {
11476        let root = rustango_root();
11477        match self {
11478            Self::I16 => quote!(#root::core::FieldType::I16),
11479            Self::I32 => quote!(#root::core::FieldType::I32),
11480            Self::I64 => quote!(#root::core::FieldType::I64),
11481            Self::F32 => quote!(#root::core::FieldType::F32),
11482            Self::F64 => quote!(#root::core::FieldType::F64),
11483            Self::Bool => quote!(#root::core::FieldType::Bool),
11484            Self::String => quote!(#root::core::FieldType::String),
11485            Self::DateTime => quote!(#root::core::FieldType::DateTime),
11486            Self::Date => quote!(#root::core::FieldType::Date),
11487            Self::Time => quote!(#root::core::FieldType::Time),
11488            Self::Uuid => quote!(#root::core::FieldType::Uuid),
11489            Self::Json => quote!(#root::core::FieldType::Json),
11490            Self::Decimal => quote!(#root::core::FieldType::Decimal),
11491            Self::Binary => quote!(#root::core::FieldType::Binary),
11492            Self::ArrayText => {
11493                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Text))
11494            }
11495            Self::ArrayInt => {
11496                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::Int))
11497            }
11498            Self::ArrayBigInt => {
11499                quote!(#root::core::FieldType::Array(#root::core::ArrayElem::BigInt))
11500            }
11501            Self::RangeInt => {
11502                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Int))
11503            }
11504            Self::RangeBigInt => {
11505                quote!(#root::core::FieldType::Range(#root::core::RangeElem::BigInt))
11506            }
11507            Self::RangeNumeric => {
11508                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Numeric))
11509            }
11510            Self::RangeDate => {
11511                quote!(#root::core::FieldType::Range(#root::core::RangeElem::Date))
11512            }
11513            Self::RangeDateTime => {
11514                quote!(#root::core::FieldType::Range(#root::core::RangeElem::DateTime))
11515            }
11516            Self::HStore => quote!(#root::core::FieldType::HStore),
11517            // Dimension comes from the `vector(dims = N)` attribute,
11518            // applied at the emission site; `0` here is just a fallback.
11519            Self::Vector => quote!(#root::core::FieldType::Vector(0)),
11520            // SRID comes from the `geometry(srid = N)` attribute, applied
11521            // at the emission site; `0` here is just a fallback.
11522            Self::Geometry => quote!(#root::core::FieldType::Geometry(0)),
11523        }
11524    }
11525
11526    fn is_integer(self) -> bool {
11527        matches!(self, Self::I16 | Self::I32 | Self::I64)
11528    }
11529
11530    /// `(SqlValue::<Variant>, default expr)` for emitting the
11531    /// `match SqlValue { … }` arm in `LoadRelated::__rustango_load_related`
11532    /// for a `ForeignKey<T, K>` FK whose K maps to `self`. The default
11533    /// fires only when the parent's `__rustango_pk_value` returns a
11534    /// different variant than expected, which is a compile-time bug —
11535    /// but we still need a value-typed fallback to keep the match
11536    /// total.
11537    fn sqlvalue_match_arm(self) -> (TokenStream2, TokenStream2) {
11538        let root = rustango_root();
11539        match self {
11540            Self::I16 => (quote!(I16), quote!(0i16)),
11541            Self::I32 => (quote!(I32), quote!(0i32)),
11542            Self::I64 => (quote!(I64), quote!(0i64)),
11543            Self::F32 => (quote!(F32), quote!(0f32)),
11544            Self::F64 => (quote!(F64), quote!(0f64)),
11545            Self::Bool => (quote!(Bool), quote!(false)),
11546            Self::String => (quote!(String), quote!(::std::string::String::new())),
11547            Self::DateTime => (
11548                quote!(DateTime),
11549                quote!(<#root::__chrono::DateTime<#root::__chrono::Utc> as ::std::default::Default>::default()),
11550            ),
11551            Self::Date => (
11552                quote!(Date),
11553                quote!(<#root::__chrono::NaiveDate as ::std::default::Default>::default()),
11554            ),
11555            Self::Time => (
11556                quote!(Time),
11557                quote!(<#root::__chrono::NaiveTime as ::std::default::Default>::default()),
11558            ),
11559            Self::Uuid => (quote!(Uuid), quote!(#root::__uuid::Uuid::nil())),
11560            Self::Json => (quote!(Json), quote!(#root::__serde_json::Value::Null)),
11561            Self::Decimal => (
11562                quote!(Decimal),
11563                quote!(<#root::__rust_decimal::Decimal as ::std::default::Default>::default()),
11564            ),
11565            Self::Binary => (quote!(Binary), quote!(::std::vec::Vec::<u8>::new())),
11566            // Arrays (#341) can never be a foreign-key primary key, so
11567            // this arm is never reached at runtime — but the match must
11568            // stay total. A bare empty `SqlValue::Array` is the dummy.
11569            Self::ArrayText | Self::ArrayInt | Self::ArrayBigInt => {
11570                (quote!(Array), quote!(::std::vec::Vec::new()))
11571            }
11572            // Ranges (#343) likewise can't be a FK PK — never reached.
11573            Self::RangeInt
11574            | Self::RangeBigInt
11575            | Self::RangeNumeric
11576            | Self::RangeDate
11577            | Self::RangeDateTime => (quote!(RangeLiteral), quote!(::std::string::String::new())),
11578            // HStore (#342) can't be a FK PK — never reached.
11579            Self::HStore => (quote!(HStore), quote!(::std::vec::Vec::new())),
11580            // Vector (#824) can't be a FK PK — never reached.
11581            Self::Vector => (quote!(Vector), quote!(::std::vec::Vec::new())),
11582            // Geometry (#443) can't be a FK PK — never reached (arm is
11583            // exhaustiveness-only; never interpolated into emitted code).
11584            Self::Geometry => (quote!(Geometry), quote!(::std::vec::Vec::new())),
11585        }
11586    }
11587}
11588
11589/// Result of walking a field's Rust type. `kind` is the underlying
11590/// `FieldType`; `nullable` is set by an outer `Option<T>`; `auto` is
11591/// set by an outer `Auto<T>` (server-assigned PK); `fk_inner` is
11592/// `Some(<T>)` when the field was `ForeignKey<T>` (or
11593/// `Option<ForeignKey<T>>`), letting the codegen reach `T::SCHEMA`.
11594#[derive(Clone, Copy)]
11595struct DetectedType<'a> {
11596    kind: DetectedKind,
11597    nullable: bool,
11598    auto: bool,
11599    fk_inner: Option<&'a syn::Type>,
11600}
11601
11602/// Extract the `T` from a `…::Auto<T>` field type. Returns `None` for
11603/// non-`Auto` types — the caller should already have routed Auto-only
11604/// codegen through this helper, so a `None` indicates a macro-internal
11605/// invariant break.
11606fn auto_inner_type(ty: &syn::Type) -> Option<&syn::Type> {
11607    let Type::Path(TypePath { path, qself: None }) = ty else {
11608        return None;
11609    };
11610    let last = path.segments.last()?;
11611    if last.ident != "Auto" {
11612        return None;
11613    }
11614    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
11615        return None;
11616    };
11617    args.args.iter().find_map(|a| match a {
11618        syn::GenericArgument::Type(t) => Some(t),
11619        _ => None,
11620    })
11621}
11622
11623fn detect_type(ty: &syn::Type) -> syn::Result<DetectedType<'_>> {
11624    let Type::Path(TypePath { path, qself: None }) = ty else {
11625        return Err(syn::Error::new_spanned(ty, "unsupported field type"));
11626    };
11627    let last = path
11628        .segments
11629        .last()
11630        .ok_or_else(|| syn::Error::new_spanned(ty, "empty type path"))?;
11631
11632    if last.ident == "Option" {
11633        let inner = generic_inner(ty, &last.arguments, "Option")?;
11634        let inner_det = detect_type(inner)?;
11635        if inner_det.nullable {
11636            return Err(syn::Error::new_spanned(
11637                ty,
11638                "nested Option is not supported",
11639            ));
11640        }
11641        if inner_det.auto {
11642            return Err(syn::Error::new_spanned(
11643                ty,
11644                "`Option<Auto<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11645            ));
11646        }
11647        return Ok(DetectedType {
11648            nullable: true,
11649            ..inner_det
11650        });
11651    }
11652
11653    if last.ident == "Auto" {
11654        let inner = generic_inner(ty, &last.arguments, "Auto")?;
11655        let inner_det = detect_type(inner)?;
11656        if inner_det.auto {
11657            return Err(syn::Error::new_spanned(ty, "nested Auto is not supported"));
11658        }
11659        if inner_det.nullable {
11660            return Err(syn::Error::new_spanned(
11661                ty,
11662                "`Auto<Option<T>>` is not supported — Auto fields are server-assigned and cannot be NULL",
11663            ));
11664        }
11665        if inner_det.fk_inner.is_some() {
11666            return Err(syn::Error::new_spanned(
11667                ty,
11668                "`Auto<ForeignKey<T>>` is not supported — Auto is for server-assigned PKs, ForeignKey is for parent references",
11669            ));
11670        }
11671        if !matches!(
11672            inner_det.kind,
11673            DetectedKind::I32 | DetectedKind::I64 | DetectedKind::Uuid | DetectedKind::DateTime
11674        ) {
11675            return Err(syn::Error::new_spanned(
11676                ty,
11677                "`Auto<T>` only supports integers (`i32` → SERIAL, `i64` → BIGSERIAL), \
11678                 `uuid::Uuid` (DEFAULT gen_random_uuid()), or `chrono::DateTime<chrono::Utc>` \
11679                 (DEFAULT now())",
11680            ));
11681        }
11682        return Ok(DetectedType {
11683            auto: true,
11684            ..inner_det
11685        });
11686    }
11687
11688    if last.ident == "ForeignKey" {
11689        let (inner, key_ty) = generic_pair(ty, &last.arguments, "ForeignKey")?;
11690        // Resolve the FK column's underlying SQL type from `K`. When the
11691        // user wrote `ForeignKey<T>` without a key parameter, the type
11692        // alias defaults to `i64` and we keep the v0.7 BIGINT shape.
11693        // When the user wrote `ForeignKey<T, K>` with an explicit `K`,
11694        // recurse into K so the column DDL emits the right SQL type
11695        // (VARCHAR for String, UUID for Uuid, …) and the load_related
11696        // emitter knows which `SqlValue` variant to match.
11697        let kind = match key_ty {
11698            Some(k) => detect_type(k)?.kind,
11699            None => DetectedKind::I64,
11700        };
11701        return Ok(DetectedType {
11702            kind,
11703            nullable: false,
11704            auto: false,
11705            fk_inner: Some(inner),
11706        });
11707    }
11708
11709    let kind = match last.ident.to_string().as_str() {
11710        "i16" => DetectedKind::I16,
11711        "i32" => DetectedKind::I32,
11712        "i64" => DetectedKind::I64,
11713        "f32" => DetectedKind::F32,
11714        "f64" => DetectedKind::F64,
11715        "bool" => DetectedKind::Bool,
11716        "String" => DetectedKind::String,
11717        "DateTime" => DetectedKind::DateTime,
11718        "NaiveDate" => DetectedKind::Date,
11719        "NaiveTime" => DetectedKind::Time,
11720        "Uuid" => DetectedKind::Uuid,
11721        "Value" => DetectedKind::Json,
11722        "Decimal" => DetectedKind::Decimal,
11723        // `Vec<u8>` → BYTEA / LONGBLOB / BLOB. Reject any other
11724        // `Vec<T>` so we don't silently accept e.g. `Vec<String>`
11725        // — that would emit Binary DDL and decode-fail at runtime.
11726        "Vec" => {
11727            let (inner, _) = generic_pair(ty, &last.arguments, "Vec")?;
11728            if let Type::Path(TypePath { path, qself: None }) = inner {
11729                if let Some(seg) = path.segments.last() {
11730                    if seg.ident == "u8" && seg.arguments.is_empty() {
11731                        return Ok(DetectedType {
11732                            kind: DetectedKind::Binary,
11733                            nullable: false,
11734                            auto: false,
11735                            fk_inner: None,
11736                        });
11737                    }
11738                }
11739            }
11740            return Err(syn::Error::new_spanned(
11741                ty,
11742                "unsupported `Vec<T>` field — only `Vec<u8>` (→ Binary) is supported; \
11743                 for a PostgreSQL array column use `Array<String>` / `Array<i32>` / `Array<i64>`",
11744            ));
11745        }
11746        // `Array<String>` / `Array<i32>` / `Array<i64>` → PG `text[]` /
11747        // `integer[]` / `bigint[]` (Django `ArrayField`, #341).
11748        "Array" => {
11749            let (inner, _) = generic_pair(ty, &last.arguments, "Array")?;
11750            let elem = match inner {
11751                Type::Path(TypePath { path, qself: None }) => {
11752                    path.segments.last().map(|s| s.ident.to_string())
11753                }
11754                _ => None,
11755            };
11756            let kind = match elem.as_deref() {
11757                Some("String") => DetectedKind::ArrayText,
11758                Some("i32") => DetectedKind::ArrayInt,
11759                Some("i64") => DetectedKind::ArrayBigInt,
11760                _ => {
11761                    return Err(syn::Error::new_spanned(
11762                        ty,
11763                        "unsupported `Array<T>` element — only `Array<String>` (→ text[]), \
11764                         `Array<i32>` (→ integer[]), and `Array<i64>` (→ bigint[]) are supported (#341)",
11765                    ));
11766                }
11767            };
11768            return Ok(DetectedType {
11769                kind,
11770                nullable: false,
11771                auto: false,
11772                fk_inner: None,
11773            });
11774        }
11775        // `Range<i32>` / `Range<i64>` / `Range<Decimal>` /
11776        // `Range<NaiveDate>` / `Range<DateTime<…>>` → PG `int4range` /
11777        // `int8range` / `numrange` / `daterange` / `tstzrange` (Django
11778        // `RangeField` family, #343).
11779        "Range" => {
11780            let (inner, _) = generic_pair(ty, &last.arguments, "Range")?;
11781            let elem = match inner {
11782                Type::Path(TypePath { path, qself: None }) => {
11783                    path.segments.last().map(|s| s.ident.to_string())
11784                }
11785                _ => None,
11786            };
11787            let kind = match elem.as_deref() {
11788                Some("i32") => DetectedKind::RangeInt,
11789                Some("i64") => DetectedKind::RangeBigInt,
11790                Some("Decimal") => DetectedKind::RangeNumeric,
11791                Some("NaiveDate") => DetectedKind::RangeDate,
11792                Some("DateTime") => DetectedKind::RangeDateTime,
11793                _ => {
11794                    return Err(syn::Error::new_spanned(
11795                        ty,
11796                        "unsupported `Range<T>` element — only `Range<i32>` (→ int4range), \
11797                         `Range<i64>` (→ int8range), `Range<Decimal>` (→ numrange), \
11798                         `Range<NaiveDate>` (→ daterange), and `Range<DateTime<Utc>>` \
11799                         (→ tstzrange) are supported (#343)",
11800                    ));
11801                }
11802            };
11803            return Ok(DetectedType {
11804                kind,
11805                nullable: false,
11806                auto: false,
11807                fk_inner: None,
11808            });
11809        }
11810        // `Cast<C>` → attribute cast (#819). The column is plain `TEXT`
11811        // (the `CastValue` impl bridges logical↔stored); the field's own
11812        // sqlx `Decode` / `Into<SqlValue>` handle the transform, so the
11813        // schema just needs `FieldType::String`.
11814        "Cast" => {
11815            return Ok(DetectedType {
11816                kind: DetectedKind::String,
11817                nullable: false,
11818                auto: false,
11819                fk_inner: None,
11820            });
11821        }
11822        // `HStore` → PG `hstore` (Django `HStoreField`, #342). No generic
11823        // parameter — always a string→string map.
11824        "HStore" => {
11825            return Ok(DetectedType {
11826                kind: DetectedKind::HStore,
11827                nullable: false,
11828                auto: false,
11829                fk_inner: None,
11830            });
11831        }
11832        // `Vector` → pgvector `vector(N)` (#824). The dimension is
11833        // supplied by `#[rustango(vector(dims = N))]`, not the type.
11834        "Vector" => {
11835            return Ok(DetectedType {
11836                kind: DetectedKind::Vector,
11837                nullable: false,
11838                auto: false,
11839                fk_inner: None,
11840            });
11841        }
11842        // `Point` → PostGIS `geometry(Point, srid)` (#443). The SRID is
11843        // supplied by `#[rustango(geometry(srid = N))]`, not the type.
11844        "Point" => {
11845            return Ok(DetectedType {
11846                kind: DetectedKind::Geometry,
11847                nullable: false,
11848                auto: false,
11849                fk_inner: None,
11850            });
11851        }
11852        other => {
11853            return Err(syn::Error::new_spanned(
11854                ty,
11855                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)"),
11856            ));
11857        }
11858    };
11859    Ok(DetectedType {
11860        kind,
11861        nullable: false,
11862        auto: false,
11863        fk_inner: None,
11864    })
11865}
11866
11867fn generic_inner<'a>(
11868    ty: &'a Type,
11869    arguments: &'a PathArguments,
11870    wrapper: &str,
11871) -> syn::Result<&'a Type> {
11872    let PathArguments::AngleBracketed(args) = arguments else {
11873        return Err(syn::Error::new_spanned(
11874            ty,
11875            format!("{wrapper} requires a generic argument"),
11876        ));
11877    };
11878    args.args
11879        .iter()
11880        .find_map(|a| match a {
11881            GenericArgument::Type(t) => Some(t),
11882            _ => None,
11883        })
11884        .ok_or_else(|| {
11885            syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11886        })
11887}
11888
11889/// Like [`generic_inner`] but pulls *two* type args — the first is
11890/// required, the second is optional. Used by the `ForeignKey<T, K>`
11891/// detection where K defaults to `i64` when omitted.
11892fn generic_pair<'a>(
11893    ty: &'a Type,
11894    arguments: &'a PathArguments,
11895    wrapper: &str,
11896) -> syn::Result<(&'a Type, Option<&'a Type>)> {
11897    let PathArguments::AngleBracketed(args) = arguments else {
11898        return Err(syn::Error::new_spanned(
11899            ty,
11900            format!("{wrapper} requires a generic argument"),
11901        ));
11902    };
11903    let mut types = args.args.iter().filter_map(|a| match a {
11904        GenericArgument::Type(t) => Some(t),
11905        _ => None,
11906    });
11907    let first = types.next().ok_or_else(|| {
11908        syn::Error::new_spanned(ty, format!("{wrapper}<T> requires a type argument"))
11909    })?;
11910    let second = types.next();
11911    Ok((first, second))
11912}
11913
11914fn to_snake_case(s: &str) -> String {
11915    let mut out = String::with_capacity(s.len() + 4);
11916    for (i, ch) in s.chars().enumerate() {
11917        if ch.is_ascii_uppercase() {
11918            if i > 0 {
11919                out.push('_');
11920            }
11921            out.push(ch.to_ascii_lowercase());
11922        } else {
11923            out.push(ch);
11924        }
11925    }
11926    out
11927}
11928
11929// ============================================================
11930//  #[derive(Form)]  —  slice 8.4B
11931// ============================================================
11932
11933/// Per-field `#[form(...)]` attributes recognised by the derive.
11934#[derive(Default)]
11935struct FormFieldAttrs {
11936    min: Option<i64>,
11937    max: Option<i64>,
11938    min_length: Option<u32>,
11939    max_length: Option<u32>,
11940    /// `#[form(clean = "fn_name")]` — Django-shape `clean_<field>` hook.
11941    /// The named static method on the form struct is called after the
11942    /// field's typed parse + length/range checks; it gets the parsed
11943    /// value by reference and returns `Result<<FieldType>, String>`.
11944    /// On Ok, the returned value replaces the parsed one; on Err, the
11945    /// message is attached to the field error list. Issue #372.
11946    clean: Option<syn::Ident>,
11947}
11948
11949/// Container-level `#[form(...)]` attributes. Currently only the
11950/// Django-shape cross-field `validate` hook (issue #373).
11951#[derive(Default)]
11952struct FormContainerAttrs {
11953    /// `#[form(validate = "fn_name")]` — Django-shape `clean()` hook.
11954    /// After every per-field parse succeeds, the named method on the
11955    /// form struct is called with `&self` and may return
11956    /// `Result<(), FormErrors>`. Errors merge into the field error
11957    /// list. Issue #373.
11958    validate: Option<syn::Ident>,
11959}
11960
11961/// Detected shape of a form field's Rust type.
11962#[derive(Clone, Copy)]
11963enum FormFieldKind {
11964    String,
11965    I16,
11966    I32,
11967    I64,
11968    F32,
11969    F64,
11970    Bool,
11971}
11972
11973impl FormFieldKind {
11974    fn parse_method(self) -> &'static str {
11975        match self {
11976            Self::I16 => "i16",
11977            Self::I32 => "i32",
11978            Self::I64 => "i64",
11979            Self::F32 => "f32",
11980            Self::F64 => "f64",
11981            // String + Bool don't go through `str::parse`; the codegen
11982            // handles them inline.
11983            Self::String | Self::Bool => "",
11984        }
11985    }
11986}
11987
11988fn expand_form(input: &DeriveInput) -> syn::Result<TokenStream2> {
11989    let root = rustango_root();
11990    let struct_name = &input.ident;
11991
11992    let Data::Struct(data) = &input.data else {
11993        return Err(syn::Error::new_spanned(
11994            struct_name,
11995            "Form can only be derived on structs",
11996        ));
11997    };
11998    let Fields::Named(named) = &data.fields else {
11999        return Err(syn::Error::new_spanned(
12000            struct_name,
12001            "Form requires a struct with named fields",
12002        ));
12003    };
12004
12005    // #373 — container-level `#[form(validate = "fn")]` hook.
12006    let container = parse_form_container_attrs(input)?;
12007    let post_field_clean: Vec<TokenStream2> = Vec::new();
12008    let _ = post_field_clean;
12009
12010    let mut field_blocks: Vec<TokenStream2> = Vec::with_capacity(named.named.len());
12011    let mut field_idents: Vec<&syn::Ident> = Vec::with_capacity(named.named.len());
12012
12013    for field in &named.named {
12014        let ident = field
12015            .ident
12016            .as_ref()
12017            .ok_or_else(|| syn::Error::new(field.span(), "tuple structs are not supported"))?;
12018        let attrs = parse_form_field_attrs(field)?;
12019        let (kind, nullable) = detect_form_field(&field.ty, field.span())?;
12020
12021        let name_lit = ident.to_string();
12022        let parse_block = render_form_field_parse(ident, &name_lit, kind, nullable, &attrs);
12023        // #372 — append the per-field `clean_<field>` call right after
12024        // the parse block when the attribute is set. The clean fn
12025        // takes &T and returns Result<T, String>; on Err we attach
12026        // the message to the field error list without aborting
12027        // (matches Django's "collect all field errors" shape).
12028        let clean_block = if let Some(clean_fn) = &attrs.clean {
12029            quote! {
12030                if __errors.fields().get(#name_lit).is_none() {
12031                    match Self::#clean_fn(&#ident) {
12032                        ::core::result::Result::Ok(__cleaned) => { #ident = __cleaned; }
12033                        ::core::result::Result::Err(__msg) => {
12034                            __errors.add(#name_lit, __msg);
12035                        }
12036                    }
12037                }
12038            }
12039        } else {
12040            quote! {}
12041        };
12042        field_blocks.push(quote! {
12043            #parse_block
12044            #clean_block
12045        });
12046        field_idents.push(ident);
12047    }
12048
12049    // #373 — after every per-field parse + clean succeeds, call the
12050    // cross-field validator if declared. Errors merge into the
12051    // outgoing FormErrors via the existing `FormErrors::merge` helper
12052    // (same primitive the DRF serializer cross-field hook uses).
12053    let cross_field_call = if let Some(validate_fn) = &container.validate {
12054        quote! {
12055            if __errors.is_empty() {
12056                let __candidate = Self { #( #field_idents ),* };
12057                if let ::core::result::Result::Err(__other) = Self::#validate_fn(&__candidate) {
12058                    __errors.merge(__other);
12059                }
12060                if !__errors.is_empty() {
12061                    return ::core::result::Result::Err(__errors);
12062                }
12063                return ::core::result::Result::Ok(__candidate);
12064            }
12065        }
12066    } else {
12067        quote! {}
12068    };
12069
12070    Ok(quote! {
12071        impl #root::forms::Form for #struct_name {
12072            fn parse(
12073                data: &::std::collections::HashMap<::std::string::String, ::std::string::String>,
12074            ) -> ::core::result::Result<Self, #root::forms::FormErrors> {
12075                let mut __errors = #root::forms::FormErrors::default();
12076                #( #field_blocks )*
12077                #cross_field_call
12078                if !__errors.is_empty() {
12079                    return ::core::result::Result::Err(__errors);
12080                }
12081                ::core::result::Result::Ok(Self {
12082                    #( #field_idents ),*
12083                })
12084            }
12085        }
12086    })
12087}
12088
12089fn parse_form_container_attrs(input: &DeriveInput) -> syn::Result<FormContainerAttrs> {
12090    let mut out = FormContainerAttrs::default();
12091    for attr in &input.attrs {
12092        if !attr.path().is_ident("form") {
12093            continue;
12094        }
12095        attr.parse_nested_meta(|meta| {
12096            if meta.path.is_ident("validate") {
12097                let s: LitStr = meta.value()?.parse()?;
12098                out.validate = Some(syn::Ident::new(&s.value(), s.span()));
12099                return Ok(());
12100            }
12101            Err(meta.error("unknown form container attribute (supported: `validate`)"))
12102        })?;
12103    }
12104    Ok(out)
12105}
12106
12107fn parse_form_field_attrs(field: &syn::Field) -> syn::Result<FormFieldAttrs> {
12108    let mut out = FormFieldAttrs::default();
12109    for attr in &field.attrs {
12110        if !attr.path().is_ident("form") {
12111            continue;
12112        }
12113        attr.parse_nested_meta(|meta| {
12114            if meta.path.is_ident("min") {
12115                let lit: syn::LitInt = meta.value()?.parse()?;
12116                out.min = Some(lit.base10_parse::<i64>()?);
12117                return Ok(());
12118            }
12119            if meta.path.is_ident("max") {
12120                let lit: syn::LitInt = meta.value()?.parse()?;
12121                out.max = Some(lit.base10_parse::<i64>()?);
12122                return Ok(());
12123            }
12124            if meta.path.is_ident("min_length") {
12125                let lit: syn::LitInt = meta.value()?.parse()?;
12126                out.min_length = Some(lit.base10_parse::<u32>()?);
12127                return Ok(());
12128            }
12129            if meta.path.is_ident("max_length") {
12130                let lit: syn::LitInt = meta.value()?.parse()?;
12131                out.max_length = Some(lit.base10_parse::<u32>()?);
12132                return Ok(());
12133            }
12134            if meta.path.is_ident("clean") {
12135                let s: LitStr = meta.value()?.parse()?;
12136                out.clean = Some(syn::Ident::new(&s.value(), s.span()));
12137                return Ok(());
12138            }
12139            Err(meta.error(
12140                "unknown form field attribute (supported: `min`, `max`, `min_length`, `max_length`, `clean`)",
12141            ))
12142        })?;
12143    }
12144    Ok(out)
12145}
12146
12147fn detect_form_field(ty: &Type, span: proc_macro2::Span) -> syn::Result<(FormFieldKind, bool)> {
12148    let Type::Path(TypePath { path, qself: None }) = ty else {
12149        return Err(syn::Error::new(
12150            span,
12151            "Form field must be a simple typed path (e.g. `String`, `i32`, `Option<String>`)",
12152        ));
12153    };
12154    let last = path
12155        .segments
12156        .last()
12157        .ok_or_else(|| syn::Error::new(span, "empty type path"))?;
12158
12159    if last.ident == "Option" {
12160        let inner = generic_inner(ty, &last.arguments, "Option")?;
12161        let (kind, nested) = detect_form_field(inner, span)?;
12162        if nested {
12163            return Err(syn::Error::new(
12164                span,
12165                "nested Option in Form fields is not supported",
12166            ));
12167        }
12168        return Ok((kind, true));
12169    }
12170
12171    let kind = match last.ident.to_string().as_str() {
12172        "String" => FormFieldKind::String,
12173        "i16" => FormFieldKind::I16,
12174        "i32" => FormFieldKind::I32,
12175        "i64" => FormFieldKind::I64,
12176        "f32" => FormFieldKind::F32,
12177        "f64" => FormFieldKind::F64,
12178        "bool" => FormFieldKind::Bool,
12179        other => {
12180            return Err(syn::Error::new(
12181                span,
12182                format!(
12183                    "Form field type `{other}` is not supported in v0.8 — use String / \
12184                     i16 / i32 / i64 / f32 / f64 / bool, optionally wrapped in Option<…>"
12185                ),
12186            ));
12187        }
12188    };
12189    Ok((kind, false))
12190}
12191
12192#[allow(clippy::too_many_lines)]
12193fn render_form_field_parse(
12194    ident: &syn::Ident,
12195    name_lit: &str,
12196    kind: FormFieldKind,
12197    nullable: bool,
12198    attrs: &FormFieldAttrs,
12199) -> TokenStream2 {
12200    // Pull the raw &str from the payload. Uses variable name `data` to
12201    // match the new `Form::parse(data: &HashMap<…>)` signature.
12202    let lookup = quote! {
12203        let __raw: ::core::option::Option<&::std::string::String> = data.get(#name_lit);
12204    };
12205
12206    let parsed_value = match kind {
12207        FormFieldKind::Bool => quote! {
12208            let __v: bool = match __raw {
12209                ::core::option::Option::None => false,
12210                ::core::option::Option::Some(__s) => !matches!(
12211                    __s.to_ascii_lowercase().as_str(),
12212                    "" | "false" | "0" | "off" | "no"
12213                ),
12214            };
12215        },
12216        FormFieldKind::String => {
12217            if nullable {
12218                quote! {
12219                    let __v: ::core::option::Option<::std::string::String> = match __raw {
12220                        ::core::option::Option::None => ::core::option::Option::None,
12221                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12222                            ::core::option::Option::None
12223                        }
12224                        ::core::option::Option::Some(__s) => {
12225                            ::core::option::Option::Some(::core::clone::Clone::clone(__s))
12226                        }
12227                    };
12228                }
12229            } else {
12230                quote! {
12231                    let __v: ::std::string::String = match __raw {
12232                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12233                            ::core::clone::Clone::clone(__s)
12234                        }
12235                        _ => {
12236                            __errors.add(#name_lit, "This field is required.");
12237                            ::std::string::String::new()
12238                        }
12239                    };
12240                }
12241            }
12242        }
12243        FormFieldKind::I16
12244        | FormFieldKind::I32
12245        | FormFieldKind::I64
12246        | FormFieldKind::F32
12247        | FormFieldKind::F64 => {
12248            let parse_ty = syn::Ident::new(kind.parse_method(), proc_macro2::Span::call_site());
12249            let ty_lit = kind.parse_method();
12250            let default_val = match kind {
12251                FormFieldKind::I16 => quote! { 0i16 },
12252                FormFieldKind::I32 => quote! { 0i32 },
12253                FormFieldKind::I64 => quote! { 0i64 },
12254                FormFieldKind::F32 => quote! { 0f32 },
12255                FormFieldKind::F64 => quote! { 0f64 },
12256                _ => quote! { Default::default() },
12257            };
12258            if nullable {
12259                quote! {
12260                    let __v: ::core::option::Option<#parse_ty> = match __raw {
12261                        ::core::option::Option::None => ::core::option::Option::None,
12262                        ::core::option::Option::Some(__s) if __s.is_empty() => {
12263                            ::core::option::Option::None
12264                        }
12265                        ::core::option::Option::Some(__s) => {
12266                            match __s.parse::<#parse_ty>() {
12267                                ::core::result::Result::Ok(__n) => {
12268                                    ::core::option::Option::Some(__n)
12269                                }
12270                                ::core::result::Result::Err(__e) => {
12271                                    __errors.add(
12272                                        #name_lit,
12273                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12274                                    );
12275                                    ::core::option::Option::None
12276                                }
12277                            }
12278                        }
12279                    };
12280                }
12281            } else {
12282                quote! {
12283                    let __v: #parse_ty = match __raw {
12284                        ::core::option::Option::Some(__s) if !__s.is_empty() => {
12285                            match __s.parse::<#parse_ty>() {
12286                                ::core::result::Result::Ok(__n) => __n,
12287                                ::core::result::Result::Err(__e) => {
12288                                    __errors.add(
12289                                        #name_lit,
12290                                        ::std::format!("Enter a valid {} value: {}", #ty_lit, __e),
12291                                    );
12292                                    #default_val
12293                                }
12294                            }
12295                        }
12296                        _ => {
12297                            __errors.add(#name_lit, "This field is required.");
12298                            #default_val
12299                        }
12300                    };
12301                }
12302            }
12303        }
12304    };
12305
12306    let validators = render_form_validators(name_lit, kind, nullable, attrs);
12307
12308    quote! {
12309        // `mut` so the per-field `clean` hook (#372) can rewrite the
12310        // parsed value in-place when it returns Ok with a normalized
12311        // form (e.g. trim / lowercase).
12312        let mut #ident = {
12313            #lookup
12314            #parsed_value
12315            #validators
12316            __v
12317        };
12318    }
12319}
12320
12321fn render_form_validators(
12322    name_lit: &str,
12323    kind: FormFieldKind,
12324    nullable: bool,
12325    attrs: &FormFieldAttrs,
12326) -> TokenStream2 {
12327    let mut checks: Vec<TokenStream2> = Vec::new();
12328
12329    let val_ref = if nullable {
12330        quote! { __v.as_ref() }
12331    } else {
12332        quote! { ::core::option::Option::Some(&__v) }
12333    };
12334
12335    let is_string = matches!(kind, FormFieldKind::String);
12336    let is_numeric = matches!(
12337        kind,
12338        FormFieldKind::I16
12339            | FormFieldKind::I32
12340            | FormFieldKind::I64
12341            | FormFieldKind::F32
12342            | FormFieldKind::F64
12343    );
12344
12345    if is_string {
12346        if let Some(min_len) = attrs.min_length {
12347            let min_len_usize = min_len as usize;
12348            checks.push(quote! {
12349                if let ::core::option::Option::Some(__s) = #val_ref {
12350                    if __s.len() < #min_len_usize {
12351                        __errors.add(
12352                            #name_lit,
12353                            ::std::format!("Ensure this value has at least {} characters.", #min_len_usize),
12354                        );
12355                    }
12356                }
12357            });
12358        }
12359        if let Some(max_len) = attrs.max_length {
12360            let max_len_usize = max_len as usize;
12361            checks.push(quote! {
12362                if let ::core::option::Option::Some(__s) = #val_ref {
12363                    if __s.len() > #max_len_usize {
12364                        __errors.add(
12365                            #name_lit,
12366                            ::std::format!("Ensure this value has at most {} characters.", #max_len_usize),
12367                        );
12368                    }
12369                }
12370            });
12371        }
12372    }
12373
12374    if is_numeric {
12375        if let Some(min) = attrs.min {
12376            checks.push(quote! {
12377                if let ::core::option::Option::Some(__n) = #val_ref {
12378                    if (*__n as f64) < (#min as f64) {
12379                        __errors.add(
12380                            #name_lit,
12381                            ::std::format!("Ensure this value is greater than or equal to {}.", #min),
12382                        );
12383                    }
12384                }
12385            });
12386        }
12387        if let Some(max) = attrs.max {
12388            checks.push(quote! {
12389                if let ::core::option::Option::Some(__n) = #val_ref {
12390                    if (*__n as f64) > (#max as f64) {
12391                        __errors.add(
12392                            #name_lit,
12393                            ::std::format!("Ensure this value is less than or equal to {}.", #max),
12394                        );
12395                    }
12396                }
12397            });
12398        }
12399    }
12400
12401    quote! { #( #checks )* }
12402}
12403
12404// ============================================================
12405//  #[derive(ViewSet)]
12406// ============================================================
12407
12408struct ViewSetAttrs {
12409    model: syn::Path,
12410    fields: Option<Vec<String>>,
12411    filter_fields: Vec<String>,
12412    search_fields: Vec<String>,
12413    /// (field_name, desc)
12414    ordering: Vec<(String, bool)>,
12415    page_size: Option<usize>,
12416    read_only: bool,
12417    perms: ViewSetPermsAttrs,
12418    /// `#[viewset(serializer = SomeSerializer)]` — render list /
12419    /// retrieve / create responses through this `#[derive(Serializer)]`
12420    /// type instead of the default field-level projection (requires the
12421    /// `serializer` feature). Tri-dialect.
12422    serializer: Option<syn::Path>,
12423}
12424
12425#[derive(Default)]
12426struct ViewSetPermsAttrs {
12427    list: Vec<String>,
12428    retrieve: Vec<String>,
12429    create: Vec<String>,
12430    update: Vec<String>,
12431    destroy: Vec<String>,
12432}
12433
12434fn expand_viewset(input: &DeriveInput) -> syn::Result<TokenStream2> {
12435    let root = rustango_root();
12436    let struct_name = &input.ident;
12437
12438    // Must be a unit struct or an empty named struct.
12439    match &input.data {
12440        Data::Struct(s) => match &s.fields {
12441            Fields::Unit | Fields::Named(_) => {}
12442            Fields::Unnamed(_) => {
12443                return Err(syn::Error::new_spanned(
12444                    struct_name,
12445                    "ViewSet can only be derived on a unit struct or an empty named struct",
12446                ));
12447            }
12448        },
12449        _ => {
12450            return Err(syn::Error::new_spanned(
12451                struct_name,
12452                "ViewSet can only be derived on a struct",
12453            ));
12454        }
12455    }
12456
12457    let attrs = parse_viewset_attrs(input)?;
12458    let model_path = &attrs.model;
12459
12460    // `.fields(&[...])` call — None means skip (use all scalar fields).
12461    let fields_call = if let Some(ref fields) = attrs.fields {
12462        let lits = fields.iter().map(|f| f.as_str());
12463        quote!(.fields(&[ #(#lits),* ]))
12464    } else {
12465        quote!()
12466    };
12467
12468    let filter_fields_call = if attrs.filter_fields.is_empty() {
12469        quote!()
12470    } else {
12471        let lits = attrs.filter_fields.iter().map(|f| f.as_str());
12472        quote!(.filter_fields(&[ #(#lits),* ]))
12473    };
12474
12475    let search_fields_call = if attrs.search_fields.is_empty() {
12476        quote!()
12477    } else {
12478        let lits = attrs.search_fields.iter().map(|f| f.as_str());
12479        quote!(.search_fields(&[ #(#lits),* ]))
12480    };
12481
12482    let ordering_call = if attrs.ordering.is_empty() {
12483        quote!()
12484    } else {
12485        let pairs = attrs.ordering.iter().map(|(f, desc)| {
12486            let f = f.as_str();
12487            quote!((#f, #desc))
12488        });
12489        quote!(.ordering(&[ #(#pairs),* ]))
12490    };
12491
12492    let page_size_call = if let Some(n) = attrs.page_size {
12493        quote!(.page_size(#n))
12494    } else {
12495        quote!()
12496    };
12497
12498    let read_only_call = if attrs.read_only {
12499        quote!(.read_only())
12500    } else {
12501        quote!()
12502    };
12503
12504    // `.serializer::<S>()` — reshape responses through a derived
12505    // serializer. Requires the downstream crate to enable the
12506    // `serializer` feature (the method is gated on it).
12507    let serializer_call = if let Some(ref ser) = attrs.serializer {
12508        quote!(.serializer::<#ser>())
12509    } else {
12510        quote!()
12511    };
12512
12513    let perms = &attrs.perms;
12514    let perms_call = if perms.list.is_empty()
12515        && perms.retrieve.is_empty()
12516        && perms.create.is_empty()
12517        && perms.update.is_empty()
12518        && perms.destroy.is_empty()
12519    {
12520        quote!()
12521    } else {
12522        let list_lits = perms.list.iter().map(|s| s.as_str());
12523        let retrieve_lits = perms.retrieve.iter().map(|s| s.as_str());
12524        let create_lits = perms.create.iter().map(|s| s.as_str());
12525        let update_lits = perms.update.iter().map(|s| s.as_str());
12526        let destroy_lits = perms.destroy.iter().map(|s| s.as_str());
12527        quote! {
12528            .permissions(#root::viewset::ViewSetPerms {
12529                list:     ::std::vec![ #(#list_lits.to_owned()),* ],
12530                retrieve: ::std::vec![ #(#retrieve_lits.to_owned()),* ],
12531                create:   ::std::vec![ #(#create_lits.to_owned()),* ],
12532                update:   ::std::vec![ #(#update_lits.to_owned()),* ],
12533                destroy:  ::std::vec![ #(#destroy_lits.to_owned()),* ],
12534            })
12535        }
12536    };
12537
12538    Ok(quote! {
12539        impl #struct_name {
12540            /// Build an `axum::Router` with the six standard REST endpoints
12541            /// for this ViewSet, mounted at `prefix`.
12542            ///
12543            /// Accepts any backend's pool — a `sqlx::PgPool`,
12544            /// `SqlitePool`, `MySqlPool`, or the [`rustango::sql::Pool`]
12545            /// enum — because they all `Into<Pool>` (#1273). This is why
12546            /// the derive no longer names `PgPool`, which would not
12547            /// exist on a project built without the `postgres` feature.
12548            pub fn router(
12549                prefix: &str,
12550                pool: impl ::core::convert::Into<#root::sql::Pool>,
12551            ) -> #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_pool(prefix, pool.into())
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}